summaryrefslogtreecommitdiffstats
path: root/scripts/hostmap-crtsh.nse
blob: 3e62fb2725392e415d9e7f6cad3d42b97168fe3b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
description = [[
Finds subdomains of a web server by querying Google's Certificate Transparency
logs database (https://crt.sh).

The script will run against any target that has a name, either specified on the
command line or obtained via reverse-DNS.

NSE implementation of ctfr.py (https://github.com/UnaPibaGeek/ctfr.git) by Sheila Berta.

References:
* www.certificate-transparency.org
]]

---
-- @args hostmap.prefix If set, saves the output for each host in a file
-- called "<prefix><target>". The file contains one entry per line.
-- @args newtargets If set, add the new hostnames to the scanning queue.
-- This the names presumably resolve to the same IP address as the
-- original target, this is only useful for services such as HTTP that
-- can change their behavior based on hostname.
--
-- @usage
-- nmap --script hostmap-crtsh --script-args 'hostmap-crtsh.prefix=hostmap-' <targets>
-- @usage
-- nmap -sn --script hostmap-crtsh <target>
-- @output
-- Host script results:
-- | hostmap-crtsh:
-- |   subdomains:
-- |     svn.nmap.org
-- |     www.nmap.org
-- |_  filename: output_nmap.org
-- @xmloutput
-- <table key="subdomains">
--  <elem>svn.nmap.org</elem>
--  <elem>www.nmap.org</elem>
--  </table>
-- <elem key="filename">output_nmap.org</elem>
---

-- TODO:
-- At the moment the script reports all hostname-like identities where
-- the parent hostname is present somewhere in the identity. Specifically,
-- the script does not verify that a returned identity is truly a subdomain
-- of the parent hostname. As an example, one of the returned identities for
-- "google.com" is "google.com.gr".
-- Since fixing it would change the script behavior that some users might
-- currently depend on then this should be discussed first. [nnposter]

author = "Paulino Calderon <calderon@websec.mx>"

license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

categories = {"external", "discovery"}

local io = require "io"
local http = require "http"
local json = require "json"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local target = require "target"
local table = require "table"
local tableaux = require "tableaux"

-- Different from stdnse.get_hostname
-- this function returns nil if the host is only known by IP address
local function get_hostname (host)
  return host.targetname or (host.name ~= '' and host.name) or nil
end

-- Run on any target that has a name
hostrule = get_hostname

local function is_valid_hostname (name)
  local labels = stringaux.strsplit("%.", name)
  -- DNS name cannot be longer than 253
  -- do not accept TLDs; at least second-level domain required
  -- TLD cannot be all digits
  if #name > 253 or #labels < 2 or labels[#labels]:find("^%d+$") then
    return false
  end
  for _, label in ipairs(labels) do
    if not (#label <= 63 and label:find("^[%w_][%w_-]*%f[-\0]$")) then
      return false
    end
  end
  return true
end

local function query_ctlogs(hostname)
  local url = string.format("https://crt.sh/?q=%%.%s&output=json", hostname)
  local response = http.get_url(url)
  if not (response.status == 200 and response.body) then
    stdnse.debug1("Error: Could not GET %s", url)
    return
  end
  local jstatus, jresp = json.parse(response.body)
  if not jstatus then
    stdnse.debug1("Error: Invalid response from %s", url)
    return
  end
  local hostnames = {}
  for _, cert in ipairs(jresp) do
    local names = cert.name_value;
    if type(names) == "string" then
      for _, name in ipairs(stringaux.strsplit("%s+", names:lower())) do
        -- if this is a wildcard name, just proceed with the static portion
        if name:find("*.", 1, true) == 1 then
          name = name:sub(3)
        end
        if name ~= hostname and not hostnames[name] and is_valid_hostname(name) then
          hostnames[name] = true
          if target.ALLOW_NEW_TARGETS then
            target.add(name)
          end
        end
      end
    end
  end

  hostnames = tableaux.keys(hostnames)
  return #hostnames > 0 and hostnames or nil
end

local function write_file(filename, contents)
  local f, err = io.open(filename, "w")
  if not f then
    return f, err
  end
  f:write(contents)
  f:close()
  return true
end

action = function(host)
  local filename_prefix = stdnse.get_script_args("hostmap.prefix")
  local hostname = get_hostname(host)
  local hostnames = query_ctlogs(hostname)
  if not hostnames then return end

  local output_tab = stdnse.output_table()
  output_tab.subdomains = hostnames
  --write to file
  if filename_prefix then
    local filename = filename_prefix .. stringaux.filename_escape(hostname)
    local hostnames_str = table.concat(hostnames, "\n")

    local status, err = write_file(filename, hostnames_str)
    if status then
      output_tab.filename = filename
    else
      stdnse.debug1("Error saving file %s: %s", filename, err)
    end
  end

  return output_tab
end