Vice Society: A Tale of Victim Data Exfiltration via PowerShell, aka Stealing off the Land

Clock Icon 16 min read

This post is also available in: 日本語 (Japanese)

Executive Summary

During a recent incident response (IR) engagement, the Unit 42 team identified that the Vice Society ransomware gang exfiltrated data from a victim network using a custom built Microsoft PowerShell (PS) script. We’ll break down the script used, explaining how each function works in order to shed light on this method of data exfiltration.

Ransomware gangs use a plethora of methods to steal data from their victims’ networks. Some gangs bring in outside tools, including tools such as FileZilla, WinSCP and rclone. Other gangs use living off the land binaries and scripts (LOLBAS) methods, such as PS scripts, copy/paste via Remote Desktop Protocol (RDP) and Microsoft’s Win32 API (e.g., Wininet.dll calls). Let’s examine what happens when a PS script is used to automate the data exfiltration stage of a ransomware attack.


Palo Alto Networks customers receive protections from and mitigations for the script described below in the following ways:

  • The XQL query provided below can be used with Cortex XDR to help track the presence of this script.
  • The Unit 42 Incident Response team can provide personalized assistance.

Additionally, the YARA rule we attached at the end of this post can be used to detect this script.

Related Unit 42 Topics Vice Society, Ransomware


Threat actors (TAs) using built-in data exfiltration methods like LOLBAS negate the need to bring in external tools that might be flagged by security software and/or human-based security detection mechanisms. These methods can also hide within the general operating environment, providing subversion to the threat actor.

For example, PS scripting is often used within a typical Windows environment. When TAs want to hide in plain sight, PS code is often a go-to.

Early in 2023, the Unit 42 IR team found the Vice Society ransomware gang using a script named w1.ps1 to exfiltrate data from a victim network. In this case, the script was recovered from the Windows Event Log (WEL). Specifically, the script was recovered from an Event ID 4104: Script Block Logging event, as found within the Microsoft-Windows-PowerShell/Operational WEL Provider.

While Script Block Logging must be enabled in Windows for all script blocks to be logged, Microsoft uses some undocumented back-end magic to record events by default that it deems to be malicious. Thus, Event ID 4104 events can be useful to your analysis even in environments where Script Block Logging has not been fully enabled.

Unit 42 researchers saw the script executed using the following PS command:

This script invocation uses a local domain controller’s (DC) IP address within a Uniform Resource Name (URN) path (shown as [redacted_ip] above), specifying the s$ admin share on the DC. Note that, since the script is deployed via one of the client’s DCs, target machines could be those that the TA has not yet gained access to directly. As such, any endpoint within the network could become a target for the script. The PS executable is given the -ExecutionPolicy Bypass parameter to bypass any Execution Policy restrictions.

The script does not require any arguments, as the onus of what files to copy out of the network is left to the script itself. Yes, you read that right: The script is automated and thus chooses what data should be exfiltrated.

Script Analysis

The script begins by declaring two constants to be used for victim identification, $id and $token. In the scripts that we identified, these values were hard-coded to “TEST” and “TEST_1”, respectively:

Logically, these variables may be set to more specific values that identify actual victims. We are unsure if this was actually a testing phase or if the values will simply remain in this testing state.

Next, the script declares the functions that serve as the heavy lifters within the code base. Table 1 provides an overview of the script’s functions. This lists functions in the order in which they are called, not the order in which they’re declared.

Function Description
Work( $disk ) Called for each mounted volume. Identifies directories for potential exfiltration, ignoring a hard-coded list of directory names.

Calls Show() function and passes directory names for all directories that do not match the ignore list.

Show( $name ) Receives directory names from the Work() function. Chunks directories into groups of five and passes groups of folders to the CreateJobLocal() function for further processing.
CreateJobLocal( $folders ) Receives groups of directories, often in groups of five, and creates PowerShell script blocks to be run as jobs via the Start-Job cmdlet.

Directory names provided go through an inclusion/exclusion process that uses keywords to select which directories to pass to the fill() function to exfiltrate.

fill( [string]$filename ) Called by CreateJobLocal() to perform the actual data exfiltration via HTTP POST requests to the threat actor’s web server.

Table 1. An overview of the script’s functions.

