├── README.md ├── WechatIMG3968.jpeg ├── struts2_hunt.py ├── struts2_hunt_v2.py └── 说明.txt /README.md: -------------------------------------------------------------------------------- 1 | # struts2_check 2 | 3 | 一个用于识别目标网站是否采用Struts2框架开发的工具Demo 4 | 5 | 这个工具来自 n1nty 大佬提供的方法,出于兴趣尝试实现一下Demo程序。 6 | 7 | ### 原理方法来自: 8 | https://threathunter.org/topic/594a9f0fde1d70c20885ccd5 9 | 10 | ### 备注: 11 | ThreatHunter: 一个专注于高级威胁发现与安全数据分析的社区 12 | 13 | ### 使用: 14 | python struts2_check.py http://www.demo.com/ 15 | -------------------------------------------------------------------------------- /WechatIMG3968.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coffeehb/struts2_check/aad59d1678162a8f0b8ed26da829439b8eafedaa/WechatIMG3968.jpeg -------------------------------------------------------------------------------- /struts2_hunt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2017/6/23 下午1:54 4 | # @Author : Komi 5 | # @File : struts2_hunt.py 6 | # @Project : POC-T 7 | # @Ver: : 0.2 8 | # Referer https://threathunter.org/topic/594a9f0fde1d70c20885ccd5 9 | 10 | import time 11 | import re 12 | import sys 13 | import urlparse 14 | import httplib, urllib, urllib2 15 | 16 | ERROR_KEYS = ['Struts Problem Report','org.apache.struts2','struts.devMode','struts-tags', 17 | 'There is no Action mapped for namespace'] 18 | 19 | # check suffix :.do,.action 20 | def checkBySuffix(info): 21 | if info['code'] == 404: 22 | return False 23 | html = info['html'] 24 | matchs_action = re.findall(r"""(['"]{1})(/?((?:(?!\1|\n|http(s)?://).)+)\.action)(\?(?:(?!\1).)*)?\1""", html, 25 | re.IGNORECASE) 26 | 27 | matchs_do = re.findall(r"""(['"]{1})(/?((?:(?!\1|\n|http(s)?://).)+)\.do)(\?(?:(?!\1).)*)?\1""", html, 28 | re.IGNORECASE) 29 | 30 | if len(matchs_do)+len(matchs_action)> 0 and (".action" in str(matchs_action) or ".do" in str(matchs_do)): 31 | return True 32 | else: 33 | return False 34 | 35 | # check devMode page 36 | def checkDevMode(url): 37 | target_url = url+"/struts/webconsole.html" 38 | info = gethtml(target_url) 39 | 40 | if info['code'] == 200 and "Welcome to the OGNL console" in info['html']: 41 | return True 42 | else: 43 | return False 44 | 45 | # check Error Messages. 46 | def checActionsErrors(url): 47 | test_tmpurls = [] 48 | 49 | test_tmpurls.append(url+"/?actionErrors=1111") 50 | test_tmpurls.append(url+"/tmp2017.action") 51 | test_tmpurls.append(url + "/tmp2017.do") 52 | test_tmpurls.append(url + "/system/index!testme.action") 53 | test_tmpurls.append(url + "/system/index!testme.do") 54 | 55 | for test_url in test_tmpurls: 56 | info = gethtml(test_url) 57 | for error_message in ERROR_KEYS: 58 | if error_message in info['html'] and info['code'] == 500: 59 | print "[+] found error_message:",error_message 60 | return True 61 | return False 62 | 63 | # check CheckboxInterceptor. 64 | def checkCheckBox(url): 65 | # url = "https://www.vuln.org/?keyword=aaa&loginname=admin&password=888" 66 | """ 67 | https://www.vuln.org/?__checkbox_keyword=aaa&loginname=admin&password=888 68 | https://www.vuln.org/?keyword=aaa&__checkbox_loginname=admin&password=888 69 | https://www.vuln.org/?keyword=aaa&loginname=admin&__checkbox_password=888 70 | em: 71 | http://wsbs.wgj.sh.gov.cn/shwgj_zwdt/core/web/welcome/index!search.action 72 | 73 | """ 74 | for match in re.finditer(r"((\A|[?&])(?P[^_]\w*)=)(?P[^&#]+)", url): 75 | 76 | info = gethtml(url.replace(match.group('parameter'), "__checkbox_"+match.group('parameter'))) 77 | check_key = 'name="{}"'.format(match.group('parameter')) 78 | check_value = 'value="false"' 79 | 80 | html = info['html'] 81 | matchs_inputTags = re.findall(r"""<\s*input[^>]*>""", html,re.IGNORECASE) 82 | for input_tag in matchs_inputTags: 83 | if check_key in input_tag and check_value in input_tag: 84 | return True 85 | 86 | return False 87 | # 给 2 个测试站,没有找到好的实现思路 88 | # 初步想法: 对比三次请求返回的文本大小差异,超时请求况,文本是否保护request_locale来做决策 89 | # 90 | # https://eservices.customs.gov.hk/MSOS/wsrh/001s0?request_locale=en_US 91 | # https://ctc.camds.org/camds/mainpage.action?request_locale=zh_CN 92 | # https://ctc.camds.org/camds/mainpage.action?request_locale=en_US 93 | # http://www.quamnet.com/newsUScontent.action?request_locale=zh_CN&articleId=3436914 94 | 95 | def checkl18n(target): 96 | 97 | info_orgi = gethtml(target) 98 | time.sleep(0.5) 99 | info_zhCN = gethtml(target+"?"+'request_locale=zh_CN') 100 | time.sleep(0.5) 101 | info_enUS = gethtml(target+"?"+ 'request_locale=en_US') 102 | time.sleep(0.5) 103 | 104 | if "request_locale=zh_CN" in info_orgi['html'] and "request_locale=en_US" in info_orgi['html']: 105 | return True 106 | 107 | if abs(len(info_zhCN['html']) - len(info_enUS['html'])) > 1024: 108 | return True 109 | 110 | return False 111 | 112 | def gethtml(url): 113 | try: 114 | request = urllib2.Request(url) 115 | request.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0') 116 | request.add_header('Accept-Language', 'en-us;q=0.5,en;q=0.3') 117 | request.add_header('Referer', request.get_full_url()) 118 | u = urllib2.urlopen(request , timeout = 3) 119 | content = u.read() 120 | try: 121 | content = content.encode("utf-8") 122 | except: 123 | content = content.decode('gbk','ignore').encode("utf-8",'ignore') 124 | return {"html":content,"code":u.code,"url":u.geturl()} 125 | except urllib2.HTTPError,e: 126 | try: 127 | return {"html":e.read(),"code":e.code,"url":e.geturl()} 128 | except: 129 | return {"html":'',"code":e.code,"url":e.geturl()} 130 | except: 131 | return {"html":"","code":404, "url":url} 132 | 133 | def poc(target): 134 | if not target.lower().startswith('http://') and not target.lower().startswith('https://'): 135 | target = 'http://' + target 136 | 137 | target = urlparse.urlparse(target).scheme + "://" + urlparse.urlparse(target).netloc 138 | 139 | html = gethtml(target) 140 | 141 | 142 | if checkDevMode(target): 143 | return "[success] %s is struts2! [checkDevMode]" % target 144 | 145 | if checkBySuffix(html): 146 | return "[success] %s is struts2! [checkBySuffix]" % target 147 | 148 | if checActionsErrors(target): 149 | return "[success] %s is struts2! [checActionsErrors]" % target 150 | 151 | if checkCheckBox(target): 152 | return "[success] %s is struts2! [checkCheckBox]" % target 153 | 154 | if checkl18n(target): 155 | return "[success] %s is struts2! [checkl18n]" % target 156 | 157 | return False 158 | 159 | if __name__ == "__main__": 160 | if len(sys.argv) > 1: 161 | result = poc(sys.argv[1]) 162 | if not result: 163 | print "[*] %s is not struts2!" % sys.argv[1] 164 | else: 165 | print result 166 | else: 167 | print "\n[*]usag: python poc.py http://www.demo.com" 168 | -------------------------------------------------------------------------------- /struts2_hunt_v2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/01/23 下午2:14 4 | # @Author : Komi 5 | # @File : struts2_hunt.py 6 | # @Project : POC-T 7 | # @Ver: : 0.4 8 | # Referer https://threathunter.org/topic/594a9f0fde1d70c20885ccd5 9 | 10 | import time 11 | import re 12 | import sys 13 | import requests 14 | import platform 15 | 16 | ERROR_KEYS = ['Struts Problem Report','org.apache.struts2','struts.devMode','struts-tags', 17 | 'There is no Action mapped for namespace'] 18 | 19 | # check suffix :.do,.action 20 | def checkBySuffix(info): 21 | if info['code'] == 404: 22 | return False 23 | html = info['html'] 24 | matchs_action = re.findall(r"""(['"]{1})(/?((?:(?!\1|\n|http(s)?://).)+)\.action)(\?(?:(?!\1).)*)?\1""", html, 25 | re.IGNORECASE) 26 | 27 | matchs_do = re.findall(r"""(['"]{1})(/?((?:(?!\1|\n|http(s)?://).)+)\.do)(\?(?:(?!\1).)*)?\1""", html, 28 | re.IGNORECASE) 29 | 30 | if len(matchs_do)+len(matchs_action)> 0 and (".action" in str(matchs_action) or ".do" in str(matchs_do)): 31 | return True 32 | else: 33 | return False 34 | 35 | # check devMode page 36 | def checkDevMode(url): 37 | target_url = url+"/struts/webconsole.html" 38 | info = gethtml(target_url) 39 | 40 | if info['code'] == 200 and "Welcome to the OGNL console" in info['html']: 41 | return True 42 | else: 43 | return False 44 | 45 | # check Error Messages. 46 | def checActionsErrors(url): 47 | test_tmpurls = [] 48 | 49 | test_tmpurls.append(url+"/?actionErrors=1111") 50 | test_tmpurls.append(url+"/tmp2017.action") 51 | test_tmpurls.append(url + "/tmp2017.do") 52 | test_tmpurls.append(url + "/system/index!testme.action") 53 | test_tmpurls.append(url + "/system/index!testme.do") 54 | 55 | for test_url in test_tmpurls: 56 | info = gethtml(test_url) 57 | for error_message in ERROR_KEYS: 58 | if error_message in info['html'] and info['code'] == 500: 59 | print ("[+] found error_message:",error_message) 60 | return True 61 | return False 62 | 63 | # check CheckboxInterceptor. 64 | def checkCheckBox(url): 65 | # url = "https://www.vuln.org/?keyword=aaa&loginname=admin&password=888" 66 | """ 67 | https://www.vuln.org/?__checkbox_keyword=aaa&loginname=admin&password=888 68 | https://www.vuln.org/?keyword=aaa&__checkbox_loginname=admin&password=888 69 | https://www.vuln.org/?keyword=aaa&loginname=admin&__checkbox_password=888 70 | em: 71 | http://wsbs.wgj.sh.gov.cn/shwgj_zwdt/core/web/welcome/index!search.action 72 | 73 | """ 74 | for match in re.finditer(r"((\A|[?&])(?P[^_]\w*)=)(?P[^&#]+)", url): 75 | 76 | info = gethtml(url.replace(match.group('parameter'), "__checkbox_"+match.group('parameter'))) 77 | check_key = 'name="{}"'.format(match.group('parameter')) 78 | check_value = 'value="false"' 79 | 80 | html = info['html'] 81 | matchs_inputTags = re.findall(r"""<\s*input[^>]*>""", html,re.IGNORECASE) 82 | for input_tag in matchs_inputTags: 83 | if check_key in input_tag and check_value in input_tag: 84 | return True 85 | 86 | return False 87 | # 给 2 个测试站,没有找到好的实现思路 88 | # 初步想法: 对比三次请求返回的文本大小差异,超时请求况,文本是否保护request_locale来做决策 89 | # 90 | # https://eservices.customs.gov.hk/MSOS/wsrh/001s0?request_locale=en_US 91 | # https://ctc.camds.org/camds/mainpage.action?request_locale=zh_CN 92 | # https://ctc.camds.org/camds/mainpage.action?request_locale=en_US 93 | # http://www.quamnet.com/newsUScontent.action?request_locale=zh_CN&articleId=3436914 94 | 95 | def checkl18n(target): 96 | 97 | info_orgi = gethtml(target) 98 | time.sleep(0.5) 99 | info_zhCN = gethtml(target+"?"+'request_locale=zh_CN') 100 | time.sleep(0.5) 101 | info_enUS = gethtml(target+"?"+ 'request_locale=en_US') 102 | time.sleep(0.5) 103 | 104 | if "request_locale=zh_CN" in info_orgi['html'] and "request_locale=en_US" in info_orgi['html']: 105 | return True 106 | 107 | if abs(len(info_zhCN['html']) - len(info_enUS['html'])) > 1024: 108 | return True 109 | 110 | return False 111 | 112 | def gethtml(url): 113 | try: 114 | headers = {} 115 | 116 | headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0' 117 | headers['Accept-Language'] ='en-us;q=0.5,en;q=0.3' 118 | headers['Referer'] = url 119 | 120 | u = requests.get(url, timeout=3, headers=headers, allow_redirects=True) 121 | content = u.text 122 | return {"html":content,"code":u.status_code,"url":url} 123 | 124 | except Exception as e: 125 | return get_html_phantomJS(url) 126 | 127 | # 使用PhantomJS获取网页源码 128 | def get_html_phantomJS(url): 129 | try: 130 | from selenium import webdriver 131 | dr = webdriver.PhantomJS() 132 | dr.get(url) 133 | time.sleep(2) 134 | return {"html": dr.page_source, "code": 200, "url": url} 135 | 136 | except Exception as e: 137 | # http://phantomjs.org/ 138 | print (e) 139 | return {"html":"", "code":500, "url":url} 140 | 141 | 142 | def poc(target): 143 | ori_target = target 144 | 145 | if not target.lower().startswith('http://') and not target.lower().startswith('https://'): 146 | target = 'http://' + target 147 | 148 | p_version = platform.python_version() 149 | 150 | if "2.7" in p_version: 151 | from urlparse import urlparse 152 | elif "3.5" in p_version: 153 | from urllib.parse import urlparse 154 | 155 | target = urlparse(target).scheme + "://" + urlparse(target).netloc 156 | 157 | index_html = gethtml(target) 158 | ori_html = gethtml(ori_target) 159 | 160 | if checkDevMode(target): 161 | return "[success] %s is struts2! [checkDevMode]" % target 162 | 163 | if checkBySuffix(index_html): 164 | return "[success] %s is struts2! [checkBySuffix]" % target 165 | 166 | 167 | if checkBySuffix(ori_html): 168 | return "[success] %s is struts2! [checkBySuffix]" % ori_target 169 | 170 | if checActionsErrors(target): 171 | return "[success] %s is struts2! [checActionsErrors]" % target 172 | 173 | if checkCheckBox(target): 174 | return "[success] %s is struts2! [checkCheckBox]" % target 175 | 176 | if checkl18n(target): 177 | return "[success] %s is struts2! [checkl18n]" % target 178 | 179 | return False 180 | 181 | if __name__ == "__main__": 182 | if len(sys.argv) > 1: 183 | result = poc(sys.argv[1]) 184 | if not result: 185 | print("[*] %s is not struts2!" % sys.argv[1]) 186 | else: 187 | print(result) 188 | else: 189 | print("\n[*]usag: python poc.py http://www.demo.com") 190 | -------------------------------------------------------------------------------- /说明.txt: -------------------------------------------------------------------------------- 1 | # 一个识别目标是否基于 Struts2 构建的工具Demo 2 | 3 | 这个工具来自 n1nty 大佬提供的方法,出于兴趣尝试实现一下Demo程序。 4 | 5 | 原理方法来自: 6 | https://threathunter.org/topic/594a9f0fde1d70c20885ccd5 7 | 8 | 备注: 9 | ThreatHunter: 一个专注于高级威胁发现与安全数据分析的社区 10 | 11 | --------------------------------------------------------------------------------