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.