require("tab") TEMPLATE_NAME="CIS MySQL Benchmarks v1.0.2" -- These accounts should be treated as admins and excluded from some of the results 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(rs, cname) local tab = {} local cpos for i=1, #rs.cols do if ( rs.cols[i].name == cname ) then cpos = i break end end if ( not(cpos) ) then return end for _, row in ipairs(rs.rows) do table.insert(tab, row[cpos]) 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") local rs = rowstab[1] for _, row in ipairs(rs.rows) do tab.addrow( result, row[1], row[2] ) 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 }