Reading saved firefox passwords via cli and other woes
smac89
Posted on September 16, 2022
I was recently working from home and needed a saved password from Firefox on my dev machine at work. Seeing as the only connection I had with the remote machine was through ssh, this meant the only option I had was to retrieve the password through the commandline.
I looked online and it wasn't long before I found a tool called nss-passwords
. I installed it and gave it a try:
ā nss-passwords -d ~/.mozilla/firefox/wnhkpui2.dev-edition-default localhost:3001
| http://localhost:3001 | japoduje@mailinator.com | oRvr2x^4#w8X@sPd |
| http://localhost:3001 | rapakejuqe@mailinator.com | veryxilu
Wow! š
Crazy how it worked soo easily! No need for superuser permissions: I can just read my saved passwords...
Hold up! Wait a minute! Why was that soo easy?
Is this safe?
Well, if you are really curious like I was and don't mind reading a bit of ocaml, the main code is right here. It's been a minute since I read OCaml code, but a cursory glance through the code reveals that it looks in your firefox profile folder (the -d
option) to find a file called logins.json
or signons.sqlite
:
(if Sys.file_exists (FilePath.concat !dir "logins.json")
then exec_json ()
else exec_sqlite ()
);
Here be dragons! š
The exec_json
function in turn reads the json file, and extracts an array called logins
, which it passes to another function called json_process
:
let exec_json () =
(** I totally
get all
of this
*)
List.iter (json_process logins.logins) !queries
Each element of the array looks like this:
{
"id": 104,
"hostname": "https://host.com",
"httpRealm": null,
"formSubmitURL": "https://host.com",
"usernameField": "email",
"passwordField": "password",
"encryptedUsername": "MXXXXPgAAAAAAAAAAAAAAAAAAAEwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=",
"encryptedPassword": "MXXXXPgAAAAAAAAAAAAAAAAAAAEwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"guid": "{c0f43272-22a3-43db-b5bd-2d0cfbe621dd}",
"encType": 1,
"timeCreated": 1662743548172,
"timeLastUsed": 1662743548172,
"timePasswordChanged": 1662743548172,
"timesUsed": 1,
}
At this point, I decided to attempt to replicate what the code is doing using just command-line. The reason being to see how easy it would be for some random npm
package you download to run a simple command to extract your passwords, so this one liner gives us the same json array we get from the above:
find -L ~/.mozilla/firefox/wnhkpui2.dev-edition-default -name 'logins.json' -exec jq '.logins' -r {} \;
Continuing... š³ļø
The json_process
function doesn't seem all too interesting. However, it calls another function called do_decrypt
, which is declared as the following in OCaml:
external do_decrypt : callback:(bool -> string) -> data:string -> string = "caml_do_decrypt"
That looks like a ffi for calling a function from another language, and in this case it looks like the function is written in C:
CAMLprim value caml_do_decrypt(value callback, value data) {
CAMLparam2(callback, data);
CAMLlocal3(res, exn, cb_data);
const char *dataString = String_val(data);
int strLen = caml_string_length(data);
SECItem *decoded = NSSBase64_DecodeBuffer(NULL, NULL, dataString, strLen);
SECStatus rv;
SECItem result = { siBuffer, NULL, 0 };
if ((decoded == NULL) || (decoded->len == 0)) {
/* Base64 decoding failed */
res = Val_int(PORT_GetError());
if (decoded) {
SECITEM_FreeItem(decoded, PR_TRUE);
}
{
value args[] = { data, res };
caml_raise_with_args(*caml_named_value("NSS_base64_decode_failed"), 2, args);
}
}
/* Base64 decoding succeeded */
/* Build the argument to password_func ((bool -> string) * exn option) */
cb_data = caml_alloc_tuple(2);
Store_field(cb_data, 0, callback);
Store_field(cb_data, 1, Val_unit); /* None */
/* Decrypt */
rv = PK11SDR_Decrypt(decoded, &result, &cb_data);
SECITEM_ZfreeItem(decoded, PR_TRUE);
if (rv == SECSuccess) {
res = caml_alloc_string(result.len);
memcpy(Bytes_val(res), result.data, result.len);
SECITEM_ZfreeItem(&result, PR_FALSE);
CAMLreturn(res);
}
/* decryption failed */
res = Val_int(PORT_GetError());
exn = Field(cb_data, 1);
{
value args[] = { data, res, exn };
caml_raise_with_args(*caml_named_value("NSS_decrypt_failed"), 3, args);
}
}
Looks like the first thing it does is to attempt to use base64
to decode the encrypted value...ok.
rv = PK11SDR_Decrypt(decoded, &result, &cb_data)
Next it is calling this PK11SDR_Decrypt
function, which seems to be part of the nss
library.
Well I don't have time to start digging into how all that works, but it turns out we can download a package called nss-tools, which contains a command for decrypting the encrypted values, called pwdecrypt
.
Extending our commandline above, we can successfully decrypt the username and password for any given domain by doing:
find -L ~/.mozilla/firefox/wnhkpui2.dev-edition-default -name 'logins.json' \
-execdir sh -c 'jq '"'"'.logins | .[] | select(.hostname | endswith("host.com")) | "\(.encryptedUsername)\n\(.encryptedPassword)"'"'"' -r "$1" | pwdecrypt -d .' -- {} \;
Replace host.com
from the above command with an actual hostname and it will spit out the username followed by the password
Woah! āš¼
What a bittersweet ending. On one hand, I've just discovered a way to read my firefox saved passwords, on the other hand I've just discovered that literally any npm package I install, can run the same command to extract my saved passwords. Thankfully I don't save real passwords on Firefox, I use bitwarden password manager, and only passwords I save on FF are passwords I use for logging into dummy test accounts.
I'm also a bit confused as to why it is Mozilla who is building the libraries and tools that enables this ease of access...
Btw if you think this is a firefox issue, think again. A quick search online reveals there are tools available for Chrome (which will most likely work on Brave, Edge, and Chromium). Someone also wrote an entire medium article detailing methods of "extracting" passwords from all major browsers.
There might be hope...š”
I haven't been able to test this, but perhaps the reason it is so easy to extract the passwords is because I haven't set a master/primary password in Firefox? š¤
Update Oct 3rd, 2022
Indeed it turns out that if you set a password, then attempting to use any of the above methods will result in a password prompt:
Using the command-line option:
Lessons learned
- Never store passwords on your browser
- Use a dedicated password manager like IPassword, Bitwarden, Lastpass, etc
- If you must store the password in the browser, then make sure to use the master password option if your browser provides one.
Posted on September 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.