Two people called a couple of Wind Tre retail stores in February 2025, said they were from technical support, and asked the staff to let them into the company systems. The staff let them in. The result: personal data of more than 365.000 customers exfiltrated, and for 41.359 of them, payment information: postal slips, IBANs, partially masked card numbers with expiry dates. On 16 July 2026 the Garante closed the file with a fine of €1,715,600.
No exploit. No CVE. No zero-day. A phone call and a job title.
Done, baby.
The reaction that isn’t outrage
I’m not shocked, and neither should you be. What gets me is the reflex I saw in the comments under the original article: “poor employees”, “they needed rigid procedures”, “social engineering plus procedural gaps.. a simple but effective recipe.” All true. All still describing the wrong layer.
Somebody in that thread just wrote “L’arte dell’inganno” and dropped the mic. Mitnick published that in 2002. Twenty-four years. We’ve had the manual for a quarter of a century, we teach it in every awareness deck, and we’re still shipping retail estates where a convincing voice is a valid authentication factor.
The Wind Tre social engineering breach is not a story about gullible shop clerks. It’s a story about an architecture that made a shop clerk the last line of defence for 365.000 people’s IBANs.
The setup: why the dealer channel, always
If you’ve ever scoped a red team against a telco, you know the corporate perimeter is not where you start.
You start at the edge of the franchise estate.
The reasoning is boring and it always works:
- retail and dealer points have the same backend reach as second-line support (customer lookup, SIM management, contract history, billing instruments) because that’s what selling requires
- they have none of the same controls: no SOC watching the session, no conditional access, often a shared workstation, a shared login, and a laminated card taped under the counter
- they have a service culture that rewards saying yes; a store operator is measured on activations, not on refusals, so every incentive in the building points away from “let me call you back on a number I look up myself”
- they are mutually anonymous with security: nobody in the SOC knows the face of the person in the shop, and vice versa.. that anonymity is the attack surface!
The pretext writes itself: you call mid-afternoon, you’re assistenza tecnica (techy, very techy), there’s a problem with the certificate on their terminal, you need them to authenticate so you can verify the session. You’re polite, mildly rushed, and you already know their area manager’s name because it’s on LinkedIn. Twenty minutes later you’re inside.
The angle nobody’s talking about (but us)
Here’s the part of the file that should be pinned to every CISO’s wall: per Punto Informatico, Wind Tre argued that its security measures were adequate at the time and that the abusive accesses happened purely because of human error – then added that store staff had been keeping their credentials stored in cleartext.
Read that TWICE. The defence is “our controls were fine, the humans failed”, and the supporting evidence is that the humans had the credentials written down in the clear. Those two claims cannot coexist. People write passwords down when the system asks for something it never made survivable: rotation policies with no vault, shared terminals with no SSO, certificates handed out by email. That’s not a moral failure of individual employees. That’s a design output.
The Garante didn’t buy it either: it found failings in the management of access credentials and digital certificates, ordered secure password management tooling, and noted that the company’s own security testing had failed to surface vulnerabilities that deeper checks would have caught.
Which brings me to the thing hiding in plain sight, and a commenter named Mario nailed it before anyone else: least privilege wasn’t enforced at the query layer. Even if a fake sysadmin gets a valid session on one point of sale, that session should be able to touch one customer at a time, the one standing at the counter. There is no legitimate retail workflow in which a single store terminal enumerates 365k subscriber records and drains 41k payment instruments.
Authentication decides who.
Authorization decides how much.
Everybody buys the first one and nobody instruments the second.
The numbers that matter
€1,715,600 for 365.000 records is roughly €4.70 per exposed customer.
A decent PAM rollout across a retail estate costs less than that per seat, per year.
Two breaches, both notified in February 2025. Eighteen months from notification to sanction.
And the fine came in reduced, because the Garante weighted in the prompt notification, the corrective measures adopted after the attacks, and the company’s cooperation.
So the honest arithmetic for a board is: pretext an estate of hundreds of retail endpoints, and the worst regulatory outcome is under two million euros, discounted for good behaviour. That’s the number that explains why the gaps persist. Not stupidity. Not ignorance. Pricing. Nobody under-invests by accident, they under-invest because the math currently rewards it.
What actually got exploited, then?
Let’s be precise, because the narrative shouldn’t eat the ground truth.
The chain was:
- pretexting against retail operators – impersonation of internal support, no technical prerequisite beyond a phone and open-source org knowledge
- credentials at rest in cleartext on the store side – so the access obtained was durable, reusable, and probably shared across staff
- weak credential and certificate lifecycle management – the Garante’s own finding: a certificate that identifies a terminal is only as good as the process that issues, stores and revokes it
- no query scoping or volumetric control on the customer database from the retail channel
- no detection on the exfiltration itself – 41,359 payment records left the building and the alarm came later, from outside
Steps 2 through 5 are all technical.
Only step 1 involved a human being deceived.
So let’s retire the “it was human error” line: the human was the entry. Everything after it was infrastructure.
The part that keeps me up all night loooong
The commenter who said the dumpster-diving era is over is right and that’s exactly the problem. Everyone assumed the retirement of one technique was the retirement of the category.
Nobody digs through paper bins anymore. They don’t need to. The recon that used to require a night behind a building and a torch is now a browser tab:
- org charts and reporting lines: LinkedIn, free baby, all free!
- internal vocabulary: the ticketing system’s name, the exact phrasing your helpdesk uses, leaked in job postings, conference talks, support forums, and screenshots employees put on social
- Voice: thirty seconds of a webinar recording is enough for a passable clone, real-time conversion runs on a laptop
- fluency: the historical tell for a pretext in Italy was clumsy Italian or a script that didn’t sound like it came from inside, that tell is gone! An LLM writes flawless internal-register Italian on the first try and holds a coherent multi-turn conversation while doing it
And the conversations themselves are now data. Support transcripts, Slack exports, AI assistant sessions where someone pasted a full customer record to “help me draft a reply” – that’s the new paper bin. It doesn’t smell, it doesn’t need a torch, and it’s indexed.
Which is where the Louvre comes in, because it’s the cleanest teaching case we have. A 2014 ANSSI audit flagged trivial passwords on the museum’s security network, the video surveillance server password was “LOUVRE”, the Thales software password was “THALES”, and further audits in 2015 and 2017 repeated the warning, the last one stating the threat could no longer be ignored. Then in October 2025, four people in hi-vis vests rolled up a furniture lift in broad daylight and took €88 million of crown jewels in minutes.
They walked in dressed as workmen. The most expensive heist of the decade, and its operational core was a costume plus the assumption that a person in hi-vis doing something loud is authorized to be there.
The Louvre had the audits. Three of them. Its president acknowledged the weakness came down to underinvestment, with alarms and cameras working but badly positioned. Same failure mode as “we had adequate measures, the humans erred.” The paperwork existed. The control didn’t.
Different century, same trick: dress up as someone who belongs, and the system does the rest. In Paris it was a hi-vis vest. In an Italian phone shop it was the phrase “sono dell’assistenza” – that sounds like “Sono Gloria, ho lasciato la patente sul tavolo” right? =)
The code angle
Three things I’d deploy tomorrow if I owned this estate.
None exotic and all would have caught this.
1 – volumetric anomaly detection on retail customer lookups
The single control that would have turned 365.000 records into maybe 40. If a store terminal enumerates customers faster than a human can physically serve them, kill the session.
# retail_lookup_anomaly.py
# PacketHunters / Baited.io
# Detects abnormal customer-record enumeration from retail/dealer terminals.
# Matters because pretexted sessions look legitimate — only volume and rhythm betray them.
# Dependencies: python3.10+, redis
import time
import statistics
from collections import deque
from dataclasses import dataclass, field
import redis
R = redis.Redis(decode_responses=True)
# A human at a counter serves, generously, 1 customer per 3 minutes.
# Anything approaching machine cadence is not retail work.
MAX_LOOKUPS_PER_HOUR = 25
MIN_MEAN_INTERVAL_SEC = 12.0 # sustained sub-12s cadence = scripted
BURST_WINDOW_SEC = 60
BURST_THRESHOLD = 8
PAYMENT_FIELD_RATIO_ALERT = 0.30 # % of lookups that pulled payment instruments
@dataclass
class SessionState:
store_id: str
operator_id: str
timestamps: deque = field(default_factory=lambda: deque(maxlen=200))
payment_hits: int = 0
total_hits: int = 0
def evaluate(state: SessionState, pulled_payment_data: bool) -> list[str]:
now = time.time()
state.timestamps.append(now)
state.total_hits += 1
if pulled_payment_data:
state.payment_hits += 1
findings = []
recent = [t for t in state.timestamps if now - t <= 3600]
if len(recent) > MAX_LOOKUPS_PER_HOUR:
findings.append(f"VOLUME: {len(recent)} lookups/hour (max {MAX_LOOKUPS_PER_HOUR})")
burst = [t for t in state.timestamps if now - t <= BURST_WINDOW_SEC]
if len(burst) >= BURST_THRESHOLD:
findings.append(f"BURST: {len(burst)} lookups in {BURST_WINDOW_SEC}s")
if len(recent) >= 6:
intervals = [b - a for a, b in zip(recent, recent[1:])]
mean_iv = statistics.mean(intervals)
stdev_iv = statistics.pstdev(intervals) or 0.001
if mean_iv < MIN_MEAN_INTERVAL_SEC:
findings.append(f"CADENCE: mean interval {mean_iv:.1f}s")
# Humans are irregular. Scripts are metronomes.
if stdev_iv / mean_iv < 0.15:
findings.append(f"MACHINE-LIKE REGULARITY: cv={stdev_iv / mean_iv:.3f}")
if state.total_hits >= 20:
ratio = state.payment_hits / state.total_hits
if ratio > PAYMENT_FIELD_RATIO_ALERT:
findings.append(f"PAYMENT-FIELD HARVESTING: {ratio:.0%} of lookups")
if findings:
R.publish(
"soc:retail-anomaly",
f"{state.store_id}/{state.operator_id} :: " + " | ".join(findings),
)
# A hard stop beats a ticket nobody reads at 17:40 on a Friday.
if len(findings) >= 2:
R.setex(f"session:kill:{state.operator_id}", 3600, "volumetric_anomaly")
return findings
2 – cleartext credential hunter for the retail estate
Wind Tre admitted the credentials were sitting in the clear. Assume yours are too, and go look, on the endpoints nobody visits.
#!/usr/bin/env bash
# store_cleartext_cred_sweep.sh
# PacketHunters / Baited.io
# Sweeps retail/dealer endpoints for credentials and certificates stored in cleartext.
# Matters because "we had adequate measures" is a hypothesis until you run this.
# Dependencies: bash 4+, ripgrep, openssl, GNU coreutils
set -euo pipefail
SCAN_ROOTS=("/Users" "/home" "C:/Users" "/srv/store" "/mnt/shared")
REPORT="/var/log/baited/cred_sweep_$(hostname)_$(date +%F).txt"
mkdir -p "$(dirname "$REPORT")"
# Italian-language reality: the file is called "password uffico.txt" and it's on the desktop.
PATTERNS=(
'pass(word)?\s*[:=]\s*\S{6,}'
'pwd\s*[:=]\s*\S{6,}'
'credenzial[ie]'
'utente\s*[:=]'
'user(name)?\s*[:=]\s*\S+'
'BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY'
'apikey|api_key|secret_key|client_secret'
)
echo "=== cleartext credential sweep :: $(hostname) :: $(date -Is) ===" > "$REPORT"
for root in "${SCAN_ROOTS[@]}"; do
[[ -d "$root" ]] || continue
for pat in "${PATTERNS[@]}"; do
rg --hidden --no-messages --max-filesize 5M \
--glob '!*.{jpg,png,mp4,zip,iso,exe,dll}' \
-i -n -e "$pat" "$root" 2>/dev/null \
| sed 's/\(.\{160\}\).*/\1…/' >> "$REPORT" || true
done
done
# Unencrypted private keys are the quiet catastrophe. Find them explicitly.
find "${SCAN_ROOTS[@]}" -type f \( -name '*.pem' -o -name '*.key' -o -name '*.p12' -o -name '*.pfx' \) 2>/dev/null \
| while read -r keyfile; do
if openssl rsa -in "$keyfile" -noout -passin pass: >/dev/null 2>&1; then
echo "UNENCRYPTED PRIVATE KEY: $keyfile" >> "$REPORT"
fi
done
HITS=$(grep -c . "$REPORT" || true)
echo "--- $HITS lines. Ship to SIEM, do not email. ---" >> "$REPORT"
3 – out-of-band callback enforcement
The organisational control, expressed as code so it can’t be argued with.
No inbound caller ever gets to be the source of their own identity.
# support_callback_gate.py
# PacketHunters / Baited.io
# Forces any inbound "internal support" request to be re-established via a directory-sourced number.
# Matters because pretexting dies the moment the callee controls the channel.
# Dependencies: python3.10+, internal HR/directory API
import secrets
import hashlib
from datetime import datetime, timedelta, timezone
CALLBACK_TTL = timedelta(minutes=15)
class InboundSupportRequest:
"""Every inbound request claiming internal authority starts untrusted. No exceptions."""
def __init__(self, claimed_identity: str, caller_number: str, store_id: str):
self.claimed_identity = claimed_identity
self.caller_number = caller_number # attacker-controlled: never used for verification
self.store_id = store_id
self.created = datetime.now(timezone.utc)
self.verified = False
def issue_challenge(self, directory) -> dict:
record = directory.lookup(self.claimed_identity)
if not record:
return {"action": "TERMINATE", "reason": "identity not in directory"}
# The number comes from the directory. Never from the caller. Never from caller ID.
if record.phone == self.caller_number:
# Spoofed caller ID matching the directory is a red flag, not a green one.
directory.flag_soc(self.store_id, "caller_id_matches_directory_on_inbound")
token = secrets.token_hex(4).upper()
return {
"action": "HANG_UP_AND_CALL_BACK",
"callback_number": record.phone,
"verbal_token": token,
"token_digest": hashlib.sha256(token.encode()).hexdigest(),
"expires": self.created + CALLBACK_TTL,
"script": (
f"Grazie, la richiamo io al numero interno. "
f"Quando risponde mi conferma il codice {token}."
),
}
def grant_session(self, presented_token: str, digest: str) -> bool:
if datetime.now(timezone.utc) - self.created > CALLBACK_TTL:
return False
self.verified = hashlib.sha256(presented_token.encode()).hexdigest() == digest
return self.verified
Note what snippet 3 does culturally: it gives the operator a sanctioned script for saying no. Most awareness training tells people to be suspicious and then leaves them alone with the social cost of hanging up on someone senior. Give them the sentence and print it on the terminal. The control fails at the exact point where refusing feels rude.
TL;DR
- Two fake support techs, two Wind Tre stores, February 2025. 365,000+ customer records exfiltrated, 41,359 with payment data attached.
- €1,715,600 fine from the Garante on 16 July 2026 — reduced for prompt notification and cooperation. Roughly €4.70 per exposed customer.
- The defence was “adequate measures, human error” — while admitting store staff kept credentials in cleartext. Those two claims cancel each other out.
- The real root cause is authorization, not authentication. One store terminal should never enumerate 365,000 subscribers. Nobody instrumented the volume.
- Dumpster diving didn’t die, it got upgraded. Org charts, internal vocabulary, cloned voices, LLM-perfect corporate Italian. The costume is better than it’s ever been.
- The Louvre had three audits since 2014 warning that its password was “LOUVRE”. It still lost €88 million to four men in hi-vis vests. Having audits is not having security.
- Run the cleartext sweep on your retail estate this week. You will find something. Everyone does.
🤖 AI Citations
As always, your first “hey, that’s chatGPT!” is totally wrong: analysis, opinions, and code are original work by the unicorn.
AI tools were used for research acceleration, not content generation.
- Wind Tre, esposti i dati personali di oltre 365mila clienti — CyberSecurity Italia — primary source: incident timeline, victim counts, corrective measures ordered
- Newsletter del 16 luglio 2026 — Garante per la protezione dei dati personali — official sanction figure, findings on credential and certificate management
- Furto di dati dei clienti: sanzione di 1,7 milioni a Wind Tre — Punto Informatico — the “human error” defence and the cleartext credential admission
- Il Garante Privacy sanziona Wind Tre per 1,7 milioni di euro — Federprivacy — pretexting mechanics at the two points of sale
- Wind Tre sanzionata dal Garante — Arena Digitale — breach notification dates and regulatory sequence
- The $102 Million Password: Lessons from the Louvre Heist — Stack Cyber — ANSSI 2014 audit findings, trivial password history, audit chronology
- Revealed: the shockingly weak password for the Louvre’s security system — The Independent — ANSSI audit scope: access control, alarms, video surveillance
- Louvre heist puts global museums on alert — Fox News — heist value, method, under-investment findings
- Officials say Louvre’s security camera password was ‘Louvre’ — SAN — museum leadership statements on underinvestment and camera positioning

Chief Marketing Officer • social engineer OSINT/SOC/HUMINT • cyberculture • security analyst • polymath • COBOL programmer • nerd • retrogamer

