pax_global_header00006660000000000000000000000064147412332110014510gustar00rootroot0000000000000052 comment=5c967bd9968993c3ffeccc169791c55d787541db raven-1.1.0/000077500000000000000000000000001474123321100126225ustar00rootroot00000000000000raven-1.1.0/LICENSE000066400000000000000000000020511474123321100136250ustar00rootroot00000000000000MIT License Copyright (c) 2023 Tristram Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. raven-1.1.0/README.md000066400000000000000000000141071474123321100141040ustar00rootroot00000000000000# Raven Raven is a Python tool that extends the capabilities of the `http.server` Python module by offering a self-contained file upload web server. While the common practice is to use `python3 -m http.server 80` to serve files for remote client downloads, Raven addresses the need for a similar solution when you need the ability to receive files from remote clients. This becomes especially valuable in scenarios such as penetration testing and incident response procedures when protocols such as SMB may not be a viable option. ### Key Features While the majority of the hard work is already being handled by the http.server module, it presents us with an opportunity to implement additional security and ease of use features without overcomplicating the overall implementation. These features currently include: - **IP Access Restrictions**: Optionally grants the ability to restrict access based on client IP addresses. You can define this access via a single IP, a comma-delimited list or by using CIDR notation. - **Organized Uploads**: Optionally organizes uploaded files into subfolders based on the remote client's IP address in a named or current working directory. Otherwise the default behavior will upload files in the current working directory. - **File Sanitation**: Sanitizes the name of each uploaded file prior to being saved to disk to help prevent potential abuse. - **Clobbering**: Verifies that the file does not already exist before it's written to disk. If it already exists, an incrementing number is appended to the filename to prevent clashes and ensure no data is overwritten. - **Detailed Logging**: Raven provides detailed logging of file uploads and interaction with the http server, including the status codes sent back to a client, its IP address, timestamp, and the saved file's location in the event a file is uploaded. ## Installation **Kali Linux (APT Package)** If you are using Kali Linux, you can easily install Raven using APT. ```bash sudo apt update sudo apt install raven ``` **Manual Installation (Git Clone)** Alternatively, if you prefer to manually install Raven or if you are using an operating system other than Kali Linux, you can use Git to clone the Raven repository from GitHub. ```bash git clone https://github.com/gh0x0st/raven.git ``` ## Usage Raven is straightforward to use and includes simple command-line arguments to manage the included feature sets: ```bash raven [--allowed-ip ] [--upload-folder ] [--organize-uploads] ``` * : The IP address for our http handler to listen on * : The port for our http handler to listen on * --allowed-ip :Restrict access to our http handler by IP address (optional) * --upload-folder : "Designate the directory to save uploaded files to (default: current working directory) * --organize-uploads: Organize file uploads into subfolders by remote client IP ## Examples Start the HTTP server on all available network interfaces, listening on port 443: `raven 0.0.0.0 443` Start the HTTP server on all on a specific interface (192.168.0.12), listening on port 443 and restrict access to 192.168.0.4: `raven 192.168.0.12 443 --allowed-ip 192.168.0.4` Start the HTTP server on all on a specific interface (192.168.0.12), listening on port 443, restrict access to 192.168.0.4 and save uploaded files to /tmp: `raven 192.168.0.12 443 --allowed-ip 192.168.0.4 --upload-folder /tmp` Start the HTTP server on all on a specific interface (192.168.0.12), listening on port 443, restrict access to 192.168.0.4 and save uploaded files to /tmp organized by remote client ip: `raven 192.168.0.12 443 --allowed-ip 192.168.0.4 --upload-folder /tmp --organize-uploads` ## Scripted Uploads Uploading files using PowerShell: ```powershell function Invoke-SendRaven { param ( [string]$Uri, [string]$FilePath ) # Target File $File = Get-Item $FilePath $Content = [System.IO.File]::ReadAllBytes($File.FullName) $Boundary = [System.Guid]::NewGuid().ToString() # Request Headers $Headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $Headers.Add("Content-Type", "multipart/form-data; boundary=$Boundary") # Create the request $Request = [System.Net.WebRequest]::Create($Uri) $Request.Method = "POST" $Request.ContentType = "multipart/form-data; boundary=$Boundary" # Request Body $Stream = $Request.GetRequestStream() $Encoding = [System.Text.Encoding]::ASCII $Stream.Write($Encoding.GetBytes("--$Boundary`r`n"), 0, ("--$Boundary`r`n").Length) $Stream.Write($Encoding.GetBytes("Content-Disposition: form-data; name=`"file`"; filename=`"$($File.Name)`"`r`n"), 0, ("Content-Disposition: form-data; name=`"file`"; filename=`"$($File.Name)`"`r`n").Length) $Stream.Write($Encoding.GetBytes("Content-Type: application/octet-stream`r`n`r`n"), 0, ("Content-Type: application/octet-stream`r`n`r`n").Length) $Stream.Write($Content, 0, $Content.Length) $Stream.Write($Encoding.GetBytes("`r`n--$Boundary--`r`n"), 0, ("`r`n--$Boundary--`r`n").Length) $Stream.Close() # Upload File $Response = $Request.GetResponse() $Response.Close() } # Usage Invoke-SendRaven -Uri http://192.168.0.12:443/ -FilePath $ENV:TEMP\HostRecon.txt ``` Uploading files using Python3: ```python #!/usr/bin/env python3 import requests import uuid # Listener url = "http://192.168.0.12:443/" # Target File file_path = "/path/to/file" file_name = file_path.split("/")[-1] with open(file_path, "rb") as file: file_content = file.read() boundary = str(uuid.uuid4()) # Request Headers headers = { "Content-Type": f"multipart/form-data; boundary={boundary}" } # Request Body body = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="file"; filename="{file_name}"\r\n' "Content-Type: application/octet-stream\r\n\r\n" f"{file_content.decode('ISO-8859-1')}\r\n" f"--{boundary}--\r\n" ) # Upload File requests.post(url, headers=headers, data=body) ``` ## License This project is licensed under the MIT License - see the LICENSE file for details. raven-1.1.0/raven.py000066400000000000000000000001371474123321100143100ustar00rootroot00000000000000#!/usr/bin/env python3 from raven.__main__ import main if __name__ == '__main__': main() raven-1.1.0/raven/000077500000000000000000000000001474123321100137355ustar00rootroot00000000000000raven-1.1.0/raven/__init__.py000066400000000000000000000000011474123321100160350ustar00rootroot00000000000000 raven-1.1.0/raven/__main__.py000066400000000000000000000365511474123321100160410ustar00rootroot00000000000000#!/usr/bin/env python3 import io import os import re import sys import html import errno import urllib import argparse import http.server import socketserver from http import HTTPStatus from datetime import datetime from ipaddress import (ip_network, ip_address) # Instantiate our FileUploadHandler class class FileUploadHandler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): # Overwrite default versions to minimize fingerprinting self.server_version = "nginx" self.sys_version = "" self.upload_dir = kwargs.pop('upload_dir', None) self.allowed_ip = kwargs.pop('allowed_ip', None) self.organize_uploads = kwargs.pop('organize_uploads', False) super().__init__(*args, **kwargs) # Define our handler method for restricting access by client ip def restrict_access(self): if not self.allowed_ip: # Access is permitted by default return True # Obtain the client ip client_ip = ip_address(self.client_address[0]) # Cycle through each entry in allowed_ips for permitted access allowed_ips = self.allowed_ip.split(',') for ip in allowed_ips: ip = ip.strip() # Check if the entry is in CIDR notation if '/' in ip: try: network = ip_network(ip, strict=False) if client_ip in network: return True except ValueError: pass elif client_ip == ip_address(ip): return True # The client ip is not permitted access to the handler # Respond back to the client with a 403 status code self.send_response(403) self.end_headers() return False # Below is a slightly modified version of # http.server.SimpleTCPRequestHandler.list_directory # The method has been modified to show the upload form # above the directory listing, as well as a change in how # link names are provided; we need the correct paths (using fullname) # to serve files requested via browsers # Additionally, we're not using the original method so that we # are able to respond with the correct Content-Length header # Each individual change is prefixed with a NOTE def list_dirs(self): # Copyright (c) 2001 Python Software Foundation; All Rights Reserved """Helper to produce a directory listing (absent index.html). Return value is either a file object, or None (indicating an error). In either case, the headers are sent, making the interface the same as for send_head(). """ # NOTE: overriding the original implementation to # append an upload form above the directory listing upload_form = """

