在之前的文章里面写了一篇php反序列化入门,后面就一直没看相关漏洞了,这次准备重新学习一下
了解5.1.X的反序列化需要先知道2个魔术方法
官方解释为一个类被当成字符串时应怎样回应
个人理解就是把对象当作字符串处理时触发的函数
1 |
|
输出,拼接一些的操作都可以触发
__toString的返回值最好为string,不然会报fatal,不过不影响运行
官方解释为在对象中调用一个不可访问方法时,__call() 会被调用。
然后看看代码
1 |
|
__call有2个参数,$name和$arguments
$name为调用函数的名称,$arguments为调用函数的参数
1 |
|
函数名为字符串,参数如果1个以上以数组返回
首先需要下载tp5版本,这次使用tp5.1.37
1 | composer create-project --prefer-dist topthink/think tp5137 |
修改\application\index\controller\Index.php
1 |
|
写一个有反序列化方法的路由就可以测试了
下面来看反序列化的链子
在入门的文章里面说过,反序列化出来的类销毁时会触发__destruct方法
在反序列化时会触发__wakeup方法
所以链子的入口可以往这些地方找,当然tp5.1的链子大佬们都已经找好了,跟着学习一下就好了
该链子的入口在think\process\pipes\Windows.php的Windows类的__destruct方法
看一下removeFiles方法
$this->files是一个可控的数组,该函数会先判断文件是否存在,如果存在使用unlink删除
这里就出现了一个任意文件删除的漏洞,可以先写一个简单的poc
1 |
|
打出该poc就可以删除指定的文件了
当然这样是不够的,继续往下看
file_exists函数是可以触发__toString的
所以在这里传一个有__toString方法的类就能继续下去了
下一个点在think\model\concern\Conversion.php
该段代码里有可以利用的__toString方法
可是向上翻会发现最开头是
1 | trait Conversion |
显然这不是一个类,trait感觉有点像一个组件,比如两个类有一些相同的函数,就可以把这些相同的函数都调出来用trait来写,然后在类中只需要use即可
这里需要去找使用了Conversion的类,可以全局搜索Conversion
可以找到Model类使用了Conversion,可是Model是一个抽象类也是不能实例化的,继续找继承该类的函数
找到\thinkphp\library\think\model\Pivot.php的Pivot是继承Model的
找到该类后先回去看__toString里的函数怎么走
直接调用了toJson方法
toJson调用了toArray
继续看toArray
上面有3个foreach,循环的数组还是都是可控的,不过这里用不到,重点是$this->append
先判断该值是否为空,然后循环该数组,键为$key,值为$name
下一个判断需要$name为数组,所以写exp的时候$this->append需要写成这样
1 | $this->append = ["aaa"=>["aaa"]]; |
继续往下走碰到getRelation函数用来处理$key变量
如果要走进下面的判断$relation的值需要为null
看getRelation函数随便写一个值都是会返回null的,所以在这里$key没有影响
继续向下看如果$relation为null,会用$this->getAttr函数再给$relation找一个值,这个值很重要,因为下面的语句在$relation调用visible函数
这里多写几句,点getAttr会发现跳到了Attribute.php
向上翻会发现这也是trait,往上看Model类use的图片会发现也使用了Attribute,所以这里都是通的
此时这里的$this是Pivot对象,在写exp的时候虽然不能实例化抽象类,还是可以控制抽象类内部的属性的
可能单独写在这里会有点迷惑,后面看到exp可能就清晰了
继续往下写
函数的第一个参数还是$key,后一个参数默认为null
又走到了$this->getData里面,继续看这个函数
array_key_exists作用为检查数组里是否有指定的键名或索引
意思就是第二个参数的数组里如果有第一个参数当作键的元素,返回true
这里的$this->data也是可控的,如果按照上面的$this->append
此时的$name=”aaa”,$this->data需要一个键为”aaa”的元素,取出该元素的值返回
至于这个元素的值需要是什么可以先往下看
这段结束了回到$this->getAttr
$fieldName和$method不需要管
后面的判断都是属性,都没有设置肯定走不了不用管
直接返回$value
回到toArray
$relation用了visible函数,在全部代码里面没有对应的visible函数可以使用
这时候需要用到上面的__call的知识了
\thinkphp\library\think\Request.php里的Request类的__call方法能让链子继续走下去
Request类中没有visible函数,调用了之后会触发里面的__call方法
所以上面说的$this->data需要一个键为”aaa”的元素,该元素的值需要设置为Request对象
$method=”visible”
$args=array(0 => “aaa”)
要走进判断需要$this->hook有一个键为visible的元素
array_unshift将$this存到$args数组的最前面
虽然下面有call_user_func_array,可是第二个参数不可控,所以还得找找
这次使用Request类中的isAjax函数,$this->hook需要这样写
1 | $this->hook = ["visible"=>[$this,"isAjax"]]; |
这里没啥需要看到的直接看$this->param函数就行
上面的判断对结果没有影响就收起来了
继续看$this->input
该函数第一个参数是$this->param可控的
该函数为最后触发命令执行的函数,贴代码详细看看
1 | public function input($data = [], $name = '', $default = null, $filter = '') |
弹出$filters数组的最后一个参数
遍历$filters内的元素
is_callable检查filters是否为可使用的函数
call_user_func调用函数和参数
1 | $value = call_user_func($filter, $value); |
1 |
|
这是完整的调用栈
看了别的师傅的exp发现把$this->config[‘var_ajax’]也指定了
这样也好万一这$this->config[‘var_ajax’]的默认参数被修改了也可以打通
走到toArray的步骤是一样的
选择第三个的foreach走到getAttr
这里的$key值是$data里面的键,再往上看会发现$data也是由$this->data和$this->relation合并来的
再来看getAttr
走到getData的相当于取出了自己的值
在前面的链子中没有设置$this->withAttr直接跳过了判断
可以看到进入判断后最后一行函数的参数都可控
1 | $value = $this->getData($name); |
调用栈