awvs v10.5 插件分析

最近分析了awvs的插件,总结下可供扫描器开发提供参考…

整体目录

AWVS11之前的版本扫描插件都在用户目录下,只是被加密存放,网上已经有人放出解密后的10.5的脚本,可以发现它是由js编写,官方已经对其进行说明,一个漏洞扫描插件包含如下两个文件:

1
2
*.script – 这是代码文件
*.xml – 这是漏洞描述文件

在关注脚本之前需要先了解这些脚本执行的时机,根据它们所属目录不同,执行时机也不同:

1
2
3
4
5
6
7
8
9
10
11
Network:此目录下的脚本文件是当扫描器完成了端口扫描模块后执行,这些脚本可以检测TCP端口的开放情况,比如检测FTP的21端口是否开放、是否允许匿名登录;

PerFile:此目录下的脚本是当扫描器爬虫爬到文件后执行,比如你可以检查当前测试文件是否存在备份文件,当前测试文件的内容等;

PerFolder:此目录下的脚本是当扫描器爬虫爬行到目录后执行,比如你可以检测当前测试目录是否存在列目录漏洞等;

PerScheme:此目录下的脚本会对每个URL的 GET、POST结构的参数进行检测,AWVS定义了的参数包括HTTP头、Cookies、GET/POST参数、文件上传(multipart/form-data)……比如你可以检测XSS、SQL注入和其他的应用程序测试;

PerServer:此目录下的脚本只在扫描开始时执行一次,比如你可以检测Web服务器中间件类型;

PostScan:此目录下的脚本只在扫描结束后执行一次,比如你可以检测存储型XSS、存储型SQL注入、存储型文件包含、存储型目录遍历、存储型代码执行、存储型文件篡改、存储型php代码执行等;

理解扫描过程后就可以看插件了,插件里大部分代码都可以直接看懂,但是它里面有一些是wvs自己定义的类和函数,官方给的sdk很简陋,需要自己去猜测,一般需要配合调试。

运行调试

这里没有调试工具,此处只能通过输出日志调试:

1
trace logInfo logWarning logErr....

它们都只能输出字符串,数字等最基础的对象,对于复杂对象可以使用for遍历输出,官方已经写了个traceObject函数可以用于输出对象的属性,它被包含在debug_helpers.inc里,注意trace级别太低只能在wvss里使用,无法在awvs里面输出。
要运行脚本有两种方式,第一种就是直接在扫描器里面选择对应插件进行扫描,插入的日志会输出在底部窗口:


另一种更灵活的方式是使用官方提供的小编辑器,它不能单独运行,需要被放在awvs的安装目录。
打开后大部分脚本都需要先导入爬虫输出(只有最下方的scanUrl和scanIp不需要爬虫数据可以直接指定),它当前的版本为1000,无法使用10.5的爬虫数据,推荐使用awvs 9.5爬站:

如上,导入数据后指定目录,输入点等,它们将在之后被使用:

如图,在运行脚本时,需要选择对应的运行方式,比如说在PerFolder目录下的脚本需要使用run per direcory运行,否则会报错。

功能分析

因为存在很多native层的类/函数,无法知道其源码,所以需要自己实现它的功能,有两种方式以确定其功能:

  1. 逆向分析:wvss存在符号信息,它使用delphi编写,先分析其类的描述信息再分析,在有的时候能起到辅助作用,比如存在一个addHTTPJobToCrawler,无法找到它的接口定义,在文中为其传入的参数为为addHTTPJobToCrawler(lastjob,1,1),通过查看其符号表和汇编代码可以看出后两个参数为是否处理lastjob里的请求里有的链接和相应里有的链接:

    又比如THTTPJob对象在设置uri与url时使用的概念和通常的概念不同,如下图在输入url后,scheme只会保留是否是https,uri是指/path?param
  2. 猜测:通过方法名,参数等可以猜测出内置对象的功能,阅读大量的插件脚本再配合上大量的测试可以猜出大部分的功能。比如先搭建一个功能齐全的站点,再使用awvs进行全站爬取,爬完以后就可以通过wvss获取大量需要的变量再验证猜测,以InputScheme这个对象为例,它作为最重要的一个对象之一也特别复杂,只能知道它是和输入点相关的对象,那么先爬站拿到如下数据:

    以这个页面为例,可以看出它在爬站的时候会记录所有发现参数组合而且inputs一共有7个,若仔细数一下发现所有参数输入点一共有8个,所以此处的inputs应该不是通常的输入点个数,使用wvss打开:

    如上可以看到当前页面有7个schemes,和inputs个数对应了,并且scanning context框可以看出每一种input(scheme)的内容,接下来查看一个指定的scheme:

    验证了猜想,看见指定input target后,inputCount数变成了4,即该scheme有4个输入点variationCount数为9,检查左侧(图片未截完整)发现爬取到了9种情况,再选择里面的两个方法进行研究:

    可以看出populateRequest的作用是把参数给输入的httpjob,而loadVariation的作用是将指定的变量载入,以便像populateRequest这种方法使用。在读脚本时可能会遇到一些不理解的函数,变量等,此时通过搜全局找其他地方的用法一般就能推断出意思。一些逻辑复杂的代码可以猜测后再直接拖出来使用浏览器或者node执行代码验证。

