Building a Shodan-Based Asset Discovery and ThinkPHP Log Exposure Scanner

Enviroment Setup

Install the official Shodan libray via pip:

pip install shodan

Initialize your API credentials:

shodan init <your_api_key>

Verify connectivity by testing a query count:

shodan count nginx

Basic Host Discovery

The following script demonstrates fundamental interaction with the Shodan API to retrieve web server assets:

import shodan
from shodan.exception import APIError

API_TOKEN = "YOUR_API_KEY_HERE"
scanner = shodan.Shodan(API_TOKEN)

def enumerate_hosts(query):
    try:
        dataset = scanner.search(query)
        print(f"Total hosts identified: {dataset['total']}")
        
        for entry in dataset['matches']:
            address = entry['ip_str']
            service_port = entry.get('port', 80)
            print(f"Discovered: {address}:{service_port}")
            
    except APIError as err:
        print(f"API communication failure: {err}")

if __name__ == "__main__":
    enumerate_hosts("apache")

The API returns a structured response containing metadata and host arrays. Key fields include total for result counts and matches containing individual host objects with properties like ip_str, port, and hostnames.

Persistent Storage Implementation

Extend the basic scanner to append discovered endpoints to a local file for batch processing:

import shodan
import sys

API_TOKEN = "YOUR_API_KEY_HERE"
client = shodan.Shodan(API_TOKEN)
OUTPUT_FILE = "discovered_targets.txt"

def harvest_targets(search_filter):
    print("[*] Initiating asset discovery...")
    
    try:
        with open(OUTPUT_FILE, mode='a', encoding='utf-8') as log:
            results = client.search(search_filter)
            print(f"[+] Hosts found: {results['total']}")
            
            for host in results['matches']:
                endpoint = f"http://{host['ip_str']}:{host['port']}"
                print(f"[+] Captured: {endpoint}")
                log.write(f"{endpoint}\n")
                
        print("[*] Discovery phase completed")
        
    except shodan.APIError as err:
        print(f"[!] Error: {err}")

if __name__ == "__main__":
    if len(sys.argv) > 1:
        harvest_targets(sys.argv[1])
    else:
        harvest_targets('thinkphp country:"CN"')

Integrated Vulnerability Detection Module

This enhanced utility combines Shodan reconnaissance with automated detection of ThinkPHP log file exposure vulnerabilities:

import shodan
import requests
import time
import sys
from shodan.exception import APIError

CONFIG = {
    'api_key': 'YOUR_API_KEY_HERE',
    'target_file': 'discovered_targets.txt',
    'vuln_file': 'confirmed_vulnerable.txt',
    'delay': 0.5
}

class SecurityScanner:
    def __init__(self):
        self.engine = shodan.Shodan(CONFIG['api_key'])
        self.log_paths = [
            '/Application/Runtime/Logs/Admin',
            '/Runtime/Logs/Admin'
        ]
    
    def reconnaissance(self, query_string):
        print("[*] Starting reconnaissance phase...")
        try:
            with open(CONFIG['target_file'], 'a', encoding='utf-8') as buffer:
                response = self.engine.search(query_string)
                print(f"[+] Total results: {response['total']}")
                
                for asset in response['matches']:
                    url = f"http://{asset['ip_str']}:{asset['port']}"
                    buffer.write(f"{url}\n")
                    print(f"[+] Queued: {url}")
                    
        except APIError as err:
            print(f"[!] Shodan API error: {err}")
    
    def vulnerability_check(self):
        print("[*] Initiating vulnerability assessment...")
        print("[!] Ensure targets exist in discovered_targets.txt")
        
        try:
            with open(CONFIG['target_file'], 'r', encoding='utf-8') as source, \
                 open(CONFIG['vuln_file'], 'a', encoding='utf-8') as sink:
                
                for line in source:
                    base_url = line.strip()
                    if not base_url:
                        continue
                        
                    for path in self.log_paths:
                        target_url = f"{base_url}{path}"
                        try:
                            resp = requests.get(target_url, timeout=10, allow_redirects=False)
                            
                            if resp.status_code == 403:
                                print(f"[CRITICAL] Exposed logs: {target_url}")
                                sink.write(f"{target_url}\n")
                                sink.flush()
                                
                        except requests.RequestException:
                            continue
                            
                        time.sleep(CONFIG['delay'])
                        
        except FileNotFoundError:
            print("[!] Target file not found. Run reconnaissance first.")
        except Exception as err:
            print(f"[!] Scan error: {err}")

def main():
    scanner = SecurityScanner()
    
    if len(sys.argv) < 2:
        print("Usage: python scanner.py <mode> [query]")
        print("Modes: 1=Recon only, 2=Scan only, 12=Full pipeline")
        return
    
    mode = sys.argv[1]
    query = sys.argv[2] if len(sys.argv) > 2 else 'thinkphp country:"CN"'
    
    if mode == "1":
        scanner.reconnaissance(query)
    elif mode == "2":
        scanner.vulnerability_check()
    elif mode == "12":
        scanner.reconnaissance(query)
        scanner.vulnerability_check()
    else:
        print("[!] Invalid mode selection")

if __name__ == "__main__":
    main()

Operational Parameters

Execute the script using positional arguments to control operational flow:

Mode 1: Reconnaissance Only

python scanner.py 1 "thinkphp country:US"

Mode 2: Vulnerability Scan Only

python scanner.py 2

Mode 12: Full Pipeline (Discover + Scan)

python scanner.py 12 "apache"

CLI Parsing Considerations

When handling command-line arguments containing quoted strings or spaces (common in Shodan queries), note that sys.argv captures arguments after shell parsing. Complex queries with multiple filters should be enclosed in quotes to prevent shell fragmentation:

# Problematic: spaces break arguments
python script.py thinkphp country:"CN"  # Args split incorrectly

# Correct: quoted query preserves integrity  
python script.py "thinkphp country:CN"

For production implementations, consider migrating to the argparse module to handle optional parameters and validate inputs more robustly than manual sys.argv length checks.

Tags: Shodan ThinkPHP Vulnerability Scanning OSINT python

Posted on Tue, 25 Aug 2026 16:35:30 +0000 by midi_mick