banner
毅种循环

毅种循环

请将那贻笑罪过以逐字吟咏 如对冰川投以游丝般倾诉

泄露地圖AK一鍵檢測利用

背景#

在一次 APK 反編譯的時候發現存在一個關於 map 的 KEY,試了幾個接口無法調用成功,具體不知道哪個對應的接口和服務,於是配合 GPT 寫了這份代碼,旨在實現自動化 AK 利用。

代碼#

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
import argparse
import requests
import random
import time
import certifi
import json
import traceback
import os
from termcolor import colored
from typing import List, Dict, Tuple, Optional, Any
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib3.exceptions import InsecureRequestWarning

# --- Banner ---
BANNER = """


                                                                                           
                            by: 地圖API密鑰全自動檢測工具
"""

# --- Configuration ---

# Emojis for output enhancement
EMOJI_DETECT = "🔍"
EMOJI_PLATFORM = "🏷️"
EMOJI_KEY_FORMAT = "🔑"
EMOJI_SERVICE = "▶️"
EMOJI_SUCCESS = "✅"
EMOJI_FAIL = "❌"
EMOJI_CLOCK = "⏱️"
EMOJI_INFO = "ℹ️"
EMOJI_ERROR = "❗"
EMOJI_WARNING = "⚠️"
EMOJI_NETWORK = "🌐"
EMOJI_QUOTA = "🚦"
EMOJI_PERM = "🔒"
EMOJI_WRITE = "💾"
EMOJI_TOOL = "🛠️"


# Disable SSL warnings (use cautiously)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# Safe coordinate bounds
SAFE_BOUNDS = {
    'global': {'lon_min': -180, 'lon_max': 180, 'lat_min': -90, 'lat_max': 90},
    'china': {'lon_min': 73.66, 'lon_max': 135.05, 'lat_min': 3.86, 'lat_max': 53.55}
}

# Test address pool
TEST_ADDRESS_POOL = [
    "北京市朝陽區望京soho",
    "上海市浦東新區陸家嘴環路1288號",
    "廣州市天河區珠江新城臨江大道5號",
    "深圳市福田區深南大道6001號",
    "成都市錦江區紅星路三段1號"
]

# --- Helper Classes ---

class GeoGenerator:
    """生成隨機地理數據。"""
    @staticmethod
    def random_coord(region='china'):
        bounds = SAFE_BOUNDS[region]
        return (
            round(random.uniform(bounds['lat_min'], bounds['lat_max']), 6),
            round(random.uniform(bounds['lon_min'], bounds['lon_max']), 6)
        )

    @staticmethod
    def random_ip():
        # 生成一個更可能被定位的公共IP範圍
        while True:
            ip = f"{random.randint(1, 223)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)}"
            parts = [int(p) for p in ip.split('.')]
            if parts[0] == 10: continue
            if parts[0] == 127: continue
            if parts[0] == 172 and 16 <= parts[1] <= 31: continue
            if parts[0] == 192 and parts[1] == 168: continue
            if parts[0] >= 224: continue
            return ip


class KeyValidator:
    """驗證密鑰格式並檢測平台。"""
    @staticmethod
    def detect_platform(key: str) -> Tuple[str, str]:
        """檢測平台並提供格式描述。"""
        if re.match(r'^[0-9a-fA-F]{32}$', key):
            return 'amap', '高德 (32位 Hex)'
        if re.match(r'^[A-Za-z0-9]{32}$', key):
            if re.match(r'^[0-9a-fA-F]{32}$', key):
                 return 'ambiguous', '可能是高德或百度 (純Hex字符)'
            return 'baidu', '百度 (32位 Alnum)'
        return 'unknown', '未知格式'


