-
Notifications
You must be signed in to change notification settings - Fork 281
Expand file tree
/
Copy pathqrcode.py
More file actions
216 lines (177 loc) · 6.66 KB
/
Copy pathqrcode.py
File metadata and controls
216 lines (177 loc) · 6.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""
MISP Expansion Module: QR Code Decoder (Anti-Quishing)
This module downloads and decodes QR codes from local attachments or remote URLs.
It includes security hardening against SSRF and DoS attacks.
"""
import binascii
import json
import re
import socket
import ipaddress
from urllib.parse import urlparse
# Third-party imports
# pylint: disable=import-error
import requests
import cv2
import numpy as np
from pyzbar import pyzbar
import urllib3
# Suppress SSL warnings for analysis purposes
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Pylint ignores for dynamic libraries like cv2
# pylint: disable=no-member
MISP_ERRORS = {"error": "Error"}
MISP_ATTRIBUTES = {
"input": ["attachment", "url", "link"],
"output": ["url", "btc"]
}
MODULE_INFO = {
"version": "0.3",
"author": "Sascha Rommelfangen",
"description": "Decode QR codes from attachments OR remote URLs (Anti-Quishing).",
"module-type": ["expansion", "hover"],
"name": "QR Code Decode",
"requirements": ["cv2", "pyzbar", "requests", "numpy"],
"input": "A QR code stored as attachment attribute or a remote URL.",
"output": "The URL or bitcoin address the QR code is pointing to.",
}
DEBUG_MODE = True
DEBUG_PREFIX = "[DEBUG] QR Code module: "
CRYPTOCURRENCIES = ["bitcoin"]
SCHEMAS = ["http://", "https://", "ftp://"]
MODULE_CONFIG = []
# --- SECURITY CONFIGURATION ---
MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB limit (Anti-DoS)
TIMEOUT_SECONDS = 10
def is_safe_url(url):
"""
SSRF Protection: Validates that the URL resolves to a public IP.
Returns: (bool, message)
"""
try:
parsed = urlparse(url)
hostname = parsed.hostname
# DNS Resolution to check real IP
ip_addr_str = socket.gethostbyname(hostname)
ip_addr = ipaddress.ip_address(ip_addr_str)
# Block private, loopback, and reserved IPs
if ip_addr.is_loopback or ip_addr.is_private or ip_addr.is_reserved:
return False, f"Blocked internal IP: {ip_addr_str}"
return True, "OK"
except Exception as e: # pylint: disable=broad-exception-caught
# Fail safe: if we can't resolve or parse, we block
return False, f"DNS Resolution failed: {str(e)}"
def fetch_url_image(target_url):
"""
Downloads image from URL with security checks (Anti-Cloaking & DoS protection).
"""
# 1. SSRF Check
is_safe, msg = is_safe_url(target_url)
if not is_safe:
return None, f"Security Block (SSRF Protection): {msg}"
try:
# Anti-Cloaking: Simulate mobile User-Agent
user_agent = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) "
"Version/15.0 Mobile/15E148 Safari/604.1"
)
headers = {'User-Agent': user_agent}
# 2. Secure Download (Stream + Size Limit)
# pylint: disable=missing-timeout
with requests.get(
target_url,
headers=headers,
timeout=TIMEOUT_SECONDS,
stream=True
) as response: # nosec
response.raise_for_status()
if 'content-length' in response.headers:
if int(response.headers['content-length']) > MAX_IMAGE_SIZE:
return None, 'Image too large (DoS protection).'
content = b""
for chunk in response.iter_content(chunk_size=8192):
content += chunk
if len(content) > MAX_IMAGE_SIZE:
return None, 'Image too large (DoS protection) - Download aborted.'
return np.frombuffer(content, np.uint8), None
except Exception as e: # pylint: disable=broad-exception-caught
return None, f"Fetch Error: {str(e)}"
# pylint: disable=too-many-return-statements, too-many-branches
def handler(q=False):
"""
Main handler function for MISP module.
"""
if q is False:
return False
q = json.loads(q)
img_array = None
filename = "unknown"
# --- CASE 1: URL Handling ---
if "url" in q or "link" in q:
target_url = q.get("url", q.get("link"))
filename = target_url
img_array, error_msg = fetch_url_image(target_url)
if error_msg:
MISP_ERRORS["error"] = error_msg
if DEBUG_MODE:
print(DEBUG_PREFIX + error_msg)
return MISP_ERRORS
# --- CASE 2: Attachment Handling ---
elif "attachment" in q:
filename = q["attachment"]
try:
img_array = np.frombuffer(binascii.a2b_base64(q["data"]), np.uint8)
except Exception: # pylint: disable=broad-exception-caught
return {'error': "Attachment error: empty or invalid data."}
else:
return {'error': 'Unsupported input. Provide an attachment or a URL.'}
# --- DECODING ---
if img_array is None:
return {'error': 'Failed to process image data.'}
try:
image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if image is None:
return {'error': 'Not a valid image file.'}
barcodes = pyzbar.decode(image)
except Exception as e: # pylint: disable=broad-exception-caught
return {'error': f'CV2/Pyzbar error: {str(e)}'}
if not barcodes:
return {'error': 'No QR code found in image.'}
for item in barcodes:
try:
result = item.data.decode()
except Exception as e: # pylint: disable=broad-exception-caught
print(f"Warning: Could not decode barcode data: {e}")
continue
if DEBUG_MODE:
print(DEBUG_PREFIX + result)
# Bitcoin logic (Legacy support)
for crypto in CRYPTOCURRENCIES:
if crypto in result:
parts = re.split(r"\:|\?", result)
if len(parts) > 1 and parts[0] in CRYPTOCURRENCIES:
return {
"results": [{
"types": ["btc"],
"values": parts[1],
"comment": f"BTC found in {filename}"
}]
}
# URL/Text Logic
is_url = any(schema in result for schema in SCHEMAS)
return {
"results": [{
"types": ["url"] if is_url else ["text"],
"values": result,
"comment": f"Decoded from {filename}"
}]
}
return {'error': "Analysis finished but no data returned."}
def introspection():
"""Returns the input and output attributes supported by the module."""
return MISP_ATTRIBUTES
def version():
"""Returns the version and configuration of the module."""
MODULE_INFO["config"] = MODULE_CONFIG
return MODULE_INFO