Author: Wyatt

Automated Command Execution via Metasploit’s RPC API

Automated Command Execution via Metasploit’s RPC API

Recently I purchased the Black Hat Go book from No Starch Press. The book has a pretty good overview of using Go for offensive security minded people. In Chapter 3 the book has a section on creating a client for Metasploit’s RPC API. The final code is publicly available on the book’s GitHub repo. Download it to follow along.

Rapid7 provides the documentation for Metasploit’s RPC API here: https://metasploit.help.rapid7.com/docs/rpc-api. All of the API calls that are implemented can be found here: https://metasploit.help.rapid7.com/docs/standard-api-methods-reference

First boot up Metasploit and start the RPC server:

msf5 > load msgrpc Pass=password ServerHost=192.168.0.15
[*] MSGRPC Service:  192.168.0.15:55552
[*] MSGRPC Username: msf
[*] MSGRPC Password: password
[*] Successfully loaded plugin: msgrpc

The code from Black Hat go relies on two environment variables to be set, MSFHOST and MSFPASS.

$ export MSFHOST=192.168.0.15:55552
$ export MSFPASS=password

The existing code will print out each session id and some basic information about each session that currently exists in the running metasploit instance. This isn’t too particularly helpful, especially with the availability of the other API calls.

$ go run main.go
Sessions:
    1  SSH test:pass (127.0.0.1:22)

The first useful case would be loading a list of commands to be run on all sessions and returning the output. For this exercise I’ll make use of the session.shell_read and session.shell_write methods to run commands on the SSH session that I have.

The session.shell_write method has the following structure:

Client:

[ "session.shell_write", "<token>", "SessionID", "id\n" ]

Server:

{ "write_count" => "3" }

In the rpc/msf.go file, two structs can be added to handle this data:

type sessionWriteReq struct {
	_msgpack  struct{} `msgpack:",asArray"`
	Method    string
	Token     string
	SessionID uint32
	Command   string
}

type sessionWriteRes struct {
	WriteCount string `msgpack:"write_count"`
}

It’s worth noting that the command needs to have a newline delimiter included in the message. I tested out a few inputs and found that consecutive commands didn’t work. Ex: “id;whoami;hostname”. Only the first command would be run.

The following method can be added to rpc/msf.go to write a command to a particular session:

func (msf *Metasploit) SessionWrite(session uint32, command string) error {
	ctx := &sessionWriteReq{
		Method:    "session.shell_write",
		Token:     msf.token,
		SessionID: session,
		Command:   command,
	}

	var res sessionWriteRes
	if err := msf.send(ctx, &res); err != nil {
		return err
	}

	return nil
}

The function doesn’t return anything other than errors as the write_count isn’t helpful to us. A method call can be added to the client/main.go file to execute commands.

msf.SessionWrite(session.ID, "id\n")

This executes commands, but prevents us from seeing the results. The next step is implementing the session.shell_read method so that we can return the results.

The session.shell_read method has the following structure:

Client:

