通达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值导致的权限绕过问题

image-20200320233755927

继续往下看任意文件上传的问题

$TYPE = $_POST["TYPE"];//获取TYPE
$DEST_UID = $_POST["DEST_UID"];//获取DEST_UID(接收方ID)
$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函数进行校验

//td_verify_ids
function td_verify_ids($ids)
{
return !preg_match("/[^0-9,]+/", $ids);
}

其中$idsget传入的ids值,PHP中没有进行赋值的变量的值为NULL,此时的!preg_match("/[^0-9,]+/", $ids)falseDEST_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,然后包含即可

image-20200321004604561