Lucene search

K
nmapAlex WeberNMAP:HTTP-GIT.NSE
HistoryJul 19, 2012 - 6:15 p.m.

http-git NSE Script

2012-07-1918:15:02
Alex Weber
nmap.org
275

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

Checks for a Git repository found in a website’s document root /.git/<something>) and retrieves as much repo information as possible, including language/framework, remotes, last commit message, and repository description.

Script Arguments

http-git.root

URL path to search for a .git directory. Default: /

slaxml.debug

See the documentation for the slaxml 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 -sV -sC &lt;target&gt;

Script Output

PORT   STATE SERVICE REASON
80/tcp open  http    syn-ack
| http-git:
|   127.0.0.1:80/.git/
|     Git repository found!
|     .git/config matched patterns 'passw'
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|     Remotes:
|       http://github.com/someuser/somerepo
|     Project type: Ruby on Rails web application (guessed from .git/info/exclude)
|   127.0.0.1:80/damagedrepository/.git/
|_    Potential Git repository found (found 2/6 expected files)

Requires


local http = require("http")
local shortport = require("shortport")
local stdnse = require("stdnse")
local string = require("string")
local table = require("table")

description = [[
Checks for a Git repository found in a website's document root
/.git/<something>) and retrieves as much repo information as
possible, including language/framework, remotes, last commit
message, and repository description.
]]

---
-- @output
-- PORT   STATE SERVICE REASON
-- 80/tcp open  http    syn-ack
-- | http-git:
-- |   127.0.0.1:80/.git/
-- |     Git repository found!
-- |     .git/config matched patterns 'passw'
-- |     Repository description: Unnamed repository; edit this file 'description' to name the...
-- |     Remotes:
-- |       http://github.com/someuser/somerepo
-- |     Project type: Ruby on Rails web application (guessed from .git/info/exclude)
-- |   127.0.0.1:80/damagedrepository/.git/
-- |_    Potential Git repository found (found 2/6 expected files)
--
-- @args http-git.root URL path to search for a .git directory. Default: /
--
-- @xmloutput
-- <table key="127.0.0.1:80/.git/">
--   <table key="remotes">
--     <elem>http://github.com/anotherperson/anotherepo</elem>
--   </table>
--   <table key="project-type">
--     <table key=".git/info/exclude">
--       <elem>JBoss Java web application</elem>
--       <elem>Java application</elem>
--     </table>
--   </table>
--   <elem key="repository-description">A nice repository</elem>
--   <table key="files-found">
--     <elem key=".git/COMMIT_EDITMSG">false</elem>
--     <elem key=".git/info/exclude">true</elem>
--     <elem key=".git/config">true</elem>
--     <elem key=".git/description">true</elem>
--     <elem key=".gitignore">false</elem>
--   </table>
--   <table key="interesting-matches">
--     <table key=".git/config">
--       <elem>passw</elem>
--     </table>
--   </table>
-- </table>

categories = { "default", "safe", "vuln" }
author = "Alex Weber"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = shortport.http

