泛微E-Office10最新远程代码执行漏洞分析

泛微E-Office10最新远程代码执行漏洞分析

前言

顺便吐槽一下某步是真卷啊,一开始没啥头绪,虽然有一些简单的情报知道最终触发点在哪个路由,直到看到了长亭公众号的提示才将思路串起来确认,原来是这样,接下来来看看我的一些倒推的思路

漏洞影响

这一漏洞的成功利用将会导致严重的安全后果。攻击者通过上传特制的PHAR文件,可以执行服务器上的任意代码,从而获得服务器的进一步控制权。最严重的情况下,这可能导致服务器的完全接管,敏感数据泄露,甚至将服务器转化为发起其他攻击的跳板。

文章不涉及具体路由以及payload的生成,仅做部分思路分享

分析

触发条件分析

首先我们来看一下触发点eoffice10/server/app/EofficeApp/Empower/Controllers/EmpowerController.php:34

1
2
3
4
5
public function importEmpower()
{
$result = $this->empowerService->importEmpower($this->request->all());
return $this->returnResult($result);
}

接着我们来看看empowerService的具体调用,参数为request对象,可以看到这里根据我们传入的file参数从数据库当中获取了对应的信息保存到了$fileInfo,接下来取得参数信息中的temp_src_file并传入file_get_contents做触发,有一定php基础的朋友都会很容易想到如果我们控制这个参数为phar://xx的形式就不难实现一个反序列化的触发了