class APITester:
    """執行API測試調用。"""
    def __init__(self):
        self.user_agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0'
        ]

    def _generate_service_configs(self, platform: str) -> Dict:
         """動態生成服務配置以獲取新隨機數據。"""
         coord = GeoGenerator.random_coord()
         random_addr = random.choice(TEST_ADDRESS_POOL)
         random_ip = GeoGenerator.random_ip()

         if platform == 'amap':
            return {
                "靜態地圖": {
                    "url": "https://restapi.amap.com/v3/staticmap",
                    "params": {"location": f"{coord[1]},{coord[0]}", "zoom": 10, "size": "400*300", "markers": f"mid,,A:{coord[1]},{coord[0]}"},
                    "is_static": True
                },
                "地理編碼": {
                    "url": "https://restapi.amap.com/v3/geocode/geo",
                    "params": {"address": random_addr}
                },
                "逆地理編碼": {
                    "url": "https://restapi.amap.com/v3/geocode/regeo",
                    "params": {"location": f"{coord[1]},{coord[0]}"}
                },
                "路徑規劃": {
                    "url": "https://restapi.amap.com/v3/direction/driving",
                    "params": {"origin": "116.481028,39.989643", "destination": "116.434446,39.90816"}
                },
                "IP定位": {
                    "url": "https://restapi.amap.com/v3/ip",
                    "params": {"ip": random_ip}
                }
            }
         elif platform == 'baidu':
            return {
                "靜態地圖": {
                    "url": "https://api.map.baidu.com/staticimage/v2",
                    "params": {"center": f"{coord[1]},{coord[0]}", "zoom": 10, "width": 400, "height": 300, "markers": f"{coord[1]},{coord[0]}"},
                    "is_static": True
                },
                "地理編碼": {
                    "url": "https://api.map.baidu.com/geocoding/v3/",
                    "params": {"address": random_addr, "output": "json"}
                },
                "逆地理編碼": {
                    "url": "https://api.map.baidu.com/reverse_geocoding/v3/",
                    "params": {"location": f"{coord[0]},{coord[1]}", "output": "json"}
                },
                "路徑規劃": {
                    "url": "https://api.map.baidu.com/direction/v2/driving",
                    "params": {"origin": "40.01116,116.339303", "destination": "39.936404,116.452562"}
                },
                "IP定位": {
                    "url": "https://api.map.baidu.com/location/ip",
                    "params": {"ip": random_ip, "coor": "bd09ll"}
                }
            }
         return {}

    def _call_api(self, url: str, params: dict, platform: str, service_name: str, is_static: bool = False) -> Tuple[bool, dict]:
        """執行單個API請求並重試。"""
        current_params = params.copy()
        retries = current_params.pop('retry', 2) + 1
        detail = {'params': params, 'latency': None, 'error': None, 'error_info': None, 'response': None}

        for attempt in range(retries):
            response = None
            try:
                headers = {'User-Agent': random.choice(self.user_agents)}
                start_time = time.time()
                response = requests.get(
                    url,
                    params=current_params,
                    headers=headers,
                    timeout=15,
                    verify=certifi.where(),
                    stream=is_static
                )
                latency = round((time.time() - start_time) * 1000, 2)
                detail['latency'] = latency

                content_type = response.headers.get('Content-Type', '').lower()

                if is_static and response.status_code == 200 and content_type.startswith('image/'):
                    detail['response'] = {'content_type': content_type, 'status_code': 200}
                    response.close()
                    return True, detail

                if response.status_code != 200:
                    error_msg = f"HTTP {response.status_code}"
                    try:
                        error_data = response.json()
                        error_msg += f". API Msg: {json.dumps(error_data, ensure_ascii=False)}"
                    except (json.JSONDecodeError, requests.exceptions.RequestException):
                        error_msg += f". Response: {response.text[:200]}..."
                    detail['error'] = error_msg
                    raise requests.HTTPError(error_msg, response=response)

                data = None
                try:
                    if 'application/json' in content_type or 'text/javascript' in content_type:
                         data = response.json()
                    elif platform == 'amap' and service_name == "IP定位" and 'text/html' in content_type and response.text.startswith('{') and response.text.endswith('}'):
                         data = json.loads(response.text)
                    else:
                         raise ValueError(f"非預期響應類型: {content_type}")

                    detail['response'] = data

                    success = False
                    error_info = ""
                    if platform == 'amap':
                        success = str(data.get('status')) == '1'
                        if not success: error_info = data.get('info', '未知高德錯誤') + f" (infocode: {data.get('infocode', '')})"
                    elif platform == 'baidu':
                        success = data.get('status') == 0
                        if not success: error_info = data.get('message', '未知百度錯誤') + f" (status: {data.get('status', '')})"
                    else: error_info = "未知平台錯誤"

                    detail['error_info'] = error_info if not success else None
                    return success, detail

                except json.JSONDecodeError as e:
                    detail['error'] = f"JSON解析失敗 ({content_type}): {str(e)} | 響應: {response.text[:200]}..."
                    return False, detail
                except ValueError as e:
                    detail['error'] = f"{str(e)} | 響應: {response.text[:200]}..."
                    return False, detail

            except requests.exceptions.Timeout:
                detail['error'] = "請求超時"
                if attempt == retries - 1: return False, detail
                time.sleep(1 + 1.5 * attempt)
            except requests.exceptions.RequestException as e:
                detail['error'] = f"請求錯誤: {type(e).__name__}" + (f" (Status: {response.status_code})" if response else "")
                if attempt == retries - 1: return False, detail
                time.sleep(1 + 1.5 * attempt)
            except Exception as e:
                 detail['error'] = f"未知處理錯誤: {type(e).__name__}: {str(e)}"
                 print(f"{EMOJI_ERROR} {colored('內部腳本錯誤', 'red')}: {traceback.format_exc()[:500]}...")
                 return False, detail
            finally:
                if response:
                    response.close()

        detail['error'] = f"超過最大重試次數 ({retries}). 最後錯誤: {detail['error']}"
        return False, detail

    def test_service(self, platform: str, service_name: str, key: str, service_config: dict) -> Tuple[bool, dict]:
        """測試單個服務及其配置。"""
        params = service_config['params'].copy()
        params['key' if platform == 'amap' else 'ak'] = key
        is_static = service_config.get('is_static', False)
        return self._call_api(service_config['url'], params, platform, service_name, is_static)


