#!/usr/bin/env python3
"""
gtfobinSUID V1.2
-----------

Developed by: Strikoder

Description:
    gtfobinsuid checks binaries discovered during SUID/SGID enumeration
    against GTFOBins for known privilege-escalation vectors.

    Modes:
      * default: try online; if no network fall back to db.txt
      * --online : force online (fail per-entry if remote unreachable)
      * --offline / --fline : force offline and use db.txt only

Usage:
    "python3 gtfobinsuid.py [--online | --offline] [--db PATH]"

"""

import sys
import re
import os
import argparse
import requests, sys
from bs4 import BeautifulSoup


DEFAULT_DB = os.path.join(os.path.dirname(__file__), "db.txt")

def read_input() -> str:
    sys.stdout.write("Paste SUID/SGID enum output. End with EOF (Ctrl-D on Linux/macOS, Ctrl-Z then Enter on Windows):\n")
    sys.stdout.flush()
    return sys.stdin.read()

def extract_basenames(text: str):
    paths = re.findall(r'(/(?:[A-Za-z0-9_\-./]+))', text)
    names = []
    seen = set()
    for p in paths:
        name = os.path.basename(p)
        if not name or name in seen:
            continue
        seen.add(name)
        names.append(name)
    return names

def normalize_binary(name: str) -> str:
    """Normalize binary names like python2.7, python3, php7.4 to base names."""
    name_lower = name.lower()
    
    # Python variants
    if name_lower.startswith('python'):
        return 'python'
    # Perl variants
    if name_lower.startswith('perl'):
        return 'perl'
    # PHP variants
    if name_lower.startswith('php'):
        return 'php'
    # Node variants
    if name_lower.startswith('node'):
        return 'node'
    
    return name

def check_gtfobin_online(name: str, timeout=3):
    """Check one binary on GTFOBins."""
    url = f"https://gtfobins.github.io/gtfobins/{name}/"
    try:
        r = requests.get(url, timeout=timeout)
        if r.status_code != 200:
            return None
        html = r.text.lower()
        has_suid = "suid" in html
        has_limited = "limited suid" in html or "limited-suid" in html
        return has_suid, has_limited, url
    except requests.RequestException:
        return None


def load_db(path: str):
    """Load db.txt and split normal vs limited SUID entries."""
    suid = set()
    limited = set()
    if not os.path.isfile(path):
        return suid, limited

    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip().lower()
            if not line or line.startswith("#"):
                continue
            if "(limited)" in line or " limited" in line or "-limited" in line:
                limited.add(line.split()[0].replace("(limited)", "").replace("-limited", "").strip())
            else:
                suid.add(line)
    return suid, limited

def update_db(db_path: str = "db.txt"):
    """
    Update the local db.txt by scraping the GTFOBins website.
    Fetches SUID and Limited SUID entries automatically.
    """
    url = "https://gtfobins.github.io/"
    print("[*] Fetching GTFOBins list...")
    collected_suid = set()
    collected_limited = set()

    try:
        r = requests.get(url, timeout=10)
        if r.status_code != 200:
            print(f"[!] Could not fetch {url}")
            return

        soup = BeautifulSoup(r.text, "html.parser")
        items = soup.select("a[href^='/gtfobins/']")
        print(f"[*] Found {len(items)} binaries, parsing...")

        for a in items:
            name = a.get_text(strip=True).lower()
            ul = a.find_next("ul")
            if not ul:
                continue
            funcs = [li.get_text(strip=True).lower() for li in ul.select("a")]
            if "suid" in funcs:
                collected_suid.add(name)
            if "limited suid" in funcs:
                collected_limited.add(name)

        if not collected_suid and not collected_limited:
            print("[!] Nothing collected, aborting.")
            return

        with open(db_path, "w", encoding="utf-8") as f:
            f.write("# Auto-generated GTFOBins SUID and Limited SUID database\n")
            f.write("# Generated by gtfobinSUID\n\n")
            for name in sorted(collected_suid):
                f.write(f"{name}\n")
            for name in sorted(collected_limited):
                f.write(f"{name} (limited)\n")

        print(f"[+] Database updated successfully: {db_path}")
        print(f"    {len(collected_suid)} SUID entries")
        print(f"    {len(collected_limited)} Limited SUID entries")

    except requests.RequestException as e:
        print(f"[!] Network error while updating db: {e}")


