Skip to content

Commit fc98e3e

Browse files
authored
Add files via upload
1 parent 50b4da6 commit fc98e3e

File tree

5 files changed

+369
-0
lines changed

5 files changed

+369
-0
lines changed

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Raven
2+
3+
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.
4+
5+
### Key Features
6+
7+
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:
8+
9+
- **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.
10+
11+
- **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.
12+
13+
- **File Sanitation**: Sanitizes the name of each uploaded file prior to being saved to disk to help prevent potential abuse.
14+
15+
- **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.
16+
17+
- **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.
18+
19+
## Usage
20+
21+
Raven is straightforward to use and includes simple command-line arguments to manage the included feature sets:
22+
23+
```bash
24+
python3 raven.py <listening_ip> <listening_port> [--allowed-ip <allowed_client_ip>] [--upload-folder <folder>] [--organize-uploads]
25+
```
26+
27+
* <listening_ip>: The IP address for our http handler to listen on
28+
* <listening_port>: The port for our http handler to listen on
29+
* --allowed-ip <allowed_client_ip>:Restrict access to our http handler by IP address (optional)
30+
* --upload-folder <folder>: "Designate the directory to save uploaded files to (default: current working directory)
31+
* --organize-uploads: Organize file uploads into subfolders by remote client IP
32+
33+
## Installation
34+
35+
Install from GitHub
36+
37+
1. Clone the Repository
38+
39+
```bash
40+
git clone https://github.com/gh0x0st/raven.git
41+
cd raven
42+
```
43+
44+
2. Install using pip3
45+
46+
```bash
47+
pip3 install .
48+
```
49+
50+
3. Add /home/USER/./local/bin to your PATH environment variable
51+
52+
```bash
53+
echo 'export PATH="/home/kali/.local/bin:$PATH"' >> ~/.zshrc
54+
source ~/.zshrc
55+
```
56+
57+
## Examples
58+
59+
Start the HTTP server on all available network interfaces, listening on port 443:
60+
61+
`raven 0.0.0.0 443`
62+
63+
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:
64+
65+
`raven 192.168.0.12 443 --allowed-ip 192.168.0.4`
66+
67+
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:
68+
69+
`raven 192.168.0.12 443 --allowed-ip 192.168.0.4 --upload-folder /tmp`
70+
71+
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:
72+
73+
`raven 192.168.0.12 443 --allowed-ip 192.168.0.4 --upload-folder /tmp --organize-uploads`
74+
75+
## License
76+
77+
This project is licensed under the MIT License - see the LICENSE file for details.
78+
79+

raven.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python3
2+
3+
from raven.__main__ import main
4+
5+
if __name__ == '__main__':
6+
main()

raven/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