class ReportGenerator:
    """生成測試結果的控制台輸出。"""
    @staticmethod
    def print_result(service: str, platform: str, success: bool, detail: dict):
        status_icon = EMOJI_SUCCESS if success else EMOJI_FAIL
        status_color = 'green' if success else 'red'
        service_status = colored("成功" if success else "失敗", status_color)

        print(f"{EMOJI_SERVICE} {service.ljust(7)} {status_icon} {service_status}")

        indent = "  │"

        latency = detail.get('latency')
        latency_str = f"{latency:.2f} ms" if latency is not None else "N/A"
        print(f"{indent} {EMOJI_CLOCK} 耗時: {latency_str}")

        params_str = " | ".join([f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={str(v)[:30]}{'...' if len(str(v))>30 else ''}"
                                 for k, v in detail.get('params', {}).items() if k not in ['key', 'ak']])
        if params_str:
             print(f"{indent} {EMOJI_INFO} 參數: {colored(params_str, 'cyan')}")

        if success:
            success_info_str = ReportGenerator._extract_success_info(service, platform, detail.get('response'))
            if success_info_str:
                print(f"{indent} {EMOJI_INFO} 結果: {colored(success_info_str, 'green')}")

        error_msg = detail.get('error')
        error_info = detail.get('error_info')

        if error_info:
            print(f"{indent} {EMOJI_WARNING} 原因: {colored(error_info, 'yellow')}")
            ReportGenerator._print_error_analysis(error_info)
        elif error_msg:
            print(f"{indent} {EMOJI_ERROR} 原因: {colored(error_msg, 'red')}")
            ReportGenerator._print_error_analysis(error_msg)
        elif not success:
            print(f"{indent} {EMOJI_ERROR} 原因: {colored('未知原因失敗', 'red')}")
        print( "  └" + "─" * 30)


    @staticmethod
    def _extract_success_info(service: str, platform: str, response_data: Optional[Any]) -> str:
        """從成功的API響應數據中提取簡要摘要。"""
        if response_data is None: return ""
        try:
            if isinstance(response_data, dict) and response_data.get('content_type', '').startswith('image/'):
                return f"成功獲取圖片 ({response_data['content_type']})"
            elif isinstance(response_data, dict):
                if service == "IP定位":
                    city = "未知"
                    if platform == 'amap': city = response_data.get('city') if isinstance(response_data.get('city'), str) and response_data.get('city') else response_data.get('adcode', 'N/A')
                    elif platform == 'baidu': city = response_data.get('content', {}).get('address_detail', {}).get('city', 'N/A')
                    return f"定位城市: {city}" if city and city != 'N/A' else "定位成功 (無城市信息)"
                elif service == "地理編碼":
                    loc = "未知"
                    if platform == 'amap': loc = response_data.get('geocodes', [{}])[0].get('location', 'N/A')
                    elif platform == 'baidu': loc_dict = response_data.get('result', {}).get('location', {}); loc = f"{loc_dict.get('lat', 'N/A')},{loc_dict.get('lng', 'N/A')}" if loc_dict else 'N/A'
                    return f"獲取坐標: {loc}"
                elif service == "逆地理編碼":
                    addr = "未知"
                    if platform == 'amap': addr = response_data.get('regeocode', {}).get('formatted_address', 'N/A')
                    elif platform == 'baidu': addr = response_data.get('result', {}).get('formatted_address', 'N/A')
                    return f"獲取地址: {addr[:40]}{'...' if len(addr)>40 else ''}"
                elif service == "路徑規劃":
                    dist, dur = "N/A", "N/A"
                    try:
                        if platform == 'amap': path = response_data.get('route', {}).get('paths', [{}])[0]; dist, dur = path.get('distance'), path.get('duration')
                        elif platform == 'baidu': route = response_data.get('result', {}).get('routes', [{}])[0]; dist, dur = route.get('distance'), route.get('duration')
                        return f"距離: {dist}米, 時間: {dur}秒"
                    except (IndexError, KeyError, TypeError): return "路徑規劃成功"
                else: return "調用成功"
            return ""
        except Exception as e:
            return colored(f"結果解析出錯: {e}", 'magenta')

    @staticmethod
    def _print_error_analysis(error_text: str):
        """根據錯誤文本關鍵字打印特定警告圖標。"""
        indent = "  │  "
        error_lower = error_text.lower()
        analysis_printed = False
        if 'quota' in error_lower or '配額' in error_lower or '並發' in error_lower or 'qps' in error_lower or 'limit' in error_lower:
            print(f"{indent}{EMOJI_QUOTA} {colored('[疑似配額/並發限制]', 'yellow')}")
            analysis_printed = True
        if 'invalid user key' in error_lower or 'key不正確' in error_lower or 'ak不存在' in error_lower or '權限校驗失敗' in error_lower or 'permission denied' in error_lower or 'key status' in error_lower or '無效' in error_lower :
            print(f"{indent}{EMOJI_PERM} {colored('[密鑰無效或服務未開通/權限不足]', 'red')}")
            analysis_printed = True
        if 'domain' in error_lower or 'ip白名單' in error_lower or 'referer' in error_lower or 'sn校驗失敗' in error_lower:
             print(f"{indent}{EMOJI_PERM} {colored('[IP/域名/Referer/SN白名單校驗失敗]', 'magenta')}")
             analysis_printed = True
        if 'timeout' in error_lower or '超時' in error_lower:
            print(f"{indent}{EMOJI_NETWORK} {colored('[請求超時]', 'blue')}")
            analysis_printed = True
        if 'json解析失敗' in error_lower or '非預期響應類型' in error_lower:
            print(f"{indent}{EMOJI_NETWORK} {colored('[響應格式錯誤或非預期]', 'magenta')}")
            analysis_printed = True
        if '請求錯誤' in error_text or 'http' in error_lower or 'connection' in error_lower or 'ssl' in error_lower:
            print(f"{indent}{EMOJI_NETWORK} {colored('[網絡/連接/HTTP錯誤]', 'blue')}")
            analysis_printed = True


