phpcms v9.6.0 sql
前言
今天看文章复现了一下 phpcms v9.6 的一个 sql 注入,觉得还有点意思 : )
代码审计
还是从 index.php 开始。
1 | include PHPCMS_PATH.'/phpcms/base.php'; |
在 /phpcms/base.php 中主要定义了一个与加载类、文件相关的 pc_base 类,上面可以看到在 index.php 的最后调用 pc_base 类的 create_app 方法。
先加载了类文件,然后实例化了 application 类,跟进该类。
在 application 类的构造函数中,实例化了 param 类,加载 param 类的方式同 application 类。param 是一个处理参数的类,跟进该类。
1 | // /phpcms/libs/classes/param.class.php line 14 |
这里调用了 new_addslashes 函数对 gpc 中的数据进行了转义。
1 | function new_addslashes($string){ |
到这里可以知道,该 cms 会对 gpc 的参数进行转义的处理,即引号是无法逃逸的,而该 cms 的 sql 语句中,大部分参数都是在引号内的,那为什么还会产生注入呢?
回到 application 类的构造函数中,接着调用了 param 类的 route_m()、route_c()、route_a() 这个几个函数去取模块、控制器和函数名,这里就是该 cms 的路由规则。
取到 m、c、a 后,调用了 application 类的 init 方法。
其中先调用了 load_controller 方法根据 m 和 c 去找到控制器类文件,实例化控制器后返回,接着在 init 方法中调用 call_user_func 去执行 a 指定的方法。
到这里就大致熟悉了该 cms 基本路由,在 /phpcms/modules/ 下有很多模块,通过 get 参数中 m 来指定模块,在每个模块下有许多类文件,通过 c 和 a 去调用指定类的指定方法。
接着我们直接定位到漏洞触发点,在 /phpcms/modules/content/down.php 文件的 dwon 类的 init 方法中。
init 方法中,先从 get 数组中取出了 a_k 变量,不要以为这里是从 get 直接取的就以为没有对值过滤或者转义,根据上面的分析,到这里时,gpc 数组中的数据已经被转义了,那为什么会有注入的,接着往下看。a_k 变量经过了 sys_auth 函数处理,由该函数第二个参数可以猜到,这应该是个解码解密类的函数,第三个参数是加解密的 key。
先继续看,sys_auth 函数处理完后,调用了 parse_str 注册变量。所以这里正常逻辑是,a_k 是个字符串,在某个地方加密后,在这里解密然后注册下面的 i、m、modelid、catid、f 等变量。看一下 parse_str 函数。
该函数接收的是 url 问号后面的参数。
可以看到,该函数会对传入的参数进行 url 解码,这样的话,可以在加密 a_k 的值时,传入 %27,到这里进行解密,并被 parse_str 注册到变量时,就会引入引号了。
回到 init 方法,通过调用 parser_str 函数,我们就可以控制下面其他变量的值了,其中的 id 变量会被带入数据库去查询,跟进下具体操作。
这里除了简单拼接外,并没有对 val 变量做其他处理。
整理一下,就是我们需要将类似m=2&modelid=3&catid=4&f=5&id=%27<payload>#
这样的字符串,通过 sys_auth 函数进行加密,然后在这里传给 get 中的 a_k 变量,解密后就可以利用 payload 了。关于 modelid 变量,我们可以注释掉部分内容,然后断点调试到这里,看一下可用的值。
因为是在有源码的情况下,所以我直接将 payload 加密进行利用,验证该处是否存在漏洞。
在 parse_str 处下个断点,然后传入该 payload。
可以看到,解密后,id 的值中还原了单引号,这样就可以实现 sql 注入了。
可是,在这里,我直接运行了 sys_auth 函数对 payload 进行加密,那如果实际场景中,密钥和我本地不同的时候,payload 就无法利用了。所以我们需要找到一个可以输入某些值,并回显输入值经过 sys_auth 函数处理后的值的地方。
vscode 中搜索下 sys_auth 函数调用处。
在 set_cookie 函数中,会调用 sys_auth 函数对传入 set_cookie 函数中的第二个参数进行加密,搜索下 set_cookie 函数的调用处,发现在 /phpcms/modules/attachment/attachments.php 的 swfupload_json 方法中,调用了 set_cookie,且参数可控。
可以看到 get 数组中的 src 变量我们可控,该变量会经过 safe_replace 函数和 json_encode 函数处理,先看 json_encode 函数。上面分析时,payload 为m=2&modelid=3&catid=4&f=5&id=%27<payload>#
,我们可以将其改成 get 中?src=&m=2&modelid=3&catid=4&f=5&id=%27<payload>#
,因为 parse_str 解析是根据 & 来分割变量的。
但这里有 safe_replace 函数处理,会将 %27 替换为空,可以用双写 %2%277 来绕过,或者在中间添加一个 %27 后面过滤的字符,如 %2*7。
找到了可以加密 payload 的地方,即 attachments 类中的 swfupload_json 方法,但是在实例化 attachments 类时,有身份验证的操作。
1 | class attachments { |
这里利用 cookie 中的值或者 post 的中 userid_flash 给属性 userid 赋值都是可以的,我以 post 中的 userid_flash 为例,我们还是要有一个类似上面的地方,可以将输出的数据通过 sys_auth 加密后回显,因为该处身份的验证使用 empty 函数,我们只要保证解密后有值即可。
这样的地方不止一处, /phpcms/modules/wap/index.php 中的 index 类的构造函数就符合要求。
1 | class index { |
不过网站的 wap 模块默认是处于禁用状态的,但响应包中仍然可以拿到 sys_auth 处理后的值。在参考文章中还有一处也可以拿到合理加密值。
总结一下。
- 在 /phpcms/modules/wap/index.php 中,get 传入 siteid =1,在响应包中拿到合理 sys_auth 函数加密值。
- 在 /phpcms/modules/attachment/attachments.php 中,post 传入 userid_flash = [上一步加密值],通过 attachments 类的构造函数中的登录检测。get 传入 src = &m=2&modelid=3&catid=4&f=5&id=%2*7 and updatexml(1,concat(1,(database())),1)#,src 的值记得 url 编码。在响应包中拿到 payload 加密后的值。
- 在 /phpcms/modules/content/down.php 中,get 传入 a_k = [上一步加密值]。
这样我们就实现了整个注入。在 phpcms 中,会将用户的 cookie 存储在数据库中,我们可以利用注入获取管理员的 cookie,然后登录管理员账号。先看一下数据库中相关信息。
注意这里的 userid 和 roleid,当管理员登录网站时,会将其修改为 1,跟进代码查看一下该逻辑。在后台登录界面,查看一下该表单的提交地址。
在 /phpcms/modules/admin/index.php 的 login 方法中,其中查询了登录用户的信息。
1 | // line 59 |
执行的具体 sql 语句如下,从 v9_admin 表中取出了身份信息。
在 v9_admin 表中也有 userid 和 roleid 这两个字段,且都为 1。
回到 login 方法,将 userid 和 roleid 这两个值存进了 session 数组中。
1 | // line 90 |
在前面提到的配置文件 /caches/configs/system.php 中定义了 session 的存储配置。
session 相关信息存放在数据库中,/phpcms/libs/classes/session_mysql.class.php 中的 write 方法如下。
这样就把 v9_session 中的 userid 和 roleid 的值修改成了 1,以记录该用户已经登录。
那如何注入出 cookie 用以伪造身份呢?在后台模块 /phpcms/modules/admin/ 下,所有类都继承自 /phpcms/modules/admin/classes/admin.class.php 中的后台基类 admin。其他类的构造函数中都调用了父类 admin 的构造函数,在 admin 类的构造函数中,调用了 check_admin 方法检查后台用户身份。
在 check_admin 方法中,从 cookie 中取出了 userid(带有前缀)对应的值,由上面知道在 get_cookie 中会调用 sys_auth 进行解密。然后从 session 中取出 userid 和 roleid,要满足 userid 和 roleid 都不为 0。根据上面分析,两个值都是 0 时,代表管理员没有登录,而管理员登录后,会将 v9_session 表中的两个值修改为 1。除此之外,get_cookie 取出的 userid 要和 session 中的相等,即也为 1。
我们可以利用前面的 wap 模块,对应文件 /phpcms/modules/wap/index.php,get 中传入 siteid = 1,会在响应包中拿到数字 1 经过 sys_auth 加密后的值。在将该值在伪造登录时传给 cookie 中的 userid(带有前缀),这样就可以通过上图的验证了。
漏洞补丁
对比 v9.6.1 中 /phpcms/modules/content/down.php 的 init 方法。
在 parse_str 前面调用了 safe_repalce 函数过滤,但如果 id 为 %2%2%2777,在加密时会调用 safe_replace 处理一次,在解密后,上图又会调用 safe_replace 处理一次,进入到 parse_str 时还是 %27,单引号也会逃逸。主要是 id 经过了下面的 intval 处理,这样就避免了该处的 sql 注入。
ref :
https://www.secpulse.com/archives/57486.html
https://mochazz.github.io/