PageRenderTime 58ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/nselib/pgsql.lua

https://github.com/prakashgamit/nmap
Lua | 628 lines | 348 code | 104 blank | 176 comment | 66 complexity | 4df630c8b7139f13e017f6c7a8e5f0b4 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0, LGPL-2.0, LGPL-2.1
  1. ---
  2. -- PostgreSQL library supporting both version 2 and version 3 of the protocol.
  3. -- The library currently contains the bare minimum to perform authentication.
  4. -- Authentication is supported with or without SSL enabled and using the
  5. -- plain-text or MD5 authentication mechanisms.
  6. --
  7. -- The PGSQL protocol is explained in detail in the following references.
  8. -- * http://developer.postgresql.org/pgdocs/postgres/protocol.html
  9. -- * http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html
  10. -- * http://developer.postgresql.org/pgdocs/postgres/protocol-message-formats.html
  11. --
  12. -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
  13. -- @author "Patrik Karlsson <patrik@cqure.net>"
  14. local bin = require "bin"
  15. local nmap = require "nmap"
  16. local stdnse = require "stdnse"
  17. local openssl = stdnse.silent_require "openssl"
  18. local string = require "string"
  19. local table = require "table"
  20. _ENV = stdnse.module("pgsql", stdnse.seeall)
  21. -- Version 0.3
  22. -- Created 02/05/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
  23. -- Revised 02/20/2010 - v0.2 - added detectVersion to automaticaly detect and return
  24. -- the correct version class
  25. -- Revised 03/04/2010 - v0.3 - added support for trust authentication method
  26. --- Supported pgsql message types
  27. MessageType = {
  28. Error = 0x45,
  29. BackendKeyData = 0x4b,
  30. AuthRequest=0x52,
  31. ParameterStatus = 0x53,
  32. ReadyForQuery = 0x5a,
  33. PasswordMessage = 0x70,
  34. }
  35. --- Supported authentication types
  36. AuthenticationType = {
  37. Success = 0x00,
  38. Plain = 0x03,
  39. MD5 = 0x05
  40. }
  41. -- Version 2 of the protocol
  42. v2 =
  43. {
  44. --- Pad a string with zeroes
  45. --
  46. -- @param str string containing the string to be padded
  47. -- @param len number containing the wanted length
  48. -- @return string containing the padded string value
  49. zeroPad = function(str, len)
  50. local padding = len - str:len()
  51. if ( padding < 0 ) then
  52. return str
  53. end
  54. for i=1,padding do
  55. str = str .. string.char(0x00)
  56. end
  57. return str
  58. end,
  59. messageDecoder = {
  60. --- Decodes an Auth Request packet
  61. --
  62. -- @param data string containing raw data recieved from socket
  63. -- @param len number containing the length as retrieved from the header
  64. -- @param pos number containing the offset into the data buffer
  65. -- @return pos number containing the offset after decoding, -1 on error
  66. -- @return response table containing zero or more of the following <code>salt</code> and <code>success</code>
  67. -- error string containing error message if pos is -1
  68. [MessageType.AuthRequest] = function( data, len, pos )
  69. local _, authtype
  70. local response = {}
  71. pos, authtype = bin.unpack(">I", data, pos)
  72. if ( authtype == AuthenticationType.MD5 ) then
  73. if ( len - pos + 1 ) < 3 then
  74. return -1, "ERROR: Malformed AuthRequest received"
  75. end
  76. pos, response.salt = bin.unpack("A4", data, pos)
  77. elseif ( authtype == AuthenticationType.Plain ) then
  78. --do nothing
  79. elseif ( authtype == 0 ) then
  80. response.success = true
  81. else
  82. stdnse.print_debug( ("unknown auth type: %d"):format(authtype) )
  83. end
  84. response.authtype = authtype
  85. return pos, response
  86. end,
  87. --- Decodes an Error packet
  88. --
  89. -- @param data string containing raw data recieved from socket
  90. -- @param len number containing the length as retrieved from the header
  91. -- @param pos number containing the offset into the data buffer
  92. -- @return pos number containing the offset after decoding
  93. -- @return response table containing zero or more of the following <code>error.severity</code>,
  94. -- <code>error.code</code>, <code>error.message</code>, <code>error.file</code>,
  95. -- <code>error.line</code> and <code>error.routine</code>
  96. [MessageType.Error] = function( data, len, pos )
  97. local tmp = data:sub(pos, pos + len - 4)
  98. local response = {}
  99. local pos_end = pos + len
  100. response.error = {}
  101. pos, response.error.message = bin.unpack("z", data, pos)
  102. return pos, response
  103. end,
  104. },
  105. --- Process the server response
  106. --
  107. -- @param data string containing the server response
  108. -- @param pos number containing the offset into the data buffer
  109. processResponse = function(data, pos)
  110. local ptype, len, status, response
  111. local pos = pos or 1
  112. pos, ptype = bin.unpack("C", data, pos)
  113. len = data:len() - 1
  114. if v2.messageDecoder[ptype] then
  115. pos, response = v2.messageDecoder[ptype](data, len, pos)
  116. if pos ~= -1 then
  117. response.type = ptype
  118. return pos, response
  119. end
  120. else
  121. stdnse.print_debug( ("Missing decoder for %d"):format(ptype) )
  122. return -1, ("Missing decoder for %d"):format(ptype)
  123. end
  124. return -1, "Decoding failed"
  125. end,
  126. --- Reads a packet and handles additional socket reads to retrieve remaining data
  127. --
  128. -- @param socket socket already connected to the pgsql server
  129. -- @param data string containing any data already retrieved from the socket
  130. -- @param pos number containing the offset into the data buffer
  131. -- @return data string containing the initial and any additional data
  132. readPacket=function(socket, data, pos)
  133. local pos = pos or 1
  134. local data = data or ""
  135. local status = true
  136. local tmp = ""
  137. local ptype, len
  138. local catch = function() socket:close() stdnse.print_debug("processResponse(): failed") end
  139. local try = nmap.new_try(catch)
  140. if ( data == nil or data:len() == 0 ) then
  141. data = try(socket:receive())
  142. end
  143. return data
  144. end,
  145. --- Sends a startup message to the server containing the username and database to connect to
  146. --
  147. -- @param socket socket already connected to the pgsql server
  148. -- @param user string containing the name of the user
  149. -- @param database string containing the name of the database
  150. -- @return status true on success, false on failure
  151. -- @return table containing a processed response from <code>processResponse</code>
  152. -- string containing error message if status is false
  153. sendStartup=function(socket, user, database)
  154. local data, response, status, pos
  155. local proto_ver, ptype, _, tmp
  156. local tty, unused, args = "", "", ""
  157. proto_ver = 0x0020000
  158. user = v2.zeroPad(user, 32)
  159. database = v2.zeroPad(database, 64)
  160. data = bin.pack(">I>IAAAAA", 296, proto_ver, database, user, v2.zeroPad(args, 64), v2.zeroPad(unused, 64), v2.zeroPad(tty,64) )
  161. socket:send( data )
  162. -- attempt to verify version
  163. status, data = socket:receive_bytes( 1 )
  164. if ( not(status) ) then
  165. return false, "sendStartup failed"
  166. end
  167. data = v2.readPacket(socket, data )
  168. pos, response = v2.processResponse( data )
  169. if ( pos < 0 or response.type == MessageType.Error) then
  170. return false, response.error.message or "unknown error"
  171. end
  172. return true, response
  173. end,
  174. --- Attempts to authenticate to the pgsql server
  175. -- Supports plain-text and MD5 authentication
  176. --
  177. -- @param socket socket already connected to the pgsql server
  178. -- @param params table containing any additional parameters <code>authtype</code>, <code>version</code>
  179. -- @param username string containing the username to use for authentication
  180. -- @param password string containing the password to use for authentication
  181. -- @param salt string containing the crypthographic salt value
  182. -- @return status true on success, false on failure
  183. -- @return result table containing parameter status information,
  184. -- result string containing an error message if login fails
  185. loginRequest = function ( socket, params, username, password, salt )
  186. local catch = function() socket:close() stdnse.print_debug("loginRequest(): failed") end
  187. local try = nmap.new_try(catch)
  188. local response = {}
  189. local status, data, len, pos, tmp
  190. if ( params.authtype == AuthenticationType.MD5 ) then
  191. local hash = createMD5LoginHash(username,password,salt)
  192. data = bin.pack( ">Iz", 40, hash)
  193. try( socket:send( data ) )
  194. elseif ( params.authtype == AuthenticationType.Plain ) then
  195. local data
  196. data = bin.pack(">Iz", password:len() + 4, password)
  197. try( socket:send( data ) )
  198. elseif ( params.authtype == AuthenticationType.Success ) then
  199. return true, nil
  200. end
  201. data, response.params = "", {}
  202. data = v2.readPacket(socket, data, 1)
  203. pos, tmp = v2.processResponse(data, 1)
  204. -- this should contain the AuthRequest packet
  205. if tmp.type ~= MessageType.AuthRequest then
  206. return false, "Expected AuthRequest got something else"
  207. end
  208. if not tmp.success then
  209. return false, "Login failure"
  210. end
  211. return true, response
  212. end,
  213. }
  214. -- Version 3 of the protocol
  215. v3 =
  216. {
  217. messageDecoder = {
  218. --- Decodes an Auth Request packet
  219. --
  220. -- @param data string containing raw data recieved from socket
  221. -- @param len number containing the length as retrieved from the header
  222. -- @param pos number containing the offset into the data buffer
  223. -- @return pos number containing the offset after decoding, -1 on error
  224. -- @return response table containing zero or more of the following <code>salt</code> and <code>success</code>
  225. -- error string containing error message if pos is -1
  226. [MessageType.AuthRequest] = function( data, len, pos )
  227. local _, authtype
  228. local response = {}
  229. pos, authtype = bin.unpack(">I", data, pos)
  230. if ( authtype == AuthenticationType.MD5 ) then
  231. if ( len - pos + 1 ) < 3 then
  232. return -1, "ERROR: Malformed AuthRequest received"
  233. end
  234. pos, response.salt = bin.unpack("A4", data, pos)
  235. elseif ( authtype == AuthenticationType.Plain ) then
  236. --do nothing
  237. elseif ( authtype == 0 ) then
  238. response.success = true
  239. else
  240. stdnse.print_debug( "unknown auth type: %d", authtype )
  241. end
  242. response.authtype = authtype
  243. return pos, response
  244. end,
  245. --- Decodes an ParameterStatus packet
  246. --
  247. -- @param data string containing raw data recieved from socket
  248. -- @param len number containing the length as retrieved from the header
  249. -- @param pos number containing the offset into the data buffer
  250. -- @return pos number containing the offset after decoding
  251. -- @return response table containing zero or more of the following <code>key</code> and <code>value</code>
  252. [MessageType.ParameterStatus] = function( data, len, pos )
  253. local tmp, _
  254. local response = {}
  255. tmp = data:sub(pos, pos + len - 4)
  256. _, response.key, response.value = bin.unpack("zz", tmp)
  257. return pos + len - 4, response
  258. end,
  259. --- Decodes an Error packet
  260. --
  261. -- @param data string containing raw data recieved from socket
  262. -- @param len number containing the length as retrieved from the header
  263. -- @param pos number containing the offset into the data buffer
  264. -- @return pos number containing the offset after decoding
  265. -- @return response table containing zero or more of the following <code>error.severity</code>,
  266. -- <code>error.code</code>, <code>error.message</code>, <code>error.file</code>,
  267. -- <code>error.line</code> and <code>error.routine</code>
  268. [MessageType.Error] = function( data, len, pos )
  269. local tmp = data:sub(pos, pos + len - 4)
  270. local _, value, prefix
  271. local response = {}
  272. local pos_end = pos + len
  273. response.error = {}
  274. while ( pos < pos_end - 5 ) do
  275. pos, prefix, value = bin.unpack("Az", data, pos)
  276. if prefix == 'S' then
  277. response.error.severity = value
  278. elseif prefix == 'C' then
  279. response.error.code = value
  280. elseif prefix == 'M' then
  281. response.error.message = value
  282. elseif prefix == 'F' then
  283. response.error.file = value
  284. elseif prefix == 'L' then
  285. response.error.line = value
  286. elseif prefix == 'R' then
  287. response.error.routine = value
  288. end
  289. end
  290. return pos, response
  291. end,
  292. --- Decodes the BackendKeyData packet
  293. --
  294. -- @param data string containing raw data recieved from socket
  295. -- @param len number containing the length as retrieved from the header
  296. -- @param pos number containing the offset into the data buffer
  297. -- @return pos number containing the offset after decoding, -1 on error
  298. -- @return response table containing zero or more of the following <code>pid</code> and <code>key</code>
  299. -- error string containing error message if pos is -1
  300. [MessageType.BackendKeyData] = function( data, len, pos )
  301. local response = {}
  302. if len ~= 12 then
  303. return -1, "ERROR: Invalid BackendKeyData packet"
  304. end
  305. pos, response.pid, response.key = bin.unpack(">I>I", data, pos)
  306. return pos, response
  307. end,
  308. --- Decodes an ReadyForQuery packet
  309. --
  310. -- @param data string containing raw data recieved from socket
  311. -- @param len number containing the length as retrieved from the header
  312. -- @param pos number containing the offset into the data buffer
  313. -- @return pos number containing the offset after decoding, -1 on error
  314. -- @return response table containing zero or more of the following <code>status</code>
  315. -- error string containing error message if pos is -1
  316. [MessageType.ReadyForQuery] = function( data, len, pos )
  317. local response = {}
  318. if len ~= 5 then
  319. return -1, "ERROR: Invalid ReadyForQuery packet"
  320. end
  321. pos, response.status = bin.unpack("C", data, pos )
  322. return pos, response
  323. end,
  324. },
  325. --- Reads a packet and handles additional socket reads to retrieve remaining data
  326. --
  327. -- @param socket socket already connected to the pgsql server
  328. -- @param data string containing any data already retrieved from the socket
  329. -- @param pos number containing the offset into the data buffer
  330. -- @return data string containing the initial and any additional data
  331. readPacket = function(socket, data, pos)
  332. local pos = pos or 1
  333. local data = data or ""
  334. local status = true
  335. local tmp = ""
  336. local ptype, len
  337. local header
  338. local catch = function() socket:close() stdnse.print_debug("processResponse(): failed") end
  339. local try = nmap.new_try(catch)
  340. if ( data:len() - pos < 5 ) then
  341. status, tmp = socket:receive_bytes( 5 - ( data:len() - pos ) )
  342. end
  343. if not status then
  344. return nil, "Failed to read packet"
  345. end
  346. if tmp:len() ~= 0 then
  347. data = data .. tmp
  348. end
  349. pos, header = v3.decodeHeader(data,pos)
  350. while data:len() < header.len do
  351. data = data .. try(socket:receive_bytes( ( header.len + 1 ) - data:len() ))
  352. end
  353. return data
  354. end,
  355. --- Decodes the postgres header
  356. --
  357. -- @param data string containing the server response
  358. -- @param pos number containing the offset into the data buffer
  359. -- @return pos number containing the offset after decoding
  360. -- @return header table containing <code>type</code> and <code>len</code>
  361. decodeHeader = function(data, pos)
  362. local ptype, len
  363. pos, ptype, len = bin.unpack("C>I", data, pos)
  364. return pos, { ['type'] = ptype, ['len'] = len }
  365. end,
  366. --- Process the server response
  367. --
  368. -- @param data string containing the server response
  369. -- @param pos number containing the offset into the data buffer
  370. -- @return pos number containing offset after decoding
  371. -- @return response string containing decoded data
  372. -- error message if pos is -1
  373. processResponse = function(data, pos)
  374. local ptype, len, status, response
  375. local pos = pos or 1
  376. local header
  377. pos, header = v3.decodeHeader( data, pos )
  378. if v3.messageDecoder[header.type] then
  379. pos, response = v3.messageDecoder[header.type](data, header.len, pos)
  380. if pos ~= -1 then
  381. response.type = header.type
  382. return pos, response
  383. end
  384. else
  385. stdnse.print_debug( "Missing decoder for %d", header.type )
  386. return -1, ("Missing decoder for %d"):format(header.type)
  387. end
  388. return -1, "Decoding failed"
  389. end,
  390. --- Attempts to authenticate to the pgsql server
  391. -- Supports plain-text and MD5 authentication
  392. --
  393. -- @param socket socket already connected to the pgsql server
  394. -- @param params table containing any additional parameters <code>authtype</code>, <code>version</code>
  395. -- @param username string containing the username to use for authentication
  396. -- @param password string containing the password to use for authentication
  397. -- @param salt string containing the crypthographic salt value
  398. -- @return status true on success, false on failure
  399. -- @return result table containing parameter status information,
  400. -- result string containing an error message if login fails
  401. loginRequest = function ( socket, params, username, password, salt )
  402. local catch = function() socket:close() stdnse.print_debug("loginRequest(): failed") end
  403. local try = nmap.new_try(catch)
  404. local response, header = {}, {}
  405. local status, data, len, tmp, _
  406. local pos = 1
  407. if ( params.authtype == AuthenticationType.MD5 ) then
  408. local hash = createMD5LoginHash(username, password, salt)
  409. data = bin.pack( "C>Iz", MessageType.PasswordMessage, 40, hash )
  410. try( socket:send( data ) )
  411. elseif ( params.authtype == AuthenticationType.Plain ) then
  412. local data
  413. data = bin.pack("C>Iz", MessageType.PasswordMessage, password:len() + 4, password)
  414. try( socket:send( data ) )
  415. elseif ( params.authtype == AuthenticationType.Success ) then
  416. return true, nil
  417. end
  418. data, response.params = "", {}
  419. data = v3.readPacket(socket, data, 1)
  420. pos, tmp = v3.processResponse(data, 1)
  421. -- this should contain the AuthRequest packet
  422. if tmp.type ~= MessageType.AuthRequest then
  423. return false, "Expected AuthRequest got something else"
  424. end
  425. if not tmp.success then
  426. return false, "Login failure"
  427. end
  428. repeat
  429. data = v3.readPacket(socket, data, pos)
  430. pos, tmp = v3.processResponse(data, pos)
  431. if ( tmp.type == MessageType.ParameterStatus ) then
  432. table.insert(response.params, {name=tmp.key, value=tmp.value})
  433. end
  434. until pos >= data:len() or pos == -1
  435. return true, response
  436. end,
  437. --- Sends a startup message to the server containing the username and database to connect to
  438. --
  439. -- @param socket socket already connected to the pgsql server
  440. -- @param user string containing the name of the user
  441. -- @param database string containing the name of the database
  442. -- @return status true on success, false on failure
  443. -- @return table containing a processed response from <code>processResponse</code>
  444. -- string containing error message if status is false
  445. sendStartup = function(socket, user, database )
  446. local data, response, status, pos
  447. local proto_ver, ptype, _, tmp
  448. proto_ver = 0x0030000
  449. data = bin.pack(">IzzzzH", proto_ver, "user", user, "database", database, 0)
  450. data = bin.pack(">I", data:len() + 4) .. data
  451. socket:send( data )
  452. -- attempt to verify version
  453. status, data = socket:receive_bytes( 2 )
  454. if ( not(status) ) then
  455. return false, "sendStartup failed"
  456. end
  457. if ( not(status) or data:match("^EF") ) then
  458. return false, "Incorrect version"
  459. end
  460. data = v3.readPacket(socket, data )
  461. pos, response = v3.processResponse( data )
  462. if ( pos < 0 or response.type == MessageType.Error) then
  463. return false, response.error.message or "unknown error"
  464. end
  465. return true, response
  466. end
  467. }
  468. --- Sends a packet requesting SSL communication to be activated
  469. --
  470. -- @param socket socket already connected to the pgsql server
  471. -- @return boolean true if request was accepted, false if request was denied
  472. function requestSSL(socket)
  473. -- SSLRequest
  474. local ssl_req_code = 80877103
  475. local data = bin.pack( ">I>I", 8, ssl_req_code)
  476. local status, response
  477. socket:send(data)
  478. status, response = socket:receive_bytes(1)
  479. if ( not(status) ) then
  480. return false
  481. end
  482. if ( response == 'S' ) then
  483. return true
  484. end
  485. return false
  486. end
  487. --- Creates a cryptographic hash to be used for login
  488. --
  489. -- @param username username
  490. -- @param password password
  491. -- @param salt salt
  492. -- @return string suitable for login request
  493. function createMD5LoginHash(username, password, salt)
  494. local md5_1 = select( 2, bin.unpack( "H16", openssl.md5(password..username) ) ):lower()
  495. return "md5" .. select( 2, bin.unpack("H16", openssl.md5( md5_1 .. salt ) ) ):lower()
  496. end
  497. --- Prints the contents of the error table returned from the Error message decoder
  498. --
  499. -- @param dberror table containing the error
  500. function printErrorMessage( dberror )
  501. if not dberror then
  502. return
  503. end
  504. for k, v in pairs(dberror) do
  505. stdnse.print_debug( ("%s=%s"):format(k, v) )
  506. end
  507. end
  508. --- Attempts to determine if the server supports v3 or v2 of the protocol
  509. --
  510. -- @param host table
  511. -- @param port table
  512. -- @return class v2 or v3
  513. function detectVersion(host, port)
  514. local status, response
  515. local socket = nmap.new_socket()
  516. socket:connect(host, port)
  517. status, response = v3.sendStartup(socket, "versionprobe", "versionprobe")
  518. socket:close()
  519. if ( not(status) and response == 'Incorrect version' ) then
  520. return v2
  521. end
  522. return v3
  523. end
  524. return _ENV;