Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC — Proof of concept for CVE-2024-54756, a vulnerability I found in GZDoom's ZScript scripting engine. | Kitploit
Tools/GitHubGitHub/chainmanner/gzdoom-arbitrary-code-execution-via-zscript-poc
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringShellcodeLearning & EducationPayload DevelopmentBinary Exploitation

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
GitHub
chainmanner/gzdoom-arbitrary-code-execution-via-zscript-poc

GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC

Proof of concept for CVE-2024-54756, a vulnerability I found in GZDoom's ZScript scripting engine.

View Repository
12101 year agoNot yet reviewed

GZDoom <= 4.13.1 Arbitary Code Execution via Malicious ZScript

A proof of concept for an arbitrary code execution vulnerability I found in GZDoom's (https://github.com/zdoom/gzdoom) ZScript functionality. An attacker can share a PK3 file containing a malicious ZScript source file and gain access to the victim's PC.

Big thank-you to Rachael and Agent Ash on the GZDoom dev team for their prompt responses, and to them and the other GZDoom devs for swiftly addressing this!

Affected versions

Confirmed to work for 4.13.0 and 4.13.1, and this probably works for earlier versions too. Be wary of anybody telling you to downgrade to version 4.13.1 or below to be able to play their WAD.

This PoC works only on Linux, but the vulnerability likely exists on Windows too. Not tested on ZDoom or LZDoom, but the vulnerability may exist there as well.

The vulnerability has been disclosed to the devs before this PoC's publication and it should no longer be present for version 4.13.2. To the best of my knowledge, this version does not include any practical breaking changes.

Disclaimer

This PoC is made and released for educational purposes, so that game/scripting engine developers may understand how vulnerabilities can arise and so that players may understand what a malicious game mod can look like. I am not responsible or liable for any misuse of this PoC. Please do not use this to compromise your fellow gamers' PCs; it is illegal (you should not need me to tell you that), and it is especially a dirtbag move to take over somebody's computer through a video game.

Using the PoC

To use this PoC, download this repo and create a PK3 file (which is really a zip file with the .pk3 extension) containing zscript.zs and MAPINFO:

git clone https://github.com/Chainmanner/GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
cd GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
zip PoC.pk3 zscript.zs MAPINFO

The default payload is to run a reverse shell to localhost on port 1337. Start the listener:

nc -nvlp 1337

Run the PoC like so:

gzdoom -iwad <your-doom-or-freedoom-wad> -file PoC.pk3

If it worked, you should now have a reverse shell to yourself.

This PoC is for Linux only. It might not work on the first try; just try it again until it does.

Explanation

NOTE: This is my first exploit writeup and I'm still working on my skill to do low-level writeups. Also, I did much of my debugging with GDB, and unfortunately I didn't have the good sense to save some memory dumps to illustrate my explanation better. Sorry! My next writeup will be better, I promise.

GZDoom is a Doom source port designed for performance and extensibility. Thanks to its powerful features, many awesome WADs, mods, and even commercial total conversions have been made. Unfortunately, where there is complexity, there is the opportunity for vulnerabilities, and in this case there were two present in the ZScript scripting engine that allowed a full exploit chain to arise.

This attack defeats ASLR and sidesteps the need to defeat stack canaries. I don't think Clang's CFI or shadow stacks would have helped here.

Vulnerabilities

The first and most important vulnerability was in how huge arrays were handled. If you allocate a small-enough array, the allocated region of memory tends to be filled with zeroes and is properly separated from other objects; no information can be gained from reading uninitialized memory, and no objects overlap with the array. However, if you allocate a huge array - say, 1073741823 32-bit words or more - you will be able to read and write up to 4 GiB of potentially uninitialized memory from the array's starting point, allowing the attacker to directly modify other objects and defeat ASLR by finding addresses having known offsets. Additionally, any other arrays created past this point will overlap with the huge one.

The second vulnerability was in memory map permissions. For faster performance, ZScript code is JIT-compiled to x86 or x86-64 bytecode whenever possible. In order to have this, the code must be written to a region of memory, and that region of memory must be executed. However, the W^X rule states that a region should be either writable or executable, but not both. If both are applied at the same time (instead of making the region writable, writing the code, and then making the region executable and unwritable), then an attacker with an arbitrary write primitive will be able to escalate it to an arbitrary code execution; they can write shellcode and jump to it by e.g. modifying the return address on the stack (assuming the attacker doesn't have an arbitrary execute primitive). If you look at the memory mappings for GZDoom when it's running, you can see there are several RWX regions:

7fcd19700000-7fcd19800000 rwxp 00000000 00:00 0
7fcd1a100000-7fcd1a200000 rwxp 00000000 00:00 0
7fcd1eb00000-7fcd1ec00000 rwxp 00000000 00:00 0

So if arbitrary write and arbitrary execute primitives are available, and the attacker knows where any RWX region is present, they can write arbitrary shellcode and execute it. Making these regions RW- when writing the JIT-compiled code and then R-X when ready to run would stop this PoC, but it would not stop an attacker from gaining code execution by, for example, modifying data on the stack (ROP) or heap.

Gadgets

Additionally, there is a useful gadget. Remember how when allocating a huge array, any other arrays created after it will overlap? That includes arrays of object pointers. Much like C++ objects, ZScript objects can contain variables and function pointers. Suppose we have this object:

class WeirdObject
{
        uint one;
        uint two;
        uint three;
        uint four;
        Function<clearscope void()> funcptr;
}

If we create an array containing a pointer to a WeirdObject instance, then the attacker can change the pointer to wherever we want using the huge array and change the pointed data by accessing the object's fields, giving us an arbitrary read/write primitive going beyond the heap. Pointers in ZScript are checked to ensure they're not null, but not to ensure that they're sane.

The presence of a function pointer also gives us an arbitrary execute primitive; that, however, is a little less straightforward, requiring the creation of a fake VMFunction to satify the virtual machine. As soon as a call to a ZScript function is introduced in the exploit code, that code is no longer JIT-compiled. Still works, but it becomes a bit more of a headache to debug and exploit. There might be a better way to do this part, but I didn't study the GZDoom internals well enough to know of it.

One thing to note: WeirdObject has inherited member variables, so the first member starts at offset 0x28.

Exploit

So now, we have the following tools:

  • Arbitrary read/write for a huge region of the heap
  • Arbitrary read/write/execute going beyond the heap
  • RWX regions

How do we chain them to make an exploit?

First, because ASLR's enabled, we need to identify where an RWX region is present. Any will do. The part of the heap available to the huge array contains addresses pointing to functions within an RWX region, but it also has addresses pointing to other regions; how do we discriminate? Recall that on Linux, ASLR has 28 bits of entropy (sometimes less!), meaning that although bits of the mask 0x7fffffe00000 in an address will be random, bits 0x0000001fffff will be static. So, with ASLR disabled, let's assume we have the following RWX regions:

[0x7ffff2f00000, 0x7ffff3000000)
[0x7ffff3900000, 0x7ffff3a00000)
[0x7ffff4300000, 0x7ffff4400000)

Then we can use the following ZScript code to print pointers to JIT-compiled ZScript functions within the RWX regions:

uint u32pBFA9000[1073741823];
uint u32RWX_L;
uint u32RWX_H;

for (i = 0; i < (1073741823 / 2); i += 2)
{
	u32RWX_L = u32pBFA9000[i];
	u32RWX_H = u32pBFA9000[i+1];
Download Tool