Allow me to set the scene and start proceedings off with a definition of an integer overflow, according to Wikipedia:
“…an integer overflow occurs when an arithmetic operation attempts to create a numeric value that is outside of the range that can be represented with a given number of digits – either higher than the maximum or lower than the minimum representable value.” [1]
To be inclusive of all audiences here, in software security we’ve got sources (typically user input) and sinks – where that input (the data) ends up. In order to overflow something (e.g. an integer overflow) we clearly need some way to be able to do that (think pouring water from a kettle into a cup), and that’s the source (us using the kettle) to overflow the cup. Cup of tea aside, what things can be accessed remotely and take user input (those sources)?
Web servers! This blog post title does not lie!
For us to start hunting these integer overflows down, we first need to look at two case studies from the past to both inspire us, let us know where we should maybe start looking and to get an appreciation for why appsec is not easy. Before we do any hacking of our own, let’s look at these two handpicked examples, let’s get into that hacker hoody mindset.
Let me take you on a journey, back to 2011, 24th August 2011 to be exact.
“A denial of service vulnerability has been found in the way the multiple overlapping ranges are handled by the Apache HTTPD server prior to version 2.2.20... The attack can be done remotely and with a modest number of requests can cause very significant memory and CPU usage on the server.” [2]
The “Range” header (and later, also the “Request-Range” header) could be abused to cause this DoS. What is this range header I hear you ask?
“The Range HTTP request header indicates the parts of a resource that the server should return. Several parts can be requested at the same time in one Range header, and the server may send back these ranges in a multipart document… Currently, only bytes units are registered which are offsets (zero-indexed & inclusive).” [3]
For example:
Now what went down was a request with lots of byte ranges, tons and tons, all with crazy offsets, mixed and matched up, conflicting with each other, overlapping, you name it… ultimately causing Apache (and in turn, the CPU usage of it) to go a little crazy server side.
Example: Range: bytes=0-,5-0,5-1,5-2,5-3,5-4,5-5,5-6,5-7,5-8,5-9,5-10,5-11,5-12,5-13 […and so on]
People were fixing this at the time with temporary patches using mod_rewrite and mod_headers, limiting the ranges there or unsetting the Range and Request-Range directives altogether.
This bug was then fixed in the codebase itself (modules/http/byterange_filter.c) the next day on 25th August 2011 and the following days with further amendments to close out edge cases as and when people were bypassing I imagine.
Code fix: Make maximum ranges to be limited to “200”.
Code fix: Check that the sum of all the lengths is not greater than the actual content length.
Code fix: Stop ranges values less than “1” being submitted.
Code fix: Check that the start of a range is not less than the end of it, both singular and in multiple ranges (the merging of ranges looks to be painful).
That was the first of two overflows we will be looking at in this blog post. For the second, let me change the dial on that DeLorean to 6 years later, 2017 – 11th July 2017 to be exact. 88mph and 1.21 Gigawatts later...
“Nginx versions since 0.5.6 up to and including 1.13.2 are vulnerable to integer overflow vulnerability in nginx range filter module resulting into leak of potentially sensitive information triggered by specially crafted request.” [4]
Would you look at that, it is an overflow in a range filter again… but this time in Nginx - vulnerable for 6 years, a variant of the Apache vulnerability from back in 2011. In this one the impact was limited, it would allow reading (leaking) of a cache file header (if the file was being served from a cache), which may reveal the internal IP address of the backend server or other sensitive information. To exploit, you had to do a few more things, first get the content-length of the file being returned, you’d then add 623 to this. This would be your first minus/negative range (or ‘suffix’). You’d then take this number and subtract it from 776000, the result of which you append onto the end of the next suffix; -9223372036854. I told you there was a little more involved!
Example:
Content-length received is 42.
42 + 623 = 665
776000 – 665 = 775335
Range: bytes=-665,-9223372036854775335
They fixed this (src/http/modules/ngx_http_range_filter_module.c), making sure that when any suffix is provided (the “-“ symbol), that the ‘end’ is less than the content-length available, and dealing with this if not.
Okay. So now that those two case studies are firmly fresh in our minds, what did we learn? Well firstly, that the first rule of software security club is still true; never trust user inputs, that’s a given. Secondly, that the developers (in both these examples) are allowing the user to specify offsets/ranges, a sliding window if you like, of the data they want to receive from the server. The problem here is that checks (or sufficient ones) were not put in place to control the start and end positions, so we get to slide our window out of those ranges and place it into other memory regions. There is added complexity around being able to specify multiple ranges which then need to be merged, which created all sorts of issues as we saw in the first DoS example. The second example was abusing the ‘suffix’ (again with multiple ranges) to move that sliding window. In the second example the result was a leak, we’re not able to write data here and overwrite EIP like in a buffer overflow, but the concept is similar. However, we can abuse the logic, overflow these ranges/offsets to control a pointer and put it places it shouldn’t be.
So this got me thinking, in what other thing, similar to the range theme, can I specify a ‘start’ or ‘end’ position to do with web servers, web technology, etc. which I can potentially abuse?
Videos!
We can skip to different positions in a video by using a slider – start from 5 seconds in, etc. Video multimedia on the web over the years have been incorporated into web server technology, especially when streaming is involved. With Nginx in the picture, I did some digging, and decided to target Flash Video (FLV) files. Nginx has a module called “ngx_http_flv_module” and wait, you need to be sitting down when you read this, move that coffee cup away…
“The ngx_http_flv_module module provides pseudo-streaming server-side support for Flash Video (FLV) files… It handles requests with the start argument in the request URI’s query string specially, by sending back the contents of a file starting from the requested byte offset and with the prepended FLV header.” [5]
Well, it is like all my Christmases came at once here – “start argument in the request” and “sending back the contents of a file starting from the requested byte offset”. I mean, that’s pretty much an open invite.
Let’s download the latest Nginx source code and have a little look. In src/http/modules/ngx_http_flv_module.c we go.
The line I want to draw your attention to is line 174. If ‘start’ is equal to NGX_ERROR or ‘start’ is greater than or equal to ‘len’ (the length of the data), then ‘start’ is set to a value “0”. I want to also draw your attention to what we don’t see here. What happens if ‘start’ is less than ‘len’, and by less, I mean not just less (as intended), but much less, less like a negative value? That black hacker hoody is now firmly on.
Well, there is nothing more left for it. We have talked the talk; it is now time to see if we can walk the walk. We need to build this bad boy and see what we’re able to do.
Since the FLV module is not built by default in Nginx we need to specify it to be built. We also want to enable debugging so we can tail -f those log files and see what is going on in deep deep Nginx land:
./configure --with-debug –with-http_flv_module
We also need to add in the FLV directive into nginx.conf so it knows what to do with it:
location ~ .flv$ { flv; }
I fire Nginx up and also launch Burp Suite because we’re going to be using it to cook up some HTTP pot pies.
Not so fast though… I’m going to need a test FLV to play with. I make a screen recording of doing a directory listing – just something random to fill up 10 seconds or so to give us something to play with. This is in .MOV format so I use ffmpeg to convert this to .FLV. I then drop this file (test.flv) into the web root of Nginx. We are now all set.
Let’s see what a normal request/response for test.flv gives us back to get some kind of baseline before we start trying to hack all the things. Note down the content-length of 1829479.
We now introduce the “start” parameter, but don’t give it any value to see what happens in reality. The same content-length of 1829479 is returned.
A value of zero? Content-length is still 1829479.
A start value of “25”. Notice how (in the response) after the “FLV” header (mentioned shortly) we no longer have that superscript underlined “o”. We are effectively sliding our window over like we did with the range fun and games earlier. Our start value is being used as an offset to start from “25”. The content-length is less than the previous one (it is now “1829467” vs “1829479”). This is as we’d expect as we are chopping some bytes off, disregarding bytes from the start.
A start value of “30”. Notice how (in the response) after the FLV header, that the file starts with “etaDataduration” which was previously “onMetaDataduration”. Proof we are moving that window thing and disregarding bytes before it, as you’d expect from changing an offset. Content-length decreases further in line with this.
At this stage, you may be wondering what I’m up to. I’m matching up what I read about in the source code and seeing if the actions I expect to happen do in reality go down that way. A lot of the time we can’t see everything from the source code review and it isn’t always representative about all of the other layers which may happen or get invoked during execution. For example, the “FLV” header we saw in the code:
We clearly knew to expect this header in the response as shown below. If you look to the right in Burp Suite below and see the hexadecimal values of this header, you can see how this maps to the ngx_flv_header[] from the code above on line 27.
Ok, back to it.
A really big number? It gets handled correctly, just as we expected from the source code – the start gets set to “0” and everything gets sent – which the content-length confirms, back to “1829479”.
A negative number? Which is something we were really interested in right from the outset because it wasn’t clear in the code how this would be handled. Well, it looks like this exception gets caught and handled nicely because it sends the full length file, treating ‘start’ effectively as if it were “0”.
Ok, what about a resultant negative value? Not by sending a negative value but overflowing by 1 byte and hoping this overflows with some kind of wraparound and turns into a negative number. We know that the full content-length is “1829479”, so let’s send a ‘start’ value of “1829480” to try just that.
The web server gives us back an empty response. We’re onto something, maybe… We need to now go look at the Nginx debug logs to find out what is happening internally.
Unfortunately, it looks like the exception has been spotted and caught. If we look at the “[alert]” above it mentions “negative size buf in output” and in the line below it there is the calculation “1829480-1829479” (which I am sure is being reported the wrong way around for logging purposes else this would be “1”). The resultant value is actually “-1”, hence the negative alert trigger.
What is responsible for catching this? If we search for the string “negative size buf in output” we get a hit in src/core/ngx_output_chain.c .
If we take a look at the code we can immediately see the ‘if’ conditional block screaming out at us, checking that if ‘bsize’ (the byte size of the content to be returned) is less than “0” then log the error (as an alert – the one which we saw) and call ngx_debug_point() which actually causes a break inside that function – hence the empty response we got back from the web server.
So we lost the hunt on this one, we won’t return back home with any prize. However, don’t be disheartened, I’m certainly not! That’s because there are many more web servers and many more input sources out there in HTTP itself, associated web technology and stacks, just waiting to be discovered and overflowed.
Happy hunting, and thanks for reading!
[1] https://en.wikipedia.org/wiki/Integer_overflow
[2] https://httpd.apache.org/security/CVE-2011-3192.txt
[3] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
[4] https://mailman.nginx.org/pipermail/nginx-announce/2017/000200.html