DiceCTF-2022 (web/misc)

06 Feb 2022

Prelude

This last weekend I competed in my first CTF competition that wasn’t DAMCTF or a weekly challenge at OSUSEC’s CTF-LEAGUE. I spent a lot of time trying to solve various web challenges but to no avail–nonetheless I managed to learn a thing or two about XSS attacks which I hope will prove valuable come future competition.

Nonetheless, I did manage to help find solutions to two relatively easy challenges so here’s the write-ups for those:

Challenge Write-Ups

undefined (misc)

For this challenge we’re given a node script file which sets almost every global object to undefined and we then have to figure out how to read a flag file through an unprotected eval line.

#!/usr/local/bin/node
// don't mind the ugly hack to read input
console.log("What do you want to run?");
let inpBuf = Buffer.alloc(2048);
const input = inpBuf.slice(0, require("fs").readSync(0, inpBuf)).toString("utf8");
inpBuf = undefined;

Function.prototype.constructor = undefined;
(async () => {}).constructor.prototype.constructor = undefined;
(function*(){}).constructor.prototype.constructor = undefined;
(async function*(){}).constructor.prototype.constructor = undefined;

for (const key of Object.getOwnPropertyNames(global)) {
    if (["global", "console", "eval"].includes(key)) {
        continue;
    }
    global[key] = undefined;
    delete global[key];
}

delete global.global;
process = undefined;

{
    let AbortController=undefined;let AbortSignal=undefined;let AggregateError=undefined;let Array=undefined;let ArrayBuffer=undefined;let Atomics=undefined;let BigInt=undefined;let BigInt64Array=undefined;let BigUint64Array=undefined;let Boolean=undefined;let Buffer=undefined;let DOMException=undefined;let DataView=undefined;let Date=undefined;let Error=undefined;let EvalError=undefined;let Event=undefined;let EventTarget=undefined;let FinalizationRegistry=undefined;let Float32Array=undefined;let Float64Array=undefined;let Function=undefined;let Infinity=undefined;let Int16Array=undefined;let Int32Array=undefined;let __dirname=undefined;let Int8Array=undefined;let Intl=undefined;let JSON=undefined;let Map=undefined;let Math=undefined;let MessageChannel=undefined;let MessageEvent=undefined;let MessagePort=undefined;let NaN=undefined;let Number=undefined;let Object=undefined;let Promise=undefined;let Proxy=undefined;let RangeError=undefined;let ReferenceError=undefined;let Reflect=undefined;let RegExp=undefined;let Set=undefined;let SharedArrayBuffer=undefined;let String=undefined;let Symbol=undefined;let SyntaxError=undefined;let TextDecoder=undefined;let TextEncoder=undefined;let TypeError=undefined;let URIError=undefined;let URL=undefined;let URLSearchParams=undefined;let Uint16Array=undefined;let Uint32Array=undefined;let Uint8Array=undefined;let Uint8ClampedArray=undefined;let WeakMap=undefined;let WeakRef=undefined;let WeakSet=undefined;let WebAssembly=undefined;let _=undefined;let exports=undefined;let _error=undefined;let assert=undefined;let async_hooks=undefined;let atob=undefined;let btoa=undefined;let buffer=undefined;let child_process=undefined;let clearImmediate=undefined;let clearInterval=undefined;let clearTimeout=undefined;let cluster=undefined;let constants=undefined;let crypto=undefined;let decodeURI=undefined;let decodeURIComponent=undefined;let dgram=undefined;let diagnostics_channel=undefined;let dns=undefined;let domain=undefined;let encodeURI=undefined;let encodeURIComponent=undefined;let arguments=undefined;let escape=undefined;let events=undefined;let fs=undefined;let global=undefined;let globalThis=undefined;let http=undefined;let http2=undefined;let https=undefined;let inspector=undefined;let isFinite=undefined;let isNaN=undefined;let module=undefined;let net=undefined;let os=undefined;let parseFloat=undefined;let parseInt=undefined;let path=undefined;let perf_hooks=undefined;let performance=undefined;let process=undefined;let punycode=undefined;let querystring=undefined;let queueMicrotask=undefined;let readline=undefined;let repl=undefined;let require=undefined;let setImmediate=undefined;let setInterval=undefined;let __filename=undefined;let setTimeout=undefined;let stream=undefined;let string_decoder=undefined;let structuredClone=undefined;let sys=undefined;let timers=undefined;let tls=undefined;let trace_events=undefined;let tty=undefined;let unescape=undefined;let url=undefined;let util=undefined;let v8=undefined;let vm=undefined;let wasi=undefined;let worker_threads=undefined;let zlib=undefined;let __proto__=undefined;let hasOwnProperty=undefined;let isPrototypeOf=undefined;let propertyIsEnumerable=undefined;let toLocaleString=undefined;let toString=undefined;let valueOf=undefined;

    console.log(eval(input));
}

After an hour or two of messing around with hidden properties __stdin/__stderr of the console object (I thought it might have some associated internal functionality with which we might be able to read the flag file–which is probably impossible) I stumbled upon a pretty neat detail: import wasn’t undefined!

undefined_cheese.png

This lead to an easy exploit of import('fs').then(function(fs){fs.readFile('/flag.txt', 'utf8', function(err,data){console.log(data)})}) to get the flag dice{who_needs_builtins_when_you_have_arguments}!

This was obviously not the intended solution–the intended solution according to the challenge author was the following: (function(){return arguments.callee.caller.arguments[1]("fs").readFileSync("/flag.txt", "utf8")})()–which basically works since the node runtime wraps the require function in such a way that we can get it via the property chain you see in the intended exploit (read author write-up for details).

knock-knock

Alright so this was pretty much the only challenge I was actually able to solve.

Basically we are given a fairly boiler plate web-app which allows us to create “notes” and access them by a cryptographic hash (i.e. we view a note by supplying its id and the token created when it was generated).

Alright, so I began by checking where the flag is located–in this case it’s supplied in an environment variable which is included in the first note generated by the app.

const db = new Database();
db.createNote({ data: process.env.FLAG });

const express = require('express');
const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));

So basically I need to find the token for the note whose id=0. Looking at the database constructor we see some curious code:

const crypto = require('crypto');

class Database {
  constructor() {
    this.notes = [];
    this.secret = `secret-${crypto.randomUUID}`;
  }
  <more code here>
}

Notice that the crypto.randomUUID isn’t actually being called… in fact by using some console.log statements in the app we can see that it’s actually setting the secret to a string of the function’s code …which will be identical on every execution of said program.

After adding a console.log statement and running the supplied Dockerfile the program spit out the token 33dfdd227c26ed77238f16db7f2c78376598da8e0bd85aa88452f8e13f49a643 and we can access the flag of dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}!

blazingly-fast

Ok–this one I didn’t solve but I thought it was really cool challenge and so I wanted to highlight the challenge someway. As such, here’s a comphrensive write-up on that challenge by one of the organizers.