L13 · Case study: Target (2013)
Module 2: Networking for Defenders · ~20 min · Lesson 13 of 71
🎯 What you'll learn
- How a password stolen from a small HVAC contractor led to 40 million stolen payment cards
- Why a flat network let attackers get from a billing portal to the cash registers
- How the data left over plain FTP, and how egress controls would have stopped it
- Why Target's security tools raised the alarm and nobody acted
🔥 Why this matters
In late 2013, in the busiest shopping weeks of the year, attackers stole card data from Target's registers for about three weeks. They got in with a vendor's login. Target's malware detection tool spotted them and sent alerts, which were looked at and set aside. This lesson pulls Module 2 together: ports, segmentation, and egress, plus the part no firewall solves: acting on alerts.
🏢 The company and the setup
Target is one of the largest US retailers, with about 1,800 US stores in 2013. Every register is a point-of-sale (POS) terminal: a computer that reads your card and passes the data on for approval. For a moment, the card data sits unencrypted in the register's memory.
One of Target's many suppliers was Fazio Mechanical Services, a refrigeration and HVAC contractor in Pennsylvania. Fazio said its data connection to Target was only for electronic billing, contract submission, and project management. A vendor login meant for invoices.
A note on sources. The main public account is the US Senate Commerce Committee staff report A "Kill Chain" Analysis of the 2013 Target Data Breach (March 2014). It says openly that it is built from media reports and expert analyses (Brian Krebs, Dell SecureWorks, Bloomberg Businessweek), not Target's forensics, which were never published. "Reported" below means exactly that.
⏱ Attack timeline
The attack path
ATTACKER
│ 1. phishing email to Fazio Mechanical (HVAC vendor);
│ password-stealing malware grabs its Target login
▼
VENDOR-FACING SYSTEM at Target
│ 2. attacker logs in with the stolen vendor credential
│ (reports point to the Ariba billing portal);
│ password only, no second factor
▼
TARGET INTERNAL NETWORK (poorly segmented)
│ 3. moves from the vendor edge to internal servers
│ (exact method never made public)
▼
POS REGISTERS in US stores
│ 4. RAM-scraping malware (a BlackPOS variant) copies
│ card data from memory at every swipe
▼
INTERNAL STAGING SERVER (a compromised Target server)
│ 5. collects the stolen card data
│ ◀── FireEye alerts, Nov 30 and Dec 2: not acted on
▼
EGRESS: outbound FTP, in plain text, to outside servers
6. ~11 GB over about two weeks, to drop servers
including one in Russia, during business hours
Step 3 is the big unknown. Krebs and Dell SecureWorks suggested the attackers abused a default account name from a BMC Software IT management product; the Senate report calls this a theory. Everyone agrees on the result: a login meant for invoices reached registers across the US.
The dates
2013 ~Sep ── Fazio credentials stolen by phishing
Sep ── Target certified PCI DSS compliant
Nov 12 ── attackers first get into Target's network
Nov 15 ── malware tested on a few registers (to 28th)
Nov 27 ── card theft starts (Black Friday: Nov 29)
~Nov 28── Symantec antivirus flags suspicious activity
Nov 30 ── most registers infected; exfil malware in;
│ FireEye alerts (Bangalore -> Minneapolis)
Dec 2 ── malware updated, more alerts; FTP exfil
│ begins
Dec 12 ── US Justice Department notifies Target
Dec 15 ── most of the malware removed
Dec 18 ── Brian Krebs breaks the story
Dec 19 ── Target confirms ~40M cards
2014 Jan 10 ── plus contact data for up to 70M customers
Mar 5 ── CIO resigns; May 5: CEO steps down
2017 May 23 ── $18.5M settlement with 47 states + DC
The numbers vary by source. Target said about 40 million cards plus contact details for up to 70 million customers, with overlap. The 2017 state settlement says "more than 41 million" cards and "more than 60 million" customers; the Senate's "as many as 110 million" just adds 40 and 70.
The ignored alarms
Bloomberg Businessweek reported that Target had installed a FireEye malware detection system months
earlier, reportedly for $1.6 million. A team in Bangalore watched it and passed alerts to the
security operations center (SOC) in Minneapolis. On 30 November and again on 2 December it
flagged the exfiltration malware with a generic label, malware.binary. Per the Senate report, it
even decoded the outside drop servers' addresses. Its automatic malware deletion was switched off.
Target said the activity "was evaluated and acted upon" and the team "determined that it did not warrant immediate follow up." Target learned of the breach when the Justice Department called, ten days after the data started leaving.
🔍 What went wrong
| Failure | What happened | Earlier lesson |
|---|---|---|
| Card data stolen in bulk | Confidentiality loss; card data in plaintext in register memory | L01 CIA triad |
| Known threat, no action | Visa warned retailers about RAM-scraping malware twice in 2013 (Reuters) | L02 risk, threat intelligence |
| Vendors as attack surface | Public supplier pages reportedly helped attackers plan | L03 attack surface |
| Invoice login reached the registers | No least privilege for vendors; "inside" meant trusted | L04 least privilege, zero trust |
| No trust boundary around vendors | "Vendor system to internal network" wasn't defended as a boundary | L05 trust boundaries |
| Tools in place, nobody acted | Equifax's monitor was blind; Target's could see, but people didn't act | L06 |
| FTP allowed out | Internal servers could open port 21 connections to the internet | L07 ports and protocols |
| Plaintext exfil, still unseen | FTP isn't encrypted, so the content was inspectable, yet nothing looked | L10 what TLS hides |
| Flat network, no egress filter | Vendor systems, servers, and registers not isolated; any destination allowed out | L12 segmentation, egress |
It also follows L03's Kill Chain, which is why the Senate used it: delivery (phishing), installation (POS and exfil malware), command and control (weeks inside), and actions on objectives (FTP uploads). Target had chances at four links and broke none.
A Verizon review done for Target just after the breach (leaked to Krebs in 2015; Target didn't confirm it) found "no controls limiting their access to any system". Testers reached registers from a hacked deli meat scale in another store.
🛡 What would have stopped it
- Vendor MFA and a separate path. A second factor makes the stolen password useless. A vendor portal with no route inward stops step 3 even without it.
- Segment the cardholder data environment (CDE), every system that stores, processes, or transmits card data, so only named systems on named ports reach it. The 2017 settlement required exactly this, plus two-factor authentication for certain accounts.
- Egress allow-lists. Registers and payment servers talk only to the processor and internal services, so FTP to Russia is impossible, not just noticeable. Control DNS too, or attackers switch to DNS tunneling (L08).
- Act on high-confidence alerts on critical systems. Two tools flagged the same server. On a CDE host, that should page someone or quarantine it automatically.
- Application allow-listing on registers, so unknown programs can't run.
🧪 Worked example: spotting and stopping the FTP exfil
Say a pod in a payments namespace runs on a node in 10.20.3.0/24. VPC Flow Logs (default format)
record each connection. One made-up line:
2 111122223333 eni-0f00example 10.20.3.41 203.0.113.77
49822 21 6 51200 73400320 1700038800 1700038860 ACCEPT OK
│ │ │ │ │
│ │ │ │ └ allowed
│ │ │ └ ~70 MB in one minute
│ │ └ protocol 6 = TCP
│ └ destination port 21 = FTP control (L07)
└ source port
Three red flags: a payments host speaking FTP at all, to an IP that isn't the payment processor, with an unusually large upload. Alerting on it is good. Making it fail is better, with a Kubernetes NetworkPolicy that denies all egress except DNS and the processor:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: payments-egress-allowlist
namespace: payments
spec:
podSelector: {} # every pod in the namespace
policyTypes: ["Egress"] # anything not listed is denied
egress:
- to: # DNS, only via cluster DNS
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}
- to: # the processor, HTTPS only
- ipBlock: {cidr: 198.51.100.0/28}
ports:
- {protocol: TCP, port: 443}
Two caveats. It only works if your network plugin enforces it: on EKS, the VPC CNI's network policy
feature (v1.14+, off by default) or a plugin such as Calico. And it matches IPs, not hostnames; for
"only api.processor.example.com", use an egress proxy or AWS Network Firewall domain rules (L12).
🧰 Lessons for your own work
- Inventory third-party access. In a stack like the one in these examples, that means gateway consumers (Kong), IAM Identity Center users, GitHub outside collaborators, and Airflow connections to partner SFTP servers. Each needs an owner, MFA, a scope, and an end date.
- Partner APIs get their own lane. In a gateway such as Kong, one consumer per partner, with an ACL group that matches only partner routes and a rate limit. Consider mTLS (L11).
- Default-deny egress for namespaces that touch payment or other sensitive data, plus tight security group egress rules. If a pod doesn't need the internet, it shouldn't have it.
- Segment east-west traffic. The database (MongoDB in the examples) accepts connections only from the services that use it.
- Decide in advance which alerts on critical systems page a human or trigger containment.
🛠 Try it (optional, ~5 min)
See what your own laptop (macOS or Linux) is talking to, grouped by program and remote port (read-only):
lsof -nP -iTCP -sTCP:ESTABLISHED \
| grep -v -e '->127\.0\.0\.1' -e '->\[::1\]' \
| awk 'NR>1 {split($9,a,"->"); n=split(a[2],p,":");
print $1, "port", p[n]}' \
| sort | uniq -c | sort -rn
# 2 zoom.us port 443
# 2 OneDrive port 443
# 1 MSTeams port 443
# 1 MongoDB port 443
lsof cuts program names to 9 characters, and the grep hides local connections (on a work laptop,
a security agent may proxy traffic through localhost). Look for anything you can't explain or not
on 443. Then ask: if a pod in your most sensitive namespace connected to port 21 on a random IP,
what would stop it, and who would find out?
🧠 Key takeaways
- Your vendors' security is part of yours; a login for invoices became a path to the registers.
- Segmentation decides whether one stolen password becomes a breach.
- Egress allow-lists turn "data walked out over FTP" into "connection refused".
- Detection is wasted without response: the tools worked, the process didn't.
- Compliance isn't security: Target passed a PCI DSS assessment two months earlier.
📖 Words to know
| Term | Meaning |
|---|---|
| Point of sale (POS) | The register system that reads a card and sends the payment for approval |
| RAM scraping | Malware that reads card data from memory before it is encrypted |
| Flat network | A network where most systems can reach most others, with few internal barriers |
| Cardholder data environment (CDE) | Systems that store, process, or transmit card data (a PCI DSS term) |
| Egress filtering | Controlling which outbound connections may leave a network |
| SOC | Security operations center: the team that watches alerts and responds |
| PCI DSS | The card industry's security standard for merchants |
⏭ Next time
Module 2 is done. Module 3, Cryptography Essentials, starts with L14: hashing, the one-way function behind password storage and file integrity checks, and why MD5 and SHA-1 are retired.
Third-party access done right
The problem with vendor accounts
Vendor accounts are where good access hygiene goes to die. They are often shared by a whole company ("the Fazio login"), protected by a password only, never expire, and are owned by nobody inside your company. When the vendor's PC is infected, as Fazio's reportedly was with password-stealing malware (possibly Citadel, though never confirmed), you inherit the vendor's security. The Senate report cites a former Target vendor manager saying Target rarely required two-factor authentication from low-level contractors, and investigators said Fazio used the free consumer version of an anti-malware tool, with no real-time protection.
You can't fix your vendors' laptops. You can decide what a stolen vendor login is able to reach.
Before and after
BEFORE (Target, as reported)
vendor PC ──password──▶ vendor portal ══▶ internal network
(infected) (Ariba) │ servers, POS,
│ everything
no barrier┘
AFTER
vendor user ──SSO + MFA──▶ IdP (one identity per person)
│ short session, scoped role
▼
vendor app, own segment or SaaS
╳ no route to internal network
╳ no route to the CDE
│ every action logged, alert on
▼ use outside the expected app
access expires on the contract end date
Six rules
- One identity per person, federated through your identity provider or the vendor's. No shared accounts, so you can see who did what and remove one person.
- Phishing-resistant MFA (passkeys or FIDO2 keys, L21) for anything beyond a public page.
- A separate network path. Vendor apps live in their own segment, VPC, or a SaaS product, with no route inward. A login to the billing app can't even reach other systems.
- Least privilege, time-boxed. Grant access for a task and a period, then remove it. Just-in-time (JIT) access means the access exists only while it's needed, and is requested, approved, and logged each time.
- Watch for out-of-place use. The Senate report's suggestion: alert when a vendor credential shows up somewhere unrelated to its job.
- Offboard on a date, not on a memory. Every grant carries an end date tied to the contract.
Worked example: a contractor in AWS IAM Identity Center
Suppose an outside data contractor needs to read reconciliation files from one S3 bucket in one
account. In IAM Identity Center, a permission set is a reusable bundle of IAM permissions
and a session length that you assign to a user or group for specific accounts. Give the contractor
their own Identity Center user in a group like ext-contractor-recon, with MFA required, and a
narrow permission set (made-up names):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::example-recon-exports",
"arn:aws:s3:::example-recon-exports/outbound/*"
]
}]
}
Set the permission set's session duration to 1 hour (the default; the maximum is 12). Assign it only for the one account that holds the bucket, never for the account that runs the Kubernetes (EKS) clusters. For just-in-time access, create the assignment when a ticket is approved and delete it afterwards. AWS publishes an open-source solution for this called TEAM (Temporary Elevated Access Management). The commands, with made-up IDs:
aws sso-admin create-account-assignment \
--instance-arn arn:aws:sso:::instance/ssoins-EXAMPLE \
--permission-set-arn \
arn:aws:sso:::permissionSet/ssoins-EXAMPLE/ps-EXAMPLE \
--principal-type GROUP --principal-id g-EXAMPLE \
--target-type AWS_ACCOUNT --target-id 111122223333
# ...work happens, every API call logged in CloudTrail...
aws sso-admin delete-account-assignment # same arguments
Add a bucket policy that denies access from anything outside that role, so a mistake in one place doesn't open the bucket. Defense in depth again (L04).
The same idea at the API gateway
For partner APIs through a gateway such as Kong, the vendor's "login" is an API key or OAuth client. Give each partner a separate gateway consumer and credential, an ACL group that matches only the partner routes, a rate limit, and, where possible, mTLS or an IP allow-list. Rotate credentials on a schedule, and when a contract ends, delete the consumer that day.
🛠 Try it (~5 min, a thought exercise)
For one system you own, write down every party outside your team that can reach it: vendors,
partner APIs, contractors, other teams' service accounts. For each one, answer four questions:
one identity per person? MFA? what can it reach beyond its job? when does it expire? If your
organization uses IAM Identity Center and you have read access, aws sso-admin list-permission-sets --instance-arn <arn>
lists the permission sets that exist, a good starting point.
🧠 Extra takeaways
- Assume vendor credentials will be stolen; design so a stolen one reaches very little.
- One identity per person, phishing-resistant MFA, and an end date on every grant.
- Give vendors a separate network path with no route into internal systems.
- Just-in-time access shrinks the window in which a stolen credential works.
| Term | Meaning |
|---|---|
| Permission set | In IAM Identity Center, a reusable set of permissions and session length assigned to users or groups per account |
| Just-in-time (JIT) access | Access granted only when requested and approved, and removed after a set time |
| Federation | Letting users sign in with an identity from another trusted identity provider |
| Shared account | One login used by many people, which makes actions untraceable |
Alert fatigue, why the alarms were ignored and how to fix it
The tools worked
The uncomfortable part of the Target story is that detection succeeded. FireEye flagged the exfiltration malware on 30 November and again on 2 December; Symantec flagged the same server around 28 November. Analysts reviewed the alerts and decided they "did not warrant immediate follow up". That's alert fatigue: when people see so many alerts, mostly harmless, that the real ones blend in.
The public reporting offers likely reasons, all from interviews rather than official findings:
- Volume. Security experts told reporters a company Target's size likely saw hundreds of alerts a day.
- Vague labels.
malware.binarysays "something", not "card data leaving from a payment server". - A new tool. It had been installed only months earlier and was still being tuned, so it may not have been trusted yet.
- Auto-delete off. Many customers turn off automatic removal because it can block legitimate traffic and files.
- A handoff. The Bangalore team escalated, and Minneapolis decided. Each step is a chance for "someone else has it".
Why mostly-false alerts drown the true ones
Some made-up but realistic numbers:
alerts per day 500
share that are false positives 99%
real problems per day 5
analyst time per alert 10 min
time to review all of them ~83 hours/day
analysts on shift 3 (= 24 hours)
│
▼
most alerts get a glance, not an investigation;
the 5 real ones look exactly like the 495 others
The fix isn't to try harder. It's to send fewer, better alerts, and to rank them by what's at stake.
Turning raw alerts into a page
raw events from every tool
│ 1. DEDUPLICATE same alert, same host -> one
▼
│ 2. ENRICH add the asset: is it in the CDE?
│ who owns it? is it internet-facing?
▼
│ 3. CORRELATE two tools, same host, same day?
│ then raise the severity
▼
│ 4. ROUTE critical asset + high confidence
│ -> page on-call, auto-isolate host
▼
a few alerts a human must act on, each with a runbook
Applied to Target: one high-severity FireEye alert, on a server that touched card data, plus an antivirus alert on the same server. After steps 2 and 3 that's one obvious, urgent incident.
Worked example: correlate across tools
A made-up day of alerts from three tools, one JSON object per line:
cat > alerts.jsonl <<'EOF'
{"tool":"ids","sig":"port-scan","host":"pos-0412"}
{"tool":"ids","sig":"port-scan","host":"pos-0977"}
{"tool":"sandbox","sig":"malware.binary","host":"etl-03"}
{"tool":"ids","sig":"weak-tls","host":"api-gw-1"}
{"tool":"sandbox","sig":"malware.binary","host":"etl-03"}
{"tool":"av","sig":"suspicious-proc","host":"etl-03"}
{"tool":"ids","sig":"port-scan","host":"pos-1180"}
EOF
# hosts flagged by more than one tool
jq -s -c 'group_by(.host)
| map({host: .[0].host, n: length,
tools: (map(.tool) | unique)})
| map(select((.tools | length) > 1))' alerts.jsonl
# [{"host":"etl-03","n":3,"tools":["av","sandbox"]}]
Seven alerts, one host worth waking someone up for. Real SIEMs (Splunk, Elastic, Sentinel) do the same with correlation rules; on AWS, Security Hub gathers GuardDuty and other findings in one place so you can do this across accounts.
Habits that keep alerts meaningful
- Every alert has an owner and a runbook. If nobody would act on it, delete or downgrade it.
- Tune weekly. Track each rule's false positive rate. Suppress known-good patterns precisely (this job, this host, this hour), never by turning the whole rule off.
- Severity follows the asset. The same signature means more on a payment server than on a test box. Tag Kubernetes namespaces and AWS accounts by data sensitivity so the alert can say so.
- Automate containment for high-confidence, high-stakes cases, such as isolating a pod with a quarantine NetworkPolicy or revoking a session, and let a human undo it.
- Measure response, not just detection: time to acknowledge, time to contain, and the share of high-severity alerts that were actually investigated.
- Review the dismissed alerts. Pick a few each week and ask "were we right?" That is how you
find your
malware.binary.
🛠 Try it (~5 min, local)
Run the jq example above in a scratch folder. Then add a line for pos-0412 from the av tool
and run it again: a register now appears too. Finally, think about your own on-call alert channel.
What share of last week's alerts led to an action? If it's under half, which rule would you tune
first?
🧠 Extra takeaways
- Detection without response is just expensive logging.
- Fewer, enriched, correlated alerts beat more alerts.
- Rank alerts by the asset at stake: a payment server isn't a test box.
- Automate containment for clear-cut cases, and review the alerts you dismissed.
| Term | Meaning |
|---|---|
| Alert fatigue | Missing real alerts because people see too many, mostly harmless ones |
| False positive | An alert for something that turns out to be harmless |
| SIEM | Security information and event management: a system that collects, correlates, and alerts on security logs |
| Correlation rule | A rule that raises an alert when several related events occur together |
| Runbook | Written steps for handling a specific alert or incident |
The quiz for this lesson and the Tests section need JavaScript.