summaryrefslogtreecommitdiffstats
path: root/scripts/mysql-audit.nse
blob: 32a1482ceab77a6548b17c13cc0050952f2a1cb8 (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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
local _G = require "_G"
local mysql = require "mysql"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"

description = [[
Audits MySQL database server security configuration against parts of
the CIS MySQL v1.0.2 benchmark (the engine can be used for other MySQL
audits by creating appropriate audit files).
]]


---
-- @usage
-- nmap -p 3306 --script mysql-audit --script-args "mysql-audit.username='root', \
--   mysql-audit.password='foobar',mysql-audit.filename='nselib/data/mysql-cis.audit'"
--
-- @args mysql-audit.username the username with which to connect to the database
-- @args mysql-audit.password the password with which to connect to the database
-- @args mysql-audit.filename the name of the file containing the audit rulebase, "mysql-cis.audit" by default
--
-- @output
-- PORT     STATE SERVICE
-- 3306/tcp open  mysql
-- | mysql-audit:
-- |   CIS MySQL Benchmarks v1.0.2
-- |       3.1: Skip symbolic links => PASS
-- |       3.2: Logs not on system partition => PASS
-- |       3.2: Logs not on database partition => PASS
-- |       4.1: Supported version of MySQL => REVIEW
-- |         Version: 5.1.54-1ubuntu4
-- |       4.4: Remove test database => PASS
-- |       4.5: Change admin account name => FAIL
-- |       4.7: Verify Secure Password Hashes => PASS
-- |       4.9: Wildcards in user hostname => FAIL
-- |         The following users were found with wildcards in hostname
-- |           root
-- |           super
-- |           super2
-- |       4.10: No blank passwords => PASS
-- |       4.11: Anonymous account => PASS
-- |       5.1: Access to mysql database => REVIEW
-- |         Verify the following users that have access to the MySQL database
-- |           user              host
-- |           root              localhost
-- |           root              patrik-11
-- |           root              127.0.0.1
-- |           debian-sys-maint  localhost
-- |           root              %
-- |           super             %
-- |       5.2: Do not grant FILE privileges to non Admin users => REVIEW
-- |         The following users were found having the FILE privilege
-- |           super
-- |           super2
-- |       5.3: Do not grant PROCESS privileges to non Admin users => REVIEW
-- |         The following users were found having the PROCESS privilege
-- |           super
-- |       5.4: Do not grant SUPER privileges to non Admin users => REVIEW
-- |         The following users were found having the SUPER privilege
-- |           super
-- |       5.5: Do not grant SHUTDOWN privileges to non Admin users => REVIEW
-- |         The following users were found having the SHUTDOWN privilege
-- |           super
-- |       5.6: Do not grant CREATE USER privileges to non Admin users => REVIEW
-- |         The following users were found having the CREATE USER privilege
-- |           super
-- |       5.7: Do not grant RELOAD privileges to non Admin users => REVIEW
-- |         The following users were found having the RELOAD privilege
-- |           super
-- |       5.8: Do not grant GRANT privileges to non Admin users => PASS
-- |       6.2: Disable Load data local => FAIL
-- |       6.3: Disable old password hashing => PASS
-- |       6.4: Safe show database => FAIL
-- |       6.5: Secure auth => FAIL
-- |       6.6: Grant tables => FAIL
-- |       6.7: Skip merge => FAIL
-- |       6.8: Skip networking => FAIL
-- |       6.9: Safe user create => FAIL
-- |       6.10: Skip symbolic links => FAIL
-- |
-- |_      The audit was performed using the db-account: root

-- Version 0.1
-- Created 05/29/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>

author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}


portrule = shortport.port_or_service(3306, "mysql")
local TEMPLATE_NAME, ADMIN_ACCOUNTS = "", ""

local function fail (err) return stdnse.format_output(false, err) end

local function loadAuditRulebase( filename )
  local rules = {}

  local env = setmetatable({
    test = function(t) table.insert(rules, t) end;
  }, {__index = _G})

  filename = nmap.fetchfile("nselib/data/" .. filename) or filename
  stdnse.debug(1, "Loading rules from: %s", filename)
  local file, err = loadfile(filename, "t", env)

  if ( not(file) ) then
    return false, fail(("Failed to load rulebase:\n%s"):format(err))
  end


  file()
  TEMPLATE_NAME = env.TEMPLATE_NAME
  ADMIN_ACCOUNTS = env.ADMIN_ACCOUNTS
  return true, rules
end

action = function( host, port )

  local username = stdnse.get_script_args("mysql-audit.username")
  local password = stdnse.get_script_args("mysql-audit.password")
  local filename = stdnse.get_script_args("mysql-audit.filename") or "mysql-cis.audit"

  if ( not(username) ) then
    return fail("No username was supplied (see mysql-audit.username)")
  end

  local status, tests = loadAuditRulebase( filename )
  if( not(status) ) then return tests end

  local socket = nmap.new_socket()
  status = socket:connect(host, port)

  local response
  status, response = mysql.receiveGreeting( socket )
  if ( not(status) ) then return response end

  status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )

  if ( not(status) ) then return fail("Failed to authenticate") end
  local results = {}

  for _, test in ipairs(tests) do
    local queries = ( "string" == type(test.sql) ) and { test.sql } or test.sql
    local rowstab = {}

    for _, query in ipairs(queries) do
      local row
      status, row = mysql.sqlQuery( socket, query )
      if ( not(status) ) then
        table.insert( results, { ("%s: ERROR: Failed to execute SQL statement"):format(test.id) } )
      else
        table.insert(rowstab, row)
      end
    end

    if ( #rowstab > 0 ) then
      local result_part = {}
      local res = test.check(rowstab)
      local status, data = res.status, res.result
      status = ( res.review and "REVIEW" ) or (status and "PASS" or "FAIL")

      table.insert( result_part, ("%s: %s => %s"):format(test.id, test.desc, status) )
      if ( data ) then
        table.insert(result_part, { data } )
      end
      table.insert( results, result_part )
    end
  end

  socket:close()
  results.name = TEMPLATE_NAME

  table.insert(results, "")
  table.insert(results, {name = "Additional information", ("The audit was performed using the db-account: %s"):format(username),
    ("The following admin accounts were excluded from the audit: %s"):format(table.concat(ADMIN_ACCOUNTS, ","))
  })

return stdnse.format_output(true, { results })
end