

Detect and analyse OS‑command‑injection probes targeting the critical Ivanti Sentry vulnerability (CVE‑2023‑XXXXX) using a lightweight honeypot.
⚠️ Disclaimer – This guide is for defensive purposes only. The code shown helps you detect, log, and alert on attempted exploitation. It does not contain or facilitate any exploit payloads. Use it in a controlled environment and follow all applicable laws and your organization’s security policies.
| Item | Why you need it | Recommended version |
|---|---|---|
| Git | Clone the repo & track changes | >= 2.30 |
| Docker (optional) | Run the honeypot in an isolated container | >= 20.10 |
| Python 3.9+ | For the Python Flask example | 3.9‑3.12 |
| Node.js 18+ (or TypeScript) | For the JS/TS example | >=18.0.0 |
| Package managers | Install dependencies | pip, npm or yarn |
| HTTP(S) endpoint (e.g., a public IP, cloud VM, or a reverse‑proxy) | Where the honeypot will listen for probes | Any reachable address |
| Alerting target (Slack webhook, email SMTP, PagerDuty, etc.) | To get notified when a probe is seen | Any HTTP‑POST‑capable service |
| Basic networking knowledge | Open firewall ports (typically 80/443) and ensure the honeypot is reachable from the internet (or from a trusted test network) | — |
Tip: If you do not want to expose a real host, you can run the honeypot inside a Docker container and map only the needed ports (e.g.,
-p 8080:8080).
# Choose a directory for your work
mkdir -p ivanti-sentry-honeypot && cd $_
git init .
# Add a minimal README (optional)
echo "# Ivanti Sentry Honeypot" > README.md
git add README.md
git commit -m "Initial commit"
# Create a virtual environment (recommended)
python3 -m venv venv
source venv/bin/activate # on Windows: venv\Scripts\activate
# Install core packages
pip install --upgrade pip
pip install flask python-dotenv requests
# Initialise a Node project
npm init -y
# Install Express, dotenv, and (optional) TypeScript dev deps
npm install express dotenv
# If you prefer TypeScript:
npm install --save-dev typescript @types/node @types/express ts-node
npx tsc --init # creates tsconfig.json
ivanti-sentry-honeypot/
├─ python/
│ ├─ app.py
│ ├─ .env
│ └─ requirements.txt
├─ js/
│ ├─ src/
│ │ └─ index.ts (or index.js)
│ ├─ .env
│ └─ package.json
└─ README.md
Below are complete, copy‑and‑paste ready examples for both languages.
Each honeypot:
/ (or a configurable path).;, &&, |, $(), backticks, sleep, cat /etc/passwd, etc.).200 OK with a generic message so the attacker sees no error details (helps avoid tipping them off).Note: The detection logic is intentionally lightweight. In production you may replace it with a more robust rule engine (e.g., [Snort/Suricata signatures], [OSSEM], or a ML‑based classifier).
File: python/app.py
#!/usr/bin/env python3
"""
Ivanti Sentry OS‑command‑injection honeypot (Flask).
- Listens on host:port defined by env vars.
- Logs every request to a rotating file.
- Detects typical command‑injection payloads.
- Sends an alert via HTTP POST (Slack webhook, etc.) on detection.
"""
import os
import re
import json
import logging
from logging.handlers import RotatingFileHandler
from flask import Flask, request, abort
import requests # for alerting
# ----------------------------
# Configuration (via .env)
# ----------------------------
HOST = os.getenv("HONEYPOT_HOST", "0.0.0.0")
PORT = int(os.getenv("HONEYPOT_PORT", 8080))
LOG_PATH = os.getenv("HONEYPOT_LOG", "honeypot.log")
ALERT_URL = os.getenv("ALERT_WEBHOOK_URL") # e.g., Slack Incoming Webhook
ALERT_METHOD = os.getenv("ALERT_METHOD", "POST").upper()
ALERT_HEADERS = json.loads(os.getenv("ALERT_HEADERS", "{}")) # optional JSON string
# Detection patterns – extend as needed
COMMAND_INJECTION_PATTERNS = [
r"[;&|]`", # ; & | followed by backtick
r"\$\s*\(", # $(
r"`[^`]*`", # backticks
r"\b(sleep|ping|curl|wget|nc|netcat|bash|sh|cmd|powershell)\b",
r"/etc/passwd",
r"/etc/shadow",
r"\\bcat\\b", # simple cat
r"%0A", # URL‑encoded newline (often used in injection)
r"%0D%0A", # CRLF
]
# Compile regexes once for efficiency
INJECTION_REGEX = [re.compile(p, re.IGNORECASE) for p in COMMAND_INJECTION_PATTERNS]
# ----------------------------
# Logging setup
# ----------------------------
logger = logging.getLogger("honeypot")
logger.setLevel(logging.INFO)
handler = RotatingFileHandler(LOG_PATH, maxBytes=5_000_000, backupCount=3)
formatter = logging.Formatter(
"%(asctime)s %(level)s %(remote_addr)s %(method)s %(path)s %(msg)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
# Helper to log in a structured way
def log_request(level, msg, extra=None):
extra = extra or {}
extra.update({
"remote_addr": request.remote_addr,
"method": request.method,
"path": request.path,
})
logger.log(level, msg, extra=extra)
# ----------------------------
# Flask app
# ----------------------------
app = Flask(__name__)
def contains_injection(text: str) -> bool:
"""Return True if any injection pattern matches the supplied text."""
if not text:
return False
for regex in INJECTION_REGEX:
if regex.search(text):
return True
return False
def send_alert(payload: dict):
"""Send alert to the configured webhook (if any)."""
if not ALERT_URL:
return # no alerting configured
try:
resp = requests.request(
method=ALERT_METHOD,
url=ALERT_URL,
json=payload,
headers=ALERT_HEADERS,
timeout=5,
)
resp.raise_for_status()
logger.info("Alert sent successfully (status %s)", resp.status_code)
except Exception as exc: # pragma: no cover – network errors are logged but not fatal
logger.error("Failed to send alert: %s", exc)
@app.before_request
def inspect_request():
"""Inspect incoming request for injection patterns."""
# Gather interesting parts: query string, body, selected headers
query_string = request.query_string.decode(errors="ignore")
# Limit body size to avoid DoS on huge payloads
body = request.get_data(as_text=True)[:4096]
# Headers we care about (User-Agent, Referer, X-Forwarded-For, etc.)
headers_of_interest = {
k: v for k, v in request.headers.items()
if k.lower() in {"user-agent", "referer", "x-forwarded-for", "x-real-ip"}
}
# Combine sources for detection
sources = {
"query_string": query_string,
"body": body,
"headers": json.dumps(headers_of_interest),
}
# Check each source
for source_name, source_text in sources.items():
if contains_injection(source_text):
# Build alert payload
alert_payload = {
"timestamp": request.headers.get("Date") or "",
"source_ip": request.remote_addr,
"method": request.method,
"path": request.path,
"matched_in": source_name,
"snippet": source_text[:200], # first 200 chars for brevity
"full_headers": dict(request.headers),
"user_agent": request.headers.get("User-Agent"),
}
# Log the detection
log_request(
logging.WARNING,
f"Potential command injection detected in {source_name}",
extra={"alert": alert_payload},
)
# Send alert (non‑blocking – fire‑and‑forget)
send_alert(alert_payload)
# We *do not* abort the request; we return a generic OK to avoid leaking info.
return None # continue to the route handler
# No injection seen – just log at INFO level (optional)
log_request(
logging.INFO,
"Request received (no injection signatures)",
extra={"qs_len": len(query_string), "body_len": len(body)},
)
@app.route("/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
def catch_all():
"""Return a harmless response."""
return "OK", 200
# ----------------------------
# Entrypoint
# ----------------------------
if __name__ == "__main__":
# Show startup banner (useful when debugging)
print(f"🚀 Honeypot listening on http://{HOST}:{PORT}")
app.run(host=HOST, port=PORT, debug=False)
File: python/requirements.txt
Flask>=3.0
python-dotenv>=1.0
requests>=2.31
File: python/.env (example)
# Bind to all interfaces; change if you want to restrict
HONEYPOT_HOST=0.0.0.0
HONEYPOT_PORT=8080
# Log file location (relative to where you run the script)
HONEYPOT_LOG=honeypot.log
# Alert webhook – e.g., Slack Incoming Webhook URL
ALERT_WEBHOOK_URL=https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX
ALERT_METHOD=POST
# Optional custom headers (JSON string)
ALERT_HEADERS={"Content-Type":"application/json"}
File: js/src/index.ts
#!/usr/bin/env node
/**
* Ivanti Sentry OS‑command‑injection honeypot (Express/Node).
*
* Features mirror the Python version:
* - Configurable host/port via .env
* - Rotating file logger (winston)
* - Simple regex‑based injection detection
* - Alert via HTTP POST (Slack webhook, etc.)
* - Generic 200 OK response for all requests
*/
import express, { Request, Response, NextFunction } from "express";
import dotenv from "dotenv";
import { createLogger, format, transports } from "winston";
import * as fs from "fs";
import * as path from "path";
import axios from "axios";
dotenv.config();
// ----------------------------
// Configuration
// ----------------------------
const HOST = process.env.HONEYPOT_HOST ?? "0.0.0.0";
const PORT = parseInt(process.env.HONEYPOT_PORT ?? "8080", 10);
const LOG_DIR = process.env.HONEYPOT_LOG_DIR ?? "./logs";
const LOG_FILE = process.env.HONEYPOT_LOG_FILE ?? "honeypot.log";
const ALERT_URL = process.env.ALERT_WEBHOOK_URL ?? "";
const ALERT_METHOD = (process.env.ALERT_METHOD ?? "POST").toUpperCase() as "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
const ALERT_HEADERS_RAW = process.env.ALERT_HEADERS ?? "{}";
const ALERT_HEADERS: Record<string, string> = JSON.parse(ALERT_HEADERS_RAW);
// Ensure log directory exists
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
// ----------------------------
// Logger (Winston with daily rotation)
// ----------------------------
const { combine, timestamp, printf, errors } = format;
const logFormat = printf(({ level, message, timestamp, ...meta }) => {
const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : "";
return `${timestamp} [${level}] ${message} ${metaStr}`;
});
const logger = createLogger({
level: "info",
format: combine(
timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
errors({ stack: true }),
logFormat
),
transports: [
new transports.File({
filename: path.join(LOG_DIR, LOG_FILE),
maxsize: 5 * 1024 * 1024, // ~5 MB
maxFiles: 3,
}),
new transports.Console({
format: combine(
timestamp({ format: "HH:mm:ss" }),
logFormat
),
}),
],
});
// ----------------------------
// Injection detection patterns
// ----------------------------
const INJECTION_PATTERNS = [
/[;&|]`/, # ; & | followed by backtick
/\$\s*\(/, # $(
/`[^`]*`/, # backticks
/\b(sleep|ping|curl|wget|nc|netcat|bash|sh|cmd|powershell)\b/i,
/\/etc\/passwd/,
/\/etc\/shadow/,
/\bcat\b/,
/%0A/, # URL‑encoded newline
/%0D%0A/, # CRLF
];
function containsInjection(text: string): boolean {
if (!text) return false;
return INJECTION_PATTERNS.some((re) => re.test(text));
}
// ----------------------------
// Express app
// ----------------------------
const app = express();
// Body parsers – limit size to mitigate DoS
app.use(express.json({ limit: "1mb" }));
app.use(express.urlencoded({ extended: true, limit: "1mb" }));
app.use(express.text({ limit: "1mb", type: "*/*" }));
// Middleware to inspect every request
app.use((req: Request, _res: Response, next: NextFunction) => {
const ip = req.ip ?? req.connection.remoteAddress ?? "unknown";
const method = req.method;
const path = req.path;
// Gather sources for detection
const queryString = req.query ? JSON.stringify(req.query) : "";
// Body may be already parsed; we fall back to raw if needed
let bodyRaw = "";
if (req.body && typeof req.body === "object") {
try {
bodyRaw = JSON.stringify(req.body);
} catch {
bodyRaw = String(req.body);
}
} else if (typeof req.body === "string") {
bodyRaw = req.body;
}
// Limit length to avoid huge strings
bodyRaw = bodyRaw.slice(0, 4096);
const headersOfInterest: Record<string, string> = {};
const interested = ["user-agent", "referer", "x-forwarded-for", "x-real-ip"];
for (const h of interested) {
const val = req.get(h);
if (val) headersOfInterest[h] = val;
}
const headersStr = JSON.stringify(headersOfInterest);
const sources = {
query_string: queryString,
body: bodyRaw,
headers: headersStr,
};
let matchedSource: string | null = null;
let matchedSnippet: string | null = null;
for (const [srcName, srcText] of Object.entries(sources)) {
if (containsInjection(srcText)) {
matchedSource = srcName;
matchedSnippet = srcText.slice(0, 200); // truncate for alert
break;
}
}
if (matchedSource) {
const alertPayload = {
timestamp: new Date().toISOString(),
source_ip: ip,
method,
path,
matched_in: matchedSource,
snippet: matchedSnippet,
headers: headersOfInterest,
user_agent: req.get("user-agent") ?? "",
};
// Log as warning
logger.warn("Potential command injection detected", {
...alertPayload,
source: matchedSource,
});
// Send alert (fire‑and‑forget)
if (ALERT_URL) {
axios({
method: ALERT_METHOD,
url: ALERT_URL,
data: alertPayload,
headers: ALERT_HEADERS,
timeout: 5000,
})
.then((resp) => {
logger.info("Alert sent", { status: resp.status });
})
.catch((err) => {
logger.error("Failed to send alert", {
message: err?.message ?? err,
});
});
}
// Do NOT abort – send generic OK to avoid leaking info
return next(); // continue to route handler
}
// No injection seen – log at info level (optional)
logger.info("Request received (no injection signatures)", {
ip,
method,
path,
qs_len: queryString.length,
body_len: bodyRaw.length,
});
return next();
});
// Catch‑all route – returns a benign response
app.all("*", (_req: Request, res: Response) => {
res.status(200).send("OK");
});
// ----------------------------
// Start server
// ----------------------------
app.listen(PORT, HOST, () => {
logger.info(`🚀 Honeypot listening on http://${HOST}:${PORT}`);
});
File: js/package.json (relevant parts)
{
"name": "ivanti-sentry-honeypot-js",
"version": "1.0.0",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "ts-node src/index.ts",
"build": "tsc"
},
"dependencies": {
"axios": "^1.7.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"winston": "^3.13.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}
File: js/.env (example)
HONEYPOT_HOST=0.0.0.0
HONEYPOT_PORT=8080
HONEYPOT_LOG_DIR=./logs
HONEYPOT_LOG_FILE=honeypot.log
ALERT_WEBHOOK_URL=https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX
ALERT_METHOD=POST
ALERT_HEADERS={"Content-Type":"application/json"}
| Variable | Description | Example | Required? |
|---|---|---|---|
HONEYPOT_HOST | IP interface to bind (0.0.0.0 = all) | 0.0.0.0 | No (default) |
HONEYPOT_PORT | TCP port the honeypot listens on | 8080 | No (default 8080) |
HONEYPOT_LOG / HONEYPOT_LOG_DIR + HONEYPOT_LOG_FILE | Path to rotating log file | honeypot.log or ./logs/honeypot.log | No (default creates file) |
ALERT_WEBHOOK_URL | Endpoint that receives a JSON alert (Slack, Teams, custom HTTP) | https://hooks.slack.com/services/... | No – if omitted, alerts are only logged locally |
ALERT_METHOD | HTTP method for alert (POST/PUT/PATCH) | POST | No (default POST) |
ALERT_HEADERS | JSON string of extra headers (e.g., auth token) | {"Authorization":"Bearer xyz"} | No |
LOG_LEVEL (optional) | Python: logging level; Node: Winston level (error, warn, info, debug) | INFO | No |
How to set them
Python – create a .env file in the python/ folder and run:
source venv/bin/activate
export $(grep -v '^#' .env | xargs) # loads vars into current shell
python app.py
Node/TS – create a .env in the js/ folder and run:
npm run start # or: npm run dev for TS with ts-node
Tip: In production you’ll likely inject these variables via your orchestration platform (Docker
-e, KubernetesenvFrom, AWS ECS task definition, etc.) rather than a plain.envfile.
Add contextual data that helps analysts triage quickly:
{
"timestamp": "2025-09-24T12:34:56.789Z",
"source_ip": "203.0.113.45",
"method": "GET",
"path": "/api/v1/status",
"matched_in": "query_string",
"snippet": "; sleep 10",
"headers": {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"referer": "https://evil.example.com/"
},
"user_agent": "Mozilla/5.0 ...",
"raw_request": "GET /?cmd=%3B+sleep+10 HTTP/1.1\\nHost: example.com\\n..."
}
Why:
source_ip lets you block or geo‑IP the attacker.matched_in tells you whether the injection appeared in query string, body, or a header.snippet gives a short, safe excerpt for quick viewing.Even a honeypot can be used as a reflector or to fill logs. Add a simple middleware:
Python (Flask‑Limiter)
pip install flask-limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
)
Apply to all routes (@limiter.limit("10/minute") on the catch‑all) or globally as above.
Node (express-rate-limit)
npm install express-rate-limit
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
standardHeaders: true,
legacyHeaders: false,
});
app.use(limiter);
Many scanners fingerprint the server via HTTP headers. You can spoof a benign header to avoid raising suspicion that the host is a honeypot:
@app.after_request
def add_fake_headers(response):
response.headers["Server"] = "Ivanti Sentry 9.1.0"
response.headers["X-Powered-By"] = "ASP.NET"
return response
app.use((req, res, next) => {
res.setHeader("Server", "Ivanti Sentry 9.1.0");
res.setHeader("X-Powered-By", "ASP.NET");
next();
});
Caution: Only do this if you intend to appear like a real Sentry instance for fingerprinting purposes. If you want the honeypot to be obviously fake, omit these headers.
Both examples already use rotating file handlers (5 MB per file, keep 3). Adjust based on expected volume:
maxBytes to 50 MB, keep 10 files.| Symptom | Likely Cause | Fix |
|---|---|---|
| No logs appear | Logging level set higher than INFO or file permission issue. | Check logger level (logger.setLevel(logging.INFO) / Winston level: "info"). Ensure the process can write to the log directory (chmod 755 ./logs). |
| Alerts never reach webhook | ALERT_WEBHOOK_URL empty, network block, or TLS verification failure. | Verify the URL is set. Test with curl -X POST -H "Content-Type: application/json" -d '{"test":1}' $ALERT_WEBHOOK_URL. If using a self‑signed cert, add NODE_TLS_REJECT_UNAUTHORIZED=0 (Node) or set verify=False in requests (Python) – only for testing. |
| High CPU usage | Unlimited request size or regex scanning huge payloads. | Enforce body size limits (as shown). Consider using httptools or awscrt for faster parsing if needed. |
| False positives (legitimate traffic flagged) | Regex too broad (e.g., catching ; in normal query parameters). | Refine patterns: require a command keyword after the special character (`;\s*(sleep |
| Container fails to start | Missing dependencies or port already in use. | Run docker ps to see if another container holds the port. Re‑build with docker compose up --build. Ensure requirements.txt / package.json are installed. |
| SELinux/AppArmor blocks network outbound | Alert webhook calls denied. | Check audit logs (ausearch -m avc -ts recent) and create appropriate policy or run container with --cap-add=NET_RAW if needed. |
| HTTPS termination fails (if you put TLS in front) | Mismatched cert or missing SNI. | Verify your reverse proxy (NGINX, Traefik) forwards the original Host header and passes TLS correctly. Use --proxy-protocol if needed. |
Before exposing the honeypot to the internet (or a trusted test network), run through this list:
| ✅ Item | Why it matters |
|---|---|
| Isolate the host | Run the honeypot in a dedicated VM, container, or sandbox with no access to production networks. |
| Least‑privilege user | Execute the process as a non‑root user (e.g., honeypot user) with only the needed filesystem rights (log dir, tmp). |
| Firewall rules | Allow inbound only on the honeypot port (e.g., 8080). Outbound only to the alert webhook and, if needed, DNS/NTP for time sync. |
| TLS termination (optional) | If you expect HTTPS probes, terminate TLS at a reverse proxy (NGINX, Caddy, Traefik) and forward plain HTTP to the app. Keep the proxy patched. |
| Secrets management | Never commit .env to source control. Use Docker secrets, Kubernetes Secret, AWS Parameter Store, or HashiCorp Vault. |
| Log integrity | Enable append‑only mode or ship logs to a remote SIEM in real‑time to prevent tampering. |
| Monitoring & health‑checks | Expose a /healthz endpoint that returns 200 if the app is up. Set up alerts if the process stops. |
| Version pinning | Lock dependencies (pip freeze > requirements.txt, npm ls --depth=0 > package-lock.json) and rebuild regularly. |
| Vulnerability scanning | Scan the container image (trivy, grype) and the host OS for known issues before deployment. |
| Testing | Send benign and malicious test payloads (e.g., ?cmd=; echo test) to verify detection and alerting works before going live. |
| Documentation | Keep a run‑book: start/stop procedures, log location, alert contacts, escalation paths. |
| Legal / policy review | Ensure that running a publicly reachable honeypot is permitted by your organization's acceptable‑use policy and local laws. |
| Backup & retention | Define how long logs are kept (e.g., 30 days) and archive older logs to cold storage (S3 Glacier, Azure Blob Archive). |
| Periodic review | Every few weeks, review false‑positive/negative rates and adjust regex patterns or add allow‑lists. |
If you want a one‑liner to get the Python version running locally (assuming Docker is available):
# Build image
docker build -t ivanti-sentry-honeypot:python -f python/Dockerfile .
# Run (map port 8080, mount a local log folder)
docker run -d \
--name sentry-honeypot \
-p 8080:8080 \
-v $(pwd)/python/logs:/app/logs \
--env-file python/.env \
ivanti-sentry-honeypot:python
A similar Dockerfile can be written for the Node/TS version (use node:20-alpine as base, copy source, npm ci, then npm start).
Feel free to extend the detection logic, integrate with a threat‑intel API (e.g., AlienVault OTX, AbuseIPDB), or feed the logs into a SIEM for deeper correlation. Stay safe, and happy hunting!
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
