绕过SSH服务器的端口转发限制

0x00 背景

在某些场景下SSH服务器会禁用掉端口转发的能力,以降低安全风险。这会导致很多依赖SSH端口转发的工具无法正常工作。

这里主要是修改了/etc/ssh/sshd_config文件中以下几项实现的:

  1. #AllowAgentForwarding yes
  2. #AllowTcpForwarding yes
  3. #X11Forwarding yes
COPY

此时,SSH服务器基本就变成了只能执行shell命令的工具,无法用于建立通信通道。

是否有办法可以绕过这一限制呢?答案是肯定的。

0x01 借尸还魂

SSH最常用的能力就是交互式命令行,所谓交互式命令行,就是允许用户进行实时输入,并将输出实时展示出来。也就是说:交互式命令行本身就是一个双向通信的通道。因此,可以编写一个程序,它会在初始化时与指定的服务器端口建立Socket连接,然后将所有stdin读到的数据实时发送给Socket,并将Socket接收到的数据写到stdout中,stderr则用于输出控制信息和日志等。

根据上面的分析,这个程序其实跟telnet命令非常相似,但又不完全相同。因此用GO写了下面这个程序:

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "net"
  6. "os"
  7. "os/signal"
  8. "strconv"
  9. "sync"
  10. "time"
  11. )
  12. func telnet(host string, port int) int {
  13. conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 10*time.Second)
  14. if err != nil {
  15. fmt.Fprintln(os.Stderr, fmt.Sprintf("[FAIL] Connect %s:%d failed: %s", host, port, err))
  16. return -1
  17. }
  18. defer conn.Close()
  19. fmt.Fprintln(os.Stderr, "[OKAY]")
  20. var wg sync.WaitGroup
  21. wg.Add(2)
  22. go handleWrite(conn, &wg)
  23. go handleRead(conn, &wg)
  24. wg.Wait()
  25. return 0
  26. }
  27. func handleRead(conn net.Conn, wg *sync.WaitGroup) int {
  28. defer wg.Add(-2)
  29. reader := bufio.NewReader(conn)
  30. buff := make([]byte, 4096)
  31. for {
  32. var bytes int
  33. var err error
  34. bytes, err = reader.Read(buff)
  35. if err != nil {
  36. fmt.Fprintln(os.Stderr, "Error to read from upstream because of", err)
  37. return -1
  38. }
  39. _, err = os.Stdout.Write(buff[:bytes])
  40. if err != nil {
  41. fmt.Fprintln(os.Stderr, "Error to write to stdout because of", err)
  42. return -1
  43. }
  44. }
  45. }
  46. func handleWrite(conn net.Conn, wg *sync.WaitGroup) int {
  47. defer wg.Add(-2)
  48. reader := bufio.NewReader(os.Stdin)
  49. buff := make([]byte, 4096)
  50. for {
  51. var bytes int
  52. var err error
  53. bytes, err = reader.Read(buff)
  54. if err != nil {
  55. fmt.Fprintln(os.Stderr, "Error to read from stdin because of", err)
  56. return -1
  57. }
  58. _, err = conn.Write(buff[:bytes])
  59. if err != nil {
  60. fmt.Fprintln(os.Stderr, "Error to write to upstream because of", err)
  61. return -1
  62. }
  63. }
  64. }
  65. func main() {
  66. if len(os.Args) < 3 {
  67. fmt.Fprintln(os.Stderr, "Usage: telnet host port")
  68. os.Exit(-1)
  69. }
  70. host := os.Args[1]
  71. port, _ := strconv.Atoi(os.Args[2])
  72. c := make(chan os.Signal, 1)
  73. signal.Notify(c, os.Interrupt)
  74. go func(){
  75. for sig := range c {
  76. // sig is a ^C, handle it
  77. fmt.Fprintln(os.Stderr, "Signal", sig)
  78. os.Exit(0)
  79. }
  80. }()
  81. os.Exit(telnet(host, port))
  82. }
COPY

完整的代码可以参考:telnet-go

0x02 暗度陈仓

