yellow.git


yellow

~ opinionated web scanning pipeline for pentesters in a hurry
git clone https://github.com/djnnvx/yellow.git
log | files | refs | archive | README | LICENSE

run-web-recon (9601B)

      1 #!/usr/bin/env python3
      2 
      3 """
      4 HTB recon script — uses yellow to set up the environment and run an initial
      5 port scan + web enum, then dispatches service-specific Kali tools based on
      6 what's actually open.
      7 """
      8 
      9 import argparse
     10 import json
     11 import shlex
     12 import shutil
     13 import subprocess
     14 import sys
     15 from pathlib import Path
     16 
     17 
     18 # ---------------------------------------------------------------------------
     19 # Execution helpers
     20 # ---------------------------------------------------------------------------
     21 
     22 def die(msg):
     23     print(f"[!] {msg}", file=sys.stderr)
     24     sys.exit(1)
     25 
     26 
     27 def run(command, check=True):
     28     """Stream a command's output live and return it. Dies on failure if check=True."""
     29     print(f"\n[>] {command}")
     30     proc = subprocess.Popen(
     31         shlex.split(command),
     32         stdout=subprocess.PIPE,
     33         stderr=subprocess.STDOUT,
     34         text=True,
     35     )
     36     buf = []
     37     for line in proc.stdout:
     38         print(line, end="", flush=True)
     39         buf.append(line)
     40     proc.wait()
     41     print()
     42     if check and proc.returncode != 0:
     43         die(f"command failed (exit {proc.returncode}): {command}")
     44     return "".join(buf)
     45 
     46 
     47 def try_run(command):
     48     """Like run() but never dies — errors are printed and execution continues."""
     49     return run(command, check=False)
     50 
     51 
     52 # ---------------------------------------------------------------------------
     53 # Port scan result parsing
     54 # ---------------------------------------------------------------------------
     55 
     56 def parse_portscan(target):
     57     """
     58     Yellow writes portscan.json to {target}/{target}/portscan.json because
     59     ScanOpts.scanPath and ScanOpts.domain are both set to the -d value,
     60     and runPortScan uses scanPath + "/" + domain as the output directory.
     61     """
     62     candidates = [
     63         Path(target) / target / "portscan.json",
     64         Path(target) / "portscan.json",
     65     ]
     66     for path in candidates:
     67         if path.exists():
     68             with open(path) as f:
     69                 data = json.load(f)
     70             return data.get("open_ports", [])
     71     print("[!] portscan.json not found — skipping service-specific scans")
     72     return []
     73 
     74 
     75 # ---------------------------------------------------------------------------
     76 # Service-specific handlers
     77 # ---------------------------------------------------------------------------
     78 
     79 def nmap_targeted(target, ports, outdir):
     80     port_str = ",".join(str(p["port"]) for p in ports)
     81     try_run(f"nmap -sV -sC -p {port_str} -oN {outdir}/nmap-services {target}")
     82 
     83 
     84 def handle_ftp(target, outdir):
     85     try_run(f"nmap -sV -sC -p 21 --script ftp-anon,ftp-bounce,ftp-syst -oN {outdir}/nmap-ftp.txt {target}")
     86 
     87 
     88 def handle_ssh(target, outdir):
     89     try_run(f"nmap -sV -sC -p 22 --script ssh-hostkey,ssh-auth-methods -oN {outdir}/nmap-ssh.txt {target}")
     90 
     91 
     92 def handle_smtp(target, outdir):
     93     try_run(f"nmap -sV -sC -p 25 --script smtp-enum-users,smtp-commands -oN {outdir}/nmap-smtp.txt {target}")
     94 
     95 
     96 def handle_dns(target, outdir):
     97     if shutil.which("dig"):
     98         try_run(f"dig any {target} @{target}")
     99         try_run(f"dig axfr {target} @{target}")
    100     try_run(f"nmap -sV -sC -p 53 --script dns-zone-transfer -oN {outdir}/nmap-dns.txt {target}")
    101 
    102 
    103 def handle_smb(target, outdir):
    104     if shutil.which("enum4linux-ng"):
    105         try_run(f"enum4linux-ng -A {target} -oJ {outdir}/enum4linux")
    106     elif shutil.which("enum4linux"):
    107         try_run(f"enum4linux -a {target}")
    108     if shutil.which("smbclient"):
    109         try_run(f"smbclient -L //{target}/ -N")
    110     netexec = shutil.which("nxc") or shutil.which("crackmapexec")
    111     if netexec:
    112         try_run(f"{netexec} smb {target}")
    113 
    114 
    115 def handle_ldap(target, outdir):
    116     try_run(f"nmap -sV -sC -p 389 --script ldap-rootdse -oN {outdir}/nmap-ldap.txt {target}")
    117     if shutil.which("ldapsearch"):
    118         try_run(f"ldapsearch -x -H ldap://{target} -b '' -s base namingContexts")
    119 
    120 
    121 def handle_web(target, port, outdir):
    122     scheme = "https" if port in (443, 8443) else "http"
    123     url = f"{scheme}://{target}:{port}"
    124     if shutil.which("nikto"):
    125         try_run(f"nikto -h {url} -o {outdir}/nikto-{port}.txt")
    126     wl = next(
    127         (w for w in [
    128             "/usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt",
    129             "/usr/share/wordlists/seclists/Discovery/Web-Content/common.txt",
    130             "/usr/share/wordlists/dirb/common.txt",
    131         ] if Path(w).exists()),
    132         None,
    133     )
    134     if wl:
    135         if shutil.which("feroxbuster"):
    136             try_run(f"feroxbuster -u {url} -w {wl} -o {outdir}/ferox-{port}.txt -q")
    137         elif shutil.which("gobuster"):
    138             try_run(f"gobuster dir -u {url} -w {wl} -o {outdir}/gobuster-{port}.txt -q")
    139 
    140 
    141 def handle_mysql(target, outdir):
    142     try_run(f"nmap -sV -sC -p 3306 --script mysql-info,mysql-empty-password -oN {outdir}/nmap-mysql.txt {target}")
    143 
    144 
    145 def handle_mssql(target, outdir):
    146     try_run(f"nmap -sV -sC -p 1433 --script ms-sql-info,ms-sql-empty-password -oN {outdir}/nmap-mssql.txt {target}")
    147 
    148 
    149 def handle_rdp(target, outdir):
    150     try_run(f"nmap -sV -sC -p 3389 --script rdp-enum-encryption -oN {outdir}/nmap-rdp.txt {target}")
    151 
    152 
    153 def handle_winrm(target, outdir):
    154     netexec = shutil.which("nxc") or shutil.which("crackmapexec")
    155     if netexec:
    156         try_run(f"{netexec} winrm {target}")
    157 
    158 
    159 def handle_vnc(target, outdir):
    160     try_run(f"nmap -sV -sC -p 5900 --script vnc-info,realvnc-auth-bypass -oN {outdir}/nmap-vnc.txt {target}")
    161 
    162 
    163 # ---------------------------------------------------------------------------
    164 # Port → handler mapping
    165 # ---------------------------------------------------------------------------
    166 
    167 SERVICE_MAP = {
    168     21:   ("FTP",    handle_ftp),
    169     22:   ("SSH",    handle_ssh),
    170     25:   ("SMTP",   handle_smtp),
    171     53:   ("DNS",    handle_dns),
    172     80:   ("HTTP",   lambda t, o: handle_web(t, 80, o)),
    173     139:  ("SMB",    handle_smb),
    174     389:  ("LDAP",   handle_ldap),
    175     443:  ("HTTPS",  lambda t, o: handle_web(t, 443, o)),
    176     445:  ("SMB",    handle_smb),
    177     1433: ("MSSQL",  handle_mssql),
    178     3306: ("MySQL",  handle_mysql),
    179     3389: ("RDP",    handle_rdp),
    180     5900: ("VNC",    handle_vnc),
    181     5985: ("WinRM",  handle_winrm),
    182     5986: ("WinRM",  handle_winrm),
    183     8080: ("HTTP",   lambda t, o: handle_web(t, 8080, o)),
    184     8443: ("HTTPS",  lambda t, o: handle_web(t, 8443, o)),
    185     8888: ("HTTP",   lambda t, o: handle_web(t, 8888, o)),
    186 }
    187 
    188 # Ports that share the same handler and should only run once
    189 DEDUP_GROUPS = [
    190     {139, 445},   # SMB
    191     {5985, 5986}, # WinRM
    192 ]
    193 
    194 
    195 # ---------------------------------------------------------------------------
    196 # Main
    197 # ---------------------------------------------------------------------------
    198 
    199 if __name__ == "__main__":
    200     parser = argparse.ArgumentParser(
    201         description="HTB recon — yellow setup + port scan + service-specific tools"
    202     )
    203     parser.add_argument("-t", "--target", required=True, help="target IP or hostname (e.g. 10.10.10.10)")
    204     parser.add_argument("-w", "--wordlist", help="custom wordlist for dir brute-forcing")
    205     parser.add_argument("-p", "--proxy", help="proxy URL")
    206     parser.add_argument("-r", "--rate-limit", type=int, default=0, dest="rate_limit")
    207     parser.add_argument("-k", "--insecure", action="store_true", help="force HTTP / skip SSL")
    208     parser.add_argument("--osint", action="store_true", help="run yellow osint step (useful for domains, not bare IPs)")
    209     parser.add_argument("--ports", default="21,22,23,25,53,80,110,135,139,143,389,443,445,993,995,1433,1723,3306,3389,5900,5985,5986,8080,8443,8888",
    210                         help="ports for yellow --port-scan")
    211     args = parser.parse_args()
    212 
    213     target = args.target
    214 
    215     if not shutil.which("yellow"):
    216         die("yellow not found in PATH")
    217     if not shutil.which("nmap"):
    218         die("nmap not found in PATH")
    219 
    220     proxy_opt   = f"-p {args.proxy}"      if args.proxy      else ""
    221     rate_opt    = f"-r {args.rate_limit}" if args.rate_limit else ""
    222     insecure_opt = "-k"                   if args.insecure   else ""
    223 
    224     # 1. create directory structure
    225     run(f"yellow -d {target}")
    226 
    227     # 2. osint (optional — mostly useful when target is a real domain)
    228     if args.osint:
    229         try_run(f"yellow osint -d {target} {proxy_opt} {rate_opt}".strip())
    230 
    231     # 3. scan with yellow (port scan + web enum)
    232     run(
    233         " ".join(filter(None, [
    234             "yellow scan",
    235             f"-d {target}",
    236             "--port-scan",
    237             f"--ports {args.ports}",
    238             proxy_opt, rate_opt, insecure_opt,
    239         ]))
    240     )
    241 
    242     # 4. parse what yellow found
    243     open_ports = parse_portscan(target)
    244     if not open_ports:
    245         print("[*] no port scan results found — done")
    246         sys.exit(0)
    247 
    248     print(f"\n[+] open ports: {[p['port'] for p in open_ports]}")
    249 
    250     scans_dir = Path(target) / "scans"
    251     scans_dir.mkdir(parents=True, exist_ok=True)
    252 
    253     # 5. nmap targeted — full service + script detection on confirmed open ports
    254     nmap_targeted(target, open_ports, scans_dir)
    255 
    256     # 6. service-specific tools
    257     seen = set()
    258     for port_info in open_ports:
    259         port = port_info["port"]
    260         entry = SERVICE_MAP.get(port)
    261         if entry is None:
    262             continue
    263 
    264         # deduplicate ports that share a handler (SMB on 139+445, WinRM on 5985+5986)
    265         group_key = next((frozenset(g) for g in DEDUP_GROUPS if port in g), None)
    266         if group_key:
    267             if group_key in seen:
    268                 continue
    269             seen.add(group_key)
    270 
    271         label, handler = entry
    272         print(f"\n[+] {label} on port {port} — launching tools")
    273         handler(target, scans_dir)
    274 
    275     print(f"\n[+] done. results in ./{target}/")