Last night I received an urgent message from a client. My machine has been hacked, someone got into the admin area, I need all of the details from this IP.
So, I grepped the logs, grabbed the appropriate entries and saw something odd.
220.127.116.11 - - [09/Dec/2010:22:15:41 -0500] "GETS /admin/index.php HTTP/1.1" 200 3505 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:18.104.22.168) Gecko/20101026 Firefox/3.6.12" 22.214.171.124 - - [09/Dec/2010:22:17:09 -0500] "GETS /admin/usermanagement.php HTTP/1.1" 200 99320 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:126.96.36.199) Gecko/20101026 Firefox/3.6.12" 188.8.131.52 - - [09/Dec/2010:22:18:05 -0500] "GETS /admin/index.php HTTP/1.1" 200 3510 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:184.108.40.206) Gecko/20101026 Firefox/3.6.12"
A modified snippet of the .htaccess file:
AuthUserFile .htpasswd AuthName "Protected Area" AuthType Basic <Limit GET POST> require valid-user </Limit>
Of course, we know GETS isn’t valid, but, why is Apache handing out status 200s and content lengths that appear to be valid? We know the area was password protected behind .htaccess and with some quick keyboard work we’ve got a system that properly prompts for Basic Authentication with a properly formed HTTP/1.0 request. Removing the <Limit> restriction from the .htaccess protects the site, but, why are these other methods able to pass through? Replacing GETS with anything other than POST, PUT, DELETE, TRACK, TRACE, OPTIONS, HEAD results in Apache treating those requests as if GET had been typed.
Let’s set up a duplicate environment on another machine to figure out what Apache is doing.
tsavo:~ mcd$ telnet devel.mia 80 Trying x.x.x.x... Connected to xxxxxxx.xxx. Escape character is '^]'. GET /htpasstest/ HTTP/1.0 HTTP/1.1 401 Authorization Required Date: Fri, 10 Dec 2010 21:29:58 GMT Server: Apache WWW-Authenticate: Basic realm="Protected Area" Vary: Accept-Encoding Content-Length: 401 Connection: close Content-Type: text/html; charset=iso-8859-1
Let’s try what they did:
tsavo:~ mcd$ telnet devel.mia 80 Trying x.x.x.x... Connected to xxxxxxx.xxx. Escape character is '^]'. GETS /htpasstest/ HTTP/1.0 HTTP/1.1 501 Method Not Implemented Date: Fri, 10 Dec 2010 21:53:58 GMT Server: Apache Allow: GET,HEAD,POST,OPTIONS,TRACE Vary: Accept-Encoding Content-Length: 227 Connection: close Content-Type: text/html; charset=iso-8859-1
Odd, this is the behavior we expected, but, not what we are experiencing on the client’s machine. Digging a little further we look at the differences and begin to suspect the machine may have been compromised. The first thing that struck was mod_negotiation – probably not. mod_actions, maybe, but, no. DAV wasn’t loaded, but, Zend Optimizer was on the machine that appeared to have been exploited. Testing the above script on the client’s machine resulted in…. exactly the same behavior — method not supported. Testing the directory that was exploited results in the GETS request served as if it was a GET request.
So, now we’ve got a particular domain on the machine that is not behaving as the config files would suggest. A quick test on the original domain, and as expected, GETS responds with the data and bypasses the authorization clause in the .htaccess. Lets try one more test:
# telnet xxxxxx.mia 80 Trying x.x.x.x... Connected to xxxxxx.xxx. Escape character is '^]'. GETS /zend.php HTTP/1.1 Host: xxxxxx.xxx HTTP/1.1 200 OK Date: Fri, 10 Dec 2010 22:02:33 GMT Server: Apache Vary: Accept-Encoding Content-Length: 602 Connection: close Content-Type: text/html
Bingo. A Zend encoded file handed us an error 200 even though it contained an invalid request method.
The solution in this case was simple, remove the <Limit> clause from the .htaccess.
The question is, is Zend Optimizer actually doing the proper thing here. Watching Apache with gdb, Zend Optimizer does appear to hook the Apache request handler a bit higher, but why is it attempting to correct an invalid request?
One of the first rules in input validation is validate and reject on error. Never try to correct the data and accept it. If you try to correct it and make a mistake, you’re just as vulnerable and hackers will try to figure out those patterns and add extra escaping into their url request. In this case, only a few pages were able to be displayed as there were checks to make sure forms were POSTed. But, the Limit in .htaccess that should have protected the application, didn’t work as expected because the invalid methods weren’t specified.
As so many applications on the web generate .htpasswd files with the Limit clause, it makes me wonder how many Zend Encoded applications are vulnerable. Take a minute to check your systems.