├── Chrome_headless_xss.py ├── README.md ├── requirements.txt └── url_location.py /Chrome_headless_xss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2/7/18 下午3:29 4 | # @Author : fz 5 | # @Site : 6 | # @File : xss.py 7 | # @Software: PyCharm 8 | 9 | 10 | import json 11 | import requests 12 | import websocket 13 | import time 14 | 15 | # 递归解析json 16 | 17 | def dict_generator(indict, pre=None): 18 | pre = pre[:] if pre else [] 19 | if isinstance(indict, dict): 20 | for key, value in indict.items(): 21 | if isinstance(value, dict): 22 | if len(value) == 0: 23 | yield pre+[key, '{}'] 24 | else: 25 | for d in dict_generator(value, pre + [key]): 26 | yield d 27 | elif isinstance(value, list): 28 | if len(value) == 0: 29 | yield pre+[key, '[]'] 30 | else: 31 | dict_flag = 0 32 | for v in value: 33 | if isinstance(v, dict): 34 | dict_flag = 1 35 | if dict_flag ==1: 36 | for v in value: 37 | for d in dict_generator(v, pre + [key]): 38 | yield d 39 | 40 | else: 41 | yield pre+[key, value] 42 | else: 43 | yield pre + [key, value] 44 | else: 45 | yield indict 46 | 47 | # 解析json获得包含payload的node_id 48 | 49 | def get_node_info(root, target): 50 | tmp = [] 51 | 52 | def get_node_id(root, target, tmp): 53 | node_value = root.get('nodeValue') 54 | if target not in node_value: 55 | if 'children' in root: 56 | children = root.get('children') 57 | if children: 58 | for item in children: 59 | get_node_id(item, target=target, tmp=tmp) 60 | else: 61 | node_id = root.get('nodeId') 62 | node_backend_id = root.get('backendNodeId') 63 | dict_tmp = {"node_value": node_value, "node_id": node_id, "node_backend_id": node_backend_id} 64 | tmp.append(dict_tmp) 65 | 66 | get_node_id(root, target, tmp=tmp) 67 | 68 | return tmp 69 | 70 | 71 | 72 | 73 | class ChromeHeadLess(object): 74 | def __init__(self, url, ip="127.0.0.1", port="9222", cookie="", post="", auth="", payload="", check_message=""): 75 | 76 | self.url = url 77 | self.cookie = cookie 78 | self.post = post 79 | self.auth = auth 80 | self.ip = ip 81 | self.port = port 82 | self.tab_id = "" 83 | self.ws_url = "" 84 | self.hook_urls = [] 85 | self.error = "" 86 | self.soc = None 87 | self.javascript_dialog_events = [] 88 | self.payload = payload 89 | self.dom_result = [] 90 | self.check_message =check_message 91 | self.request_id = [] 92 | self.response_body = [] 93 | chrome_web = "http://%s:%s/json/new" % (ip, port) 94 | try: 95 | response = requests.get(chrome_web) 96 | self.ws_url = response.json().get("webSocketDebuggerUrl") 97 | self.tab_id = response.json().get("id") 98 | self.soc = websocket.create_connection(self.ws_url) 99 | self.soc.settimeout(2) 100 | except Exception, e: 101 | self.error = str(e) 102 | 103 | def close_tab(self): 104 | """ 105 | 关闭tab 106 | :return: 107 | """ 108 | try: 109 | requests.get("http://%s:%s/json/close/%s" % (self.ip, self.port, self.tab_id)) 110 | except Exception, e: 111 | #print "ERROR:%s" % e 112 | self.error = str(e) 113 | 114 | def get_tab_list(self): 115 | ''' 116 | 获取当前teb_list 117 | :return: 118 | ''' 119 | 120 | try: 121 | response = requests.get("http://%s:%s/json" % (self.ip, self.port)) 122 | tem_str = response.content 123 | table_list = json.loads(tem_str) 124 | except Exception, e: 125 | self.error = str(e) 126 | 127 | return table_list 128 | 129 | def send_msg(self, id, method, params): 130 | """ 131 | 给ChromeHeadless的server 发执行命令 132 | :param id: 133 | :param method: 134 | :param params: 135 | :return: 136 | """ 137 | navcom = json.dumps({ 138 | "id": id, 139 | "method": method, 140 | "params": params 141 | }) 142 | self.soc.send(navcom) 143 | 144 | # 对插入的自定义标签进行判断,如果存在则判断存在level2等级的xss 145 | 146 | def level_2_check(self, result_json, result_list): 147 | 148 | tmp_json = result_json 149 | 150 | for i in dict_generator(tmp_json): 151 | if i[-2] == 'localName' and i[-1] == 'webscan': 152 | 153 | if len(result_list) != 0: 154 | if result_list[0]['level'] != '3': 155 | result_list[0]['vul'] = 'xss' 156 | result_list[0]['level'] = '2' 157 | else: 158 | pass 159 | 160 | # 判断payload是否存在dom树的nodeValue中 161 | def node_value_check(self, result_json, payload): 162 | flag =False 163 | tmp_json = result_json 164 | for i in dict_generator(tmp_json): 165 | if i[-2] == 'nodeValue' and payload in i[-1]: 166 | flag = True 167 | return flag 168 | 169 | 170 | 171 | 172 | # 对payload最后在html渲染后的结果,通过outHtml的结果判断是否在服务端进行了实体编码 173 | 174 | def level_1_check(self, payload, result_list): 175 | out_time = 4 176 | start_time = time.time() 177 | while (time.time() - start_time) < out_time: 178 | try: 179 | result = self.soc.recv() 180 | result_json = dict(json.loads(result)) 181 | if '''"id":2324''' in result: 182 | node_id_list =[] 183 | tmp_node_id = get_node_info(result_json['result']['root'], self.payload) 184 | for item in tmp_node_id: 185 | node_id_list.append(item['node_id']) 186 | print tmp_node_id 187 | for item in node_id_list: 188 | node_id = item 189 | self.send_msg(id=2325, method="DOM.getOuterHTML", params={'nodeId': node_id}) 190 | 191 | if '''"id":2325''' in result: 192 | print result 193 | if payload in result_json['result']['outerHTML']: 194 | result_list[0]['vul'] = 'xss' 195 | result_list[0]['level'] = '1' 196 | else: 197 | pass 198 | except Exception, e: 199 | self.error = e 200 | print self.error 201 | 202 | 203 | 204 | 205 | 206 | def get_chrome_msg(self): 207 | """ 208 | 循环监听 209 | :return: 210 | """ 211 | # 对整体请求设置最大延迟时间, 212 | out_time = 4 213 | start_time = time.time() 214 | 215 | while (time.time() - start_time) < out_time: 216 | try: 217 | result = self.soc.recv() 218 | result_json = dict(json.loads(result)) 219 | 220 | if "Network.requestWillBeSent" in result: 221 | # hook url 222 | if result_json["params"]["request"]["url"] not in self.hook_urls: 223 | if "postData" in result_json["params"]["request"]: 224 | post = result_json["params"]["request"]["postData"] 225 | else: 226 | post = "" 227 | self.hook_urls.append({ 228 | "url": result_json["params"]["request"]["url"], 229 | "method": result_json["params"]["request"]["method"], 230 | "post": post, 231 | "vul": "", 232 | "level": "0" 233 | }) 234 | self.request_id.append(result_json["params"]['requestId']) 235 | # self.send_msg() 236 | 237 | # 如果第一个返回包的content-type是json,则直接判断没有漏洞(忽略特定版本ie的情况) 238 | 239 | if "Network.responseReceived" in result: 240 | self.response_body.append(result_json['params']['response']) 241 | if 'application/json' in self.response_body[0]['headers']["Content-Type"]: 242 | break 243 | 244 | elif "Page.javascriptDialogOpening" in result: 245 | # hook alert 246 | if result_json["params"] not in self.javascript_dialog_events: 247 | self.javascript_dialog_events.append(result_json["params"]) 248 | 249 | if result_json["params"]["message"] == self.check_message: 250 | if result_json["params"]["url"] != 'about:blank': 251 | for item in self.hook_urls: 252 | if item["url"] in result_json['params']['url']: 253 | item["vul"] = "xss" 254 | item["level"] = "3" 255 | break 256 | else: 257 | self.hook_urls[0]['vul']= "xss" 258 | self.hook_urls[0]['level'] = "3" 259 | break 260 | 261 | elif "Page.domContentEventFired" in result: 262 | # dom加载完以后 执行on事件的javascript 263 | self.send_msg(id=2323, method="Runtime.evaluate", 264 | params={"expression": "\nvar nodes = document.all;" 265 | "\nfor(var i=0;i chromeheadless.out 2>&1 & 32 | ``` 33 | chrome_headless_xss 34 | ``` 35 | # tmp_url为添加payload的url,如果是post请求则为原始url 36 | chrome_headless_drive = ChromeHeadLess(url=tmp_url, 37 | ip="127.0.0.1", 38 | port="9222", 39 | cookie="", 40 | post="", 41 | auth="", 42 | payloads= payload 43 | #添加监听弹窗内容 44 | check_message = "you_alert_message") 45 | scan_result = chrome_headless_drive.run() 46 | ``` 47 | scan_result结果: 48 | ``` 49 | # level 3 代表触发了Page.javascriptDialogOpening事件 50 | {'url': u'http://xss.php', 'vul': 'xss', 'post': '', 'method': u'GET', 'level': '3'} 51 | # level 2 代表dom树的节点包含了我们自定义的标签 52 | {'url': u'http://xss.php', 'vul': 'xss', 'post': '', 'method': u'GET', 'level': '2'} 53 | # level 1 代表渲染后的nodeValue包含我们的payload 54 | {'url': u'http://xss.php', 'vul': 'xss', 'post': u'id1=1&id2=2test_test', 'method': u'POST', 'level': '1'} 55 | ``` 56 | 源码链接: 57 | ``` 58 | https://github.com/neverlovelynn/chrome_headless_xss/ 59 | ``` 60 | 文章链接: 61 | ``` 62 | https://blog.formsec.cn/2018/07/12/%E5%9F%BA%E4%BA%8EChrome-headless%E7%9A%84XSS%E6%A3%80%E6%B5%8B/ 63 | ``` 64 | tips: 65 | 由于最新版本的chrome69在linux下会存在一个 lost ui context的error,所以推荐使用chrome64的稳定版本。历史安装包连接: 66 | https://www.slimjet.com/chrome/google-chrome-old-version.php 67 | 有时候也会存在chrome崩溃的情况,也可以使用supervise创建守护进程自动重启。 68 | 69 | 70 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client==0.48.0 2 | requests==2.18.4 3 | 4 | -------------------------------------------------------------------------------- /url_location.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2/7/18 下午3:29 4 | # @Author : fz 5 | # @Site : 6 | # @File : url_location.py 7 | # @Software: PyCharm 8 | 9 | 10 | import json 11 | import requests 12 | import websocket 13 | import time 14 | 15 | 16 | def check_url_location(result_list, payload): 17 | 18 | list_len = len(result_list) 19 | tmp_list = result_list 20 | i = 1 21 | if list_len != 0: 22 | while (i