mirror of
https://github.com/nmap/nmap.git
synced 2025-12-10 09:49:05 +00:00
o [NSE] Added a MySQL audit script and a rulebase that supports auditing a
subset of the MySQL CIS 1.0.2 Benchmark. [Patrik]
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Nmap Changelog ($Id$); -*-text-*-
|
||||
|
||||
o [NSE] Added a MySQL audit script and a rulebase that supports auditing a
|
||||
subset of the MySQL CIS 1.0.2 Benchmark. [Patrik]
|
||||
|
||||
o [NSE] Added ipv6 support to the wsdd, dnssd and upnp libraries. Applied
|
||||
patch from Dan Miller that fixes errors in processing and sorting ipv6
|
||||
addresses in scripts using these libraries. [Daniel Miller, Patrik]
|
||||
|
||||
213
nselib/data/mysql-cis.audit
Normal file
213
nselib/data/mysql-cis.audit
Normal file
@@ -0,0 +1,213 @@
|
||||
require("tab")
|
||||
|
||||
TEMPLATE_NAME="CIS MySQL Benchmarks v1.0.2"
|
||||
|
||||
-- These accounts should be treated as admins and excluded from some of the results
|
||||
local ADMIN_ACCOUNTS={"root", "debian-sys-maint"}
|
||||
|
||||
-- Checks whether a resultset is empty or not
|
||||
local function isEmpty(rows)
|
||||
if ( #rows > 0 ) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Extracts a column from a row and return all occurances as an array
|
||||
local function col2tab(rows, cname)
|
||||
local tab = {}
|
||||
for _, row in ipairs(rows) do table.insert(tab, row[cname]) end
|
||||
return tab
|
||||
end
|
||||
|
||||
local function createINstmt(tab)
|
||||
local tab2 = {}
|
||||
for i=1, #tab do tab2[i] = ("'%s'"):format(tab[i]) end
|
||||
return stdnse.strjoin(",", tab2)
|
||||
end
|
||||
|
||||
|
||||
-- This next section contains all the tests
|
||||
|
||||
-- Logging
|
||||
test { id="3.1", desc="Skip symbolic links", sql="SHOW variables WHERE Variable_name = 'log_error' AND Value IS NOT NULL", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="3.2", desc="Logs not on system partition", sql="SHOW variables WHERE Variable_name = 'log_bin' AND Value <> 'OFF'", check=function(rowstab)
|
||||
local log = col2tab(rowstab[1], 'Value')
|
||||
return { status = isEmpty(rowstab[1]), result = log, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="3.2", desc="Logs not on database partition", sql="SHOW variables WHERE Variable_name = 'log_bin' AND Value <> 'OFF'", check=function(rowstab)
|
||||
local log = col2tab(rowstab[1], 'Value')
|
||||
return { status = isEmpty(rowstab[1]), result = log, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
-- General
|
||||
test { id="4.1", desc="Supported version of MySQL", sql="SHOW VARIABLES like 'version'", check=function(rowstab)
|
||||
local ver = col2tab(rowstab[1], 'Value')[1]
|
||||
return { status = true, review = true, result = ("Version: %s"):format(ver) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="4.4", desc="Remove test database", sql="SHOW DATABASES like 'test'", check=function(rowstab) return { status = isEmpty(rowstab[1]) } end }
|
||||
|
||||
test { id="4.5", desc="Change admin account name", sql="SELECT user FROM mysql.user WHERE user='root';", check=function(rowstab) return { status = isEmpty(rowstab[1]) } end }
|
||||
|
||||
test { id="4.7", desc="Verify Secure Password Hashes", sql="SELECT DISTINCT user, password from mysql.user where length(password) < 41 AND length(password) > 0", check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having weak password hashes"
|
||||
return { status = isEmpty(rowstab[1]), result = users }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="4.9", desc="Wildcards in user hostname", sql="select user from mysql.user where host = '%'", check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found with wildcards in hostname"
|
||||
return { status = isEmpty(rowstab[1]), result = users }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="4.10", desc="No blank passwords", sql="select distinct user, password from mysql.user where length(password) = 0 or password is null", check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having blank/empty passwords"
|
||||
return { status = isEmpty(rowstab[1]), result = users }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="4.11", desc="Anonymous account", sql="select distinct user from mysql.user where user =''", check=function(rowstab) return { status = isEmpty(rowstab[1]) } end }
|
||||
|
||||
|
||||
-- MySQL Permissions
|
||||
test { id="5.1", desc="Access to mysql database",
|
||||
sql = { "SELECT user, host FROM mysql.db WHERE db = 'mysql' and ((Select_priv = 'Y') or (Insert_priv = 'Y') " ..
|
||||
"or (Update_priv = 'Y') or (Delete_priv = 'Y') or (Create_priv = 'Y') or (Drop_priv = 'Y'))",
|
||||
"SELECT user, host FROM mysql.user WHERE (Select_priv = 'Y') or (Insert_priv = 'Y') or " ..
|
||||
"(Update_priv = 'Y') or (Delete_priv = 'Y') or (Create_priv = 'Y') or (Drop_priv = 'Y')" },
|
||||
check = function(rowstab)
|
||||
|
||||
local result = tab.new(2)
|
||||
tab.addrow(result, "user", "host")
|
||||
|
||||
for _, rows in ipairs(rowstab) do
|
||||
for _, row in ipairs(rows) do
|
||||
tab.addrow( result, row.user, row.host )
|
||||
end
|
||||
end
|
||||
|
||||
return { status = false, review = true, result = { tab.dump(result), name="Verify the following users that have access to the MySQL database" } }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.2", desc="Do not grant FILE privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE File_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the FILE privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.3", desc="Do not grant PROCESS privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE Process_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the PROCESS privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.4", desc="Do not grant SUPER privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE Super_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the SUPER privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.5", desc="Do not grant SHUTDOWN privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE Shutdown_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the SHUTDOWN privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.6", desc="Do not grant CREATE USER privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE Create_user_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the CREATE USER privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.7", desc="Do not grant RELOAD privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE Reload_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the RELOAD privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="5.8", desc="Do not grant GRANT privileges to non Admin users",
|
||||
sql=("SELECT user, host FROM mysql.user WHERE Grant_priv = 'Y' AND user NOT IN (%s)"):format(createINstmt(ADMIN_ACCOUNTS)),
|
||||
check=function(rowstab)
|
||||
local users = col2tab(rowstab[1], 'user')
|
||||
users.name = ( #users > 0 ) and "The following users were found having the GRANT privilege"
|
||||
return { status = isEmpty(rowstab[1]), result = users, review = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
-- MySQL Configuraiton options
|
||||
test { id="6.2", desc="Disable Load data local", sql="SHOW variables WHERE Variable_name = 'local_infile' AND Value='OFF'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.3", desc="Disable old password hashing", sql="SHOW variables WHERE Variable_name = 'old_passwords' AND Value='OFF'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.4", desc="Safe show database", sql="SHOW variables WHERE Variable_name = 'safe_show_database' AND Value='ON'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.5", desc="Secure auth", sql="SHOW variables WHERE Variable_name = 'secure_auth' AND Value='ON'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.6", desc="Grant tables", sql="SHOW variables WHERE Variable_name = 'skip_grant_tables' AND Value='OFF'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.7", desc="Skip merge", sql="SHOW variables WHERE Variable_name = 'have_merge_engine' AND Value='DISABLED'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.8", desc="Skip networking", sql="SHOW variables WHERE Variable_name = 'skip_networking' AND Value='ON'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.9", desc="Safe user create", sql="select @@global.sql_mode, @@session.sql_mode FROM dual WHERE @@session.sql_mode='NO_AUTO_CREATE_USER' AND @@global.sql_mode='NO_AUTO_CREATE_USER'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
|
||||
test { id="6.10", desc="Skip symbolic links", sql="SHOW variables WHERE Variable_name = 'have_symlink' AND Value='DISABLED'", check=function(rowstab)
|
||||
return { status = not(isEmpty(rowstab[1])) }
|
||||
end
|
||||
}
|
||||
170
scripts/mysql-audit.nse
Normal file
170
scripts/mysql-audit.nse
Normal file
@@ -0,0 +1,170 @@
|
||||
description = [[
|
||||
Audit MySQL database server
|
||||
]]
|
||||
|
||||
|
||||
---
|
||||
-- @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
|
||||
--
|
||||
-- @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 http://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "safe"}
|
||||
|
||||
require 'shortport'
|
||||
require 'mysql'
|
||||
|
||||
portrule = shortport.port_or_service(3306, "mysql")
|
||||
local TEMPLATE_NAME = ""
|
||||
|
||||
local function loadAuditRulebase( filename )
|
||||
|
||||
local file, err = loadfile(filename)
|
||||
local rules = {}
|
||||
|
||||
if ( not(file) ) then
|
||||
return false, ("ERROR: Failed to load rulebase:\n%s"):format(err)
|
||||
end
|
||||
|
||||
setfenv(file, setmetatable({
|
||||
test = function(t) table.insert(rules, t) end;
|
||||
}, {__index = _G}))
|
||||
|
||||
file()
|
||||
TEMPLATE_NAME = getfenv(file)["TEMPLATE_NAME"]
|
||||
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")
|
||||
|
||||
if ( not(filename) ) then
|
||||
return "\n No audit rulebase file was supplied (see mysql-audit.filename)"
|
||||
end
|
||||
|
||||
if ( not(username) ) then
|
||||
return "\n No username was supplied (see mysql-audit.username)"
|
||||
end
|
||||
|
||||
local status, tests = loadAuditRulebase( filename )
|
||||
if( not(status) ) then return rules 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 "ERROR: 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, {"", ("The audit was performed using the db-account: %s"):format(username)})
|
||||
|
||||
return stdnse.format_output(true, { results })
|
||||
end
|
||||
@@ -107,6 +107,7 @@ Entry { filename = "ms-sql-info.nse", categories = { "default", "discovery", "sa
|
||||
Entry { filename = "ms-sql-query.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "ms-sql-tables.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "ms-sql-xp-cmdshell.nse", categories = { "intrusive", } }
|
||||
Entry { filename = "mysql-audit.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "mysql-brute.nse", categories = { "auth", "intrusive", } }
|
||||
Entry { filename = "mysql-databases.nse", categories = { "discovery", "intrusive", } }
|
||||
Entry { filename = "mysql-empty-password.nse", categories = { "auth", "intrusive", } }
|
||||
|
||||
Reference in New Issue
Block a user