PageRenderTime 32ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/nselib/pgsql.lua

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