Rating:

SECCON CTF Quals - 2019

Pwn / 332 - lazy

lazy.chal.seccon.jp 33333

solution

By @jaidTw

Credits to @HexRabbit

This challenge didn't give us any file. Connect to the challenge first.

1: Public contents
2: Login
3: Exit

Select option 1

Welcome to public directory
You can download contents in this directory
diary_4.txt
diary_3.txt
diary_1.txt
login_source.c
diary_2.txt

I can download login_source.c, there were only soure code of functions login and input.

#define BUFFER_LENGTH 32
#define PASSWORD "XXXXXXXXXX"
#define USERNAME "XXXXXXXX"

int login(void){
        char username[BUFFER_LENGTH];
        char password[BUFFER_LENGTH];
        char input_username[BUFFER_LENGTH];
        char input_password[BUFFER_LENGTH];

        memset(username,0x0,BUFFER_LENGTH);
        memset(password,0x0,BUFFER_LENGTH);
        memset(input_username,0x0,BUFFER_LENGTH);
        memset(input_password,0x0,BUFFER_LENGTH);

        strcpy(username,USERNAME);
        strcpy(password,PASSWORD);

        printf("username : ");
        input(input_username);
        printf("Welcome, %s\n",input_username);

        printf("password : ");
        input(input_password);


        if(strncmp(username,input_username,strlen(USERNAME)) != 0){
                puts("Invalid username");
                return 0;
        }

        if(strncmp(password,input_password,strlen(PASSWORD)) != 0){
                puts("Invalid password");
                return 0;
        }

        return 1;
}


void input(char *buf){
        int recv;
        int i = 0;
        while(1){
                recv = (int)read(STDIN_FILENO,&buf[i],1);
                if(recv == -1){
                        puts("ERROR!");
                        exit(-1);
                }
                if(buf[i] == '\n'){
                        return;
                }
                i++;
        }
}

Apparently, there's a out-of-bound read inside input(), it won't stop reading until a '\n' is encountered. Because \x00 won't stop the read, in input(input_password), we can overwrite username, input_username, password, input_password into same strings to pass the check.

There was a new option after successfully logged in.

Logged in!
1: Public contents
2: Login
3: Exit
4: Manage

I could download the binary lazy using option 4, but . is not allowed in the input by the program, so I couldn't download libc.so.6.

Welcome to private directory
You can download contents in this directory, but you can't download contents with a dot in the name
lazy
libc.so.6
Input file name

After used the script to get lazy, I started to reverse it.

First, in login(), here were the username and password, so just use them to login in the following expoits.

strcpy(username, "_H4CK3R_");
strcpy(password, "3XPL01717");
  • In option 1 public(): Switch the directory to ./q/public, but input use fgets(), no overflow here.
unsigned __int64 public() {
  char *HOME; // rax
  char s[24]; // [rsp+0h] [rbp-20h]
  if ( chdir("./q/public") == -1 ) {
    ...
  }
  puts("Welcome to public directory");
  puts("You can download contents in this directory");
  listing();
  fgets(s, 20, stdin);
  download(s);
  HOME = getenv("HOME");
  if ( chdir(HOME) == -1 ) {
    ...
  }
}
  • In option 4 filter(): Switch the directory to ./q/private. Input used input() here, so a stack overflow, followed by a format string vulnerability.
void __fastcall filter()
{
  char *HOME; // rax
  char s[24]; // [rsp+0h] [rbp-20h]
  
  ...
  if ( chdir("./q/private") == -1 ) {
    ...
  }
  puts("Welcome to private directory");
  puts("You can download contents in this directory, but you can't download contents with a dot in the name");
  listing();
  puts("Input file name");
  input(s);
  if ( strchr(s, '.') ) {
     exit(-1);
  }
  printf("Filename : ");
  printf(s);
  puts("OK! Downloading...");
  download(s);
  HOME = getenv("HOME");
  if ( chdir(HOME) == -1 ) {
    exit(-1);
  }
}

Stack canary is on, so we need to leak the canary by format string first, then do the ROP.

Because the . check of the path is perform outside of download(), so I used ROP to first call input(), getting the filename wrote to a buffer, then call download() with the buffer to download anyfile. But soon, we found that when trying to download libc.so.6, we always got an unexpected EOF and terminated, so the libc was incomplete(~4MB/10MB), I tried many tools such as objdump, nm and one_gadget, but none of them worked with a incomplete libc.

Later, I tried another approach: control the directory using chdir(), then call listing() to traverse and search for the flag, then use download() to get the flag. This looked promising and I successfully read the directory.

run.sh
lazy
ld.so
cat
.profile
libc.so.6
810a0afb2c69f8864ee65f0bdca999d7_FLAG
.bashrc
q
.bash_logout

Unfortunately, the filename of 810a0afb2c69f8864ee65f0bdca999d7_FLAG exceeded the limit of download() (>27), I could only try to use open(), read(), puts() to read the file (There's no a rdx gadget, but you can assign rdx an appropriate value for read() by calling strlen()).

Now, I could read most of the files, including those under /proc, however, I still couldn't read the flag, and I found there was a cat in the same directory, so I guessed the flag should be read by the cat with setuid. This means the only ways to get the flag were to invoke the shell or use execve to run the setuid cat, which I thought were impossible without using libc.

When I was almost giving it up, my teammate @HexRabbit found that IDA Pro can parse the incomplete libc.so.6, so we only need to leak __libc_start_main from stack, calculate the address of system, then use ROP to read sh and jump to system, and that's it.

Here's the script

$ ./cat 810a0afb2c69f8864ee65f0bdca999d7_FLAG
SECCON{Keep_Going!_KEEP_GOING!_K33P_G01NG!}
Original writeup (https://github.com/10secTW/ctf-writeup/blob/master/2019/SECCON%20CTF%20quals/lazy/README_en.md).