记一次某 CMS 的代码审计(下)
接下来再看 g 方法,g 方法也是将 f 方法的返回值作为参数传入。
首先第 113 行从配置文件中获取了路由的相关信息,并赋值给 $u。随后 115 行判断传入的 $q 参数,这里判断为 false,不进入语句。
第 119 行将路由信息进行遍历,匹配传入的 url 路径信息。匹配成功则将 url 路径信息中的路由键替换为路由值,最后将替换后的 url 路径信息返回。
i 方法也是将刚刚返回的 url 路径信息传入,我们来看一下具体实现。第 133 行从配置文件中读取了配置模块信息,注意这里第二个参数为 true,所以 $w 为一个数组(home,admin,api)。
程序第 134-139 行将传入的 url 路径信息分割为数组 $r_array。第 140-155 行是程序的路由分配,根据数组元素的长度,将值分别赋值给 $h 数组元素。
注意这里的 m、c、f 并非是我们自定义的元素,而是 Kernel.php 文件中所原有的,我们可以回顾最开始被解密的文件。因此猜测 m 对应模块,c 对应控制器,f 对应方法。
继续阅读代码,第 156-167 行是为 m、c、f 进行初始化。第 168-171 行是判断模块是否存在于预定义的数组中,最后返回包含路由信息的数组 $h。
接下来我们再看看 k 方法,k 方法传入了包含程序路由信息的数组。第 178-180 行,定义了代表模块的常量 M,并根据模块定义了模块下的模型路径、控制器路径常量。
第 181-195 行判断模块模板路径是否存在于当前模块的模板路径中,以及网站根路径是否存在于当前模块的模板路径中,将路径赋值给 APP_VIEW_PATH 视图模板常量。
第 194 行,判断控制器中是否存在.字符。若存在则将其替换为/字符,获取其路径中的文件名,并将首字母大写赋值给 $controller 变量。
若不存在.字符,则直接进行首字母大写并赋值给 $controller 变量。第 205 行获取完整的控制器路径,第 208-223 行根据模块及控制器来定义常量。
第 226 行定义控制器常量 C,第 227-235 行根据 REQUEST_URI 值来定义 URL 常量。第 227-246 行根据包含路由信息的变量 $a 元素数及 $dd 值,获取 GET 请求参数,最终返回控制器名称。
随后返回 Kernel.php 文件,我们还剩 l、m 方法没有看,下面看一下 l 方法。
第 251 行判断程序是否处于调试模式,debug 默认为 false,若处于调试模式会获取一些配置文件并检查关键文件是否存在。
第 252-265 行判断,如果当前模块为 api 时,request 函数判断用户请求是否携带了 sid 参数,若存在则开启 Session,我们跟进看一下 request 函数。
第 532-536 行检查用户的请求类型(POST/GET),随后将其传入数组 $condition 中,并参数名及数组传入 filter 过滤函数中,我们再次跟进 filter。
代码首先在第 289 行判断传入的参数是否在数组键中,且是否拥有对应的变量描述文本。
若满足条件则将其赋值给 $vartext 变量,不满足则将参数赋值给 $vartext。随后第 296-316 行,根据请求类型获取对应的参数值赋值给 $data。
随后的代码对数据进行了非常严格的类型检测、过滤、转义等操作,最终返回数据。
过滤代码使用了自定义的 preg_replace_r 函数,其作用是递归替换。
转义代码将数组、对象数据类型分别转化为字符串类型,随后又用了 htmlspecialchars 函数以及 addslashes 函数进行处理,htmlspecialchars 函数使用 ENT_QUOTES 参数,无法单引号绕过,同时指定了 UTF-8 编码。
读完后我们回到 Kernel.php 文件继续读 l 方法。程序第 261-265 行,若当前模块不为 api,则对用户的浏览器及操作系统进行检测。
分别跟进 checkBs 和 checkOs 方法。首先 checkBs 注释写的很明确,首先 105-106 行读取配置文件,获取黑/白名单。
随后使用 get_user_bs 方法获取客户端浏览器类型,并根据黑/白名单进行拦截/放行。
我们跟进 get_user_bs 方法,首先判断 UA 信息是否存在,若存在则将其转化为小写,并赋值给 $user_agent 变量。
其次检测参数 $bs 是否存在,若存在则判断 $user_agent 是否包含在 $bs 中,并返回 true/false。
随后即进行与已定义的浏览器进行固定检测,由于 $user_bs 的值为写死的,无法通过修改 UA 来进行日志注入等操作。
随后 get_user_os 函数原理相同,不再记录。随后 266-279 行都是在检测相应文件是否存在,若存在则进行包含。第 280-284 行,程序检查对应控制器文件是否存在,存在则进行实例化。
至此,l 方法我们也已经读完了,还剩下最后的 m 方法,我们跟进看看。
方法 m 接受一个控制器路径参数,第 288-291 行都是将控制器路径信息进行赋值。第 292 行判断对应的控制器文件是否存在,若不存在则返回 404 错误。第 306-309 行,判断类中是否存在对应的类方法,若不存在则返回错误信息。
第 310-332 行对类进行了实例化操作,并判断对应方法在类中是否存在。若存在则判断是否存在同名方法,若存在则执行方法,将返回值赋值给 $nn。若不存在同名构造方法,则返回赋值实例化后的值。
若类中不存在对应方法,判断类中是否存在 _empty 方法,若存在则执行获取返回赋值给 $nn,若不存在则返回报错信息。最后第 333-337 行判断返回的 $nn 是否为空,若不为空则输出 $nn。
至此,index.php 入口文件我们已经读完,接下来看看 admin.php 文件。
admin.php
可以看到,一切都是那么的熟悉,跟 index.php 的区别就是常量 URL_BIND 变为了 admin。
最后我们再来看一下最后一个入口文件 api.php
api.php
结构也是一样的,常量 URL_BIND 变为了 api。
04 漏 洞
操作系统/浏览器黑白名单绕过
Kernel.php 的第 250-262 行,若当前模块不为 api 时,系统会检查用户的操作系统/浏览器是否在黑白名单中。
跟进 checkBs,105-106 行检查浏览器浏览器是否在配置文件的黑白名单中,在 111 行获取用户浏览器。
跟进 get_user_bs,系统直接通过 UA 头获取用户浏览器,进行黑白名单比对。
因此我们直接去到 config.php 中设置黑名单,禁止 firefox 浏览器访问。
设置后发现页面已经无法访问
通过 BurpSuite 将 UA 头进行修改,即可绕过限制,操作系统相关黑白名单同理。
05 存储型 XSS
过滤不严格导致的存储型 XSS
后台文章内容 -> 新闻内容 -> 新闻新增处存在存储型 XSS
首先黑盒测了一波,随便新建了一篇文章,并上传了一张图片附件。
抓包并观察数据包,发现 content 字段为我们刚刚提交的正文部分。
将其进行 URL 解码,发现内容中存在 HTML 标签。
因此我们可以尝试在 HTML 标签中加入一段 XSS 的 Payload,触发存储型 XSS
将修改后的 content 值重新放到 Burp 中重发,并将标题名改为了 hello2022,服务器返回 200 成功
接下来我们回到前台,进入新闻中心进行查看,成功触发 XSS
代码分析:
首先根据数据包路由信息,我们定位到代码位置:
/apps/admin/controller/ContentController::add
可以看到,上来就是使用自定义的 post 方法获取一堆数据。
但是我们注意到,在这里的 post 方法大部分都是没有指定第 2 个参数的,而第 2 个参数的作用是限制数据类型。
而 add 方法也仅对部分变量做了规范化处理,并未对 content 进行相应处理,如过滤敏感标签、事件等。
随后将 content 变量放入了一个 data 数组中,调用 addContent 写入数据库,最终导致存储型 XSS。
由于逻辑相似,系统新闻内容、产品内容、案例内容、招聘内容等功能点均存在存储型 XSS漏洞。
上传 pdf 文件导致的存储型 XSS
在查看代码的配置文件 config.php 时,发现允许上传 pdf 文件,就想到了这个漏洞。
首先生成一个嵌有 js 脚本的 pdf 文件,然后正常作为附件上传即可,具体生成操作可参考百度。
前台访问文章附件触发:
06 反射型 XSS
看代码实在是看累了,就把网站丢进了扫描器。首先是扫到了一个 XSS ,我们验证一下结果。
好像还真的存在,类型的标签乱了。
我们随便敲个 ext_color=1,F12 看一下
可以看到图中的信息在 a 标签的 href 中被原样输出了,因此我们可以构造payload。
payload:http://127.0.0.1:8002/?product/&ext_color=1%22%3C/a%3E%3Cimg%20src=1%20onerror=alert(`1`)%3E
成功弹窗
07 SQL 注入
随后又扫到了一个 SQL 注入,我们验证一下结果。
恩?好像确实有东西
我们在 vscode 中搜索关键字 OR a.filename= 。发现第 559-566 行即拼接 SQL 语句的地方,$id 参数没有做过滤。
接下来我们最好能把拼接好的 SQL 语句打印出来,根据完整的语句构造注入。首先看到这个结构想到了 ThinkPHP,在 TP 中可以使用 fetchSql、buildSql 等方法打印 SQL 语句。
但尝试后发现行不通,使用 buildSql 后程序直接报错了,看来只能是用其他方法。
我们跟进最后的 find 方法,在第 992 行插入 var_dump 强行打印拼接后的 SQL 语句试一试。
成功返回了 SQL 语句,可以看到我们输入的12'" 在圆括号中包裹。
接下来我们就可以根据语句来构造特定语句了(这里要注意程序默认使用的是 sqlite 数据库)
Payload:http://127.0.0.1:8003/?1')/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,randomblob(1000000000)--
可以看到,根据右下角延时时间的对比,成功证明存在 SQL 注入漏洞。
文章到这里就结束了,出于个人原因这套 CMS 我并没有审计完成。后续如果时间允许的话,我也会继续以文章的形式分享出来。
文章中所写的内容只是我个人在进行代码审计学习时所记录的笔记,希望能帮助到一些想要学习代码审计的同学。其中如果有错误的内容也希望大佬们能帮我指出,大家一起学习和进步。
