iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Security

《朝 HTB CPTS 前進:資安新手的 30 天實作筆記》系列 第 12 篇

Day 12 — SQL Injection:一個單引號,就能撬開整個資料庫

  • 分享至 

  • xImage
  •  

在 Bashed 那幾天,我們確實有碰到 Web——用 ffuf 做目錄爆破、找到 phpbash,再透過 Web Shell 取得立足點。

但說實話,Bashed 的 Web 部分比較像是「剛好有一扇門開著,我們走了進去」。真正讓我們拿到 root 的,是後面的 sudo -l 跟 cron job——Web 對那台靶機來說,只是個入口。

可是 Web 漏洞的世界,其實遠比這個複雜得多。而且這 30 天後面還會碰到 AD 跟 Windows 環境,那些又比 Linux 的 Web 靶機複雜不少。

HTB 的靶機是完整的環境,適合拿來驗證「一整條攻擊鏈」;但如果目的只是想把某個漏洞的觀念搞清楚,PortSwigger 可能更適合。

因為 PortSwigger 的 Lab 通常會聚焦在一個漏洞情境,讓我們可以把注意力放在漏洞本身,做完就能抓到這個漏洞的核心邏輯是什麼。當我們只是「純粹要理解跟研究一個漏洞」時用PortSwigger 可能更有效率,也更適合我們在這 30 天的鐵人賽裡——用有限的時間,一起走過最多主題。

所以今天,我們先用 PortSwigger 把之前沒一起細看、但其實非常重要的 SQL injection 搞清楚;明天再一起看 Command Injection。


碰到 Web 服務,我到底要怎麼走?

雖然今天的目標是 SQL Injection,但我們先來一起看一個更大的問題~

碰到一個 Web 服務,到底要做什麼?又要按照什麼順序做?

前面在 Bashed,我們碰到 80 port 的時候,其實就已經走過一次這個流程了。

當時我們是:先看原始碼、Developer Tools,真的沒找到突破口,再開始跑工具。

因為像 ffuf 這種目錄爆破會對目標發出大量 HTTP Request,在真實環境的滲透測試中,比較容易產生大量流量、被偵測到。所以能靠眼睛先看的,就先看。

第一階段:Information Gathering(先搞清楚「這個網站有什麼」)

我通常會這樣排順序:

步驟 做什麼 為什麼這樣排
Step 1 瀏覽器打開,看網站內容 最直覺、也最安靜;有時首頁就藏著線索
Step 2 檢視原始碼、Developer Tools 可能看到版本資訊、JavaScript、隱藏路徑
Step 3 看 robots.txt 有時會直接寫出不想被搜尋引擎找到的路徑
Step 4 目錄爆破 前面沒突破口,再用工具系統性地找更多目錄與檔案

這四步本質在——了解這個 Web 有什麼,目標是找出可以下手的功能/輸入點。

第二階段:Exploitation(找到輸入點後,開始測漏洞)

一旦找到輸入點,腦中的問題就會從:

「這個網站有什麼?」

切換成:

「這個地方可以測什麼?」

不同類型的輸入點,對應到不同的漏洞測試方向:

輸入點類型 可能對應的漏洞
URL 參數/搜尋框/登入表單 SQL Injection、XSS
檔案上傳 File Upload
可以執行系統指令的地方 Command Injection

把整個思路串起來

Web Service
    ↓
【Information Gathering】這個網站有什麼?
    ↓
瀏覽器 → 原始碼 / DevTools → robots.txt → 目錄爆破
    ↓
找到功能/輸入點
    ↓
【Exploitation】這個地方可以測什麼?
    ↓
根據輸入點類型,選擇對應的漏洞測試

而今天要練習的 SQL Injection,就是走到「找到輸入點、進入 Exploitation」之後,其中一條測試路線

這跟之前打靶機的流程是一樣的邏輯——先列舉、再利用。只是現在的戰場從「Port 跟服務」變成了「Web 頁面跟輸入點」。


Web 應用怎麼跟資料庫溝通

要理解 SQL injection,得先知道 Web 應用跟資料庫之間是怎麼互動的。

以在購物網站搜尋商品為例,我們一起看看背後大概發生了這些事:

