diff options
author | Bogdan Dobrelya <bogdando@yahoo.com> | 2023-06-29 22:12:28 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-29 22:12:28 +0200 |
commit | c7868ccc2ba89796789f943d3767400f7ae000b4 (patch) | |
tree | e03722ddb36bf3ca658e05a99ba90ed1e739028b | |
parent | 6dede46eadde47e7141e11f7134844f93012595a (diff) | |
download | wee-slack-c7868ccc2ba89796789f943d3767400f7ae000b4.tar.gz |
Firefox-snap and chrome/chrome-beta support (#884)
* Add support for cookies decryption for Chrome/beta
* Add support for firefox-snap, and multiple default/custom profiles
paths
* If no token can be found, like no key localConfig_v2 in
webappsstore2, try the best to fetch it from leveldb (or online copy
of it, if locked by running browser). When no luck even with that,
suggest the user to fetch it manually from dev console, and pass it
as an extra argument. This way it still saves some extra cycles for
the script users by fetching d/d-s values, and formatting the
ultimate registration command string.
Not tested on Darwin, leaving this as an exercise for you
Adding Chromium support should be straightforward and can be done in
follow-ups.
Signed-off-by: Bohdan Dobrelia <bdobreli@redhat.com>
Co-authored-by: Bogdan Dobrelya <bogdando@mail.ru>
-rwxr-xr-x | extract_token_from_browser.py | 210 |
1 files changed, 180 insertions, 30 deletions
diff --git a/extract_token_from_browser.py b/extract_token_from_browser.py index 5798ba0..5db183f 100755 --- a/extract_token_from_browser.py +++ b/extract_token_from_browser.py @@ -1,64 +1,214 @@ #!/usr/bin/env python3 +# Examples: +# python ./extract_token_from_browser.py chrome --profile="Profile 2" \ +# --token=xoxc-foo-bar +# python ./extract_token_from_browser.py firefox-snap --token=xoxc-baz import argparse import json -from pathlib import Path +import os +import secretstorage +import shutil import sqlite3 import sys +from pathlib import Path +from secretstorage.exceptions import SecretStorageException +from sqlite3 import OperationalError +try: + import_err = None + from Crypto.Cipher import AES + from Crypto.Protocol.KDF import PBKDF2 + from plyvel import DB + from plyvel._plyvel import IOError as pIOErr +except ModuleNotFoundError as e: + import_err = e + pass + + +class AESCipher: + + def __init__(self, key): + self.key = key + + def decrypt(self, text): + cipher = AES.new(self.key, AES.MODE_CBC, IV=(b' ' * 16)) + return self._unpad(cipher.decrypt(text)) + + def _unpad(self, s): + return s[:-ord(s[len(s) - 1:])] + + parser = argparse.ArgumentParser( description="Extract Slack tokens from the browser files" ) parser.add_argument( "browser", help="Which browser to extract from", metavar="<browser>" ) +parser.add_argument( + "--token", help="Extracted token", metavar="<token>", nargs='?' +) +parser.add_argument( + "--profile", help="Profile to look up cookies for", metavar="<profile>", nargs='?' +) args = parser.parse_args() -if args.browser != "firefox": - print("Currently only firefox is supported by this script", file=sys.stderr) +if args.browser not in ["firefox", "firefox-snap", "chrome", "chrome-beta"]: + print("Currently only firefox. firefox-snap, chrome, " + "chrome-beta are supported by this script", file=sys.stderr) sys.exit(1) if sys.platform.startswith("linux"): - firefox_path = Path.home().joinpath(".mozilla/firefox") + iterations = 1 + if args.browser == "firefox-snap": + browser_data = Path.home().joinpath( + "snap/firefox/common/.mozilla/firefox") + elif args.browser == "firefox": + browser_data = Path.home().joinpath(".mozilla/firefox") + else: + browser_data = Path.home().joinpath(".config/google-%s" % args.browser) elif sys.platform.startswith("darwin"): - firefox_path = Path.home().joinpath("Library/Application Support/Firefox/Profiles") + iterations = 1003 + if args.browser in ["firefox", "firefox-snap"]: + browser_data = Path.home().joinpath( + "Library/Application Support/Firefox/Profiles") + else: + browser_data = Path.home().joinpath( + "Library/Application Support/Chrome") else: - print("Currently only Linux and macOS is supported by this script", file=sys.stderr) + print("Currently only Linux and macOS is supported by this script", + file=sys.stderr) sys.exit(1) +leveldb_path = None +profile = args.profile +if args.browser in ["firefox", "firefox-snap"]: + cookie_d_query = ( + "SELECT value FROM moz_cookies WHERE host = '.slack.com' " + "AND name = 'd'" + ) + cookie_ds_query = ( + "SELECT value FROM moz_cookies WHERE host = '.slack.com' " + "AND name = 'd-s'" + ) + if not profile: + profile = "*.default*" + + default_profile_path = max( + [ x for x in browser_data.glob(profile)], key=os.path.getctime) + if not default_profile_path: + print("Couldn't find the default profile for Firefox", file=sys.stderr) + sys.exit(1) + cookies_path = default_profile_path.joinpath("cookies.sqlite") +else: + if import_err: + print("Missing required modules for Chrome browser support", + file=sys.stderr) + raise import_err + + # b'v10' is for Chromium, but not Chrome, it seems? + prefix = b'v11' + cookie_d_query = ( + "SELECT encrypted_value FROM cookies WHERE " + "host_key = '.slack.com' AND name = 'd'" + ) + cookie_ds_query = ( + "SELECT encrypted_value FROM cookies WHERE " + "host_key = '.slack.com' AND name = 'd-s'" + ) + if not profile: + profile = "Default" + + default_profile_path = browser_data.joinpath(profile) + cookies_path = default_profile_path.joinpath("Cookies") + leveldb_path = default_profile_path.joinpath("Local Storage/leveldb") + +cookie_d_value = None +cookie_ds_value = None try: - default_profile_path = next(firefox_path.glob("*.default-release")) -except StopIteration: - print("Couldn't find the default profile for Firefox", file=sys.stderr) - sys.exit(1) + con = sqlite3.connect(f"file:{cookies_path}?immutable=1", uri=True) + cookie_d_value = con.execute(cookie_d_query).fetchone()[0] + cookie_ds_value = con.execute(cookie_ds_query).fetchone()[0] +except TypeError: + if not cookie_d_value: + print("Couldn't find the 'd' cookie value", file=sys.stderr) + sys.exit(1) + pass +finally: + con.close() -cookies_path = default_profile_path.joinpath("cookies.sqlite") -con = sqlite3.connect(f"file:{cookies_path}?immutable=1", uri=True) -cookie_d_query = ( - "SELECT value FROM moz_cookies WHERE host = '.slack.com' AND name = 'd'" -) -cookie_d_value = con.execute(cookie_d_query).fetchone()[0] -cookie_ds_query = ( - "SELECT value FROM moz_cookies WHERE host = '.slack.com' AND name = 'd-s'" -) -cookie_ds_values = con.execute(cookie_ds_query).fetchone() -con.close() +if args.browser in ["chrome", "chrome-beta"]: + bus = secretstorage.dbus_init() + try: + collection = secretstorage.get_default_collection(bus) + for item in collection.get_all_items(): + if item.get_label() == 'Chrome Safe Storage': + passwd = item.get_secret() + break + else: + raise Exception('Chrome password not found!') + except SecretStorageException: + print("Error communicating org.freedesktop.secrets, trying 'peanuts' " + "as a password", file=sys.stderr) + passwd = 'peanuts' + + salt = b'saltysalt' + length = 16 + key = PBKDF2(passwd, salt, length, iterations) -if cookie_ds_values: - cookie_value = f"d={cookie_d_value};d-s={cookie_ds_values[0]}" + cipher = AESCipher(key) + + if cookie_d_value[:3] == prefix: + cookie_d_value = cipher.decrypt(cookie_d_value[3:]).decode('utf8') + + if cookie_ds_value and cookie_ds_value[:3] == prefix: + cookie_ds_value = cipher.decrypt(cookie_ds_value[3:]).decode('utf8') + +if cookie_ds_value: + cookie_value = f"d={cookie_d_value};d-s={cookie_ds_value}" else: cookie_value = cookie_d_value local_storage_path = default_profile_path.joinpath("webappsstore.sqlite") -con = sqlite3.connect(f"file:{local_storage_path}?immutable=1", uri=True) local_storage_query = "SELECT value FROM webappsstore2 WHERE key = 'localConfig_v2'" -local_config_str = con.execute(local_storage_query).fetchone()[0] -con.close() +teams = [] +local_config = None +try: + con = sqlite3.connect(f"file:{local_storage_path}?immutable=1", uri=True) + local_config_str = con.execute(local_storage_query).fetchone()[0] + local_config = json.loads(local_config_str) +except (OperationalError, TypeError): + if not args.token and args.browser not in ["chrome", "chrome-beta"]: + print("Couldn't find the token for team, extract it manually " + "by running 'window.prompt(\"Session token:\", " + "TS.boot_data.api_token)' in your browser dev console " + "and specify it in --token", file=sys.stderr) + sys.exit(1) +finally: + con.close() + +if not local_config and leveldb_path: + try: + db = DB(str(leveldb_path)) + except pIOErr: + leveldb_copy = str(leveldb_path) + ".bak" + os.makedirs(leveldb_copy, exist_ok=True) + shutil.copytree(leveldb_path, leveldb_copy, + dirs_exist_ok=True) + print(f"Leveldb was locked by a running browser - made an online copy " + "of it in {leveldb_copy}", file=sys.stderr) + db = DB(str(leveldb_copy)) + + local_storage_value = db.get( + b'_https://app.slack.com\x00\x01localConfig_v2') + local_config = json.loads(local_storage_value[1:]) + teams = [ + team for team in local_config["teams"].values() if not team["id"].startswith("E") + ] + +if not teams: + teams = [{"token": args.token, "name": "Slack"}] -local_config = json.loads(local_config_str) -teams = [ - team for team in local_config["teams"].values() if not team["id"].startswith("E") -] register_commands = [ f"{team['name']}:\n/slack register {team['token']}:{cookie_value}" for team in teams ] |