扫描器开发 - 1
背景
由于手工测试过于繁琐,而且基本上常见的漏洞判断都是重复动作。
一般在渗透测试挖掘漏洞的基本流程如下:
数据包解析
数据包解析一般包括 请求方法解析、参数解析、http/s协议识别,这里我偷个懒使用burpsuite的接口,然后将header、method、参数组合为一个字典
然后通过socket 传送到扫描端,就省去了自己去解析参数。
# PARAM_URL 0 , PARAM_BODY 1
def getParamaters(self, params, ptype):
params_dict = {}
for i in params:
if i.getType() == ptype:
# params_dict[i.getName()] = json.loads(self._helpers.urlDecode(i.getValue()))
params_dict[i.getName()] = self._helpers.urlDecode(i.getValue())
return params_dict
def parseRequest(self, messageInfo):
httpService = messageInfo.getHttpService()
analyzeRequest = self._helpers.analyzeRequest(messageInfo)
host = httpService.getHost()
port = httpService.getPort()
protocol = httpService.getProtocol()
method = analyzeRequest.getMethod()
full_url = analyzeRequest.getUrl().toString()
bp_headers = analyzeRequest.getHeaders()
content_type = analyzeRequest.getContentType()
# self.stdout.println(host + str(port) + protocol)
reqUri, bp1_headers = '\r\n'.join(bp_headers).split('\r\n', 1)
headers = dict(re.findall(r"(?P<name>.*?): (?P<value>.*?)\r\n", bp1_headers + '\r\n'))
# self.stdout.println(headers)
body = messageInfo.getRequest()[analyzeRequest.getBodyOffset():].tostring() if messageInfo.getRequest()[
analyzeRequest.getBodyOffset():].tostring() else '{}'
params = analyzeRequest.getParameters()
paramsINURL = self.getParamaters(params, 0)
paramsINBODY = self.getParamaters(params, 1)
send_data = {}
send_data['host'] = host
send_data['port'] = port
send_data['protocol'] = protocol
send_data['method'] = method
send_data['full_url'] = full_url
send_data['headers'] = headers
send_data['content_type'] = content_type
send_data['body'] = body
send_data['param_in_url'] = paramsINURL
send_data['param_in_body'] = paramsINBODY
发送的数据为:
{'headers': {u'Accept': u'*/*', u'PDD-CONFIG': u'V4:002.059900', u'User-Agent': u'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ', u'Connection': u'close', u'Host': u'101.35.212.35', u'Accept-Encoding': u'gzip, deflate', u'vip': u'101.35.212.35'}, 'method': u'GET', 'full_url': u'http://101.35.212.35:80/d?id=25196&ttl=1&dn=1', 'param_in_body': {}, 'body': '{}', 'protocol': u'http', 'content_type': 0, 'port': 80, 'host': u'101.35.212.35', 'param_in_url': {u'dn': u'1', u'ttl': u'1', u'id': u'25196'}}
http参数处理
平时我们在测试漏洞一般过程为 替换参数value,然后发送请求,根据响应或者dnslog的一些返回来判断漏洞是否存在。所以我们开发自动化漏洞扫描器就是要模拟手工测试行为。
上面一步我们已经将参数都解析出来并生成一个dict。
常见参数形式包括:
GET 或 POST application/x-www-form-urlencoded
a=1
a={"x":123}
a={"x":[1,2,3]}
a={"x":{"y":"bbb"}}
a={"x":{"y":["bbb","ccc"]}}
a={"x":{"y":{"bbb":"ccc"}}}
a={"x":{"y":{"bbb":[1,2]}}}
POST application/json
{"x":123}
{"x":[1,2,3]}
{"x":{"y":"bbb"}}
{"x":{"y":["bbb","ccc"]}}
{"x":{"y":{"bbb":"ccc"}}}
{"x":{"y":{"bbb":[1,2]}}}
还有多层嵌套json 的结构。
这里需要分别对每个参数值替换或者追加payload。这里直接采用了 https://github.com/w-digital-scanner/w13scan/blob/cd6935719edec9ad8131561a2a93bbf07024cf72/W13SCAN/lib/core/common.py#L430 的 updateJsonObjectFromStr 方法,对此做了一些微小的改动,可以支持无限嵌套dict、list的解析和payloa替换追加。
def updateJsonObjectFromStr(self, base_obj, update_str: str, mode: int):
"""
为数据中的value 添加 、替换为 update_str
:param base_obj:
:param update_str:
:param mode: 0, 替换 1 追加 2 ssrf
:return: 返回带有update_str的字典
"""
assert (type(base_obj) in (list, dict))
base_obj = copy.deepcopy(base_obj)
# 存储上一个value是str的对象,为的是更新当前值之前,将上一个值还原
last_obj = None
# 如果last_obj是dict,则为字符串,如果是list,则为int,为的是last_obj[last_key]执行合法
last_key = None
last_value = None
# 存储当前层的对象,只有list或者dict类型的对象,才会被添加进来
curr_list = [base_obj]
# 只要当前层还存在dict或list类型的对象,就会一直循环下去
while len(curr_list) > 0:
# 用于临时存储当前层的子层的list和dict对象,用来替换下一轮的当前层
tmp_list = []
for obj in curr_list:
# 对于字典的情况
if type(obj) is dict:
for k, v in obj.items():
if k not in self.black_params_list:
# 如果不是list, dict, str类型,直接跳过 {"action":"xx","data":{"isPreview":false}} 这里不会替换isPreview, 他是bool类型
if type(v) not in (list, dict, str, int):
continue
# list, dict类型,直接存储,放到下一轮
if type(v) in (list, dict):
tmp_list.append(v)
# 字符串类型的处理
else:
# 如果上一个对象不是None的,先更新回上个对象的值
if last_obj is not None:
last_obj[last_key] = last_value
# 重新绑定上一个对象的信息
last_obj = obj
last_key, last_value = k, v
# 执行更新
if mode == 0:
obj[k] = update_str
elif mode == 1:
obj[k] = str(v) + update_str
elif mode == 2:
obj[k] = self.generate_ssrf_payload(update_str)
# 生成器的形式,返回整个字典
yield base_obj
# 列表类型和字典差不多
elif type(obj) is list:
for i in range(len(obj)):
# 为了和字典的逻辑统一,也写成k,v的形式,下面就和字典的逻辑一样了,可以把下面的逻辑抽象成函数
k, v = i, obj[i]
if v not in self.black_params_list:
if type(v) not in (list, dict, str, int):
continue
if type(v) in (list, dict):
tmp_list.append(v)
else:
if last_obj is not None:
last_obj[last_key] = last_value
last_obj = obj
last_key, last_value = k, v
if mode == 0:
obj[k] = update_str
elif mode == 1:
obj[k] = str(v) + update_str
elif mode == 2:
obj[k] = self.generate_ssrf_payload(update_str)
yield base_obj
curr_list = tmp_list
生成的数据如下:每一个http请求都为一个字典
[{
'headers': {
'Accept': '*/*',
'PDD-CONFIG': 'V4:002.059900',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ',
'Connection': 'close',
'Host': '101.35.212.35',
'Accept-Encoding': 'gzip, deflate',
'vip': '101.35.212.35'
},
'method': 'GET',
'full_url': 'http://101.35.212.35:80/d?id=25196&ttl=1&dn=[1,2,3]&x={"a":{"b":"y"}}',
'param_in_body': {},
'body': '{}',
'protocol': 'http',
'content_type': 0,
'port': 80,
'host': '101.35.212.35',
'param_in_url': {
'dn': [1, 2, 3],
'ttl': 'PAYLOAD',
'x': {
'a': {
'b': 'y'
}
},
'id': 25196
}
}, {
'headers': {
'Accept': '*/*',
'PDD-CONFIG': 'V4:002.059900',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ',
'Connection': 'close',
'Host': '101.35.212.35',
'Accept-Encoding': 'gzip, deflate',
'vip': '101.35.212.35'
},
'method': 'GET',
'full_url': 'http://101.35.212.35:80/d?id=25196&ttl=1&dn=[1,2,3]&x={"a":{"b":"y"}}',
'param_in_body': {},
'body': '{}',
'protocol': 'http',
'content_type': 0,
'port': 80,
'host': '101.35.212.35',
'param_in_url': {
'dn': [1, 2, 3],
'ttl': 1,
'x': {
'a': {
'b': 'y'
}
},
'id': 'PAYLOAD'
}
}, {
'headers': {
'Accept': '*/*',
'PDD-CONFIG': 'V4:002.059900',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ',
'Connection': 'close',
'Host': '101.35.212.35',
'Accept-Encoding': 'gzip, deflate',
'vip': '101.35.212.35'
},
'method': 'GET',
'full_url': 'http://101.35.212.35:80/d?id=25196&ttl=1&dn=[1,2,3]&x={"a":{"b":"y"}}',
'param_in_body': {},
'body': '{}',
'protocol': 'http',
'content_type': 0,
'port': 80,
'host': '101.35.212.35',
'param_in_url': {
'dn': ['PAYLOAD', 2, 3],
'ttl': 1,
'x': {
'a': {
'b': 'y'
}
},
'id': 25196
}
}, {
'headers': {
'Accept': '*/*',
'PDD-CONFIG': 'V4:002.059900',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ',
'Connection': 'close',
'Host': '101.35.212.35',
'Accept-Encoding': 'gzip, deflate',
'vip': '101.35.212.35'
},
'method': 'GET',
'full_url': 'http://101.35.212.35:80/d?id=25196&ttl=1&dn=[1,2,3]&x={"a":{"b":"y"}}',
'param_in_body': {},
'body': '{}',
'protocol': 'http',
'content_type': 0,
'port': 80,
'host': '101.35.212.35',
'param_in_url': {
'dn': [1, 'PAYLOAD', 3],
'ttl': 1,
'x': {
'a': {
'b': 'y'
}
},
'id': 25196
}
}, {
'headers': {
'Accept': '*/*',
'PDD-CONFIG': 'V4:002.059900',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ',
'Connection': 'close',
'Host': '101.35.212.35',
'Accept-Encoding': 'gzip, deflate',
'vip': '101.35.212.35'
},
'method': 'GET',
'full_url': 'http://101.35.212.35:80/d?id=25196&ttl=1&dn=[1,2,3]&x={"a":{"b":"y"}}',
'param_in_body': {},
'body': '{}',
'protocol': 'http',
'content_type': 0,
'port': 80,
'host': '101.35.212.35',
'param_in_url': {
'dn': [1, 2, 'PAYLOAD'],
'ttl': 1,
'x': {
'a': {
'b': 'y'
}
},
'id': 25196
}
}, {
'headers': {
'Accept': '*/*',
'PDD-CONFIG': 'V4:002.059900',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ',
'Connection': 'close',
'Host': '101.35.212.35',
'Accept-Encoding': 'gzip, deflate',
'vip': '101.35.212.35'
},
'method': 'GET',
'full_url': 'http://101.35.212.35:80/d?id=25196&ttl=1&dn=[1,2,3]&x={"a":{"b":"y"}}',
'param_in_body': {},
'body': '{}',
'protocol': 'http',
'content_type': 0,
'port': 80,
'host': '101.35.212.35',
'param_in_url': {
'dn': [1, 2, 3],
'ttl': 1,
'x': {
'a': {
'b': 'PAYLOAD'
}
},
'id': 25196
}
}]
http重放所需的元素生成完成就需要进行重放,这里采用了 requests 库。
def assemble_parameter(self, d):
"""
组装参数为字符串
"""
return '&'.join([k if v is None else '{0}={1}'.format(k, json.dumps(v, separators=(',', ':')) if isinstance(v, (dict,list)) else v) for k, v in d.items()])
def sendGetRequest(self, url, p, h, protocol):
"""
发送get请求数据
:param url: url
:param p: get参数
:param h: 请求头
:param protocol: http or https
:return:
"""
if self.use_proxy == 'YES':
if protocol == 'https':
return requests.get(url=self.parseUrl(url), params=self.assemble_parameter(p), headers=h, proxies=self.proxy, verify=False,
allow_redirects=self.redirect)
else:
return requests.get(url=self.parseUrl(url), params=self.assemble_parameter(p), headers=h, proxies=self.proxy,
allow_redirects=self.redirect)
else:
if protocol == 'https':
return requests.get(url=self.parseUrl(url), params=self.assemble_parameter(p), headers=h, verify=False, allow_redirects=self.redirect)
else:
return requests.get(url=self.parseUrl(url), params=self.assemble_parameter(p), headers=h, allow_redirects=self.redirect)
def sendPostRequest(self, url, p, d, h, protocol):
"""
发送post请求
:param url: url
:param p: get参数
:param d: post data
:param h: 请求头
:param protocol: http or https
:return:
"""
if self.use_proxy == 'YES':
if protocol == 'https':
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=self.assemble_parameter(d), headers=h, proxies=self.proxy, verify=False,
allow_redirects=self.redirect)
else:
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=self.assemble_parameter(d), headers=h, proxies=self.proxy,
allow_redirects=self.redirect)
else:
if protocol == 'https':
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=self.assemble_parameter(d), headers=h, verify=False,
allow_redirects=self.redirect)
else:
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=self.assemble_parameter(d), headers=h, allow_redirects=self.redirect)
def sendPostJsonRequest(self, url, p, d, h, protocol):
"""
发送 application/json 数据
:param url:
:param p:
:param d:
:param h:
:param protocol:
:return:
"""
if self.use_proxy == 'YES':
if protocol == 'https':
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=json.dumps(d, separators=(',', ':')), headers=h, proxies=self.proxy,
verify=False, allow_redirects=self.redirect)
else:
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=json.dumps(d, separators=(',', ':')), headers=h, proxies=self.proxy,
allow_redirects=self.redirect)
else:
if protocol == 'https':
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=json.dumps(d, separators=(',', ':')), headers=h, verify=False,
allow_redirects=self.redirect)
else:
return requests.post(url=self.parseUrl(url), params=self.assemble_parameter(p), data=json.dumps(d, separators=(',', ':')), headers=h,
allow_redirects=self.redirect)
def processRequest(self, request_data):
"""
重放http/s 数据
:param request_data:
:return:
"""
h = self.pop_black_headers(request_data['headers'])
protocol = request_data['protocol']
method = request_data['method']
content_type = request_data['content_type']
url = request_data['full_url']
param_in_url = request_data['param_in_url']
param_in_body = request_data['param_in_body']
body = request_data['body']
if method == 'GET' and param_in_url:
return self.sendGetRequest(url, param_in_url, h, protocol)
elif method == 'POST' and content_type == 1:
return self.sendPostRequest(url, param_in_url, param_in_body, h, protocol)
elif method == 'POST' and content_type == 4:
return self.sendPostJsonRequest(url, param_in_url, body, h, protocol)
注意在重放前需要 忽略一些请求头和自定义忽略参数
content-length
if-modified-since
if-none-match
pragma
cache-control
SQL注入识别
注入可分为 报错注入、盲注,由于现在waf比较多,所以考虑用尽量不触发waf的基础上来进行探测注入。
这里主要探讨盲注的探测方式。
这里使用了余弦相似度算法
self.bool_str_tuple = ('\'', '\'\'')
self.bool_str_tuple_second = ("'||'x", "'||'")
self.bool_str_tuple_third = ("'+'x", "'+'") if self.content_type == 4 else ("'%2b'x", "'%2b'")
self.bool_int_tuple = ('-x', '-0', '-false')
self.bool_order_tuple = (",1-x", ",1",",true")
1、页面不存在随机值的时候
- str 类型注入判断流程
- int类型注入判断流程
- order by 类型注入判断流程
2、 页面存在随机值干扰
可参考:https://mp.weixin.qq.com/s/iX8_C53QKGCL0XjqdrqbPQ,会存在一定误报和漏报。
SSRF漏洞探测
这个比较简单批量替换参数为dnslog地址。
有时候dnslog有延时,所以我们可考虑将ssrf探测请求全加入到数据库。
生成唯一的ssrf地址
def generate_uuid(self):
"""
生成唯一字符串
:return:
"""
return ''.join(str(uuid.uuid4()).split('-'))[0:10]
def generate_ssrf_payload(self, s):
"""
生成SSRF dnslog 域名
:return:
"""
poc = self.generate_uuid() + '.'+ s + '.' + self.ssrfpayload
self.ssrf_list.append(poc)
return "http://" + poc
socket 服务端接受请求
class MyUDPServer(ThreadingMixIn, UDPServer):
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, queue=None):
self.queue = queue
UDPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate=bind_and_activate)
class MyUDPHandler(socketserver.BaseRequestHandler):
def __init__(self, request, client_address, server):
self.queue = server.queue
BaseRequestHandler.__init__(self, request, client_address, server)
def parse(self,p):
x = {}
for k,v in p.items():
try:
v1 = json.loads(v)
except:
v1 = v
x[k] = v1
return x
def handle(self): # 必须要有handle方法;所有处理必须通过handle方法实现
# self.request is the Udp socket connected to the client
self.data = self.request[0].strip()
data_dict = eval(self.data.decode('utf-8'))
data_dict['param_in_url'] = self.parse(data_dict['param_in_url'])
data_dict['param_in_body'] = self.parse(data_dict['param_in_body'])
self.queue.put(data_dict)
if __name__ == "__main__":
logger = CommonLog(__name__).getlog()
HOST, PORT = "127.0.0.1", 8883
queue = queue.Queue()
model = CosineSimilarity()
server = MyUDPServer((HOST, PORT), MyUDPHandler, queue=queue) # 实例化一个多线程UDPServer
server.max_packet_size = 8192 * 20
# Start the server
SERVER_THREAD = threading.Thread(target=server.serve_forever)
SERVER_THREAD.daemon = True
SERVER_THREAD.start()
logger.info('----- udp server start at 127.0.0.1:8083 ----')
while True:
while not queue.empty():
data = queue.get()
http = HttpWappalyzer()
content_type = data['content_type']
sqlbool = SQLBool(http, model, content_type)
sqlboolThread = threading.Thread(target=sqlbool.scan, args=(copy.deepcopy(data),))
sqlboolThread.start()
sqlboolThread.join()
使用 https://www.vulnspy.com/dvwa-wooyun/ 靶场进行测试,基本能探测出所有的SQL注入漏洞。
参考
- https://github.com/w-digital-scanner/w13scan/ W13Scan
- https://mp.weixin.qq.com/s/iX8_C53QKGCL0XjqdrqbPQ SQL注入点检测-文本内容相似度