[ "session.shell_read", "<token>", "SessionID", "ReadPointer ]

Server:

{
"seq" => "32",
"data" => "uid=0(root) gid=0(root)…"
}

Similarly to the write operation, two structs for reading the results can be used:

type sessionReadReq struct {
	_msgpack    struct{} `msgpack:",asArray"`
	Method      string
	Token       string
	SessionID   uint32
	ReadPointer string
}

type sessionReadRes struct {
	Seq  uint32 `msgpack:"seq"`
	Data string `msgpack:"data"`
}

The ReadPointer is interesting as it allows for us to maintain state. Rapid7 encourages this behavior as it allows for collaboration. We will need to determine how to obtain the current ReadPointer before writing data to ensure only my client’s output is returned. For now let’s stick with a value of 0 to ensure we capture all output. Add the following method:

func (msf *Metasploit) SessionRead(session uint32, readPointer uint32) (string, error) {
	ctx := &sessionReadReq{
		Method:      "session.shell_read",
		Token:       msf.token,
		SessionID:   session,
		ReadPointer: string(readPointer),
	}

	var res sessionReadRes
	if err := msf.send(ctx, &res); err != nil {
		return "", err
	}

	return res.Data, nil
}

A small addition to client/main.go can be made to read all data from the session:

data, err := msf.SessionRead(session.ID, 0)
if err != nil {
	log.Panicln(err)
}
fmt.Printf("%s\n", data)

Running the new code gives the following

$ go run main.go
Sessions:
    1  SSH test:pass (127.0.0.1:22)
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
[snip]
test:x:1001:1001:Mista Test,,,:/home/test:/bin/bash
uid=1001(test) gid=1001(test) groups=1001(test)

Woah. I didn’t list out /etc/passwd! Looks like the results are spitting out more than the “id” command that was specified. It’s time to figure out how to get the latest ReadPointer instead of 0.

Digging through the other methods:

The session.ring_last method will return the last issued ReadPointer (sequence number) for the specified Shell session.

https://metasploit.help.rapid7.com/docs/standard-api-methods-reference#section-session-ring-last

Perfect! Let’s add two additional structs to manage the request and response:

type sessionRingLastReq struct {
	_msgpack    struct{} `msgpack:",asArray"`
	Method      string
	Token       string
	SessionID   uint32
}

type sessionRingLastRes struct {
	Seq  uint32 `msgpack:"seq"`
}

The structs should all look very similar since the requests and responses are nearly identical.

First off let’s send a request to connect and get the last sequence number for our ReadPointer. I’ll create a SessionReadPointer method to obtain this value:

func (msf *Metasploit) SessionReadPointer(session uint32) (uint32, error) {
	ctx := &sessionRingLastReq{
		Method:    "session.ring_last",
		Token:     msf.token,
		SessionID: session,
	}

	var sesRingLast sessionRingLastRes
	if err := msf.send(ctx, &sesRingLast); err != nil {
		return 0, err
	}

	return sesRingLast.Seq, nil
}

In my client/main.go code I can add a call to this function prior to writing a command and then update the read call to use this returned value.

readPointer, err := msf.SessionReadPointer(session.ID)
if err != nil {
	log.Panicln(err)
}
msf.SessionWrite(session.ID, "id\n")
data, err := msf.SessionRead(session.ID, readPointer)
if err != nil {
	log.Panicln(err)
}
fmt.Printf("%s\n", data)

I can then go ahead and update the code:

$ go run main.go
Sessions:
    1  SSH test:pass (127.0.0.1:22)
uid=1001(test) gid=1001(test) groups=1001(test)

Awesome. Only the results of the command specified will be printed out. How can I expand on this to automate running scripts on each session?

For this second part I will ingest a file that has as many commands as I wish to run that are separated by new line characters.

An example would be:

whoami
date
id
hostname

I’ll transfer the code from the client/main.go into the rpc/msf.go file to make it reusable:

func (msf *Metasploit) SessionExecute(session uint32, command string) (string, error) {
	readPointer, err := msf.SessionReadPointer(session)
	if err != nil {
		return "", err
	}
	msf.SessionWrite(session, command)
	data, err := msf.SessionRead(session, readPointer)
	if err != nil {
		return "", err
	}
	return data, nil
}

The next step is reading the file into a slice. I went ahead and used the bufio package to scan the file line by line. I added the following underneath the variable declarations to my client/main.go file.

commands := []string{}
if len(os.Args) == 2 {
	file, err := os.Open(os.Args[1])
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		commands = append(commands, scanner.Text())
	}
}

The contents of the file specified in the first argument will be read into the commands slice. Printing out the contents of commands provides:

[whoami date id hostname]

In the rpc/msf.go file I added a new function to wrap around SessionExecute. The bufio scanner removed the newline character from each line, so this helper method can add it back and reuse the SessionExecute method as many times as needed. The results are returned on an error or once all the commands are done.

func (msf *Metasploit) SessionExecuteList(session uint32, commands []string) (string, error) {
	var results string
	for _, command := range commands {
		tCommand  := fmt.Sprintf("%s\n", command)
		result, err := msf.SessionExecute(session, tCommand)
		if err != nil {
			return results, err
		}
		results += result
	}

	return results, nil
}

Finally within the client/main.go file I added a check to see if the commands variable has any commands to run on each session. If it does, we can call msf.SessionExecuteList and print out the results.

if len(commands) > 0 {
	data, _ := msf.SessionExecuteList(session.ID, commands)
	fmt.Printf("%s", data)
}

Running the code gives the following:

go run main.go commands.txt
Sessions:
    1  SSH test:pass (127.0.0.1:22)
