UTCTF 2026 Writeups | InferiorAK

These are some of my Writeups from my recent CTF, UTCTF 2026 where I shared my thoughts and approaches on how I overcame the given problems.

My Solves x All solved by my Team Integrated Hawkers

I also participated in last year’s UTCTF, UTCTF 2025. It was really exciting and so now. Here are my writeups of this year listed below:

Misc:

  • W3W2
  • QRecreate
  • Jail Break
  • Insanity Check: Hat Trick Denied
  • Double Check
  • Breadcrumbs

Forensics:

  • Half Awake
  • Last Byte Standing

Past year’s OSINT Writeups: https://medium.com/@inferiorak/three-words-all-osint-challenge-utctf-2025-7b1ef89c8cbf

So let’s just begin with the Misc.

Misc — W3W2 (OSINT Challenge)

Description:

W3W2
Points: 919
1 (100% liked) 0
The three words I would use to describe this location are...

Flag format: utflag{word1.word2.word3}

By Caleb (@eden.caleb.a on discord)

Solution:

  • Given Image:
W3W2.jpg
  • It’s Merchandice & Gift Shop
  • Then I searched in Google Lens:
1. Google Lens Search
2. Google Map Matching Objects
3. Google Map Similar Objects with similar Angle
  • Exact Coordinates: 20.7815417,-156.4626497
  • Searched the coordinates in What3Words
4. What3Words Block Matched
  • Final Flag:
utflag{bystander.boulders.pillowcase}

Misc — QRecreate

Description:

QRecreate
744
4 (100% liked) 0
I managed to bypass the IPS to exfiltrate the secrets you wanted from the target's intranet. I just hope you remember the encoding structure we agreed on. by Emmett (@emdawg25 on discord)

Solution:

  • Goal: Reconstruct the final QR code from many image chunks, then decode the hidden message to recover the flag.

Provided Files / Structure

The challenge directory contains many folders with base64-like names:

  • MDAx/, MDAy/, MDAz/, …, MTAw/
  • each folder contains: data/img.png

So there are 100 chunk images total.

Key Observation

The folder names are Base64-encoded indices.

Examples:

  • MDAx -> 001 -> 1
  • MDEw -> 010 -> 10
  • MTAw -> 100 -> 100

Decoding and sorting these indices gives the intended sequence order of chunks from 1 to 100.

Reconstruction Logic

  1. Enumerate all top-level folders.
  2. For each folder, read data/img.png.
  3. Base64-decode the folder name to get numeric index.
  4. Sort by numeric index ascending.
  5. Verify chunk dimensions:
  • each tile is 74×74
  • total chunks = 100, which is a perfect square

6. Place chunks row-major into a 10×10 grid.

7. Save final stitched image as reconstructed_qr.png.

Resulting image size:

  • 10 * 74 = 740
  • final QR canvas: 740×740

Python Script (Reconstruction + Payload Decode)

#!/usr/bin/env python3
import os
import base64
import math
from PIL import Image

ROOT = "."
OUT = "reconstructed_qr.png"

chunks = []
for d in os.listdir(ROOT):
img_path = os.path.join(ROOT, d, "data", "img.png")
if os.path.isfile(img_path):
try:
idx = int(base64.b64decode(d).decode())
chunks.append((idx, img_path))
except Exception:
# Skip anything that is not a valid base64 index folder
pass

chunks.sort(key=lambda x: x[0])
n = len(chunks)
side = int(math.isqrt(n))
assert side * side == n, f"Chunk count {n} is not a perfect square"

imgs = [Image.open(path).convert("RGB") for _, path in chunks]
tile_w, tile_h = imgs[0].size

canvas = Image.new("RGB", (side * tile_w, side * tile_h), (255, 255, 255))

for i, tile in enumerate(imgs):
r, c = divmod(i, side) # row-major order
canvas.paste(tile, (c * tile_w, r * tile_h))

canvas.save(OUT)
print(f"[+] saved {OUT} ({canvas.size[0]}x{canvas.size[1]})")

# Payload found from reconstructed QR:
payload_b64 = "dXRmbGFne3MzY3IzdHNfQHJlX0Bsd0B5c193MXRoMW5fczNjcjN0c30"
print("[+] payload:", payload_b64)
print("[+] decoded:", base64.b64decode(payload_b64).decode())
Merged QRcode

Decoding the Reconstructed QR

  • From the rebuilt QR, the extracted data is:
dXRmbGFne3MzY3IzdHNfQHJlX0Bsd0B5c193MXRoMW5fczNjcjN0c30
  • Base64-decoding that yields:
utflag{s3cr3ts_@re_@lw@ys_w1th1n_s3cr3ts}

