PageRenderTime 26ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/nselib/xmpp.lua

https://gitlab.com/g10h4ck/nmap-gsoc2015
Lua | 455 lines | 336 code | 35 blank | 84 comment | 35 complexity | ddf28ca5f1d47a2d3082d52d22477035 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0, Apache-2.0, LGPL-2.0, LGPL-2.1, MIT
  1. --- A XMPP (Jabber) library, implementing a minimal subset of the protocol
  2. -- enough to do authentication brute-force.
  3. --
  4. -- The XML parsing of tags isn't optimal but there's no other easy way
  5. -- (nulls or line-feeds) to match the end of a message. The parse_tag
  6. -- method in the XML class was borrowed from the initial xmpp.nse
  7. -- script written by Vasiliy Kulikov.
  8. --
  9. -- The library consist of the following classes:
  10. -- * <code>XML</code> - containing a minimal XML parser written by
  11. -- Vasiliy Kulikov.
  12. -- * <code>TagProcessor</code> - Contains processing code for common tags
  13. -- * <code>XMPP</code> - containing the low-level functions used to
  14. -- communicate with the Jabber server.
  15. -- * <code>Helper</code> - containing the main interface for script
  16. -- writers
  17. --
  18. -- The following sample illustrates how to use the library to authenticate
  19. -- to a XMPP sever:
  20. -- <code>
  21. -- local helper = xmpp.Helper:new(host, port, options)
  22. -- local status, err = helper:connect()
  23. -- status, err = helper:login(user, pass, "DIGEST-MD5")
  24. -- </code>
  25. --
  26. -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
  27. -- @author Patrik Karlsson <patrik@cqure.net>
  28. -- Version 0.2
  29. -- Created 07/19/2011 - v0.1 - Created by Patrik Karlsson
  30. -- Revised 07/22/2011 - v0.2 - Added TagProcessors and two new auth mechs:
  31. -- CRAM-MD5 and LOGIN <patrik@cqure.net>
  32. local base64 = require "base64"
  33. local nmap = require "nmap"
  34. local sasl = require "sasl"
  35. local stdnse = require "stdnse"
  36. local string = require "string"
  37. local table = require "table"
  38. _ENV = stdnse.module("xmpp", stdnse.seeall)
  39. -- This is a trivial XML processor written by Vasiliy Kulikov. It doesn't
  40. -- fully support XML, but it should be sufficient for the basic XMPP
  41. -- stream handshake. If you see stanzas with uncommon symbols, feel
  42. -- free to enhance these regexps.
  43. XML = {
  44. ---XML tag table
  45. --@class table
  46. --@name XML.tag
  47. --@field name The tag name
  48. --@field attrs The tag attributes as a key-value table
  49. --@field start True if this was an opening tag.
  50. --@field contents The contents of the tag
  51. --@field finish true if the tag was closed.
  52. ---Parse an XML tag
  53. --@name XML.parse_tag
  54. --@param s String containing the XML tag
  55. --@return XML tag table
  56. --@see XML.tag
  57. parse_tag = function(s)
  58. local _, _, contents, empty, name = string.find(s, "([^<]*)<(/?)([?:%w-]+)")
  59. local attrs = {}
  60. if not name then
  61. return
  62. end
  63. for k, v in string.gmatch(s, "%s([%w:]+)='([^']+)'") do
  64. attrs[k] = v
  65. end
  66. for k, v in string.gmatch(s, "%s([%w:]+)=\"([^\"]+)\"") do
  67. attrs[k] = v
  68. end
  69. local finish = (empty ~= "") or (s:sub(#s-1) == '/>')
  70. return { name = name,
  71. attrs = attrs,
  72. start = (empty == ""),
  73. contents = contents,
  74. finish = finish }
  75. end,
  76. }
  77. TagProcessor = {
  78. ["failure"] = function(socket, tag)
  79. return TagProcessor["success"](socket,tag)
  80. end,
  81. ["success"] = function(socket, tag)
  82. if ( tag.finish ) then return true end
  83. local newtag
  84. repeat
  85. local status, data = socket:receive_buf(">", true)
  86. if ( not(status) ) then
  87. return false, ("ERROR: Failed to process %s tag"):format(tag.name)
  88. end
  89. newtag = XML.parse_tag(data)
  90. until( newtag.finish and newtag.name == tag.name )
  91. if ( newtag.name == tag.name ) then return true, tag end
  92. return false, ("ERROR: Failed to process %s tag"):format(tag.name)
  93. end,
  94. ["challenge"] = function(socket, tag)
  95. local status, data = socket:receive_buf(">", true)
  96. if ( not(status) ) then return false, "ERROR: Failed to read challenge tag" end
  97. local tag = XML.parse_tag(data)
  98. if ( not(status) or tag.name ~= "challenge" ) then
  99. return false, "ERROR: Failed to process challenge"
  100. end
  101. return status, (tag.contents and base64.dec(tag.contents))
  102. end,
  103. }
  104. XMPP = {
  105. --- Creates a new instance of the XMPP class
  106. --
  107. -- @name XMPP.new
  108. -- @param host table as received by the action function
  109. -- @param port table as received by the action function
  110. -- @param options table containing options, currently supported
  111. -- * <code>timeout</code> - sets the socket timeout
  112. -- * <code>servername</code> - sets the server name to use in
  113. -- communication with the server.
  114. -- * <code>starttls</code> - start TLS handshake even if it is optional.
  115. new = function(self, host, port, options)
  116. local o = { host = host,
  117. port = port,
  118. options = options or {},
  119. auth = { mechs = {} } }
  120. o.options.timeout = o.options.timeout and o.options.timeout or 10
  121. o.servername = stdnse.get_hostname(host) or o.options.servername
  122. setmetatable(o, self)
  123. self.__index = self
  124. return o
  125. end,
  126. --- Sends data to XMPP server
  127. -- @name XMPP.send
  128. -- @param data string containing data to send to server
  129. -- @return status true on success false on failure
  130. -- @return err string containing error message
  131. send = function(self, data)
  132. -- this ain't pretty, but we try to "flush" what's left of the receive
  133. -- buffer, prior to send. This way we account for not reading to the
  134. -- end of one message resulting in the next read reading from our
  135. -- previous message.
  136. self.socket:set_timeout(1)
  137. repeat
  138. local status = self.socket:receive_buf("\0", false)
  139. until(not(status))
  140. self.socket:set_timeout(self.options.timeout * 1000)
  141. return self.socket:send(data)
  142. end,
  143. --- Receives a XML tag from the server
  144. --
  145. -- @name XMPP.receive_tag
  146. -- @param tag [optional] if unset, receives the next available tag
  147. -- if set, reads until the given tag has been found
  148. -- @param close [optional] if set, matches a closing tag
  149. -- @return true on success, false on error
  150. -- @return The XML tag table, or error message
  151. -- @see XML.tag
  152. receive_tag = function(self, tag, close)
  153. local result
  154. repeat
  155. local status, data = self.socket:receive_buf(">", true)
  156. if ( not(status) ) then return false, data end
  157. result = XML.parse_tag(data)
  158. until( ( not(tag) and (close == nil or result.finish == close ) ) or
  159. ( tag == result.name and ( close == nil or result.finish == close ) ) )
  160. return true, result
  161. end,
  162. --- Connects to the XMPP server
  163. -- @name XMPP.connect
  164. -- @return status true on success, false on failure
  165. -- @return err string containing an error message if status is false
  166. connect = function(self)
  167. assert(self.servername,
  168. "Cannot connect to XMPP server without valid server name")
  169. -- we may be reconnecting using SSL
  170. if ( not(self.socket) ) then
  171. self.socket = nmap.new_socket()
  172. self.socket:set_timeout(self.options.timeout * 1000)
  173. local status, err = self.socket:connect(self.host, self.port)
  174. if ( not(status) ) then
  175. return false, err
  176. end
  177. end
  178. local data = ("<?xml version='1.0' ?><stream:stream to='%s' xmlns='jabber:client'" ..
  179. " xmlns:stream='http://etherx.jabber.org/streams'" ..
  180. " version='1.0'>"):format(self.servername)
  181. local status, err = self:send(data)
  182. if ( not(status) ) then return false, "ERROR: Failed to connect to server" end
  183. local version, start_tls
  184. repeat
  185. local status, tag = self:receive_tag()
  186. if ( not(status) ) then return false, "ERROR: Failed to connect to server" end
  187. if ( tag.name == "stream:stream" ) then
  188. version = tag.attrs and tag.attrs.version
  189. elseif ( tag.name == "starttls" and tag.start ) then
  190. status, tag = self:receive_tag()
  191. if ( not(status) ) then
  192. return false, "ERROR: Failed to connect to server"
  193. end
  194. if ( tag.name ~= "starttls" ) then
  195. start_tls = tag.name
  196. else
  197. start_tls = "optional"
  198. end
  199. elseif ( tag.name == "mechanism" and tag.finish ) then
  200. self.auth.mechs[tag.contents] = true
  201. end
  202. until(tag.name == "stream:features" and tag.finish)
  203. if ( version ~= "1.0" ) then
  204. return false, "ERROR: Only version 1.0 is supported"
  205. end
  206. if ( start_tls == "required" or self.options.starttls) then
  207. status, err = self:send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
  208. if ( not(status) ) then return false, "ERROR: Failed to initiate STARTTLS" end
  209. local status, tag = self:receive_tag()
  210. if ( not(status) ) then return false, "ERROR: Failed to receive from server" end
  211. if ( tag.name == "proceed" ) then
  212. status, err = self.socket:reconnect_ssl()
  213. self.options.starttls = false
  214. return self:connect()
  215. end
  216. end
  217. return true
  218. end,
  219. --- Logs in to the XMPP server
  220. --
  221. -- @name XMPP.login
  222. -- @param username string
  223. -- @param password string
  224. -- @param mech string containing a supported authentication mechanism
  225. -- @return status true on success, false on failure
  226. -- @return err string containing error message if status is false
  227. login = function(self, username, password, mech)
  228. assert(mech == "PLAIN" or
  229. mech == "DIGEST-MD5" or
  230. mech == "CRAM-MD5" or
  231. mech == "LOGIN",
  232. "Unsupported authentication mechanism")
  233. local auth = ("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " ..
  234. "mechanism='%s'/>"):format(mech)
  235. -- we currently don't do anything with the realm
  236. local realm
  237. -- we need to cut the @domain.tld from the username
  238. if ( username:match("@") ) then
  239. username, realm = username:match("^(.*)@(.*)$")
  240. end
  241. local status, result
  242. if ( mech == "PLAIN" ) then
  243. local mech_params = { username, password }
  244. local auth_data = sasl.Helper:new(mech):encode(table.unpack(mech_params))
  245. auth = ("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " ..
  246. "mechanism='%s'>%s</auth>"):format(mech, base64.enc(auth_data))
  247. status, result = self.socket:send(auth)
  248. if ( not(status) ) then return false, "ERROR: Failed to send SASL PLAIN authentication" end
  249. status, result = self:receive_tag()
  250. if ( not(status) ) then return false, "ERROR: Failed to receive login response" end
  251. if ( result.name == "failure" ) then
  252. status = TagProcessor[result.name](self.socket, result)
  253. end
  254. else
  255. local status, err = self.socket:send(auth)
  256. if(not(status)) then return false, "ERROR: Failed to initiate SASL login" end
  257. local chall
  258. status, result = self:receive_tag()
  259. if ( not(status) ) then return false, "ERROR: Failed to retrieve challenge" end
  260. status, chall = TagProcessor[result.name](self.socket, result)
  261. if ( mech == "LOGIN" ) then
  262. if ( chall ~= "User Name" ) then
  263. return false, ("ERROR: Login expected 'User Name' received: %s"):format(chall)
  264. end
  265. self.socket:send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" ..
  266. base64.enc(username) ..
  267. "</response>")
  268. status, result = self:receive_tag()
  269. if ( not(status) or result.name ~= "challenge") then
  270. return false, "ERROR: Receiving tag from server"
  271. end
  272. status, chall = TagProcessor[result.name](self.socket, result)
  273. if ( chall ~= "Password" ) then
  274. return false, ("ERROR: Login expected 'Password' received: %s"):format(chall)
  275. end
  276. self.socket:send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" ..
  277. base64.enc(password) ..
  278. "</response>")
  279. status, result = self:receive_tag()
  280. if ( not(status) ) then return false, "ERROR: Failed to receive login challenge" end
  281. if ( result.name == "failure" ) then
  282. status = TagProcessor[result.name](self.socket, result)
  283. return false, "Login failed"
  284. end
  285. else
  286. local mech_params = { username, password, chall, "xmpp", "xmpp/" .. self.servername }
  287. local auth_data = sasl.Helper:new(mech):encode(table.unpack(mech_params))
  288. auth_data = "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" ..
  289. base64.enc(auth_data) .. "</response>"
  290. status, err = self.socket:send(auth_data)
  291. -- read to the end tag regardless of what it is
  292. -- it should be one of either: success, challenge or error
  293. repeat
  294. status, result = self:receive_tag()
  295. if ( not(status) ) then return false, "ERROR: Failed to receive login challenge" end
  296. if ( result.name == "failure" ) then
  297. status = TagProcessor[result.name](self.socket, result)
  298. return false, "Login failed"
  299. elseif ( result.name == "success" ) then
  300. status = TagProcessor[result.name](self.socket, result)
  301. if ( not(status) ) then return false, "Failed to process success message" end
  302. return true, "Login success"
  303. elseif ( result.name ~= "challenge" ) then
  304. return false, "ERROR: Failed to receive login challenge"
  305. end
  306. until( result.name == "challenge" and result.finish )
  307. if ( result.name == "challenge" and mech == "DIGEST-MD5" ) then
  308. status, result = self.socket:send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>")
  309. if ( not(status) ) then return false, "ERROR: Failed to send DIGEST-MD5 request" end
  310. status, result = self:receive_tag()
  311. if ( not(status) ) then return false, "ERROR: Failed to receive DIGEST-MD5 response" end
  312. end
  313. end
  314. end
  315. if ( result.name == "success" ) then
  316. return true, "Login success"
  317. end
  318. return false, "Login failed"
  319. end,
  320. --- Retrieves the available authentication mechanisms
  321. -- @name XMPP.getAuthMechs
  322. -- @return table containing all available authentication mechanisms
  323. getAuthMechs = function(self) return self.auth.mechs end,
  324. --- Disconnects the socket from the server
  325. -- @name XMPP.disconnect
  326. -- @return status true on success, false on failure
  327. -- @return error message if status is false
  328. disconnect = function(self)
  329. local status, err = self.socket:close()
  330. self.socket = nil
  331. return status, err
  332. end,
  333. }
  334. Helper = {
  335. --- Creates a new Helper instance
  336. -- @name Helper.new
  337. -- @param host table as received by the action function
  338. -- @param port table as received by the action function
  339. -- @param options table containing options, currently supported
  340. -- * <code>timeout</code> - sets the socket timeout
  341. -- * <code>servername</code> - sets the server name to use in
  342. -- communication with the server.
  343. new = function(self, host, port, options)
  344. local o = { host = host,
  345. port = port,
  346. options = options or {},
  347. xmpp = XMPP:new(host, port, options),
  348. state = "" }
  349. setmetatable(o, self)
  350. self.__index = self
  351. return o
  352. end,
  353. --- Connects to the XMPP server and starts the initial communication
  354. -- @name Helper.connect
  355. -- @return status true on success, false on failure
  356. -- @return err string containing an error message is status is false
  357. connect = function(self)
  358. if ( not(self.host.targetname) and
  359. not(self.options.servername) ) then
  360. return false, "ERROR: Cannot connect to XMPP server without valid server name"
  361. end
  362. self.state = "CONNECTED"
  363. return self.xmpp:connect()
  364. end,
  365. --- Login to the XMPP server
  366. --
  367. -- @name Helper.login
  368. -- @param username string
  369. -- @param password string
  370. -- @param mech string containing a supported authentication mechanism
  371. -- @see Helper.getAuthMechs
  372. -- @return status true on success, false on failure
  373. -- @return err string containing error message if status is false
  374. login = function(self, username, password, mech)
  375. return self.xmpp:login(username, password, mech)
  376. end,
  377. --- Retrieves the available authentication mechanisms
  378. -- @name Helper.getAuthMechs
  379. -- @return table containing all available authentication mechanisms
  380. getAuthMechs = function(self)
  381. if ( self.state == "CONNECTED" ) then
  382. return self.xmpp:getAuthMechs()
  383. end
  384. return
  385. end,
  386. --- Closes the connection to the server
  387. -- @name Helper.close
  388. close = function(self)
  389. self.xmpp:disconnect()
  390. self.state = "DISCONNECTED"
  391. end,
  392. }
  393. return _ENV;