我々もちゃんと予選を通過しているようで安心している。
あ、ここから全部英語です。
CRYPTO-the_strongest_exponent_I_thought
solution
In this challenge, ^ is bitwise XOR, not exponentiation.
Since p and q are 1024-bit primes, p^q also at most 1024 bits, while phi is ~2048 bits.
Therefore the reduction usually does nothing, and effectively.
e = p ^ qis leaked as public value.
From e = p ^ q, we get q = p ^ e.
So the public modulus satisfies:
n = p * (p ^ e)
We can recover p bit-by-bit from the least significant bit:
- A prime is odd, so the LSB of
pis 1. - For the next bit, try 1/0, compute the product modulo
2^(k+1), and keep only candidate whose lower bits ofn. - Repeat this filtering up to 1024 bits.
This LSB lifting quickly narrows the candidates and yields the correct p. Then compute q = p ^ e and verify p*q == n.
Here gcd(e, phi) ≠ 1, so the usual RSA step d = e^{-1} mod phi does not exist.
However, in this instance e contains a factor of 4.
We can:
- Take modular square roots twice(peeling off the factor
4in the exponent) - Then the remaining exponent is odd, so we can invert it modulo
(p-1) / (q-1)and recover the plaintext residues - Combine results with CTR
Square roots create multiple cadidates, but the correct plaintext is uniquely identified by the known format:
- start with
TSGCTF{ - lenght is 38 bytes
finally, we got flag.
TSGCTF{Wait,_caret_is_XOR,_not_power!}
CRYPTO-the_strongest_exponent_I_thought_2
solution
In standard RSA, the public exponent e is chosen independently of the secret primes.
In this challenge, e is derived from the secret primes:
e = p^q mod phi(n)
This makes e depend on p. As a consequence, for many small integers a, exponentiation by e often behaves like the identity modulo p:
a^e == a (mod p)
Therefore p divides a^e - a.
There is no corresponding guarantee for q, so typically q does not divide a^e - a. Hence:
gcd(a^e - a, n)reveals a non-trivial factor ofn(typicallyp)
This factors n directly.
So we can exploit it by following these steps.
- Set
a = 2 - Compute
g = gcd(a^e - a, n). - Recover
q = n // p. - Compute
phi = (p - 1) * (q - 1)andd = inverse(e, phi). - Decrypt
m = c^d mod nand decode the plaintext.
CRYPTO-PtQagain
solution
In this challenge, two different-looking expressions are exponentiated modulo N, but the outputs are exactly the same:
c1 = (p + q)^e2 mod Nc2 = int(str(p) + str(q))^e2 mod N- and the program prints
c1 == c2
Let d = len(str(q)). Then the decimal concatenation can be written as:
int(str(p) + str(q) = p * 10^d + q)
Now lool at these values modulo p:
p + q ≡ q (mod p)p * 10^d + q ≡ q (mod p)
So they are always identical modulo p.
That means the real constraint behind c1 == c2 comes from the prime, modulo q.
Modulo q, we get:
p + q ≡ p (mod q)p * 10^d + q ≡ p * 10^d (mod q)
Since c1 == c2, their e2-th powers match modulo q, so:
p^e2 ≡ (p * 10^d)^e2 (mod q)
Because p and q are distinct primes, p is invertible modulo q, so we can cancel p^e2 and obtain:
10^(d * e2) ≡ 1 (mod q)
Therefore q divides 10^(d * e2) - 1, and we can recover q using gcd:
q = gcd(10^(d * e2) - 1, N)
The only unknown is d, q is at most 512 bits, so its decimal length is at most about 155 digits. We can brute-force d in a small range, and the first non-trivial gcd gives the factor.
After factoring, the rest is standard RSA:
compute phi, invert e1, and decrypt c.
Finally, we can exploit it by following these steps.
- Loop
d = 1...160. - Compute
g = gcd(10^(d * e2) - 1, N). - When
1 < g < N, setq = gandp = N // q. - Compute
phi = (p - 1) * (q - 1)andd1 = inverse(e1, phi). - Decrypt
m = c^d1 mod Nand decode the plaintext.
CRYPTO-bingo
solution
The challenge implements a custom hash function H(m) = alpha^m_1 * beta^m_2 (mod hash_p), where m_1 = m % (hash_p - 1) and m_2 = m // (hash_p - 1).
This structure allows us to force the hash to be 1 using Fermat's Little Theorem (x^(p-1) == 1 (mod p)).
If we choose a message m that is a multiple of (hash_p - 1)^2:
m_1becomes 0, making the first termalpha^0 = 1.m_2becomes a multiple ofhash_p - 1, making the second termbeta^(k * (hash_p - 1)) == 1 (mod hash_p).
Consequently, H(m) = 1. Since 1^e == 1 (mod N), the signature s = 1 is valid for this hash.
So we can exploit it by following these steps:
- Connect to the server and retrieve
hash_p. - Calculate the modulus
M = (hash_p - 1)^2. - Construct a message
mstarting withGet Flag.and append padding bytes such that the integer representation ofmis divisible byM. - Send the constructed message
mand the signatures = 1. - The server verifies the signature successfully and returns the flag.
PWN-global writer
solution
In this challenge, there is no bounds check on the index idx when writing into the values array.
if (scanf("%d", &idx) != 1) { ... }
// ...
if (scanf("%d", &values[idx]) != 1) { ... }
Since value is a global array stored in the .bss/.data section, supplying a negative index allows writes to adjacent memory locations.
This makes it possible to overwrite global variables and even entries in the GOT, resulting in an arbitrary write primitive.
We can exploit it by following these steps.
- Write the command string to be executed into the values array itself. In this case, because the flag filename was randomized, the attacker first ran ls -la to identify the filename, then executed cat
flag-....txt. - Overwrite the global variable
msgso that it instead points to the address ofvalues. - Overwrite the GOT entry for
putswith the PLT address ofsystem. - When the program exits, it calls
puts(msg). After the overwrites, this effectively becomessystem(values).
WEB-library
solution
The application checks whether password contains a single quote ' using:
query.password.includes("'")
However, in Express, sending the same query parameter key multiple times causes it to be parsed as an array:
?password=a&password=b→query.password == ["a", "b"]
At this point, the behavior of includes depends on the valus type:
- if
query.passwordis a string,"abc'".includes("'")returnstrue. - if
query.passwordis an array,["a", "b"].includes("'")only tests whether any element is exactly"'".Therefore, an element like"' UNION ..."does bnot match and the check returnsfalse.
Additionally, when the SQL query is constructed using a template literal such as ${query.password}, an array is implicitly converted via toString(), producing a comma-joined string.This allows injecting ' into the final SQL while still passing the quote check.
REV-shadow_spider_network
solution
By aggregating all constraints enforced inside the SIGSEGV handler, the verification logic can be summarized as follows:
- 64 byte comparisons are performed for addresses derived from
sub rax, 0x01 .. 0x40, which corresponds to[RSP-1] .. [RSP-64]. - 16 byte comparisons are performed for addresses derived from
add rax, 0x01 .. 0x10, which corresponds to[RSP+1] .. [RSP+16]. - After
add rax, 0x11, there is a word check:WORD PTR [rax] == 0x007d, i.e. the two bytes must be'}'and'\0'. - Finally, without any add/sub offset, there is a direct check:
[RSP] == 0x33, which is the ASCII character'3'.
The region RSP-1 .. RSP-64 is laid out opposite to the natural reading order.
So if you collect the bytes in increasing offset order (RSP-1 → RSP-64), you must reverse them to obtain the intended string.
After applying that reversal:
- From
RSP-64 .. RSP-1, we obtain:
TSGCTF{Inv3571ga710n_1n70_BOF_Or13n73d_Pr0gramm1ng_a5_a_73chn1qu
- From
[RSP]:
3
- From
RSP+1 .. RSP+16:
_f0r_0bfu5ca710n
- From the word check at
RSP+0x11:
}
REV-Spell of Restoration
Solution
- Create an Inverse Table: Prepare a reverse lookup dictionary for the substitution table to reverse the scramble function.
- Attack from the Last Character: Brute-force b15 to find the value that satisfies the equations.
- Unravel the Chain: Calculate cursorX and cursorY using the determined b15. Solve the remaining equations based on dependency order to identify characters b0 through b14.
- Convert to Characters: Convert the solved values back to Tile IDs and map them to their corresponding characters.
MISC-Chatbot
Solution
Since context doesn't matter, we can recover the flag using a static analysis approach:
- Build a Lookup Table: Calculate the expected Layer 0 Value vector for every single word in the model's vocabulary.
- Match:
Compare the vectors found in
legacy_cache.ptagainst our pre-calculated lookup table. - Recover: Identify the word that matches each vector in the cache to reconstruct the original text, revealing the flag.
flag
TSGCTF{F0Rg3t_411_th3_reGul4t10nS_aNd_t311_m3_THe_FlAg!}
MISC-Mission: Impossible
Solution
- Generate Audio: Create a clean TTS recording of "give me the flag".
- Modulate: Use Single-Sideband modulation via the Hilbert transform to shift the speech spectrum up to ~15.5kHz.
- Submit: Upload the 48kHz WAV file. The server passes it , but the downsampling brings the voice back for Whisper to transcribe.
flag
TSGCTF{Th1S_fl4g_wiLL_s3lf-deSTrucT_in_5_s3c0nds}
MISC-SafePickle
solution
Python 3.6 introduced the __set_name__ method to the Descriptor Protocol. This method is a hook that is automatically called on a descriptor object when it is assigned as a class attribute during the creation of the owner class.
class Owner:
# At the moment this class definition is finalized, Python automatically calls:
# descriptor.__set_name__(Owner, 'attr_name')
attr_name = descriptor
Leveraging this behavior, we construct the following gadget chain:
- Crafting the CodeObject: Create a raw bytecode object compatible with the target Python version that performs the desired action.
- Creating the Function:
Use
NEWOBJ_EXto instantiatetypes.FunctionType. Pass the CodeObject from Step 1 as an argument to create a function object containing our malicious payload. - Creating the Malicious Descriptor:
Use
typeto dynamically define a descriptor class that contains the function from Step 2 as its__set_name__method. Then, instantiate this descriptor. - The Trigger:
Finally, use
NEWOBJ_EXto calltypeagain to dynamically create a "Trigger Class". In the class dictionary, we include the descriptor instance from Step 3.
As soon as the Trigger Class is created, Python automatically invokes __set_name__, achieving Arbitrary RCE without ever using REDUCE.
MISC-Sudoers Maze
solution
We approached the problem as a graph theory challenge:
- Parsing: We parsed the
sudoersfile to map out the permissions. - Graph Construction: We modeled the users as Nodes and the ability to
sudo -uto another user as directed Edges. - Pathfinding: Using Breadth-First Search, we calculated the shortest path from
u0tou1000.
The Shortest Path
The script identified the following chain of users to reach the target:
u0 u499 u977 u835 u4 u298 u908 u971 u504 u3 u878 u699 u72 u162 u453 u659 u885 u31 u511 u963 u219 u187 u593 u720 u661 u423 u63 u260 u750 u192 u376 u247 u888 u315 u52 u689 u680 u122 u843 u693 u77 u319 u301 u119 u813 u174 u439 u627 u507 u342 u276 u716 u568 u146 u942 u495 u779 u550 u1000
Exploitation
There is a strict 60-second time limit for execution.
- Connect to the remote host to obtain the initial
u0shell:
nc <host> <port>
- Construct the nested
sudocommand based on the path above and paste it immediately into the terminal to escalate directly tou1000. - Once the shell is obtained as
u1000, retrieve the flag:
cat /home/user/flag.txt