local bin = require "bin"
local comm = require "comm"
local ldap = require "ldap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"

description = [[
Attempts to retrieve the Novell Universal Password for a user. You
must already have (and include in script arguments) the username and password for an eDirectory server
administrative account.
]]

---
-- Universal Password enables advanced password policies, including extended 
-- characters in passwords, synchronization of passwords from eDirectory to
-- other systems, and a single password for all access to eDirectory.
--
-- In case the password policy permits administrators to retrieve user
-- passwords ("Allow admin to retrieve passwords" is set in the password
-- policy) this script can retrieve the password.
--
-- @args ldap-novell-getpass.account The name of the account to retrieve the
--       password for
-- @args ldap-novell-getpass.username The LDAP username to use when connecting
--       to the server
-- @args ldap-novell-getpass.password The LDAP password to use when connecting
--       to the server
-- 
-- @usage
-- nmap -p 636 --script ldap-novell-getpass --script-args \
--	'ldap-novell-getpass.username="CN=admin,O=cqure", \
-- 	ldap-novell-getpass.password=pass1234, \
-- 	ldap-novell-getpass.account="CN=paka,OU=hr,O=cqure"'
--
-- @output
-- PORT    STATE SERVICE REASON
-- 636/tcp open  ldapssl syn-ack
-- | ldap-novell-getpass: 
-- |   Account: CN=patrik,OU=security,O=cqure 
-- |_  Password: foobar
--

-- Version 0.1
-- Created 05/11/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>


author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}


portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})

function action(host,port)

	local username = stdnse.get_script_args("ldap-novell-getpass.username")
	local password = stdnse.get_script_args("ldap-novell-getpass.password") or ""
	local account = stdnse.get_script_args("ldap-novell-getpass.account")

	if ( not(username) ) then
		return "\n  ERROR: No username was supplied (ldap-novell-getpass.username)"
	end
	if ( not(account) ) then
		return "\n  ERROR: No account was supplied (ldap-novell-getpass.account)"
	else
		-- do some basic account validation
		if ( not(account:match("^[Cc][Nn]=.*,") ) ) then
			return "\n  ERROR: The account argument should be specified as:\n" ..
					"  	\"CN=name,OU=orgunit,O=org\""
		end
	end

	-- In order to discover what protocol to use (SSL/TCP) we need to send a
	-- few bytes to the server. An anonymous bind should do it
	local anon_bind = bin.pack("H", "300c020101600702010304008000" )
	local socket, _, opt = comm.tryssl( host, port, anon_bind, nil )
	if ( not(socket) ) then
		return "\n  ERROR: Failed to connect to LDAP server"
	end
		
	local status, errmsg = ldap.bindRequest( socket, { 
		version = 3,
		username = username,
		password = password
		}
	)

	if ( not(status) ) then return errmsg end

	-- Start encoding the NMAS Get Password Request
	local NMASLDAP_GET_PASSWORD_REQUEST = "2.16.840.1.113719.1.39.42.100.13"
	local NMASLDAP_GET_PASSWORD_RESPONSE = "2.16.840.1.113719.1.39.42.100.14"
	-- Add a trailing zero to the account name
	local data = ldap.encode( account .. '\0' )
	
	-- The following section could do with more documentation
	-- It's based on packet dumps from the getpass utility available from Novell Cool Solutions
	-- encode the account name as a sequence
	data = ldap.encode( { _ldaptype = '30', bin.pack("H", "020101") .. data } )
	data = ldap.encode( { _ldaptype = '81', data } )
	data = ldap.encode( { _ldaptype = '80', NMASLDAP_GET_PASSWORD_REQUEST } ) .. data
	data = ldap.encode( { _ldaptype = '77', data } )

	-- encode the whole extended request as a sequence
	data = ldap.encode( { _ldaptype = '30', bin.pack("H", "020102") .. data } )

	status = socket:send(data)
	if ( not(status) ) then	return "ERROR: Failed to send request" end
	
	status, data = socket:receive()
	if ( not(status) ) then return data end
	socket:close()
	
	local _, response = ldap.decode(data)

	-- make sure the result code was a success
	local rescode = ( #response >= 2 ) and response[2]
	local respname = ( #response >= 5 ) and response[5]

	if ( rescode ~= 0 ) then 
		local errmsg = ( #response >= 4 ) and response[4] or "An unknown error occured"
		return "\n  ERROR: " .. errmsg
	end

	-- make sure we get a NMAS Get Password Response back from the server
	if ( respname ~= NMASLDAP_GET_PASSWORD_RESPONSE ) then return end
	
	local universal_pw = ( #response >= 6 and #response[6] >= 3 ) and response[6][3]

	if ( universal_pw ) then
		local output = {}
		table.insert(output, ("Account: %s"):format(account))
		table.insert(output, ("Password: %s"):format(universal_pw))
		return stdnse.format_output(true, output)
	else
		return "\n  ERROR: No password was found"
	end
end
