設(shè)計)
簡介這是一款基于Python開發(fā)的XSS漏洞檢測腳本源碼面向安全測試人員、滲透測試初學(xué)者及Web安全研究者用于快速識別網(wǎng)頁中的跨站腳本注入風(fēng)險。腳本通過構(gòu)造多種XSS攻擊載荷并監(jiān)控服務(wù)器響應(yīng)與頁面行為變化判斷目標是否存在漏洞適合在授權(quán)測試場景中做初步安全排查。資源包共87個文件、約3.19MB以37個py源文件與33個pyc編譯文件為核心另有txt詞表與說明、bat與sh運行腳本、readme文檔及少量依賴壓縮包目錄中可見brutexss、mechanize、beautifulsoup等模塊痕跡便于理解檢測邏輯與請求解析流程。目前已有366人學(xué)習(xí)下載。對于希望掌握XSS檢測原理的讀者可從中獲取完整的腳本實現(xiàn)、載荷詞表、跨平臺運行方式與模塊組織思路既能直接用于安全測試練習(xí)也可作為二次開發(fā)與防御驗證的參考基礎(chǔ)。1. 從一次被反射型 XSS 打穿的登錄頁說起去年幫一個朋友看他那套剛上線的后臺登錄頁有個?redirect參數(shù)前端直接把它塞進location.href。我隨手構(gòu)造了一個帶javascript:偽協(xié)議的鏈接發(fā)過去頁面當場執(zhí)行。他一臉懵這也能算漏洞這就是 XSS 最容易被低估的地方——它不挑系統(tǒng)只挑開發(fā)者有沒有把用戶輸入當數(shù)據(jù)看。這篇要聊的是用 Python 寫一個簡單高效的 XSS 漏洞檢測腳本。注意關(guān)鍵詞是「簡單高效」和「腳本設(shè)計」不是讓你造一個替代 Burp、AWVS 的掃描器而是做一個能塞進 CI、能批量跑、能自己改 payload 的輕量工具。適合兩類人一類是做 Web 安全測試、想有個可控檢測器的從業(yè)者另一類是寫 Python 爬蟲順手想加一層安全校驗的開發(fā)者。讀完你能拿到一套可復(fù)現(xiàn)的檢測思路、一份能直接跑的源碼骨架以及幾個我踩過的坑。XSS 檢測這件事工具不是越重越好很多時候一個幾百行的腳本比裝一個幾 G 的掃描器更貼合你的場景。2. XSS 檢測腳本到底在檢測什么反射、存儲與 DOM 的判定邊界2.1 三種 XSS 在腳本視角下的差異寫檢測腳本之前得先想清楚你要檢測的是哪一類。XSS 分反射型、存儲型、DOM 型它們在腳本里的檢測邏輯完全不同混在一起寫只會讓代碼變成一鍋粥。反射型 XSS 的特征是你發(fā)出去的 payload會在同一個響應(yīng)里原樣或變形后出現(xiàn)。檢測邏輯就是「發(fā)請求 → 收響應(yīng) → 在響應(yīng)體里找 payload 的痕跡」。這是最容易自動化的一類也是腳本的主戰(zhàn)場。存儲型 XSS 的特征是payload 先被存進數(shù)據(jù)庫之后在別的頁面被讀出來。腳本沒法直接看到數(shù)據(jù)庫只能通過「提交 → 再訪問列表/詳情頁 → 檢查是否出現(xiàn)」這個兩步流程來間接判斷。難點在于你不知道數(shù)據(jù)什么時候、在哪個頁面被渲染所以存儲型檢測往往需要人工指定「回顯頁面」。DOM 型 XSS 更麻煩payload 根本不經(jīng)過服務(wù)端是前端 JS 自己把location.hash、document.referrer這類源塞進了innerHTML或eval。服務(wù)端響應(yīng)里看不到 payload傳統(tǒng)「比對響應(yīng)體」的方法直接失效。腳本層面只能做靜態(tài)分析——掃 JS 代碼里有沒有危險的 sink 函數(shù)或者用無頭瀏覽器跑一遍看有沒有彈窗。我一般會先明確腳本的定位優(yōu)先做反射型存儲型做半自動DOM 型做靜態(tài)提示。想一個腳本通吃三類最后往往三類都做不深。2.2 檢測的核心判定回顯、編碼與上下文很多人以為「響應(yīng)里出現(xiàn)了 payload」就是有漏洞這是最大的誤區(qū)。真正的判定要看三件事payload 是否回顯、回顯時是否被編碼、回顯在什么上下文里。舉個例子你發(fā)scriptalert(1)/script響應(yīng)里出現(xiàn)lt;scriptgt;alert(1)lt;/scriptgt;這是被 HTML 實體編碼了不構(gòu)成漏洞。如果出現(xiàn)的是原樣的scriptalert(1)/script那基本可以確認。但如果它出現(xiàn)在input value...的屬性里script標簽反而不觸發(fā)得換成 onmouseoveralert(1) x這種閉合屬性的 payload。所以一個靠譜的檢測腳本判定邏輯不能只看「字符串是否出現(xiàn)」還要看特殊字符 有沒有被轉(zhuǎn)義payload 落在 HTML 正文、屬性、script 塊還是 URL 里有沒有被 WAF 攔截返回 403 或空響應(yīng)下面這段是我常用的「回顯 編碼」判定核心import re def analyze_reflection(response_text, payload): 判斷 payload 在響應(yīng)中的回顯狀態(tài) 返回: (是否原樣回顯, 上下文類型) if payload not in response_text: return False, none # 找到 payload 出現(xiàn)的位置取前后文判斷上下文 idx response_text.find(payload) context response_text[max(0, idx - 50): idx len(payload) 50] # 判斷是否被 HTML 實體編碼編碼后 payload 不會原樣出現(xiàn)這里做二次確認 encoded payload.replace(, lt;).replace(, gt;) if encoded in response_text and payload not in response_text: return False, encoded # 判斷上下文 if re.search(rscript[^]*[^]* re.escape(payload), context): return True, script if re.search(r[^]\s\w\s*\s*[\][^\]* re.escape(payload), context): return True, attribute return True, html這段代碼的邏輯是先確認 payload 是否出現(xiàn)再排除被實體編碼的情況最后用正則判斷它落在 script 塊、屬性還是 HTML 正文里。context取前后 50 個字符是為了給正則足夠的匹配空間。參數(shù)上payload必須和實際發(fā)送的一致如果你發(fā)送時做了 URL 編碼這里要先解碼再比對否則永遠匹配不上。提示上下文判斷直接決定你下一步用什么 payload。落在屬性里就補 onmouseover...落在 script 塊里就試/scriptscript...別拿著一個 payload 打天下。2.3 為什么選 Python 而不是現(xiàn)成掃描器現(xiàn)成掃描器AWVS、Xray 這類能力確實強但在幾個場景下不如自己寫腳本第一可控性。掃描器的 payload 是內(nèi)置的你想加一個針對自家業(yè)務(wù)的繞過 payload得等它更新或者寫插件。自己寫腳本payload 就是一個列表想加就加。第二可集成。CI 流水線里跑一個幾百行的 Python 腳本比部署一個掃描器服務(wù)輕太多。python xss_scan.py --url xxx一行命令就能接進 Jenkins 或 GitLab CI。第三可理解。掃描器報一個漏洞你未必清楚它怎么判定的。自己寫的腳本每一行判定邏輯你都門兒清誤報漏報都能自己調(diào)。代價是你得自己處理爬蟲、參數(shù)提取、并發(fā)、去重這些臟活。所以「簡單高效」的定位很重要——不要試圖做全站爬取先支持「給定 URL 和參數(shù)」這一種輸入方式把檢測邏輯打磨好再考慮擴展。3. 用 requests BeautifulSoup 搭出可復(fù)現(xiàn)的檢測骨架3.1 環(huán)境準備與依賴選擇先把環(huán)境搭起來。Python 版本建議 3.8 以上依賴就三個requests發(fā)請求beautifulsoup4解析 HTML 提取表單和鏈接urllib.parse處理 URL 參數(shù)標準庫自帶不用裝。pip install requests beautifulsoup4如果你要檢測 DOM 型再加一個playwright但那是進階部分先不引入。這里刻意不裝selenium因為它要配瀏覽器驅(qū)動對「簡單」這個定位來說是負擔(dān)。選requests而不是httpx或aiohttp是因為它的同步模型寫起來最直觀調(diào)試也方便。檢測腳本的瓶頸通常在目標站點的響應(yīng)速度不在客戶端并發(fā)所以同步夠用。真要提速用concurrent.futures開線程池就行不必上異步。3.2 提取可測參數(shù)從 URL 和表單入手檢測的第一步是找到「可以注入的地方」。最常見的是 URL 查詢參數(shù)和 HTML 表單。下面這段負責(zé)把這兩類輸入點提取出來from urllib.parse import urlparse, parse_qs, urlencode, urlunparse from bs4 import BeautifulSoup def extract_url_params(url): 提取 URL 中的查詢參數(shù)返回 {參數(shù)名: 原值} 字典 parsed urlparse(url) params parse_qs(parsed.query) # parse_qs 返回的是列表取第一個值即可 return {k: v[0] for k, v in params.items()} def extract_forms(html, base_url): 提取頁面中所有表單的 action、method 和輸入字段 soup BeautifulSoup(html, html.parser) forms [] for form in soup.find_all(form): action form.get(action, ) method form.get(method, get).lower() # 拼接完整 action URL if action.startswith(http): full_action action else: full_action base_url.rstrip(/) / action.lstrip(/) fields {} for inp in form.find_all([input, textarea]): name inp.get(name) if name: fields[name] inp.get(value, ) forms.append({action: full_action, method: method, fields: fields}) return formsextract_url_params用parse_qs把?a1b2拆成字典注意它默認返回列表因為同名參數(shù)可能多個這里取第一個值簡化處理。extract_forms遍歷所有form把 action 拼成完整 URL再收集每個 input 的 name 和默認 value。參數(shù)說明base_url要傳當前頁面的完整地址否則相對路徑的 action 拼不出來。拿到這些輸入點后檢測邏輯就是對每個參數(shù)把原值替換成 payload重新構(gòu)造請求發(fā)出去看響應(yīng)。3.3 構(gòu)造請求與注入 payload構(gòu)造請求時有個細節(jié)GET 參數(shù)要重新編碼POST 表單要用data提交。下面這段把「替換參數(shù)值 → 發(fā)請求 → 拿響應(yīng)」封裝成一個函數(shù)import requests def inject_and_request(url, param, payload, methodget, form_dataNone): 把指定參數(shù)替換為 payload 并發(fā)送請求 method: get 或 post form_data: POST 時的完整表單數(shù)據(jù) headers { User-Agent: Mozilla/5.0 (XSS-Scanner-Test), Accept: text/html,application/xhtmlxml, } try: if method get: parsed urlparse(url) params parse_qs(parsed.query) params[param] [payload] # 替換目標參數(shù) new_query urlencode(params, doseqTrue) new_url urlunparse(parsed._replace(querynew_query)) resp requests.get(new_url, headersheaders, timeout10, verifyFalse) else: data dict(form_data or {}) data[param] payload resp requests.post(url, datadata, headersheaders, timeout10, verifyFalse) return resp except requests.RequestException as e: print(f[!] 請求失敗: {e}) return None關(guān)鍵點GET 用urlencode(params, doseqTrue)重新編碼doseqTrue保證列表值被正確展開。POST 直接改data字典。timeout10防止卡死verifyFalse是為了測自簽名證書的內(nèi)網(wǎng)站點——但生產(chǎn)環(huán)境別關(guān)證書校驗這里只是檢測場景的妥協(xié)。注意verifyFalse會帶來中間人風(fēng)險僅限內(nèi)網(wǎng)或測試環(huán)境使用。對外部站點檢測時把它去掉。3.4 判定回顯并輸出結(jié)果把前面的判定函數(shù)接上主流程就成型了PAYLOADS [ scriptalert(1)/script, \ onmouseoveralert(1) x\, img srcx onerroralert(1), javascript:alert(1), ] def scan_url(url): 掃描單個 URL 的所有查詢參數(shù) params extract_url_params(url) findings [] for param in params: for payload in PAYLOADS: resp inject_and_request(url, param, payload, methodget) if resp is None: continue reflected, context analyze_reflection(resp.text, payload) if reflected: findings.append({ url: url, param: param, payload: payload, context: context, }) print(f[] 疑似 XSS: {param} | 上下文: {context} | payload: {payload}) break # 一個參數(shù)命中就不再試其他 payload return findingsPAYLOADS列表覆蓋了 HTML 正文、屬性閉合、img onerror、偽協(xié)議四種典型場景。break的作用是一個參數(shù)只要有一個 payload 命中就不再浪費請求試其他的這是「高效」的體現(xiàn)。analyze_reflection返回的context幫你判斷下一步該用哪種 payload 深入驗證。這套骨架跑起來對反射型 XSS 的檢出率已經(jīng)不錯。但它有明顯邊界不處理 JS 動態(tài)渲染、不處理需要登錄的頁面、不處理 CSRF token。這些在下一章展開。4. 避坑與排查五個讓腳本誤報漏報的血淚經(jīng)驗4.1 現(xiàn)象明明有漏洞卻報「未發(fā)現(xiàn)」原因目標頁面需要登錄腳本沒帶 Cookie請求被重定向到登錄頁響應(yīng)里自然沒有 payload 回顯。解決加一個--cookie參數(shù)把登錄后的 Cookie 傳進來。或者用requests.Session()先模擬登錄把 session 保持住。我一般會留一個session.headers.update({Cookie: cookie_str})的入口手動貼 Cookie 最省事。4.2 現(xiàn)象報了一堆漏洞人工一看全是誤報原因判定邏輯只看「payload 是否出現(xiàn)」沒排除被編碼的情況或者 payload 出現(xiàn)在title、注釋里這種不觸發(fā)的位置。解決用 2.2 節(jié)的analyze_reflection先排除實體編碼再判斷上下文。另外script出現(xiàn)在textarea或pre里也不觸發(fā)可以在上下文判斷里加一條如果 payload 前面最近的標簽是textarea/pre/code降級為「疑似」。4.3 現(xiàn)象腳本跑著跑著卡死或超時原因目標站點響應(yīng)慢或者某個請求觸發(fā)了服務(wù)端的慢查詢requests默認沒有超時會一直等。解決所有請求強制加timeout(5, 10)即連接 5 秒、讀取 10 秒。再配合concurrent.futures.ThreadPoolExecutor加并發(fā)但線程數(shù)別超過 10否則容易把目標站打掛也容易觸發(fā) WAF。4.4 現(xiàn)象payload 被 WAF 攔截全部返回 403原因WAF 識別了script、onerror這類特征字符串。解決準備一套編碼變形的 payload比如scrscriptipt、img srcx onerroralert(1)換成img srcx onerroralert(1)JS 字符串拼接、大小寫混寫ScRiPt。但要注意變形 payload 的判定邏輯也要跟著變不能拿原始 payload 去比對響應(yīng)。4.5 現(xiàn)象存儲型 XSS 檢測時提交后不知道去哪找回顯原因存儲型的數(shù)據(jù)回顯頁面不確定可能是列表頁、詳情頁也可能是后臺。解決腳本層面加一個--verify-url參數(shù)讓使用者手動指定「提交后去哪個頁面找」。流程變成向提交接口發(fā) payload → 訪問 verify-url → 在響應(yīng)里找 payload。這是半自動方案但比盲目爬全站靠譜得多。5. 從單點檢測到批量掃描并發(fā)、去重與結(jié)果落盤5.1 用線程池把掃描速度提上來單線程逐個參數(shù)試 payload遇到參數(shù)多的站點會很慢。用ThreadPoolExecutor把「參數(shù) × payload」的任務(wù)并行化from concurrent.futures import ThreadPoolExecutor, as_completed def batch_scan(urls, max_workers8): 批量掃描多個 URL每個 URL 內(nèi)部參數(shù)串行URL 之間并行 all_findings [] with ThreadPoolExecutor(max_workersmax_workers) as executor: future_map {executor.submit(scan_url, u): u for u in urls} for future in as_completed(future_map): url future_map[future] try: findings future.result() all_findings.extend(findings) except Exception as e: print(f[!] {url} 掃描異常: {e}) return all_findingsmax_workers8是個經(jīng)驗值再高容易觸發(fā)目標站的限流。as_completed保證誰先跑完誰先處理不用等最慢的那個。注意這里 URL 之間并行單個 URL 內(nèi)部的參數(shù)還是串行——因為同一站點的請求太密集容易被封串行能降低風(fēng)險。5.2 結(jié)果去重與落盤同一個漏洞可能被多個 payload 命中需要去重。去重鍵用「URL 參數(shù)名」import json def dedup_findings(findings): 按 url param 去重保留第一個命中的 payload seen set() result [] for f in findings: key (f[url], f[param]) if key not in seen: seen.add(key) result.append(f) return result def save_findings(findings, pathxss_result.json): 結(jié)果落盤為 JSON方便后續(xù)接入報告系統(tǒng) with open(path, w, encodingutf-8) as fp: json.dump(findings, fp, ensure_asciiFalse, indent2) print(f[*] 結(jié)果已保存到 {path}共 {len(findings)} 條)落盤用 JSON 而不是 CSV是因為 findings 里有嵌套結(jié)構(gòu)payload、contextJSON 更自然。ensure_asciiFalse保證中文正常顯示。這份 JSON 可以直接喂給后續(xù)的報告生成腳本或者接進 CI 的產(chǎn)物歸檔。5.3 接入 CI 的一個最小示例把腳本包成命令行工具用argparse接收參數(shù)import argparse def main(): parser argparse.ArgumentParser(description輕量 XSS 檢測腳本) parser.add_argument(--url, help單個目標 URL) parser.add_argument(--urls-file, helpURL 列表文件每行一個) parser.add_argument(--cookie, help登錄 Cookie 字符串) parser.add_argument(--output, defaultxss_result.json, help結(jié)果輸出路徑) args parser.parse_args() urls [] if args.url: urls.append(args.url) if args.urls_file: with open(args.urls_file, encodingutf-8) as fp: urls.extend(line.strip() for line in fp if line.strip()) findings batch_scan(urls) findings dedup_findings(findings) save_findings(findings, args.output) # 有漏洞時返回非零退出碼方便 CI 判斷 if findings: exit(1) if __name__ __main__: main()exit(1)是關(guān)鍵CI 流水線靠退出碼判斷這一步是否失敗。有漏洞就返回 1流水線紅燈強制人工確認。這樣 XSS 檢測就從「想起來才跑一次」變成了「每次提交都跑」的常態(tài)化檢查。提示CI 里跑的時候把--urls-file指向一份維護好的關(guān)鍵頁面清單別全站爬。全站掃描又慢又容易誤傷聚焦登錄、搜索、評論這類高風(fēng)險的輸入點就夠了。6. 讓檢測更準的一招用無頭瀏覽器驗證 DOM 型 XSS前面整套邏輯對反射型夠用但 DOM 型 XSS 是它的盲區(qū)——payload 不進服務(wù)端響應(yīng)analyze_reflection永遠返回 False。補上這塊最實用的辦法是用 Playwright 跑一遍頁面監(jiān)聽alert彈窗。from playwright.sync_api import sync_playwright def check_dom_xss(url, payload): 用無頭瀏覽器加載頁面監(jiān)聽 alert 是否觸發(fā) triggered False with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() # 攔截 alert觸發(fā)即標記 page.on(dialog, lambda dialog: (setattr_check(), dialog.dismiss())) try: page.goto(url, timeout15000) page.wait_for_timeout(2000) # 等 JS 執(zhí)行完 except Exception as e: print(f[!] 頁面加載失敗: {e}) browser.close() return triggered def setattr_check(): global triggered triggered True這段代碼的核心是page.on(dialog, ...)Playwright 能捕獲頁面彈出的alert/confirm/prompt一旦觸發(fā)就說明 payload 執(zhí)行了。wait_for_timeout(2000)是給 JS 渲染留時間具體數(shù)值看頁面復(fù)雜度調(diào)。headlessTrue保證在服務(wù)器上無界面運行。用法上把 DOM 型檢測作為反射型檢測的補充對每個 URL先跑反射型邏輯如果沒命中再用無頭瀏覽器加載一次看有沒有彈窗。這樣兩類漏洞都能覆蓋。不過要提醒一句無頭瀏覽器很重啟動一次幾百毫秒到幾秒不適合對每個參數(shù)都跑。我一般只對「頁面里有明顯 JS 參數(shù)處理」的 URL 啟用它比如帶#錨點參數(shù)、或者 URL 里有redirect、callback這類字段的。最后說個我自己的習(xí)慣這套腳本我從來不指望它「零誤報」而是把它當成一個「篩子」——它負責(zé)把可疑的點篩出來人工再花幾分鐘確認。真正讓我放心的不是腳本報了多少而是我知道它的判定邏輯每一行在干什么誤報漏報都能自己調(diào)。XSS 檢測這件事工具的可控性比能力上限更重要。希望幫到你。本文還有配套的精品資源點擊獲取