By Max Ammann and Emilio López
Our application security team leaves no stone unturned; our audits dive deeply into areas ranging from device firmware, operating system kernels, and cloud systems to widely used technology such as mobile and web applications. This post examines two issues we identified over the past few years that, though unrelated, both showcase our commitment to securing open-source software: a potential denial-of-service (DoS) threat hidden in JSON Web Tokens (JWTs), and an oversight within the Linux kernel that could enable circumvention of critical kernel security mechanisms.
Unraveling a DoS threat in JOSE libraries
JWT and JSON Object Signing and Encoding (JOSE) are expansive standards that describe the creation and use of encrypted and/or signed JSON-based tokens. While these standards are widely used and represent a significant improvement over previous solutions for identity claims, they are not without drawbacks, and have several well-known footguns, like the JWT “none” signature algorithm.
Our finding concerns an attack that was a part of a lineup of new JWT attacks presented by Tom Tervoort at BlackHat USA 2023: “Three New Attacks Against JSON Web Tokens.” The “billion hashes attack”, which results in denial-of-service due to a lack of validation in JWT key encryption, caught our colleague Matt Schwager’s attention. Upon further examination, he discovered it applied to several more libraries used in the Go and Rust ecosystems: go-jose, jose2go, square/go-jose, and josekit-rs.
These libraries all support key encryption with PBES2, a feature meant to allow for password-based encryption of the Content Encryption Key (CEK) in JSON Web Encryption (JWE). A key is first derived from a password by using PBES2 schemes, which execute a number of PBKDF2 iterations. Then that key is used to encrypt and decrypt the token contents.
This wouldn’t normally be an issue, but unfortunately, the number of iterations is contained as part of the token, on the p2c header parameter, which an attacker can easily manipulate. Consider, for example, the token header shown below:
By using a very large iteration count in the p2c field, an attacker can cause a DoS on any application that attempts to process this token. Whoever receives and attempts to verify this token will first need to perform 2,147,483,647 PBKDF2 iterations to derive the CEK before they can even verify if the token is valid, costing significant amounts of compute time.
We reported the issue to the go-jose, jose2go, and josekit-rs library maintainers, and it has been fixed by limiting the maximum value usable for p2c in go-jose/go-jose
on version 3.0.1 (commit 65351c27657d); on dvsekhvalnov/jose2go
on version 1.6.0 (commits a4584e9dd712 and 8e9e0d1c6b39); and on hidekatsu-izuno/josekit-rs
on version 0.8.5 (commits 1f3278a33f0e, 8b60bd0ea8ce, and 7e448ce66c1c). square/go-jose
remains unfixed, as the library is deprecated, and users are encouraged to migrate to go-jose/go-jose
.
Alternatively, the risk can also be mitigated by not relying purely on the token’s alg
parameter. After all, if your application does not expect to receive a token using PBES2 or any lesser-used algorithm, there is no reason to try to process one. jose2go allows implementing opt-in stricter validation of alg
and enc
parameters today, and go-jose’s next major version will require passing a list of acceptable algorithms when processing a token, allowing developers to explicitly list a set of expected algorithms.
KASLR bypass in privilege-less containers
Next is a vulnerability that has been fixed since 2020, but never got a CVE assigned by the Linux kernel maintainers. In the following paragraphs, we’ll go into the details of a previously unknown but fixed KASLR bypass.
Back in 2020, Trail of Bits engineer Dominik Czarnota (aka disconnect3d) discovered a vulnerability in the Linux kernel that could expose internal pointer addresses within unprivileged Docker containers, allowing a malicious actor to bypass Kernel Address Space Layout Randomization (KASLR) for kernel modules.
KASLR is an important defense mechanism in operating systems, primarily used to deter exploit attempts. It is a security technique that randomizes the kernel memory address locations between reboots. On top of that, kernel addresses must be hidden from userspace; otherwise, the mitigation would make no sense, as such kernel address disclosure would effectively bypass the KASLR mitigation.
While there are places where kernel addresses are shown to userspace programs, on many systems they should be available only when the user has the CAP_SYSLOG
Linux capability. (Capabilities split root user privileges so it is possible to be the root user, or a user with uid 0, while having a limited set of privileges.) In particular, the manual page for the CAP_SYSLOG
capability reads: “View kernel addresses exposed via /proc and other interfaces when /proc/sys/kernel/kptr_restrict
has the value 1.” This means that only processes that are executed with the capability CAP_SYSLOG
should be able to read kernel addresses.
However, Dominik discovered that this was not the case from within a Docker container where processes that are run from the root user without CAP_SYSLOG
were able to observe kernel addresses. By default, Docker containers are unprivileged, which means that root users are restricted in what they can do (e.g., they cannot perform actions that require CAP_SYSLOG
). This can also be demonstrated without Docker by using the capsh tool run from the root user to remove the CAP_SYSLOG
capability:
The underlying cause of the issue was that the credentials were checked incorrectly. The sysctl toggle kernel.kptr_restrict
indicates whether restrictions are placed on exposing kernel addresses: the value “2” means that the addresses are always hidden; “1” means that they are shown only if the user has CAP_SYSLOG
; and “0” means that they are always shown. Instead of ensuring that the user had the CAP_SYSLOG
capability before showing the addresses, only the value of kptr_restrict was being considered to decide whether to show or hide the addresses. The addresses were always exposed if kptr_restrict
was 1, while they should have been hidden if the user did not have CAP_SYSLOG
. The issue was fixed in commit b25a7c5af905.
After discovering this vulnerability, we followed a responsible disclosure process with Docker and the Linux kernel’s security team. Dominik initially notified the Docker team about this, since he thought the vulnerability originated from Docker, and also reported other sysfs filesystem leaks (where other sysfs paths leaked information such as the names of services run outside of the container, other container IDs, and information about devices). The disclosure timeline is provided at the end of this post.
Although we received only silence from Docker despite multiple requests for updates, the Linux community swiftly rectified the issue in the kernel, ensuring that the security cornerstone of Linux remains robust. The KASLR bypass bug fix was backported to various Ubuntu LTS versions, while the other sysfs leaks from Docker were not fixed at all. However, Linux kernel releases before 4.19 are vulnerable to the KASLR bypass. Ubuntu 18, which uses kernel 4.15, is still vulnerable because the fix was not backported.
Do you need audits in 2024?
These two vulnerabilities are quite different. The DoS issue relates to parsing and interpreting user input, while the kernel vulnerability is an information leak (strictly speaking, it is an access control vulnerability). These differences affect the detectability of bugs: if you execute a program with a DoS vulnerability that provides a service, you’ll likely notice right away when your service is exploited because the availability of your service will be compromised. By contrast, if an attacker exploits an access control vulnerability, you probably won’t notice when your service is exploited.
This difference in detectability is important for automated testing. For instance, fuzzing, as showcased in the Trail of Bits Testing Handbook, requires the program to crash or hang. Therefore, we mostly find DoS bugs in the memory-safe programs we fuzz. Automatically finding access control bugs through fuzzing is more challenging because it requires the implementation of fuzzing invariants.
Security audits are still indispensable tools for finding vulnerabilities, just like fuzzing is! Our audits integrate fuzzing whenever possible, and we look for opportunities to enforce invariants to catch nasty logic bugs.
Disclosure timeline for KASLR bypass in privilege-less containers
- June 6, 2020: Reported the vulnerability to Docker.
- June 11, 2020: Docker replied that they would probably block the sysfs paths that leaks information via the “masked paths” feature, and that the memory address disclosure should be reported to the Linux kernel developers.
- June 11, 2020: Informed the intent to contact [email protected] about the kASLR bypass.
- June 11 to June 18, 2020: Performed a deeper analysis of the kASLR bypass.
- June 18, 2020: Reported the bug to [email protected].
- June 18, 2020: Bug confirmed by Kees Cook.
- June 19 to June 21, 2020: Kernel developers discuss how to patch the issue.
- June 30, 2020: Requested an update from Docker.
- July 3 to July 14, 2020: Patches that fix the issue land in the Linux kernel.
- July 11, 2020: Requested an update from Docker again about other sysfs leaks, and informed them that the KASLR bypass issue has been fixed in Linux 4.19, 5.4 and 5.7 kernels.
- December 3, 2020: Requested an update from Docker once again, and informed the intent to disclose the issues publicly. Docker did not reply.