我們輸入關鍵字
    ↓
Web 把我們的輸入「組」進一段 SQL 語句,拿去問資料庫
    ↓
資料庫執行這段 SQL,回傳結果
    ↓
頁面顯示搜尋結果

問題就出在第二步——Web 把我們的輸入「組」進 SQL 語句這件事。

如果採用直接拼接的方式(我們輸入什麼,就原封不動接進 SQL、完全不檢查),那我們輸入的內容就有機會「混」進 SQL 指令裡,被資料庫當成指令的一部分執行。這就是 SQL injection 的破口。


SQL injection 的成因

先看正常情況。當我們在搜尋框輸入 Apple,網站組出來的查詢會長這樣:

SELECT * FROM products WHERE name LIKE '%Apple%'

意思是「幫我找出名稱包含 Apple 的商品」,很正常。

但如果我們輸入的不是商品名,而是 ' OR '1'='1-- -,這段查詢就會變成:

SELECT * FROM products WHERE name LIKE '%' OR '1'='1'-- -

問題來了:'1'='1' 這個條件永遠成立(1 當然等於 1),而前面用 OR 連接,代表「只要其中一個成立就算數」。於是整個條件永遠為真,資料庫就把所有商品通通回傳了。

根本問題:網站沒有分辨「我們輸入的是資料」還是「我們輸入的是指令」,直接把輸入原封不動拼進查詢裡執行。

這就是為什麼叫「Injection(注入)」——我們把 SQL 指令注入進了原本的查詢裡,讓資料庫照著我們的意思跑。


SQL injection 可以做到什麼

別小看這個破口,一旦成立,能做的事比想像中多:

攻擊類型 可以做到什麼
繞過登入 不需要帳號密碼就能進去
讀取其他使用者的資料 看到別人的個資、密碼雜湊
摸清資料庫結構 知道有哪些資料表、哪些欄位
讀取伺服器檔案 某些設定下可以讀到系統檔案

這篇我們會走的,主要是——摸清資料庫結構、把裡面的資料撈出來。


SQL injection 的幾種類型

同樣是 SQL Injection,會因為「資料庫怎麼把結果回給我們」而分成好幾種類型~~快來一起認識他們!

UNION-based

最直接、拿到的資料最多的一種。

用 UNION 把我們自己的查詢「接」在原本的查詢後面,讓資料庫連我們想要的資料一起回傳到頁面上。

就像去咖啡廳點一杯咖啡,結果店員連廚房的食材清單也一起端出來給你——本來只該看到商品,卻連資料庫深處的東西都被撈出來了。

Error-based

靠「錯誤訊息」洩漏資料。

有些資料庫的錯誤訊息太過熱心,會把出錯的細節(甚至查詢到的內容)直接寫在報錯裡。攻擊者就故意讓它出錯,從錯誤訊息裡把資料「讀」出來。

就像問一個藏不住話的人問題,他嘴上說「我不能講」,卻在解釋為什麼不能講的時候,把答案全抖出來了。

Boolean-based

頁面不會直接給資料,只能用「是/不是」慢慢猜。

當頁面不會回傳資料、也不噴錯誤,只會因為條件成立與否而有不同反應(例如有沒有顯示商品)。這時就只能問一堆是非題:「第一個字是 a 嗎?」成立就是 Yes、不成立就是 No,一個字元一個字元把資料拼出來。

有點像玩「猜猜我是誰」,只能問是非題,靠一次次縮小範圍把答案逼出來。

Time-based

連「是/不是」的畫面差異都沒有,只剩時間可以觀察。

頁面反應完全一樣、什麼線索都不給。這時攻擊者會讓資料庫「條件成立就故意延遲幾秒」,透過回應變慢來判斷猜測對不對。

就像在全黑的房間裡問問題,看不到對方點頭搖頭,只能靠「他停頓了多久」來判斷答案。

那實際上,怎麼知道要用哪一種?

通常會從最簡單的動作開始——先在輸入點丟一個單引號 ',看看網站有什麼反應,再依反應決定往哪個方向走:

輸入一個單引號 '
    ↓