raven/__main__.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import errno
5+
import os
6+
import re
7+
import sys
8+
import http.server
9+
import socketserver
10+
from datetime import datetime
11+
from ipaddress import ip_network, ip_address
12+
13+
14+
# Instantiate our FileUploadHandler class
15+
class FileUploadHandler(http.server.SimpleHTTPRequestHandler):
16+
def __init__(self, *args, **kwargs):
17+
self.upload_folder = kwargs.pop('upload_folder', None)
18+
self.allowed_ip = kwargs.pop('allowed_ip', None)
19+
self.organize_uploads = kwargs.pop('organize_uploads', False)
20+
super().__init__(*args, **kwargs)
21+
22+
# Definer our handler method for restricting access by client ip
23+
def restrict_access(self):
24+
if not self.allowed_ip:
25+
# No IP restriction, allow access
26+
return True
27+
28+
# Obtain client ip
29+
client_ip = ip_address(self.client_address[0])
30+
31+
# Parse through each entry in allowed_ips
32+
allowed_ips = self.allowed_ip.split(',')
33+
for ip in allowed_ips:
34+
ip = ip.strip()
35+
# Check if the entry is in CIDR notation
36+
if '/' in ip:
37+
try:
38+
network = ip_network(ip, strict=False)
39+
if client_ip in network:
40+
return True
41+
except ValueError:
42+
pass
43+
elif client_ip == ip_address(ip):
44+
return True
45+
46+
# If none of the addresses check out, send a 403
47+
self.send_response(403)
48+
self.end_headers()
49+
return False
50+
51+
# Define our GET handler method
52+
def do_GET(self):
53+
if self.path == '/':
54+
# Check access restrictions
55+
if not self.restrict_access():
56+
return
57+
58+
# Send HTTP response status code 200 back to the client
59+
self.send_response(200)
60+
self.send_header('Content-type', 'text/html')
61+
self.end_headers()
62+
self.wfile.write(b"""
63+
<!DOCTYPE html>
64+
<html>
65+
<head>
66+
<title>Raven File Upload</title>
67+
</head>
68+
<body>
69+
<form method="POST" enctype="multipart/form-data">
70+
<input type="file" name="file">
71+
<input type="submit" value="Upload">
72+
</form>
73+
</body>
74+
</html>
75+
""")
76+
77+
# Define our POST handler method
78+
def do_POST(self):
79+
if self.path == '/':
80+
# Check access restrictions
81+
if not self.restrict_access():
82+
return
83+
84+
# Inspect incoming multipart/form-data content
85+
content_type = self.headers['Content-Type']
86+
if content_type.startswith('multipart/form-data'):
87+
try:
88+
# Extract and parse multipart/form-data content
89+
content_length = int(self.headers['Content-Length'])
90+
form_data = self.rfile.read(content_length)
91+
92+
# Extract the boundary from the content type header
93+
boundary = content_type.split('; ')[1].split('=')[1]
94+
95+
# Split the form data using the boundary
96+
parts = form_data.split(b'--' + boundary.encode())
97+
98+
for part in parts:
99+
if b'filename="' in part:
100+
# Extract filename from Content-Disposition header
101+
headers, data = part.split(b'\r\n\r\n', 1)
102+
content_disposition = headers.decode()
103+
filename = re.search(r'filename="(.+)"', content_disposition).group(1)
104+
105+
# Sanitize the filename
106+
filename = sanitize_filename(filename)
107+
108+
# Organize uploads into subfolders by remote client IP
109+
if self.organize_uploads and self.client_address:
110+
client_ip = self.client_address[0]
111+
upload_folder = os.path.join(self.upload_folder, client_ip)
112+
os.makedirs(upload_folder, exist_ok=True)
113+
file_path = os.path.join(upload_folder, filename)
114+
else:
115+
upload_folder = self.upload_folder # Use the original upload folder
116+
file_path = os.path.join(upload_folder, filename)
117+
118+
# Generate a unique filename in case the file already exists
119+
file_path = prevent_clobber(upload_folder, filename)
120+
121+
# Save the uploaded file in binary mode
122+
with open(file_path, 'wb') as f:
123+
f.write(data)
124+
125+
# Send HTTP response status code 200 back to the client
126+
self.send_response(200)
127+
self.end_headers()
128+
self.wfile.write(b"""
129+
<!DOCTYPE html>
130+
<html>
131+
<head>
132+
<meta http-equiv="refresh" content="3;url=/">
133+
</head>
134+
<body>
135+
<p>File uploaded successfully. Redirecting in 3 seconds...</p>
136+
</body>
137+
</html>
138+
""")
139+
140+
# Print the path where the uploaded file was saved
141+
now = datetime.now().strftime("%d/%b/%Y %H:%M:%S")
142+
print(f"{self.client_address[0]} - - [{now}] \"File saved {file_path}\"")
143+
return
144+
except Exception as e:
145+
print(f"Error processing the uploaded file: {str(e)}")
146+
147+
# Send HTTP response status code 400 back to the client
148+
self.send_response(400)
149+
self.end_headers()
150+
self.wfile.write(b'No file uploaded.')
151+
152+
153+
# Normalizes the filename, then remove any characters that are not letters, numbers, underscores, dots, or hyphens
154+
def sanitize_filename(filename):
155+
pass1 = os.path.normpath(filename)
156+
final = re.sub(r'[^\w.-]', '_', pass1)
157+
return final
158+
159+
160+
# Appends a file name with an incrementing number if it happens to exist already
161+
def prevent_clobber(upload_folder, filename):
162+
file_path = os.path.join(upload_folder, filename)
163+
counter = 1
164+
165+
while os.path.exists(file_path):
166+
base_name, file_extension = os.path.splitext(filename)
167+
new_filename = f"{base_name}_{counter}{file_extension}"
168+
file_path = os.path.join(upload_folder, new_filename)
169+
counter += 1
170+
171+
return file_path
172+
173+
174+
def main():
175+
# Build the parser
176+
parser = argparse.ArgumentParser(
177+
description="A lightweight file upload service used for penetration testing and incident response.",
178+
usage="python3 raven.py <listening_ip> <listening_port> [--allowed-ip <allowed_client_ip>] [--upload-folder <upload_directory>] [--organize-uploads]"
179+
)
180+
181+
# Configure our arguments
182+
parser.add_argument("host", help="The IP address for our http handler to listen on")
183+
parser.add_argument("port", type=int, help="The port for our http handler to listen on")
184+
parser.add_argument("--allowed-ip", help="Restrict access to our http handler by IP address (optional)")
185+
parser.add_argument("--upload-folder", default=os.getcwd(), help="Designate the directory to save uploaded files to (default: current working directory)")
186+
parser.add_argument("--organize-uploads", action="store_true", help="Organize file uploads into subfolders by remote client IP")
187+
188+
# Parse the command-line arguments
189+
args = parser.parse_args()
190+
191+
# Check if no arguments were provided
192+
if len(sys.argv) == 1:
193+
parser.print_help()
194+
sys.exit(1)
195+
196+
# Initializing variables
197+
host = args.host
198+
port = args.port
199+
allowed_ip = args.allowed_ip
200+
upload_folder = args.upload_folder
201+
organize_uploads = args.organize_uploads
202+
server = None
203+
204+
try:
205+
# Check if the specified upload folder exists, if not try to create it
206+
if not os.path.exists(upload_folder):
207+
os.makedirs(upload_folder)
208+
209+
# Create our HTTP request handler with the new parameter
210+
server = socketserver.TCPServer((host, port), lambda *args, **kwargs: FileUploadHandler(*args, **kwargs, upload_folder=upload_folder, allowed_ip=allowed_ip, organize_uploads=organize_uploads))
211+
212+
# Output HTTP request handler details
213+
print(f"[*] Serving HTTP on {host} port {port} (http://{host}:{port}/)")
214+
215+
# Output additional details
216+
if allowed_ip:
217+
print(f"[*] Listener access is restricted to {allowed_ip}")
218+
else:
219+
print(f"[*] Listener access is unrestricted")
220+
221+
if organize_uploads:
222+
print(f"[*] Uploads will be organized by client IP in {upload_folder}")
223+
else:
224+
print(f"[*] Uploads will be saved in {upload_folder}")
225+
226+
# Start our HTTP request handler
227+
server.serve_forever()
228+
except KeyboardInterrupt:
229+
print("\nKeyboard interrupt received, exiting.")
230+
except OSError as ose:
231+
if ose.errno == errno.EADDRNOTAVAIL:
232+
print(f"[!] The IP address '{host}' does not appear to be available on this system")
233+
else:
234+
print(f"[!] {str(ose)}")
235+
except Exception as ex:
236+
print(f"[!] {str(ex)}")
237+
finally:
238+
if server:
239+
server.server_close()
240+
241+
242+
if __name__ == '__main__':
243+
main()

