..

corCTF 2024: web/erm

Solution writeup

Starting point:

erm guys? why does goroo have the flag?

  • live challenge link
  • erm.tar.gz containing app source

First impressions

It’s a library of fake CTF challenge writeups!

The tarball contains everything needed to run the app locally, and the ORM code in db.js helpfully generates random data. The flag is a field on goroo’s Member object, but the endpoint app.js provides to list users only lists the ones who haven’t been kicked. Unfortunately, goroo was kicked :(

The other app endpoints return information about challenges and categories. Users are associated to challenges, and the /api/writeup/{slug} endpoint returns a writeup with its author’s user info. Sadly, goroo also is not the author of any challenges, so it’s time to find a vuln.

Bug hunting

Easily ruled out:

  • non-api routes - they all get the actual data from /api/* routes
  • /api/members - straightforward listing of non-kicked members
  • /api/writeup/:slug - either returns a writeup, or doesn’t

Only /api/writeups is left. The frontend passes a parameter to this endpoint:

GET /api/writeups?where[category]={category}

How is that filter implemented? Let’s have a look:

db.Writeup.findAll(req.query)

Wait, so the query params object is just passed directly to an ORM method? Is there a custom implementation?

Nope, no custom implementation. We have full control of the options passed to Sequelize’s findAll, via Express’s magic query param handling!

The solve

Sequelize provides a way to not only include associated objects (Member, author of Writeup), but to nest included associations. This allows traversing transitive associations. The only trick is constructing a set of query params to get members for categories for authors for writeups.

My expectations from the docs didn’t quite match how the app handled my inputs. I had to experiment a bit and add debug logging to my local instance to get a working exploit. After a bit of trial, error, and inference, I arrived at:

GET /api/writeups?include[all]=All&include[include][all]=All&[include][include][all]=All

This returned a huge mess of JSON data, which I passed through a formatter and grep to extract the flag.

themed with nostyleplease