解密脚本

网上的的解密后插件有部分插件存在错误,查找资料没有找到解密代码,原作者是对主程序进行脱壳再直接从内存dump数据,因为发现官方的sdk提供一个很小的编辑调试工具未加壳,于是直接从这上面分析,它使用delphi编写,只需要注意它的调用约定为:

1
eax edx ecx stack...

首先直接分析加密后的脚本发现它们都以CAFCCACA开头,它应该是魔数,查看大小发现小于被解密的后的数据,应该是用了压缩算法,密文长度未对齐可能用了自定义算法,流算法或使用了特殊的填充算法,接着开始调试:
分析方式是在kernel32.CreateFile下断点,接着运行代码,导入被加密的库:

之后通过在输入数据上打访问断点可以发现首先判断魔数,再被解密,在解密部分卡了很久,twofish本身就很小众,这里又不是常规的加密模式,此处可以发现加密密钥等数据,之后会使用CBC进行加密:

特别要注意,它没有使用填充或者密文窃取,而是对最后一个不满足大小的块做如下操作:

在解密完成后,在解密后的数据上打内存访问断点,继续运行会发现断在如下位置:

回溯就能看到明文信息了:

解密代码如下,在块加解密上我直接用了serhanoztuna的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# coding=utf-8
import os
import zlib

from mylogger import logger
from twofish import Twofish

IV = '6A 91 CE 6A 59 D4 F0 42 6D AB C0 F2 35 C2 88 69'
KEY = '67 68 6E 5B 38 5C 43 5A 2A 3C 4E 74 57 34 37 73'
MAGIC = '\xCA\xFC\xCA\xCA'


class TwofishCBC(object):
BLOCKSIZE = 16

def __init__(self, iv, key):
self.iv = bytearray(iv)
self.key = bytes(key)

def _byte2hex(self, in_):
return ''.join(("%02X" % i for i in in_))

def _DecryptECB(self, in_):
res = Twofish(self.key).decrypt(in_)
return bytearray(res)

def _EncryptECB(self, in_):
res = Twofish(self.key).encrypt(in_)
return bytearray(res)

