Thinkphp5.0.24反序列化漏洞分析与利用

Thinkphp5.0.24反序列化

0x01前言

  • 最近在学习代码审计,因为java还不太擅长就先学习php的代码审计。thinkphp框架是php比较经典的一个框架了,所以就先选择了thinkphp进行审计。争取一到两周能发一篇审计的博客,督促自己不要偷懒。(这篇就拖了3个月,3个月前写了一半,但是后面一半到今天才写完,最终还是偷懒了,哈哈)

0x02下载与设置功能点

  • thinkphp 5.0.24下载地址:http://www.thinkphp.cn/donate/download/id/1279.html

  • 因为从官网上下载的thinkphp5.0.24自身是没有使用反序列化的功能的,所以我们需要自己添加上去,以便利用漏洞。我们将application/index/controller/Index.php的index()方法修改为

	public function index()
	{
		$yhck = unserialize($_GET['yhck']);
		var_dump($yhck);
	}
  • 这样我们就可以通过app\index\Index路由的index()方法使用反序列化的功能了

0x03寻找pop链

  • thinkphp5.0.24反序列化漏洞的流程大致是通过__toString()方法调用__call()方法最终实现写webshell,因此我们需要先找到可以利用的__toString()方法。在这里我们首先寻找的是think\process\pipes\Windows(对应thinkphp/think/process/pipes/windows.php文件)里的__destruct()方法。

  • 跟入removeFiles()方法

  • 我们可以看到removeFiles()方法的if分支中有file_exists()函数,执行file_exists()时$filename会被当做字符串,因此我们可以通过file_exists()函数触发__toString()函数,在这里选择\think\Model中的__toString()进行调用,进入\think\Model中的__toString()方法

  • 跟进toJson()方法

  • 跟进toArray()方法

  • 我们可以看到第912行,$value调用了一个getAttr()方法,如果$value可控的话,我们就可以通过控制$value调用__call()方法。我们往上跟进,看看$value是否可控。

  • 向上跟进,发现902行$value是由getRelationData()的返回值进行赋值的,我们跟入getRelationData()函数

  • 我们发现getRelationData()函数中如果我们传入的参数为Relation类且满足if分支的条件,那么$value就会由$this->parent的值决定,我们现在已经进到了if分支,那么就先看满不满足if分支的条件。$this->parent的值可控,if分支的第一个条件可以满足,接下来我们看第二个条件,跟进isSelfRelation()函数

  • isSelfRelation()函数返回$this->selfRelation,可控,因此我们可以满足if的第二个条件,跟进getModel()函数

  • getModel()函数返回$this->query->getModel(),$query可控,因此此时我们需要查找哪个类的getModel()可控,在这里找到了\think\db\Query类,跟进\think\db\Query类

  • \think\db\Query类的getModel()方法返回$this->model,可控,并且$this->parent可控,因此第三个条件满足,if分支满足

  • 接下来我们需要判断是否能传入一个Relation类的参数,我们回到\think\Model,发现调用getRelationData()函数时传入的是$modelRelation变量,跟进$relation(),发现$relation()函数是根据$relation的值进行调用的并且要满足method_exists()函数,跟进parseName()

  • parseName()函数只对传进来的$name做了一些大小写替换,没有实质上的过滤操作,因此$name可控,$relation可控

  • $relation可控的前提下我们要满足method_exists()函数就需要我们将$relation的值设定为\think\Model拥有的方法,在这里我们选择的是getError()方法。这个方法在这里用处很大,第一是这个方法是\think\Model拥有的方法,第二是$this->error可控,这样我们不仅满足了method_exists()函数,还让$modelRelation可控,这样$value也就可控了

  • 虽然$value可控,但是我们还要满足两个if的条件才能调用__call()方法,我们跟进这两个if条件

  • 第一个if条件需要满足$modelRelation存在getBindAttr()函数,并且$bindAttr变量由getBindAttr()函数返回值决定,我们全局搜索一下getBindAttr()函数,发现Relation类中不存在该方法,但OneToOne中存在且OneToOne是Relation类的子类、$this->bindAttr可控

  • 跟进OneToOne.php,发现OneToOne是个抽象类,无法生成实例,我们全局搜索继承它的类,发现HasOne类继承了OneToOne类,因此我们可以令$modelRelation的值为HasOne,此时便可满足第一个if条件,由于$this->bindAttr可控,因此我们也能满足第二个if条件

  • 我们往下跟进,发现$attr变量由$bindAttr决定,且$attr变量用于912行的$value->getAttr()中,因此$value->getAttr($attr)可控,所以我们可以根据$value->getAttr($attr)调用__call()方法,此时我们需要寻找能写webshell的__call()方法,在这里选择的是think\console\Output类

  • 在这里$method和$this->styles是可控的,array_unshift()对调用block()方法没有影响,因此我们跟进block()方法

  • 跟进writeln()函数

  • 跟进write()函数

  • 在这里$this->handle是可控的,我们寻找能写webshell的write()方法,此次选择了 think\session\driver\Memcache类

  • 在这里$this->handler又是可控的,我们继续寻找能写webshell的set()方法,此次选择了think\cache\driver\File 类,我们可以看到think\cache\driver\File 类的set()方法通过file_put_contents()将$data写进了文件,我们跟入$data和$filename,看$data与$filename是否可控

  • 往上跟进发现$filename的值是由getCacheKey()方法决定的,我们跟进getCacheKey()函数

  • 从getCacheKey()函数80行处的语句我们可以知道$filename的后缀是写死的,为php,并且文件名的一部分可控,这时如果$data可控的话就可以getshell了。我们跟进$data,发现$data最终是由think\console\Output类的write()方法决定的,$data的值为true,已经被写死了

  • 这样就说明了file_put_contents()函数能写入php文件,但内容不可控,无法写shell。继续往下看,发现有个setTagItem()函数,跟进该函数看看

  • 我们可以看到在setTagItem()函数中又一次调用了set()函数,并且这次的$key是可控的,$value由之前的$filename决定,这也意味着我们可以通过setTagItem()再一次的写入php文件进行getshell

  • 到这里整个pop链已经梳理完了,接下来我们看看如何利用这条pop链进行getshell

