# Exploiting the Source Engine (Part 2) - Full-Chain Client RCE in Source using Frida

Full server-to-client RCE chain in Source Engine 1: array index underflow in CL_CopyExistingEntity plus a ZIP pakfile negative-size stack infoleak defeat ASLR for arbitrary code execution.

- Payout: unconfirmed
- Severity: critical
- Program: Valve
- Credited: Geebz
- Disclosed: 2021-05-04
- Write-up quality: 9/10
- Original: https://ctf.re//source-engine/exploitation/2021/05/01/source-engine-2/
- Library page: https://bugbountylibrary.com/r/blog%3Actf-re-source-engine-exploitation-2021-05-01-source-engine-2
- Tags: domain:desktop impact:data-leak impact:rce kind:bounty platform:blog sev:critical tech:windows technique:chained technique:source-review vuln:info-disclosure vuln:memory-corruption vuln:rce

**Target:** Source Engine 1 client (hl2.exe) via malicious game server
**Root cause:** CL_CopyExistingEntity passes attacker-controlled m_nNewEntity to GetClientNetworkable, which indexes m_EntityCacheInfo without bounds checks (Asserts compiled out in release), returning an arbitrary IClientNetworkable* whose vtable call is hijacked. A separate pakfile ZIP negative-size bug causes uninitialized stack memory to be uploaded to the server, leaking a return address to defeat ASLR.
**Steps:**
1. Craft BSP with embedded pakfile containing a file whose ZIP uncompressedSize is negative, bypassing the signed net_maxfilesize check
2. Server sends RequestFile for that file; client's CNetChan::SendSubChannelData reads past EOF into a 0x100 stack buffer and uploads uninitialized stack contents repeatedly
3. Parse leaked fragments to recover a bf_read::ReadBytes return address (Engine_Leak2) and compute engine.dll base
4. Replicate sv_mumble_positionalaudio ConVar to place a fake vtable pointer at its m_pszString (aligned via entitylist offset)
5. Send NET_Tick to set g_ClientGlobalVariables->tickcount to a stack pivot gadget address
6. Send SVC_PacketEntities with m_nNewEntity=0x26DA to trigger GetClientNetworkable OOB, returning &sv_mumble_positionalaudio->m_pszString as fake IClientNetworkable*
7. Virtual call at vtable+0x1C dereferences tickcount as function pointer, pivoting to ROP chain in packet buffer
8. ROP chain calls ShellExecuteA on sv_downloadurl value (e.g. C:/Windows/System32/winver.exe) to execute arbitrary code
**How found:** Audited low-level networking code and noticed GetClientNetworkable's bounds Asserts are stripped in release builds, making m_nNewEntity an arbitrary index into a global array. For the infoleak, noticed pakfile files can be requested back from the client and that g_pFileSystem->Size reads uncompressedSize from the ZIP header as a signed int, allowing negative sizes to bypass the length check and cause uninitialized stack reads.
**Exploitation:** Malicious dedicated server exploits any connecting client: leaks engine.dll base via pakfile stack infoleak, plants fake vtable via replicated ConVar, sets tickcount to pivot gadget via NET_Tick, triggers OOB in SVC_PacketEntities to hijack virtual call, then ROPs to ShellExecuteA for code execution. No client config required; works across TF2, CS:GO, L4D2.
**Escalation:** Chains memory corruption with a separate infoleak to defeat ASLR, enabling a single static ROP chain across all Source 1 titles without xinput.dll.
**Insight:** In game engines, release builds strip Asserts, so any 'validated' index into a global array is effectively attacker-controlled; look for functions where a network-controlled index feeds a global table lookup. Also, ZIP header uncompressedSize is signed and trusted for size checks, and file-transfer subchannels that ignore Read() return values leak stack memory when reading past EOF.
