背景#
在一次 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 調用過程。
快速開始
- 運行腳本:
python your_script_name.py -k your_amap_key
輸出如下: