代码审计-通达OA任意文件上传配合文件包含

wanganzgj / 2024-09-25 / 原文

任意文件上传的漏洞点在ispirit/im/upload.php这个文件里面

$P = $_POST["P"];
if (isset($P) || ($P != "")) {    //$p变量必须存在,文件才会继续执行
	ob_start();
	include_once "inc/session.php";
	session_id($P);
	session_start();
	session_write_close();
}
else {
	include_once "./auth.php";
}
$TYPE = $_POST["TYPE"];
$DEST_UID = $_POST["DEST_UID"];
$dataBack = array();
if (($DEST_UID != "") && !td_verify_ids($ids)) {    //如果$DEST_UID不为空和传入的参数为数字和逗号进行下一步,可以追踪下td_verify_ids函数
	$dataBack = array("status" => 0, "content" => "-ERR " . _("接收方ID无效"));
	echo json_encode(data2utf8($dataBack));
	exit();
}

if (strpos($DEST_UID, ",") !== false) {    //判断$DEST_UID里是否有逗号,有逗号则不执行代码,没有则强制转化为整数
}
else {
	$DEST_UID = intval($DEST_UID);
}

if ($DEST_UID == 0) {      //$DEST_UID等于0且不为2则执行下列代码
	if ($UPLOAD_MODE != 2) {
		$dataBack = array("status" => 0, "content" => "-ERR " . _("接收方ID无效"));
		echo json_encode(data2utf8($dataBack));
		exit();
	}
}

$MODULE = "im";

if (1 <= count($_FILES)) {   //当传入一个或多个文件时且$UPLOAD_MODE等于1时,执行下列代码
	if ($UPLOAD_MODE == "1") {
		if (strlen(urldecode($_FILES["ATTACHMENT"]["name"])) != strlen($_FILES["ATTACHMENT"]["name"])) {
//使用了URL解码函数并比较解码前后的文件名长度,如果不相等则证明该文件进行了URL编码。      
			$_FILES["ATTACHMENT"]["name"] = urldecode($_FILES["ATTACHMENT"]["name"]);
//将文件名替换成解码后的文件名
		}
	}

	$ATTACHMENTS = upload("ATTACHMENT", $MODULE, false);
//漏洞产生函数:upload,追踪upload

	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();
}
function upload($PREFIX, $MODULE, $OUTPUT)
{
	if (strstr($MODULE, "/") || strstr($MODULE, "\\")) {
//判断$MODULE里是否有斜杠和反斜杠且$OUTPUT为false就进行下一步
		if (!$OUTPUT) {
			return _("参数含有非法字符。");
		}

		Message(_("错误"), _("参数含有非法字符。"));
		exit();
	}

	$ATTACHMENTS = array("ID" => "", "NAME" => "");
//初始化$ATTACHMENTS数组,并存入名为ID和NAME的key
	reset($_FILES);
//让$_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;
}

由此可知利用漏洞需满足四个条件

    1. P=1

    2、UPLOAD_MODE=1

    3、DEST_UID=1

    4、表单名为ATTACHMENT

但是文件上传的路径并不在目录根路径,所以还需要利用文件包含漏洞来执行代码

文件包含漏洞在ispirit/interface/gateway.php文件

点击查看代码
ob_start();
include_once "inc/session.php";
include_once "inc/conn.php";
include_once "inc/utility_org.php";

//$P变量不为空则进行正则匹配
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存在则继续执行
if ($json) {
//去除反斜杠
	$json = stripcslashes($json);
//转换为数组
	$json = (array) json_decode($json);
//进行数组循环
	foreach ($json as $key => $val ) {
//如果键为data,则$val转化为数组
		if ($key == "data") {
			$val = (array) $val;
//$val数组进行循环
			foreach ($val as $keys => $value ) {
				$keys = $value;
			}
		}

//如果键为url,则将其值赋值给$url
		if ($key == "url") {
			$url = $val;
		}
	}

//如果$url不为空
	if ($url != "") {
//如果$url开头为斜杠,则去除斜杠
		if (substr($url, 0, 1) == "/") {
			$url = substr($url, 1);
		}
//如果$url包含特定字符串("general/"、"ispirit/"或"module/"),则包含对应文件
		if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
			include_once $url;
		}
	}

	exit();
}

由上代码可知,实现文件包含漏洞要满足两个条件

    json为true

    url包含特定字符串("general/"、"ispirit/"或"module/")

poc:json={}&url=general/../../attach/im/2409/xxx.txt