zepher
test
Mon Apr 27 16:20:51 CDT 2020
uid=1001(test) gid=1001(test) groups=1001(test)

The output could be cleaned up a bit especially with multiple sessions. Perhaps the command output along with more of the session metadata could be put into JSON for easy parsing.

The proof of concept is powerful. It allows for command execution in a collaborative environment that scales well. Overall the API provides an opportunity to automate some of the manual tasks that are restricted to msfconsole. I recommend playing around with some of the other API calls and taking a look at Black Hat Go.

The final code can be found on my Github repo: https://github.com/wdahlenburg/msf-rpc-client

Post Exploitation through Git with SSH Keys

Post Exploitation through Git with SSH Keys

Git is a version control system that allows content to be shared and modified. It is popularized by GitHub, however many other companies have their own Git server. These servers can provide a wealth of information during engagements. It can be helpful for research on a company by providing a list of developers that are contributing to a company repository. This information can be put into popular tools such as Gitrob or truffleHog and leak potential secrets that could compromise a company. For companies that run their own GitHub Enterprise server, their rules on passwords in GitHub may be more lax.

There hasn’t been much discussion into post-exploitation through GitHub. This is likely due to many security professionals not knowing how to use git or just running out of time on engagements. Diving into post-exploitation with GitHub is an excellent way to steal private repositories and impact production code.

In a linux environment, SSH keys are normally stored in the ~/.ssh/ folder. They come in public/private key pairs. A developer can use these keys to authenticate to GitHub over SSH. This allows them to read and write content. GitHub allows for repositories of code to be stored with public or private permissions.

GitHub allows many keys to be stored for a user account. This is helpful because it allows users to create different keys on their different computers. For an attacker, the scope is increased every time a new key is added to GitHub. An attacker only has to compromise one of those keys to gain persistent access to GitHub.

A Metasploit auxiliary module was written to quickly enumerate local SSH keys and test their access to GitHub. Other modules can be used to scrape keys from a host. A compromised user will only have access to their keys unless they can make a lateral escalation. A root user that is compromised will have access to all keys on the host. This metasploit module speeds up the checking and can be used with Github and GitLab. It can easily be extended to other servers as well.

Currently available in Metasploit

Once this private key is obtained, it can be used with a simple config file:

$ cat ~/.ssh/config
Host github
HostName github.com
User git
IdentityFile /home/wy/.ssh/id_ed25519

Access can be tested through:

$ssh -T git@github

If successful, the response will tell who the user is.

The first option for post-exploitation is to read private repositories. GitHub does not provide an easy way to view these without a Personal Access Token, so guessing/bruteforcing will have to be done. Other git servers may allow querying to private repos such as Gitolite (https://bytefreaks.net/gnulinux/bash/how-to-list-all-available-repositories-on-a-git-server-via-ssh). In organizations that deploy production creds through layering on private repos, this could be an easy task.

Example:

  • My-repo-1 is the code base
  • My-repo-1-shadow contains the creds that are layered on top of My-repo-1

By appending -shadow to the known public repos, a user with a stolen ssh key could download the sensitive credentials.

If there aren’t any patterns, a suggestion would be to look for references to other projects and create a wordlist. A for loop trying to clone each repo for the user could lead to success.

Reading sensitive repos is a great win, but it limits what can be done. Writing to a repo brings on endless opportunities to backdoor code. How often do developers trust what’s already been committed? Most start their day off doing a git pull on a repo to make sure they are up to date. An attacker could modify code and put a backdoor into the master branch. The next developer that runs it could grant a reverse shell to the attacker. If build processes are weak, this code could be automatically pushed into production.

SSH Keys can provide a method of persistence in an environment. They are commonly used for access to servers, but the extended trust to servers like GitHub allows for an attacker to maintain access in an organization. Revoking the keys from the compromised server will still allow an attacker to use the keys to access GitHub. An attacker that has access to the Git server can add their backdoor and wait until they are let back in.

How can this be avoided?

SSH keys can be configured to use passphrases. Most people don’t use them. It is highly recommended to enforce passphrases on SSH keys.

Passphrases can even be added to existing private keys:

$ ssh-keygen -p -f ~/.ssh/id_rsa

An attacker will have to crack the password on the key to be able to use it. This isn’t a failsafe, but provides some defense in depth for an already compromised host.