# SECCON CTF 2021 author writeup (4 web challenges)

Thank you for playing SECCON CTF 2021! I hope you had fun.
I created the following web challenges in the CTF:

This post describes author writeups and unintended solutions[1] for the above 4 challenges.
If you know other solutions, please report to me.

## Sequence as a Service 1

Description:

I've heard that SaaS is very popular these days. So, I developed it, too. You can access it here.

### Overview

In the endpoint /api/getValue, you can post a pair of a sequence and an index ($n$) to get the $n$-th value of the sequence.

The web service parse and evaluate the given sequence with LJSON in a subprocess.
LJSON provides a parse function and a stringify function for extended JSON with pure functions. Moreover, LJSON.parseWithLib(lib, ...) enable to use functions in lib.

The flag location is /flag.txt in the remote server.

### Solution

My solution is to gain RCE by injecting a function into the prototype of lib.

### Unintended Solutions

[ \$("self").__proto__ = require('child_process') ]

[ Using a bug in the parser of LJSON ]

## Sequence as a Service 2

Description:

NEW FEATURE: You can get values from two sequences at the same time! Go here.

### Overview

In the endpoint /api/getValue, you can post two pairs of a sequence and an index ($n$) to get the $n$-th value for each sequence.

This challenge differs from SaaS 1 in the following points:

• lib doesn't have the function self.
• For each request, LJSON.parseWithLib is executed twice.

### Solution

My solution is to gain RCE by Prototype Pollution.

LJSON.parse disallows usage of variables that don't exist in the scope:

However, by the above Prototype Pollution, you can use the variable (function) eval although it doesn't exist in the scope.

### Unintended Solutions

[ Prototype Pollution to Array.prototype.join ]

[ require('./lib.js').__proto__ = require('child_process') ]

[ Prototype Pollution using toString defined as an impure function ]

(reported from ./V)

[ Using a bug in LJSON's parser ]

Description:

Do you like cookies? If so, go here now!

### Overview

This challenge provides the following web page:

You can modify {{VIEW}} with query parameter view. So, it enables content injection. However, you cannot attack with XSS by CSP:

In this challenge, you can report a URL and then a bot accesses it with a flag cookie.

### Solution

This challenge is a DOM Clobbering puzzle. The goal is as follows:

• No errors occur.
• window2 === window3 is true.
• window1.location.origin can be changed as you like. So, you can make the bot redirect to your server.

My intended DOM Clobbering:

The mechanism of this DOM Clobbering in Chrome is as follows.

Now, consider the following HTML and ?window=form:

window.form is an instance of HTMLCollection because there are multiple elements with id="form". The instance has the <input> element and the <a> element.

1. window.form.form is the <input> element because it has id="form" attribute.
2. window.form.form.form is the <form> element because HTMLInputElement has form property.
3. window.form.form.form.form is the <input> element.
4. window.form.form.form.form.form is the <form> element.
5. window.form.form.form.form.form.form is the <input> element.
6. ...
7. window2 is the <input> element.
8. ...
9. window3 is the <input> element.

Then, window2 === window3 is true.

1. window.form.location is the <a> element because it has name="location" attribute.
2. window.form.location.origin is "https://example.com" because HTMLAnchorElement has origin property.
3. window.form.location.pathname is "/" because HTMLAnchorElement has pathname property.

Then, the page is redirected to https://example.com/.

### Other Solutions

You can exploit with not form property, but another property.

[ DOM Clobbering with parentNode property ]

[ DOM Clobbering with parentElement property ]

• http://web:3000/?window=parentElement&view=<form id="parentElement" name="parentElement"><input name="parentElement"></form><a id="parentElement" name="location" href="http://evil.example.com/"></a>
(reported from ./V)

[ DOM Clobbering with ownerDocument property without <form> ]

## x-note

Description:

Here is a secure note app!
Flag format: SECCON{[_0-9a-z]+}

### Overview

This challenge provides a web service:

• A user can create an account, login, and logout.
• A user can post a note with a string.
• A user can search for notes that contain a string.
• A user can report a URL to a bot.
• The bot accesses it after creating an account and posting a flag note.

### Solution

My solution is a XS-Search attack. The goal is to construct an oracle to judge the prefix of a flag.

#### Step 1: CSRF and posting object notes

You can make a bot post notes as objects by CSRF.
For example, if the form body is

then the note is the following object:

Note the following:

• The note will cause an error when rendered in EJS because the toString is not a function.
• The note will come up with searches for ?search=SECCON{a, ?search=SECCON{b, ?search=SECCON{c, and so on.

#### Step 2: Two kinds of EJS rendering errors

There are two kinds of errors caused by note[toString]=x in EJS rendering.

Error A (if the note is hit first for a search):

Error B (otherwise):

In this web servcie, if an error occurs, the request is redirected to an error page:

This redirect is implemented without encoding (e.g. encodeURIComponent). It means that the Error A adds a query parameter " filteredNotes" to the redirected request:

#### Step 3: Infinite redirects and finite redirects

The key factor in this step is a validation for request parameters:

Now, consider the following search URL:

The redirected URL for Error A is

Then, this query parameters are parsed as follows:

The redirected request violates the validation for request parameters because req.query[" filteredNotes"].length > 500 is true. So, it is redirected to the error page again and the second redirected request also violates the validation. This means that infinite redirects will occur.

On the other hand, the query parameters of the redirected URL for Error B are parsed as follows:

The redirected request passes the validation because req.query[" filteredNotes"].length > 500 is false.

Thus, the two kinds of errors can make the difference between infinite redirects and finite redirects.

#### Step 4: XS-Leak with frame counting

The templete of the error page is

Because this page outputs unescaped msg, you can cause Content Injection there. However, XSS Injection is banned by CSP:

Now, consider the output of msg contains <iframe></iframe>.

If the error page is rendered, it increments window.length. However, if infinite redirects occur, the page is not rendered and window.length is not incremented.

By a well-known technique[2], you can access the window.length from a cross-site page. So, you can detect whether infinite redirects have occurred.

#### Exploitation code

Therefore, you can construct an oracle to judge the prefix of a flag by combining the above steps.

You can steal the flag by serving the following pages on your server:

### Unintended Solutions

[ Using <meta name="referrer" content="unsafe-url"> to judge the two kinds of EJS errors ]

1. I welcome unintended solutions because they help me learn something and create diversity in the challenges (but, as an author, I should emit no unintended solutions to maintain the quality of challenges). ↩︎

2. Frame Counting | XS-Leaks Wiki: https://xsleaks.dev/docs/attacks/frame-counting/ ↩︎