pendora-box/pendora-box.py

521 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
import hashlib
import requests
import sys
from os import geteuid, chdir
import subprocess
from io import BytesIO
import zipfile
import rpmfile
import gzip
from bs4 import BeautifulSoup
import re
def compute_file_hash(filepath):
with open(filepath, 'rb') as file_for_hash:
data = file_for_hash.read()
filesize = len(data)
# Github sha value is computed as such:
return hashlib.sha1(b"blob " + bytes(str(filesize), 'utf-8') + b"\0" + data).hexdigest()
def get_master_info(repo, filepath, credz):
url = f"https://api.github.com/repos/{repo}/contents/{filepath}"
r = requests.get(url, auth=credz)
sha = r.json()['sha']
dlurl = r.json()['download_url']
return sha, dlurl
def get_last_release_info(repo, credz):
url = f"https://api.github.com/repos/{repo}/releases"
r = requests.get(url, auth=credz)
for release in r.json():
if not release['draft'] and not release['prerelease']:
return release['tag_name']
def extract_bin(archtype, binpath, destpath, content):
ioobj = BytesIO(content) # Get a File object from bytes ;)
if archtype == 'zip':
with zipfile.ZipFile(ioobj, "r") as zf:
filenames = zf.namelist()
for fn in filenames:
if binpath in fn:
with open(destpath, 'wb') as f:
f.write(zf.read(fn))
break
elif archtype == 'rpm':
with rpmfile.RPMFile(fileobj=ioobj) as rpm:
# Extract a fileobject from the archive
fd = rpm.extractfile(binpath)
with open(destpath, 'wb') as f:
f.write(fd.read())
elif archtype == 'gz':
with open(destpath, 'wb') as f:
f.write(gzip.decompress(content))
ioobj.close()
def githubmastersync(reponame, filepaths, credz):
for filepath in filepaths:
localfile = Path('files').joinpath(Path(filepath).name)
print(f" * {localfile} ", end='')
lastsha, dlurl = get_master_info(reponame, filepath, credz)
if not localfile.exists():
r = requests.get(dlurl)
with open(localfile, 'wb') as f:
f.write(r.content)
print('-> Installed! ;)')
else:
sha = compute_file_hash(localfile)
if sha == lastsha:
print('-> Up-to-date.')
else:
r = requests.get(dlurl)
with open(localfile, 'wb') as f:
f.write(r.content)
print('-> Updated!')
def githubreleasesync(reponame, repoinfo, credz):
local_version = repoinfo['local_version']
last_version = get_last_release_info(reponame, credz)
short_version = last_version.replace('v', '')
nobeta_version = last_version.replace('p1-Beta', '') # See https://github.com/PowerShell/Win32-OpenSSH/releases
filenames = repoinfo['files']
for filename in filenames:
if isinstance(filename, dict):
inpath = filename['inpath']
outpath = filename['outpath']
filename = filename['filename']
filename = filename.replace('{last_version}', last_version).replace('{short_version}', short_version).replace('{nobeta_version}', nobeta_version)
localfile = Path('files').joinpath(outpath)
print(f" * {localfile} ", end='')
else:
filename = filename.replace('{last_version}', last_version).replace('{short_version}', short_version).replace('{nobeta_version}', nobeta_version)
localfile = Path('files').joinpath(Path(filename).name)
print(f" * {localfile} ", end='')
if filename.endswith('.gz'):
is_gz, is_zip = True, False
elif filename.endswith('.zip'):
is_gz, is_zip = False, True
else:
is_gz, is_zip = False, False
urldl = f'https://github.com/{reponame}/releases/download/{last_version}/{filename}'
if not localfile.exists():
content = requests.get(urldl, auth=credz).content
if is_gz:
extract_bin('gz', inpath, localfile, content)
elif is_zip:
extract_bin('zip', inpath, localfile, content)
else:
with open(localfile, 'wb') as f:
f.write(content)
print('-> Installed! ;)')
else:
if local_version == last_version:
print('-> Up-to-date.')
else:
content = requests.get(urldl, auth=credz).content
if is_gz:
extract_bin('gz', inpath, localfile, content)
elif is_zip:
extract_bin('zip', inpath, localfile, content)
else:
with open(localfile, 'wb') as f:
f.write(content)
print('-> Updated!')
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['githubreleasesync'][reponame]['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
def ncatsync(conf):
r = requests.get('https://nmap.org/dist/')
last_version = r.text.split('The latest Nmap release is version ')[1].split('.\n')[0]
local_version = conf['local_version']
for filename in conf['files']:
localfile = Path('files').joinpath(Path(filename).name)
print(f" * {localfile} ", end='')
if filename == "ncat.exe":
archtype = 'zip'
binpath = 'ncat.exe'
destpath = 'files/ncat.exe'
urldl = f'https://nmap.org/dist/nmap-{last_version}-win32.zip'
elif filename == "ncat":
archtype = 'rpm'
binpath = './usr/bin/ncat'
destpath = 'files/ncat'
urldl = f'https://nmap.org/dist/ncat-{last_version}-1.x86_64.rpm'
if not localfile.exists():
content = requests.get(urldl).content
extract_bin(archtype, binpath, destpath, content)
print('-> Installed! ;)')
else:
if local_version == last_version:
print('-> Up-to-date.')
else:
content = requests.get(urldl).content
extract_bin(archtype, binpath, destpath, content)
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['ncat']['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
print('-> Updated!')
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['ncat']['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
def netcatsync(conf):
# https://eternallybored.org/misc/netcat/ seems to be the most stable from all tested. (04/2022)
r = requests.get('https://eternallybored.org/misc/netcat/')
soup = BeautifulSoup(r.content, 'lxml')
versions = []
for link in soup.find_all('a', {'href': re.compile('netcat.+win.+')}):
version = link['href'].split('-')[-1].split('.zip')[0]
versions.append(version)
last_version = max(versions)
local_version = conf['local_version']
for filename in conf['files']:
# nc64.exe is also available in the archive feel free to add it.
localfile = Path('files').joinpath(Path(filename).name)
print(f" * {localfile} ", end='')
archtype = 'zip'
binpath = filename
destpath = f'files/{filename}'
urldl = f'https://eternallybored.org/misc/netcat/netcat-win32-{last_version}.zip'
if not localfile.exists():
content = requests.get(urldl).content
extract_bin(archtype, binpath, destpath, content)
print('-> Installed! ;)')
else:
if local_version == last_version:
print('-> Up-to-date.')
else:
content = requests.get(urldl).content
extract_bin(archtype, binpath, destpath, content)
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['netcat']['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
print('-> Updated!')
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['netcat']['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
def plinksync(conf):
r = requests.get('https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html')
last_version = r.text.split('PuTTY.\nCurrently this is ')[1].split(', ')[0]
local_version = conf['local_version']
for filename in conf['files']:
localfile = Path('files').joinpath(Path(filename).name)
print(f" * {localfile} ", end='')
urldl = 'https://the.earth.li/~sgtatham/putty/latest/w32/plink.exe'
if not localfile.exists():
content = requests.get(urldl).content
with open(localfile, 'wb') as f:
f.write(content)
print('-> Installed! ;)')
else:
if local_version == last_version:
print('-> Up-to-date.')
else:
content = requests.get(urldl).content
with open(localfile, 'wb') as f:
f.write(content)
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['plink']['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
print('-> Updated!')
with open("config.json", "r") as jsonfile:
data = json.load(jsonfile)
data['plink']['local_version'] = last_version
with open("config.json", "w") as jsonfile:
json.dump(data, jsonfile, indent=4)
def update(config):
print("Updating...")
with open("credz.json", "r") as jsonfile:
credz = json.load(jsonfile)
credz = (credz['username'], credz['token'])
for reponame, filepaths in config['githubmastersync'].items():
githubmastersync(reponame, filepaths, credz)
for reponame, repoinfo in config['githubreleasesync'].items():
githubreleasesync(reponame, repoinfo, credz)
# ncatsync(config['ncat'])
netcatsync(config['netcat'])
plinksync(config['plink'])
make_executable()
def make_executable():
path = Path('files')
for fpath in path.glob("*"):
if fpath.name != '.gitkeep':
fpath.chmod(0o777)
def yes_or_no(question, default="yes"):
"""Ask a yes/no question via input() and return their answer.
"question" is a string that is presented to the user.
"default" is the presumed answer if the user just hits <Enter>.
It must be "yes" (the default), "no" or None (meaning
an answer is required of the user).
The "answer" return value is True for "yes" or False for "no".
"""
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
if default is None:
prompt = " [y/n] "
elif default == "yes":
prompt = " [Y/n] "
elif default == "no":
prompt = " [y/N] "
else:
raise ValueError("invalid default answer: '%s'" % default)
while True:
print(question + prompt)
choice = input().lower()
if default is not None and choice == "":
return valid[default]
elif choice in valid:
return valid[choice]
else:
print("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
def rmtree(root):
for p in root.iterdir():
if p.is_dir():
rmtree(p)
else:
p.unlink()
root.rmdir()
def print_menu(menu_options):
for key in menu_options.keys():
print(key, '->', menu_options[key])
def is_sudo():
return True if geteuid() == 0 else False
def ask_port(default_port):
while True:
try:
choice = input(f'What port would you like to listen on ? [{default_port}]: ')
if choice == '':
port = default_port
else:
port = int(choice)
break
except ValueError:
print('Wrong input. Please enter a number ...')
return port
def listen_http(files_dir):
port = ask_port(80)
ips = get_ips()
print('The HTTP server is about to start.\nA good start ;) :')
for iname in ips:
if port == 80:
print(f' -> {iname}: http://{ips[iname]}/linpeas.sh')
else:
print(f' -> {iname}: http://{ips[iname]}:{port}/linpeas.sh')
cmd = ['python', '-m', 'http.server', '-d', files_dir, str(port)]
if port < 1024 and not is_sudo():
print('Listening on any port under 1024 requires root permissions.')
cmd.insert(0, 'sudo')
subprocess.call(cmd)
def listen_smb(files_dir, version):
port = ask_port(445)
ips = get_ips()
print('The HTTP server is about to start.\nA good start ;) :')
for iname in ips:
if port == 445:
print(f' -> {iname}: \\\\{ips[iname]}\\share\\winPEASany.exe')
else:
print(f' -> {iname}: \\\\{ips[iname]}:{port}\\share\\winPEASany.exe # This syntax (:port) is not supported on Windows ?')
if version == 1:
cmd = ['smbserver.py', '-port', str(port), 'share', files_dir]
elif version == 2:
cmd = ['smbserver.py', '-smb2support', '-port', str(port), 'share', files_dir]
else:
sys.exit('Wrong SMB version')
if port < 1024 and not is_sudo():
print('Listening on any port under 1024 requires root permissions.')
cmd.insert(0, 'sudo')
subprocess.call(cmd)
def get_public_ip():
return requests.get('https://api.ipify.org').text
def get_ips():
ips = dict()
try:
ips['public'] = get_public_ip()
except requests.exceptions.ConnectionError:
pass
cmd = ['ip', 'a']
out = subprocess.run(cmd, capture_output=True).stdout.decode('utf-8')
lines = out.split('\n')
while("" in lines):
lines.remove("")
for line in lines:
if line[0].isdigit():
iname = line.split(' ')[1].split(':')[0]
if 'inet ' in line:
ip = line.split(' ')[5].split('/')[0]
if ip != '127.0.0.1':
ips[iname] = ip
return ips
def menu_choice(menu_options):
while(True):
print_menu(menu_options)
option = ''
try:
option = int(input('Enter your choice: '))
except ValueError:
print('Wrong input. Please enter a number ...')
files_dir = Path.cwd().joinpath('files')
if option == 1:
listen_http(files_dir)
elif option == 2:
listen_smb(files_dir, 1)
elif option == 3:
listen_smb(files_dir, 2)
elif option == 0:
print('Quitting')
sys.exit()
else:
print('Invalid option. Please enter a valid number.')
if __name__ == '__main__':
chdir(Path(__file__).resolve().parent) # Change current dir to source location
parser = argparse.ArgumentParser(description='Sync your files and starts a listener on HTTP, SMB or SMB2.')
parser.add_argument('-u', '--update', action='store_true', help='update your files (described in config.json)')
args = parser.parse_args()
with open("config.json", "r") as jsonfile:
config = json.load(jsonfile)
if args.update:
update(config)
tmp = Path('files/tmp')
is_empty = not any(tmp.iterdir())
if not is_empty:
if yes_or_no("The folder 'files/tmp' is not empty. Would you like to remove the content ?", default="no"):
for p in tmp.iterdir():
if p.is_dir():
rmtree(p)
else:
p.unlink()
print('Choose a service to start a listener:')
menu_options = {
1: 'HTTP',
2: 'SMB1',
3: 'SMB2',
0: 'Exit',
}
menu_choice(menu_options)