PageRenderTime 45ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/nselib/xmpp.lua

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