戰(zhàn):從手工注入到參數(shù)化防御)
1. 為什么SQL注入至今仍是繞不開的必修課但凡接觸過Web安全方向的人對DVWA這個(gè)靶場都不會陌生。它全稱Damn Vulnerable Web Application是一套刻意設(shè)計(jì)得千瘡百孔的練習(xí)環(huán)境專門用來讓安全學(xué)習(xí)者在合法可控的條件下親手體驗(yàn)各種經(jīng)典漏洞的利用過程。而SQL Injection也就是SQL注入幾乎是所有人打開DVWA后第一個(gè)真正上手實(shí)戰(zhàn)的模塊。原因很簡單它原理直觀、危害巨大、利用手法層次分明從最低難度的直接拼接到高難度的盲注與繞過一條線走下來能把數(shù)據(jù)庫交互這件事的里里外外摸個(gè)通透。這篇文章要聊的就是DVWA里SQL Injection這個(gè)模塊的完整拆解。我會從它背后的代碼邏輯講起把Low、Medium、High、Impossible四個(gè)安全等級的實(shí)現(xiàn)差異逐一剖開再配合手工注入的完整實(shí)操流程、參數(shù)構(gòu)造的推導(dǎo)過程、以及我在反復(fù)練習(xí)中踩過的坑和總結(jié)出來的排查技巧。不管你是剛?cè)腴T、連union select都還沒寫利索的新手還是已經(jīng)能盲注跑數(shù)據(jù)、想回頭把底層邏輯再夯實(shí)一遍的老手這篇內(nèi)容都能給你一些能直接抄作業(yè)的東西。需要先明確一點(diǎn)本文所有操作都只在本地搭建的DVWA靶場環(huán)境中進(jìn)行目的是理解漏洞成因、掌握防御思路。任何脫離授權(quán)環(huán)境、針對真實(shí)系統(tǒng)的測試都是違規(guī)的這個(gè)邊界必須守住。下面進(jìn)入正題。2. DVWA靶場環(huán)境與SQL注入模塊的整體設(shè)計(jì)2.1 DVWA的定位與安全等級機(jī)制DVWA最巧妙的設(shè)計(jì)是它的安全等級切換功能。同一個(gè)功能模塊在Low、Medium、High、Impossible四個(gè)等級下后端代碼的防護(hù)強(qiáng)度完全不同。Low幾乎不設(shè)防Medium加了基礎(chǔ)過濾High上了更嚴(yán)格的限制Impossible則是教科書級別的正確寫法。這種遞進(jìn)式設(shè)計(jì)的好處在于你能直觀看到同一個(gè)漏洞防護(hù)每加強(qiáng)一層攻擊手法就要相應(yīng)升級從而真正理解防御措施到底在防什么。SQL Injection模塊的功能本身很簡單頁面上有一個(gè)輸入框你輸入一個(gè)用戶ID它去數(shù)據(jù)庫里查這個(gè)ID對應(yīng)的用戶名然后把結(jié)果返回給你。就這么一個(gè)看似無害的查詢功能在Low等級下卻能被玩出花來——因?yàn)樗腟QL語句是直接字符串拼接的。2.2 數(shù)據(jù)庫結(jié)構(gòu)與查詢邏輯DVWA默認(rèn)使用MySQL數(shù)據(jù)庫SQL Injection模塊涉及的核心表是users表里面有兩個(gè)關(guān)鍵字段user_id和first_name、last_name。正常查詢的SQL語句長這樣SELECT first_name, last_name FROM users WHERE user_id $id;這里的$id就是你在輸入框里填的內(nèi)容被原封不動地塞進(jìn)了SQL語句。理解這一點(diǎn)是理解整個(gè)注入攻擊的鑰匙——你的輸入不再是數(shù)據(jù)而是變成了代碼的一部分。當(dāng)你輸入1語句變成WHERE user_id 1正常返回。當(dāng)你輸入1 OR 11語句就變成了WHERE user_id 1 OR 11條件恒真于是返回所有用戶。這就是注入的本質(zhì)讓用戶輸入突破數(shù)據(jù)邊界篡改SQL語義。2.3 四個(gè)等級的實(shí)現(xiàn)差異概覽在動手之前先把四個(gè)等級的代碼邏輯差異理清楚這樣你在實(shí)操時(shí)才知道自己面對的是什么。安全等級核心防護(hù)手段可利用性Low無任何過濾直接拼接完全可利用任意注入Mediummysqli_real_escape_string轉(zhuǎn)義 下拉框限制輸入數(shù)字型注入仍可利用High限制輸入長度 會話校驗(yàn) 分頁查詢需借助其他注入點(diǎn)或二次注入Impossible參數(shù)化查詢預(yù)處理語句無法注入這張表先放在這里后面每個(gè)等級我都會展開講。你會發(fā)現(xiàn)Medium和High的防護(hù)看似嚴(yán)密但都有各自的破綻而這些破綻恰恰是真實(shí)世界里很多系統(tǒng)翻車的地方。3. Low等級從手工注入到拖庫的完整實(shí)操3.1 判斷注入類型與閉合方式進(jìn)入SQL Injection頁面把安全等級調(diào)到Low。第一步不是急著寫payload而是先判斷注入類型。在輸入框里填1正常返回。填1如果報(bào)錯說明存在字符型注入且閉合符是單引號。DVWA的Low等級正是這種情況填1會拋出SQL語法錯誤。這一步的判斷邏輯很關(guān)鍵。SQL注入按數(shù)據(jù)類型分有數(shù)字型和字符型。數(shù)字型不需要閉合符直接1 and 11就行字符型則需要先閉合前面的引號。DVWA這里是字符型所以payload要圍繞單引號來構(gòu)造。判斷閉合方式的方法就是逐個(gè)嘗試、、)等看哪個(gè)能造成語法錯誤哪個(gè)能讓語句恢復(fù)正常。3.2 確定字段數(shù)量ORDER BY的妙用閉合方式確定后下一步是確定查詢返回了幾個(gè)字段。因?yàn)楹竺嬉肬NION SELECT聯(lián)合查詢而UNION要求前后兩個(gè)SELECT的字段數(shù)必須一致。確定字段數(shù)最常用的方法是ORDER BY。依次輸入1 ORDER BY 1 # 1 ORDER BY 2 # 1 ORDER BY 3 ##是MySQL的注釋符用來把后面的原始SQL注釋掉避免語法錯誤。當(dāng)ORDER BY 2正常返回、ORDER BY 3報(bào)錯時(shí)說明查詢結(jié)果有2個(gè)字段。DVWA這里就是2個(gè)字段。這個(gè)推導(dǎo)過程不能跳字段數(shù)錯了后面的UNION查詢必然失敗。注意注釋符在不同數(shù)據(jù)庫里不一樣。MySQL用#或--注意--后面有個(gè)空格SQL Server用--Oracle用--。DVWA是MySQL所以#和--都能用。我習(xí)慣用#因?yàn)椴挥霉芸崭袷∈隆?.3 UNION SELECT回顯數(shù)據(jù)字段數(shù)確定后就可以用UNION SELECT把想要的數(shù)據(jù)回顯到頁面上。先測試回顯位置1 UNION SELECT 1,2 #頁面上會顯示兩個(gè)數(shù)字告訴你哪個(gè)字段的內(nèi)容會顯示出來。DVWA里兩個(gè)字段都會顯示。接著就可以查數(shù)據(jù)庫信息了1 UNION SELECT database(), user() #這會返回當(dāng)前數(shù)據(jù)庫名和當(dāng)前數(shù)據(jù)庫用戶。繼續(xù)往下查所有表名1 UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schema database() #information_schema是MySQL自帶的元數(shù)據(jù)數(shù)據(jù)庫里面存著所有庫、表、列的信息。group_concat把多行結(jié)果拼成一行方便一次性看全。查到表名后查users表的列名1 UNION SELECT 1, group_concat(column_name) FROM information_schema.columns WHERE table_name users #最后拖出賬號密碼1 UNION SELECT user, password FROM users #到這一步整個(gè)users表的數(shù)據(jù)就全出來了。DVWA里的密碼是MD5哈??梢阅萌ピ诰€解密或者用工具跑。3.4 實(shí)操心得與常見卡點(diǎn)第一次做的時(shí)候我最容易卡在三個(gè)地方。第一是忘了閉合引號payload寫一半就報(bào)錯第二是字段數(shù)沒對齊UNION查詢直接失敗第三是注釋符用錯導(dǎo)致后面的原始SQL沒被注釋掉語法錯誤。這三個(gè)點(diǎn)幾乎是新手必踩的坑。還有一個(gè)細(xì)節(jié)group_concat默認(rèn)有長度限制如果表特別大可能顯示不全??梢杂胓roup_concat(table_name SEPARATOR ||)自定義分隔符或者配合limit分頁查。DVWA數(shù)據(jù)量小一般不會遇到這個(gè)問題但真實(shí)環(huán)境里這個(gè)坑很常見。4. Medium等級轉(zhuǎn)義與下拉框下的突破口4.1 代碼層面的防護(hù)變化切到Medium等級你會發(fā)現(xiàn)輸入框變成了下拉選擇框只能選1到5這幾個(gè)數(shù)字。同時(shí)后端代碼里加了mysqli_real_escape_string函數(shù)對輸入進(jìn)行轉(zhuǎn)義。這個(gè)函數(shù)會把單引號、雙引號、反斜杠等特殊字符前面加上反斜杠讓它們失去語法意義??雌饋矸雷o(hù)到位了但問題出在SQL語句的構(gòu)造方式上。Medium等級的SQL語句變成了SELECT first_name, last_name FROM users WHERE user_id $id;注意這里的$id外面沒有引號了。也就是說它把用戶輸入當(dāng)成了數(shù)字型來處理。而mysqli_real_escape_string對數(shù)字型注入是無效的——因?yàn)樗晦D(zhuǎn)義引號類字符而數(shù)字型注入根本不需要引號。4.2 數(shù)字型注入的利用既然不需要閉合引號payload就變得非常直接1 OR 11或者用UNION1 UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schema database()下拉框的限制怎么辦用抓包工具攔截請求把id參數(shù)的值改掉再放行就行。因?yàn)橄吕蛑皇乔岸讼拗坪蠖瞬]有校驗(yàn)傳入的值是不是1到5。這是典型的前端校驗(yàn)不可信案例。4.3 這個(gè)等級教給我們的防御教訓(xùn)Medium等級是一個(gè)非常好的反面教材。它做了兩件事轉(zhuǎn)義特殊字符、限制前端輸入。但兩件事都沒做到位。轉(zhuǎn)義函數(shù)選錯了場景數(shù)字型不需要轉(zhuǎn)義引號前端限制又形同虛設(shè)。真實(shí)世界里很多系統(tǒng)就是這樣看起來加了防護(hù)實(shí)際上防護(hù)和場景不匹配等于沒防。正確的做法應(yīng)該是不管什么類型統(tǒng)一用參數(shù)化查詢。這一點(diǎn)在Impossible等級里會看到標(biāo)準(zhǔn)答案。5. High等級長度限制與會話機(jī)制下的迂回5.1 防護(hù)升級的具體表現(xiàn)High等級的防護(hù)又上了一個(gè)臺階。首先輸入框的maxlength被限制得很短你沒法直接輸入長payload。其次后端代碼里用了LIMIT 1只返回一條結(jié)果。更關(guān)鍵的是它把查詢和顯示拆成了兩步先查詢把結(jié)果存到會話里再跳轉(zhuǎn)到另一個(gè)頁面顯示。這意味著你沒法像Low那樣直接在同一個(gè)頁面看到回顯。代碼邏輯大致是這樣的$id $_SESSION[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id LIMIT 1;;5.2 繞過思路從回顯注入到盲注因?yàn)轫撁娌辉僦苯踊仫@查詢結(jié)果UNION SELECT的回顯路子走不通了。這時(shí)候就要轉(zhuǎn)向盲注Blind Injection。盲注分兩種布爾盲注和時(shí)間盲注。布爾盲注的思路是構(gòu)造一個(gè)條件如果條件為真頁面返回正常如果為假頁面返回異常或空白。通過不斷提問是/否來逐字符推斷數(shù)據(jù)。比如1 AND (SELECT SUBSTRING(database(),1,1)) d #如果數(shù)據(jù)庫名第一個(gè)字符是d頁面正常否則異常。這樣一個(gè)個(gè)字符試最終拼出完整信息。時(shí)間盲注則更隱蔽不管條件真假頁面都正常返回但通過SLEEP()函數(shù)讓頁面延遲響應(yīng)來傳遞信息1 AND IF((SELECT SUBSTRING(database(),1,1)) d, SLEEP(3), 0) #如果條件為真頁面會卡3秒否則立即返回。通過響應(yīng)時(shí)間判斷條件真假。5.3 長度限制的繞過High等級限制了輸入長度但抓包改包依然可以繞過前端限制。因?yàn)閙axlength只是HTML屬性攔截請求后隨便改。真正需要關(guān)注的是后端有沒有做長度校驗(yàn)——DVWA的High等級后端并沒有嚴(yán)格校驗(yàn)長度所以改包后長payload照樣能打進(jìn)去。實(shí)操提示盲注手工跑非常耗時(shí)一個(gè)字符一個(gè)字符試一個(gè)數(shù)據(jù)庫名可能要試幾十次。實(shí)際練習(xí)時(shí)建議寫個(gè)簡單的Python腳本自動化或者用sqlmap這類工具輔助。但手工跑一遍的價(jià)值在于你能徹底理解盲注的邏輯知道工具在背后做了什么。5.4 盲注腳本的核心邏輯如果要寫腳本核心邏輯就是遍歷字符集對每個(gè)位置構(gòu)造payload根據(jù)響應(yīng)判斷字符值。偽代碼大概是這樣import requests chars abcdefghijklmnopqrstuvwxyz0123456789_ result for pos in range(1, 20): for c in chars: payload f1 AND SUBSTRING(database(),{pos},1){c} # # 發(fā)送請求判斷響應(yīng) if is_true(payload): result c break print(result)這個(gè)腳本跑起來幾分鐘就能把數(shù)據(jù)庫名、表名、列名、數(shù)據(jù)全跑出來。但前提是你得先手工驗(yàn)證過payload的正確性知道怎么判斷真假。6. Impossible等級參數(shù)化查詢的正確姿勢6.1 代碼實(shí)現(xiàn)解析Impossible等級的代碼是整篇文章里唯一值得抄的部分。它的核心是參數(shù)化查詢也叫預(yù)處理語句。代碼邏輯大致如下$stmt $db-prepare(SELECT first_name, last_name FROM users WHERE user_id ? LIMIT 1;); $stmt-bind_param(i, $id); $stmt-execute();這里的關(guān)鍵是?占位符和bind_param。用戶輸入不再拼接到SQL語句里而是作為參數(shù)單獨(dú)傳給數(shù)據(jù)庫。數(shù)據(jù)庫在執(zhí)行時(shí)會先編譯SQL模板再把參數(shù)填進(jìn)去。因?yàn)镾QL結(jié)構(gòu)已經(jīng)固定用戶輸入無論包含什么字符都只能作為數(shù)據(jù)存在無法改變SQL語義。6.2 為什么參數(shù)化查詢能根治注入?yún)?shù)化查詢之所以有效是因?yàn)樗汛a和數(shù)據(jù)徹底分離了。在拼接模式下用戶輸入和SQL語句混在一起數(shù)據(jù)庫無法區(qū)分哪部分是開發(fā)者寫的、哪部分是用戶輸入的。而在參數(shù)化模式下SQL模板先被編譯參數(shù)后填充數(shù)據(jù)庫明確知道哪些是結(jié)構(gòu)、哪些是值。這就好比你去餐廳點(diǎn)菜菜單是固定的你只能在指定位置填數(shù)量不能自己改菜單。另外Impossible等級還加了CSRF token校驗(yàn)和輸入類型檢查但這些是錦上添花核心還是參數(shù)化查詢。只要用了參數(shù)化查詢SQL注入就從根本上被堵死了。6.3 參數(shù)化查詢的常見誤區(qū)很多人以為用了ORM框架就自動安全了其實(shí)不然。有些ORM的某些方法仍然會拼接字符串比如某些框架里的whereRaw、orderBy拼接等。參數(shù)化查詢的關(guān)鍵是所有用戶可控的輸入都必須通過參數(shù)綁定的方式傳入不能有任何一處是字符串拼接。這一點(diǎn)在真實(shí)項(xiàng)目里要特別警惕因?yàn)橐粋€(gè)項(xiàng)目里只要有一處漏網(wǎng)整個(gè)防線就破了。7. 常見問題與排查技巧實(shí)錄7.1 注入失敗的原因排查表現(xiàn)象可能原因排查方向輸入單引號無報(bào)錯不是字符型注入或報(bào)錯被屏蔽嘗試數(shù)字型payload檢查是否盲注UNION查詢報(bào)錯字段數(shù)不一致重新用ORDER BY確定字段數(shù)回顯位置為空查詢結(jié)果未顯示在該字段調(diào)整UNION中數(shù)字的位置盲注腳本跑不出結(jié)果判斷真假的邏輯有誤手工驗(yàn)證單個(gè)payload的響應(yīng)差異抓包改包無效后端有額外校驗(yàn)檢查是否有token或會話校驗(yàn)7.2 幾個(gè)容易忽略的細(xì)節(jié)第一個(gè)細(xì)節(jié)是編碼問題。有時(shí)候payload里的空格、注釋符會被URL編碼影響導(dǎo)致實(shí)際傳輸?shù)膬?nèi)容和你想的不一樣。建議在抓包工具里直接看原始請求確認(rèn)payload沒有被意外編碼。第二個(gè)細(xì)節(jié)是數(shù)據(jù)庫版本差異。MySQL 5.0以上才有information_schema老版本需要用其他方式猜表名。DVWA用的是較新版本所以information_schema可用。真實(shí)環(huán)境里如果遇到老數(shù)據(jù)庫手法要相應(yīng)調(diào)整。第三個(gè)細(xì)節(jié)是權(quán)限問題。information_schema里的數(shù)據(jù)受當(dāng)前數(shù)據(jù)庫用戶權(quán)限限制如果權(quán)限低可能看不到某些表。這時(shí)候可以嘗試直接猜常見表名比如admin、user、member等。7.3 我的避坑經(jīng)驗(yàn)踩過幾次坑之后我養(yǎng)成了一個(gè)習(xí)慣每換一個(gè)等級先把后端代碼讀一遍。DVWA的源碼是開放的讀代碼比盲目試payload效率高得多。你知道它怎么過濾、怎么拼接、怎么返回就能直接推導(dǎo)出該用什么手法而不是一個(gè)個(gè)試。另一個(gè)經(jīng)驗(yàn)是手工注入一定要做一遍哪怕你后面都用工具。因?yàn)楣ぞ邎?bào)錯的時(shí)候你得知道它在干什么、卡在哪一步。手工做過一遍你看工具的輸出就知道問題出在哪個(gè)環(huán)節(jié)排查起來快得多。8. 從靶場到實(shí)戰(zhàn)的思維遷移DVWA的SQL Injection模塊練完你掌握的不應(yīng)該只是幾個(gè)payload。更重要的是理解了一件事漏洞的根源在于用戶輸入被當(dāng)成了代碼執(zhí)行。這個(gè)思維可以遷移到很多地方——命令注入、XSS、模板注入本質(zhì)上都是同一個(gè)問題數(shù)據(jù)和代碼的邊界被打破了。防御的思路也是通用的永遠(yuǎn)不要相信用戶輸入永遠(yuǎn)用參數(shù)化的方式處理數(shù)據(jù)永遠(yuǎn)在數(shù)據(jù)進(jìn)入執(zhí)行環(huán)境之前做好隔離。DVWA的四個(gè)等級其實(shí)就是防御思路從沒有到錯誤到部分正確到完全正確的演進(jìn)過程。把這個(gè)演進(jìn)過程吃透比記住一百個(gè)payload都有用。我個(gè)人在實(shí)際操作中的體會是SQL注入這個(gè)模塊值得反復(fù)刷。第一遍你可能只是照著教程敲payload第二遍你會開始思考為什么這么構(gòu)造第三遍你就能自己推導(dǎo)出針對不同場景的變種手法。每一遍都有新的收獲這也是DVWA這么多年一直沒被淘汰的原因。