def decrypt(self, cipher_text):
# 先处理完整块
cipher_len = len(cipher_text)
tmp = bytearray(16)
res = bytearray(cipher_len)
i = 0
for j in range(cipher_len // self.BLOCKSIZE):
tmp[0:16] = cipher_text[i:i + 16]
res[i:i + 16] = self._DecryptECB(cipher_text[i:i + 16])
res[i:i + 16] = self._xor(res[i:i + 16], self.iv)
self.iv[0:16] = tmp[0:16]
i = (j + 1) * self.BLOCKSIZE

# 处理剩下部分
if cipher_len % self.BLOCKSIZE:
self.iv = self._EncryptECB(self.iv)
res[i:] = cipher_text[i:]
res[i:] = self._xor(res[i:], self.iv, cipher_len % self.BLOCKSIZE)
return bytes(res)

def _xor(self, data1, data2, size=16):
res = bytearray(size)
for i in range(size):
res[i] = data1[i] ^ data2[i]
return res


def decrypt(cipherText, iv=IV, key=KEY):
iv = bytearray.fromhex(iv)
key = bytearray.fromhex(key)
logger.debug("密文数据 : len -> {:02} content -> {}".format(len(cipherText), cipherText.encode('hex')))
# 开始解密
twofish = TwofishCBC(iv=iv, key=key)
decStr = twofish.decrypt(cipherText)
logger.debug("明文结果 -> {}".format(decStr.encode('hex')))
# 开始解压
res = zlib.decompress(decStr)
logger.debug("解压结果 -> {}".format(res))


def decrypt_wvs_script(path):
plain_text = bytes()
try:
with open(path, 'rb') as fd:
magic = fd.read(4)
if magic != MAGIC:
logger.info("非加密文件:{}".format(path))
else:
cipherText = fd.read()
plain_text = decrypt(cipherText)
except Exception as e:
logger.exception("发生异常: {} -> {}".format(path, e))
return plain_text


def main():
for root, _, files in os.walk(r'D:\Users\qihoo\Desktop\PerFolder'):
for fn in files:
path = os.path.join(root, fn)
res = decrypt_wvs_script(path)
if not res:
continue
try:
with open(path, 'w') as fd:
fd.write(res)
except Exception as e:
logger.exception(e)


if __name__ == '__main__':
main()

测试环境

由于不需要考虑插件本身是否存在错误,关注的是我们是否实现了插件运行所依赖的环境以及运行效果一致,可以在分析完插件后根据请求构造对应的响应,例如在测试Perljam2文件上传漏洞时,其检测逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
if (pathExtension && (pathExtension == "cgi" || pathExtension == "pl")) {  //扩展名为.pl
var fileInputName = "";
// make a list with all file inputs
for (var i = 0; i < this.scheme.inputCount; i++) {
if (this.scheme.getInputFlags(i) & INPUT_FLAG_IS_FILE) {
fileInputName = this.scheme.getInputName(i);
break;
}
}

if (fileInputName) {

var url = this.scheme.path + "?/etc/passwd"; //url里存在/etc/passwd查询字符串

var body = '-----------------------------23780209327207' + CRLF();
body = body + 'Content-Disposition: form-data; name="' + fileInputName + '"' + CRLF();
body = body + CRLF();
body = body + 'ARGV' + CRLF();
body = body + '-----------------------------23780209327207' + CRLF();
body = body + 'Content-Disposition: form-data; name="' + fileInputName + '"; filename="1.txt"' + CRLF();
body = body + 'Content-Type: text/plain' + CRLF();
body = body + CRLF();
body = body + 'test' + CRLF(); // 上传文件的内容为test
body = body + '-----------------------------23780209327207--' + CRLF();

this.lastJob = new THTTPJob();
this.lastJob.url = scanUrl;
this.lastJob.verb = 'POST';
this.lastJob.uri = url;
this.lastJob.request.addHeader('Content-type', 'multipart/form-data; boundary=---------------------------23780209327207', true);
this.lastJob.request.body = body;

this.lastJob.execute();

// look for /etc/passwd
if (!this.lastJob.wasError) { // 收到响应没有错误且包含passwd里的内容
var regex = /((root|bin|daemon|sys|sync|games|man|mail|news|www-data|uucp|backup|list|proxy|gnats|nobody|syslog|mysql|bind|ftp|sshd|postfix):[\d\w-\s,]+:\d+:\d+:[\w-_\s,]*:[\w-_\s,\/]*:[\w-_,\/]*[\r\n])/;
var m = regex.exec(this.lastJob.response.body);
if (m) {
this.alertPerljam2(fileInputName, m[0]);
return true;
}
}
}
}

因此可以针对关键请求实现以下响应,即通过代码覆盖来做测试靶机:

1
2
3
4
5
6
7
8
9
10
11
12
@post('/File_Upload/upload.pl')
def File_Upload_POST():
text = 'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\n'
try:
upload_file = request.files['upload_file']
file_content = upload_file.file.read()
except Exception as e:
return e
if request.query_string == '/etc/passwd':
if file_content == 'test':
return text
# ...

对于复杂的脚本,可以使用代理+burpsuite的方式监控对应脚本的发包数据以调试靶机:

附:twofish加密过程