Intentionally desktop-first — best experienced on a workstation
Portfolio
Security Lab Log · Entry 007

Confirmed IOC Matching & Severity Scoring Correction

Analyst
Yana Ivanov
Date
March 19, 2026
Classification
Public — Educational Use
Builds On
Lab Log 006 — zeek_triage.py
New Function
get_known_bad_hits()
Validated Against
Lumma Stealer · Easy As 123 C2
IOC MATCHING · DYNAMIC DNS DETECTION · SEVERITY LOGIC CORRECTED · MARCH 19 2026
Section 01

The Problem With Volume-Based Severity

When Lab Log 006 validated the triage script against the Easy As 123 C2 pcap, it scored MEDIUM. The reasoning at the time: only 61.5KB of data had been sent to the C2 server, which fell below the CRITICAL threshold of 1MB. Low data volume, lower urgency.

That logic is wrong. Volume measures damage already done — it says nothing about the threat that is present right now. The Easy As 123 machine had a confirmed active C2 connection to 45.131.214.85, sending POST requests to /fakeurl.htm every 60 seconds for over four hours. The malware was installed, it had established contact with its operator, and it was waiting for a command payload. An attacker with access to that C2 server could have sent ransomware, a credential harvester, or a lateral movement tool at any moment.

The correct severity question is not "how much damage has occurred?" — it is "is a confirmed threat present?" A machine with an active C2 connection is CRITICAL regardless of byte count. The window is open. The severity logic needed to be corrected.

Before — Volume-Based Scoring

Easy As 123: 61.5KB sent → MEDIUM

Lumma Stealer: 2.27MB sent → CRITICAL

Flaw: Confuses "not much damage yet" with "not serious." A confirmed C2 beacon is always serious regardless of how many bytes have moved.

After — IOC-Based Scoring

Easy As 123: fakeurl.htm confirmed IOC → CRITICAL

Lumma Stealer: 5 confirmed IOCs → CRITICAL

Fix: Any match against KNOWN_BAD_DOMAINS immediately scores CRITICAL — threat confirmed regardless of data volume.

Section 02

The KNOWN_BAD_DOMAINS List

The fix required a third configuration list alongside SUSPICIOUS_TLDS and SAFE_HOSTS. Where SUSPICIOUS_TLDS scores domains probabilistically based on TLD abuse patterns, KNOWN_BAD_DOMAINS matches specific confirmed IOCs — either from malware previously analyzed in these labs, or from infrastructure categories that are essentially never used by legitimate organizations.

IOCTypeReason
fakeurl.htmConfirmed IOCEasy As 123 C2 URI — analyzed in Lab Log 003. CMD=POLL beaconing payload.
set_agentConfirmed IOCLumma Stealer registration endpoint — analyzed in Lab Log 004. Browser fingerprint exfiltration.
whitepepper.suConfirmed IOCLumma Stealer primary C2 domain. .su = Soviet Union TLD, criminal infrastructure.
communicationfirewall-security.ccConfirmed IOCLumma Stealer secondary C2 domain. Deceptive name designed to appear legitimate.
holiday-forever.ccConfirmed IOCLumma Stealer payload delivery domain. .cc = Cocos Islands, high abuse TLD.
whooptm.cyouConfirmed IOCLumma Stealer C2 domain. .cyou = China You, extremely high abuse rate.
megafilehubConfirmed IOCLumma Stealer payload hosting. Partial match catches all subdomains.
duckdns.orgDynamic DNSFree dynamic DNS. No identity verification. Widely abused by RATs and C2 infrastructure.
no-ip.comDynamic DNSOne of the most abused dynamic DNS providers globally. Rarely seen in legitimate enterprise traffic.
ddns.netDynamic DNSFree subdomain service with no verification. Common in commodity malware C2.
hopto.orgDynamic DNSNo-IP subdomain provider. Frequently flagged in threat intelligence feeds.
duckdns.orgDynamic DNSPopular with malware authors for fast infrastructure rotation.
zapto.org / sytes.netDynamic DNSNo-IP subdomain family. Near-zero legitimate enterprise usage.

Why dynamic DNS providers belong on a bad list: Dynamic DNS lets anyone point a subdomain at any IP address and change it instantly — no registration, no identity verification. That flexibility is exactly what malware authors need: if a C2 IP gets blocked, they can redirect the domain to a new one within minutes. Legitimate enterprises with real infrastructure budgets and IT departments have no reason to use free dynamic DNS. Seeing any of these providers in enterprise network traffic is a strong indicator of malware or unauthorized software.

