diff --git a/CHANGELOG b/CHANGELOG
index a46195886..7f17d1093 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*-
+o Added two new scripts mysql-query and mysql-dump-hashes, which add support
+ for performing custom MySQL queries and dump MySQL password hashes. [Patrik
+ Karlsson]
+
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]
diff --git a/scripts/mysql-dump-hashes.nse b/scripts/mysql-dump-hashes.nse
new file mode 100644
index 000000000..3f32f710c
--- /dev/null
+++ b/scripts/mysql-dump-hashes.nse
@@ -0,0 +1,100 @@
+description = [[
+Dumps the password hashes from an MySQL server in a format suitable for
+cracking by tools such as John-the-ripper. In order to do so the user
+needs to have the appropriate DB privileges (root).
+
+The username and password arguments take precedence
+over credentials discovered by the mysql-brute and mysql-empty-password
+scripts.
+]]
+
+---
+-- @usage
+-- nmap -p 3306 --script mysql-dump-hashes --script-args='username=root,password=secret'
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3306/tcp open mysql
+-- | mysql-dump-hashes:
+-- | root:*9B500343BC52E2911172EB52AE5CF4847604C6E5
+-- | debian-sys-maint:*92357EE43977D9228AC9C0D60BB4B4479BD7A337
+-- |_ toor:*14E65567ABDB5135D0CFD9A70B3032C179A49EE7
+--
+-- @args username the username to use to connect to the server
+-- @args password the password to use to connect to the server
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"auth", "discovery", "safe"}
+
+local shortport = require('shortport')
+local mysql = require('mysql')
+
+dependencies = {"mysql-empty-password", "mysql-brute"}
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") or ""
+
+local function fail(err) return ("\n ERROR: %s"):format(err or "") end
+
+local function getCredentials()
+ -- first, let's see if the script has any credentials as arguments?
+ if ( arg_username ) then
+ return { [arg_username] = arg_password }
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ return { ['root'] = nmap.registry.mysqlusers['root'] }
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ return nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ end
+end
+
+local function mysqlLogin(socket, username, password)
+ local status, response = mysql.receiveGreeting( socket )
+ if ( not(status) ) then
+ return response
+ end
+ return mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+end
+
+
+action = function(host, port)
+ local creds = getCredentials()
+ if ( not(creds) ) then
+ stdnse.print_debug(2, "No credentials were supplied, aborting ...")
+ return
+ end
+
+ local result = {}
+ for username, password in pairs(creds) do
+ local socket = nmap.new_socket()
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, response = mysqlLogin(socket, username, password)
+ if ( status ) then
+ local query = "SELECT DISTINCT CONCAT(user, ':', password) FROM mysql.user WHERE password <> ''"
+ local status, rows = mysql.sqlQuery( socket, query )
+ socket:close()
+ if ( status ) then
+ result = mysql.formatResultset(rows, { noheaders = true })
+ break
+ end
+ else
+ socket:close()
+ end
+ end
+
+ if ( result ) then
+ return stdnse.format_output(true, result)
+ end
+end
\ No newline at end of file
diff --git a/scripts/mysql-query.nse b/scripts/mysql-query.nse
new file mode 100644
index 000000000..cbc7be9f3
--- /dev/null
+++ b/scripts/mysql-query.nse
@@ -0,0 +1,115 @@
+description = [[
+Runs a query against a MySQL database and returns the results as a table.
+]]
+
+---
+-- @usage
+-- nmap -p 3306 --script mysql-query --script-args='query=""[,username=,password=]'
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3306/tcp open mysql
+-- | mysql-query:
+-- | host user
+-- | 127.0.0.1 root
+-- | localhost debian-sys-maint
+-- | localhost root
+-- | ubu1110 root
+-- |
+-- | Query: SELECT host, user FROM mysql.user
+-- |_ User: root
+--
+-- @args query the query for which to return the results
+-- @args username (optional) the username used to authenticate to the database server
+-- @args password (optional) the password used to authenticate to the database server
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"auth", "discovery", "safe"}
+
+local shortport = require('shortport')
+local mysql = require('mysql')
+local tab = require('tab')
+
+dependencies = {"mysql-empty-password", "mysql-brute"}
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") or ""
+local arg_query = stdnse.get_script_args(SCRIPT_NAME .. ".query")
+local arg_noheaders = stdnse.get_script_args(SCRIPT_NAME .. ".noheaders") or false
+
+local function fail(err) return ("\n ERROR: %s"):format(err or "") end
+
+local function getCredentials()
+ -- first, let's see if the script has any credentials as arguments?
+ if ( arg_username ) then
+ return { [arg_username] = arg_password }
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ return { ['root'] = nmap.registry.mysqlusers['root'] }
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ return nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ end
+end
+
+local function mysqlLogin(socket, username, password)
+ local status, response = mysql.receiveGreeting( socket )
+ if ( not(status) ) then
+ return response
+ end
+ return mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+end
+
+
+action = function(host, port)
+ if ( not(arg_query) ) then
+ stdnse.print_debug(2, "No query was given, aborting ...")
+ return
+ end
+
+ local creds = getCredentials()
+ if ( not(creds) ) then
+ stdnse.print_debug(2, "No credentials were supplied, aborting ...")
+ return
+ end
+
+ if ( arg_noheaders == '1' or arg_noheaders == 'true' ) then
+ arg_noheaders = true
+ else
+ arg_noheaders = false
+ end
+
+ local result = {}
+ local last_error
+
+ for username, password in pairs(creds) do
+ local socket = nmap.new_socket()
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to server")
+ end
+ local status, response = mysqlLogin(socket, username, password)
+ if ( status ) then
+ local status, rs = mysql.sqlQuery( socket, arg_query )
+ socket:close()
+ if ( status ) then
+ result = mysql.formatResultset(rs, { noheaders = arg_noheaders })
+ result = ("%s\nQuery: %s\nUser: %s"):format(result, arg_query, username)
+ last_error = nil
+ break
+ else
+ last_error = rs
+ end
+ else
+ socket:close()
+ end
+ end
+ return stdnse.format_output(true, (last_error and ("ERROR: %s"):format(last_error) or result))
+end
\ No newline at end of file
diff --git a/scripts/script.db b/scripts/script.db
index bb340a7fd..42af14048 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -230,8 +230,10 @@ Entry { filename = "ms-sql-xp-cmdshell.nse", categories = { "intrusive", } }
Entry { filename = "mysql-audit.nse", categories = { "discovery", "safe", } }
Entry { filename = "mysql-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "mysql-databases.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "mysql-dump-hashes.nse", categories = { "auth", "discovery", "safe", } }
Entry { filename = "mysql-empty-password.nse", categories = { "auth", "intrusive", } }
Entry { filename = "mysql-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "mysql-query.nse", categories = { "auth", "discovery", "safe", } }
Entry { filename = "mysql-users.nse", categories = { "auth", "intrusive", } }
Entry { filename = "mysql-variables.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "nat-pmp-info.nse", categories = { "default", "discovery", "safe", } }