http-csrf NSE Script

2013-08-23T02:08:09
ID NMAP:HTTP-CSRF.NSE
Type nmap
Reporter George Chatzisofroniou
Modified 2015-11-05T20:41:05

Description

This script detects Cross Site Request Forgeries (CSRF) vulnerabilities.

It will try to detect them by checking each form if it contains an unpredictable token for each user. Without one an attacker may forge malicious requests.

To recognize a token in a form, the script will iterate through the form's attributes and will search for common patterns in their names. If that fails, it will also calculate the entropy of each attribute's value. A big entropy means a possible token.

A common use case for this script comes along with a cookie that gives access in pages that require authentication, because that's where the privileged exist. See the http library's documentation to set your own cookie.

Script Arguments

http-csrf.singlepages

The pages that contain the forms to check. For example, {/upload.php, /login.php}. Default: nil (crawler mode on)

http-csrf.checkentropy

If this is set the script will also calculate the entropy of the field's value to determine if it is a token, rather than just checking its name. Default: true

slaxml.debug

See the documentation for the slaxml library.

httpspider.doscraping, httpspider.maxdepth, httpspider.maxpagecount, httpspider.noblacklist, httpspider.url, httpspider.useheadfornonwebfiles, httpspider.withindomain, httpspider.withinhost

See the documentation for the httpspider library.

http.host, http.max-body-size, http.max-cache-size, http.max-pipeline, http.pipeline, http.truncated-ok, http.useragent

See the documentation for the http library.

smbdomain, smbhash, smbnoguest, smbpassword, smbtype, smbusername

See the documentation for the smbauth library.

Example Usage

nmap -p80 --script http-csrf.nse <target>

Script Output

PORT   STATE SERVICE REASON
80/tcp open  http    syn-ack
| http-csrf:
| Spidering limited to: maxdepth=3; maxpagecount=20; withinhost=some-very-random-page.com
|   Found the following CSRF vulnerabilities:
|
|     Path: http://www.example.com/
|     Form id: search_bar_input
|     Form action: /search
|
|     Path: http://www.example.com/c/334/watches.html
|     Form id: custom_price_filters
|     Form action: /search
|
|     Path: http://www.example.com/c/334/watches.html
|     Form id: custom_price_filters
|_    Form action: /c/334/rologia-xeiros-watches.html

Requires

  • http
  • formulas
  • shortport
  • stdnse
  • table
  • string
  • httpspider

                                        
                                            description = [[
This script detects Cross Site Request Forgeries (CSRF) vulnerabilities.

It will try to detect them by checking each form if it contains an unpredictable
token for each user. Without one an attacker may forge malicious requests.

To recognize a token in a form, the script will iterate through the form's
attributes and will search for common patterns in their names. If that fails, it
will also calculate the entropy of each attribute's value. A big entropy means a
possible token.

A common use case for this script comes along with a cookie that gives access
in pages that require authentication, because that's where the privileged
exist. See the http library's documentation to set your own cookie.
]]

---
-- @usage nmap -p80 --script http-csrf.nse <target>
--
-- @args http-csrf.singlepages The pages that contain the forms to check.
--       For example, {/upload.php,  /login.php}. Default: nil (crawler
--       mode on)
-- @args http-csrf.checkentropy If this is set the script will also calculate
--       the entropy of the field's value to determine if it is a token,
--       rather than just checking its name. Default: true
--
-- @output
-- PORT   STATE SERVICE REASON
-- 80/tcp open  http    syn-ack
-- | http-csrf:
-- | Spidering limited to: maxdepth=3; maxpagecount=20; withinhost=some-very-random-page.com
-- |   Found the following CSRF vulnerabilities:
-- |
-- |     Path: http://www.example.com/
-- |     Form id: search_bar_input
-- |     Form action: /search
-- |
-- |     Path: http://www.example.com/c/334/watches.html
-- |     Form id: custom_price_filters
-- |     Form action: /search
-- |
-- |     Path: http://www.example.com/c/334/watches.html
-- |     Form id: custom_price_filters
-- |_    Form action: /c/334/rologia-xeiros-watches.html
--
---

categories = {"intrusive", "exploit", "vuln"}
author = "George Chatzisofroniou"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

local http = require "http"
local formulas = require "formulas"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"
local string = require "string"
local httpspider = require "httpspider"

portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")

-- Checks if this is really a token.
isToken = function(value)

  local minlength = 8
  local minentropy = 72

  -- If it has a reasonable length.
  if #value > minlength then

    local entropy = formulas.calcPwdEntropy(value)

    -- Does it have a big entropy?
    if entropy >= minentropy then
      -- If it doesn't contain any spaces but contains at least one digit.
      if not string.find(value, " ") and string.find(value, "%d") then
        return 1
      end
    end
  end

  return 0

end

action = function(host, port)

  local singlepages = stdnse.get_script_args("http-csrf.singlepages")
  local checkentropy = stdnse.get_script_args("http-csrf.checkentropy") or false

  local csrfvuln = {}
  local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME, withinhost = 1 } )

  if (not(crawler)) then
    return
  end

  crawler:set_timeout(10000)

  local index, response, path
  while (true) do

    if singlepages then
      local k, target,
      k, target = next(singlepages, index)
      if (k == nil) then
        break
      end
      response = http.get(host, port, target)
      path = target

    else
      local status, r = crawler:crawl()
      -- if the crawler fails it can be due to a number of different reasons
      -- most of them are "legitimate" and should not be reason to abort
      if (not(status)) then
        if (r.err) then
          return stdnse.format_output(false, r.reason)
        else
          break
        end
      end

      response = r.response
      path = tostring(r.url)
    end

    if response.body then

      local forms = http.grab_forms(response.body)

      for i, form in ipairs(forms) do

        form = http.parse_form(form)

        local resistant = false
        if form and form.action then
          for _, field in ipairs(form['fields']) do

            -- First we check the field's name.
            if field['value'] then
              resistant = string.find(field['name'], "[Tt][Oo][Kk][Ee][Nn]") or string.find(field['name'], "[cC][sS][Rr][Ff]")
              -- Let's be sure, by calculating the entropy of the field's value.
              if not resistant and checkentropy then
                resistant = isToken(field['value'])
              end

              if resistant then
                break
              end
            end

          end

          if not resistant then

            -- Handle forms with no id or action attributes.
            form['id'] = form['id'] or ""
            form['action'] = form['action'] or "-"

            local msg = "\nPath: " .. path .. "\nForm id: " .. form['id'] .. "\nForm action: " .. form['action']
            table.insert(csrfvuln, { msg } )
          end
        end
      end

      if (index) then
        index = index + 1
      else
        index = 1
      end
    end

  end

  -- If the table is empty.
  if next(csrfvuln) == nil then
    return "Couldn't find any CSRF vulnerabilities."
  end

  table.insert(csrfvuln, 1, "Found the following possible CSRF vulnerabilities: ")

  csrfvuln.name = crawler:getLimitations()

  return stdnse.format_output(true, csrfvuln)

end