Notes:

  • The phrase in the description, “remember the encoding structure we agreed on”, hints at using encoded folder names as ordering metadata.
  • A common pitfall is sorting folders lexicographically; decoding to numeric index is the safe method.

Misc — Jail Break

Description:

Jail Break
410
5 (45% liked) 6
We've built the world's most secure Python sandbox. Nothing can escape. Probably. Hopefully. Run it locally: python3 jail.py By Garv (@GarvK07 on discord)

Solution:

Given jail.py:

import sys

_ENC = [0x37, 0x36, 0x24, 0x2e, 0x23, 0x25, 0x39, 0x32, 0x3b, 0x1d, 0x28, 0x23, 0x73, 0x2e, 0x1d, 0x71, 0x31, 0x21, 0x76, 0x32, 0x71, 0x1d, 0x2f, 0x76, 0x31, 0x36, 0x71, 0x30, 0x3f]
_KEY = 0x42

def _secret():
return ''.join(chr(b ^ _KEY) for b in _ENC)

BANNED = [
"import", "os", "sys", "system", "eval",
"open", "read", "write", "subprocess", "pty",
"popen", "secret", "_enc", "_key"
]

SAFE_BUILTINS = {
"print": print,
"input": input,
"len": len,
"str": str,
"int": int,
"chr": chr,
"ord": ord,
"range": range,
"type": type,
"dir": dir,
"vars": vars,
"getattr": getattr,
"setattr": setattr,
"hasattr": hasattr,
"isinstance": isinstance,
"enumerate": enumerate,
"zip": zip,
"map": map,
"filter": filter,
"list": list,
"dict": dict,
"tuple": tuple,
"set": set,
"bool": bool,
"bytes": bytes,
"hex": hex,
"oct": oct,
"bin": bin,
"abs": abs,
"min": min,
"max": max,
"sum": sum,
"sorted": sorted,
"reversed": reversed,
"repr": repr,
"hash": hash,
"id": id,
"callable": callable,
"iter": iter,
"next": next,
"object": object,
}

# _secret is in globals but not documented - players must find it
GLOBALS = {"__builtins__": SAFE_BUILTINS, "_secret": _secret}

print("=" * 50)
print(" Welcome to PyJail v1.0")
print(" Escape to get the flag!")
print("=" * 50)
print()

while True:
try:
code = input(">>> ")
except EOFError:
break

blocked = False
for word in BANNED:
if word.lower() in code.lower():
print(f" [BLOCKED] Nice try!")
blocked = True
break

if blocked:
continue

try:
exec(compile(code, "<jail>", "exec"), GLOBALS)
except Exception as e:
print(f" [ERROR] {e}")
  • It’s just simply XORed, we don’t need to do anything beacause the source code is already given.
  • Maybe it wasn’t the Itentional, a netcat server should have been provided instead this. Btw….
  • Here is the reversing XOR scriptto get the flag:
_ENC = [0x37, 0x36, 0x24, 0x2e, 0x23, 0x25, 0x39, 0x32, 0x3b, 0x1d, 0x28, 0x23, 0x73, 0x2e, 0x1d, 0x71, 0x31, 0x21, 0x76, 0x32, 0x71, 0x1d, 0x2f, 0x76, 0x31, 0x36, 0x71, 0x30, 0x3f]
_KEY = 0x42

def _secret():
return ''.join(chr(b ^ _KEY) for b in _ENC)

flag = _secret()
print(flag)
  • Final Flag:
utflag{py_ja1l_3sc4p3_m4st3r}

Misc — Insanity Check: Hat Trick Denied

Description:

Insanity Check: Hat Trick Denied
936
2 (100% liked) 0

After a gap year, the sequel to "Insanity Check: Redux" and "Insanity Check: Reimagined" is finally here!
The flag is in CTFd, but, as always, you'll have to work for it.

(This challenge does not require any brute-force -- as per the rules of the competition, brute-force tools like dirbuster are not allowed, there is a clear solution path without it if you know where to look.)

By Caleb (@eden.caleb.a on discord)

Solution:

Initial Findings:

User-agent: *
Disallow: /admin
Disallow: /2065467898
Disallow: /3037802467
  • Visiting https://utctf.live/2065467898 returned:
[REDACTED HTML]

<h1>File not found</h1>
<!-- 2, 7, 9, 7, 8, 13, 17, 39, 85, 4, 57, 4, 93, 30, 104, 27, 44, 23, 89, 8, 30, 68, 107, 112, 54, 0, 30, 11, 2, 92, 66, 23, 31 -->

