iotsploit-exploits 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iotsploit_exploits/__init__.py +1 -0
- iotsploit_exploits/adb_check/__init__.py +0 -0
- iotsploit_exploits/adb_check/adb_check.py +493 -0
- iotsploit_exploits/demo/__init__.py +0 -0
- iotsploit_exploits/demo/async_sleep_attack.py +106 -0
- iotsploit_exploits/demo/stream_data_attack.py +184 -0
- iotsploit_exploits/flood_attack/__init__.py +0 -0
- iotsploit_exploits/flood_attack/flood_attack.py +129 -0
- iotsploit_exploits/flood_attack/syn_flood_attack.py +233 -0
- iotsploit_exploits/greatfet_echo.py +103 -0
- iotsploit_exploits/greatfet_rubber_duck.py +417 -0
- iotsploit_exploits/hydra_cracker/weak_pass.txt +471 -0
- iotsploit_exploits/hydra_cracker/weak_pass_simple.txt +5 -0
- iotsploit_exploits/hydra_ssh_attack.py +159 -0
- iotsploit_exploits/ip_scan/__init__.py +0 -0
- iotsploit_exploits/ip_scan/ip_scan.py +196 -0
- iotsploit_exploits/nmap_scan/__init__.py +0 -0
- iotsploit_exploits/nmap_scan/nmap_scan.py +207 -0
- iotsploit_exploits/plugin_ssh.py +146 -0
- iotsploit_exploits/rubber_duck_scripts/linux_infogather.txt +126 -0
- iotsploit_exploits/rubber_duck_scripts/windows_payload.txt +93 -0
- iotsploit_exploits/serial/__init__.py +0 -0
- iotsploit_exploits/serial/picocom_serial_reader.py +704 -0
- iotsploit_exploits/simple_rubber_duck.py +183 -0
- iotsploit_exploits/wifi_scan/__init__.py +0 -0
- iotsploit_exploits/wifi_scan/wifi_scan.py +242 -0
- iotsploit_exploits-0.0.6.dist-info/METADATA +65 -0
- iotsploit_exploits-0.0.6.dist-info/RECORD +30 -0
- iotsploit_exploits-0.0.6.dist-info/WHEEL +4 -0
- iotsploit_exploits-0.0.6.dist-info/entry_points.txt +16 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pluggy
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from iotsploit_core.core.exploit_spec import ExploitResult
|
|
5
|
+
from iotsploit_django.tools.net_audit_mgr import NetAudit_Mgr
|
|
6
|
+
from iotsploit_django.tools.env_mgr import Env_Mgr
|
|
7
|
+
from iotsploit_django.tools.wifi_mgr import WiFi_Mgr
|
|
8
|
+
from iotsploit_django.tools.vehicle_utils import query_tcam_ip
|
|
9
|
+
from iotsploit_django.tools.bash_script_engine import Bash_Script_Mgr
|
|
10
|
+
from iotsploit_core.core.base_plugin import BasePlugin
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
hookimpl = pluggy.HookimplMarker("exploit_mgr")
|
|
14
|
+
|
|
15
|
+
class IPScanPlugin(BasePlugin):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__({
|
|
18
|
+
'Name': 'IP Network Scanner',
|
|
19
|
+
'Description': 'Scans network for active IP addresses and open ports',
|
|
20
|
+
'License': 'GPL',
|
|
21
|
+
'Author': ['iotsploit'],
|
|
22
|
+
'Parameters': {
|
|
23
|
+
'scan_type': {
|
|
24
|
+
'type': 'str',
|
|
25
|
+
'required': True,
|
|
26
|
+
'description': 'Type of scan to perform',
|
|
27
|
+
'default': 'tcam_ap_forward',
|
|
28
|
+
'validation': {
|
|
29
|
+
'choices': ['tcam_ap_forward']
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
'scan_mode': {
|
|
33
|
+
'type': 'str',
|
|
34
|
+
'required': True,
|
|
35
|
+
'description': 'Scan mode: fast (common ports) or full (all ports)',
|
|
36
|
+
'default': 'fast',
|
|
37
|
+
'validation': {
|
|
38
|
+
'choices': ['fast', 'full']
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
self.net_audit_mgr = NetAudit_Mgr.Instance()
|
|
45
|
+
self.env_mgr = Env_Mgr.Instance()
|
|
46
|
+
self.wifi_mgr = WiFi_Mgr.Instance()
|
|
47
|
+
self.bash_mgr = Bash_Script_Mgr.Instance()
|
|
48
|
+
|
|
49
|
+
# Constants
|
|
50
|
+
self.TCAM_AP_SCAN_IP_LIST = "tcam_ap_scan_ip_list"
|
|
51
|
+
self.DEL_ROUTES_CMDS = "del_routes_cmds"
|
|
52
|
+
|
|
53
|
+
# IP Lists
|
|
54
|
+
self.scan_iplist_fast = [
|
|
55
|
+
"10.0.0.0/8", #扫这个段会很慢
|
|
56
|
+
"198.18.0.0/16",
|
|
57
|
+
"198.99.0.0/16",
|
|
58
|
+
"192.168.0.0/16",
|
|
59
|
+
"169.254.0.0/16",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
self.scan_iplist_full = [
|
|
63
|
+
"198.18.34.1",
|
|
64
|
+
"198.18.36.1",
|
|
65
|
+
"198.18.36.2",
|
|
66
|
+
"198.18.36.3",
|
|
67
|
+
"198.18.36.4",
|
|
68
|
+
"198.18.35.2",
|
|
69
|
+
"198.18.35.3",
|
|
70
|
+
"198.18.35.7",
|
|
71
|
+
"198.18.36.177",
|
|
72
|
+
"198.18.35.4",
|
|
73
|
+
"198.18.35.11",
|
|
74
|
+
"198.18.35.12",
|
|
75
|
+
"198.18.35.13",
|
|
76
|
+
"198.18.35.14",
|
|
77
|
+
"198.18.32.18",
|
|
78
|
+
"198.18.32.1",
|
|
79
|
+
"198.18.32.2",
|
|
80
|
+
"198.99.34.1",
|
|
81
|
+
"198.18.34.2",
|
|
82
|
+
"198.18.34.10",
|
|
83
|
+
"198.18.34.11",
|
|
84
|
+
"198.18.34.15",
|
|
85
|
+
"198.18.32.19",
|
|
86
|
+
"198.18.34.3",
|
|
87
|
+
"198.18.34.4",
|
|
88
|
+
"198.18.34.5",
|
|
89
|
+
"198.18.34.14",
|
|
90
|
+
"198.18.37.20",
|
|
91
|
+
"198.18.35.5",
|
|
92
|
+
"198.18.35.161",
|
|
93
|
+
"198.18.35.6",
|
|
94
|
+
"198.18.35.128",
|
|
95
|
+
"198.18.36.96",
|
|
96
|
+
"198.18.35.8",
|
|
97
|
+
"198.18.32.17",
|
|
98
|
+
"198.18.35.9",
|
|
99
|
+
"198.18.35.10",
|
|
100
|
+
"198.18.32.24",
|
|
101
|
+
"198.99.34.15",
|
|
102
|
+
"192.168.1.3",
|
|
103
|
+
"10.1.6.6",
|
|
104
|
+
"10.1.4.6",
|
|
105
|
+
"10.1.2.6",
|
|
106
|
+
"10.1.5.6",
|
|
107
|
+
"10.1.3.6"
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
self.fast_ports = "21,22,23,5555,50000,8000,11111,12345,10086,53,1883,13400"
|
|
111
|
+
|
|
112
|
+
@hookimpl
|
|
113
|
+
def initialize(self, device_plugin=None):
|
|
114
|
+
logger.debug("Initializing IPScanPlugin")
|
|
115
|
+
self.scan_iplist = []
|
|
116
|
+
self.scan_portlist = ""
|
|
117
|
+
|
|
118
|
+
def _add_routes(self, viaip, dstips):
|
|
119
|
+
for ip in dstips:
|
|
120
|
+
route_cmds = self.env_mgr.query(self.DEL_ROUTES_CMDS, [])
|
|
121
|
+
route_cmds.append(f"ip route del {ip} via {viaip}")
|
|
122
|
+
self.env_mgr.set(self.DEL_ROUTES_CMDS, route_cmds)
|
|
123
|
+
self.bash_mgr.exec_cmd(f"ip route add {ip} via {viaip}")
|
|
124
|
+
|
|
125
|
+
def _del_routes(self):
|
|
126
|
+
route_cmds = self.env_mgr.query(self.DEL_ROUTES_CMDS, [])
|
|
127
|
+
for cmd in route_cmds:
|
|
128
|
+
self.bash_mgr.exec_cmd(cmd)
|
|
129
|
+
self.env_mgr.set(self.DEL_ROUTES_CMDS, [])
|
|
130
|
+
|
|
131
|
+
def tcam_ap_ip_scan(self):
|
|
132
|
+
iplist = self.env_mgr.query(self.TCAM_AP_SCAN_IP_LIST)
|
|
133
|
+
if iplist is None:
|
|
134
|
+
tcamip = query_tcam_ip()
|
|
135
|
+
if tcamip and self.wifi_mgr.status()['WIFI_MODE'] == "STA":
|
|
136
|
+
self._add_routes(tcamip, self.scan_iplist)
|
|
137
|
+
iplist = self.net_audit_mgr.ip_detect(self.scan_iplist)
|
|
138
|
+
self._del_routes()
|
|
139
|
+
self.env_mgr.set(self.TCAM_AP_SCAN_IP_LIST, iplist)
|
|
140
|
+
else:
|
|
141
|
+
return ExploitResult(False, "WiFi status incorrect", {})
|
|
142
|
+
return iplist
|
|
143
|
+
|
|
144
|
+
@hookimpl
|
|
145
|
+
def execute(self, target: Optional[dict] = None, parameters: Optional[dict] = None) -> ExploitResult:
|
|
146
|
+
"""
|
|
147
|
+
Execute IP network scan.
|
|
148
|
+
|
|
149
|
+
Note: This plugin scans predefined internal IP lists to check if TCAM AP
|
|
150
|
+
exposes vehicle internal network. The 'target' parameter is not used.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
target: Not used by this plugin (scans predefined IP lists)
|
|
154
|
+
parameters: Plugin configuration
|
|
155
|
+
- scan_type: Type of scan ('tcam_ap_forward')
|
|
156
|
+
- scan_mode: Scan mode ('fast' or 'full')
|
|
157
|
+
"""
|
|
158
|
+
# Get parameters from 'parameters' dict (not from 'target')
|
|
159
|
+
parameters = parameters or {}
|
|
160
|
+
scan_type = parameters.get('scan_type', self.info['Parameters']['scan_type']['default'])
|
|
161
|
+
scan_mode = parameters.get('scan_mode', self.info['Parameters']['scan_mode']['default'])
|
|
162
|
+
|
|
163
|
+
logger.info(f"Starting IP scan: type={scan_type}, mode={scan_mode}")
|
|
164
|
+
|
|
165
|
+
# Set scan parameters based on mode
|
|
166
|
+
if scan_mode == "fast":
|
|
167
|
+
self.scan_iplist = self.scan_iplist_fast
|
|
168
|
+
self.scan_portlist = self.fast_ports
|
|
169
|
+
elif scan_mode == "full":
|
|
170
|
+
self.scan_iplist = self.scan_iplist_full
|
|
171
|
+
self.scan_portlist = "1-65535"
|
|
172
|
+
else:
|
|
173
|
+
return ExploitResult(False, f"Invalid scan mode: {scan_mode}", {})
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
if scan_type == "tcam_ap_forward":
|
|
177
|
+
iplist = self.tcam_ap_ip_scan()
|
|
178
|
+
if iplist:
|
|
179
|
+
return ExploitResult(False, f"TCAM AP exposed internal IPs: {iplist}", {"exposed_ips": iplist})
|
|
180
|
+
return ExploitResult(True, "No internal IPs exposed through TCAM AP", {})
|
|
181
|
+
# Add other scan types here...
|
|
182
|
+
|
|
183
|
+
return ExploitResult(False, f"Unknown scan type: {scan_type}", {})
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error during IP scan: {str(e)}")
|
|
187
|
+
return ExploitResult(False, f"IP scan failed: {str(e)}", {})
|
|
188
|
+
|
|
189
|
+
@hookimpl
|
|
190
|
+
def cleanup(self):
|
|
191
|
+
logger.info("Cleaning up IPScanPlugin")
|
|
192
|
+
self._del_routes()
|
|
193
|
+
|
|
194
|
+
def register_plugin(pm):
|
|
195
|
+
ip_scan_plugin = IPScanPlugin()
|
|
196
|
+
pm.register(ip_scan_plugin)
|
|
File without changes
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pluggy
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from iotsploit_core.core.base_plugin import BasePlugin
|
|
6
|
+
from iotsploit_core.core.exploit_spec import ExploitResult
|
|
7
|
+
from iotsploit_core.core.tool_manager import get_tool_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
hookimpl = pluggy.HookimplMarker("exploit_mgr")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _extract_ip_from_target(target: Any) -> Optional[str]:
|
|
15
|
+
"""
|
|
16
|
+
Extract vehicle IP address from target dict.
|
|
17
|
+
|
|
18
|
+
Design decision:
|
|
19
|
+
- Plugin runs in dict mode at the boundary.
|
|
20
|
+
- The canonical key for vehicle IP is `ip_address` (consistent with core domain Vehicle.get_info()).
|
|
21
|
+
"""
|
|
22
|
+
if not isinstance(target, dict):
|
|
23
|
+
return None
|
|
24
|
+
ip = target.get("ip_address") or target.get("ip") or target.get("host")
|
|
25
|
+
if isinstance(ip, str) and ip.strip():
|
|
26
|
+
return ip.strip()
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_nmap_grepable(output: str) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Parse nmap grepable output (-oG -).
|
|
33
|
+
|
|
34
|
+
We intentionally use grepable output instead of XML to keep parsing lightweight
|
|
35
|
+
and avoid bringing in additional dependencies.
|
|
36
|
+
"""
|
|
37
|
+
hosts: List[Dict[str, Any]] = []
|
|
38
|
+
current_host: Optional[Dict[str, Any]] = None
|
|
39
|
+
|
|
40
|
+
for raw_line in (output or "").splitlines():
|
|
41
|
+
line = raw_line.strip()
|
|
42
|
+
if not line:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
# Example:
|
|
46
|
+
# Host: 192.168.1.10 () Status: Up
|
|
47
|
+
# Host: 192.168.1.10 () Ports: 22/open/tcp//ssh///, 80/open/tcp//http///
|
|
48
|
+
if line.startswith("Host: "):
|
|
49
|
+
# Split once at "Status:" or "Ports:" if present
|
|
50
|
+
# Keep it robust against slight format changes.
|
|
51
|
+
host_ip = None
|
|
52
|
+
try:
|
|
53
|
+
# "Host: <ip> (" -> take second token
|
|
54
|
+
host_ip = line.split()[1]
|
|
55
|
+
except Exception:
|
|
56
|
+
host_ip = None
|
|
57
|
+
|
|
58
|
+
# Start a new host record when we see a Host line with Status/Ports
|
|
59
|
+
if host_ip:
|
|
60
|
+
if current_host is None or current_host.get("ip") != host_ip:
|
|
61
|
+
current_host = {"ip": host_ip, "status": None, "ports": []}
|
|
62
|
+
hosts.append(current_host)
|
|
63
|
+
|
|
64
|
+
if current_host is None:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if "Status:" in line:
|
|
68
|
+
# Status: Up/Down
|
|
69
|
+
status = line.split("Status:", 1)[1].strip()
|
|
70
|
+
current_host["status"] = status
|
|
71
|
+
|
|
72
|
+
if "Ports:" in line:
|
|
73
|
+
ports_str = line.split("Ports:", 1)[1].strip()
|
|
74
|
+
parsed_ports: List[Dict[str, Any]] = []
|
|
75
|
+
for entry in ports_str.split(","):
|
|
76
|
+
e = entry.strip()
|
|
77
|
+
if not e:
|
|
78
|
+
continue
|
|
79
|
+
# "22/open/tcp//ssh///"
|
|
80
|
+
fields = e.split("/")
|
|
81
|
+
if len(fields) < 3:
|
|
82
|
+
continue
|
|
83
|
+
port_s, state, proto = fields[0], fields[1], fields[2]
|
|
84
|
+
service = fields[4] if len(fields) > 4 else ""
|
|
85
|
+
try:
|
|
86
|
+
port = int(port_s)
|
|
87
|
+
except ValueError:
|
|
88
|
+
continue
|
|
89
|
+
parsed_ports.append(
|
|
90
|
+
{
|
|
91
|
+
"port": port,
|
|
92
|
+
"state": state,
|
|
93
|
+
"protocol": proto,
|
|
94
|
+
"service": service,
|
|
95
|
+
"raw": e,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
current_host["ports"] = parsed_ports
|
|
99
|
+
|
|
100
|
+
return {"hosts": hosts}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class NmapScanPlugin(BasePlugin):
|
|
104
|
+
def __init__(self):
|
|
105
|
+
super().__init__(
|
|
106
|
+
{
|
|
107
|
+
"Name": "Nmap Scan",
|
|
108
|
+
"Description": "Run nmap against vehicle ip_address (dict-mode target).",
|
|
109
|
+
"License": "GPL",
|
|
110
|
+
"Author": ["iotsploit"],
|
|
111
|
+
"RequiresRoot": False,
|
|
112
|
+
"Parameters": {
|
|
113
|
+
"ports": {
|
|
114
|
+
"type": "str",
|
|
115
|
+
"required": False,
|
|
116
|
+
"description": "Ports to scan. Example: '22,80,443' or '1-1000'. Default: top ports (nmap default).",
|
|
117
|
+
"default": "",
|
|
118
|
+
},
|
|
119
|
+
"scan_args": {
|
|
120
|
+
"type": "str",
|
|
121
|
+
"required": False,
|
|
122
|
+
"description": "Extra nmap args appended as-is. Example: '-sV --reason'.",
|
|
123
|
+
"default": "-sV --reason",
|
|
124
|
+
},
|
|
125
|
+
"timeout_s": {
|
|
126
|
+
"type": "int",
|
|
127
|
+
"required": False,
|
|
128
|
+
"description": "Execution timeout in seconds (ToolManager subprocess timeout).",
|
|
129
|
+
"default": 120,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@hookimpl
|
|
136
|
+
def initialize(self, device_plugin: Optional[Any] = None):
|
|
137
|
+
# No-op; tool availability checked on execute.
|
|
138
|
+
logger.debug("Initializing NmapScanPlugin")
|
|
139
|
+
|
|
140
|
+
@hookimpl
|
|
141
|
+
def execute(self, target: Optional[Any] = None, parameters: Optional[dict] = None) -> ExploitResult:
|
|
142
|
+
parameters = parameters or {}
|
|
143
|
+
|
|
144
|
+
ip = _extract_ip_from_target(target)
|
|
145
|
+
if not ip:
|
|
146
|
+
return ExploitResult(False, "Missing target ip_address (dict target expected)", {})
|
|
147
|
+
|
|
148
|
+
tool_mgr = get_tool_manager()
|
|
149
|
+
if not tool_mgr.is_available("nmap"):
|
|
150
|
+
status = tool_mgr.get_tool_status("nmap")
|
|
151
|
+
return ExploitResult(
|
|
152
|
+
False,
|
|
153
|
+
f"nmap not available: {getattr(status, 'value', str(status))}",
|
|
154
|
+
{"tool": "nmap", "status": getattr(status, "value", str(status))},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
ports = (parameters.get("ports") or "").strip()
|
|
158
|
+
scan_args = (parameters.get("scan_args") or "").strip()
|
|
159
|
+
timeout_s = parameters.get("timeout_s", 120)
|
|
160
|
+
try:
|
|
161
|
+
timeout_s = int(timeout_s)
|
|
162
|
+
except Exception:
|
|
163
|
+
timeout_s = 120
|
|
164
|
+
|
|
165
|
+
# Build args:
|
|
166
|
+
# -oG - : grepable output to stdout for lightweight parsing
|
|
167
|
+
args: List[str] = []
|
|
168
|
+
if scan_args:
|
|
169
|
+
# naive split; good enough for typical CLI-like args
|
|
170
|
+
args.extend(scan_args.split())
|
|
171
|
+
args.extend(["-oG", "-", "-Pn"])
|
|
172
|
+
if ports:
|
|
173
|
+
args.extend(["-p", ports])
|
|
174
|
+
args.append(ip)
|
|
175
|
+
|
|
176
|
+
logger.info("Running nmap scan on %s (ports=%s)", ip, ports or "default")
|
|
177
|
+
result = tool_mgr.execute("nmap", args=args, timeout=timeout_s)
|
|
178
|
+
|
|
179
|
+
# nmap can return non-zero even with useful output; treat as success if we got stdout.
|
|
180
|
+
stdout = (result.stdout or "").strip()
|
|
181
|
+
stderr = (result.stderr or "").strip()
|
|
182
|
+
parsed = _parse_nmap_grepable(stdout)
|
|
183
|
+
|
|
184
|
+
success = bool(parsed.get("hosts")) or result.success
|
|
185
|
+
message = "nmap scan completed" if success else "nmap scan failed"
|
|
186
|
+
|
|
187
|
+
data: Dict[str, Any] = {
|
|
188
|
+
"target_ip": ip,
|
|
189
|
+
"command": result.command,
|
|
190
|
+
"return_code": result.return_code,
|
|
191
|
+
"execution_time": result.execution_time,
|
|
192
|
+
"stdout": stdout,
|
|
193
|
+
"stderr": stderr,
|
|
194
|
+
"parsed": parsed,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return ExploitResult(success, message, data)
|
|
198
|
+
|
|
199
|
+
@hookimpl
|
|
200
|
+
def cleanup(self):
|
|
201
|
+
logger.debug("Cleaning up NmapScanPlugin")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def register_plugin(pm):
|
|
205
|
+
pm.register(NmapScanPlugin())
|
|
206
|
+
|
|
207
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import os
|
|
2
|
+
# Disable pwntools terminal mode before importing to prevent fileno() errors in non-TTY environments
|
|
3
|
+
os.environ.setdefault("PWNLIB_NOTERM", "1")
|
|
4
|
+
|
|
5
|
+
import pluggy
|
|
6
|
+
import logging
|
|
7
|
+
from pwn import ssh
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
from iotsploit_core.core.exploit_spec import ExploitResult
|
|
10
|
+
from iotsploit_core.core.base_plugin import BasePlugin
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
hookimpl = pluggy.HookimplMarker("exploit_mgr")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_ip_from_target(target: Optional[dict]) -> Optional[str]:
|
|
17
|
+
"""
|
|
18
|
+
Extract IP address from target dict.
|
|
19
|
+
|
|
20
|
+
Design decision:
|
|
21
|
+
- Plugin runs in dict mode at the boundary.
|
|
22
|
+
- The canonical key for IP is `ip_address` (consistent with core domain Target.get_info()).
|
|
23
|
+
"""
|
|
24
|
+
if not isinstance(target, dict):
|
|
25
|
+
return None
|
|
26
|
+
ip = target.get("ip_address") or target.get("ip") or target.get("host")
|
|
27
|
+
if isinstance(ip, str) and ip.strip():
|
|
28
|
+
return ip.strip()
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
class SSHPlugin(BasePlugin):
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__({
|
|
34
|
+
'Name': 'SSH Remote Command',
|
|
35
|
+
'Description': 'Connect to target via SSH and execute a command.',
|
|
36
|
+
'License': 'GPL',
|
|
37
|
+
'Author': ['iotsploit'],
|
|
38
|
+
'Parameters': {
|
|
39
|
+
'user': {
|
|
40
|
+
'type': 'str',
|
|
41
|
+
'required': False,
|
|
42
|
+
'description': 'SSH username',
|
|
43
|
+
'default': 'root'
|
|
44
|
+
},
|
|
45
|
+
'passwd': {
|
|
46
|
+
'type': 'str',
|
|
47
|
+
'required': False,
|
|
48
|
+
'description': 'SSH password',
|
|
49
|
+
'default': '123456'
|
|
50
|
+
},
|
|
51
|
+
'cmd': {
|
|
52
|
+
'type': 'str',
|
|
53
|
+
'required': False,
|
|
54
|
+
'description': 'Command to run on the remote host',
|
|
55
|
+
'default': 'ls -l'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
self.ssh_mgr = SSH_Mgr()
|
|
60
|
+
|
|
61
|
+
@hookimpl
|
|
62
|
+
def initialize(self):
|
|
63
|
+
logger.debug("Initializing SSHPlugin")
|
|
64
|
+
|
|
65
|
+
@hookimpl
|
|
66
|
+
def execute(self, target: Optional[dict] = None, parameters: Optional[dict] = None) -> ExploitResult:
|
|
67
|
+
# Extract IP from target dict (normalized by exploit_manager)
|
|
68
|
+
target_ip = _extract_ip_from_target(target)
|
|
69
|
+
if not target_ip:
|
|
70
|
+
logger.warning("No valid target IP. Expected dict with 'ip_address' key.")
|
|
71
|
+
return ExploitResult(False, "Missing target ip_address (dict target expected)", {})
|
|
72
|
+
|
|
73
|
+
# Merge parameters with defaults
|
|
74
|
+
parameters = parameters or {}
|
|
75
|
+
user = parameters.get('user', self.info['Parameters']['user']['default'])
|
|
76
|
+
passwd = parameters.get('passwd', self.info['Parameters']['passwd']['default'])
|
|
77
|
+
cmd = parameters.get('cmd', self.info['Parameters']['cmd']['default'])
|
|
78
|
+
|
|
79
|
+
logger.info(f"Executing SSH command on {target_ip}: {cmd}")
|
|
80
|
+
|
|
81
|
+
ssh_context = self.ssh_mgr.open_ssh(target_ip, user, passwd)
|
|
82
|
+
if not ssh_context:
|
|
83
|
+
return ExploitResult(False, "Failed to establish SSH connection", {})
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = self.ssh_mgr.ssh_cmd(ssh_context, cmd)
|
|
87
|
+
self.ssh_mgr.close_ssh(ssh_context)
|
|
88
|
+
if result is None:
|
|
89
|
+
return ExploitResult(False, "SSH command execution failed", {})
|
|
90
|
+
return ExploitResult(True, "SSH command executed successfully", {"result": result})
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.exception("SSH command execution error")
|
|
93
|
+
try:
|
|
94
|
+
self.ssh_mgr.close_ssh(ssh_context)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
return ExploitResult(False, f"SSH execution failed: {str(e)}", {})
|
|
98
|
+
|
|
99
|
+
class SSH_Mgr:
|
|
100
|
+
__ssh_connect_timeout_S = 1
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def Instance():
|
|
104
|
+
return _instance
|
|
105
|
+
|
|
106
|
+
def __init__(self):
|
|
107
|
+
self.__ssh_dict = {}
|
|
108
|
+
|
|
109
|
+
def open_ssh(self, ip: str, user: str, passwd: str):
|
|
110
|
+
try:
|
|
111
|
+
ssh_context = ssh(host=ip, user=user, password=passwd, timeout=self.__ssh_connect_timeout_S)
|
|
112
|
+
logger.info("SSH connect %s success.", ip)
|
|
113
|
+
return ssh_context
|
|
114
|
+
except Exception:
|
|
115
|
+
logger.exception("SSH connect %s fail!", ip)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def close_ssh(self, ssh_context: Any):
|
|
119
|
+
if ssh_context is None:
|
|
120
|
+
logger.error("SSH context invalid!")
|
|
121
|
+
return
|
|
122
|
+
try:
|
|
123
|
+
ssh_context.close()
|
|
124
|
+
logger.info("SSH close success.")
|
|
125
|
+
except Exception:
|
|
126
|
+
logger.exception("SSH close fail!")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
def ssh_cmd(self, ssh_context: Any, ssh_cmd: str):
|
|
130
|
+
if ssh_context is None:
|
|
131
|
+
logger.error("SSH context invalid!")
|
|
132
|
+
return None
|
|
133
|
+
try:
|
|
134
|
+
# Run the command via remote process and collect output
|
|
135
|
+
proc = ssh_context.process(["sh", "-lc", ssh_cmd])
|
|
136
|
+
cmd_result = proc.recvall().decode('utf-8', errors='ignore')
|
|
137
|
+
logger.info("SSH run CMD success. CMD: %s\nResult:\n%s", ssh_cmd, cmd_result)
|
|
138
|
+
return cmd_result
|
|
139
|
+
except Exception:
|
|
140
|
+
logger.exception("SSH run CMD fail! CMD: %s", ssh_cmd)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
_instance = SSH_Mgr()
|
|
144
|
+
|
|
145
|
+
def register_plugin(pm):
|
|
146
|
+
pm.register(SSHPlugin())
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
REM Linux Information Gathering Script for Rubber Duck
|
|
2
|
+
REM -----------------------------------------------
|
|
3
|
+
REM This script opens a terminal and gathers basic system information
|
|
4
|
+
|
|
5
|
+
REM Wait for OS to recognize the device
|
|
6
|
+
DELAY 1000
|
|
7
|
+
|
|
8
|
+
REM Open terminal (Ctrl+Alt+T on most Linux distros)
|
|
9
|
+
CTRL ALT T
|
|
10
|
+
DELAY 1500
|
|
11
|
+
|
|
12
|
+
REM Create a directory for collected information
|
|
13
|
+
STRING mkdir -p ~/duck_info
|
|
14
|
+
DELAY 200
|
|
15
|
+
ENTER
|
|
16
|
+
DELAY 500
|
|
17
|
+
|
|
18
|
+
REM Switch to that directory
|
|
19
|
+
STRING cd ~/duck_info
|
|
20
|
+
DELAY 200
|
|
21
|
+
ENTER
|
|
22
|
+
DELAY 300
|
|
23
|
+
|
|
24
|
+
REM Create a script file to collect information
|
|
25
|
+
STRING cat > collect.sh << 'EOF'
|
|
26
|
+
DELAY 200
|
|
27
|
+
ENTER
|
|
28
|
+
STRING #!/bin/bash
|
|
29
|
+
DELAY 100
|
|
30
|
+
ENTER
|
|
31
|
+
STRING echo "=== System Information Collected by Rubber Duck ===" > report.txt
|
|
32
|
+
DELAY 100
|
|
33
|
+
ENTER
|
|
34
|
+
STRING echo -e "\n=== Date and Time ===" >> report.txt
|
|
35
|
+
DELAY 100
|
|
36
|
+
ENTER
|
|
37
|
+
STRING date >> report.txt
|
|
38
|
+
DELAY 100
|
|
39
|
+
ENTER
|
|
40
|
+
STRING echo -e "\n=== Hostname and IP ===" >> report.txt
|
|
41
|
+
DELAY 100
|
|
42
|
+
ENTER
|
|
43
|
+
STRING hostname -I >> report.txt
|
|
44
|
+
DELAY 100
|
|
45
|
+
ENTER
|
|
46
|
+
STRING echo -e "\n=== System Info ===" >> report.txt
|
|
47
|
+
DELAY 100
|
|
48
|
+
ENTER
|
|
49
|
+
STRING uname -a >> report.txt
|
|
50
|
+
DELAY 100
|
|
51
|
+
ENTER
|
|
52
|
+
STRING echo -e "\n=== CPU Info ===" >> report.txt
|
|
53
|
+
DELAY 100
|
|
54
|
+
ENTER
|
|
55
|
+
STRING lscpu | grep "Model name" >> report.txt
|
|
56
|
+
DELAY 100
|
|
57
|
+
ENTER
|
|
58
|
+
STRING echo -e "\n=== Memory Info ===" >> report.txt
|
|
59
|
+
DELAY 100
|
|
60
|
+
ENTER
|
|
61
|
+
STRING free -h >> report.txt
|
|
62
|
+
DELAY 100
|
|
63
|
+
ENTER
|
|
64
|
+
STRING echo -e "\n=== Disk Space ===" >> report.txt
|
|
65
|
+
DELAY 100
|
|
66
|
+
ENTER
|
|
67
|
+
STRING df -h >> report.txt
|
|
68
|
+
DELAY 100
|
|
69
|
+
ENTER
|
|
70
|
+
STRING echo -e "\n=== Network Connections ===" >> report.txt
|
|
71
|
+
DELAY 100
|
|
72
|
+
ENTER
|
|
73
|
+
STRING netstat -tun | head -20 >> report.txt
|
|
74
|
+
DELAY 100
|
|
75
|
+
ENTER
|
|
76
|
+
STRING echo -e "\n=== Running Processes ===" >> report.txt
|
|
77
|
+
DELAY 100
|
|
78
|
+
ENTER
|
|
79
|
+
STRING ps aux | head -20 >> report.txt
|
|
80
|
+
DELAY 100
|
|
81
|
+
ENTER
|
|
82
|
+
STRING echo -e "\n=== Logged In Users ===" >> report.txt
|
|
83
|
+
DELAY 100
|
|
84
|
+
ENTER
|
|
85
|
+
STRING who >> report.txt
|
|
86
|
+
DELAY 100
|
|
87
|
+
ENTER
|
|
88
|
+
STRING echo -e "\n=== Package Information ===" >> report.txt
|
|
89
|
+
DELAY 100
|
|
90
|
+
ENTER
|
|
91
|
+
STRING if [ -x "$(command -v dpkg)" ]; then dpkg --list | grep -v "^ii" | wc -l >> report.txt; fi
|
|
92
|
+
DELAY 100
|
|
93
|
+
ENTER
|
|
94
|
+
STRING if [ -x "$(command -v rpm)" ]; then rpm -qa | wc -l >> report.txt; fi
|
|
95
|
+
DELAY 100
|
|
96
|
+
ENTER
|
|
97
|
+
STRING echo "Information collection complete!" >> report.txt
|
|
98
|
+
DELAY 100
|
|
99
|
+
ENTER
|
|
100
|
+
STRING EOF
|
|
101
|
+
DELAY 200
|
|
102
|
+
ENTER
|
|
103
|
+
DELAY 500
|
|
104
|
+
|
|
105
|
+
REM Make the script executable
|
|
106
|
+
STRING chmod +x collect.sh
|
|
107
|
+
DELAY 200
|
|
108
|
+
ENTER
|
|
109
|
+
DELAY 500
|
|
110
|
+
|
|
111
|
+
REM Run the script
|
|
112
|
+
STRING ./collect.sh
|
|
113
|
+
DELAY 200
|
|
114
|
+
ENTER
|
|
115
|
+
DELAY 5000
|
|
116
|
+
|
|
117
|
+
REM Display the collected information
|
|
118
|
+
STRING cat report.txt
|
|
119
|
+
DELAY 200
|
|
120
|
+
ENTER
|
|
121
|
+
DELAY 2000
|
|
122
|
+
|
|
123
|
+
REM Close the terminal with exit command
|
|
124
|
+
STRING exit
|
|
125
|
+
DELAY 200
|
|
126
|
+
ENTER
|