# --- Main Execution ---

def parse_args():
    """解析命令行參數。"""
    parser = argparse.ArgumentParser(
        description=f"{EMOJI_TOOL} 地圖API密鑰全自動檢測工具 (v3.6)", # Version bump for banner
        formatter_class=argparse.RawTextHelpFormatter
        )
    parser.add_argument('-k', '--keys', nargs='+', help="直接在命令行指定一個或多個密鑰")
    parser.add_argument('-f', '--file', help="包含密鑰的文件路徑 (每行一個密鑰)")
    parser.add_argument('-t', '--threads', type=int, default=5, help="並發測試線程數 (默認: 5)")
    parser.add_argument('-o', '--output', help="將成功的檢測結果輸出到指定的JSON文件")
    parser.add_argument('--skip-static', action='store_true', help="跳過靜態地圖服務的檢測")

    temp_args, _ = parser.parse_known_args()
    if not temp_args.keys and not temp_args.file:
         # Print banner before showing help on error
         print(colored(BANNER, 'cyan'))
         parser.print_help()
         print(colored("\n錯誤: 必須通過 -k 或 -f 提供至少一個密鑰。", "red"))
         exit(1)

    return parser.parse_args()

def main(keys_to_test: List[str], num_threads: int, output_file: Optional[str], skip_static: bool):
    """主檢測工作流。"""
    tester = APITester()
    futures_map = {}
    results_for_output = []

    print(f"{EMOJI_DETECT} 開始檢測 {len(keys_to_test)} 個密鑰,使用 {num_threads} 個線程...")
    if skip_static: print(f"{EMOJI_INFO} 已設置跳過靜態地圖檢測。")

    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        for key in keys_to_test:
            key = key.strip()
            if not key: continue

            platform, key_format_desc = KeyValidator.detect_platform(key)
            masked_key = f"{key[:6]}...{key[-4:]}" if len(key) > 10 else key

            if platform == 'unknown':
                print(colored(f"\n{EMOJI_FAIL} 無效密鑰格式: {key}", "red"))
                continue
            if platform == 'ambiguous':
                 print(colored(f"\n{EMOJI_WARNING} 檢測到模糊密鑰: {masked_key}", "yellow"))
                 print(colored(f"  格式 ({key_format_desc}) 可能屬於高德或百度,將嘗試兩者。", "yellow"))
                 platforms_to_try = ['amap', 'baidu']
            else:
                 platforms_to_try = [platform]

            for p in platforms_to_try:
                print(colored(f"\n{EMOJI_DETECT} 正在檢測 {p.upper()} 服務 - 密鑰: {masked_key}", "blue", attrs=['bold']))
                print(f"  {EMOJI_PLATFORM} 平台識別: {p.upper()}")
                print(f"  {EMOJI_KEY_FORMAT} Key格式: {key_format_desc}")

                platform_services = tester._generate_service_configs(p)

                for service_name, service_config in platform_services.items():
                    if skip_static and service_config.get('is_static', False):
                        continue

                    future = executor.submit(tester.test_service, p, service_name, key, service_config)
                    futures_map[future] = {'key': key, 'platform': p, 'service_name': service_name}

        print(colored("\n--- 檢測結果 ---", attrs=['bold']))
        processed_count = 0
        total_tasks = len(futures_map)

        for future in as_completed(futures_map):
            processed_count += 1
            context = futures_map[future]
            key = context['key']
            platform = context['platform']
            service_name = context['service_name']
            masked_key = f"{key[:6]}...{key[-4:]}" if len(key) > 10 else key

            try:
                success, detail = future.result()
                ReportGenerator.print_result(service_name, platform, success, detail)

                if success and output_file:
                    result_item = {
                        'key': key,
                        'platform': platform,
                        'service_name': service_name,
                        'request_params': detail.get('params'),
                        'response_summary': ReportGenerator._extract_success_info(service_name, platform, detail.get('response')),
                        'latency_ms': detail.get('latency'),
                    }
                    results_for_output.append(result_item)

            except Exception as exc:
                 print(colored(f"{EMOJI_FAIL} {service_name.ljust(7)}", 'red'))
                 print(f"  └─ {colored(f'執行 {service_name} (平台: {platform}, 密鑰: {masked_key}) 時發生內部錯誤: {exc}', 'red')}")

    if output_file:
        print(colored(f"\n{EMOJI_WRITE} 正在將 {len(results_for_output)} 條成功結果寫入到: {output_file}", "blue"))
        try:
            with open(output_file, 'w', encoding='utf-8') as f:
                json.dump(results_for_output, f, ensure_ascii=False, indent=4)
            print(colored(f"{EMOJI_SUCCESS} 成功寫入 JSON 文件。", "green"))
        except IOError as e:
            print(colored(f"{EMOJI_ERROR} 寫入文件失敗: {e}", "red"))
        except TypeError as e:
             print(colored(f"{EMOJI_ERROR} 序列化結果為JSON時失敗 (可能包含無法序列化的數據): {e}", "red"))