-- We consider 200 to mean "okay, file exists and we received its contents".
local STATUS_OK = 200
-- Long strings (like a repository's description) will be truncated to this
-- number of characters in normal output.
local TRUNC_LENGTH = 60

function action(host, port)
  local out

  -- We can accept a single root, or a table of roots to try
  local root_arg = stdnse.get_script_args("http-git.root")
  local roots
  if type(root_arg) == "table" then
    roots = root_arg
  elseif type(root_arg) == "string" or type(root_arg) == "number" then
    roots = { tostring(root_arg) }
  elseif root_arg == nil then -- if we didn't get an argument
    roots = { "/" }
  end

  -- Try each root in succession
  for _, root in ipairs(roots) do
    root = tostring(root)
    root = root or '/'

    -- Put a forward slash on the beginning and end of the root, if none was
    -- provided. We will print this, so the user will know that we've mangled it
    if not string.find(root, "/$") then -- if there is no slash at the end
      root = root .. "/"
    end
    if not string.find(root, "^/") then -- if there is no slash at the beginning
      root = "/" .. root
    end

    -- If we can't get a valid /.git/HEAD, don't even bother continuing
    -- We could try for /.git/, but we will not get a 200 if directory
    -- listings are disallowed.
    local resp = http.get(host, port, root .. ".git/HEAD")
    local sha1_pattern = "^%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x"
    if resp.status == STATUS_OK and ( resp.body:match("^ref: ") or resp.body:match(sha1_pattern) ) then
      out = out or {}
      local replies = {}
      -- This function returns true if we got a 200 OK when
      -- fetching 'filename' from the server
      local function ok(filename)
        return (replies[filename].status == STATUS_OK)
      end
      -- These are files that are small, very common, and don't
      -- require zlib to read
      -- These files are created by creating and using the repository,
      -- or by popular development frameworks.
      local repo = {
        ".gitignore",
        ".git/COMMIT_EDITMSG",
        ".git/config",
        ".git/description",
        ".git/info/exclude",
      }

      local pl_requests = {} -- pl_requests = pipelined requests (temp)
      -- Go through all of the filenames and do an HTTP GET
      for _, name in ipairs(repo) do -- for every filename
        http.pipeline_add(root .. name, nil, pl_requests)
      end
      -- Do the requests.
      replies = http.pipeline_go(host, port, pl_requests)
      if replies == nil then
        stdnse.debug1("pipeline_go() error. Aborting.")
        return nil
      end

      for i, reply in ipairs(replies) do
        -- We want this to be indexed by filename, not an integer, so we convert it
        -- We added to the pipeline in the same order as the filenames, so this is safe.
        replies[repo[i]] = reply -- create index by filename
        replies[i] = nil -- delete integer-indexed entry
      end

      -- Mark each file that we tried to get as 'found' (true) or 'not found' (false).
      local location = host.ip .. ":" .. port.number .. root .. ".git/"
      out[location] = {}
      -- A nice shortcut
      local loc = out[location]
      loc["files-found"] = {}
      for name, _ in pairs(replies) do
        loc["files-found"][name] = ok(name)
      end

      -- Look through all the repo files we grabbed and see if we can find anything interesting.
      local interesting = { "bug", "key", "passw", "pw", "user", "secret", "uid" }
      for name, reply in pairs(replies) do
        if ok(name) then
          for _, pattern in ipairs(interesting) do
            if string.match(reply.body, pattern) then
              -- A Lua idiom - don't create this table until we actually have something to put in it
              loc["interesting-matches"] = loc["interesting-matches"] or {}
              loc["interesting-matches"][name] = loc["interesting-matches"][name] or {}
              table.insert(loc["interesting-matches"][name], pattern)
            end
          end
        end
      end

      if ok(".git/COMMIT_EDITMSG") then
        loc["last-commit-message"] = replies[".git/COMMIT_EDITMSG"].body
      end

      if ok(".git/description") then
        loc["repository-description"] = replies[".git/description"].body
      end

      -- .git/config contains a list of remotes, so we try to extract them.
      if ok(".git/config") then
        local config = replies[".git/config"].body
        local remotes = {}

        -- Try to extract URLs of all remotes.
        for url in string.gmatch(config, "\n%s*url%s*=%s*(%S*/%S*)") do
          table.insert(remotes, url)
        end

        for _, url in ipairs(remotes) do
          loc["remotes"] = loc["remotes"] or {}
          table.insert(loc["remotes"], url)
        end
      end

      -- These are files that are used by Git to determine what files to ignore.
      -- We use this list to make the loop below (used to determine what kind of
      -- application is in the repository) more generic.
      local ignorefiles = {
        ".gitignore",
        ".git/info/exclude",
      }
      local fingerprints = {
        -- Many of these taken from https://github.com/github/gitignore
        { "%.scala_dependencies", "Scala application" },
        { "npm%-debug%.log", "node.js application" },
        { "joomla%.xml", "Joomla! site" },
        { "jboss/server", "JBoss Java web application" },
        { "wp%-%*%.php", "WordPress site" },
        { "app/config/database%.php", "CakePHP web application" },
        { "sites/default/settings%.php", "Drupal site" },
        { "local_settings%.py", "Django web application" },
        { "/%.bundle", "Ruby on Rails web application" }, -- More specific matches (MyFaces > JSF > Java) on top
        { "%.py[dco]", "Python application" },
        { "%.jsp", "JSP web application" },
        { "%.bundle", "Ruby application" },
        { "%.class", "Java application" },
        { "%.php", "PHP application" },
      }
      -- The XML produced here is divided by ignorefile and is sorted from first to last
      -- in order of specificity. e.g. All JBoss applications are Java applications,
      -- but not all Java applications are JBoss. In that case, JBoss and Java will
      -- be output, but JBoss will be listed first.
      for _, file in ipairs(ignorefiles) do
        if ok(file) then -- We only test all fingerprints if we got the file.
          for _, fingerprint in ipairs(fingerprints) do
            if string.match(replies[file].body, fingerprint[1]) then
              loc["project-type"] = loc["project-type"] or {}
              loc["project-type"][file] = loc["project-type"][file] or {}
              table.insert(loc["project-type"][file], fingerprint[2])
            end
          end
        end
      end
    end
  end

  -- If we didn't get anything, we return early. No point doing the
  -- normal formatting!
  if out == nil then
    return nil
  end

  -- Truncate to TRUNC_LENGTH characters and replace control characters (newlines, etc) with spaces.
  local function summarize(str)
    str = stdnse.string_or_blank(str, "<unknown>")
    local original_length = #str
    str = string.sub(str, 1, TRUNC_LENGTH)
    str = string.gsub(str, "%c", " ")
    if original_length > TRUNC_LENGTH then
      str = str .. "..."
    end
    return str
  end

  -- We convert the full output to pretty output for -oN
  local normalout
  for location, info in pairs(out) do
    normalout = normalout or {}
    -- This table gets converted to a string format_output, and then inserted into the 'normalout' table
    local new = {}
    -- Headings for each place we found a repo
    new["name"] = location

    -- How sure are we that this is a Git repository?
    local count = { tried = 0, ok = 0 }
    for _, found in pairs(info["files-found"]) do
      count.tried = count.tried + 1
      if found then count.ok = count.ok + 1 end
    end

    -- If 3 or more of the files we were looking for are not on the server,
    -- we are less confident that we got a real Git repository
    if count.tried - count.ok <= 2 then
      table.insert(new, "Git repository found!")
    else                                                          -- We already got .git/HEAD, so we add 1 to 'tried' and 'ok'
      table.insert(new, "Potential Git repository found (found " .. (count.ok + 1) .. "/" .. (count.tried + 1) .. " expected files)")
    end

    -- Show what patterns matched what files
    for name, matches in pairs(info["interesting-matches"] or {}) do
      table.insert(new, ("%s matched patterns '%s'"):format(name, table.concat(matches, "' '")))
    end

    if info["repository-description"] then
      table.insert(new, "Repository description: " .. summarize(info["repository-description"]))
    end

    if info["last-commit-message"] then
      table.insert(new, "Last commit message: " .. summarize(info["last-commit-message"]))
    end

    -- If we found any remotes in .git/config, process them now
    if info["remotes"] then
      local old_name = info["remotes"]["name"]  -- in case 'name' is a remote
      info["remotes"]["name"] = "Remotes:"
      -- Remove the newline from format_output's output - it looks funny with it
      local temp = string.gsub(stdnse.format_output(true, info["remotes"]), "^\n", "")
      -- using 'temp' here because gsub() has multiple return values that insert() will try
      -- to use, and I don't know of a better way to prevent that ;)
      table.insert(new, temp)
      info["remotes"]["name"] = old_name
    end

    -- Take the first guessed project type from each ignorefile
    if info["project-type"] then
      for name, types in pairs(info["project-type"]) do
        table.insert(new, "Project type: " .. types[1] .. " (guessed from " .. name .. ")")
      end
    end
    -- Insert this location's information.
    table.insert(normalout, new)
  end

  return out, stdnse.format_output(true, normalout)
end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

Related for NMAP:HTTP-GIT.NSE