Reading saved firefox passwords via cli and other woes
Tags: firefox, commandline, security, password
Published: 2022-09-16 08:16:23

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`](https://github.com/glondu/nss-passwords/tree/0.3). I [installed](https://packages.ubuntu.com/source/jammy/nss-passwords) 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](https://github.com/glondu/nss-passwords/blob/0.3/main.ml). 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`:

```ocaml
(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`:

```ocaml
let exec_json () =
  (** I totally
      get all
      of this
  *)
  List.iter (json_process logins.logins) !queries
```

Each element of the array looks like this:

```json
{
    "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](https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts) to extract your passwords_, so this one liner gives us the same json array we get from the above:

```bash
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:

```ocaml
external do_decrypt : callback:(bool -> string) -> data:string -> string = "caml_do_decrypt"
```

That looks like a [ffi](https://en.wikipedia.org/wiki/Foreign_function_interface) for calling a function from another language, and in this case it looks like the [function](https://github.com/glondu/nss-passwords/blob/0.3/nss_stubs.c#L98) is written in C:

```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.


```c
rv = PK11SDR_Decrypt(decoded, &result, &cb_data)
```

Next it is calling this `PK11SDR_Decrypt` function, which seems to be part of the [`nss`](https://github.com/nss-dev/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](https://packages.ubuntu.com/jammy/libnss3-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:

```sh
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](https://firefox-source-docs.mozilla.org/security/nss/index.html) who is building the [libraries](https://github.com/nss-dev/nss) and [tools](https://github.com/mozilla/nss-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](https://github.com/hassaanaliw/chromepass) (which will most likely work on Brave, Edge, and Chromium). Someone also wrote an entire [medium article](https://kylemistele.medium.com/stealing-saved-browser-passwords-your-new-favorite-post-exploitation-technique-c5e72c86159a) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6y739clh98tava4nx4ah.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/usbugeqd71go029rgztl.png)

**Using the command-line option:**
![Commandline password prompt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxbww87z5hfczqp4az3d.gif)

### Lessons learned
- Never store passwords on your browser
- Use a dedicated password manager like IPassword, [Bitwarden](https://bitwarden.com/), 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.