要使用telnet-go提供的通信通道,需要与ParamikoASyncSSH之类的SSH库进行集成才行。下面是使用ASyncSSH进行集成的核心逻辑:

  1. class SSHProcessTunnel(SSHTunnel):
  2. """SSH Tunnel Over Process StdIn and StdOut"""
  3. def __init__(self, tunnel, url, address):
  4. super(SSHProcessTunnel, self).__init__(tunnel, url, address)
  5. self._process = None
  6. @classmethod
  7. def has_cache(cls, url):
  8. return False
  9. async def _log_stderr(self):
  10. while not self.closed():
  11. error_line = await self._process.stderr.readline()
  12. error_line = error_line.strip()
  13. utils.logger.warn(
  14. "[%s][stderr] %s" % (self.__class__.__name__, error_line.decode())
  15. )
  16. await asyncio.sleep(0.5)
  17. self._process = None
  18. async def connect(self):
  19. ssh_conn = await self.create_ssh_conn()
  20. if not ssh_conn:
  21. return False
  22. bin_path = self._url.path
  23. cmdline = "%s %s %d" % (bin_path, self._addr, self._port)
  24. self._process = await ssh_conn.create_process(cmdline, encoding=None)
  25. await asyncio.sleep(0.5)
  26. if self._process.exit_status is not None and self._process.exit_status != 0:
  27. utils.logger.error(
  28. "[%s] Create process %s failed: [%d]%s"
  29. % (
  30. self.__class__.__name__,
  31. cmdline,
  32. self._process.exit_status,
  33. await self._process.stderr.read(),
  34. )
  35. )
  36. return False
  37. status_line = await self._process.stderr.readline()
  38. if status_line.startswith(b"[OKAY]"):
  39. utils.safe_ensure_future(self._log_stderr())
  40. return True
  41. elif status_line.startswith(b"[FAIL]"):
  42. utils.logger.warn(
  43. "[%s] Connect %s:%d failed: %s"
  44. % (
  45. self.__class__.__name__,
  46. self._addr,
  47. self._port,
  48. status_line.decode(),
  49. )
  50. )
  51. return False
  52. else:
  53. raise RuntimeError("Unexpected stderr: %s" % status_line.decode())
  54. async def read(self):
  55. if self._process:
  56. buffer = await self._process.stdout.read(4096)
  57. if buffer:
  58. return buffer
  59. raise utils.TunnelClosedError()
  60. async def write(self, buffer):
  61. if self._process:
  62. return self._process.stdin.write(buffer)
  63. else:
  64. raise utils.TunnelClosedError()
  65. def closed(self):
  66. return self._process is None or self._process.exit_status is not None
  67. def close(self):
  68. if self._process:
  69. self._process.stdin.write(b"\x03")
COPY

完整的代码可以参考:turbo-tunnel

turbo-tunnel中可以使用以下方法将流量转发给SSH服务器:

  1. turbo-tunnel -l http://:8080/ -t ssh+process://root:password@1.1.1.1:2222/usr/local/bin/telnet
COPY

/usr/local/bin/telnettelnet-go在服务器上的路径,需要设置好可执行权限。

然后,本地通过http://127.0.0.1:8080代理访问的流量都会转发到ssh服务器上,从而实现了通过ssh服务器进行端口转发的目的。

0x03 总结

利用进程的实时输入输出,可以解决SSH服务器不支持端口转发的问题,从而绕过服务器限制,建立通信通道。这种方式应用场景更广,也更加隐蔽,只是使用上需要提前将一个文件拷贝到SSH服务器上,这里可能少数场景会有些阻碍(例如删除了chmod命令),需要寻找绕过这些限制的方法。

不过总的来说,使用这种方法,大大提升了建立SSH隧道的成功率,具有较大的实际应用价值。

分享
1 comment
Anonymous
Markdown is supported
头像
gongqfcommentedover 2 years ago

博主您好,我在 https://github.com/drunkdream/stpyv8 看到了你的项目,并且 https://pypi.org/project/st-pyv8/#files 下载了您编译windows二进制文件,安装到windows 7 x64环境里后,发现一个问题,不知道如何向您反映,只好在这里留言,请见谅。
这个问题是这样的:
在windows 7 x64 里用python 3.6 3.7 3.8 pip安装stpyv8后,只要在python环境里,import STPyV8 这个包,就算其他什么都不运行,这个python程序就无法正常退出,比如quit() sys.exit()都不行。但是除了不能退出python环境以外,其他整个程序的代码都能正常运行并执行完毕。
在win 10 ltsc 2019 x64 里用python 3.7和3.8都测试过,都能正常退出。
另外请问能否编译一个windows x86 环境的二进制包,因为我的程序所用的大部分还在使用32位的windows系统。
如果能帮忙解决一下这个问题,将万分感谢。我的邮箱是 gongqf@gmail.com ,期望您的回复。