File upload



""" try: # NOTE: quick and dirty override to the original implementation # to skip the missing favicon.ico if 'favicon.ico' in self.translate_path(self.path): try: self.send_error( HTTPStatus.NOT_FOUND, "Resource not found") # Worst case scenario, we send a 404 twice except BrokenPipeError: self.send_error( HTTPStatus.NOT_FOUND, "Resource not found") finally: return None # NOTE: use http.server.SimpleHTTPRequestHandler.translate_path # since we already have access to it dir_list = os.listdir(self.translate_path(self.path)) # NOTE: add ValueError to exception list to avoid # processing NULL characters except (OSError, ValueError): self.send_error( HTTPStatus.NOT_FOUND, "Resource not found") return None dir_list.sort(key=lambda a: a.lower()) r = [] try: displaypath = urllib.parse.unquote(self.path, errors='surrogatepass') except UnicodeDecodeError: displaypath = urllib.parse.unquote(self.path) displaypath = html.escape(displaypath, quote=False) enc = sys.getfilesystemencoding() title = f'Directory listing for {displaypath}' r.append('') r.append('') r.append('') r.append(f'') r.append(f'Raven File Upload/Download\n') r.append(f'\n{upload_form}

{title}

') r.append('
\n
    ') for name in dir_list: fullname = os.path.join(self.path, name) displayname = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): displayname = name + "/" linkname = name + "/" if os.path.islink(fullname): displayname = name + "@" # Note: a link to a directory displays with @ and links with / # NOTE: overriding the original implementation to use # fullname instead of linkname so files are served # correctly to browser clients r.append('
  • %s
  • ' % (urllib.parse.quote(fullname, errors='surrogatepass'), html.escape(displayname, quote=False))) r.append('
\n
\n\n\n') encoded = '\n'.join(r).encode(enc, 'surrogateescape') f = io.BytesIO() f.write(encoded) f.seek(0) self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html; charset=%s" % enc) self.send_header("Content-Length", str(len(encoded))) self.end_headers() return f # Serve requested file def serve_file(self, canon_name, basename): with open(canon_name, 'rb') as f: content = f.read() self.send_response(HTTPStatus.OK) self.send_header('Content-Type', 'application/octet-stream') self.send_header('Content-Disposition', 'attachment; filename={}'.format(basename)) self.send_header('Content-Length', str(len(content))) self.end_headers() self.wfile.write(content) # Define our GET handler method def do_GET(self): BASENAME = os.path.basename(self.path) CANON_FILENAME = self.translate_path(self.path) if not os.path.isfile(CANON_FILENAME): # Check if we are restricting access if not self.restrict_access(): return try: self.wfile.write(self.list_dirs().read()) except AttributeError: pass else: self.serve_file(CANON_FILENAME, BASENAME) # Define our POST handler method def do_POST(self): # Check if we are restricting access if not self.restrict_access(): return # Inspect incoming multipart/form-data content content_type = self.headers['Content-Type'] if content_type.startswith('multipart/form-data'): try: # Extract and parse multipart/form-data content content_length = int(self.headers['Content-Length']) form_data = self.rfile.read(content_length) # Extract the boundary from the content type header boundary = content_type.split('; ')[1].split('=')[1] # Split the form data using the boundary parts = form_data.split(b'--' + boundary.encode()) for part in parts: if b'filename="' in part: # Extract the filename from Content-Disposition header headers, data = part.split(b'\r\n\r\n', 1) content_disposition = headers.decode() filename = re.search(r'filename="(.+)"', content_disposition).group(1) # Sanitize the filename based on our requirements filename = sanitize_filename(filename) # Organize uploads into subfolders by client IP otherwise use the default if self.organize_uploads and self.client_address: client_ip = self.client_address[0] upload_dir = os.path.join(self.upload_dir, client_ip) os.makedirs(upload_dir, exist_ok=True) file_path = os.path.join(upload_dir, filename) else: upload_dir = self.upload_dir file_path = os.path.join(upload_dir, filename) # Generate a unique filename in case the file already exists file_path = prevent_clobber(upload_dir, filename) # Save the uploaded file in binary mode so we don't corrupt any content with open(file_path, 'wb') as f: f.write(data[:-2]) # Respond back to the client with a 200 status code self.send_response(200) self.end_headers() # Send an HTML response to the client for redirection self.wfile.write(b""" Redirecting...

