泛微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为空,这样在第二步利用更新数据库时才不会覆盖用户数据