Figure 1 provides an overview of the process flow between functions, helping to highlight how the script functions.

Image 1 is a function diagram of the w1.ps1 script. It starts with the file and ends with the uploading of the file via HTTP POST events to the threat actor.
Figure 1. Function diagram of the w1.ps1 script.

The Beginning

Before calling any of the declared functions, the script identifies any mounted drives on the system via Windows Management Instrumentation (WMI). A call to get-wmiobject win32_volume with some simple filtering provided an array named $drives, which will contain a list of drives mounted on the machine. Each drive path found is then passed to the Work() function individually. Figure 2 shows the associated code snippet.

Image 2 is a screenshot of a script. Highlighted by 1 is the line starting ForEach-Object and highlighted by 2 is the line starting with Work.
Figure 2. The script’s preamble code that identifies and then processes each mounted volume.

The following actions are taken in the preamble code:

  1. It creates an array named $drives and fills the array with a list of mounted volumes on the host.
    1. The DriveType enum in the win32_volume references local disks. See Microsoft’s Win32_Volume Class documentation for more information.
  2. It iterates through the identified drives on the host (as $drive), passing each identified drive path to the Work() function.

Figure 3 shows an example of what this code might do on an average Windows host that only has a single drive mounted.

Image 3 is a screenshot of code. Highlighted in red are the drive and drives variables.
Figure 3. Example values for the $drives and $drive variables on a host with a single mounted drive.

For each drive name identified, the preamble calls the Work() function to process directories on the drive.

Work() Function

Each time Work() is invoked, the function receives a drive path (as $disk) to use for directory searching and processing. Figure 4 shows the beginning of the Work() function.

Image 4 is a screenshot of many lines of code showing the start of the Work() function. Highlighted are three lines: the array, the $store, and the function.
Figure 4. The beginning of the Work() function.

The following actions are taken in the above code:

  1. It creates $folders and $jobs arrays.
  2. It creates the $store tuple, which stores the above created arrays.
  3. It declares the Show() function.

Figure 5 shows the remaining code of the Work() function that resides just beneath the Show() function.

Image 5 is a screenshot of many lines of code showing the end of the Work() function. Highlighted are three areas.
Figure 5. Remainder of the Work() function.

The following actions are taken in the above code:

  1. It passes the current volume string to Get-ChildItem and filters out a series of 31 potential directory paths to avoid processing system and/or application-based files. It then passes each root directory name to the Show() function for further processing.
  2. After passing the root directory folders to the Show() function, the Work() function recursively searches through sub-directories in the root directories. Similar to the previous filtering, sub-directories that do not match an exclude list are sent to the Show() function for processing.
  3. The Show() function creates PowerShell jobs to facilitate data exfiltration. The function processes groups of five directories at a time. This section of code serves as a fail safe to ensure that any remaining grouping of folders are processed.
    • For example, if a total of 212 directories are identified, this bit of code would ensure that the final two directories are processed.

Show() Function

The Show() function receives directory names for processing. Figure 6 provides an overview of the Show() function.

Image 6 is a screenshot of many lines of code showing an overview of the Show() function. Highlighted are two areas: the line starting with if and the line starting with while.
Figure 6. An overview of the Show() function.

The following actions are taken in the above code:

  1. The function collects provided directory names until it can create a grouping of five directory names. Once the function has been provided five directory names, it passes them to the CreateJobLocal() function to create PowerShell jobs to facilitate data exfiltration from the directory group.
  2. The script implements rate limiting in that it only wants to process up to 10 jobs of five directory groups at one time. Should more than 10 jobs be running, the script sleeps for five seconds and re-checks the number of running jobs.
    • Note: This shows a professional level of coding in terms of the overall script design. The script was written to avoid inundating the host’s resources. The exact reason for this lies with the author, but the methodology aligns with general coding best practices.

CreateJobLocal() Function

The CreateJobLocal() function sets up a multi-processing queue for data exfiltration. Figure 7 shows the beginning portion of the CreateJobLocal() function.

Image 7 is a screenshot of many lines of code showing an overview of the CreateJobLocal() function. Highlighted are two areas: the line starting with if and the line starting with while.
Figure 7. An overview of the CreateJobLocal() function.