0x04利用pop链

  • 利用的时候我们首先需要绕过exit()的限制,因为利用file_put_contents()写入文件时内容有exit()函数并且在比较靠前的位置,如果执行到了exit()函数就会自动退出,不会执行我们写入的shell,所以我们需要绕过这个函数,这里用到php的伪协议进行绕过,具体原理见下图

  • 访问之后写入的文件内容

  • 也就是说我们只需要在文件名中使用伪协议即可对exit()函数进行绕过

  • 到这里我们其实可以写出整个payload了,但是目前只能写出Linux下的payload。为什么说是linux下的payload呢,因为windows文件名不能包含“<”、“?”、“>”等字符,但我们在使用伪协议时使用了这几个字符,所以我们想在windows下利用这条pop链的话还需要想一些其他的办法,此时我们就需要寻找其他的地方去赋值文件名,在这里我们找的是think\cache\driver\Memcached的set()方法,即当程序走到Memcache.php中的write方法时我们不直接赋予$this->handle为File对象,而是赋值为cache中的Memcached对象

  • 我们可以看到think\cache\driver\Memcached set()方法的第114行又调用了一次$this->handler->set()方法,并且文件名是通过$key决定的,然而$key又是通过getCacheKey()决定的,我们跟进getCacheKey()方法

  • 根据getCacheKey()方法我们知道$key是可控的,但是$value在114行的set()方法中又是不可控的,为0,此次虽然写进了一个文件,但并不能获取shell,所以我们需要通过setTagItem()方法写shell

  • 可以看到setTagItem()方法将$name作为$value传入set()函数,也就是将$key作为函数内容传入,这样我们就可以控制文件名和函数内容getshell了

  • 还需要注意一个坑点就是在通过这种方法在windows写shell的话需要php关闭短标签,否则执行的时候会报错,这时候我们可以通过iconv.UCS-4LE.UCS-4BE伪协议进行绕过

0x05利用效果

  • 最后通过写出的poc在windows下利用的效果如下

0x06参考:

posted @ 2021-11-28 22:08  Yhck  阅读(11699)  评论(0编辑  收藏  举报