Skip to content

xark

In Square CTF 2022, 250 points

Guest challenge by Alok Menghrajani.

http://chals.2022.squarectf.com:4105

Challenge files: xark.zip

The website allows a user to query a database via the query builder library knex.

js
knex.schema.hasTable('crushes').then(function(exists) {
    if (!exists) {
        console.log("crushes table doesn't exist, initializing...");
        knex.schema.createTable('crushes', function(table) {
            table.increments('id').primary();
            table.string('from').notNullable();
            table.string('to').notNullable();
            table.string('message').notNullable();
            table.index(['to']);
        }).then();
        knex('crushes').insert({
            from: config.init.flag,
            to: config.init.flag,
            message: 'This is the flag!',
        }).then();
    }
});


app.post('/data', async (req, res) => {
    if (req.body.to) {
        const crushes = await knex('crushes')
            .select()
            .where({
                to: req.body.to
            })
            .limit(50);
        res.send(crushes);
    } else {
        res.status(400).send({});
    }
});

The app employs two express plugins to parse HTTP request bodies:

js
app.use(express.json());
app.use(express.urlencoded({
    extended: false
}));

While we are not able to send complex objects (anything other than a string) to the app via urlencoded bodies (as express.urlencoded extended mode is turned off), we can do so using JSON bodies instead.

Notably, as we have seen in mongodb nosql injection challenges, passing complex objects to database query engines usually turn out to be a bad idea, especially in weakly and dynamically typed languages like javascript. However, I had never seen such a vulnerability outside of mongodb.

The issue here arises from the combination of knex and the mysqljs library.

Let's look at the query code:

js
  const crushes = await knex('crushes')
            .select()
            .where({
                to: req.body.to
            })
            .limit(50);

Suppose req.body.to is an array, like [0]. Then knex would generate a 'parameterized' SQL query like this:

query: select * from `crushes` where `to` = ? limit ?
bindings: [ [0], 50 ]

which would be passed to the underlying database driver library to handle.

When MySQLjs is used, the Connection.query function is used, which calls Connection.format to combine the SQL query and bound values to a raw SQL query.

Connection.format eventually calls SqlString.format which calls SqlString.escape on each of the bound values. If the bound value is an array, SqlString.arrayToList is called:

js
SqlString.arrayToList = function arrayToList(array, timeZone) {
  var sql = '';

  for (var i = 0; i < array.length; i++) {
    var val = array[i];

    if (Array.isArray(val)) {
      sql += (i === 0 ? '' : ', ') + '(' + SqlString.arrayToList(val, timeZone) + ')';
    } else {
      sql += (i === 0 ? '' : ', ') + SqlString.escape(val, true, timeZone);
    }
  }

  return sql;
};

In our case, the array [0] is simply converted to 0.

This seems slightly odd, but I think MySQLjs expects that array parameters be enclosed in parentheses in the SQL query. See this stackoverflow answer.

After all this, the raw SQL query returned by MySQLjs is

sql
select * from `crushes` where `to` = 0 limit 50

Since to is a string field, and comparing strings to 0 or false returns true in MySQL, the query is equivalent to

sql
select * from `crushes` where 1=1 limit 50

which returns the flag.

Interestingly this is already a known issue which has been open for over 6 years.