1
0
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:
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-*-
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
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-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", } }