A command line tool for flashing the USB Rubber Ducky on the go written in Python with cmd!
We start of with declaring the config file path for the USB at the start
CONFIG_FILE_PATH = "/usr/bin/duckyshell/config.yaml"
And at the end I just have the function for running the application
if __name__ == '__main__':
shell = DuckyShell()
shell.cmdloop()
This is the class for the whole CLI program.
When it runs it starts of with clearing the screen (as msfconsole does), prints out the intro, setting the prompt and declaring the USB and file path variables as None
.
# Clears the screen
os.system(f"clear")
# Prints out the intro
intro = "\nHak5 USB Rubber Ducky CLI\n\nType 'help' to list available commands or 'help <command>' to get help for a specific command.\n"
# Prompt for the CLI
prompt = "ducky_sh3ll > "
# Setting the usb path to None for now
usb_path = None
file_path = None
And we declare the help messages for each command
# Help messages for each command
help_messages = {
'usb': 'Set the USB path for connected USB devices.',
'file': 'Set the file to the designated inject.bin you want.',
'list': 'List the connected USB devices.',
'run': 'Copy the inject.bin to the USB Rubber Ducky.',
'quit': 'Exit the DuckyShell.',
'exit': 'Exit the DuckyShell.',
}
I wrote this so you can "set and forget" the path of the USB so you don't need to set it each time you run the program. It reads the config file and sets the usb_path
to the path inside of the config.
def load_usb_path(self):
# Load the USB path from the config file
if os.path.exists(CONFIG_FILE_PATH):
with open(CONFIG_FILE_PATH, 'r') as config_file:
config = yaml.safe_load(config_file)
self.usb_path = config.get('usb_path')
Here is the function for saving the usb path to the config file.
def save_usb_path(self):
# Save the USB path to the config file
config = {'usb_path': self.usb_path}
with open(CONFIG_FILE_PATH, 'w') as config_file:
yaml.safe_dump(config, config_file)
This is done by getting the current directory of where the user is and filtering the list on what the user is typing and returning the different possibilities.
I wanted this feature since I use it so much in the terminal, so I implemented it in this project.
def complete_file(self, text, line, begidx, endidx):
# Get a list of files and directories in the current directory
current_dir = os.getcwd()
files_and_dirs = os.listdir(current_dir)
# Filter the list based on the current text being entered
options = [entry for entry in files_and_dirs if entry.startswith(text)]
return options
def complete_usb(self, text, line, begidx, endidx):
# Get a list of files and directories in the current directory
current_dir = os.getcwd()
files_and_dirs = os.listdir(current_dir)
# Filter the list based on the current text being entered
options = [entry for entry in files_and_dirs if entry.startswith(text)]
# Returning results
return options
This is done by searching the dev/sd
folder in Linux with regex to only return the devices that are present.
def get_connected_usb_devices(self):
devices = glob.glob('/dev/sd*[!0-9]')
return [os.path.basename(device) for device in devices]
Is the command that runs the flashing for the USB. It checks if both the usb_path
and file_path
are set, and if it is an .bin
file extention. Then it copies it over to the USB, replacing the old inject.bin
file.
def do_run(self, arg):
# Check for USB path provided
if not self.usb_path:
print("USB path not provided. Use 'set_usb_path' command to set it.")
return
# Check for file path provided
if not self.file_path:
print("File path not provided. Use 'set_file_path' command to set it.")
return
# Check if the file is a binary file
if not self.file_path.endswith('.bin'):
print(f"Invalid file type: {self.file_path} is not a text file (.txt)")
return
# Copy the binary file to the USB Rubber Ducky
os.system(f"cp {self.file_path} {self.usb_path}")
print(f"File copied to USB drive: {self.usb_path}")
# Delete the temporary text file
os.remove(self.file_path)
Set the path for only connected USB devices
def do_usb(self, line):
usb_path = line.strip()
# Check if the USB device is connected
usb_devices = [device[0] for device in self.get_connected_usb_devices()]
if usb_path not in usb_devices:
print("USB device is not connected.")
return
# Set the USB path for future commands
self.usb_path = usb_path
self.save_usb_path()
print(f"USB path set to: {usb_path}")
Set the file to the file you want. It must be an ìnject.bin
file (Hak5) so it checks for that with the use of Regular expressions.
def do_file(self, line):
file_path = line.strip()
# Check if the file path exists
if not os.path.exists(file_path):
print("File path does not exist.")
return
# Check if the file path has the correct extension
if not re.match(r"inject.bin", file_path):
print("Invalid file format. Only 'inject.bin' files are allowed.")
return
# Set the file path for future copy operations
self.file_path = file_path
print(f"File path set to: {file_path}")
This lists the connected USB devices and I think this function is really cool. We use the lsblk
command in Linux to filter out and look for only the connected USB devices the user can provide as input.
def do_list(self, arg):
output = subprocess.check_output(['lsblk', '-o', 'NAME,MODEL', '-n', '-l']).decode('utf-8').strip()
lines = output.split('\n')
devices = [line.split() for line in lines]
usb_devices = [device for device in devices if device and (device[0].startswith('sd') or device[0].startswith('hd'))]
if usb_devices:
print("Connected USB devices:")
for device in usb_devices:
if len(device) >= 2:
print(f"Name: {device[0]} Model: {device[1]}")
else:
print(f"Name: {device[0]} Model: Unknown")
else:
print("No USB devices found.")
If you press enter in the terminal, it returns nothing! Soooo we need to implement that with just checking for and empty line and using pass
in Python!
def emptyline(self):
pass
This is the help command. As previous we declared the help sentences, so for this we look for the command, and if it is present, we print out the help_message
for the designated command!
def do_help(self, arg):
"""
List available commands with their usage.
Usage: help [command]
"""
if arg:
# Show help for a specific command
if arg in self.help_messages:
print(self.help_messages[arg])
else:
print(f"Command '{arg}' not found.")
else:
# Show help for all commands
print("\nAvailable commands:")
for command, help_message in self.help_messages.items():
print(f"{command}: {help_message}")
print("\n")
To quit the program, you have two choises. You could either run quit
or exit
to quit or exit the program when you are done!
def do_quit(self, arg):
# Exit the DuckyShell
print("Exiting DuckyShell...")
return True
def do_exit(self, arg):
# Exit the DuckyShell
print("\nExiting DuckyShell...\n")
return True
And thats all for the duckyshell.py
file!
I want other people to use this program. So I wanted to share it via writing an install.sh
file that takes care of everything!
#!/bin/bash
echo -e "Installer script for duckyshell\n\n"
# Clone the GitHub repository
echo -e "Cloning the Github repository\n"
git clone https://github.com/DanielBoye/duckyshell.git
# Copy the folder to /usr/bin
echo -e "\nCopying folders\n"
sudo cp -R duckyshell/ /usr/bin/duckyshell
# Create an alias for running duckyshell.py as 'ducky'
echo -e "Creating aliases\n"
echo "alias ducky='python3 /usr/bin/duckyshell/duckyshell.py'" >> ~/.bashrc
# Reload the bashrc file to apply the alias
echo -e "Reload the .bashrc file\n"
source ~/.bashrc
# Provide executable permissions to the program
echo -e "Make the program executable\n"
sudo chmod +x /usr/bin/duckyshell/duckyshell.py
# Cleanup: remove the cloned repository
echo -e "Clean up install\n"
rm -rf duckyshell/
# This fixes something
source ~/.bashrc
# Installation completed
echo -e "DuckyShell has been installed.\n"
echo -e "You can now use 'ducky' command to run it.\n"