From 0bc8e658116daa04cbac13454d81a5347778a914 Mon Sep 17 00:00:00 2001 From: patrik Date: Tue, 26 Jan 2010 09:40:38 +0000 Subject: [PATCH] Add the scripts mysql-brute mysql-datatabase mysql-empty-password mysql-users mysql-variables and the mysql module --- CHANGELOG | 8 + nselib/mysql.lua | 502 +++++++++++++++++++++++++++++++ scripts/mysql-brute.nse | 85 ++++++ scripts/mysql-databases.nse | 99 ++++++ scripts/mysql-empty-password.nse | 54 ++++ scripts/mysql-users.nse | 92 ++++++ scripts/mysql-variables.nse | 100 ++++++ 7 files changed, 940 insertions(+) create mode 100644 nselib/mysql.lua create mode 100644 scripts/mysql-brute.nse create mode 100644 scripts/mysql-databases.nse create mode 100644 scripts/mysql-empty-password.nse create mode 100644 scripts/mysql-users.nse create mode 100644 scripts/mysql-variables.nse diff --git a/CHANGELOG b/CHANGELOG index c738f3771..1c2e05cd0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,13 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added 5 new NSE scripts and a library for use with MySQL. + * mysql-brute uses the unpwdb library to guess credentials for MySQL + * mysql-databases queries MySQL for a list of databases + * mysql-empty-password attempts to authenticate anonymously or as root with + an empty password + * mysql-users queries MySQL for a list of database users + * mysql-variables queries MySQL for it's variables and their settings + o [NSE] Added the new daap-get-library script which uses the Digital Audio Access Protocol to enumerate the contents of a library. The contents contain the name of the artist, album and song. diff --git a/nselib/mysql.lua b/nselib/mysql.lua new file mode 100644 index 000000000..59a71ad8a --- /dev/null +++ b/nselib/mysql.lua @@ -0,0 +1,502 @@ +--- Simple MySQL Library supporting a very limited subset of operations +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- +-- +-- @author = "Patrik Karlsson " +-- +-- Version 0.2 +-- +-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson +-- Revised 01/23/2010 - v0.2 - added query support, cleanup, documentation + +-- http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol + +module(... or "mysql", package.seeall) + +require 'openssl' + +Capabilities = +{ + LongPassword = 0x1, + FoundRows = 0x2, + LongColumnFlag = 0x4, + ConnectWithDatabase = 0x8, + DontAllowDatabaseTableColumn = 0x10, + SupportsCompression = 0x20, + ODBCClient = 0x40, + SupportsLoadDataLocal = 0x80, + IgnoreSpaceBeforeParanthesis = 0x100, + Speaks41ProtocolNew = 0x200, + InteractiveClient = 0x400, + SwitchToSSLAfterHandshake = 0x800, + IgnoreSigpipes = 0x1000, + SupportsTransactions = 0x2000, + Speaks41ProtocolOld = 0x4000, + Support41Auth = 0x8000 +} + +ExtCapabilities = +{ + SupportsMultipleStatments = 0x1, + SupportsMultipleResults = 0x2 +} + +Charset = +{ + latin1_COLLATE_latin1_swedish_ci = 0x8 +} + +ServerStatus = +{ + InTransaction = 0x1, + AutoCommit = 0x2, + MoreResults = 0x4, + MultiQuery = 0x8, + BadIndexUsed = 0x10, + NoIndexUsed = 0x20, + CursorExists = 0x40, + LastRowSebd = 0x80, + DatabaseDropped = 0x100, + NoBackslashEscapes = 0x200 +} + +Command = +{ + Query = 3 +} + +local MAXPACKET = 16777216 +local HEADER_SIZE = 4 + + +--- Parses a MySQL header +-- +-- @param data string of raw data +-- @return response table containing the fields len and packetno +local function decodeHeader( data, pos ) + + local response = {} + local pos, tmp = pos or 1, 0 + + pos, tmp = bin.unpack( "I", data, pos ) + response.len = bit.band( tmp,255 ) + response.number = bit.rshift( tmp, 24 ) + + return pos, response +end + +--- Recieves the server greeting upon intial connection +-- +-- @param socket already connected to the remote server +-- @return status true on success, false on failure +-- @return response table with the following fields proto, version, +-- threadid, salt, capabilities, charset and +-- status or error message on failure (status == false) +function receiveGreeting( socket ) + + local catch = function() socket:close() stdnse.print_debug("receiveGreeting(): failed") end + local try = nmap.new_try(catch) + local data = try( socket:receive(4) ) + local pos, response, tmp, _ + + pos, response = decodeHeader( data, 1 ) + + if response.len > data:len() then + stdnse.print_debug( "Missing %d bytes of data, receiving ... ", response.len - data:len() ) + end + + pos, response.proto = bin.unpack( "C", data, pos ) + pos, response.version = bin.unpack( "z", data, pos ) + pos, response.threadid = bin.unpack( "I", data, pos ) + pos, response.salt, _ = bin.unpack( "A8C", data, pos ) + pos, response.capabilities = bin.unpack( "S", data, pos ) + pos, response.charset = bin.unpack( "C", data, pos ) + pos, response.status = bin.unpack( "S", data, pos ) + pos, _ = bin.unpack( "A13", data, pos ) + pos, tmp = bin.unpack( "A12", data, pos ) + + response.salt = response.salt .. tmp + + return true, response + +end + +--- Creates a hashed value of the password and salt according to MySQL authentication post version 4.1 +-- +-- @param pass string containing the users password +-- @param salt string containing the servers salt as obtained from receiveGreeting +-- @return reply string containing the raw hashed value +local function createLoginHash(pass, salt) + + local hash_stage1 = openssl.sha1( pass ) + local hash_stage2 = openssl.sha1( hash_stage1 ) + local hash_stage3 = openssl.sha1( salt .. hash_stage2 ) + local reply = "" + + local pos, b1, b2, b3, _ = 1, 0, 0, 0 + + for pos=1, hash_stage1:len() do + _, b1 = bin.unpack( "C", hash_stage1, pos ) + _, b2 = bin.unpack( "C", hash_stage3, pos ) + + reply = reply .. string.char( bit.bxor( b2, b1 ) ) + end + + return reply + +end + +--- Attempts to Login to the remote mysql server +-- +-- @param socket already connected to the remote server +-- @param params table with additional options to the loginrequest +-- current supported fields are charset and authversion +-- authversion is either "pre41" or "post41" (default is post41) +-- currently only post41 authentication is supported +-- @param username string containing the username of the user that is authenticating +-- @param password string containing the users password or nil if empty +-- @param salt string containing the servers salt as recieved from receiveGreeting +-- @return status boolean +-- @return response table or error message on failure +function loginRequest( socket, params, username, password, salt ) + + local catch = function() socket:close() stdnse.print_debug("receiveGreeting(): failed") end + local try = nmap.new_try(catch) + local packetno = 1 + local authversion = params.authversion or "post41" + local username = username or "" + + if authversion ~= "post41" then + return false, "Unsupported authentication version: " .. authversion + end + + local clicap = Capabilities.LongPassword + clicap = clicap + Capabilities.LongColumnFlag + clicap = clicap + Capabilities.SupportsLoadDataLocal + clicap = clicap + Capabilities.Speaks41ProtocolNew + clicap = clicap + Capabilities.InteractiveClient + clicap = clicap + Capabilities.SupportsTransactions + clicap = clicap + Capabilities.Support41Auth + + local extcapabilities = ExtCapabilities.SupportsMultipleStatments + extcapabilities = extcapabilities + ExtCapabilities.SupportsMultipleResults + + local packet = bin.pack( "S", clicap ) + packet = packet .. bin.pack( "S", extcapabilities ) + packet = packet .. bin.pack( "I", MAXPACKET ) + packet = packet .. bin.pack( "C", Charset.latin1_COLLATE_latin1_swedish_ci ) + packet = packet .. bin.pack( "A", string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) ) + packet = packet .. bin.pack( "z", username ) + + if ( password ~= nil and password:len() > 0 ) then + local hash = createLoginHash( password, salt ) + packet = packet .. bin.pack( "A", string.char( 0x14 ) .. hash ) + else + packet = packet .. bin.pack( "C", 0 ) + end + + local tmp = packet:len() + bit.lshift( packetno, 24 ) + + packet = bin.pack( "I", tmp ) .. packet + + try( socket:send(packet) ) + packet = try( socket:receive(packet) ) + + local pos, response = decodeHeader( packet ) + local is_error + + pos, is_error = bin.unpack( "C", packet, pos ) + + if is_error > 0 then + pos, response.errorcode = bin.unpack( "S", packet, pos ) + + local has_sqlstate + pos, has_sqlstate = bin.unpack( "C", packet, pos ) + + if has_sqlstate == 35 then + pos, response.sqlstate = bin.unpack( "A5", packet, pos ) + end + + pos, response.errormessage = bin.unpack( "z", packet, pos ) + + return false, response.errormessage + else + response.errorcode = 0 + pos, response.affectedrows = bin.unpack( "C", packet, pos ) + pos, response.serverstatus = bin.unpack( "S", packet, pos ) + pos, response.warnings = bin.unpack( "S", packet, pos ) + end + + return true, response + +end + +--- Decodes a single column field +-- +-- http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Field_Packet +-- +-- @param data string containing field packets +-- @param pos number containing position from which to start decoding +-- the position should point to the data in this buffer (ie. after the header) +-- @return pos number containing the position after the field was decoded +-- @return field table containing catalog, database, table, +-- origt_table, name, orig_name, +-- length and type +function decodeField( data, pos ) + + local header, len + local def, _ + local field = {} + + pos, len = bin.unpack( "C", data, pos ) + pos, field.catalog = bin.unpack( "A" .. len, data, pos ) + + pos, len = bin.unpack( "C", data, pos ) + pos, field.database = bin.unpack( "A" .. len, data, pos ) + + pos, len = bin.unpack( "C", data, pos ) + pos, field.table = bin.unpack( "A" .. len, data, pos ) + + pos, len = bin.unpack( "C", data, pos ) + pos, field.orig_table = bin.unpack( "A" .. len, data, pos ) + + pos, len = bin.unpack( "C", data, pos ) + pos, field.name = bin.unpack( "A" .. len, data, pos ) + + pos, len = bin.unpack( "C", data, pos ) + pos, field.orig_name = bin.unpack( "A" .. len, data, pos ) + + -- should be 0x0C + pos, _ = bin.unpack( "C", data, pos ) + + -- charset, in my case 0x0800 + pos, _ = bin.unpack( "S", data, pos ) + + pos, field.length = bin.unpack( "I", data, pos ) + pos, field.type = bin.unpack( "A6", data, pos ) + + return pos, field + +end + +--- Decodes the result set header packet into it's sub components +-- +-- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Result_Set_Header_Packet +-- +-- @param socket socket already connected to MySQL server +-- @return table containing the following header, fields and data +function decodeQueryResponse( socket ) + + local catch = function() socket:close() stdnse.print_debug("sqlQuery(): failed") end + local try = nmap.new_try(catch) + local data, header, pos + local rs, blocks = {}, {} + local block_start, block_end + local EOF_MARKER = 254 + + data = try( socket:receive(HEADER_SIZE) ) + pos, header = decodeHeader( data, pos ) + + -- + -- First, Let's attempt to read the "Result Set Header Packet" + -- + if data:len() < header.len then + data = data .. try( socket:receive( header.len - data:len() ) ) + end + + rs.header = data:sub( 1, HEADER_SIZE + header.len ) + + -- abort on MySQL error + if rs.header:sub(HEADER_SIZE + 1, HEADER_SIZE + 1) == string.char(0xFF) then + -- is this a 4.0 or 4.1 error message + if rs.header:find("#") then + return false, rs.header:sub(HEADER_SIZE+10) + else + return false, rs.header:sub(HEADER_SIZE+4) + end + end + + pos = HEADER_SIZE + header.len + 1 + + -- Second, Let's attempt to read the "Field Packets" and "Row Data Packets" + -- They're separated by an "EOF Packet" + for i=1,2 do + + -- marks the start of our block + block_start = pos + + while true do + + if data:len() - pos < HEADER_SIZE then + data = data .. try( socket:receive( HEADER_SIZE - ( data:len() - pos ) ) ) + end + + pos, header = decodeHeader( data, pos ) + + if data:len() - pos < header.len - 1 then + data = data .. try( socket:receive( header.len - ( data:len() - pos ) ) ) + end + + if header.len > 0 then + local _, b = bin.unpack("C", data, pos ) + + -- Is this the EOF packet? + if b == EOF_MARKER then + -- we don't want the EOF Packet included + block_end = pos - HEADER_SIZE + pos = pos + header.len + break + end + end + + pos = pos + header.len + + end + + blocks[i] = data:sub( block_start, block_end ) + + end + + + rs.fields = blocks[1] + rs.data = blocks[2] + + return true, rs + +end + +--- Decodes as field packet and returns a table of field tables +-- +-- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Field_Packet +-- +-- @param data string containing field packets +-- @param count number containing the amount of fields to decode +-- @return status boolean (true on success, false on failure) +-- @return fields table containing field tables as returned by decodeField +-- or string containing error message if status is false +function decodeFieldPackets( data, count ) + + local pos, header + local field, fields = {}, {} + + if count < 1 then + return false, "Field count was less than one, aborting" + end + + for i=1, count do + pos, header = decodeHeader( data, pos ) + pos, field = decodeField( data, pos ) + table.insert( fields, field ) + end + + return true, fields +end + +-- Decodes the result set header +-- +-- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Result_Set_Header_Packet +-- +-- @param data string containing the result set header packet +-- @return number containing the amount of fields +function decodeResultSetHeader( data ) + + local _, fields + + if data:len() ~= HEADER_SIZE + 1 then + return false, "Result set header was incorrect" + end + + _, fields = bin.unpack( "C", data, HEADER_SIZE + 1 ) + + return true, fields +end + +--- Decodes the row data +-- +-- 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 ) + + local len, pos = 0, 1, 1 + local header, row, rows = {}, {}, {} + + while pos < data:len() do + row = {} + pos, header = decodeHeader( data, pos ) + + for i=1, count do + pos, len = bin.unpack("C", data, pos ) + pos, row[fields[i].name] = bin.unpack("A" .. len, data, pos) + end + + table.insert( rows, row ) + + end + + return true, rows + +end + +--- Sends the query to the MySQL server and then attempts to decode the response +-- +-- @param socket socket already connected to mysql +-- @param query string containing the sql query +-- @return status true on success, false on failure +-- @return rows table containing row tabels as decoded by decodeDataPackets +function sqlQuery( socket, query ) + + local catch = function() socket:close() stdnse.print_debug("sqlQuery(): failed") end + local try = nmap.new_try(catch) + local packetno = 0 + local querylen = query:len() + 1 + local packet, packet_len, pos, header + local status, fields, field_count, rows, rs + + packet = bin.pack("ICA", querylen, Command.Query, query ) + + -- + -- http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Result_Set_Header_Packet + -- + -- (Result Set Header Packet) the number of columns + -- (Field Packets) column descriptors + -- (EOF Packet) marker: end of Field Packets + -- (Row Data Packets) row contents + -- (EOF Packet) marker: end of Data Packets + + try( socket:send(packet) ) + + -- + -- Let's read all the data into a table + -- This way we avoid the hustle with reading from the socket + status, rs = decodeQueryResponse( socket ) + + if not status then + return false, rs + end + + status, field_count = decodeResultSetHeader(rs.header) + + if not status then + return false, field_count + end + + status, fields = decodeFieldPackets(rs.fields, field_count) + + if not status then + return false, fields + end + + status, rows = decodeDataPackets(rs.data, fields, field_count) + + if not status then + return false, rows + end + + return true, rows + +end diff --git a/scripts/mysql-brute.nse b/scripts/mysql-brute.nse new file mode 100644 index 000000000..6572b8c08 --- /dev/null +++ b/scripts/mysql-brute.nse @@ -0,0 +1,85 @@ +description = [[ +Performs password guessing against MySQL +]] + +--- +-- @output +-- 3306/tcp open mysql +-- | mysql-brute: +-- | root: => Login Correct +-- |_ test:test => Login Correct + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' +require 'stdnse' +require 'mysql' +require 'unpwdb' + +-- Version 0.3 +-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson +-- Revised 01/23/2010 - v0.2 - revised by Patrik Karlsson, changed username, password loop, added credential storage for other mysql scripts, added timelimit +-- Revised 01/23/2010 - v0.3 - revised by Patrik Karlsson, fixed bug showing account passwords detected twice + +portrule = shortport.port_or_service(3306, "mysql") + +action = function( host, port ) + + local socket = nmap.new_socket() + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local result, response, status, aborted = {}, nil, nil, false + local valid_accounts = {} + local usernames, passwords + local username, password + local max_time = unpwdb.timelimit() ~= nil and unpwdb.timelimit() * 1000 or -1 + local clock_start = nmap.clock_ms() + + -- set a reasonable timeout value + socket:set_timeout(5000) + + usernames = try(unpwdb.usernames()) + passwords = try(unpwdb.passwords()) + + for username in usernames do + for password in passwords do + + if max_time>0 and nmap.clock_ms() - clock_start > max_time then + aborted=true + break + end + + try( socket:connect(host.ip, port.number, "tcp") ) + response = try( mysql.receiveGreeting( socket ) ) + + stdnse.print_debug( string.format("Trying %s/%s ...", username, password ) ) + + status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt ) + socket:close() + + if status then + -- Add credentials for other mysql scripts to use + if nmap.registry.mysqlusers == nil then + nmap.registry.mysqlusers = {} + end + nmap.registry.mysqlusers[username]=password + + table.insert( valid_accounts, string.format("%s:%s => Login Correct", username, password:len()>0 and password or "" ) ) + break + end + + end + passwords("reset") + end + + local output = stdnse.format_output(true, valid_accounts) + + if max_time > 0 and aborted then + output = output .. string.format(" \n\nscript aborted execution after %d seconds", max_time/1000 ) + end + + return output + +end diff --git a/scripts/mysql-databases.nse b/scripts/mysql-databases.nse new file mode 100644 index 000000000..a15cfad94 --- /dev/null +++ b/scripts/mysql-databases.nse @@ -0,0 +1,99 @@ +description = [[ +Attempts to list all databases on the MySQL server +]] + +--- +-- @args mysqluser The username to use for authentication. (If unset it attempts to use credentials found by mysql-brute or mysql-empty-password) +-- @args mysqlpass The password to use for authentication. (If unset it attempts to use credentials found by mysql-brute or mysql-empty-password) +-- +-- @output +-- 3306/tcp open mysql +-- | mysql-databases: +-- | information_schema +-- | mysql +-- | horde +-- | album +-- | mediatomb +-- |_ squeezecenter + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "intrusive"} + +require 'shortport' +require 'stdnse' +require 'mysql' + +dependencies = {"mysql-brute", "mysql-empty-password"} + +-- Version 0.1 +-- Created 01/23/2010 - v0.1 - created by Patrik Karlsson + +portrule = shortport.port_or_service(3306, "mysql") + +action = function( host, port ) + + local socket = nmap.new_socket() + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local result, response, dbs = {}, nil, {} + local users = {} + local nmap_args = nmap.registry.args + local status, rows + + -- set a reasonable timeout value + socket:set_timeout(5000) + + -- first, let's see if the script has any credentials as arguments? + if nmap_args.mysqluser then + users[nmap_args.mysqluser] = nmap_args.mysqlpass or "" + -- 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 + users['root'] = nmap.registry.mysqlusers['root'] + else + -- we didn't have root, so let's make sure we loop over them all + users = nmap.registry.mysqlusers + end + -- last, no dice, we don't have any credentials at all + else + stdnse.print_debug("No credentials supplied, aborting ...") + return + end + + -- + -- Iterates over credentials, breaks once it successfully recieves results + -- + for username, password in pairs(users) do + + try( socket:connect(host.ip, port.number, "tcp") ) + + response = try( mysql.receiveGreeting( socket ) ) + 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" ) + if status then + for i=1, #rows do + -- cheap way of avoiding duplicates + dbs[rows[i]['Database']] = rows[i]['Database'] + end + + -- 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 + break + end + end + 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-empty-password.nse b/scripts/mysql-empty-password.nse new file mode 100644 index 000000000..b1b422bb2 --- /dev/null +++ b/scripts/mysql-empty-password.nse @@ -0,0 +1,54 @@ +description = [[ +Checks for MySQL servers with an empty root and/or anonymous password +]] + +--- +-- @output +-- 3306/tcp open mysql +-- | mysql-empty-password: +-- | anonymous account has empty password +-- |_ root account has empty password + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' +require 'stdnse' +require 'mysql' + +-- Version 0.3 +-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson +-- Revised 01/23/2010 - v0.2 - revised by Patrik Karlsson, added anonymous account check +-- Revised 01/23/2010 - v0.3 - revised by Patrik Karlsson, fixed abort bug due to try of loginrequest + +portrule = shortport.port_or_service(3306, "mysql") + +action = function( host, port ) + + local socket = nmap.new_socket() + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local result, response = {}, nil + local users = {"", "root"} + + -- set a reasonable timeout value + socket:set_timeout(5000) + + for _, v in ipairs( users ) do + try( socket:connect(host.ip, port.number, "tcp") ) + response = try( mysql.receiveGreeting( socket ) ) + status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, v, nil, response.salt ) + if response.errorcode == 0 then + table.insert(result, string.format("%s account has empty password", ( v=="" and "anonymous" or v ) ) ) + if nmap.registry.mysqlusers == nil then + nmap.registry.mysqlusers = {} + end + nmap.registry.mysqlusers[v=="" and "anonymous" or v] = "" + end + socket:close() + end + + return stdnse.format_output(true, result) + +end diff --git a/scripts/mysql-users.nse b/scripts/mysql-users.nse new file mode 100644 index 000000000..e93dd278a --- /dev/null +++ b/scripts/mysql-users.nse @@ -0,0 +1,92 @@ +description = [[ +Attempts to list all users on the MySQL server +]] + +--- +-- @args mysqluser The username to use for authentication. (If unset it attempts to use credentials found by mysql-brute or mysql-empty-password) +-- @args mysqlpass The password to use for authentication. (If unset it attempts to use credentials found by mysql-brute or mysql-empty-password) +-- +-- @output +-- 3306/tcp open mysql +-- | mysql-users: +-- | test +-- | root +-- | test2 +-- | album +-- | debian-sys-maint +-- | horde +-- | mediatomb +-- |_ squeezecenter + + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "intrusive"} + +require 'shortport' +require 'stdnse' +require 'mysql' + +dependencies = {"mysql-brute", "mysql-empty-password"} + +-- Version 0.1 +-- Created 01/23/2010 - v0.1 - created by Patrik Karlsson + +portrule = shortport.port_or_service(3306, "mysql") + +action = function( host, port ) + + local socket = nmap.new_socket() + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local result, response = {}, nil + local users = {} + local nmap_args = nmap.registry.args + local status, rows + + -- set a reasonable timeout value + socket:set_timeout(5000) + + -- first, let's see if the script has any credentials as arguments? + if nmap_args.mysqluser then + users[nmap_args.mysqluser] = nmap_args.mysqlpass or "" + -- 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 + users['root'] = nmap.registry.mysqlusers['root'] + else + -- we didn't have root, so let's make sure we loop over them all + users = nmap.registry.mysqlusers + end + -- last, no dice, we don't have any credentials at all + else + stdnse.print_debug("No credentials supplied, aborting ...") + return + end + + -- + -- Iterates over credentials, breaks once it successfully recieves results + -- + for username, password in pairs(users) do + + try( socket:connect(host.ip, port.number, "tcp") ) + + response = try( mysql.receiveGreeting( socket ) ) + 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" ) + if status then + for i=1, #rows do + table.insert(result, rows[i]['user']) + end + break + end + end + socket:close() + end + + return stdnse.format_output(true, result) + +end diff --git a/scripts/mysql-variables.nse b/scripts/mysql-variables.nse new file mode 100644 index 000000000..33344cc8e --- /dev/null +++ b/scripts/mysql-variables.nse @@ -0,0 +1,100 @@ +description = [[ +Attempts to show all variables on the MySQL server +]] + +--- +-- @args mysqluser The username to use for authentication. (If unset it attempts to use credentials found by mysql-brute or mysql-empty-password) +-- @args mysqlpass The password to use for authentication. (If unset it attempts to use credentials found by mysql-brute or mysql-empty-password) +-- +-- @output +-- 3306/tcp open mysql +-- | mysql-variables: +-- | auto_increment_increment: 1 +-- | auto_increment_offset: 1 +-- | automatic_sp_privileges: ON +-- | back_log: 50 +-- | basedir: /usr/ +-- | binlog_cache_size: 32768 +-- | bulk_insert_buffer_size: 8388608 +-- | character_set_client: latin1 +-- | character_set_connection: latin1 +-- | character_set_database: latin1 +-- | . +-- | . +-- | . +-- | version_comment: (Debian) +-- | version_compile_machine: powerpc +-- | version_compile_os: debian-linux-gnu +-- |_ wait_timeout: 28800 + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "intrusive"} + +require 'shortport' +require 'stdnse' +require 'mysql' + +dependencies = {"mysql-brute", "mysql-empty-password"} + +-- Version 0.1 +-- Created 01/23/2010 - v0.1 - created by Patrik Karlsson + +portrule = shortport.port_or_service(3306, "mysql") + +action = function( host, port ) + + local socket = nmap.new_socket() + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local result, response = {}, nil + local users = {} + local nmap_args = nmap.registry.args + local status, rows + + -- set a reasonable timeout value + socket:set_timeout(5000) + + -- first, let's see if the script has any credentials as arguments? + if nmap_args.mysqluser then + users[nmap_args.mysqluser] = nmap_args.mysqlpass or "" + -- 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 + users['root'] = nmap.registry.mysqlusers['root'] + else + -- we didn't have root, so let's make sure we loop over them all + users = nmap.registry.mysqlusers + end + -- last, no dice, we don't have any credentials at all + else + stdnse.print_debug("No credentials supplied, aborting ...") + return + end + + -- + -- Iterates over credentials, breaks once it successfully recieves results + -- + for username, password in pairs(users) do + + try( socket:connect(host.ip, port.number, "tcp") ) + + response = try( mysql.receiveGreeting( socket ) ) + 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" ) + if status then + for i=1, #rows do + table.insert(result, string.format("%s: %s" , rows[i]['Variable_name'], rows[i]['Value']) ) + end + end + end + + socket:close() + end + + return stdnse.format_output(true, result) + +end