[REDACTED HTML]
  • Visiting https://utctf.live/3037802467 returned:
[REDACTED HTML]

<h1>File not found</h1>
<!-- 119, 115, 111, 107, 105, 106, 106, 110, 114, 105, 102, 106, 50, 106, 55, 122, 115, 101, 54, 106, 113, 48, 52, 57, 105, 112, 108, 100, 111, 53, 49, 114, 98 -->

[REDACTED HTML]

Solving Idea:

  • Both comments are equal-length integer arrays.
    Try XOR pairwise: chr(a[i] ^ b[i]).
  • Solve Script:
a = [2, 7, 9, 7, 8, 13, 17, 39, 85, 4, 57, 4, 93, 30, 104, 27, 44, 23, 89, 8, 30, 68, 107, 112, 54, 0, 30, 11, 2, 92, 66, 23, 31]
b = [119, 115, 111, 107, 105, 106, 106, 110, 114, 105, 102, 106, 50, 106, 55, 122, 115, 101, 54, 106, 113, 48, 52, 57, 105, 112, 108, 100, 111, 53, 49, 114, 98]

flag = ''.join(chr(x ^ y) for x, y in zip(a, b))
print(flag)
  • Final Flag:
utflag{I'm_not_a_robot_I_promise}

Misc — Double Check

Description:

Double Check
100
14 (100% liked) 0
We're planning on deploying some new static sites for our officers. We've cloned a template from Hugo's Static Site Generator (SSG). Can you make sure that our website is clean before it's deployed?

https://github.com/Jarpiano/utctf-profile

By Jared (@jarpiano on discord)

Solution:

git log -p | grep -i "utflag{"
1. Very recent interesting commit before the CTF
  • I just saw that history manually and found the flag
2. Flag found
  • Final Flag:
utflag{n07h1n6_70_h1d3}

Misc — Breadcrumbs

Description:

Breadcrumbs
100
40 (100% liked) 0
Every trail has a beginning. This one starts here: https://gist.github.com/garvk07/3f9c505068c011e0fd6abd9ddf56aecb Follow the breadcrumbs. The flag is at the end.

By Garv (@GarvK07 on discord)

Solution:

You've found the first breadcrumb. The next step is closer than you think.

aHR0cHM6Ly9naXN0LmdpdGh1Yi5jb20vZ2FydmswNy9iYTQwNjQ2MGYyZTkzMmI1NDk2Y2EyNTk3N2JlMjViZQ==
  • Decoded it from base64, and found another gist: poem.txt
Gather your wits, the path winds on from here,
In shadows deep, the truth is never clear,
Secrets hide where few would dare to look,
Three letters follow, open up the book.

p.s. https://gist.github.com/garvk07/963e70be662ea81e96e4e63553038d1a
# A curious little script...
# Nothing to see here.
# 68747470733a2f2f676973742e6769746875622e636f6d2f676172766b30372f3564356566383539663533306333643539336134613363373538306432663239
# Move along.

def analyse(data):
return data[::-1]

results = analyse("dead beef")
print(results)
  • I just took the Hex encoded string:
68747470733a2f2f676973742e6769746875622e636f6d2f676172766b30372f3564356566383539663533306333643539336134613363373538306432663239
You've reached the end of the trail. Your reward:
hgsynt{s0yy0j1at_gu3_pe4jy_ge41y}
  • Decoded the flag like string from ROT13
  • Finally I got the Flag:
utflag{f0ll0w1ng_th3_cr4wl_tr41l}

Forensics — Half Awake

Description:

Half Awake
Points: 525
5 (100% liked) 0
Our SOC captured suspicious traffic from a lab VM right before dawn. Most packets look like ordinary client chatter, but a few are pretending to be something they are not.

Solution:

Solution Summary:

  • The PCAP hides a fake “TLS-like” stream that actually contains a ZIP (PK) payload.
  • Inside the zip there is stage2.bin and a hint text.
  • stage2.bin is XOR-related to another string with key 0xb7, and merging printable characters from paired strings yields the flag.

Step 1: Read the HTTP instructions

  • From packet stream (tcp.stream eq 0), the response to /instructions.hello says:
1. instructions
Read this slowly:
1) mDNS names are hints: alert.chunk, chef.decode, key.version
2) Not every 'TCP blob' is really what it pretends to be
3) If you find a payload that starts with PK, treat it as a file

Step 2: Check mDNS for key material

  • Filter on mDNS traffic and inspect key.version.local TXT response.
  • The response contains 00b7, i.e. XOR key 0xb7.
2. mDNS Records
3. XOR Key
4. Looking for Encrypted Traffics after thr TXT Record

