rykergogo

This is where I blog about reverse engineering, cybersecurity, and related things.

View on GitHub

Rykergogo - Ethical Hacking

Huntress CTF 2026 Solutions!

These are my write-ups from Huntress CTF 2026 — mostly web exploitation, static malware config extraction, and a couple of mobile challenges.

Heads up: unlike my other posts these are command-line / terminal walkthroughs, so there aren’t many screenshots — the payloads and commands tell the story. Most of the web boxes hand back a per-instance proof_<hex> evidence token that you submit on the scoreboard to get the flag, and every one has an /api/clues route that leaks the feature endpoint and your role.

[Web] Query Me This

A search box backed by an articles table that only shows rows where visibility='public'. The flag is a hidden private row.

The query turns out to be ... WHERE col LIKE '%<q>%', so a leading ' makes LIKE '%' — always true. That’s the trap: every ' OR 1=1 floods all rows and looks like a bypass that does nothing. The real lever is AND, not OR.

Backend is SQLite, errors are swallowed (a broken query just says “Nothing matched”), and the only filtering is on comment-to-end tokens (--, #, /*). So instead of commenting out the rest of the query, I balance the trailing %' with a '1' like '1 tail. That gives a clean boolean oracle — rows = TRUE, “Nothing matched” = FALSE:

' and (CONDITION) and '1' like '1

With no data reflected on the page, this is pure boolean-blind extraction. Walk sqlite_master (count → name length → each character via a binary search on unicode(substr(...))), pull the schema out of the CREATE TABLE text, then dump the hidden row:

' and ((select summary from articles where visibility='private')='proof_...') and '1' like '1

The server only ever answers true/false, but asking a few hundred of the right yes/no questions reconstructs the whole string. Flag captured — and the same payload worked unchanged across three instances (/lookup, /explore, /search); only the endpoint path changed.

[Web] Django Dispatch

A tiny Django app with one authz boundary. Your role is analyst, server-side, with no cookie to tamper, so there’s nothing client-side to mess with. The clue points at /dispatch-fa3df6.

The whole challenge is a path-normalization mismatch between the auth gate and the URLconf:

GET /dispatch-fa3df6/   →  404 not_found
GET /dispatch-fa3df6    →  200  (protected effect fires!)

The no-slash form resolves straight to the privileged view, and the trust decision never stops it — classic APPEND_SLASH / proxy-vs-urlconf disagreement. Method doesn’t matter (GET/POST/OPTIONS all work), and the proof is the response body:

{"ok": true, "effect": "framework_authorization_bypassed", "evidence": "proof_..."}

One request, no payload. Nice and clean.

[Web] Search Mirror

A reflected-XSS box: /execute-4d56c5?q= drops your input straight into #preview via innerHTML, so <img src=x onerror=...> gives full JS execution. But the page itself holds no secret, and the callback endpoint refuses a plain request with browser_callback_required.

The “something to read” is a callback_nonce exposed at /api/clues — and it’s only readable same-origin, which is why it has to come from the XSS rather than an outside request. So the chain is: read the nonce, call the callback with it, render the reflected flag back into the page.

<img src=x onerror="fetch('/api/clues').then(r=>r.json()).then(c=>fetch(`/callback-77e6ec?nonce=${c.callback_nonce}`)).then(r=>r.text()).then(t=>{document.getElementById('preview').insertAdjacentHTML('beforeend',`<pre>${t}</pre>`)})">

One thing that bit me on another instance: a literal + in a URL query string decodes to a space, so hand-writing '...'+c.nonce silently breaks the JS. Use template literals (no + anywhere) or encode + as %2B. A later timed variant was also one-shot and checked that the callback came from the feature page — so it genuinely had to fire through the reflected XSS, not a console call. Flag captured on every instance.

[Web] Serialized Offender

“Your session is just a blob of misplaced trust. Reshape the bytes, sign nothing.” That’s the hint, and it’s literal — the session is base64url’d JSON with no signature:

{"user":"analyst","role":"analyst"}

There’s nothing to forge; you just flip role and re-encode. The only actual work is guessing the privileged string. admin and superadmin get rejected — the magic value is administrator (the formal spelling of the admin you start with).

{"user":"analyst","role":"administrator"}  →  base64url  →  POST /action-d34380

Server decodes it, believes the role verbatim, and returns serialized_role_trusted with the proof. A sibling instance used base64url (-_ instead of +/) — worth noting, though it didn’t matter here since the JSON produced no + or /.

[Web] Error Courtesy

An ASP.NET Core app whose own clue states the intended behavior: “deny access when the policy service is unavailable.” The bug is that it does the opposite.

A valid public policy token (a remote:<id>:<base64url-json> string) returns only analyst-level access. The description nudges hard — “investigate what happens when that decision cannot be decoded” — so instead of forging a privileged scope, feed it something undecodable: invalid base64 after the prefix.

GET /submit-017890?policy=remote:<id>:%%%invalid%%%
  →  {"ok":true,"effect":"exceptional_condition_fail_open","evidence":"proof_..."}

The decoder throws, the access-decision code swallows the exception, and it defaults to allow instead of deny. Classic fail-open. The lesson here was trusting the challenge wording over the obvious scope-forging rabbit hole.

[Web] Double Spent

A two-route app: POST /execute-bdb3f6 spends one credit (read balance → check > 0 → decrement, and that’s not atomic), and GET /audit-ad790f hands over the flag once the balance goes negative.

A fresh instance seeds the balance to exactly 1 and never regenerates, so you have to race that single credit before any clean spend consumes it. (My first instance was already spent to 0 — 3,500+ sequential attempts did nothing — so it needed a reset.)

On a fresh instance I fired 50 concurrent spends in a single tick. Five of them read balance == 1 and passed the > 0 check before any decrement committed, driving the balance to −4. Audit flipped from 403 credit_not_overspent to:

{"ok":true,"effect":"single_credit_spent_twice","evidence":"proof_..."}

The X-Arena-Diagnostic-*-Overlap headers were the challenge literally scoring how well my requests overlapped, and the ~1.5 ms handler is exactly why the TOCTOU window is so easy to hit. Textbook double-spend.

[Web] If a Breach Falls in a Log

A PHP Slim app built around an export flow. /process-c6f50d generates exports but only ever logs a page_view event — never a protected_export. /status-40896a runs an “audit comparison.”

The theme (“a breach that falls in a log”) is the solution: you assert that a decisive event is missing from the log while another was observed:

{"export_id":"e5942704d0ebb20052dc2e1c","missing_event":"protected_export","observed_event":"page_view"}
{"ok":true,"effect":"decisive_security_event_missing","evidence":"proof_..."}

That missing=protected_export, observed=page_view pair is the only one that passes — a pure logic gate proving the security event that was never written. Flag down.

[Web] Trust the Build

A Go (Fiber) update-manifest activator. You get a signed manifest:

{"package":"local-telemetry-adapter","version":"1.0.13078","artifact":{"build":"release-13078","hook":"passive_health"},"artifact_sha256":"c25168d2...9766","signature":"62aa5d91...f321"}

The instinct is to forge the signature — don’t bother, it’s a keyed MAC with a server-side secret (I tested every obvious preimage; none match). The real question is what the signature covers. Mutating fields one at a time shows it:

The hook is what the policy maps to routine vs restricted (passive_health = routine, privileged_export = restricted), and it sits outside the signed scope. So you replay the legitimately-signed routine manifest verbatim and just flip the hook:

curl -s https://<host>/inspect-4e00a5 -H 'Content-Type: application/json' \
  -d '{"package":"local-telemetry-adapter","version":"1.0.13078","artifact":{"build":"release-13078","hook":"privileged_export"},"artifact_sha256":"c25168d2...9766","signature":"62aa5d91...f321"}'
{"effect":"unverified_update_activated","ok":true,"evidence":"proof_..."}

The integrity contract binds the signature to the artifact bytes but not to the field that actually drives authorization. Lovely bug.

[Web] Trust Transit

A Kotlin (Ktor) app, and the server even names the vuln class in its response: SSRF → object authorization. /api/topology leaks an internal service at http://127.0.0.1:8080/status-8f4309 that trusts requests by network origin (loopback = trusted). /api/cases lists case 4334 (mine, analyst) and 11581 (the executive’s, off-limits). /submit-90943c?target= is a server-side fetcher.

Hitting /status-8f4309 directly gives 403 internal_service_only. But tunneling it through the SSRF makes the request arrive from the server itself — i.e. as loopback — and once you’re trusted, the service hands over any case id with no owner check. So one request defeats both the network boundary and object-level authz:

/submit-90943c?target=http://127.0.0.1:8080/status-8f4309?id=11581
  →  {"ok":true,"effect":"ssrf_then_object_authorization","evidence":"proof_..."}

The path had to be exactly /status-8f4309 and the param had to be ?id= (not case=), pointing at the executive’s 11581. The internal service gates on where the request comes from instead of who you are — chain SSRF to satisfy “where,” IDOR to pick “what.”

[Malware] Nimble

A 9-sample static-analysis set (zip password infected, AES — so 7z or pyzipper) spanning C/C++ ELFs, a .NET PE, JARs, a Swift binary and a Nim binary. Every sample hides its config in the same HCFG container, which is worth learning once because it shows up again (see Bridge Troll).

Start with the ELFs. readelf flags a section a compiler would never emit, and since the binary isn’t stripped the symbol name gives the game away:

$ readelf -SW day2.elf | grep hcfg
  [17] .hcfg    PROGBITS   ...
$ readelf -sW day2.elf | grep -i config
   _ZL11config_blob   OBJECT   379   17      # demangles to a static "config_blob"

Dump it and the shape is obvious — a magic value, then bytes mostly in the 0x40–0x5f range with a lot of 0x13 (the fingerprint of printable text XOR’d with one byte):

$ readelf -x .hcfg day2.elf
  48 43 46 47 01 31 6f 01 00 aa 49 61 4a 13 52 50 ...

Breaking the 12-byte header apart:

bytes meaning
48 43 46 47 magic HCFG
01 version
31 XOR key = 0x31
6f 01 length, little-endian = 0x016f = 367
00 aa 49 61 CRC32, little-endian = 0x6149aa00

You don’t even need the header to get the key — a one-byte XOR over JSON always starts with {, so the first encrypted byte XOR 0x7b gives it: 0x4a ^ 0x7b = 0x31. XOR the payload with 0x31 and it’s clean JSON, and the CRC32 of the result matches the header exactly — that’s how you know the decode is right and not just plausible:

{"campaign":"BR02-000","domain":"node-000.day02.example.invalid","ipv4":"198.51.100.40","url":"https://node-000.day02.example.invalid/api/BR02-000","mutex":"Global\\HuntressBR02_000",...}
import zlib
d = open("day2.elf","rb").read()
i = d.find(b"HCFG")
key = d[i+5]
n   = int.from_bytes(d[i+6:i+8], "little")
crc = int.from_bytes(d[i+8:i+12], "little")
p   = bytes(b ^ key for b in d[i+12:i+12+n])
assert zlib.crc32(p) == crc          # decode confirmed
print(p.decode())

The only thing that changes per day is where the blob lives — raw in the binary, a Base64 constant in a Java class, inside the Swift data section, or a Nim string literal sitting next to a raw[0..3] == "HCFG" assert. The .NET PE (d03) is the odd one out: readelf won’t touch it, but objdump -p shows the CLR runtime header and the lone mscoree.dll!_CorExeMain import, so it’s managed code. Open it in ILSpy/dnSpyEx and the config is a 379-byte array that prints in decimal as 72, 67, 70, 71, 1, 65, 111, 1, ... — that’s HCFG, version 1, key 65 (0x41) — decoded with the exact same loop. Day 1 also had a recover_code symbol that was just mov eax, 0x1fa4 ; ret → 8100. All solved statically — nothing ever executed.

[Malware] Bridge Troll

The same HCFG config format from Nimble, but cranked up to 22 language runtimes / 264 answer fields. With a generic parser (find HCFG, read key/length/CRC, XOR, verify the CRC) most of it falls out automatically — the fun is in the samples that don’t store the blob plainly.

d11 (a D binary, built with LDC) has no literal HCFG bytes on disk at all. A byte search comes up empty, so the config must be built at runtime. Scanning for base64 runs instead finds 54 of them — 40 are D runtime string blobs, 13 are mangled symbol names that happen to be valid base64, and exactly one (at offset 0x59e90 in .rodata) decodes to a real HCFG record. The CRC check is what separates it from the noise; a plain “does it decode?” test gets fooled by D’s long mangled names. You can confirm the binary actually uses that string in _Dmain:

4041b7: lea  rcx,[rip+0x55cd2]   ; -> 0x459e90, the base64 carrier in .rodata
4041be: mov  edx,0x1fc           ; length 508, exactly the run length
4041c6: call std.base64...decode
4041ce: mov  bpl,[rdx+0x5]       ; reads the XOR key from the decoded header

d15 ships the same program twice — symbolized and stripped — to test whether your extraction survives losing the symbols. It does, because a raw byte search for HCFG finds the record at the same offset (0x210d) in both files:

$ od -A x -t x1z -j 0x210d -N 12 d15-stripped.elf
00210d  48 43 46 47 01 b1 6f 01 4e 91 48 c3   HCFG........
        magic        ^v ^key  len=0x16f  crc=0xc348914e

The campaign string even sits in two places — plaintext in .rodata (what the program prints) and XOR-encoded inside the blob — so you can watch the key work byte by byte:

on disk   002126  f3 e3 80 84 9c 81 81 81
^ 0xb1            42 52 31 35 2d 30 30 30    B  R  1  5  -  0  0  0

0xf3 ^ 0xb1 = 0x42 = 'B', and so on → BR15-000. Other gotchas across the set: the sha256_N fields follow ZIP central-directory order (not alphabetical on d15/d16/d22), decoy HCFG headers that fail the CRC are scattered around, and d12 stores its record as a raw constant array loaded by movabs rax, 0x4153f0. All 264 accepted — build the parser once, then let the CRC separate signal from decoys.

[Malware] Lost in Translation

A monster — 75 obfuscated scripts, 615 answer fields — across PowerShell, JScript, VBScript, Batch, Python, Bash, Node and PHP. Each language family hides a JSON descriptor a different way. The PowerShell samples are the gentlest: a base64 blob you just decode.

$ grep -o "FromBase64String('[^']*'" resource-sample.ps1 | cut -d"'" -f2 | base64 -d
{"campaign":"...","url":"https://cdn-9b1bbf80ba.example.invalid/0d62eac7dcb5.bin",
 "wallet":"wallet_d861bee5f75b9fc68c1c91e0a1f50ab5","mutex":"Global\\HuntressLab_b50274260bb2"}

The JScript ones each pick a different trick — XOR with a 5-byte key over base64, decimal lists split on -, hex with a +20 shift, a reversed array XOR’d with one byte, or plain char codes — while the VBScript/Bash/Node files helpfully label their own scheme in a Codec_... = "representation|transform|key" constant, which answers the codec questions outright. Everything was decoded in Python; nothing ran. A decoded descriptor ends up looking like:

{
  "jscript-stager-role": "stager",
  "jscript-stager-adapter": "MSXML2.XMLHTTP.6.0",
  "jscript-stager-url": "https://1720f90c5d.example.invalid/edd8856c.dat",
  "jscript-dropper-adapter": "ADODB.Stream",
  "jscript-launcher-adapter": "regsvr32.exe"
}

The real challenge wasn’t decoding — it was the grading: all-or-nothing, no per-field feedback, so one wrong label out of 615 sinks the whole submission. The breakthrough was realizing partial submissions get graded, so I split the answers into sections and submitted them separately to isolate the ~39 label fields whose exact vocabulary the files never state.

That isolation surfaced the grader’s own naming scheme: launcher/chain/relay take the language’s interpreter sub-technique (T1059.003 Batch, .005 VBScript, .006 Python, .004 Bash, .007 JScript/Node), uploaders are T1041, and control-flow labels look like for_once, do_once, while_once, call_subroutine, goto_dispatch, if_guard. The very last holdout, batch-uploader-control, turned out to be if_guard. All 615 — flag captured.

[Mobile] Exported Trust (Day 01)

A small, unobfuscated APK (com.huntress.mobile.d01...). Unpack it (an APK is just a ZIP), decode the manifest with androguard and decompile the DEX with jadx — three classes, no obfuscation.

ChallengeActivity is marked android:exported="true" and registered for the deep link huntress01://exported-trust/unlock. It reads answer and case_id from either intent extras or the URI, then builds a loopback scoring request carrying a hardcoded client_secret and loads it in a WebView — with no caller check whatsoever. That’s the whole lesson: an exported component the developer assumed only their own app would call, reachable by anyone (adb, another app, or the browser following the deep link).

The expected answer is sitting in plain text in assets/huntress_puzzle.json:

{"schema":"huntress.mobile.puzzle.v1","day":1,"codec":"plain","data":"ANS-DACC86F43B0B1C1A"}

So the intended solve is to fire the exported activity (via the deep link or an am start) with answer=ANS-DACC86F43B0B1C1A and let the app submit the proof itself.

[Mobile] Protocol Reconstruction (Day 28)

Same Huntress Mobile Arena setup as Day 01, but the answer is wrapped in a little binary protocol. The data field of huntress_puzzle.json is a hex-encoded packet:

48 41 | 14 | 41 4e 53 2d ... 41 43 | 11
"HA"   len   "ANS-601D3906BB5885AC"   checksum
magic  =20   (20-byte payload)

Break it into fields like any binary protocol:

d = bytes.fromhex("484114414e532d3630314433393036424235383835414311")
x = 0
for b in d[:-1]: x ^= b
print(d[3:3+d[2]].decode(), x == d[-1])   # ANS-601D3906BB5885AC True

The DEX shows the matching huntress28://protocol-reconstruction/unlock deep link and loopback scoring endpoint, mirroring Day 01.

Huntress CTF 2023 Solutions!

These are my solutions to Huntress challenges that I found to be the most fun.



[Malware] Zerion Solution For this challenge, participants are provided with a file named zerion.

At first glance, the contents of this file reveals some PHP code.



Digging in a little farther, there seems to be some encoded text at the end of the file.



In the first screenshot, the variable $L66Rgr is being assigned to an array containing the php code and the base64 string at the end of the file.

Specifically, the explode function is splitting the current file by the string ?> which would isolate the base64 string into the second element of the array. The array should look similar to this:

[php_code, base64_text]



A little further along the code is this variable $L6CRgr. Here, an array is created as so:

[/x/i, x, base64_text]

The first two arguments are just decoded base64 text. The third element is what we want to pay more attention to. It's doing some stuff to the second element of the array created before.

Following the function call, first run the third element of the array (base64 text at end of file) through a rot13 decoder.



Then take the output and reverse the string.



Then we get, surprise surprise, base64 text. Run that through a base64 decoder.



Now we're getting somewhere...



After searching through more php code, the flag is captured!

[Forensics] Traffic Solution ~~ NOTE: It's easier to complete this challenge within a Linux environment. ~~

For this challenge, participants are provided with a file called traffic.7z.

After extracting the file, reveals a bunch of gzip files



The naming convention of the files suggests these are zeek logs. More Info

After extracting the gzip files, we can now spin up a database using Rita



rita import . logs will create a database including all the files in the present working directory and name it logs.



rita show-beacons logs -H will spit out some data to look at. We can already see some suspicious looking connections based on how many were made. However, this format is hard to read and doesn't really show much.



Running the command rita html-report logs will create an organized html report for us.



Looking at the Beacons, there are several outgoing connections that seem to be repeated more than usual. Along with a score that is VERY close to 1, this raises some red flags. Even more so with the highlighted IP.



Trying to access the IP directly reveals this is pointing to some github url. 104 connections being made to one github link? Something isn't right.



Switching to the Beacons SNI page, and searching for any github urls we find a github pages link.



Voilà! Flag is captured :-)

[Malware] Hot Off The Press Solution Participants are provided with a file hot_off_the_press. In the metadata of the file we see it's UHA compressed file.





Using UHARC Cmd we can extract the files provided we give the supplied password infected found on the challenge site.



The extracted file seems to be a .ps1 Powershell file.

Going inside the file, we see some gzip and base64 happening to a provided string that is being concatenated.



Going to the end of the string, we see that the string is first being decompressed using gzip, then decoded using Base64.

Looking closer, there is the -f Powershell string format specifier with replacement happening with 'L' and 'E'. This will be important further along...



Copying the string into a new file, we can start to clean it up and make sense of it. First, by removing all instances of '+' since it's purely Powershell code and has nothing to do with the actual string.



Next we can remove '. It's safe to assume it doesn't have anything to do with the string.



Remember the replacement with 'L' and 'E' mentioned before? Here it comes into play. There are several {0} and {1} placements throughout the string. Now, we can replace every {0} with L and every {1} with E.





Following the function call of the original Powershell code, put the cleaned up string into a gzip decompressor.



We have some more powershell code, and from first glance there doesn't seem to be anything interesting.



Until we notice a very long Base64 string.



Just copy and paste the string into a Base64 decoder and the flag is captured!
[Forensics] Backdoored Splunk Solution For this challenge, participants are provided with a zip file containing some files used for Splunk Add-on for Windows.



We want to direct our attention to the bin folder since these files were deemed suspicious. By viewing a certain powershell file within bin/powershell/nt6-health.ps1 we can see a web request being made to the provided container on the challenge site.



This web request includes an authorization header which is important to note... If we go to the challenge site we are met with this json message.



Now we can craft a HTTP GET request to the site adding in the authorization header with the value we found from the powershell file.



You can use any tool you want to do this. Personally, I'm using Postman because it's simple and gets the job done, but other tools like Burp Suite should work fine.



Once the request is sent, the server responds back with some Base64 encoded text inside a HTML comment.



Decode the text using your favorite tool, and flag captured!
[Misc.] Rock, Paper, Psychic Solution
Participants are provided with a 7zip archive which contains an exe file. Using DIE we can see it's a 64-bit exe file.





From the entropy analysis page, the binary seems to not be packed, which makes our job a lot easier.



Next open the binary in a dissassembler and begin to look for any interesting strings. I'm using IDA Freeware



Here we can see several interesting strings that have to do with the gameplay along with some interesting Hex and seemingly random string of characters.



Clicking into the Hex string, we see it's referenced in a function called printFlag.



Searching through the function, it will lead you to a fromRC4 function and then to a genKeystream function. Now we have an understanding that the flag is not hardcoded in the binary, rather it's generated at run time. To investigate further, we need a debugger.



Before going further, take note of the address where printFlag is since we can try to trigger it during run time.



Open the binary in a debugger of your choice, I'm using x64dbg



Run through the game, and the instruction will go to a different location in memory. We can exploit this by changing the next instruction to jump to the location of printFlag we saved earlier. Instruction should look similar to jmp 0x4168F0. Your address might be different.



Continue with execution, and if done properly, flag should appear. Well done!
[Misc.] PRESS PLAY ON TAPE Solution
This challenge is one of my personal favorites simply because of how unique it is.

Participants are provided with a wav file named pressplayontape.wav. Playing the audio file reveals an ear piercing sound, and a spectrogram reveals nothing.



This requires us to think a little outside the box... Do a quick search on the filename and this will pop up.



Hmm.... Commodore 64. That's computer related, right? Turns out, Commodore 64 had it's own special audio file format with the extension .tap that could be used to hide messages and data when recorded from VHS tapes. So, now it's time to try to convert this .wav into .tap.



To accomplish this, I'm using C64 Tape Decode. Specifically, wav2tap.exe. Now we have the raw Tape data.



We can now take this data and save it into a tap file. I've named it output.tap.



Here, I'm using a tap analysis tool TapEx to analyze output.tap. Another tool from commodore.software. Going to the View tab we see several sections. The section that looks the most interesting seems to be the CBM DATA FIRST. It contains some hex.



Pop this hex into an online tool and convert to ascii, and there's the flag!
[Forensics] Texas Chainsaw Massacre: Tokyo Drift Solution
This challenge is difficult and therefore one of my favorites.

Participants are provided with a Windows Event Log named Application Logs.evtx.



Viewing this file with the built in Windows Event Viewer, not much can be found from the log alone.



If we take a look at the event log, there is some interesting obfuscated text planted in the file here.



This text is obfuscated powershell code, so we can edit it. Remove the last statement to print it separately and run the script. Now we have our first stage of deobfuscation.



Copy the output and modify it so the first statement is on a separate line. Make additional edits as needed and run, and now we have something that is starting to look like recognizable code. This task is all about trial and error...



You know the drill, copy the output and make modifications as necessary to get it to run properly. However, this time, it's not spitting anything out. Hmm... We need to try something else.



Maybe it's nothing we're doing wrong, the code just isn't supposed to print anything at this stage. So we force it to print output with Write-Output $output. With $output assigned to the code.



Taking the output from the previous run, copy that output and format it so it spits out the result. Your code should look similar to mine, and now we run and get something that looks like pure Powershell code. Now we're close.



Looking closer at the output from the previous run, the code seems to be trying to get some data from the Dns Name eventlog.zip.



We also notice the data taken from the remote server is being decoded from base64 and converted into a readable string. We can force this to run by taking the code out of the try catch block and just running it independently on the next line.



Run that code, and flag is down!

NOTE: This challenge is based off a fairly recent attack which involves hiding shellcode in Windows Event Logs. More info here.
[Malware] Speakfriend Solution
Participants are provided with a 7zip archive named main.7z. Within this archive is a main file which seems to be an ELF file.





We can use Ghidra to dissassemble the file and we can already see some weird Hex in the main function.



Take the hex and convert to Ascii, and you get a string that's reversed. By reversing the string again, we get something that looks meaningful. Take note of this string it will be important later.



If we notice the response from a simple GET request to the container, there is a header User-Agent that looks very similar to the string we just extracted from the main.elf.



Make a GET request to the container again, but this time replace the User-Agent header with the string we extracted from the main file. The response we get is a link to somewhere else on the server.



Make a GET request to the provided url, and you win!
[Malware] RAT Solution
Participants are provided with a 7zip archive named rat.7z. Extracting the archive reveals a file rat.



This file has the "MZ" signature which indicates a PE (Portable Executable). Now it's time to analyze this further within a virtual environment.



Detect It Easy indicates this is a .NET 64 Bit exe file.



It's always good to check entropy to see if it needs unpacking. All indications point to not being packed which makes the job easier.



Here, we can use a .NET dissassembler and debugger dnSpy to take a look inside the code. There's a decrypt function that's worth looking at.



If we place a breakpoint on the return array after decrypting and run, we get an array with the magic bytes signature 4D 5A. This indicates it's decrypting and probably going to try to execute the PE file contained within the array.



We can view the array in a Memory Map and dump the selection to a file to analyze this file further.



Using DIE, this file seems to be a .NET 32 Bit exe.



This file seems to also not be packed.



Using dnSpy again, this file seems to be the actual RAT that will attempt to spin up a reverse shell (Client.Helper probably) along with applying some features in Settings.



Going into Settings, there's more decryption going on that seems to be getting the settings for the stub. This will be important in a little bit...



Scrolling down we notice a string Flag that isn't used by any code present in the file. Copy this string down as we'll use it.



What we can do now is place a breakpoint on the return statement in Decrypt and replace the input with the string we just copied down.



Step over the return statement to view the return value, and nice job! We took down the RAT.
[Malware] Batchfuscation Solution
Participants are provided with a file batchfuscation and upon inspection, it's a batch script.





To print the output of the script into a text file, we can add this code to the top of the file and run it.



The output prints something like this, and we can notice some replacement going on with the alphabet, numbers, and some special characters.



Using the replacement provided, we can craft the word flag. We need to include %% as we'll be searching for it in the script.



Searching for this word, we find several instances. It looks like flag is apart of some sentences here, and looking closer each sentence flag is apart of, starts the same. Let's print this out during runtime and log it.



Copy the entire sentence including flag and replace with echo + (sentence + flag) as shown.



After running the script again, the log will now show the characters of the flag indexed from 1 to 37 (including flag{}). We basically have the flag, now it's just a matter of magic string formatting.



We can narrow the text down to just the flag characters by using a regex expression.



At this point, use whatever methods and tools you want to format the string further to piece together the flag. For example, I just stripped the text down to the (index)=(flag_character) and semi-ordered by the index to make it easier to piece together.

[Forensics] Tragedy Redux


Participants are provided with a 7zip file containing a file tragedy_redux. Upon inspection it has a magic bytes signature that doesn't signify any known file type.



Looking through the contents of the file for clues to it's type, it has a word/vbaProject.bin which means it's likely a word document with macro(s).



The magic bytes signature for word documents is 50 4B 03 04, so we can overwrite the previous incorrect signature. List of file signatures.



Opening the file in Microsoft Word or any software that can handle word documents, reveals a page with the dictionary definition of tragedy.



Checking the macros in the document we see some interesting VB code that's jumping through hoops to decode a string. You can sit and manually step through the code yourself, but I think it's best to run this and see what the output string is.



Now, we can take this code and modify as needed to fit the VB program we're going to run. We can also remove the If ActiveDocument.Name snippet as this isn't going to be triggered in a word document.



Running the code reveals a Base64 encoded string.



Decode the message using a Base64 decoder. Nice work!