File uploaded successfully. Redirecting in 3 seconds...

""") # Print the path where the uploaded file was saved to the terminal now = datetime.now().strftime("%d/%b/%Y %H:%M:%S") print(f"{self.client_address[0]} - - [{now}] \"File saved {file_path}\"") return except Exception as e: print(f"Error processing the uploaded file: {str(e)}") # Something bad happened if we get to this point # Error details are provided by http.server on the terminal # Respond back to the client with a 400 status code self.send_response(400) self.end_headers() # Normalizes the filename, then remove any characters that are not letters, numbers, underscores, dots, or hyphens def sanitize_filename(filename): normalized = os.path.normpath(filename) sanitized = re.sub(r'[^\w.-]', '_', normalized) return sanitized # Appends a file name with an incrementing number if it happens to exist already def prevent_clobber(upload_dir, filename): file_path = os.path.join(upload_dir, filename) counter = 1 # Keep iterating until a unique filename is found while os.path.exists(file_path): base_name, file_extension = os.path.splitext(filename) new_filename = f"{base_name}_{counter}{file_extension}" file_path = os.path.join(upload_dir, new_filename) counter += 1 return file_path # Generates the epilog content for argparse, providing usage examples def generate_epilog(): examples = [ "examples:", " Start the HTTP server on all available network interfaces, listening on port 443", " raven 0.0.0.0 443\n", " Bind the HTTP server to a specific address (192.168.0.12), listening on port 443, and restrict access to 192.168.0.4", " raven 192.168.0.12 443 --allowed-ip 192.168.0.4\n", " Bind the HTTP server to a specific address (192.168.0.12), listening on port 443, restrict access to 192.168.0.4, and save uploaded files to /tmp", " raven 192.168.0.12 443 --allowed-ip 192.168.0.4 --upload-dir /tmp\n", " Bind the HTTP server to a specific address (192.168.0.12), listening on port 443, restrict access to 192.168.0.4, and save uploaded files to /tmp organized by remote client IP", " raven 192.168.0.12 443 --allowed-ip 192.168.0.4 --upload-dir /tmp --organize-uploads", ] return "\n".join(examples) def main(): # Build the parser parser = argparse.ArgumentParser( description="A lightweight file upload service used for penetration testing and incident response.", usage="raven [lhost] [lport] [--allowed-ip ] [--upload-dir ] [--organize-uploads]", epilog=generate_epilog(), formatter_class=argparse.RawDescriptionHelpFormatter ) # Configure our arguments parser.add_argument("lhost", nargs="?", default="0.0.0.0", help="The IP address for our HTTP handler to listen on (default: listen on all interfaces)") parser.add_argument("lport", nargs="?", type=int, default=8080, help="The port for our HTTP handler to listen on (default: 8080)") parser.add_argument("--allowed-ip", help="Restrict access to our HTTP handler by IP address (optional)") parser.add_argument("--upload-dir", default=os.getcwd(), help="Designate the directory to save uploaded files to (default: current working directory)") parser.add_argument("--organize-uploads", action="store_true", help="Organize file uploads into subfolders by remote client IP") # Parse the command-line arguments args = parser.parse_args() # Initializing configuration variables host = args.lhost port = args.lport allowed_ip = args.allowed_ip upload_dir = args.upload_dir organize_uploads = args.organize_uploads server = None try: # Check if the specified upload folder exists, if not try to create it if not os.path.exists(upload_dir): os.makedirs(upload_dir) # set SO_REUSEADDR (See man:socket(7)) socketserver.TCPServer.allow_reuse_address = True # Create an HTTP server instance with our custom request handling with socketserver.TCPServer((host, port), lambda *args, **kwargs: FileUploadHandler(*args, **kwargs, upload_dir=upload_dir, allowed_ip=allowed_ip, organize_uploads=organize_uploads)) as server: # Print our handler details to the terminal print(f"[*] Serving HTTP on {host} port {port} (http://{host}:{port}/)") # Print additional details to the terminal if allowed_ip: print(f"[*] Listener access is restricted to {allowed_ip}") else: print(f"[*] Listener access is unrestricted") if organize_uploads: print(f"[*] Uploads will be organized by client IP in {upload_dir}") else: print(f"[*] Uploads will be saved in {upload_dir}") # Start the HTTP server and keep it running until we stop it server.serve_forever() except KeyboardInterrupt: print("\nKeyboard interrupt received, exiting.") except OSError as ose: if ose.errno == errno.EADDRNOTAVAIL: print(f"[!] The IP address '{host}' does not appear to be available on this system") exit(ose.errno) else: print(f"[!] {str(ose)}") exit(ose.errno) except Exception as ex: print(f"[!] {str(ex)}") exit(ose.errno) finally: if server: server.server_close() if __name__ == '__main__': main() raven-1.1.0/setup.py000066400000000000000000000030301474123321100143300ustar00rootroot00000000000000#!/usr/bin/env python3 from setuptools import setup, find_packages setup( name='raven', version='1.1.0', url='https://github.com/gh0x0st/raven', author='Tristram', author_email="discord:blueteamer", description='A lightweight file upload service used for penetration testing and incident response.', long_description=open('README.md').read(), long_description_content_type='text/markdown', license="MIT", keywords=['file upload', 'penetration testing', 'HTTP server', 'SimpleHTTPServer'], packages=find_packages(), install_requires=[], entry_points={ 'console_scripts': ['raven = raven.__main__:main'], }, classifiers=[ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", "Intended Audience :: System Administrators", "Intended Audience :: Security", "Topic :: Utilities", "Topic :: Security", "Topic :: Security :: Penetration Testing", "Topic :: System :: Networking", "Topic :: System :: Systems Administration", ], python_requires='>=3.6', )