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