setup.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
3+
from setuptools import setup, find_packages
4+
5+
setup(
6+
name='raven',
7+
version='1.0.0',
8+
url='https://github.com/gh0x0st/raven',
9+
author='Tristram',
10+
author_email="discord:blueteamer",
11+
description='A lightweight file upload service used for penetration testing and incident response.',
12+
long_description=open('README.md').read(),
13+
long_description_content_type='text/markdown',
14+
license="MIT",
15+
keywords=['file upload', 'penetration testing', 'HTTP server', 'SimpleHTTPServer'],
16+
packages=find_packages(),
17+
install_requires=[],
18+
entry_points={
19+
'console_scripts': ['raven = raven.__main__:main'],
20+
},
21+
classifiers=[
22+
"Development Status :: 3 - Alpha",
23+
"License :: OSI Approved :: MIT License",
24+
"Programming Language :: Python :: 3",
25+
"Programming Language :: Python :: 3.6",
26+
"Programming Language :: Python :: 3.7",
27+
"Programming Language :: Python :: 3.8",
28+
"Programming Language :: Python :: 3.9",
29+
"Programming Language :: Python :: 3.10",
30+
"Operating System :: OS Independent",
31+
"Intended Audience :: System Administrators",
32+
"Intended Audience :: Security",
33+
"Topic :: Utilities",
34+
"Topic :: Security",
35+
"Topic :: Security :: Penetration Testing",
36+
"Topic :: System :: Networking",
37+
"Topic :: System :: Systems Administration",
38+
],
39+
python_requires='>=3.6',
40+
)

0 commit comments

Comments
 (0)