Fourth and last post in the Offensive Callback series. This one covers what happens server-side once the callback server has an IP address — the full intelligence enrichment pipeline that runs automatically for every new source IP that hits the system.
The goal is to go from “an IP connected” to “here’s everything we know about that IP” with minimal manual work.
The IPINT table
Every unique IP that hits the server gets a record in the IPINT table. Schema:
| Column | Content |
|---|---|
IP |
Source IP address |
MAC |
MAC address (if available from ARP / LAN) |
GEO |
Geolocation JSON (country, city, ISP, ASN, lat/lon) |
DATA |
WHOIS + OSINT results (Shodan, CriminalIP, VirusTotal) |
INTEL |
Combined intelligence summary |
SCAN |
Boolean — whether Nmap has been run against this IP |
The DATA column is a JSON object that accumulates data from multiple sources over time. Each OSINT fetch merges its result into the existing object:
1 | const updatedData = { ...existingData, shodan: shodanData }; |
So a fully enriched record looks like:
1 | { |
Each source is independent — if Shodan fails, CriminalIP still runs, and the partial result is stored.
Layer 1 — GeoIP (ip-api.com)
The first enrichment that runs on every new IP is geolocation, via ip-api.com. It’s free, no API key required, and fast. The server queries it server-side (not from the browser, so no CORS issues):
1 | const response = await axios.get( |
Result stored in the GEO column:
1 | { |
GeoIP is cached — if the GEO column is already populated, the lookup is skipped. This avoids hammering ip-api with repeated queries for the same IP and respects their free tier rate limits.
The function includes retry logic with a 3-second backoff, up to 3 attempts:
1 | async function fetchGeoLocation(ip, retries = 3) { |
GeoIP failure is non-fatal — the rest of the enrichment pipeline runs even if geolocation fails.
Layer 2 — WHOIS
WHOIS runs right after GeoIP via the whois-json package. It queries the standard WHOIS servers for the IP’s registration information: registrant, abuse contact, network block, registration date.
1 | const whoisData = await whois(ip); |
Like GeoIP, it’s cached — if DATA.whois already exists, the query is skipped. The function has clean error handling after I had a bug where a thrown error inside the catch block was silently swallowing WHOIS failures and causing downstream code to fire on undefined.
WHOIS is most useful for identifying:
- VPS/cloud providers (AWS, DigitalOcean, Vultr, Hetzner blocks)
- TOR exit nodes and VPN providers
- Corporate netblocks (when someone from a company’s IP range hits your dealer)
- Abuse contacts for reporting
Layer 3 — Shodan
Shodan integration uses the shodan-client npm package. Triggered manually from the /scanners dashboard view or via the API, never automatically (to avoid burning API credits on every hit).
1 | const ShodanClient = require('shodan-client'); |
What Shodan returns for an IP that has indexed data:
- Open ports and services (SSH, HTTP, HTTPS, RDP, SMB, etc.)
- Service banners and version strings
- SSL certificate details
- Historical data — when the host was last seen, what ports were open then
- CVEs detected (if Shodan has matched vulnerability signatures)
- Tags:
cloud,tor,vpn,scanner,honeypot
Stored under DATA.shodan. From the dashboard you can see at a glance whether a source IP is running exposed services — a corporate IP with RDP open on 3389 is interesting context for an assessment.
Layer 4 — CriminalIP
CriminalIP is a threat intelligence platform focused on IP reputation. The API call is to their asset report endpoint:
1 | const { data } = await axios.get( |
What comes back:
- Abuse score: numerical threat level (0–100)
- Port scan results: what CriminalIP sees exposed on the IP
- Inbound/outbound connection counts: how many systems this IP has connected to or been connected from
- Reputation categories:
malicious,scanner,proxy,bot,tor - Recent activity: timeline of observed malicious behavior
- Domain associations: domains that have pointed to this IP
CriminalIP is particularly good at identifying IPs from known botnets, TOR exit nodes, and commercial VPN providers. If an IP that hit your dealer shows up as a known scanner or botnet node, that’s signal about what’s actually happening vs. what looks like a real user.
Stored under DATA.criminalip. The /scanners dashboard shows the abuse score prominently and color-codes the IP row in the log view.
Layer 5 — VirusTotal
VirusTotal’s IP address endpoint aggregates results from 70+ security vendors:
1 | const url = `https://www.virustotal.com/api/v3/ip_addresses/${ip}`; |
Key data from the response:
- Malicious votes: how many engines have flagged this IP as malicious
- Suspicious votes: how many have flagged as suspicious
- Community score: aggregated community reputation
- JARM hash: TLS fingerprint of the server on that IP (useful for C2 infrastructure identification)
- Historical samples: malware samples that communicated with this IP
- Communicating files: known malware families seen connecting to this host
- Passive DNS: domains that have resolved to this IP over time
VirusTotal is most valuable for identifying C2 infrastructure — if a callback from what looks like a normal user’s IP shows 15/70 AV vendors flagging it as malicious, it’s worth investigating whether that user is connecting from a compromised machine or using a proxy.
Stored under DATA.virustotal.
Layer 6 — Nmap
For IPs where you want active scanning (controlled environments, authorized assessments), the server can trigger an Nmap scan via the node-nmap package. The scan result is stored and the SCAN boolean on the IPINT record is set to true.
This is never automatic — always manually triggered from the /scanners dashboard. Nmap scanning an IP without authorization is a separate legal question from passive collection, so it stays behind a manual trigger.
IP blacklisting — iptables integration
The most operationally useful defensive feature: from the /options panel you can ban an IP directly to iptables.
1 | async function banIP(ip) { |
The -I flag inserts the rule at the top of the INPUT chain, so it takes effect immediately without a restart. The blacklist is persisted to blacklist.json so rules can be restored on server restart.
Unban is symmetric:
1 | async function unbanIP(ip) { |
Notice the IP validation before any iptables call:
1 | function isValidIP(ip) { |
This is the command injection guard. The IP goes directly into a shell command via exec('iptables ...'), so without validation, a malicious IP value in the database (think stored XSS in a log field) could execute arbitrary commands as root. The validation rejects anything that isn’t a valid IPv4 or IPv6 address.
Use cases for the ban system:
- Scanners: automated security scanners hit the server constantly. Once identified (CriminalIP tags, Shodan
scannertag), ban them to reduce noise in the logs - Known bad actors: if you’re running an operation and identify an adversarial IP trying to enumerate your dealers, ban it
- Internal ops: ban your own test IPs to keep log views clean during development
The /infoIPX view
The dashboard has a dedicated IP detail view at /infoIPX that aggregates all layers for a single IP into one page: GeoIP card, WHOIS block, Shodan ports table, CriminalIP score gauge, VirusTotal verdict count, and all log entries from that source. This is the view I use when I want to understand a specific callback — who sent it, from where, and what their infrastructure looks like.
Practical workflow
When a new IP hits and I want to understand it quickly:
- GeoIP is already populated automatically — check country and ISP
- WHOIS is already populated — check netblock owner
- Run Shodan from the
/scannerspanel — check exposed services and tags - If Shodan shows scanner/honeypot/bot tags, run CriminalIP for reputation score
- If there are malware associations, run VirusTotal for full vendor context
- Decision: interesting target (keep watching), noise (ban via iptables), or known infrastructure (tag and note)
The whole pipeline is designed around the reality that most hits are noise — crawlers, scanners, accidental traffic. The OSINT layers help filter signal from noise without manually investigating every source IP.
That wraps the four-part series on Offensive Callback. The tool is available on GitHub — the project has evolved a lot since the first post introducing 5ELG back in 2024, and what I’ve been calling “Offensive Callback” is the current state of that work.
![Keramon [Child / Virus] Keramon](/blog/img/digimon-pets/keramon.gif)
![Chrysalimon [Adult / Unknown] Chrysalimon](/blog/img/digimon-pets/chrysalimon.gif)