The following actions are taken in the above code:

  1. It creates a pseudo random name for the job being created. Job names will consist of five alpha characters (including lower- and upper-case characters).
    • For example, the following are five job names generated by the script during a random debugging session: iZUIb, dlHxF, VCHYu, FyrCb and GVILA.
  2. It sets up a PowerShell job, which has a code structure that will be a script block created at this point in the script.

At this point in CreateJobLocal(), the fill() function is declared. We will return to this shortly. First, we will continue with the remainder of the CreateJobLocal() function. Figure 8 shows the next chunk of this code.

Image 8 is a screenshot of many lines of code showing the remainder of the CreateJobLocal() function. Highlighted by numbers 3, 4, 5 are the foreach and if sections
Figure 8. Additional code belonging to the CreateJobLocal() function.

The following are descriptions of the above CreateJobLocal() code base:

  1. It creates a $fileList array for files to exfiltrate, then loops through directories in the current group (as noted above, it typically processes directories in groups of five).
  2. It sets up inclusion and exclusion arrays named $include and $excludes.
  3. It loops through directories in the given directory group and filters folders to include based on hard-coded values in the $include array using a regular expression.

At this point, the function uses excludes to filter further the files that should be exfiltrated.

Image 9 is a screenshot of many lines of code showing the remainder of the CreateJobLocal() function. Highlighted by numbers 6, 7 and 8 are areas of interest: two arrays and the foreach section
Figure 9. Remainder of the CreateJobLocal() function.

The following are descriptions of the remaining CreateJobLocal() code base:

  1. If a directory matches the include list, it finds all files within the directory that do not have extensions found on the exclude list, are larger than 10 KB, and have an extension.
    • Note: Testing confirmed that the script ignores both files that are under 10 KB in size and those that do not have a file extension.
  2. Even if a directory does not match the include list via a regular expression match, the directory’s files are checked to see if they should be included for exfiltration.
    • This looks to serve as a second chance for files to match the inclusion list, as the comparison is done with the -Include parameter of the Get-ChildItem cmdlet as opposed to the -Like comparison that performs a regex comparison in step 5 above.
  3. It loops through the files identified for exfiltration and calls the fill() function to exfiltrate each file.

Figure 10 shows the first group of five folders the scripts selected when run within one of our malware analysis virtual machines (VMs). These values will differ based on the machine on which the script is run. We simply wanted to show where the script began searching for data within our test environment.

Image 10 is a screenshot of the folders selected by the script for exfiltration. Highlighted in red in the blue bottom pane is the C drive path.
Figure 10. An example run of the script showing the first five directories identified for exfiltration.

Fill() Function

The fill() function performs the actual data exfiltration. This function serves to build the URLs that will be used to exfiltrate files, and it uses a System.Net.Webclient object to perform the actual exfiltration via HTTP POST events using the object’s .UploadFile method. Figure 11 shows the fill() function.

Image 11 is a screenshot of many lines of code showing an overview of the fill(function) function. Highlighted by red numbers showing the order of actions taken by the script.
Figure 11. An overview of the fill() function.

The following actions are taken in the above code:

  1. Though not technically part of the actual fill() function, the variables $id and $token from the first two lines of the overall script are used within each file upload URL.
  2. It builds a $prefix value that includes the two most important indicators of compromise (IoCs) from the script.
    • An IP address
      1. This is the TA’s infrastructure / server IP address to which the files will be uploaded.
    • A network port number
      1. This port number may be 80, 443, or it may be a custom port number such as one normally associated with the ephemeral port range.

Note: For the purposes of this article, we are redacting this information.

  1. It instantiates a WebClient object that will be used to perform the HTTP-based data exfiltration.
  2. It builds a $fullPath variable, which is the full file path to the file being uploaded.
    • Note: This is important because this means that each HTTP POST event will include the file’s full path. If you are able to obtain the source host’s IP address along with this path, you will then be able to build out a list of exfiltrated files after the fact.
  3. It builds the full URL for the file upload, $uri, by combining the $prefix, $token, $id and $fullPath variables.
  4. It calls the WebClient.UploadFile() method to upload the file.
    • Note: This creates an HTTP POST event.

Example HTTP Activity

