DEV Community

smac89
smac89

Posted on • Edited on

Reading saved firefox passwords via cli and other woes

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


Enter fullscreen mode Exit fullscreen mode

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 ()
);


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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,
}


Enter fullscreen mode Exit fullscreen mode

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 {} \;


Enter fullscreen mode Exit fullscreen mode

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"


Enter fullscreen mode Exit fullscreen mode

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);
  }
}


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

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 .' -- {} \;


Enter fullscreen mode Exit fullscreen mode

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? πŸ€”

Firefox settings

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 nss-passwords script:
Gnome pinentry

Using the command-line option:
Commandline password prompt

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.

Top comments (1)

Collapse
 
iainelder profile image
Iain Samuel McLean Elder

This article makes a great counterpoint to Tavis Ormandy's analysis of the browser integration features of dedicated password managers. He concluded that it was safer to use the browser's built-in manager for auto-completion.

You have shown that it is easy to use the Firefox's own manager in an unsafe way as well! Thanks for the technical analysis.