Step 3: Find the fake protocol stream and carve the ZIP

  • Follow suspicious TCP stream around port 443 (tcp.stream eq 4 in the screenshot).
  • Even though packets are labeled TLS, the reassembled payload contains PK… and file names (stage2.bin, readme.txt), confirming ZIP content.
5. Got Zip File with PK Header
  • Then I dumped the Request as Hex and decoded it as the Fixed ZIP.
  • Saved data as flag.zip, then:
unzip -l flag.zip

Contents:

  • stage2.bin (41 bytes)
  • readme.txt (hint: “not everything here is encrypted the same way”)

Step 4: Decode stage2 and merge character positions

  • stage2.bin (latin1) gives:
75 c3 66 db 61 d0 7b df 34 db 66 e8 61 c0 34 dc 33 e8 73 84 33 e8 74 df 33 e8 70 c5 30 c3 30 d4 30 db 5f c3 72 86 63 dc 7d

Due to some unprintable chars I gave Hex encoded

  • XOR with 0xb7 gives:
c2 74 d1 6c d6 67 cc 68 83 6c d1 5f d6 77 83 6b 84 5f c4 33 84 5f c3 68 84 5f c7 72 87 74 87 63 87 6c e8 74 c5 31 d4 6b ca
  • If you see carefully, it will make sense:
stage2 : u_f_a_{...
xored : _t_l_g...
merged : utflag{...
  • Decoding flag using this:
import string

p1 = bytes.fromhex("75 c3 66 db 61 d0 7b df 34 db 66 e8 61 c0 34 dc 33 e8 73 84 33 e8 74 df 33 e8 70 c5 30 c3 30 d4 30 db 5f c3 72 86 63 dc 7d")
p2 = bytes.fromhex("c2 74 d1 6c d6 67 cc 68 83 6c d1 5f d6 77 83 6b 84 5f c4 33 84 5f c3 68 84 5f c7 72 87 74 87 63 87 6c e8 74 c5 31 d4 6b ca")
mixed = list(zip(p1, p2))

allowed = string.printable
flag = ""
for pair in mixed:
x, y = pair[0], pair[1]
flag += chr(x) if chr(x) in allowed else chr(y)

print(flag)
  • Final Flag:
utflag{h4lf_aw4k3_s33_th3_pr0t0c0l_tr1ck}

Forensics — Last Byte Standing

Description:

Last Byte Standing
673
1 (100% liked) 0
A midnight network capture from a remote office was marked “routine” and archived without review. Hours later, incident response flagged it for one subtle anomaly that nobody could explain. Find what was missed and recover the flag.

Solution:

  • The anomaly was subtle — each sync-cache.nexthop-lab.net DNS query had one extra byte appended after the standard DNS question section.
  • In a normal DNS query, the packet ends with:
<domain name> 00 0001 0001

(null terminator, Type A, Class IN)

  • But here, each query had one more byte after that — either 0x30 (‘0’) or 0x31 (‘1’).
1. The Hidden Channel: DNS Packet Padding

The Decode

440 packets × 1 bit each = 440 bits (55 bytes)
  • Each last byte was 0x30 or 0x31 → a binary bitstream, decoded 8 bits at a time (MSB-first):
0111 0101 → 0x75 → 'u'
0111 0100 → 0x74 → 't'
0110 0110 → 0x66 → 'f'
0110 1100 → 0x6C → 'l'
...
  • Final Flag:
utflag{d1g_t0_th3_l4st_byt3}

Keynote: Title Meaning

“Last Byte Standing” — The single extra byte hiding at the end of each DNS packet, after all the standard DNS structure, invisible to any tool that just looks at DNS fields normally.

So then, here I am done with my writeups and I will try to add my other writeups and the approaches that I progressed and how I think.
Thanks.

My Contacts:

LinkedIn: https://www.linkedin.com/in/taseen-kpc/
GitHub: https://github.com/InferiorAK
Medium: https://medium.com/@inferiorak
YouTube: https://www.youtube.com/@inferiorak
Facebook: https://www.facebook.com/InferiorAK
Twitter: https://x.com/inferiorak
E-mail: [email protected]

My Team Links:

LinkedIn:https://www.linkedin.com/company/integratedhawkers
Facebook:
https://www.facebook.com/IntegratedHawkers
Website:
https://integratedhawkers.com/
GitHub:
https://github.com/IntegratedHawkers
CTFtime:
https://ctftime.org/team/299872
E-mail: [email protected]


UTCTF 2026 Writeups | InferiorAK was originally published in OSINT Team on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

❤️ Help Fight Human Trafficking
Support Larry Cameron's mission — 20,000+ victims rescued