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.



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:

- It’s Merchandice & Gift Shop
- Then I searched in Google Lens:


- Picked up the matched one.
- https://ululanis-hawaiian-shave-ice-kihei.wheree.com/
- Searched the place on Google Map : Ululani’s Hawaiian Shave Ice — Kihei
- The provided image’s place was just after that one
- Found Merchandise & Gift shop

- Then got Correct Google Map Angle of Merchandise & Gift shop

- Exact Coordinates: 20.7815417,-156.4626497
- Searched the coordinates in What3Words

- 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
- Enumerate all top-level folders.
- For each folder, read data/img.png.
- Base64-decode the folder name to get numeric index.
- Sort by numeric index ascending.
- 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())

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:
- First I went to the GitHub Repo: https://github.com/Jarpiano/utctf-profile
- In this kind of challenges we first need to see the commit logs
- You can just clone the repo and run:
git log -p | grep -i "utflag{"
- This is the easiest approach I always take
- But here I didn’t need to do even clonning, because the flag was just so easy to get from commit history manually
- I just saw a recent suspicious commit: https://github.com/Jarpiano/utctf-profile/commit/a1546afedb6edeffa9227d70b1f5e110bda9f7e6

- I just saw that history manually and found the flag

- 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:
- I first went to the gist: https://gist.github.com/garvk07/3f9c505068c011e0fd6abd9ddf56aecb
- Found start.txt:
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
- Then got another gist: analysis.py
# 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
- Decoding it found another git: final.txt
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:

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.



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.

- 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’).

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.