• 漏洞扫描器开发系列 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注入漏洞。


    参考

  • 相关阅读:
    java.io.IOException: Premature EOF
    springmvc集成shiro例子
    eclipse调试(debug)的时候,出现Source not found,Edit Source Lookup Path,一闪而过
    【译】Core Java Questions and Answers【1-33】
    Spring bean依赖注入、bean的装配及相关注解
    【译】Spring 4 基于TaskScheduler实现定时任务(注解)
    【译】Spring 4 + Hibernate 4 + Mysql + Maven集成例子(注解 + XML)
    【译】Spring 4 @Profile注解示例
    【译】Spring 4 @PropertySource和@Value注解示例
    【译】Spring 4 自动装配、自动检测、组件扫描示例
  • 原文地址:https://www.cnblogs.com/depycode/p/15922860.html
Copyright © 2020-2023  润新知