解析)
1. 題目概覽與破題思路先交代一下背景。這道題是 RoarCTF 2019 的 Web 題后來被 BUUCTF 平臺收錄題名就叫 Easy Java難度定位不高但考的點非常典型——Java Web 應(yīng)用的配置信息泄露與源碼還原。別看它叫 Easy實際做起來還是有一些彎路的尤其是對不熟悉 Java Web 目錄結(jié)構(gòu)和 Tomcat 部署規(guī)范的選手來說容易卡在“知道要讀文件但不知道讀哪個文件”這個環(huán)節(jié)上。打開題目后環(huán)境是一個很簡潔的登錄頁面帶有用戶名和密碼輸入框底下有個登錄按鈕。老實說我一開始以為要走 SQL 注入或者弱口令的路子試了幾個常見組合都沒反應(yīng)也沒有報錯回顯。這里就有了第一個經(jīng)驗CTF 里如果登錄框看起來“太正?!蓖皇且粋€幌子真正的突破口藏在請求包里。于是我用 Burp Suite 抓了下登錄請求發(fā)現(xiàn)提交方式是 POST參數(shù)名是username和password響應(yīng)就是簡單的login failed。這看起來確實是個非常普通的登錄邏輯。我繼續(xù)看了下響應(yīng)頭發(fā)現(xiàn)服務(wù)器是 Apache Tomcat這基本坐實了題目就是圍繞 Java Web 容器展開的。再結(jié)合題名里的 Easy Java思路一下就清晰了這道題十有八九是讓選手通過 Tomcat 的目錄特性去讀WEB-INF下的配置文件再順著源碼拿到 flag。先說結(jié)論這道題的本質(zhì)是利用 POST 參數(shù)直接讀取服務(wù)器上的任意文件突破口是WEB-INF/web.xml配置文件泄露隨后下載并反編譯 class 文件從代碼邏輯中恢復(fù)出 flag。整個過程沒有復(fù)雜的攻擊鏈更像是一次“配置不當(dāng)引發(fā)的信息泄露 源碼審計”組合拳。適合什么人看剛接觸 CTF Web 方向、對 Java Web 目錄結(jié)構(gòu)不熟、或者想搞明白WEB-INF到底為什么不能直接訪問的朋友這篇應(yīng)該能幫你省不少時間。如果你已經(jīng)會做這道題也可以看看我在下文寫的一些踩坑記錄尤其是反編譯和路徑拼接那部分可能對你后續(xù)做同類題目也有幫助。2. 核心思路拆解為什么會想到讀 WEB-INF2.1 一切從 Tomcat 目錄結(jié)構(gòu)說起Tomcat 是 Java Web 最常用的容器之一它的 Web 應(yīng)用部署結(jié)構(gòu)是有規(guī)約的。一個標(biāo)準(zhǔn)的 Web 應(yīng)用目錄長這樣webapp/ ├── index.jsp ├── META-INF/ │ └── MANIFEST.MF └── WEB-INF/ ├── web.xml ├── classes/ └── lib/其中WEB-INF目錄是 Java Web 應(yīng)用的核心保護(hù)區(qū)里面放著web.xmlWeb 應(yīng)用的部署描述符定義了 servlet 映射、過濾器、監(jiān)聽器、歡迎頁面等信息相當(dāng)于整個應(yīng)用的“路由表”和“配置總綱”classes/存放編譯后的.class字節(jié)碼文件按包名分目錄存放lib/存放應(yīng)用依賴的 jar 包。按照 Java Servlet 規(guī)范WEB-INF目錄下的資源不應(yīng)該被瀏覽器直接通過 URL 訪問到。也就是說你直接訪問http://target/WEB-INF/web.xml是拿不到內(nèi)容的容器會返回 404 或訪問被拒絕。這個設(shè)計的初衷是防止攻擊者直接下載到配置文件或字節(jié)碼。但問題來了如果應(yīng)用自身提供了文件讀取功能而且沒有對路徑做過濾那WEB-INF的保護(hù)就成了擺設(shè)。攻擊者可以通過應(yīng)用自己的接口用WEB-INF/web.xml作為參數(shù)值讓服務(wù)器把這個“受保護(hù)”的文件內(nèi)容返回出來。這就好比你給保險柜加了密碼鎖結(jié)果主人把鑰匙掛在門口還告訴你“隨便拿”。2.2 這道題是怎么泄露的回到這道題。登錄頁面提交登錄時請求包長這樣POST /login HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded Content-Length: 39 usernameadminpassword123456這里有個容易忽略的細(xì)節(jié)雖然頁面只提交了username和password兩個字段但服務(wù)端接受參數(shù)時未必只處理這兩個。我用 Burp 的 Repeater 隨手加了一個參數(shù)filename試試POST /login HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded usernameadminpassword123456filenameWEB-INF/web.xml結(jié)果響應(yīng)直接返回了一段 XML 文檔開頭就是?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee看到這里我心里基本有數(shù)了——這就是經(jīng)典的WEB-INF/web.xml泄露。服務(wù)端在處理 POST 請求時不僅處理登錄邏輯還會根據(jù)filename參數(shù)去讀取服務(wù)器上的文件而且沒有限制讀取路徑。這個參數(shù)名本身也起得很直白filename正常人一眼就能猜到它的作用。2.3 為什么不直接讀 /flag可能有朋友會問既然能讀任意文件為什么不直接讀/flag這就是我前面為什么說“還得懂點 Java Web 結(jié)構(gòu)”的原因。這個 Tomcat 應(yīng)用跑在 Docker 容器里它的工作目錄是/app有些版本是/usr/local/tomcat/webapps/ROOT或類似路徑flag 文件大概率直接放在文件系統(tǒng)根目錄/flag但是直接提交filename/flag的時候返回是空或者報錯。原因其實不復(fù)雜代碼讀取文件時很可能是基于 Web 應(yīng)用的工作目錄比如/app去拼接相對路徑的。你傳/flag拼接后變成了/app//flag這個路徑當(dāng)然不存在。而傳WEB-INF/web.xml拼接后就是/app/WEB-INF/web.xml正好落在應(yīng)用目錄里所以能讀到。這就引出了第二個關(guān)鍵點讀取任意文件時要知道服務(wù)器的工作目錄和路徑拼接規(guī)則。在不知道絕對路徑的情況下先用相對路徑WEB-INF/web.xml去探測是一個通用且高效的思路。3. 實操復(fù)現(xiàn)全過程3.1 信息收集先把請求包摸透我打開題目的登錄頁在瀏覽器里隨便輸入了admin / admin點擊登錄后打開 Burp Suite 的 HTTP History找到剛才那條 POST 請求。請求報文里除了常規(guī)的username和password之外沒有別的可疑參數(shù)響應(yīng)是純文本login failed。再看響應(yīng)頭Server: Apache-Coyote/1.1這個響應(yīng)頭是 Tomcat 的標(biāo)準(zhǔn)標(biāo)識之一。Apache-Coyote/1.1不等于 Apache HTTP Server它其實是 Tomcat 的連接器Connector標(biāo)識。只要看到這個頭基本可以確認(rèn)后端就是 Tomcat。既然確認(rèn)了是 Tomcat我的思路就變成了能不能通過某個參數(shù)讀文件我把請求體改成usernameadminpassword123456filenameWEB-INF/web.xml發(fā)送后響應(yīng)不再是login failed而是直接返回整個web.xml的內(nèi)容。這一步的關(guān)鍵在于參數(shù)名filename不是頁面里本來就有的是我后加的。也就是說這套后端邏輯里可能本來就存在一個文件讀取的功能只是沒有在頁面上暴露出來需要我們自己猜測參數(shù)名。filename很常見如果猜不中還可以試試file、path、src、url等。3.2 從 web.xml 里挖出源碼文件路徑拿到web.xml后先把內(nèi)容從頭到尾看一遍。里面內(nèi)容不少但最重要的幾個片段是servlet servlet-nameLoginServlet/servlet-name servlet-classcom.ctf.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping還有servlet servlet-nameFlagServlet/servlet-name servlet-classcom.ctf.FlagServlet/servlet-class /servlet servlet-mapping servlet-nameFlagServlet/servlet-name url-pattern/flag/url-pattern /servlet-mapping這里的信息量非常大。第一應(yīng)用的 Java 包名是com.ctf第二存在一個FlagServlet映射路徑是/flag。按理說登錄后訪問/flag應(yīng)該能拿到 flag但我試了一下直接訪問/flag返回了一個 403 或者空白說明這個 servlet 里有權(quán)限校驗邏輯。那怎么拿到FlagServlet的邏輯呢答案還是靠文件讀取。既然web.xml里已經(jīng)給出了類的完全限定名com.ctf.FlagServlet而WEB-INF/classes目錄下面就是按包名存放的.class文件那對應(yīng)的字節(jié)碼路徑就是WEB-INF/classes/com/ctf/FlagServlet.class于是我用同樣的方式提交POST /login HTTP/1.1 usernameadminpassword123456filenameWEB-INF/classes/com/ctf/FlagServlet.class這次響應(yīng)不再是 XML 或者文本而是一大坨二進(jìn)制內(nèi)容也就是FlagServlet.class的原樣字節(jié)碼。因為我用的是 Burp 的 Repeater返回的二進(jìn)制沒有直接渲染但能看到一堆亂碼。這其實就是下載成功了。3.3 反編譯 class 拿到核心邏輯拿到.class文件后下一步是反編譯。常用的工具我按推薦順序列一下工具適用場景備注JD-GUI圖形化查看雙擊打開 jar 包或 class 文件即可適合快速閱讀jadx主要用于 Android但也能打開普通 class有命令行版適合批量反編譯IDEA / Eclipse內(nèi)置 Java 反編譯器或裝插件我主力用 IntelliJ IDEA 的 FernFlower 插件CFR / FernFlower命令行適合寫腳本批量處理javapJDK 自帶只能看字節(jié)碼指令不太直觀但能看方法簽名我一般首選 IDEA 直接打開.class文件它會自動反編譯并顯示成可讀的 Java 源碼。這次反編譯FlagServlet.class后的核心邏輯大概是這樣的public class FlagServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String flag flag{...}; PrintWriter out resp.getWriter(); String user req.getParameter(user); if (admin.equals(user)) { out.print(flag); } else { out.print(permission denied); } } }實際的原題源碼可能有差異但邏輯大體就是判斷請求參數(shù)user的值是不是admin如果是就輸出 flag。這里我直接訪問/flag返回 403可能是在 web.xml 里配置了security-constraint限制只有登錄用戶才能訪問或者 servlet 里還做了 session 校驗。那繞過方式也很直接既然doGet方法只是判斷參數(shù)不校驗 session那我直接在 URL 上帶參訪問GET /flag?useradmin HTTP/1.1 Host: target或者用 POST 提交useradmin。我發(fā)出去之后響應(yīng)直接返回了 flag。到這里這道題就解完了。3.4 另一種思路直接爆破參數(shù)值反編譯這條路是最穩(wěn)妥的但如果你沒有反編譯環(huán)境或者 class 文件下載不全其實也可以盲猜參數(shù)。比如既然FlagServlet里大概率有判斷條件常見參數(shù)名無非是user、username、name、role、isAdmin、auth這一類的再用常見的值admin、true、1去試。不過這種爆破方式效率低如果遇到隨機(jī)參數(shù)名就抓瞎了所以還是建議老老實實把web.xml和 class 文件讀出來做靜態(tài)分析一勞永逸。4. 遇到的一些坑和排查實錄4.1 參數(shù)名猜不中怎么辦這道題我一開始用的是filename試了兩次就成功了但如果換一套環(huán)境參數(shù)名可能是file、path或者dir。我的經(jīng)驗是先觀察頁面本身的 URL 和請求參數(shù)再結(jié)合應(yīng)用的常見框架來猜。比如頁面里有上傳功能那參數(shù)可能叫img有下載功能可能叫filename。如果純靠猜建議列一個常見參數(shù)名字典用 Burp Intruder 跑一遍Payload 就寫WEB-INF/web.xml觀察哪次響應(yīng)長度異常。4.2 讀取 web.xml 后內(nèi)容亂碼或空白這種情況通常是編碼問題。web.xml本身是 UTF-8 編碼的 XML但有些服務(wù)器默認(rèn)用 ISO-8859-1 輸出導(dǎo)致中文注釋亂碼。解決辦法是在響應(yīng)頭里查看Content-Type是否包含charsetUTF-8或者在 Burp 的 Render 標(biāo)簽頁里手動切換編碼。如果是二進(jìn)制 class 文件下載用 Burp 的保存響應(yīng)功能直接存成.class文件再用反編譯器打開不要復(fù)制文本否則文件會損壞。4.3 讀了 web.xml 但 class 文件 404這有兩種常見情況一是路徑拼接問題。web.xml里寫的類名是com.ctf.FlagServlet但實際編譯后的文件不一定在WEB-INF/classes下可能打包成了 jar 放在WEB-INF/lib里或者類名和文件名大小寫不一致。遇到這種情況先看看web.xml里有沒有context-param或者其他配置透露實際的目錄結(jié)構(gòu)也可以先讀取一下WEB-INF/classes/com/ctf/目錄下的文件列表但目錄列表不一定能直接讀出來。二是應(yīng)用使用了 Spring Boot 或者 Maven 多模塊結(jié)構(gòu)class 文件路徑跟標(biāo)準(zhǔn)的 Tomcat 布置不一樣。這種情況比較少見因為 Spring Boot 默認(rèn)是打 jar 包運(yùn)行的里面沒有直接暴露WEB-INF/classes的路徑。如果碰到這種可以試試讀取BOOT-INF/classes/前綴的路徑。4.4 反編譯出來代碼不全javap只能看到方法簽名看不到具體邏輯。所以我建議優(yōu)先用 FernFlower 或 CFR 這類反編譯器。如果反編譯出來的代碼有大量goto或者變量名變成var1、var2不影響閱讀邏輯主要是因為編譯時開啟了混淆或者沒有保留調(diào)試信息。這道題沒開混淆所以還原度很高。4.5 直接訪問 /flag 接口報 403web.xml里很可能配置了security-constraint限制了/flag的訪問。我試過直接 GET 返回 403但帶上useradmin之后就能訪問說明這個接口本身只是做參數(shù)判斷沒有 session 校驗。如果遇到帶 session 校驗的那就需要先通過登錄接口拿到合法的 session cookie再把 cookie 帶入/flag請求里。這道題不需要看代碼說話。5. 防御視角如何防止 WEB-INF 泄露做完了題值得反過來想一想如果把這道題的漏洞放在生產(chǎn)環(huán)境會有什么后果答案是很嚴(yán)重的。WEB-INF/classes目錄下的class文件包含了全部業(yè)務(wù)邏輯攻擊者拿到之后可以順藤摸瓜找到數(shù)據(jù)庫地址、密鑰、內(nèi)部 API 接口、OSS 的 AccessKey甚至可以反編譯出完整的數(shù)據(jù)庫表結(jié)構(gòu)和 SQL 語句這基本上等同于把應(yīng)用的底褲全部扒光。要防住這類漏洞第一道防線是做好文件讀取接口的路徑校驗。任何用戶可控的文件路徑參數(shù)都不能直接用來拼接文件系統(tǒng)路徑。要么做白名單校驗要么過濾掉WEB-INF、META-INF、..、/等關(guān)鍵字要么統(tǒng)一使用 UUID 等無規(guī)則文件名來映射真實路徑。第二道防線是 Web 容器層面的防護(hù)。Tomcat 本身默認(rèn)會攔截WEB-INF目錄的直接訪問但攔截不攔截取決于應(yīng)用是否通過DefaultServlet暴露了文件下載功能。如果應(yīng)用自己實現(xiàn)了文件讀取接口那容器默認(rèn)規(guī)則就失效了。所以生產(chǎn)環(huán)境建議給文件讀取接口加獨立的接口鑒權(quán)比如強(qiáng)制要求管理員令牌而不是依賴容器默認(rèn)策略。第三道防線是收好源碼和配置的暴露面。web.xml里不要寫敏感信息類名也不要起得那么直白比如這題直接叫FlagServlet簡直是把答案寫在臉上。部署時盡量開啟編譯優(yōu)化和反調(diào)試選項雖然 Java 字節(jié)碼本身很難完全防反編譯但至少可以增加分析成本。6. 這類題型的舉一反三做完這道題可以順手歸納一下 Java Web 方向常見的信息泄露題型以后遇到心理會更有底題目特征可能考點破題思路登錄框 Tomcat 響應(yīng)頭WEB-INF 泄露、弱口令、SQL 注入先抓包看參數(shù)再傳filenameWEB-INF/web.xmlSpring Boot 報錯頁面Actuator 未授權(quán)、錯誤頁堆棧泄露訪問/actuator、/env、/heapdump文件下載功能任意文件讀取、路徑穿越參數(shù)值改為../../../../etc/passwd先試相對路徑上傳功能 解析差異畸形文件解析、后綴繞過上傳 JSP 混淆擴(kuò)展名或利用容器解析差異反序列化特征java開頭響應(yīng)頭RCE 反序列化用 ysoserial 生成 payload 探測單就“通過 web.xml 泄露 class 文件”這個鏈路我自己在不少 CTF 題目和實戰(zhàn)滲透項目里都見過變體。有些題目給的不是filename而是file參數(shù)有些則把邏輯放在 JS 里面需要先去讀前端源碼才能發(fā)現(xiàn)參數(shù)。所以做題的時候第一步永遠(yuǎn)是打開 Burp 看完整請求和響應(yīng)把前端源碼也過一遍。7. 續(xù)一步擴(kuò)展如果這道題再加一個條件題目本身到這里已經(jīng)解完了但我覺得可以再延伸思考一下因為這種加難度的方式在比賽里特別常見。假如這道題不讓直接下載 class 文件也就是說WEB-INF/classes被攔截了那怎么辦比較可行的思路是嘗試通過錯誤頁面構(gòu)造異常讓容器把web.xml里的 servlet-class 值回顯出來。比如訪問不存在的參數(shù)導(dǎo)致 500有些框架會把堆棧信息或配置文件內(nèi)容帶出來。另外還可以嘗試在filename參數(shù)里構(gòu)造路徑穿越繞到容器其他目錄讀取部署包源碼比如filename../webapps/ROOT/WEB-INF/web.xml不同版本的 Tomcat其工作目錄和部署路徑會有差異多試幾個相對層級往往會有意外收獲。再或者可以利用 Java 的日志泄露來獲得源碼路徑。如果服務(wù)器開啟了訪問日志且我們能通過文件讀取接口讀到日志文件日志里通常記錄了每個請求的完整路徑和參數(shù)可以從中推斷出更多文件路徑。不過說到底這些都是為了彌補(bǔ)獲取源碼階段的信息不足核心思路不變找到配置 - 定位源碼 - 審計邏輯 - 獲取目標(biāo)內(nèi)容。我自己在復(fù)現(xiàn)這道題時還特地把整個請求流程寫成了腳本用 Python 的requests庫模擬核心代碼大約是這樣import requests url http://target/login headers {Content-Type: application/x-www-form-urlencoded} # 第一步讀取 web.xml data {username: admin, password: 123456, filename: WEB-INF/web.xml} resp requests.post(url, datadata, headersheaders) print(resp.text) # 第二步下載 class 文件 data[filename] WEB-INF/classes/com/ctf/FlagServlet.class resp requests.post(url, datadata, headersheaders) with open(FlagServlet.class, wb) as f: f.write(resp.content)腳本跑完后拿著被下載的 class 文件丟進(jìn)反編譯器查看代碼邏輯最后直接構(gòu)造/flag?useradmin的請求拿到 flag。整個過程到這里完全閉環(huán)。根據(jù)我個人經(jīng)驗Easy Java 這道題是典型的“看著容易、做著繞”的題目核心價值就在于幫你梳理清楚 Java Web 的標(biāo)準(zhǔn)目錄結(jié)構(gòu)和WEB-INF的保護(hù)邊界。以后再遇到 Tomcat 類題目我會第一時間先確認(rèn)是否存在任意文件讀取再結(jié)合web.xml去還原整個應(yīng)用代碼。順帶說一句用 Burp 的 Repeater 時記得把Content-Type設(shè)置為application/x-www-form-urlencoded不然服務(wù)端可能解析不到 POST body 的參數(shù)這個問題我最早排查了半天才發(fā)現(xiàn)。