Section 03

The get_known_bad_hits() Function

A new function was added to the script between the username lookup and the HTTP analysis steps. It checks both http.log and ssl.log against every entry in KNOWN_BAD_DOMAINS, deduplicates matches across both logs, and returns a list of confirmed hits with the matched IOC, the full host or domain, the destination IP, and the source log file.

get_known_bad_hits() — New Function
def get_known_bad_hits(log_dir):
    """Check http.log and ssl.log for confirmed malicious IOCs."""
    hits = []
    seen = set()  # prevents same host appearing twice if in both logs

    # Check http.log — match against host AND URI path
    http_path = os.path.join(log_dir, "http.log")
    if os.path.exists(http_path):
        for row in parse_zeek_log(http_path):
            host     = row.get("host", "")
            uri      = row.get("uri", "")
            ip       = row.get("id.resp_h", "")
            combined = host + uri  # fakeurl.htm is in the URI, not the host
            for ioc in KNOWN_BAD_DOMAINS:
                if ioc in combined and host not in seen:
                    seen.add(host)
                    hits.append({
                        "indicator": ioc,
                        "host":      host,
                        "ip":        ip,
                        "source":    "http.log"
                    })

    # Check ssl.log — match against SNI server_name field
    ssl_path = os.path.join(log_dir, "ssl.log")
    if os.path.exists(ssl_path):
        for row in parse_zeek_log(ssl_path):
            server_name = row.get("server_name", "")
            ip          = row.get("id.resp_h", "")
            for ioc in KNOWN_BAD_DOMAINS:
                if ioc in server_name and server_name not in seen:
                    seen.add(server_name)
                    hits.append({
                        "indicator": ioc,
                        "host":      server_name,
                        "ip":        ip,
                        "source":    "ssl.log"
                    })

    return hits

Key Design Decision — Matching Combined Host + URI

The Easy As 123 C2 beacon posts to 45.131.214.85/fakeurl.htm. The IOC fakeurl.htm is in the URI path, not the host field. If the function only checked host, it would never match. By concatenating host + uri into a single string before checking, the function catches IOCs in either field with one comparison.

Severity Scoring Update

The severity logic was updated to treat any confirmed IOC match as CRITICAL — regardless of data volume:

# Known bad hits always escalate to CRITICAL
if known_bad or total_sent > 1_000_000:
    severity_display = red(bold("CRITICAL"))
elif total_sent > 100_000 or findings > 3:
    severity_display = orange(bold("HIGH"))
elif findings > 0:
    severity_display = orange("MEDIUM")
else:
    severity_display = "LOW"

The known_bad or condition means the script short-circuits to CRITICAL the moment any confirmed IOC is found — it does not wait to calculate data volumes first.

Section 04

Validated Output — Both Pcaps

Lumma Stealer — 5 Confirmed IOC Matches

python3 zeek_triage.py ~/Desktop/zeek-lumma
Analyzing Zeek logs in: /Users/yanai/Desktop/zeek-lumma
  [+] Host identity: done
  [+] Username: done
  [+] Known bad IOCs: done — 5 matches
  [+] HTTP analysis: done — 1 hits
  [+] TLS analysis: done — 5 hits

============================================================
  ZEEK TRIAGE REPORT
============================================================

[ INFECTED HOST IDENTITY ]
------------------------------------------------------------
  IP Address : 10.1.21.58
  MAC Address: 00:21:5d:c8:0e:f2
  Hostname   : DESKTOP-ES9F3ML
  Username   : gwyatt

[ CONFIRMED MALICIOUS IOCs ]5 matches
------------------------------------------------------------
  !! MATCH  whitepepper.su  153.92.1.49
             IOC: set_agent  [http.log]
  !! MATCH  media.megafilehub4.lat  104.21.48.156
             IOC: megafilehub  [ssl.log]
  !! MATCH  whooptm.cyou  62.72.32.156
             IOC: whooptm.cyou  [ssl.log]
  !! MATCH  holiday-forever.cc  80.97.160.24
             IOC: holiday-forever.cc  [ssl.log]
  !! MATCH  communicationfirewall-security.cc  104.21.9.36
             IOC: communicationfirewall-security.cc  [ssl.log]

