背景

由于手工测试过于繁琐,而且基本上常见的漏洞判断都是重复动作。

一般在渗透测试挖掘漏洞的基本流程如下:

python实现漏洞扫描器 漏洞扫描器开发_User

数据包解析

数据包解析一般包括 请求方法解析、参数解析、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

发送的数据为:

python实现漏洞扫描器 漏洞扫描器开发_python实现漏洞扫描器_02

{'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 类型注入判断流程

python实现漏洞扫描器 漏洞扫描器开发_User_03

  • int类型注入判断流程

python实现漏洞扫描器 漏洞扫描器开发_json_04

  • order by 类型注入判断流程

python实现漏洞扫描器 漏洞扫描器开发_json_05

2、 页面存在随机值干扰

可参考:https://mp.weixin.qq.com/s/iX8_C53QKGCL0XjqdrqbPQ,会存在一定误报和漏报。

python实现漏洞扫描器 漏洞扫描器开发_json_06

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

python实现漏洞扫描器 漏洞扫描器开发_json_07

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注入漏洞。

python实现漏洞扫描器 漏洞扫描器开发_python实现漏洞扫描器_08


python实现漏洞扫描器 漏洞扫描器开发_Mac_09

参考