$ / 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
flags redacted; flag images and local flag.txt files removed