頁面直接噴出 SQL 錯誤訊息?
    ├─ 是 → 錯誤訊息裡看得到資料嗎?
    │         ├─ 看得到 → Error-based
    │         └─ 只報錯、看不到資料 → 試 UNION-based
    │
    └─ 否(沒噴錯,但畫面怪怪的 / 沒東西)
              ↓
        改變條件,畫面會跟著不一樣嗎?(例如商品有/沒有顯示)
              ├─ 會 → Boolean-based
              └─ 完全沒差別 → Time-based

一般會從能拿到最多資料的 UNION-based 開始試

這篇我們就會實戰到 UNION-based(好好期待xddd。


今天的實驗:PortSwigger Lab

Lab:SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
https://ithelp.ithome.com.tw/upload/images/20260926/20184189A1a2vYajz2.png

目標:讓所有商品都顯示,包含未上架的


進入練習的LAB

點ACCESS THE LAB,進來今天練習的LAB
https://ithelp.ithome.com.tw/upload/images/20260926/201841898ZM7aI8du8.png


先設定 Burp Suite 攔截請求

Burp Suite 就像在我們的瀏覽器跟網站之間放了一個「中間人」,所有的請求都會先經過 Burp,讓我們可以看到、甚至修改請求的內容,再送出去。

那為什麼要多這一層?因為瀏覽器把請求送出去後我們就管不到了,透過 Burp 攔在中間,就能在請求送出「之前」把內容改成我們想測試的樣子

Step 1:安裝 FoxyProxy

Chrome 本身沒辦法單獨設定代理,要裝 FoxyProxy 插件來幫忙切換。

**代理(Proxy)**就是幫我們轉送網路請求的「中間人」。

  • 平常:瀏覽器直接把請求送到網站
  • 設定代理後:瀏覽器先把請求交給 Burp,再由 Burp 轉出去

這樣 Burp 才能攔在中間,看到並修改我們的請求。

而 FoxyProxy 的角色,就是幫我們一鍵切換「這次要不要走 Burp」

從 Chrome Web Store 搜尋 FoxyProxy Standard 安裝。

安裝後在擴充功能找到FoxyProxy,釘選後FoxyProxy icon就會在擴充功能列
https://ithelp.ithome.com.tw/upload/images/20260926/20184189Y8nyTA3A3k.png

Step 2:設定代理

安裝好之後,點 FoxyProxy 的狐狸圖示 → Options → Add,填入:

  • Title:Burp
  • Proxy Type:HTTP
  • IP:127.0.0.1
  • Port:8080

https://ithelp.ithome.com.tw/upload/images/20260926/20184189EG4oESt4ao.png

儲存之後,點 FoxyProxy 圖示切換到 Burp 模式。
https://ithelp.ithome.com.tw/upload/images/20260926/20184189mKqbtEoxt6.png

Step 3:安裝 Burp 的 CA 憑證

為什麼要裝憑證?因為 HTTPS 的流量是加密的,Burp 要能看到內容,需要先建立信任關係。
https://ithelp.ithome.com.tw/upload/images/20260926/20184189Ju2MSAOcBo.png

代理生效後,搜尋 http://burp 或 http://127.0.0.1:8080,點 CA Certificate 下載憑證,然後在 Chrome 的憑證管理裡匯入。

匯入憑證步驟:

  1. 開啟 Chrome,點選右上角的 三點選單 圖示,
  2. 選擇 設定
  3. 點選左側選單的 隱私權和安全性
  4. 點選 安全性
  5. 找到 管理憑證 並點擊它
    https://ithelp.ithome.com.tw/upload/images/20260926/20184189DWuSwBH5td.png
    6.匯入至信任的憑證
    https://ithelp.ithome.com.tw/upload/images/20260926/20184189qce8CKFHSr.png

Step 4:開啟 Intercept

Burp Suite → Proxy → Intercept → 確認按鈕顯示 Intercept is on

設定完後,我們在瀏覽器的每個動作,Burp 都會攔下來讓我們看,更方便瀏覽,也可做一些測試,有些時候在瀏覽器會被阻擋的行為,在curl或burp反而不會被攔截,更能靈活測試。
https://ithelp.ithome.com.tw/upload/images/20260926/20184189fVkcRxzXhT.png


方法一:用 OR 1=1 讓所有商品現形

Step 1:攔截請求,確認有沒有 SQL injection

瀏覽器點 Gifts 分類,Burp 攔截到請求,找到 category=Gifts 這一行,點該攔截節請求,看目前的Request:
https://ithelp.ithome.com.tw/upload/images/20260926/20184189QKyURf4jKz.png

把 category=Gifts 這一行,送到Repeater,以方便我們修改封包內容並重送封包,在 Gifts 後面加單引號:

category=Gifts'

點 Send 送出

https://ithelp.ithome.com.tw/upload/images/20260926/20184189W14rlNOzaa.png

出現錯誤——單引號破壞了查詢的語法結構,確認這網頁的category查詢有 SQL injection 漏洞。


Step 2:觀察正常商品數量

先攔截一個正常的Gifts請求,Gifts 分類顯示3個商品。

https://ithelp.ithome.com.tw/upload/images/20260926/20184189ghrO46w4ct.png


Step 3:注入 OR 1=1-- -,看商品數量有沒有變

在 Burp Intercept 裡把 category 改成

category=Gifts' OR 1=1-- -

,點 Send 送出。
https://ithelp.ithome.com.tw/upload/images/20260926/201841894fyjhknYjr.png

商品數量變8個(變多了!!!)——代表原本有某個條件把部分資料過濾掉,這個條件被我們的 OR 1=1-- - ,恆為True繞過了。

任務已經成功,OR 1=1-- - 讓所有商品都顯示,包含未上架的——Lab 的目標達成,Congratulations 出現了。

但如果今天不能用 OR 1=1 繞過呢?在滲透測試時,可能會遇到客戶的環境有不同的防護機制,如果我們想讓過防護機制,幫客戶確實了解還存在的弱點或漏洞,就需要有多套測試想法解決~
如果我只想精準地把未上架的商品撈出來呢?**

這就需要自己去找出「是什麼條件在控制顯示/隱藏」,再用那個條件下查詢。

所以我們繼續往下挖!看不仰賴1=1,該如何破關?
這裡想到另外一個做法是 :找出「是什麼條件在控制顯示/隱藏未上架的商品」,再觀察這個條件的值,把他們全部列舉出~是不是就有機會也殊途同歸呢

方法二:用 UNION 把資料撈出來

想一起嘗試方法二,我們就往下看下去囉~

(1) 用 ORDER BY 推斷欄位數

要進一步用 UNION 挖資料,需要先知道原本的 SQL 查詢有幾個欄位。
因為 UNION SELECT 合併查詢時,前後的欄位數需要對得上,所以可以利用 ORDER BY 一個一個往上試。

ORDER BY 是用來指定「按照第幾個欄位排序」,所以如果指定的欄位不存在,就會報錯。

簡單來說:

  • ORDER BY n 成功 → 至少有 n 個欄位
  • ORDER BY n 報錯 → 沒有第 n 個欄位

先從 1 開始:

category=Gifts' order by 1-- -

https://ithelp.ithome.com.tw/upload/images/20260926/20184189CFsAlO05Pu.png

ORDER BY 1 成功,出現 4 個商品。

接著繼續往比較大的數字嘗試:

category=Gifts' order by 8-- -

https://ithelp.ithome.com.tw/upload/images/20260926/2018418947pIRFOScF.png

ORDER BY 8 也成功,出現 4 個商品。

我們繼續試:

category=Gifts' order by 9-- -

https://ithelp.ithome.com.tw/upload/images/20260926/20184189nQ3n9xZMon.png

這次報錯了。

所以可以推回來:ORDER BY 8 成功、ORDER BY 9 報錯

代表原本查詢的結果是 8 個欄位,等一下 UNION SELECT 也要是 8 個欄位才不會出現錯誤


(2) 從 information_schema 找資料

知道欄位數之後,就可以開始往資料庫裡面挖了。這裡會用到 information_schema。

可以先把它想成資料庫裡面的「目錄」,裡面會記錄:

  • 有哪些 schema
  • 有哪些資料表
  • 資料表有哪些欄位
  • 欄位名稱是什麼

schema 是什麼?

可以想成資料表的「分類資料夾」,層級長這樣:

database(資料庫)
└── schema(例如 public)
    └── table(資料表,例如 products)
        └── column(欄位,例如 name, released...)

而 public 就是 PostgreSQL 預設的 schema,沒特別指定時建立的表都會丟進去,所以把範圍縮到 public 通常就能找到目標資料表了

先看看有哪些資料表

挖資料庫資料表

category=Gifts' UNION SELECT NULL,NULL,table_name,NULL,NULL,NULL,NULL,NULL FROM INFORMATION_SCHEMA.TABLES -- -

https://ithelp.ithome.com.tw/upload/images/20260926/201841895XUkeG9TcI.png

執行後會發現跑出一大堆表,卻找不到想要的 products 相關資料表

INFORMATION_SCHEMA.TABLES 會把所有 schema 的表都撈出來,包含 PostgreSQL 一堆內建系統表(放在 pg_catalog、information_schema 這些 schema 裡),是資料庫自己運作用的,不是我們的目標,所以清單才會這麼長。

所以接著把範圍縮小到 public(使用者資料放的地方):

category=Gifts' UNION SELECT NULL,NULL,table_name,NULL,NULL,NULL,NULL,NULL FROM INFORMATION_SCHEMA.TABLES where table_schema='public'-- -

https://ithelp.ithome.com.tw/upload/images/20260926/20184189rIjfU7ZEw4.png

這樣清單就乾淨多了,products 也會冒出來,接著就可以繼續往下找。

(3) 尋找 products 這張表有哪些欄位?

category=Gifts' UNION SELECT NULL,NULL,column_name,NULL,NULL,NULL,NULL,NULL FROM INFORMATION_SCHEMA.columns where table_name='products'-- - 

https://ithelp.ithome.com.tw/upload/images/20260926/20184189mqDXQ1lC7R.png

這時候可以在 products 的Render結果形式上,看到散布著 price、rating、name、id 等字眼

感覺應該是資料表的欄位!

我們把price跟rating拿去查,發現他們都屬於<h3>element
https://ithelp.ithome.com.tw/upload/images/20260926/20184189qhnU6eSWUR.png

https://ithelp.ithome.com.tw/upload/images/20260926/20184189jG8j2m6zPq.png

我們大膽假設 裡放的就是資料表的欄位名稱,查<h3>,看到其中有一個<h3>element中,寫著released(感覺很像是我們在尋找的控制顯示/隱藏未上架商品的欄位名稱
https://ithelp.ithome.com.tw/upload/images/20260926/20184189m9rfT3bBy5.png

哪些欄位可以被顯示?

前面已經研究過,把資料放在第 3 個位置時不會報錯、還能正常顯示在畫面上(挖 table_name、column_name 也都是放這裡看到的)。

所以這裡就沿用同樣的做法,把想看的 released 也放到第 3 個位置:

category=Gifts' UNION SELECT NULL,NULL,released,NULL,NULL,NULL,NULL,NULL FROM products-- -

https://ithelp.ithome.com.tw/upload/images/20260926/20184189j0K9APBGTh.png

結果卻出現 error。

為什麼這次報錯了?

前面放 table_name、column_name 都沒問題,怎麼換成 released 就報錯了?

關鍵是 UNION 的規則:兩邊合併時,不只欄位數要一樣,每個對應位置的資料型別也要對得上。

而原本查詢的第 3 個位置放的是文字,所以我們 UNION 上去的第 3 個位置也必須是文字:

  • table_name、column_name 是文字 → 對得上,成功
  • released 是數字(0 或 1) → 型別對不上,報錯

那如果就是想把 released 這個數字欄位塞進這個只吃文字的位置呢?我們可以用 CAST 解決

用 CAST 轉型

CAST 的作用就是把資料轉換成另一種型別。

既然第 3 個位置只吃文字,那就用 CAST(released as varchar) 把 released 轉成文字(varchar 就是字串),型別對上了,就能正常塞進去:

category=Gifts' UNION SELECT NULL,NULL,CAST(released as varchar),NULL,NULL,NULL,NULL,NULL FROM products-- - 

https://ithelp.ithome.com.tw/upload/images/20260926/20184189R6htarzK91.png

這次就可以正常看到 released 的內容了。


再加條件把我們的目標-上架/未上架資料都篩出來

既然現在已經知道:

  • 有一張 products 資料表
  • 有 released 這個欄位
  • released 可以用來判斷產品是否發布(值只有 0 和 1)

那就可以用 where 加入條件來篩資料:

category=Gifts' UNION SELECT NULL,NULL,name,NULL,NULL,NULL,NULL,NULL FROM products where released =0 or released=1-- -

https://ithelp.ithome.com.tw/upload/images/20260926/20184189cE1ztTIc0F.png

恭喜我們!!!方法二也成功了!!!

https://ithelp.ithome.com.tw/upload/images/20260926/20184189aT9K1jfCF7.png


那要怎麼預防?

看完怎麼打,反過來看怎麼防,其實會更懂這個漏洞的本質。

還記得根本問題嗎?網站分不清「哪些是資料、哪些是指令」,把使用者輸入直接拼進 SQL 裡。 所以防禦的核心很簡單——就是不要把使用者輸入直接拼接到查詢語句裡。

而要做到這件事,最主要、也最推薦的方法就是 Prepared Statement(參數化查詢)。

1. Prepared Statement(參數化查詢)— 最有效

概念是:先把 SQL 的「骨架」跟「使用者填的值」分開。骨架是固定的、事先寫好的,值則是另外填進去的。

不安全:把輸入直接拼進字串
    "... WHERE name = '" + 使用者輸入 + "'"

安全:骨架固定,值另外傳
    "... WHERE name = ?"  ← 使用者輸入從這個 ? 傳進去

這樣一來,就算使用者輸入 ' OR '1'='1,它也會被當成單純的「值」來處理,而不會被當成 SQL 指令執行。破口從源頭就被堵住了。

2. 跳脫特殊字元(Escaping / Sanitization)

如果沒辦法用 Prepared Statement,退而求其次的做法是「淨化」輸入——把 ' 這類在 SQL 裡有特殊意義的字元先跳脫處理,讓它們失去破壞力。

不過這方法比較容易有漏網之魚(不同資料庫、不同情境的跳脫規則不一樣),所以是輔助手段,不是首選。

3. 輸入驗證(Input Validation)

對輸入做基本檢查,例如「數量」欄位就只接受數字、Email 就檢查格式。可以擋掉一部分明顯的惡意輸入,但不能單靠它——聰明的 payload 還是可能繞過,它比較像輔助的第二道防線。


本日回顧

今天其實遇到了不少的突發狀況—其中最嚴重的是幾乎所有截圖因為離線上傳失敗,HackMD 一度無法預覽,時間超級緊迫,一度很擔心來不及交。

但後來想想,這或許也是一種培養心性的過程。

滲透測試的實際工作裡,意外本來就是日常。可能正在打一台機器,手邊同時還有三個案子在跑;可能剛準備好要做測試,客戶突然說系統要維護;可能以為某個服務是目標,結果根本不在測試範圍內。

不是所有事情都會按計畫走。

這次遇到截圖上傳失敗、目標靶機突然連線中斷等等——每一個當下都有點慌,但最後還是一個一個解決了。

而我們現在在準備的 CPTS,本來就是一個很艱難的考試。考試裡也會遇到各種意外,沒有人保證環境一定穩定、工具一定順手。

或許這些小插曲,都是在提前練習「遇到意外的時候怎麼保持冷靜、繼續往前」這件事。

滲透工程師需要技術,但也需要在混亂裡保持清醒的能力。

今天算是兩個都練到了 😅

明天的主題是Command Injection!希望明天練習過程一切順順利利!那我們明天見囉~~~


上一篇
Day 11 — Bashed 的完整技術發現:多個問題怎麼串成攻擊鏈
下一篇
Day 13 — Command Injection:一個查庫存的按鈕,竟然能讓伺服器執行指令 👀
系列文
《朝 HTB CPTS 前進:資安新手的 30 天實作筆記》 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言