CLI 및 기타 문제를 통해 저장된 Firefox 비밀번호 읽기

나는 최근에 집에서 일하고 있었고 직장에서 내 dev 컴퓨터에 Firefox에서 저장된 암호가 필요했습니다. 원격 시스템과의 유일한 연결은 ssh를 통한 것이므로 이것이 내가 가진 유일한 옵션은 명령줄을 통해 암호를 검색하는 것임을 의미했습니다.

나는 온라인에서 nss-passwords 이라는 도구를 찾을 때까지 오래 걸리지 않았습니다. 나는 installed 그것을 시도했다:

➜ nss-passwords -d ~/.mozilla/firefox/wnhkpui2.dev-edition-default localhost:3001
| http://localhost:3001 | [email protected]   | oRvr2x^4#w8X@sPd |
| http://localhost:3001 | [email protected] | veryxilu


우와! 🎉



너무 쉽게 작동하는 방법에 미쳤습니다! 수퍼유저 권한이 필요하지 않습니다. 저장된 비밀번호를 읽을 수 있습니다...

잠깐! 잠깐 기다려요! 왜 그렇게 쉬웠습니까?

안전한가요?



글쎄, 당신이 나처럼 정말 궁금하고 약간의 ocaml을 읽는 것을 꺼리지 않는다면 주요 코드는 바로 here 입니다. OCaml 코드를 읽은 지 1분이 지났지만 코드를 대략적으로 살펴보면 Firefox 프로필 폴더(-d 옵션)에서 logins.json 또는 signons.sqlite라는 파일을 찾는다는 것을 알 수 있습니다.

(if Sys.file_exists (FilePath.concat !dir "logins.json")
 then exec_json ()
 else exec_sqlite ()
);


여기 용이 있습니다! 🐉


exec_json 함수는 json 파일을 읽고 logins 라는 배열을 추출하여 json_process 라는 다른 함수에 전달합니다.

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


배열의 각 요소는 다음과 같습니다.

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


이 시점에서 저는 명령줄만 사용하여 코드가 수행하는 작업을 복제하기로 결정했습니다. 따라서 이 한 줄은 위에서 얻은 것과 동일한 json 배열을 제공합니다.

find -L ~/.mozilla/firefox/wnhkpui2.dev-edition-default -name 'logins.json' -exec jq '.logins' -r {} \;


계속... 🕳️


json_process 함수는 그다지 흥미롭지 않은 것 같습니다. 그러나 do_decrypt 라는 또 다른 함수를 호출하는데, OCaml에서 다음과 같이 선언되어 있습니다.

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


이것은 다른 언어에서 함수를 호출하기 위한 ffi처럼 보이며, 이 경우에는 function이 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);
  }
}


가장 먼저 하는 일은 base64를 사용하여 암호화된 값을 해독하는 것 같습니다...좋아요.

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


다음으로 PK11SDR_Decrypt 라이브러리의 일부인 것으로 보이는 이 nss 함수를 호출합니다.

모든 것이 어떻게 작동하는지 파헤칠 시간이 없지만 nss-tools 이라는 패키지를 다운로드할 수 있습니다. 이 패키지에는 pwdecrypt 라는 암호화된 값을 해독하는 명령이 포함되어 있습니다.

위의 명령줄을 확장하면 다음을 수행하여 지정된 도메인의 사용자 이름과 암호를 성공적으로 해독할 수 있습니다.

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


위 명령에서 host.com를 실제 호스트 이름으로 바꾸면 사용자 이름과 암호가 나옵니다.

와! ✋🏼



씁쓸하고 달콤한 결말. 한편으로는 firefox에 저장된 비밀번호를 읽을 수 있는 방법을 발견했고, 다른 한편으로는 내가 설치한 모든 npm 패키지가 동일한 명령을 실행하여 저장된 비밀번호를 추출할 수 있다는 사실을 방금 발견했습니다. 고맙게도 저는 Firefox에 실제 암호를 저장하지 않고 bitwarden 암호 관리자를 사용하며 FF에 저장한 암호만 더미 테스트 계정에 로그인하는 데 사용하는 암호입니다.

나는 또한 Mozillalibraries을 구축하는 사람이 tools인 이유에 대해 약간 혼란스럽습니다.

Btw 이것이 Firefox 문제라고 생각되면 다시 생각하십시오. 온라인에서 빠르게 검색하면 Chrome에 사용할 수 있는 도구가 있음을 알 수 있습니다(Brave, Edge 및 Chromium에서 대부분 작동함). 누군가는 또한 모든 주요 브라우저에서 암호를 "추출"하는 방법을 자세히 설명하는 전체medium article를 작성했습니다.

희망이 있을지도...💡



나는 이것을 테스트할 수 없었지만 암호를 추출하기가 그렇게 쉬운 이유는 Firefox에서 마스터/기본 암호를 설정하지 않았기 때문일 것입니다. 🤔



교훈


  • 브라우저에 암호를 저장하지 마십시오
  • .
  • 전용 암호 관리자 사용
  • 좋은 웹페이지 즐겨찾기