# SECCON CTF 2022 Quals writeup - English

Thank you for playing SECCON CTF 2022 Quals!
Just like last year, I wrote some challenges for this CTF.

My challenge list:

Challenge Category Difficulty Keywords Solved
skipinx web wamup query parser, DoS 102
easylfi web easy curl, URL globbing, LFI 62
bffcalc web medium CRLF injection, request splitting 41
piyosay web medium Trusted Types, DOMPurify, RegExp 19
denobox web medium-hard prototype pollution, import maps 1
spanote web hard Chrome, disk cache, bfcache 1
latexipy misc easy pyjail, magic comment 8
txtchecker misc medium magic file, ReDoS 23
noiseccon misc medium-hard[1] Perlin noise 22

## [web] skipinx

• 102 teams solved / 100 points
• (TODO: URL to the git repository)

Description:

ALL YOU HAVE TO DO IS SKIP NGINX

• http://skipinx.seccon.games:8080

### Overview

This is a simple server-side challenge.

The sever returns a response of Access here directly, not via nginx :( for your request:

nginx/default.conf:

The nginx adds a query parameter proxy=nginx to each request, and proxies the request to the backend server.

web/index.js:

The backend server returns a flag only if a request doesn't have proxy=nginx.

Can you access the backend server without going through nginx?

### Solution

Express uses qs as a default query parser:

Also, Express uses default values for options on qs:

Option parameterLimit specifies the maximum number of query parameters and the default value is 1000.

The parameter is used in:

As you can see, Express ignores parameters after a parameterLimit-th parameter.
Thus, if you send a request with more than 1000 query parameters, proxy=nginx is ignored.

## [web] easylfi

• 62 teams solved / 124 points
• (TODO: URL to the git repository)

Description:

• http://easylfi.seccon.games:3000

### Overview

This is server-side challenge.

You access the server:

If you submit test, the server redirects to /hello.html?%7Bname%7D=test:

Source code (web/app.py):

The goal is stealing a flag from /flag.txt.

### Solution

#### Step 1: path traversal

The server uses curl to read files:

Unfortunately, path traversal to /flag.txt is prevented:

By the way, curl has a feature of URL globbing, and you can access multiple resources at the same time.
You can bypass the above defense using this feature:

However, the following WAF hides the flag response:

#### Step 2: bypassing WAF

The server returns a response after the following process:

The implementation of the template engine is as follows:

Is it possible to show the flag string without SECCON by abusing this template engine?

The first important point is that validate("{") is True. You can bypass it with this bug and URL globbing.

• URL: file:///app/public/{.}./{.}./{app/public/hello.html,flag.txt}
• params:

The process in the template engine is as follows.

The initial state:

"{name}""{":

"{""}{":

"{!</h1>\n</body>\n</html>\n--_curl_--file:///app/public/../../flag.txt\nSECCON}""":

## [web] bffcalc

• 41 teams solved / 149 points
• (TODO: URL to the git repository)

Description:

There is a simple calculator!

• http://bffcalc.seccon.games:3000

### Overview

This web service is a simple calculator:

docker-copmose.yml:

• nginx: It proxies requests to bff and report
• bff: It serves static files and proxies requests to backend.
• backend: It evaluate a simple expression and returns the result.

The server uses cherrypy as a framework. A bot sets a flag as a cookie value with a HttpOnly attribute.

### Solution

#### Step 1: XSS

Firstly, there is a trivial XSS vulnerability in index.html:
index.html

However, you cannot read the flag cookie since it has a HttpOnly attribute.

#### Step 2: CRLF injection

bff's proxy process to backend is as follows:

bff constructs HTTP requests and sends them using socket.

Herein, the process for headers in cherrypy is as follows:

cherrypy decodes headers following RFC 2047. So, you can attack with CRLF injection by sending decoded \r\n on headers.

Is it possible to construct a request whose response includes the flag cookie with CRLF injection?

backend uses waitress as a WSGI to cherrypy. The following implementation is important to solve this challenge:

If the first line of a HTTP request is invalid, waitress returns the invalid HTTP method in the body. By abusing this behavior, you can steal the flag from the response body.

There are three points to steal the flag:

• HTTP request splitting with CRLF injection
• Adjusting a Content-Length value of the first splitted request so that the cookie value is at the position of the HTTP method of the second splitted request
• Adding another cookie so that the second request matches with the following regex:
• E.g. document.cookie = '/?a=b HTTP/1.1'
• Then, the first line is flag=SECCON{real_flag}; /?a=b HTTP/1.1 and it matches with the regex.

## [web] piyosay

• 19 teams solved / 210 points
• (TODO: URL to the git repository)

Description:

I know the combination of DOMPurify and Trusted Types is a perfect defense for XSS attacks.

• http://piyosay.seccon.games:3000

### Overview

• This is a client-side challenge.
• CSP: trusted-types default dompurify; require-trusted-types-for 'script'
• A bot's has a flag as a cookie value.

The essential code in this challenge is only the following part of web/result.html:

### Solution

#### Step 1: XSS with bypassing Trusted Types

The settings of Trusted Types is as follows:

For example, you can bypass it to XSS with the following payload:

However, you cannot steal a flag from document.cookie because the flag is deleted in:

#### Step 2: RegExp in DOMPurify

By the way, what is the following line in createHTML?

JavaScript has interesting(?) behavior in RegExp:

"".match(/^\$/) is a process to delete values of the above static properties. If this line does not exist, you can steal the flag from RegExp.input with:

By the way, DOMPurify uses regular expressions when it sanitizes strings:

This fact is useful to solve this challenge.

#### Step 3: just a XSS puzzle game!

You are ready to steal the flag.

Example URL:

If you report this URL, the server of ATTACK_BASE_URL will receive ECCON{real_flag} SECCON{REDACTED}">.

## [web] denobox

• 1 teams solved / 500 points
• (TODO: URL to the git repository)

Description:

Your program runs in a sandbox!

• http://denobox.seccon.games:3000

### Overview

This is a Deno sandbox challenge.

• The server-side language is Rust.
• The server creates a TypeScript program and executes it using deno run as a subprocess.
• Permission option: --allow-write=.

You can generate a TypeScript program with user-defined parts under the constraints of a validator:

You can execute your program with specified JSON as input data:

You can get the JSON result of the execution:

The {{FLAG}} in source code is replaced with a flag string:

The goal is to steal the flag string in this if statement.

### Solution

#### Step 1: prototype pollution

The validator limits user-defined parts by traversing AST of TypeScript.
Example limitation:

There is a trivial Prototype Pollution vulnerability. Also, unlike usual Prototype Pollution, you can pollute something by methods of some built-in Objects (E.g., Object, String, and Array).

By Prototype Pollution, can you do something in the following parts:

Interestingly, by the following pollution, you can specify an arbitrary string for crypto.randomUUID().replaceAll("-", ""):

So, you can specify the name of the output JSON file with a suffix .json.

#### Step 2: import maps in Deno

From v1.18, Deno has a feature of auto-discovery of the config file:

In this challenge settings, if there is deno.json in the current directory, the deno command reads it as a config file. This is possible using the Prototype Pollution described in Step 1.

You will notice a interesting property importMap if you check the schema of the configuration:

Import maps:

Using this property, you can assign https://deno.land/std@0.161.0/crypto/mod.ts into an arbitrary file. Of course, it includes your JavaScript file served on your server!

Thus, you can do RCE! However, note that there is a permission --allow-write=. and you cannot read the source code.

#### Step 3: JavaScript Proxy

The goal is to hook any process in the following:

It is possible using JavaScript Proxy:

### Solver

evil.js:

index.js:

## [web] spanote

• 1 teams solved / 500 points
• (TODO: URL to the git repository)

Description:

Single Page Application makes our note app simple.

• http://spanote.seccon.games:3000

### Overview

There is a simple note application.

Create a note:

Delete a note:

• The bot accesses a reported URL after creating a note with a flag string.
• There is no CSP, but it is seemingly impossible to do XSS

### Solution

#### Step 1: Understanding cache behavior in Google Chrome

Let me get straight to the point, in my solution, you can XSS by abusing cache behavior in Google Chrome. To solve this challenge, you need to have some knowledge of cache behavior (or experiment it).

There are two impotant types of cache:

As a interesting point of disk cache, the cache includes not only the HTTP response rendered to a web page, but also those fetched with fetch. In other words, if you access the URL for a fetched resource, the browser will render the resource on the page.

There is another important point. If both disk cache and bfcache are valid for an accessed page at back/forward navigations, the bfcache has priority over the disk cache. So, it is necessary to have a situation where bfcache is disabled to trigger the above behavior of disk cache.

#### Step 2: Rendering a fetch response with disk cache

Let's try the interesting behavior in this challenge.

Firstly, you have to disable bfcache[2]. There are many conditions where bfcache is disabled, the list is:

The easy way is to use RelatedActiveContentsExist.

Therefore, the following procedure reproduces the behavior:

1. Access a web page (E.g. https://example.com)
2. Execute open("http://spanote.seccon.games:3000/api/token")
• The server returns a response with 500 status code.
3. In the opend tab, access http://spanote.seccon.games:3000/
• Then, the response of http://spanote.seccon.games:3000/api/token is cached as a disk cache.
4. Execute history.back()
• The cached JSON response is rendered on the page!

You can confirm that disk cache is used using DevTools in Google Chrome:

#### Step 3: HTML rendering with handling Content-Type

This web service returns responses only with application/json or application/octet-stream. So you cannot do XSS by rendering them.

Herein, note that notes are served with @fastify/static:

The implementation is as follows:

The Content-Type is defined according to the extension of a served file. The extension for text/html is .html.

By the way, there is a trivial CSRF vulnerability for two APIs to create/delete a note. So you can call them freely.

The API to delete a note is as follows:

If you call the API with noteId=<img src=0 onerror="alert(1)">.html, Content-Type of the response for

is text/html:

If you render it by the above technique, a XSS occurs:

#### Step 4: Code golf

Note that if the XSS payload is too long, you cannot use it as a part of URL and the XSS fails.
Implementation of fastify:

The limitation is 100 characters, so you have to play code golf.

### Solver

public/index.html:

public/back.html:

index.js:

## [misc] latexipy

• 8 teams solved / 305 points
• (TODO: URL to the git repository)

Description:

Latexify as a Service

### Overview

The service returns a $\LaTeX$ expression for a given function.

For example:

Source code:

Flag location: /flag.txt

### Solution

The limitation using AST prevents trivial RCEs.

As a important point, ast.parse ignores comments in the source code. By the way, Python has a feature called magic comment:

Magic comment is just a comment in get_fn_name, but it is recognized as a magic comment for module imports:

In fact, you can bypass it with UTF-7:

+AAo- is \n on the UTF-7 encoding, and the above code as a module is:

It is also possible to bypass it using other encodings, e.g. raw_unicode_escape and unicode_escape.

## [misc] txtchecker

• 23 teams solved / 193 points
• (TODO: URL to the git repository)

Description:

I'm creating a text file checker. It still in the process of implementation...

### Overview

Source code (a bash script):

There are only three lines!
The server executes the script when a player logins with SSH.

Flag location: /flag.txt

### Solution

#### Step 1: Magic file injection

You can specify the arguments of the file command.

man file:

You can specify a magic file with -m option and some special files (e.g. /dev/tty and /proc/self/fd/0).

However, you cannot get the result of the file command somce the server does not output anything.

#### Step 2: A time-based attack with ReDoS

man magic:

You can use regex, so you can also do ReDoS! Try a time-based attack with ReDoS.

## [misc] noiseccon

• 22 teams solved / 197 points
• (TODO: URL to the git repository)

Description:

Noise! Noise! Noise!

### Overview

Source code:

The server returns a noise image using https://github.com/josephg/noisejs.

Example noise:

The noise is generated with an algorithm called Perlin noise:

### Solution

#### Step 1: Finding coordinates of lattice

flagInt/scakeX/scaleY affect only the offsets of the noise. In other words, you may extract flag information from the "position" of a noise.

Implementation of Perlin noise:

Each gradient gradP is defined on a seed value, and parlin2(x, y) is computed using the gradients of four neighbour lattice points of (x, y). The value is in $\left[-1, 1\right]$.

Also, each gradient is selected randomly from:

We are considering two dimensions in this challenge, so the candidates of gradients are as follows:

$\begin{pmatrix}1\\1\end{pmatrix}, \begin{pmatrix}-1\\1\end{pmatrix}, \begin{pmatrix}1\\-1\end{pmatrix}, \begin{pmatrix}-1\\-1\end{pmatrix}, \begin{pmatrix}1\\0\end{pmatrix}, \begin{pmatrix}-1\\0\end{pmatrix}, \begin{pmatrix}0\\1\end{pmatrix}, \begin{pmatrix}0\\-1\end{pmatrix}$

Herein, consider the following state:

• gradP[X+perm[Y]] $= \begin{pmatrix}0\\ \plusmn 1\end{pmatrix}$
• gradP[X+1+perm[Y]] $= \begin{pmatrix}0\\ \plusmn 1\end{pmatrix}$

Then,

$\forall x\in \left[X, X+1\right], \mathtt{perlin2}(x, Y) = 0\textrm{.}$

Proof:

For $\forall x\in \left[X, X+1\right]$,

• n00: $n_{00} = \begin{pmatrix}0\\\plusmn 1\end{pmatrix} \cdot \begin{pmatrix}x - \lfloor x \rfloor \\ Y - \lfloor Y \rfloor\end{pmatrix} = \begin{pmatrix}0\\\plusmn 1\end{pmatrix} \cdot \begin{pmatrix}x - \lfloor x \rfloor \\ 0\end{pmatrix} = 0$
• n10: $n_{10} = \begin{pmatrix}0\\\plusmn 1\end{pmatrix} \cdot \begin{pmatrix}x - \lfloor x \rfloor -1 \\ Y - \lfloor Y \rfloor\end{pmatrix} = \begin{pmatrix}0\\\plusmn 1\end{pmatrix} \cdot \begin{pmatrix}x - \lfloor x \rfloor -1 \\ 0\end{pmatrix} = 0$

and $\mathtt{fade}(Y - \lfloor Y \rfloor) = 0$. So,

$\mathtt{perlin2}(x, Y) = \mathtt{lerp}\left(n_{00}, n_{10}, \mathtt{fade}(x - \lfloor x \rfloor) \right) = \mathtt{lerp}\left(0, 0, \mathtt{fade}(x - \lfloor x \rfloor) \right) = 0 \,{}_\blacksquare$

Conversely, it is not true in general at other cases.

Thus, if the size of the interval of $x$ such that each $\mathtt{perlin2}(x, y_0)$ is $0$ with a fixed integer $y_0$ is $1$, let $x_0$ be an endpoint of the interval. Then, $(x_0, y_0)$ is one of the lattice points with high probability.

The source code for the experiment:

The result show an interval between x=400 and x=500 as a the lattice size. So you can find the "position" of the lattice.

#### Step 2: An oracle for each bit

Source code:

For noise.perlin2(offsetX + x * 0.05, offsetY + y * 0.05), The offsetX and offsetY contribute only their fractional parts to the position of lattice.

Based on these factors, you can construct an oracle to identify 0/1 for each bit of a flag. Please see the following solver for details.

### Flag

1. Because of my lack of consideration, many players solved this challenge by unintended solutions ↩︎

2. In fact, you can skip this step because bfcache is disabled by default options of puppeteer. ↩︎