Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancing the FTP protocol #40

Merged
merged 17 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions nxc/protocols/ftp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from nxc.config import process_secret
from nxc.connection import *
from nxc.logger import NXCAdapter
Expand Down Expand Up @@ -80,16 +81,42 @@ def plaintext_login(self, username, password):
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(cred_id, host_id)

if username in ["anonymous", ""] and password in ["", "-"]:
if username in ["anonymous", ""]:
self.logger.success(f"{username}:{process_secret(password)} {highlight('- Anonymous Login!')}")
else:
self.logger.success(f"{username}:{process_secret(password)}")

NeffIsBack marked this conversation as resolved.
Show resolved Hide resolved
if self.args.ls:
files = self.list_directory_full()
self.logger.display(f"Directory Listing")
for file in files:
self.logger.highlight(file)
# If the default directory is specified, then we will list the current directory
if self.args.ls == ".":
files = self.list_directory_full()
# If files is false, then we encountered an exception
if not files:
return False
# If there are files, then we can list the files
self.logger.display(f"Directory Listing")
for file in files:
self.logger.highlight(file)
else:
# If the default directory is not specified, then we will list the specified directory
self.logger.display(f"Directory Listing for {self.args.ls}")
# Change to the specified directory
try:
self.conn.cwd(self.args.ls)
except error_perm as error_message:
self.logger.fail(f"Failed to change directory. Response: ({error_message})")
self.conn.close()
return False
# List the files in the specified directory
files = self.list_directory_full()
for file in files:
self.logger.highlight(file)

if self.args.get:
self.get_file(f"{self.args.get}")

if self.args.put:
self.put_file(self.args.put[0], self.args.put[1])

if not self.args.continue_on_success:
self.conn.close()
Expand All @@ -101,9 +128,56 @@ def list_directory_full(self):
# in the future we can use mlsd/nlst if we want, but this gives a full output like `ls -la`
# ftplib's "dir" prints directly to stdout, and "nlst" only returns the folder name, not full details
files = []
self.conn.retrlines("LIST", callback=files.append)
try:
self.conn.retrlines("LIST", callback=files.append)
except error_perm as error_message:
self.logger.fail(f"Failed to list directory. Response: ({error_message})")
self.conn.close()
return False
return files

def get_file(self, filename):
# Extract the filename from the path
downloaded_file = filename.split("/")[-1]
try:
# Check if the current connection is ASCII (ASCII does not support .size())
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Check if the file exists
self.conn.size(filename)
# Attempt to download the file
self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write)
except error_perm as error_message:
self.logger.fail(f"Failed to download the file. Response: ({error_message})")
self.conn.close()
return False
except FileNotFoundError:
self.logger.fail(f"Failed to download the file. Response: (No such file or directory.)")
self.conn.close()
return False
# Check if the file was downloaded
if os.path.isfile(downloaded_file):
self.logger.success(f"Downloaded: {filename}")
else:
self.logger.fail(f"Failed to download: {filename}")

def put_file(self, local_file, remote_file):
try:
# Attempt to upload the file
self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb"))
except error_perm as error_message:
self.logger.fail(f"Failed to upload file. Response: ({error_message})")
return False
except FileNotFoundError:
self.logger.fail(f"Failed to upload file. {local_file} does not exist locally.")
return False
# Check if the file was uploaded
if self.conn.size(remote_file) > 0:
self.logger.success(f"Uploaded: {local_file} to {remote_file}")
else:
self.logger.fail(f"Failed to upload: {local_file} to {remote_file}")

def supported_commands(self):
raw_supported_commands = self.conn.sendcmd("HELP")
supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist]
Expand Down
4 changes: 3 additions & 1 deletion nxc/protocols/ftp/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ def proto_args(parser, std_parser, module_parser):
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)")

cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access")
cgroup.add_argument("--ls", action="store_true", help="List files in the directory")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory")
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
NeffIsBack marked this conversation as resolved.
Show resolved Hide resolved
return parser
1 change: 1 addition & 0 deletions tests/data/test_file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test file used to test FTP upload and download
2 changes: 2 additions & 0 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ netexec ssh TARGET_HOST -u USERNAME -p '' --key-file data/test_key.priv
##### FTP- Default test passwords and random key; switch these out if you want correct authentication
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --ls
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --put data/test_file.txt
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --get test_file.txt
netexec ftp TARGET_HOST -u data/test_users.txt -p test_passwords.txt --no-bruteforce
netexec ftp TARGET_HOST -u data/test_users.txt -p test_passwords.txt --no-bruteforce --continue-on-success
netexec ftp TARGET_HOST -u data/test_users.txt -p test_passwords.txt