1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-12 02:39:03 +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:
patrik
2011-06-17 06:12:01 +00:00
parent 092772e1b5
commit 0a3bf95897
4 changed files with 387 additions and 0 deletions

View File

@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*- # 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 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 patch from Dan Miller that fixes errors in processing and sorting ipv6
addresses in scripts using these libraries. [Daniel Miller, Patrik] addresses in scripts using these libraries. [Daniel Miller, Patrik]

213
nselib/data/mysql-cis.audit Normal file
View 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
View 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

View File

@@ -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-query.nse", categories = { "discovery", "safe", } }
Entry { filename = "ms-sql-tables.nse", categories = { "discovery", "safe", } } Entry { filename = "ms-sql-tables.nse", categories = { "discovery", "safe", } }
Entry { filename = "ms-sql-xp-cmdshell.nse", categories = { "intrusive", } } 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-brute.nse", categories = { "auth", "intrusive", } }
Entry { filename = "mysql-databases.nse", categories = { "discovery", "intrusive", } } Entry { filename = "mysql-databases.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "mysql-empty-password.nse", categories = { "auth", "intrusive", } } Entry { filename = "mysql-empty-password.nse", categories = { "auth", "intrusive", } }