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}/")