rwth2011 CTF – mastermind

Mastermind was an easy service, written on Ruby.

Download (mmd.rb and mmd.db)

Summary: SQL Injection, guessable id’s, guessable flag (by id)

Here we have a source code file – mmd.rb and SQLite database file – mmd.db. The functionality is rather simple – you can create a game (random number with a secret string (flag) as a prize), play a game (guess a number) with given id, and list unsolved games.

1. SQL Injection

There are many places to inject:

79: @db.execute "INSERT INTO Games (secret,reward) VALUES (\"#{secret}\", \"#{opts}\")"
80: row = @db.execute("SELECT id FROM Games WHERE secret=\"#{secret}\" and reward=\"#{opts}\" LIMIT 1")
81: send(client, "Created game \"#{row[0][0]}\" with reward \"#{opts}\".")
117: if a == nil or b == nil
118:	rows = @db.execute("SELECT id FROM Games WHERE solved=0")
119: else
120:	rows = @db.execute("SELECT id FROM Games WHERE id >= \"#{a}\" AND id <= \"#{b}\" AND solved=0")
121: end
123: send(client, "Unsolved games: " + rows.join(", "))

The easiest way to fix it is to filter the whole client’s request:

l = client.gets
l = l.gsub(/"/, "")

Though, there is a place where there is no ” in query – we just add it:

@db.execute("UPDATE Games SET solved=1 WHERE id=\"#{id}\"")

The exploit is pretty simple:

$ nc 2323
Greetings, Professor Falken.
How about a nice game of Mastermind?
list, play [id] [guess], new [reward], quit
list " UNION SELECT reward FROM Games ORDER BY id desc--
Unsolved games: FLAG3, FLAG2, FLAG1

So you get all the flags, and the most recent first.

2. Guessing flags

After the gamebot posted a flag, it solved the game: he knew the id and guessed the number’s digits by one. That takes max 60 requests. While he is solving it, we can do a list command and get the flags’ ids. I don’t know how we can fix it, because it seems that listing unsolved games is an important part of the functionality.
But we can shorten guessing time buy changing rand(10**DIGITS) to “000000” – so the bot will make only 6 requests. Funny, don’t?

3. Guessing ids

Now, the enemy can’t list id’s to guess the flags. But id’s are generated by auto_increment in SQLite, so they are 1, 2, 3, …

So, we can bruteforce id’s and guess all the flags. And after each round, we just check s small number of id’s, so it’s rather fast.

The fix is simple:

rand_id = rand(10**10).to_s
@db.execute "INSERT INTO Games (id, secret,reward) VALUES (\"#{rand_id}\", \"#{secret}\", \"#{opts}\")"

So, the new command code is:

if cmd == "new"
    rows = @db.execute("SELECT COUNT() FROM Games WHERE solved=0")
    if rows[0][0] > MAXGAMES
        send(client, "Maximum number of unsolved games already reached.")
        send(client, "Please solve those games first before creating new ones.")
    secret = "0"*DIGITS
    rand_id = rand(10**10).to_s
    @db.execute "INSERT INTO Games (id, secret,reward) VALUES (\"#{rand_id}\", \"#{secret}\", \"#{opts}\")"
    row = @db.execute("SELECT id FROM Games WHERE secret=\"#{secret}\" and reward=\"#{opts}\" LIMIT 1")
    send(client, "Created game \"#{row[0][0]}\" with reward \"#{opts}\".")

Also, there was a bug – MAXGAMES was only 10, so sometimes the service rejected to accept new flags. The fix is to change the constant or to comment out that code.

Leave a Reply

Your email address will not be published.