"Fossies" - the Fresh Open Source Software Archive

Member "nmap-7.91/nselib/mysql.lua" (9 Oct 2020, 16423 Bytes) of package /linux/misc/nmap-7.91.tgz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "mysql.lua": 7.90_vs_7.91.

    1 ---
    2 -- Simple MySQL Library supporting a very limited subset of operations.
    3 --
    4 -- https://dev.mysql.com/doc/internals/en/client-server-protocol.html
    5 --
    6 -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
    7 --
    8 -- @author Patrik Karlsson <patrik@cqure.net>
    9 
   10 local nmap = require "nmap"
   11 local stdnse = require "stdnse"
   12 local string = require "string"
   13 local table = require "table"
   14 local math = require "math"
   15 _ENV = stdnse.module("mysql", stdnse.seeall)
   16 
   17 -- Version 0.3
   18 --
   19 -- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
   20 -- Revised 01/23/2010 - v0.2 - added query support, cleanup, documentation
   21 -- Revised 08/24/2010 - v0.3 - added error handling for receiveGreeting
   22 --                             fixed a number of incorrect receives and changed
   23 --                             them to receive_bytes instead.
   24 
   25 local tab = require('tab')
   26 
   27 local HAVE_SSL, openssl = pcall(require,'openssl')
   28 
   29 Capabilities =
   30 {
   31   LongPassword = 0x1,
   32   FoundRows = 0x2,
   33   LongColumnFlag = 0x4,
   34   ConnectWithDatabase = 0x8,
   35   DontAllowDatabaseTableColumn = 0x10,
   36   SupportsCompression = 0x20,
   37   ODBCClient = 0x40,
   38   SupportsLoadDataLocal = 0x80,
   39   IgnoreSpaceBeforeParenthesis = 0x100,
   40   Speaks41ProtocolNew = 0x200,
   41   InteractiveClient = 0x400,
   42   SwitchToSSLAfterHandshake = 0x800,
   43   IgnoreSigpipes = 0x1000,
   44   SupportsTransactions = 0x2000,
   45   Speaks41ProtocolOld = 0x4000,
   46   Support41Auth = 0x8000
   47 }
   48 
   49 ExtCapabilities =
   50 {
   51   SupportsMultipleStatments = 0x1,
   52   SupportsMultipleResults = 0x2,
   53   SupportsAuthPlugins = 0x8,
   54 }
   55 
   56 Charset =
   57 {
   58   latin1_COLLATE_latin1_swedish_ci = 0x8
   59 }
   60 
   61 ServerStatus =
   62 {
   63   InTransaction = 0x1,
   64   AutoCommit = 0x2,
   65   MoreResults = 0x4,
   66   MultiQuery = 0x8,
   67   BadIndexUsed = 0x10,
   68   NoIndexUsed = 0x20,
   69   CursorExists = 0x40,
   70   LastRowSebd = 0x80,
   71   DatabaseDropped = 0x100,
   72   NoBackslashEscapes = 0x200
   73 }
   74 
   75 Command =
   76 {
   77   Query = 3
   78 }
   79 
   80 local MAXPACKET = 16777216
   81 local HEADER_SIZE = 4
   82 
   83 
   84 --- Parses a MySQL header
   85 --
   86 -- @param data string of raw data
   87 -- @return response table containing the fields <code>len</code> and <code>packetno</code>
   88 local function decodeHeader( data, pos )
   89 
   90   local response = {}
   91   local pos, tmp = pos or 1, 0
   92 
   93   tmp, pos = string.unpack( "<I4", data, pos )
   94   response.len = ( tmp & 255 )
   95   response.number = ( tmp >> 24 )
   96 
   97   return pos, response
   98 end
   99 
  100 --- Receives the server greeting upon initial connection
  101 --
  102 -- @param socket already connected to the remote server
  103 -- @return status true on success, false on failure
  104 -- @return response table with the following fields <code>proto</code>, <code>version</code>,
  105 -- <code>threadid</code>, <code>salt</code>, <code>capabilities</code>, <code>charset</code> and
  106 -- <code>status</code> or error message on failure (status == false)
  107 function receiveGreeting( socket )
  108 
  109   local catch = function() socket:close() stdnse.debug1("receiveGreeting(): failed") end
  110   local try = nmap.new_try(catch)
  111   local data = try( socket:receive_bytes(HEADER_SIZE) )
  112   local pos, response, tmp, _
  113 
  114   pos, response = decodeHeader( data, 1 )
  115 
  116   -- do we need to read the remainder
  117   if ( #data - HEADER_SIZE < response.len ) then
  118     local tmp = try( socket:receive_bytes( response.len - #data + HEADER_SIZE ) )
  119     data = data .. tmp
  120   end
  121 
  122   local is_error
  123   is_error, pos = string.unpack("B", data, pos)
  124 
  125   if ( is_error == 0xff ) then
  126     response.errorcode, pos = string.unpack( "<I2", data, pos )
  127     response.errormsg = data:sub(pos)
  128 
  129     return false, response.errormsg
  130   end
  131 
  132   response.proto = is_error
  133   response.version, response.threadid, pos = string.unpack( "<zI4", data, pos )
  134 
  135   if response.proto == 10 then
  136     response.salt, response.capabilities, pos = string.unpack("<c8xI2", data, pos)
  137       local auth_plugin_len
  138     if pos < #data then
  139       response.charset, response.status,
  140       response.extcapabilities, -- capabilities, upper 2 bytes
  141       auth_plugin_len, tmp, pos = string.unpack( "<BI2 I2 Bc10", data, pos )
  142       if tmp ~= "\0\0\0\0\0\0\0\0\0\0" then
  143         stdnse.debug2("reserved bytes are not nulls")
  144       end
  145       if response.capabilities & Capabilities.Support41Auth > 0 then
  146         tmp, pos = string.unpack("c" .. (math.max(13, auth_plugin_len - 8) - 1) .. "x", data, pos)
  147         response.salt = response.salt .. tmp
  148       end
  149       if response.extcapabilities & ExtCapabilities.SupportsAuthPlugins > 0 then
  150         response.auth_plugin_name = string.unpack("z", data, pos)
  151       end
  152     end
  153   elseif response.proto == 9 then
  154     response.auth_plugin_data, pos = string.unpack( "z", data, pos )
  155   else
  156     stdnse.debug2("Unknown MySQL protocol version: %d", response.proto)
  157   end
  158 
  159   response.errorcode = 0
  160 
  161   return true, response
  162 
  163 end
  164 
  165 
  166 --- Creates a hashed value of the password and salt according to MySQL authentication post version 4.1
  167 --
  168 -- @param pass string containing the users password
  169 -- @param salt string containing the servers salt as obtained from <code>receiveGreeting</code>
  170 -- @return reply string containing the raw hashed value
  171 local function createLoginHash(pass, salt)
  172   local hash_stage1
  173   local hash_stage2
  174   local hash_stage3
  175   local reply = {}
  176   local pos, b1, b2, b3, _ = 1, 0, 0, 0
  177 
  178   if ( not(HAVE_SSL) ) then
  179     return nil
  180   end
  181 
  182   hash_stage1 = openssl.sha1( pass )
  183   hash_stage2 = openssl.sha1( hash_stage1 )
  184   hash_stage3 = openssl.sha1( salt .. hash_stage2 )
  185 
  186   for pos=1, hash_stage1:len() do
  187     b1 = string.unpack( "B", hash_stage1, pos )
  188     b2 = string.unpack( "B", hash_stage3, pos )
  189 
  190     reply[pos] = string.char( b2 ~ b1 )
  191   end
  192 
  193   return table.concat(reply)
  194 
  195 end
  196 
  197 
  198 --- Attempts to Login to the remote mysql server
  199 --
  200 -- @param socket already connected to the remote server
  201 -- @param params table with additional options to the loginrequest
  202 --               current supported fields are <code>charset</code> and <code>authversion</code>
  203 --               authversion is either "pre41" or "post41" (default is post41)
  204 --               currently only post41 authentication is supported
  205 -- @param username string containing the username of the user that is authenticating
  206 -- @param password string containing the users password or nil if empty
  207 -- @param salt string containing the servers salt as received from <code>receiveGreeting</code>
  208 -- @return status boolean
  209 -- @return response table or error message on failure
  210 function loginRequest( socket, params, username, password, salt )
  211 
  212   local catch = function() socket:close() stdnse.debug1("loginRequest(): failed") end
  213   local try = nmap.new_try(catch)
  214   local packetno = 1
  215   local authversion = params.authversion or "post41"
  216   local username = username or ""
  217 
  218   if not(HAVE_SSL) then
  219     return false, "No OpenSSL"
  220   end
  221 
  222   if authversion ~= "post41" then
  223     return false, "Unsupported authentication version: " .. authversion
  224   end
  225 
  226   local clicap = Capabilities.LongPassword
  227   clicap = clicap + Capabilities.LongColumnFlag
  228   clicap = clicap + Capabilities.SupportsLoadDataLocal
  229   clicap = clicap + Capabilities.Speaks41ProtocolNew
  230   clicap = clicap + Capabilities.InteractiveClient
  231   clicap = clicap + Capabilities.SupportsTransactions
  232   clicap = clicap + Capabilities.Support41Auth
  233 
  234   local extcapabilities = ExtCapabilities.SupportsMultipleStatments
  235   extcapabilities = extcapabilities + ExtCapabilities.SupportsMultipleResults
  236 
  237   local hash = ""
  238   if ( password ~= nil and password:len() > 0 ) then
  239     hash = createLoginHash( password, salt )
  240   end
  241 
  242   local packet = string.pack( "<I2I2I4B c23 zs1",
  243     clicap,
  244     extcapabilities,
  245     MAXPACKET,
  246     Charset.latin1_COLLATE_latin1_swedish_ci,
  247     string.rep("\0", 23),
  248     username,
  249     hash
  250     )
  251 
  252   local tmp = packet:len() + ( packetno << 24 )
  253 
  254   packet = string.pack( "<I4", tmp ) .. packet
  255 
  256   try( socket:send(packet) )
  257   packet = try( socket:receive_bytes(HEADER_SIZE) )
  258   local pos, response = decodeHeader( packet )
  259 
  260   -- do we need to read the remainder
  261   if ( #packet - HEADER_SIZE < response.len ) then
  262     local tmp = try( socket:receive_bytes( response.len - #packet + HEADER_SIZE ) )
  263     packet = packet .. tmp
  264   end
  265 
  266   local is_error
  267 
  268   is_error, pos = string.unpack( "B", packet, pos )
  269 
  270   if is_error > 0 then
  271     local has_sqlstate
  272     response.errorcode, has_sqlstate, pos = string.unpack( "<I2B", packet, pos )
  273 
  274     if has_sqlstate == 35 then
  275       response.sqlstate, pos = string.unpack( "c5", packet, pos )
  276     end
  277 
  278     response.errormessage, pos = string.unpack( "z", packet, pos )
  279 
  280     return false, response.errormessage
  281   else
  282     response.errorcode = 0
  283     response.affectedrows,
  284     response.serverstatus,
  285     response.warnings, pos = string.unpack( "<BI2I2", packet, pos )
  286   end
  287 
  288   return true, response
  289 
  290 end
  291 
  292 --- Decodes a single column field
  293 --
  294 -- http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Field_Packet
  295 --
  296 -- @param data string containing field packets
  297 -- @param pos number containing position from which to start decoding
  298 --            the position should point to the data in this buffer (ie. after the header)
  299 -- @return pos number containing the position after the field was decoded
  300 -- @return field table containing <code>catalog</code>, <code>database</code>, <code>table</code>,
  301 --         <code>origt_table</code>, <code>name</code>, <code>orig_name</code>,
  302 --         <code>length</code> and <code>type</code>
  303 function decodeField( data, pos )
  304 
  305   local _
  306   local field = {}
  307 
  308   field.catalog,
  309   field.database,
  310   field.table,
  311   field.orig_table,
  312   field.name,
  313   field.orig_name,
  314   _, -- should be 0x0C
  315   _, -- charset, in my case 0x0800
  316   field.length,
  317   field.type, pos = string.unpack( "<s1s1s1s1s1s1BI2I4c6", data, pos )
  318 
  319   return pos, field
  320 
  321 end
  322 
  323 --- Decodes the result set header packet into its sub components
  324 --
  325 -- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Result_Set_Header_Packet
  326 --
  327 -- @param socket socket already connected to MySQL server
  328 -- @return table containing the following <code>header</code>, <code>fields</code> and <code>data</code>
  329 function decodeQueryResponse( socket )
  330 
  331   local catch = function() socket:close() stdnse.debug1("decodeQueryResponse(): failed") end
  332   local try = nmap.new_try(catch)
  333   local data, header, pos
  334   local rs, blocks = {}, {}
  335   local block_start, block_end
  336   local EOF_MARKER = 254
  337 
  338   data = try( socket:receive_bytes(HEADER_SIZE) )
  339   pos, header = decodeHeader( data, pos )
  340 
  341   --
  342   -- First, Let's attempt to read the "Result Set Header Packet"
  343   --
  344   if data:len() < header.len then
  345     data = data .. try( socket:receive_bytes( header.len - #data + HEADER_SIZE ) )
  346   end
  347 
  348   rs.header = data:sub( 1, HEADER_SIZE + header.len )
  349 
  350   -- abort on MySQL error
  351   if rs.header:sub(HEADER_SIZE + 1, HEADER_SIZE + 1) == "\xFF" then
  352     -- is this a 4.0 or 4.1 error message
  353     if rs.header:find("#") then
  354       return false, rs.header:sub(HEADER_SIZE+10)
  355     else
  356       return false, rs.header:sub(HEADER_SIZE+4)
  357     end
  358   end
  359 
  360   pos = HEADER_SIZE + header.len + 1
  361 
  362   -- Second, Let's attempt to read the "Field Packets" and "Row Data Packets"
  363   -- They're separated by an "EOF Packet"
  364   for i=1,2 do
  365 
  366     -- marks the start of our block
  367     block_start = pos
  368 
  369     while true do
  370 
  371       if data:len() - pos < HEADER_SIZE then
  372         data = data .. try( socket:receive_bytes( HEADER_SIZE - ( data:len() - pos ) ) )
  373       end
  374 
  375       pos, header = decodeHeader( data, pos )
  376 
  377       if data:len() - pos < header.len - 1 then
  378         data = data .. try( socket:receive_bytes( header.len - ( data:len() - pos ) ) )
  379       end
  380 
  381       if header.len > 0 then
  382         local b = string.unpack("B", data, pos )
  383 
  384         -- Is this the EOF packet?
  385         if b == EOF_MARKER then
  386           -- we don't want the EOF Packet included
  387           block_end = pos - HEADER_SIZE - 1
  388           pos = pos + header.len
  389           break
  390         end
  391       end
  392 
  393       pos = pos + header.len
  394 
  395     end
  396 
  397     blocks[i] = data:sub( block_start, block_end )
  398 
  399   end
  400 
  401 
  402   rs.fields = blocks[1]
  403   rs.data = blocks[2]
  404 
  405   return true, rs
  406 
  407 end
  408 
  409 --- Decodes as field packet and returns a table of field tables
  410 --
  411 -- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Field_Packet
  412 --
  413 -- @param data string containing field packets
  414 -- @param count number containing the amount of fields to decode
  415 -- @return status boolean (true on success, false on failure)
  416 -- @return fields table containing field tables as returned by <code>decodeField</code>
  417 --         or string containing error message if status is false
  418 function decodeFieldPackets( data, count )
  419 
  420   local pos, header
  421   local field, fields = {}, {}
  422 
  423   if count < 1 then
  424     return false, "Field count was less than one, aborting"
  425   end
  426 
  427   for i=1, count do
  428     pos, header = decodeHeader( data, pos )
  429     pos, field = decodeField( data, pos )
  430     table.insert( fields, field )
  431   end
  432 
  433   return true, fields
  434 end
  435 
  436 -- Decodes the result set header
  437 --
  438 -- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Result_Set_Header_Packet
  439 --
  440 -- @param data string containing the result set header packet
  441 -- @return number containing the amount of fields
  442 function decodeResultSetHeader( data )
  443 
  444   if data:len() ~= HEADER_SIZE + 1 then
  445     return false, "Result set header was incorrect"
  446   end
  447 
  448   local fields = string.unpack( "B", data, HEADER_SIZE + 1 )
  449 
  450   return true, fields
  451 end
  452 
  453 --- Decodes the row data
  454 --
  455 -- ref: http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Row_Data_Packet
  456 --
  457 -- @param data string containing the row data packet
  458 -- @param count number containing the number of fields to decode
  459 -- @return status true on success, false on failure
  460 -- @return rows table containing row tables
  461 function decodeDataPackets( data, count )
  462 
  463   local pos = 1
  464   local rows = {}
  465 
  466   while pos <= data:len() do
  467     local row = {}
  468     local header
  469     pos, header = decodeHeader( data, pos )
  470 
  471     for i=1, count do
  472       row[i], pos = string.unpack("s1", data, pos)
  473     end
  474 
  475     table.insert( rows, row )
  476 
  477   end
  478 
  479   return true, rows
  480 
  481 end
  482 
  483 --- Sends the query to the MySQL server and then attempts to decode the response
  484 --
  485 -- @param socket socket already connected to mysql
  486 -- @param query string containing the sql query
  487 -- @return status true on success, false on failure
  488 -- @return rows table containing row tables as decoded by <code>decodeDataPackets</code>
  489 function sqlQuery( socket, query )
  490 
  491   local catch = function() socket:close() stdnse.debug1("sqlQuery(): failed") end
  492   local try = nmap.new_try(catch)
  493   local packetno = 0
  494   local querylen = query:len() + 1
  495   local packet, packet_len, pos, header
  496   local status, fields, field_count, rows, rs
  497 
  498   packet = string.pack("<I4B", querylen, Command.Query) .. query
  499 
  500   --
  501   -- http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol#Result_Set_Header_Packet
  502   --
  503   -- (Result Set Header Packet)  the number of columns
  504   -- (Field Packets)             column descriptors
  505   -- (EOF Packet)                marker: end of Field Packets
  506   -- (Row Data Packets)          row contents
  507   -- (EOF Packet)                marker: end of Data Packets
  508 
  509   try( socket:send(packet) )
  510 
  511   --
  512   -- Let's read all the data into a table
  513   -- This way we avoid the hustle with reading from the socket
  514   status, rs = decodeQueryResponse( socket )
  515 
  516   if not status then
  517     return false, rs
  518   end
  519 
  520   status, field_count = decodeResultSetHeader(rs.header)
  521 
  522   if not status then
  523     return false, field_count
  524   end
  525 
  526   status, fields = decodeFieldPackets(rs.fields, field_count)
  527 
  528   if not status then
  529     return false, fields
  530   end
  531 
  532   status, rows = decodeDataPackets(rs.data, field_count)
  533 
  534   if not status then
  535     return false, rows
  536   end
  537 
  538   return true, { cols = fields, rows = rows }
  539 end
  540 
  541 ---
  542 -- Formats the resultset returned from <code>sqlQuery</code>
  543 --
  544 -- @param rs table as returned from <code>sqlQuery</code>
  545 -- @param options table containing additional options, currently:
  546 --        - <code>noheaders</code> - does not include column names in result
  547 -- @return string containing the formatted resultset table
  548 function formatResultset(rs, options)
  549   options = options or {}
  550   if ( not(rs) or not(rs.cols) or not(rs.rows) ) then
  551     return
  552   end
  553 
  554   local restab = tab.new(#rs.cols)
  555   local colnames = {}
  556 
  557   if ( not(options.noheaders) ) then
  558     for _, col in ipairs(rs.cols) do table.insert(colnames, col.name) end
  559     tab.addrow(restab, table.unpack(colnames))
  560   end
  561 
  562   for _, row in ipairs(rs.rows) do
  563     tab.addrow(restab, table.unpack(row))
  564   end
  565 
  566   return tab.dump(restab)
  567 end
  568 
  569 return _ENV;