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

/nselib/sasl.lua

https://github.com/prakashgamit/nmap
Lua | 471 lines | 251 code | 55 blank | 165 comment | 22 complexity | 33b43d53dbd2150189f834bedc35bc03 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0, LGPL-2.0, LGPL-2.1
  1. ---
  2. -- Simple Authentication and Security Layer (SASL).
  3. --
  4. -- The library contains some low level functions and a high level class.
  5. --
  6. -- The <code>DigestMD5</code> class contains all code necessary to calculate
  7. -- a DIGEST-MD5 response based on the servers challenge and the other
  8. -- necessary arguments (@see DigestMD5.new).
  9. -- It can be called throught the SASL helper or directly like this:
  10. -- <code>
  11. -- local dmd5 = DigestMD5:new(chall, user, pass, "AUTHENTICATE", nil, "imap")
  12. -- local digest = dmd5:calcDigest()
  13. -- </code>
  14. --
  15. -- The <code>NTLM</code> class contains all code necessary to calculate a
  16. -- NTLM response based on the servers challenge and the other necessary
  17. -- arguments (@see NTLM.new). It can be called through the SASL helper or
  18. -- directly like this:
  19. -- <code>
  20. -- local ntlm = NTLM:new(chall, user, pass)
  21. -- local response = ntlm:calcResponse()
  22. -- </code>
  23. --
  24. -- The Helper class contains the high level methodes:
  25. -- * <code>new</code>: This is the SASL object constructor.
  26. -- * <code>set_mechanism</code>: Sets the authentication mechanism to use.
  27. -- * <code>set_callback</code>: Sets the encoding function to use.
  28. -- * <code>encode</code>: Encodes the parameters according to the
  29. -- authentication mechanism.
  30. -- * <code>reset_callback</code>: Resets the authentication function.
  31. -- * <code>reset</code>: Resets the SASL object.
  32. --
  33. -- The script writers should use the Helper class to create SASL objects,
  34. -- and they can also use the low level functions to customize their
  35. -- encoding functions.
  36. --
  37. -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
  38. -- Version 0.2
  39. -- Created 07/17/2011 - v0.1 - Created by Djalal Harouini
  40. -- Revised 07/18/2011 - v0.2 - Added NTLM, DIGEST-MD5 classes
  41. local bin = require "bin"
  42. local bit = require "bit"
  43. local smbauth = require "smbauth"
  44. local stdnse = require "stdnse"
  45. local string = require "string"
  46. _ENV = stdnse.module("sasl", stdnse.seeall)
  47. local HAVE_SSL, openssl = pcall(require, 'openssl')
  48. if ( not(HAVE_SSL) ) then
  49. stdnse.print_debug(1,
  50. "sasl.lua: OpenSSL not present, SASL support limited.")
  51. end
  52. local MECHANISMS = { }
  53. if HAVE_SSL then
  54. -- Calculates a DIGEST MD5 response
  55. DigestMD5 = {
  56. --- Instantiates DigestMD5
  57. --
  58. -- @param chall string containing the base64 decoded challenge
  59. -- @return a new instance of DigestMD5
  60. new = function(self, chall, username, password, method, uri, service, realm)
  61. local o = { nc = 0,
  62. chall = chall,
  63. challnvs = {},
  64. username = username,
  65. password = password,
  66. method = method,
  67. uri = uri,
  68. service = service,
  69. realm = realm }
  70. setmetatable(o, self)
  71. self.__index = self
  72. o:parseChallenge()
  73. return o
  74. end,
  75. -- parses a challenge received from the server
  76. -- takes care of both quoted and unqoted identifiers
  77. -- regardless of what RFC says
  78. parseChallenge = function(self)
  79. local results = {}
  80. local start, stop = 0,0
  81. if self.chall then
  82. while(true) do
  83. local name, value
  84. start, stop, name = self.chall:find("([^=]*)=%s*", stop + 1)
  85. if ( not(start) ) then break end
  86. if ( self.chall:sub(stop + 1, stop + 1) == "\"" ) then
  87. start, stop, value = self.chall:find("(.-)\"", stop + 2)
  88. else
  89. start, stop, value = self.chall:find("([^,]*)", stop + 1)
  90. end
  91. name = name:lower()
  92. if name == "digest realm" then name="realm" end
  93. self.challnvs[name] = value
  94. start, stop = self.chall:find("%s*,%s*", stop + 1)
  95. if ( not(start) ) then break end
  96. end
  97. end
  98. end,
  99. --- Calculates the digest
  100. calcDigest = function( self )
  101. local uri = self.uri or ("%s/%s"):format(self.service, "localhost")
  102. local realm = self.realm or self.challnvs.realm or ""
  103. local cnonce = stdnse.tohex(openssl.rand_bytes( 8 ))
  104. local qop = "auth"
  105. local qop_not_specified
  106. if self.challnvs.qop then
  107. qop_not_specified = false
  108. else
  109. qop_not_specified = true
  110. end
  111. self.nc = self.nc + 1
  112. local A1_part1 = openssl.md5(self.username .. ":" .. (self.challnvs.realm or "") .. ":" .. self.password)
  113. local A1 = stdnse.tohex(openssl.md5(A1_part1 .. ":" .. self.challnvs.nonce .. ':' .. cnonce))
  114. local A2 = stdnse.tohex(openssl.md5(("%s:%s"):format(self.method, uri)))
  115. local digest = stdnse.tohex(openssl.md5(A1 .. ":" .. self.challnvs.nonce .. ":" ..
  116. ("%08d"):format(self.nc) .. ":" .. cnonce .. ":" ..
  117. qop .. ":" .. A2))
  118. local b1
  119. if not self.challnvs.algorithm or self.challnvs.algorithm == "MD5" then
  120. b1 = stdnse.tohex(openssl.md5(self.username..":"..(self.challnvs.realm or "")..":"..self.password))
  121. else
  122. b1 = A1
  123. end
  124. -- should we make it work when qop == "auth-int" (we would need entity-body here, which
  125. -- might be complicated)?
  126. local digest_http
  127. if not qop_not_specified then
  128. digest_http = stdnse.tohex(openssl.md5(b1 .. ":" .. self.challnvs.nonce .. ":" ..
  129. ("%08d"):format(self.nc) .. ":" .. cnonce .. ":" .. qop .. ":" .. A2))
  130. else
  131. digest_http = stdnse.tohex(openssl.md5(b1 .. ":" .. self.challnvs.nonce .. ":" .. A2))
  132. end
  133. local response = "username=\"" .. self.username .. "\""
  134. response = response .. (",%s=\"%s\""):format("realm", realm)
  135. response = response .. (",%s=\"%s\""):format("nonce", self.challnvs.nonce)
  136. response = response .. (",%s=\"%s\""):format("cnonce", cnonce)
  137. response = response .. (",%s=%08d"):format("nc", self.nc)
  138. response = response .. (",%s=%s"):format("qop", "auth")
  139. response = response .. (",%s=\"%s\""):format("digest-uri", uri)
  140. response = response .. (",%s=%s"):format("response", digest)
  141. response = response .. (",%s=%s"):format("charset", "utf-8")
  142. -- response_table is used in http library because the request should
  143. -- be a little bit different then the string generated above
  144. local response_table = {
  145. username = self.username,
  146. realm = realm,
  147. nonce = self.challnvs.nonce,
  148. cnonce = cnonce,
  149. nc = ("%08d"):format(self.nc),
  150. qop = qop,
  151. ["digest-uri"] = uri,
  152. algorithm = self.challnvs.algorithm,
  153. response = digest_http
  154. }
  155. return response, response_table
  156. end,
  157. }
  158. -- The NTLM class handling NTLM challenge response authentication
  159. NTLM = {
  160. --- Creates a new instance of the NTLM class
  161. --
  162. -- @param chall string containing the challenge received from the server
  163. -- @param username string containing the username
  164. -- @param password string containing the password
  165. -- @return new instance of NTML
  166. new = function(self, chall, username, password)
  167. local o = { nc = 0,
  168. chall = chall,
  169. username = username,
  170. password = password}
  171. setmetatable(o, self)
  172. self.__index = self
  173. o:parseChallenge()
  174. return o
  175. end,
  176. --- Converst str to "unicode" (adds null bytes for every other byte)
  177. -- @param str containing string to convert
  178. -- @return unicode string containing the unicoded str
  179. to_unicode = function(str)
  180. local unicode = ""
  181. for i = 1, #str, 1 do
  182. unicode = unicode .. bin.pack("<S", string.byte(str, i))
  183. end
  184. return unicode
  185. end,
  186. --- Parses the NTLM challenge as received from the server
  187. parseChallenge = function(self)
  188. local NTLM_NegotiateUnicode = 0x00000001
  189. local NTLM_NegotiateExtendedSecurity = 0x00080000
  190. local pos, _, message_type
  191. pos, _, message_type, _, _,
  192. _, self.flags, self.chall, _,
  193. _, _, _ = bin.unpack("<A8ISSIIA8LSSI", self.chall)
  194. if ( message_type ~= 0x02 ) then
  195. error("NTLM parseChallenge expected message type: 0x02")
  196. end
  197. self.is_extended = ( bit.band(self.flags, NTLM_NegotiateExtendedSecurity) == NTLM_NegotiateExtendedSecurity )
  198. local is_unicode = ( bit.band(self.flags, NTLM_NegotiateUnicode) == NTLM_NegotiateUnicode )
  199. self.workstation = "NMAP-HOST"
  200. self.domain = self.username:match("^(.-)\\(.*)$") or "DOMAIN"
  201. if ( is_unicode ) then
  202. self.workstation = self.to_unicode(self.workstation)
  203. self.username = self.to_unicode(self.username)
  204. self.domain = self.to_unicode(self.domain)
  205. end
  206. end,
  207. --- Calculates the response
  208. calcResponse = function(self)
  209. local ntlm, lm = smbauth.get_password_response(nil, self.username, self.domain, self.password, nil, "v1", self.chall, self.is_extended)
  210. local msg_type = 3
  211. local response
  212. local BASE_OFFSET = 72
  213. local offset
  214. local encrypted_random_sesskey = ""
  215. local flags = 0xa2888205 -- (NTLM_NegotiateUnicode | \
  216. -- NTLM_RequestTarget | \
  217. -- NTLM_NegotiateNTLM | \
  218. -- NTLM_NegotiateAlwaysSign | \
  219. -- NTLM_NegotiateExtendedSecurity | \
  220. -- NTLM_NegotiateTargetInfo | \
  221. -- NTLM_NegotiateVersion | \
  222. -- NTLM_Negotiate128 | \
  223. -- NTLM_Negotiate56)
  224. response = bin.pack("<AI", "NTLMSSP\0", msg_type)
  225. offset = BASE_OFFSET + #self.workstation + #self.username + #self.domain
  226. response = response .. bin.pack("<SSI", #lm, #lm, offset)
  227. offset = offset + #lm
  228. response = response .. bin.pack("<SSI", #ntlm, #ntlm, offset)
  229. offset = BASE_OFFSET
  230. response = response .. bin.pack("<SSI", #self.domain, #self.domain, offset)
  231. offset = BASE_OFFSET + #self.domain
  232. response = response .. bin.pack("<SSI", #self.username, #self.username, offset)
  233. offset = BASE_OFFSET + #self.domain + #self.username
  234. response = response .. bin.pack("<SSI", #self.workstation, #self.workstation, offset)
  235. offset = offset + #self.workstation + #lm + #ntlm
  236. response = response .. bin.pack("<SSI", #encrypted_random_sesskey, #encrypted_random_sesskey, offset)
  237. response = response .. bin.pack("<I", flags)
  238. -- add version info (major 5, minor 1, build 2600, reserved(1-3) 0,
  239. -- NTLM Revision 15)
  240. response = response .. bin.pack("<CCSCCCC", 5, 1, 2600, 0, 0, 0, 15)
  241. response = response .. self.domain .. self.username .. self.workstation .. ntlm .. lm .. encrypted_random_sesskey
  242. return response
  243. end
  244. }
  245. --- Encodes the parameters using the <code>CRAM-MD5</code> mechanism.
  246. --
  247. -- @param username string.
  248. -- @param password string.
  249. -- @param challenge The challenge as it is returned by the server.
  250. -- @return string The encoded string on success, or nil if Nmap was
  251. -- compiled without OpenSSL.
  252. function cram_md5_enc(username, password, challenge)
  253. local encode = stdnse.tohex(openssl.hmac('md5',
  254. password,
  255. challenge))
  256. return username.." "..encode
  257. end
  258. --- Encodes the parameters using the <code>DIGEST-MD5</code> mechanism.
  259. --
  260. -- @param username string.
  261. -- @param password string.
  262. -- @param challenge The challenge as it is returned by the server.
  263. -- @param service string containing the service that is requesting the
  264. -- encryption (eg. POP, IMAP, STMP)
  265. -- @param uri string containing the URI
  266. -- @return string The encoded string on success, or nil if Nmap was
  267. -- compiled without OpenSSL.
  268. function digest_md5_enc(username, password, challenge, service, uri)
  269. return DigestMD5:new(challenge,
  270. username,
  271. password,
  272. "AUTHENTICATE",
  273. uri,
  274. service):calcDigest()
  275. end
  276. function ntlm_enc(username, password, challenge)
  277. return NTLM:new(challenge, username, password):calcResponse()
  278. end
  279. else
  280. function cram_md5_enc()
  281. error("cram_md5_enc not supported without OpenSSL")
  282. end
  283. function digest_md5_enc()
  284. error("digest_md5_enc not supported without OpenSSL")
  285. end
  286. function ntlm_enc()
  287. error("ntlm_enc not supported without OpenSSL")
  288. end
  289. end
  290. MECHANISMS["CRAM-MD5"] = cram_md5_enc
  291. MECHANISMS["DIGEST-MD5"] = digest_md5_enc
  292. MECHANISMS["NTLM"] = ntlm_enc
  293. --- Encodes the parameters using the <code>PLAIN</code> mechanism.
  294. --
  295. -- @param username string.
  296. -- @param password string.
  297. -- @return string The encoded string.
  298. function plain_enc(username, password)
  299. return username.."\0"..username.."\0"..password
  300. end
  301. MECHANISMS["PLAIN"] = plain_enc
  302. --- Checks if the given mechanism is supported by this library.
  303. --
  304. -- @param mechanism string to check.
  305. -- @return mechanism if it is supported, otherwise nil.
  306. -- @return callback The mechanism encoding function on success.
  307. function check_mechanism(mechanism)
  308. local lmech, lcallback
  309. if mechanism then
  310. mechanism = string.upper(mechanism)
  311. if MECHANISMS[mechanism] then
  312. lmech = mechanism
  313. lcallback = MECHANISMS[mechanism]
  314. else
  315. stdnse.print_debug(3,
  316. "sasl library does not support '%s' mechanism", mechanism)
  317. end
  318. end
  319. return lmech, lcallback
  320. end
  321. --- This is the SASL Helper class, script writers should use it to create
  322. -- SASL objects.
  323. --
  324. -- Usage of the Helper class:
  325. --
  326. -- local sasl_enc = sasl.Helper.new("CRAM-MD5")
  327. -- local result = sasl_enc:encode(username, password, challenge)
  328. --
  329. -- sasl_enc:set_mechanism("LOGIN")
  330. -- local user, pass = sasl_enc:encode(username, password)
  331. Helper = {
  332. --- SASL object constructor.
  333. --
  334. -- @param mechanism The authentication mechanism to use
  335. -- (optional parameter).
  336. -- @param callback The encoding function associated with the
  337. -- mechanism (optional parameter).
  338. -- @usage
  339. -- local sasl_enc = sasl.Helper:new()
  340. -- local sasl_enc = sasl.Helper:new("CRAM-MD5")
  341. -- local sasl_enc = sasl.Helper:new("CRAM-MD5", my_cram_md5_func)
  342. -- @return sasl object.
  343. new = function(self, mechanism, callback)
  344. local o = {}
  345. setmetatable(o, self)
  346. self.__index = self
  347. if self:set_mechanism(mechanism) then
  348. self:set_callback(callback)
  349. end
  350. return o
  351. end,
  352. --- Sets the SASL mechanism to use.
  353. --
  354. -- @param string The authentication mechanism.
  355. -- @usage
  356. -- local sasl_enc = sasl.Helper:new()
  357. -- sasl_enc:set_mechanism("CRAM-MD5")
  358. -- sasl_enc:set_mechanism("PLAIN")
  359. -- @return mechanism on success, or nil if the mechanism is not
  360. -- supported.
  361. set_mechanism = function(self, mechanism)
  362. self.mechanism, self.callback = check_mechanism(mechanism)
  363. return self.mechanism
  364. end,
  365. --- Associates A custom encoding function with the authentication
  366. -- mechanism.
  367. --
  368. -- Note that the SASL object by default will have its own
  369. -- callback functions.
  370. --
  371. -- @param callback The function associated with the authentication
  372. -- mechanism.
  373. -- @usage
  374. -- -- My personal CRAM-MD5 encode function
  375. -- function cram_md5_encode_func(username, password, challenge)
  376. -- ...
  377. -- end
  378. -- local sasl_enc = sasl.Helper:new("CRAM-MD5")
  379. -- sasl_enc:set_callback(cram_md5_handle_func)
  380. -- local result = sasl_enc:encode(username, password, challenge)
  381. set_callback = function(self, callback)
  382. if callback then
  383. self.callback = callback
  384. end
  385. end,
  386. --- Resets the encoding function to the default SASL
  387. -- callback function.
  388. reset_callback = function(self)
  389. self.callback = MECHANISMS[self.mechanism]
  390. end,
  391. --- Resets all the data of the SASL object.
  392. --
  393. -- This methode will clear the specified SASL mechanism.
  394. reset = function(self)
  395. self:set_mechanism()
  396. end,
  397. --- Returns the current authentication mechanism.
  398. --
  399. -- @return mechanism on success, or nil on failures.
  400. get_mechanism = function(self)
  401. return self.mechanism
  402. end,
  403. --- Encodes the parameters according to the specified mechanism.
  404. --
  405. -- @param ... The parameters to encode.
  406. -- @usage
  407. -- local sasl_enc = sasl.Helper:new("CRAM-MD5")
  408. -- local result = sasl_enc:encode(username, password, challenge)
  409. -- local sasl_enc = sasl.Helper:new("PLAIN")
  410. -- local result = sasl_enc:encode(username, password)
  411. -- @return string The encoded string on success, or nil on failures.
  412. encode = function(self, ...)
  413. return self.callback(...)
  414. end,
  415. }
  416. return _ENV;