def print_banner ():
    """Print startup banner with project name and auther. """
    print ("""
           █████       ██████           █████      ███              █████████  █████  █████ █████ ██████████
          ░░███       ███░░███         ░░███      ░░░              ███░░░░░███░░███  ░░███ ░░███ ░░███░░░░███
  ███████ ███████    ░███ ░░░   ██████  ░███████  ████  ████████  ░███    ░░░  ░███   ░███  ░███  ░███   ░░███
 ███░░███░░░███░    ███████    ███░░███ ░███░░███░░███ ░░███░░███ ░░█████████  ░███   ░███  ░███  ░███    ░███
░███ ░███  ░███    ░░░███░    ░███ ░███ ░███ ░███ ░███  ░███ ░███  ░░░░░░░░███ ░███   ░███  ░███  ░███    ░███
░███ ░███  ░███ ███  ░███     ░███ ░███ ░███ ░███ ░███  ░███ ░███  ███    ░███ ░███   ░███  ░███  ░███    ███
░░███████  ░░█████   █████    ░░██████  ████████  █████ ████ █████░░█████████  ░░████████   █████ ██████████
 ░░░░░███   ░░░░░   ░░░░░      ░░░░░░  ░░░░░░░░  ░░░░░ ░░░░ ░░░░░  ░░░░░░░░░    ░░░░░░░░   ░░░░░ ░░░░░░░░░░
 ███ ░███                                                                                           v1.2
░░██████
 ░░░░░░
""")

def print_hint(name: str):
    """Print special hints for certain binaries."""
    hints = {
        'sudo': "[!] HINT: 'sudo' with SUID might indicate CVE exploits or misconfigurations (check Baron Samedit & version vulnerabilities)",
        'pkexec': "[!] HINT: 'pkexec' with SUID might indicate CVE exploits (e.g., PwnKit CVE-2021-4034) - not typically a GTFOBins vector",
        'ssh-agent': "[!] HINT: 'ssh-agent' might be a false positive - usually not exploitable via SUID",
        'ndsudo': "[!] HINT: 'ndsudo' is vulnerable to CVE-2024-32019 - exploit available at https://github.com/AzureADTrent/CVE-2024-32019-POC (can be compiled on attacker machine then moved to target)",
    }
    
    name_lower = name.lower()
    if name_lower in hints:
        print(f"    {hints[name_lower]}")

def main():
    parser = argparse.ArgumentParser(description="Check binaries against GTFOBins for SUID / Limited SUID.")
    parser.add_argument("--online", action="store_true", help="Query GTFOBins website directly for each binary (ignores local db).")
    parser.add_argument("--offline", "--fline", dest="offline", action="store_true", help="Use only the local database file (db.txt)")
    parser.add_argument("--db", default="db.txt", help="Path to local database file for offline lookups (default: ./db.txt).")
    parser.add_argument("--update-db", action="store_true", help="Fetch latest SUID and Limited-SUID entries from GTFOBins and rebuild db.txt.")


    args = parser.parse_args()

    print_banner()
    sys.stdout.reconfigure(line_buffering=True)

    # Updating db if needed
    if args.update_db:
        update_db(args.db)
        return

    sys.stdout.write("\nDo you want to see the SUID/SGID enumeration commands? (y/n): ")
    sys.stdout.flush()
    response = input().strip().lower()

    if response in ['y', 'yes']:
        print("\n[*] SUID/SGID Enumeration Commands:")
        print("find / -perm -u=s -type f 2>/dev/null; find / -perm -g=s -type f 2>/dev/null")

    text = read_input()
    if not text.strip():
        print("No input provided.")
        return

    names = extract_basenames(text)
    if not names:
        print("No binaries found.")
        return

    suid_db, limited_db = load_db(args.db)


    # Determine mode
    mode = "online"
    if args.offline:
        mode = "offline"
    elif not args.online:
        # auto: try to connect once
        try:
            requests.head("https://gtfobins.github.io", timeout=3)
            mode = "online"
        except requests.RequestException:
            mode = "offline"

    print(f"\n[MODE] Running in {mode.upper()} mode\n")

    for name in names:
        normalized = normalize_binary(name)
        
        if mode == "online":
            result = check_gtfobin_online(normalized)
            if not result:
                print(f"[NOT FOUND] {name}")
                print_hint(name)
                continue
            has_suid, has_limited, url = result
            if has_suid:
                print(f"[FOUND] {name} -> {url}")
                print_hint(name)
            elif has_limited:
                print(f"[FOUND - Limited SUID] {name} -> {url}")
                print_hint(name)
            else:
                print(f"[NOT FOUND] {name}")
                print_hint(name)

        else:
            lnorm = normalized.lower()
            url = f"https://gtfobins.github.io/gtfobins/{lnorm}/"
            if lnorm in suid_db:
                print(f"[FOUND] {name} -> {url} ")
                print_hint(name)
            elif lnorm in limited_db:
                print(f"[FOUND - Limited SUID] {name} -> {url} ")
                print_hint(name)
            else:
                print(f"[NOT FOUND] {name}")
                print_hint(name)



if __name__ == "__main__":
    main()