To see what the script’s POST requests would look like on the threat actor’s web server, we set up a server on a local VM, directed our malware analysis machine to use this VM as its gateway and ran the script. The following are three example POST requests as created by the script when executed within our test environment.

Please note that the 192.168.42[.]100 address above is the IP of the test client VM that we used. In a real world scenario, Vice Society’s web server would denote the victim’s egressing IP address in this location.

Based on the above results, we can garner some important things about the HTTP activity initiated by the script:

  1. The fullpath POST parameter does not include the drive letter from which the file was sent.
  2. The script does not provide a user agent string to the web server.

If you have a network security monitoring (NSM) or intrusion detection system (IDS) such as Zeek, or a packet capture system running in your environment, you might be able to see the outgoing POST requests. Those outgoing logs might reveal the length of the requests in bytes (focus on bytes out versus total bytes), which could help identify which versions of files were exfiltrated.


Vice Society’s PowerShell data exfiltration script is a simple tool for data exfiltration. Multi-processing and queuing are used to ensure the script does not consume too many system resources. However, the script’s focus on files over 10 KB with file extensions and in directories that meet its include list means that the script will not exfiltrate data that doesn’t fit this description.

Unfortunately, the nature of PS scripting within the Windows environment makes this type of threat difficult to prevent outright. We have provided tips and tricks related to detection and hunting this type of threat in the Detection and Hunting section. Using these tips, especially using the provided YARA rule, we wish you the best of luck in identifying this threat. Don’t let the ransomware gangs automate the loss of your data!

Palo Alto Networks customers receive protections from and mitigations for the script described below in the following ways:

  • The XQL query provided below can be used with Cortex XDR to help track the presence of this script.
  • The Unit 42 Incident Response team can provide personalized assistance.

If you think you may have been compromised or have an urgent matter, get in touch with the Unit 42 Incident Response team or call:

  • North America Toll-Free: 866.486.4842 (866.4.UNIT42)
  • EMEA: +
  • APAC: +65.6983.8730
  • Japan: +81.50.1790.0200

Detection and Hunting

  • Implement the YARA rule provided in this article within your security systems.
  • Enable PowerShell Module and Script Block Logging in PowerShell
    • Check Windows Event Logs Event IDs 400, 600, 800, 4103 and 4104
    • Search for the script’s function names in 4104 events:
      • Work( $disk )
      • Show( $name )
      • CreateJobLocal( $folders )
      • fill( [string]$filename )
  • Monitor for command lines that include the following: powershell.exe -ExecutionPolicy Bypass -file \\[internal_ip_address]\s$\w1.ps1
  • Look for HTTP POST events to /upload endpoints on unknown remote HTTP servers.
  • Look for HTTP activity direct to external IP addresses, if you have this visibility.
  • Detect spikes in network traffic:
    • Do you have a network baseline? Use it to determine when network traffic from a given or set of hosts far exceeds the baseline.
    • Do you have a SIEM, SOAR or log aggregation utility that will allow you to alert on HTTP POST sizes? Perhaps look for when a count of POST events to a given site – especially an IP address – exceeds a baseline. Also look into alerting for when a POST event has a request size over a given threshold. For example, you might want to alert when any POST event has a file size > 10 MB. This will require tuning and insight into what is normal in your environment.
    • Look into network traffic spikes generated by non-expected accounts. For example, should your Domain Admin, Enterprise Admin or general service accounts be making large POST requests? Is this something for which you can generate alerts?

Indicators of Compromise

Since the script in question was recovered from an Event ID 4104 WEL event, a hash of the true, original file as it may have resided on disk is not available. However, we have included the filename of the script along with the contents recovered from the script in this section.

Note: We have opted not to release any IP addresses or port numbers. Furthermore, these IoCs will not be provided upon request.


  • w1.ps1


The following YARA rule was written to help identify this script. As of this article’s publication date, the script only yielded one false positive in VirusTotal Intelligence’s Retro Hunt system over a one-year period. The rule looks for the two lines of code that set up the victim identity information and the string concatenation methods used to build the URIs used for data exfiltration via HTTP.

Unit 42 Managed Threat Hunting Queries

Additional Resources

Appendix: Inclusions and Exclusions

Work() function exclusions

CreateLocalJob() Includes

CreateLocalJob() Excludes

Enlarged Image