Bobles and Narnes - First-Person Writeup¶
Challenge Overview¶
I analyzed a Bun + Express bookstore app where I started with $1000, but the Flag item cost $1,000,000.
At first glance, that looked impossible to buy directly.
The target behavior was:
- Add items to cart via
/cart/add. - Checkout via
/cart/checkout. - Receive a ZIP of purchased files.
The key idea was to make the server think the expensive flag item was a sample during the price check, but store it as a full item in the database before checkout.
Initial Recon¶
I reviewed the main backend logic in server.js.
Important parts I identified:
- Cart schema stores
is_sampleincart_items(server.js:40). - Price check in
/cart/addexcludes any product whereis_sampleis truthy (server.js:138). - Cart entries are bulk inserted using
await db\INSERT INTO cart_items ${db(cartEntries)}`(server.js:150`). - During checkout, file selection depends on DB
item.is_sample; truthy gives*_sample, falsy gives full file (server.js:170).
I also checked frontend behavior in site/main.js:
- UI sends
is_sample: true/falsefrom button text (site/main.js:56,site/main.js:61). - There is no server-side validation that request objects have consistent keys.
Root Cause¶
The exploit comes from a mismatch between:
- Price calculation using raw user JSON (
productsToAdd) before insert. - Bun SQL helper
db(cartEntries)inferring insert columns from object keys.
In my exploit script, I exploited this by sending two objects in one products array:
- First object: cheap book with no
is_samplekey. - Second object: flag book with
is_sample: 1.
Because the first object lacks is_sample, the Bun helper builds insert columns without is_sample, so the second object’s is_sample is dropped on insert.
That leaves DB rows with is_sample = NULL.
Why this works:
- Add-time price check:
- For the flag object,
is_sample = 1, so it is treated as sample and excluded fromadditionalSum(server.js:138). - Only the cheap item is charged, so request passes.
- Checkout-time file selection:
- Inserted
is_sampleisNULL, which is falsy. - Falsy branch serves the full file (
flag.txt) instead offlag_sample.txt(server.js:170).
I confirmed the challenge intentionally includes only books/flag_sample.txt, which contains just lactf{, while the real flag.txt is available remotely through the vulnerable checkout path.
Exploit Script¶
I used a short exploit script that:
- Registers a random user.
- Sends crafted
/cart/addJSON with mixed keys. - Calls
/cart/checkout. - Parses returned ZIP and prints file contents.
Critical payload:
{
"products": [
{"book_id": "a3e33c2505a19d18"},
{"book_id": "2a16e349fb9045fa", "is_sample": 1}
]
}
Result¶
The saved output in out shows successful exploitation:
- Remaining balance after add:
990. - ZIP contained
flag.txtandpart-time-parliament.pdf. flag.txtcontent was printed directly.
Recovered flag:
lactf{hojicha_chocolate_dubai_labubu}