diff --git a/CHANGELOG b/CHANGELOG index a65aab0a1..a46195886 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o Improved the mysql library to handle multiple columns with the same name, + added a formatResultset function to format a query response to a table + suitable for script output. [Patrik Karlsson] + o The message "nexthost: failed to determine route to ..." is now a warning rather than a fatal error. Addresses that are skipped in this way are recorded in the XML output as elements. [David diff --git a/nselib/data/mysql-cis.audit b/nselib/data/mysql-cis.audit index 0645fe0bb..9cd9f8d2e 100644 --- a/nselib/data/mysql-cis.audit +++ b/nselib/data/mysql-cis.audit @@ -3,7 +3,7 @@ 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"} +ADMIN_ACCOUNTS={"root", "debian-sys-maint"} -- Checks whether a resultset is empty or not local function isEmpty(rows) @@ -12,9 +12,19 @@ local function isEmpty(rows) end -- Extracts a column from a row and return all occurances as an array -local function col2tab(rows, cname) +local function col2tab(rs, cname) local tab = {} - for _, row in ipairs(rows) do table.insert(tab, row[cname]) end + 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 @@ -92,10 +102,9 @@ test { id="5.1", desc="Access to mysql database", 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 + 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" } } diff --git a/nselib/mysql.lua b/nselib/mysql.lua index d9af71471..0ec1877d0 100644 --- a/nselib/mysql.lua +++ b/nselib/mysql.lua @@ -17,6 +17,8 @@ module(... or "mysql", package.seeall) -- fixed a number of incorrect receives and changed -- them to receive_bytes instead. +local tab = require('tab') + local HAVE_SSL = false if pcall(require,'openssl') then @@ -455,11 +457,10 @@ end -- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Row_Data_Packet -- -- @param data string containing the row data packet --- @param fields table containing the field data as recieved from decodeFieldPackets -- @param count number containing the number of fields to decode -- @return status true on success, false on failure -- @return rows table containing row tables -function decodeDataPackets( data, fields, count ) +function decodeDataPackets( data, count ) local len, pos = 0, 1, 1 local header, row, rows = {}, {}, {} @@ -470,7 +471,7 @@ function decodeDataPackets( data, fields, count ) for i=1, count do pos, len = bin.unpack("C", data, pos ) - pos, row[fields[i].name] = bin.unpack("A" .. len, data, pos) + pos, row[i] = bin.unpack("A" .. len, data, pos) end table.insert( rows, row ) @@ -530,12 +531,39 @@ function sqlQuery( socket, query ) return false, fields end - status, rows = decodeDataPackets(rs.data, fields, field_count) + status, rows = decodeDataPackets(rs.data, field_count) if not status then return false, rows end - return true, rows - + return true, { cols = fields, rows = rows } +end + +--- +-- Formats the resultset returned from sqlQuery +-- +-- @param rs table as returned from sqlQuery +-- @param options table containing additional options, currently: +-- - noheaders - does not include column names in result +-- @return string containing the formated resultset table +function formatResultset(rs, options) + options = options or {} + if ( not(rs) or not(rs.cols) or not(rs.rows) ) then + return + end + + local restab = tab.new(#rs.cols) + local colnames = {} + + if ( not(options.noheaders) ) then + for _, col in ipairs(rs.cols) do table.insert(colnames, col.name) end + tab.addrow(restab, unpack(colnames)) + end + + for _, row in ipairs(rs.rows) do + tab.addrow(restab, unpack(row)) + end + + return tab.dump(restab) end diff --git a/scripts/mysql-audit.nse b/scripts/mysql-audit.nse index 45b3bd56e..8c17c5fd1 100644 --- a/scripts/mysql-audit.nse +++ b/scripts/mysql-audit.nse @@ -86,7 +86,7 @@ require 'shortport' require 'mysql' portrule = shortport.port_or_service(3306, "mysql") -local TEMPLATE_NAME = "" +local TEMPLATE_NAME, ADMIN_ACCOUNTS = "", "" local function loadAuditRulebase( filename ) @@ -103,6 +103,7 @@ local function loadAuditRulebase( filename ) file() TEMPLATE_NAME = getfenv(file)["TEMPLATE_NAME"] + ADMIN_ACCOUNTS = getfenv(file)["ADMIN_ACCOUNTS"] return true, rules end @@ -121,7 +122,7 @@ action = function( host, port ) end local status, tests = loadAuditRulebase( filename ) - if( not(status) ) then return rules end + if( not(status) ) then return tests end local socket = nmap.new_socket() status = socket:connect(host, port) @@ -166,7 +167,10 @@ action = function( host, port ) socket:close() results.name = TEMPLATE_NAME - table.insert(results, {"", ("The audit was performed using the db-account: %s"):format(username)}) + 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(stdnse.strjoin(",", ADMIN_ACCOUNTS)) + }) return stdnse.format_output(true, { results }) end \ No newline at end of file diff --git a/scripts/mysql-databases.nse b/scripts/mysql-databases.nse index 503686038..b6ee7f7ac 100644 --- a/scripts/mysql-databases.nse +++ b/scripts/mysql-databases.nse @@ -78,13 +78,10 @@ action = function( host, port ) status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt ) if status and response.errorcode == 0 then - status, rows = mysql.sqlQuery( socket, "show databases" ) + local status, rs = mysql.sqlQuery( socket, "show databases" ) if status then - for i=1, #rows do - -- cheap way of avoiding duplicates - dbs[rows[i]['Database']] = rows[i]['Database'] - end - + result = mysql.formatResultset(rs, { noheaders = true }) + -- if we got here as root, we've got them all -- if we're here as someone else, we cant be sure if username == 'root' then @@ -94,11 +91,5 @@ action = function( host, port ) end socket:close() end - - for _, v in pairs( dbs ) do - table.insert(result, v) - end - return stdnse.format_output(true, result) - end diff --git a/scripts/mysql-users.nse b/scripts/mysql-users.nse index 6b98b8e00..81a64d74e 100644 --- a/scripts/mysql-users.nse +++ b/scripts/mysql-users.nse @@ -81,12 +81,9 @@ action = function( host, port ) status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt ) if status and response.errorcode == 0 then - status, rows = mysql.sqlQuery( socket, "SELECT DISTINCT user FROM mysql.user" ) + status, rs = mysql.sqlQuery( socket, "SELECT DISTINCT user FROM mysql.user" ) if status then - for i=1, #rows do - table.insert(result, rows[i]['user']) - end - break + result = mysql.formatResultset(rs, { noheaders = true }) end end socket:close() diff --git a/scripts/mysql-variables.nse b/scripts/mysql-variables.nse index 8420d6acc..0cd6e997b 100644 --- a/scripts/mysql-variables.nse +++ b/scripts/mysql-variables.nse @@ -89,10 +89,10 @@ action = function( host, port ) status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt ) if status and response.errorcode == 0 then - status, rows = mysql.sqlQuery( socket, "show variables" ) + local status, rs = mysql.sqlQuery( socket, "show variables" ) if status then - for i=1, #rows do - table.insert(result, string.format("%s: %s" , rows[i]['Variable_name'], rows[i]['Value']) ) + for _, row in ipairs(rs.rows) do + table.insert(result, ("%s: %s"):format(row[1], row[2]) ) end end end