0x00 前言
太久没有整活了可以说是十分懈怠了。现在整活的门槛也越来越高,去年群友还只是每天rce就很开心了,现在不整几个核弹都不叫漏洞了,群友日益增涨的多巴胺阈值使我十分的焦虑。虽说之前整了个洲际导弹般的漏洞但那也是半个多月以前的事情了,我也很难接受一直停滞不前的自己。我觉得搞技术挣不挣钱无所谓,作为赛博人一定要不断突破自己多整点有趣的东西。言归正传,前阵子在看blackhat看到一个很新颖的攻击方式叫做timeless attack。这个技术真的太牛了着实让我眼前一亮,这篇文章也是从学习timeless attack到对其思想进行延伸后的一些新的技巧。下面我先介绍一下timeless attack大致是个什么东西。
具体可以看一下blackhat的这个文章(https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Timeless-Timing-Attacks.pdf)
陆队在TQLCTF里也出过这一块的题目,各位可以参考一下他写的writeup里对该攻击的阐述,中文的会好理解一些(https://blog.zeddyu.info/2022/02/21/2022-02-21-PracticalTimingTimeless/)。我这边简单来描述一下:首先假设服务器有一段处理加解密或者查询类的代码,比如:
str = "hello"
for i in range(len(str)):
if input[i] == str[i]:
continue
else:
break
假设input来自于我们给予的输入,那么这里就可以看成是一个循环逐个字符比对输入和str是否相等,如果遇到某位不相等就中断比对,如果该位字符相等则进入下一位对比直到比对完毕为止。那么这会有什么问题呢?假设我们给定输入 input1 = "haxxx" 以及 input2 = "hexxx",那按照逐位对比的逻辑,则input1会在第二位的时候终止循环,而input2会在第三位的时候终止循环,那么是不是意味着input1的输入要比input2的输入程序在执行的时候少执行了一次循环。也就是说input1比input2会提前结束程序。那么这种执行时间的差异就体现出来了,假设这个时间差长达1秒,那么我们完全可以使用时间盲注的方式来逐个字符串判断出来str的值。但是实际情况是这样一个循环的差异通常的时间差异很小,可能在纳秒级别,这么小的执行时差在网络层面的远程攻击者是不可能探测到的,因为网络本身就存在极大的抖动,这些抖动造成的时间差会直接盖过程序那几纳秒的差异。为了探测到这种差异,timeless attack诞生了。timeless的核心思想是通过将时间差异转化成另一个可以被观测的度量差异,在作者给出的实际案例则是利用http2.0中新增的Multiplexing协议,将时间差异转化成返回包的先后顺序,时间波动太大我们无法观测,但是返回包的先后顺序则是可以被观测到的,而这个先后顺序和时间的转换是在服务器上进行的,因此完全可以无视网络抖动。那么为什么必须得是http2.0的Multiplexing协议才行呢?这个问题就很有趣了,一般来说有http在传输时候有几种情况
协议版本
传输方式
效果
http1.0
原始方式
一个tcp只有一个请求和响应
http1.1
基础的keepalive
复用同一个tcp,多个请求时,一个请求一个响应顺序执行
http1.1
pipeline模式
复用一个tcp,多个请求时,同时发送多个请求,服务端顺序响应这几个请求,按照先进先出的原则强制响应顺序
http2.0
Multiplexing
复用一个tcp,采用http2.0的封装,多个请求时,多个h2的帧,请求会并发进行处理,响应是乱序返回的(客户端根据帧信息自己会重组)
大家可以看一下网上这篇文章基本讲的很通俗易懂了(https://www.cnblogs.com/cmyoung/p/14604135.html)
了解了这些后我们着重看Multiplexing,先简单看一下h2的帧格式
可以看到这里在传统http报文外侧又定义了几个头部,其中有一个头部叫做stream identifier 这个就是用来表示一个h2帧在整个流里的顺序,有了这个所以h2可以乱序响应,客户端根据这个id进行重组就行了。而h2的帧可以看成对http的报文外侧加了一层封装形成的数据即可。明白了帧的基本概念,那么再看下图就比较明朗了
由于这里的h2帧在服务端是并发处理的,其返回的时候因为有帧信息来表示所以就直接乱序返回了,也正是因为如此h2才能做到并发处理。那么这时候回到前面的timeless attack上来。正因为其并发处理和乱序响应,那么就可以将两个其余内容一致仅仅是输入字符差一位的请求放在两个相邻的h2帧中同时发送给服务端,那么服务端在并发处理这两个帧的时候就会因为处理的差异导致返回的顺序变化,处理时间久的就会排在处理时间短的之后返回,我们观测到这种现象后就可以推敲出谁延迟久进而推测出具体每一位的字符比对情况了。
上面大概描述了一下timeless attack,关于这个攻击的复现和解释并不是本文的重点。铺垫完后,进一步拓展才是我想做的事情。当我们感叹timeless attack的精妙的时候又不得不叹息其必须是在http2.0之上才会有效,而平日里看到的大部分应用和设备都是http1.1。那么我们能不能在http1.1上复刻出timeless attack或者是类似的方式便于我们探测极小的执行差异呢?考虑到限定在http1.1的条件下,那么我们能使用的可能就只有pipeline了。那么根据参考网上的只言片语我们可以构思出几个关于pipeline的推测:
由于pipeline是强制顺序响应的,那么其请求和响应的顺序是强制固定的
在1的基础上,假设服务端存在并发处理,那么服务端可能是在通过cl分割http报文后将报文发送给不同的线程进行处理,然后将响应在缓存里排序再返回
在1的基础上,假设服务端并不存在并发处理,那么可以简单看成一个tcp链接从头到尾都是由一个线程进行处理的,也就是说只有请求1被处理完才会处理请求2
那我现在要做的就是设计一个简单的实验然后观测一下服务端在单一tcp的情况下对pipeline里的请求处理是不是存在并发处理。那我只需要写两个php文件:
//1.php
<?php
sleep(5);
echo "hello\n";
list($t1, $t2) = explode(' ', microtime());
echo (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000);
?>
//2.php
<?php
echo "hello2\n";
list($t1, $t2) = explode(' ', microtime());
echo (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000);
?>
可以看到两个php文件的主要差异在于1.php在执行前进行了5秒的延迟,而2.php则没有,两个php文件都会打印当前时间戳。那么这时候我使用burp构造两个仅挨着的请求,如果是并发处理的,则2.php打印出来的时间肯定要早于1.php,反之则说明是单线程进行顺序处理的。
上图我对我的环境发送了在一个pipeline里的两个get请求,返回只有一个这其实有问题,正常情况应该是会有两个挨着的响应。为了克服这个问题,我直接用wireshark抓
在wireshark里可以看到确实有两个响应,由于被提前tcp fin了所以burp里看到的是一个(环境问题就不管他了)。我们看一下具体的响应时间:
1.php打印出来的时间是1649358045734
2.php打印出来的时间是1649358045735
可以看到2.php时间比1.php晚1微妙,这说明是单线程顺序执行。那么我们就排除了多线程并发处理的可能性。简单总结一下pipeline:
由于pipeline是强制顺序响应的,那么其请求和响应的顺序是强制固定的
服务端在接受pipeline的请求时以单一线程对其进行分割并进行处理,只有请求1处理完成后才会处理请求2
上面确定了pipeline是单线程顺序处理这一点后,我一开始其实是灰心的,因为没有了并发又是强制顺序,根本就不可能有什么timeless空间换时间的度量置换方式。如果执行真有延迟,那么也只能是时间延迟,那么这么小的时间延迟想被观测几乎是不可能了。置换这条路走不通了,那么放大呢?什么叫做放大?放大就是把原本细小的变化通过一些方式对其进行放大,就像使用放大镜去观测沙子会比肉眼去观测看到的清楚多。 那么好,既然你pipeline是单线程,那么我就利用你pipeline单线程让你不断的处理同一个请求,假如请求A和请求B的执行时间差异1ms,那么请求A*1000和请求B*1000的整个时间差异就可以达到1秒! 确实,放大了1000倍后看起来可行性还挺高的,即使有一些网络延迟抖动之类的干扰,也完全可以通过多次请求的平均数在概率上对其进行矫正。那么真的可以无条件无限放大吗?答案是否定的,在实际的场景里,pipeline的最大处理请求数受到服务器中间件的配置影响,比如apache里默认在启用keepalive的情况下会设置
这个设置的意思就是,单个tcp链接中最多会被服务器接受的请求个数为100个。超出这个数字的请求数将不会被处理,会被服务器直接丢弃。
我们可以在一些服务器的响应中看到显式的展示keep-alive头部,里面会写着max=100,也就是说pipeline最大支持100个请求。如果仅仅放大一百倍,可以说是食之无味弃之可惜了。那么有没有无限制的呢?确实有!
像这样子,响应里keepalive只有一个timeout并没有max的情况下则意味着其没有对pipeline数量进行限制,那么也就是说我们的放大场景是存在的这时候只要无限的构造pipeline请求就可以无限叠加倍率。
这是我在网上找到的一个大厂的资产其并没有限制pipeline的数量,因此也可以受到放大的影响。看看fofa的情况,首先看看存在keepalive头部的有多少
有300w个,那么没有限制max的有多少呢?
有20w个,差不多十分之一,这数量确实还可以。当然这里的数据是有keepalive头部的,那些没有头部但也会表现出pipeline的节点就不好统计了。
不得不说timeless attack真的是给我上了一课,这种方式的侧信道是真的非常有趣和巧妙。在最后我通过不断的卷最后也勉强填补了在http1.1情况下可以用的无限放大的技巧。如果你不挖掘一些极端的漏洞可能并不会觉得它有什么用,但如果你像我一样挑战不可能,那么在极端的场景下它或许会成为你到达最后目标的关键一环。