作者:LoRexxar'@知道創宇404實驗室
日期:2021年3月3日
這是一個由有條件的任意用戶登錄+低權限文件上傳+低權限目錄穿越+低權限文件包含組成。可能是盯著國內OA的人太多了,這個漏洞在2020年9月28號的11.8版本中被更新修復,比較可惜的是,一次更新修復了全部的漏洞邏輯,不禁令人驚嘆。
今天就一起來看看整個漏洞的邏輯~
有條件的任意用戶登錄
其實如果關注過通達OA的朋友,應該都會知道通達OA是一個特別龐雜的OA系統,整個系統涉及到2萬多個PHP文件,其中除了能訪問到的Web邏輯以外,OA還內置了特別多的其他功能,可能是用于定制版的OA,也可能壓根就是邏輯太多就有很多忘記了。這里的這個漏洞就特別的樸素。
mobile\auth_mobi.php

這里可以注意到左邊傳入的uid會直接從數據庫查詢,然后查詢出來的session id會被直接賦值給當前用戶。
也就是說,如果當前站點有正在登錄的用戶,我們通過遍歷uid就可以登錄所有當前在線的賬號。(唯一的問題是,通達OA有自動掉線機制,不過OA系統有在線用戶都很正常)
這里的修復方案也很奇怪,可以注意看上圖中右邊就是11.8的代碼,這段代碼直接就被刪除了...
低權限文件上傳+低權限目錄穿越
在通達OA中,其實涉及到上傳文件的地方并不少,而且后臺本身就有上傳文件的功能,但是通達OA在這方面做的比較好,它設計了兩個限制給文件上傳。
首先我們關注上傳文件的邏輯,主要函數為td_copy和td_move_uploaded_file這兩個,這里我們先關注td_move_uploaded_file
function td_move_uploaded_file($filename, $destination)
{
if (!is_uploadable($destination, true)) {
Message(_("禁止"), _("禁止創建此類型文件"));
Button_Back();
exit();
}
return move_uploaded_file($filename, $destination);
}
這里我們直接跟進is_uploadable函數
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;
}
...
可以關注到的是:
1、不能沒有.
2、不能.之后為空
3、.之后3個字符不能是PHP
第一反應是可以用phtml或者pht等繞過,但可惜通達內置的nginx在這方面配置的很好。
location ~ \.php$ {
fastcgi_pass OfficeFPM;
fastcgi_index index.php;
include fastcgi.conf;
add_header X-Frame-Options SAMEORIGIN;
}
首先避免了奇奇怪怪的文件后綴,只有php才解析執行。
其次通達還配置了專門的附件目錄
location /attachment {
deny all;
}
一般來說,除非找到繞過的辦法,否則所有的文件都會被上傳到這個目錄下,那么無論我們是否能繞過后綴限制,我們都沒辦法解析執行php文件。
所以這里我們需要找到一個配合目錄穿越的文件上傳點。
/general/reportshop/utils/upload.php line 116
else {
$uploaddir = MYOA_ATTACH_PATH . "reportshop/attachment/";
if (!is_dir(MYOA_ATTACH_PATH . "reportshop/attachment")) {
if (!is_dir(MYOA_ATTACH_PATH . "reportshop")) {
mkdir(MYOA_ATTACH_PATH . "reportshop");
}
mkdir(MYOA_ATTACH_PATH . "reportshop/attachment");
}
$s_code = mb_detect_encoding($filename, array("UTF-8"), true);
if ($s_code == "UTF-8") {
$filename = iconv("UTF-8", "GBK//IGNORE", $filename);
}
if (isset($rid)) {
if (!preg_match("/^\{([0-9A-Z]|-){36}\}$/", $rid) || preg_match("/^\{([0-9A-Z]|-){36}\}$/", $cid)) {
if (isset($json)) {
echo "{";
echo "new_name:'',\n";
echo "error: 'true',\n";
echo "msg: '文件不符合要求'\n";
echo "}";
}
else {
echo "文件不符合要求!";
}
exit();
}
if (!check_filename($filename) || !check_filetype($filename)) {
if (isset($json)) {
echo "{";
echo "new_name:'',\n";
echo "error: 'true',\n";
echo "msg: '文件不符合要求'\n";
echo "}";
}
else {
echo "文件不符合要求!";
}
exit();
}
if (file_exists($uploaddir . $filename)) {
$p = strpos($filename, "_");
$s = substr($filename, $p + 1, strlen($filename) - $p - 1);
$s_prefix = str_replace("-", "", str_replace("}", "", str_replace("{", "", $rid . $cid)));
if ($filename != "{" . $s_prefix . "}_" . $s) {
td_copy($uploaddir . $filename, "$uploaddir{" . $s_prefix . "}_" . $s);
}
}
}
else if (!empty($_FILES)) {
$s_n = $_FILES[$fileid]["name"];
if (!check_filename($s_n) || !check_filetype($s_n)) {
if (isset($json)) {
echo "{";
echo "new_name:'',\n";
echo "error: 'true',\n";
echo "msg: '文件不符合要求'\n";
echo "}";
}
else {
echo "文件不符合要求!";
}
exit();
}
if (($s_n[0] != "{") && isset($newid)) {
$s_n = "{" . $newid . "}_" . $s_n;
}
if (td_move_uploaded_file($_FILES[$fileid]["tmp_name"], $uploaddir . $s_n)) {
}
else {
$b_res = "false";
}
首先176行過濾非常簡單,后綴不是對應的幾個就行了,這里其實過濾沒啥用,有很多過濾方式,php5啊,php.,php::$DATA都可以繞過
function check_filetype($s_name)
{
$p = strrpos($s_name, ".");
if ($p !== false) {
$postfix = strtolower(substr($s_name, $p + 1));
if (in_array($postfix, array("php", "exe", "js"))) {
return false;
}
}
return true;
}
繞過的重心主要在這幾行
if (($s_n[0] != "{") && isset($newid)) {
$s_n = "{" . $newid . "}_" . $s_n;
}
if (td_move_uploaded_file($_FILES[$fileid]["tmp_name"], $uploaddir . $s_n)) {
}
else {
$b_res = "false";
}
這里可以關注到newid被直接拼接進了路徑中,且沒有設計專門的過濾,導致我們可以穿越任意目錄寫,當newid為../../../../目錄相應就為
D:/MYOA/webroot/attachment/reportshop/attachment/{321/../../../../a}_.txt
這里的修復方式也很直接,newid被添加了額外的過濾。

截至目前為止,我們可以將一個非php文件傳到任意為止了。在這里曾經困擾了我很久,因為這里的文件上傳實際上受到了3個以上的限制,且所有的限制都集中在php后綴,但這里明顯是個不太現實的目標。所以與其繼續去研究怎么找一個蹩腳的繞過方式,不如去找一個可以文件包含的地方。這里就用到了之前公開的任意文件包含漏洞,之前的漏洞修復方式主要是限制了..和權限。
任意文件包含
這里我們先看看之前的任意文件包含漏洞。
/ispirit/interface/gateway.php
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, "..") !== false) {
echo _("ERROR URL");
exit();
}
if ((strpos($url, "general/") === 0) || (strpos($url, "ispirit/") === 0) || (strpos($url, "module/") === 0)) {
include_once $url;
}
}
可以看到這里限制了general、ispirit、module開頭,且添加了專門的過濾,過濾了..等目錄穿越符號。
這里簡單了,我們直接上傳txt文件到general目錄下,然后包含即可。

這里也可以看到,在11.8版本中,這個文件包含被直接改成指向文件的了。
總結
這個組合漏洞最早是我在2020年年初挖的,一直存在手里也沒用上,沒想到突然就更新了,修復方式還特別像是翻著漏洞文檔一行一行修復的,就感覺很無奈。
其實之前通達OA的安全性一直受人詬病,在11.6開始,逐漸加入全局過濾,然后nginx的配置也經過很多次更新,比較關鍵的任意用戶登錄又一再修復,其實后臺的漏洞都無關緊要了,這也能說明通達的安全人員也是下了一番苦工的~
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1492/