[ SEVERITY SUMMARY ]
------------------------------------------------------------
  Confirmed malicious IOCs : 5
  Suspicious HTTP requests : 1
  Suspicious TLS domains   : 5
  Total data exfiltrated   : 2.27 MB
  Overall severity         : CRITICAL

Easy As 123 — Severity Corrected From MEDIUM to CRITICAL

python3 zeek_triage.py ~/Desktop/zeek-easy123
Analyzing Zeek logs in: /Users/yanai/Desktop/zeek-easy123
  [+] Host identity: done
  [+] Username: done
  [+] Known bad IOCs: done — 1 matches
  [+] HTTP analysis: done — 1 hits
  [+] TLS analysis: done — 0 hits

============================================================
  ZEEK TRIAGE REPORT
============================================================

[ INFECTED HOST IDENTITY ]
------------------------------------------------------------
  IP Address : -
  MAC Address: 00:e0:4c:68:08:00
  Hostname   : brads-MBP
  Username   : brolf

[ CONFIRMED MALICIOUS IOCs ]1 matches
------------------------------------------------------------
  !! MATCH  45.131.214.85  45.131.214.85
             IOC: fakeurl.htm  [http.log]

[ SEVERITY SUMMARY ]
------------------------------------------------------------
  Confirmed malicious IOCs : 1
  Suspicious HTTP requests : 1
  Suspicious TLS domains   : 0
  Total data exfiltrated   : 61.5 KB
  Overall severity         : CRITICAL

Why the Easy As 123 host field shows the IP twice: When malware connects directly to a raw IP address rather than a domain name, Zeek's http.log records the IP in both the host field and the id.resp_h field. There is no domain name to display because the malware never performed a DNS lookup — it hardcoded the C2 IP directly. This is itself a behavioral indicator: legitimate software almost always connects to domain names, not raw IPs.

Section 05

Script Evolution — Lab 006 vs Lab 007

CapabilityLab 006Lab 007
Detection layers TLD matching + safe host filtering IOC matching + TLD matching + safe host filtering
Severity basis Data volume only Confirmed IOC presence OR data volume
Easy As 123 score MEDIUM — incorrect CRITICAL — correct
Lumma Stealer score CRITICAL — correct CRITICAL — correct
Configuration lists 2 — SUSPICIOUS_TLDS, SAFE_HOSTS 3 — adds KNOWN_BAD_DOMAINS
Functions 5 analysis functions 6 analysis functions — adds get_known_bad_hits()
Report sections 4 sections 5 sections — adds CONFIRMED MALICIOUS IOCs
Section 06

NIST SP 800-171 Control Mapping

Applicable Controls
3.14.6
Monitor for malicious code. KNOWN_BAD_DOMAINS implements a signature-based detection layer equivalent in principle to antivirus IOC matching — any traffic to a confirmed malicious domain triggers an immediate CRITICAL alert, satisfying the requirement to monitor for and respond to malicious code at network connection points.
3.14.7
Identify unauthorized use. Direct C2 connections to raw IPs — as seen in Easy As 123 — are identified and flagged by IOC matching even when no domain name is present. The absence of DNS resolution is itself flagged as a behavioral indicator in the report output.
3.3.1
Audit logging. Each confirmed IOC match records the matched indicator, full host/domain, destination IP, and source log file — producing a structured audit record suitable for incident documentation and legal hold requirements.
Section 07

Lessons Learned

Severity Logic Must Reflect Threat Reality, Not Just Damage Metrics

The MEDIUM score for Easy As 123 was a design flaw, not an analysis error. The underlying data was correct — host identity, C2 IP, request count, data volumes were all accurately identified. The mistake was in what the scoring asked: how much data moved, rather than is a threat confirmed. Security tooling that scores low damage as low severity will systematically undertriage active C2 connections that haven't yet executed their payload — exactly the most important moment to catch them.

IOC Lists Are Only Valuable If They Grow

The current KNOWN_BAD_DOMAINS list contains IOCs from two malware families analyzed across four lab sessions. Every new pcap analyzed is an opportunity to add confirmed indicators. Dynamic DNS providers were added not from specific malware analysis but from threat intelligence knowledge — they represent a category of infrastructure that is empirically associated with malicious activity. Both types of entries belong on the list, and both should be documented with their source so future analysts understand why each entry is there.

Next Lab

Lab Log 008 will build the Mac application wrapper — a drag-and-drop GUI that runs zeek_triage.py without any terminal interaction. The goal is a self-contained tool that an analyst at any experience level can use on any Zeek log folder.