1
2
3
4
5
6
7
8
9
10
11
12
public function importEmpower($param)
{
if (empty($param["file"])) {
return ["code" => ["no_file", "register"]];
}
$fileInfo = app($this->attachmentService)->getOneAttachmentById($param["file"]);
if (empty($fileInfo) || !isset($fileInfo["temp_src_file"]) || !is_file($fileInfo["temp_src_file"])) {
return ["code" => ["no_file", "register"]];
}
$param["file"] = $fileInfo["temp_src_file"];
$content = file_get_contents($param["file"]);
......省略......

接下来我们继续倒推看看getOneAttachmentById的实现调用(eoffice10/server/app/EofficeApp/Attachment/Services/AttachmentService.php:520),由于调用过程的代码比较简单就不带着一行一行阅读了,这里仅列出关键调用的代码

1
2
3
4
5
6
public function getMoreAttachmentByRelId($relId, $mulit = true)
{
return $this->handleAttachmentsCommon($relId, $mulit, function ($relId) {
return app($this->attachmentRelRepository)->getAttachmentRelList(["rel_id" => [$relId, "in"]]);
});
}

继续跟入eoffice10/server/app/EofficeApp/Attachment/Services/AttachmentService.php:660

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
private function handleAttachmentsCommon($ids, $mulit, $before)
{
if (empty($ids)) {
return [];
}
$list = $before($ids);
if (0 < count($list)) {
$map = $relId2AttachmentId = [];
foreach ($list as $item) {
$tableSuffix = $item->year . "_" . $item->month;
if (isset($map[$tableSuffix])) {
$map[$tableSuffix][] = $item->rel_id;
} else {
$map[$tableSuffix] = [$item->rel_id];
}
$relId2AttachmentId[$item->rel_id] = $item->attachment_id;
}
$allAttachments = [];
foreach ($map as $tableSuffix => $relIds) {
list($year, $month) = explode("_", $tableSuffix);
$tableName = $this->getAttachmentTableName($year, $month);
$attachments = app($this->attachmentRepository)->getAttachments($tableName, ["rel_id" => [$relIds, "in"]]);
$allAttachments = array_merge($allAttachments, $this->handleAttachments($attachments, $tableName, $relId2AttachmentId, $mulit));
}
return $allAttachments;
} else {
return [];
}
}

eoffice10/server/app/EofficeApp/Attachment/Services/AttachmentService.php:701

1
2
3
4
5
6
7
8
9
10
11
12
13
private function handleAttachments($attachments, $tableName, $relId2AttachmentId, $mulit)
{
$handleAttachments = [];
if (0 < count($attachments)) {
foreach ($attachments as $attachment) {
$handleAttachment = $this->handleOneAttachment($attachment, $tableName, $relId2AttachmentId[$attachment->rel_id], $mulit);
if (!empty($handleAttachment)) {
$handleAttachments[] = $handleAttachment;
}
}
}
return $handleAttachments;
}

最终走到eoffice10/server/app/EofficeApp/Attachment/Services/AttachmentService.php:714,之前提到了我们需要控制的参数为temp_src_file

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
private function handleOneAttachment($attachment, $tableName, $attachmentId, $mulit)
{
$handleAttachment = [];
if ($attachment) {
$attachmentBasePath = $attachment->attachment_base_path ? $attachment->attachment_base_path : getAttachmentDir("attachment");
$handleAttachment["id"] = $attachment->rel_id;
$handleAttachment["attachment_id"] = $attachmentId;
$handleAttachment["attachment_name"] = $this->transEncoding($attachment->attachment_name, "utf-8");
$handleAttachment["affect_attachment_name"] = $attachment->affect_attachment_name;
$handleAttachment["attachment_base_path"] = $attachmentBasePath;
$handleAttachment["attachment_relative_path"] = $attachment->attachment_path;
$handleAttachment["attachment_type"] = $attachment->attachment_type;
$handleAttachment["attachment_mark"] = $attachment->attachment_mark;
$handleAttachment["category"] = $attachment->attachment_mark;
$handleAttachment["relation_table"] = $attachment->relation_table;
if ($mulit) {
if (!$attachment->attachment_size) {
$attachmentFullPath = $this->makeAttachmentFullPath($attachmentBasePath, $attachment->attachment_path, $attachment->attachment_name, true);
$attachment->attachment_size = $this->updateAttachmentSize($tableName, $attachment->rel_id, $attachmentFullPath);
}
$handleAttachment["thumb_attachment_name"] = $this->makeAttachmentBase64($attachmentBasePath, $attachment);
} else {
$handleAttachment["thumb_attachment_name"] = $attachment->thumb_attachment_name;
}
$handleAttachment["attachment_size"] = $attachment->attachment_size;
$handleAttachment["attachment_time"] = $attachment->created_at;
$handleAttachment["attachment_path"] = $this->makeAttachmentPath($attachmentId);
$handleAttachment["temp_src_file"] = $this->makeAttachmentFullPath($attachmentBasePath, $attachment->attachment_path, $attachment->affect_attachment_name);
$handleAttachment["attachment_base_path"] = $this->makeAttachmentBasePath($attachmentBasePath, $attachment->attachment_path, $attachment->affect_attachment_name);
if (!file_exists($handleAttachment["temp_src_file"])) {
$handleAttachment["attachment_base_path"] = getAttachmentDir("attachment");
$handleAttachment["temp_src_file"] = $handleAttachment["attachment_base_path"] . $attachment->attachment_path . $attachment->affect_attachment_name;
if (!file_exists($handleAttachment["temp_src_file"])) {
$handleAttachment["attachment_base_path"] = \App\Utils\Utils::getAttachmentDir("attachment");
$handleAttachment["temp_src_file"] = $handleAttachment["attachment_base_path"] . $attachment->attachment_path . $attachment->affect_attachment_name;
}
}
}
return $handleAttachment;
}

在上面的代码中可以看到最终组成为:

temp_src_file = attachment_base_path + attachment_path +affect_attachment_name

因此要想控制我们能触发phar反序列化,必须要能控制参数attachment_base_path

attachment_base_path参数控制

而要想控制这个参数我们则需要用到另一个路由eoffice10/server/app/EofficeApp/Attachment/Services/AttachmentService.php:1772,通过函数migrateAttachmentPath的调用我们最终可以实现对参数attachment_base_path的控制,这里仅需要通过web传入desc_path参数即可,同时保证source_path不为空

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
public function migrateAttachmentPath($data)
{
if (!isset($data["source_path"]) || !isset($data["desc_path"])) {
return false;
}
$logerPath = storage_path("logs/") . "attachment_migrate.log";
file_put_contents($logerPath, date("Y-m-d H:i:s") . " >> source_path:" . $data["source_path"] . " -- desc_path:" . $data["desc_path"] . "\r\n", FILE_APPEND);
$sourcePath = $data["source_path"] ? rtrim($data["source_path"], "/") . "/" : "";
$descPath = $data["desc_path"] ? rtrim($data["desc_path"], "/") . "/" : "";
if ($sourcePath === $descPath) {
return true;
}
$attachmentTables = app($this->attachmentRelRepository)->getAttachmentTables();
if (!empty($attachmentTables)) {
$attachmentRepository = app($this->attachmentRepository);
return array_map(function ($table) {
return $attachmentRepository->migrateAttachmentPath($table, $sourcePath, $descPath);
}, $attachmentTables);
}
return true;
}

eoffice10/server/app/EofficeApp/Attachment/Repositories/AttachmentRepository.php:160

public function migrateAttachmentPath($table, $sourcePath, $descPath)
{
if (\Schema::hasTable($table)) {
return \DB::table($table)->where("attachment_base_path", $sourcePath)->update(["attachment_base_path" => $descPath]);
}
return false;
}

知道了如何控制达成phar协议的控制,接下来就剩如何上传文件了

文件上传

在路由文件web.php中很容易找到这个方法,对应的处理类在

eoffice10/server/app/EofficeApp/Attachment/Services/AttachmentService.php:576

这里我们仅仅需要注意上传的文件名后缀需要是inc/evf/license即可,同时如果是inc后缀则需要文件名为register.inc

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
public function attachmentAuthFile($file, $data)
{
$fileErrCode = $file->getError();
if (0 < $fileErrCode) {
return ["code" => ["0x01100" . $fileErrCode, "upload"]];
}
$userId = isset($data["user_id"]) ? $data["user_id"] : "";
$attachmentId = $this->makeAttachmentId($userId);
$newPath = $this->createCustomDir($attachmentId);
if (isset($newPath["code"])) {
return $newPath;
}
$isWriteable = $this->isWriteable($newPath);
if (!$newPath || !$isWriteable) {
return ["code" => ["0x011010", "upload"]];
}
$originName = $file->getClientOriginalName();
$fileType = strtolower($file->getClientOriginalExtension());
if ($fileType == "inc" || $fileType == "evf" || $fileType == "license") {
if ($fileType == "inc" && $originName != "register.inc") {
return ["code" => ["0x011021", "upload"]];
}
$originNameSerect = md5(time() . $originName) . "." . $fileType;
try {
$file->move($newPath, $originNameSerect);
$newFullFileName = $newPath . $originNameSerect;
$attachmentPaths = $this->parseAttachmentPath($newFullFileName);
$attachmentInfo = ["attachment_id" => $attachmentId, "attachment_name" => $originName, "affect_attachment_name" => $originNameSerect, "new_full_file_name" => $newFullFileName, "thumb_attachment_name" => "", "attachment_size" => filesize($newFullFileName), "attachment_type" => $fileType, "attachment_create_user" => $userId, "attachment_base_path" => $attachmentPaths[0], "attachment_path" => $attachmentPaths[1], "attachment_mark" => 9, "relation_table" => "auth", "rel_table_code" => $this->getRelationTableCode("auth")];
return $this->handleAttachmentDataTerminal($attachmentInfo);
} catch (\Exception $e) {
throw new \Exception(new \Illuminate\Http\JsonResponse(error_response("0x011013", $e->getMessage()), 500));
}
}
return ["code" => ["0x011011", "upload"]];
}

至于最终反序列化phar文件的构造就更简单了,网上的nday即可,提示下恶意类关键词Illuminate\Broadcasting\PendingBroadcast,接下来懂得都懂😏

注意事项

上传文件时建议传入一个随机字符或者最好保持为空保证最终attachment_base_path为空,这样在第二步利用更新数据库时才不会覆盖用户数据