if __name__ == "__main__":
    # --- Print Banner First ---
    print(colored(BANNER, 'cyan')) # Choose a color for the banner

    args = parse_args() # Parse arguments after printing banner
    keys = []

    if args.keys:
        keys.extend(args.keys)
    if args.file:
        try:
            with open(args.file, 'r', encoding='utf-8') as f:
                keys.extend([line.strip() for line in f if line.strip()])
        except FileNotFoundError:
             print(colored(f"{EMOJI_ERROR} 錯誤:密鑰文件 '{args.file}' 未找到。", "red"))
             exit(1)
        except Exception as e:
             print(colored(f"{EMOJI_ERROR} 錯誤:讀取密鑰文件 '{args.file}' 時出錯: {e}", "red"))
             exit(1)

    unique_keys = sorted(list(set(keys)))

    # Input validation is handled within parse_args now

    main(unique_keys, args.threads, args.output, args.skip_static)
    print(colored(f"\n{EMOJI_TOOL} 檢測完成。", attrs=['bold']))

地圖 API 密鑰全自動檢測工具

簡介

地圖 API 密鑰全自動檢測工具 是一款高效、全面的解決方案,旨在簡化滲透測試過程中地圖 API 密鑰的管理和風險評估工作。 無論是滲透測試人員還是安全研究人員,在分析應用程序或系統對地圖 API 的依賴時,常常面臨密鑰有效性驗證、服務權限探測和潛在安全風險評估等多重挑戰。 本工具通過自動化執行這些關鍵任務,極大地提升了水洞效率,並幫助識別與地圖服務相關的潛在安全漏洞.

主要功能

  • 密鑰有效性驗證: 多接口,多服務調用自動驗證地圖 API 密鑰的有效狀態 (高德 / 百度),快速識別可能被濫用或已洩露的密鑰。
  • 批量密鑰測試: 支持批量測試多個地圖 API 密鑰,提高滲透測試效率,快速評估大量密鑰的風險。

技術特點

  • Python 開發: 采用 Python 語言開發,具有良好的跨平台性和可讀性。
  • Requests 庫: 使用 Requests 庫發送 HTTP 請求,簡化 API 調用過程。

快速開始

  1. 運行腳本:
    python your_script_name.py -k your_amap_key

輸出如下:

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。