Skip to content

Commit 755f6a0

Browse files
mikethemandi
andauthored
blog: incident report - phishing (#18462)
Co-authored-by: Dustin Ingram <[email protected]>
1 parent 7824278 commit 755f6a0

File tree

3 files changed

+269
-1
lines changed

3 files changed

+269
-1
lines changed

docs/blog/posts/2025-07-28-pypi-phishing-attack.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ date: 2025-07-28
77
tags:
88
- security
99
- transparency
10+
links:
11+
- posts/2025-07-31-incident-report-phishing-attack.md
12+
1013
---
11-
(Ongoing, preliminary report)
14+
15+
Read the follow-up post: [Phishing Attack Follow-Up](2025-07-31-incident-report-phishing-attack.md)
16+
17+
---
18+
~~(Ongoing, preliminary report)~~
19+
1220

1321
PyPI has not been hacked, but users are being targeted by a phishing attack
1422
that attempts to trick them into logging in to a fake PyPI site.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
---
2+
title: "PyPI Phishing Attack: Incident Report"
3+
description: Follow-up on the recent phishing attack targeting PyPI users.
4+
authors:
5+
- miketheman
6+
date: 2025-07-31
7+
tags:
8+
- security
9+
- transparency
10+
links:
11+
- posts/2025-07-28-pypi-phishing-attack.md
12+
---
13+
14+
# Incident Report: Phishing Attack
15+
16+
Over the past few days, a phishing attack targeting PyPI users via email was uncovered.
17+
Our [initial report](2025-07-28-pypi-phishing-attack.md) was posted to raise awareness of the attack,
18+
and to provide some initial details on the attack vector.
19+
20+
Social media posts linking to the initial report have been shared widely,
21+
PyPI itself has not been breached with this attack.
22+
23+
## Summary
24+
25+
* 4 user accounts were successfully phished, now either disabled or credentials rotated
26+
* 2 API Tokens were generated by the attackers, which have since been revoked
27+
* 2 releases of the `num2words` project were uploaded by the attacker, which have since been removed
28+
* The phishing domain has been taken down
29+
30+
<!-- more -->
31+
32+
This appears to be similar in nature to a recent incident involving `npm` packages,
33+
where attackers successfully phished end-user credentials,
34+
and then compromised popular packages to distribute malware via npmjs.com.
35+
Read a [report on this attack here](https://www.bleepingcomputer.com/news/security/popular-npm-linter-packages-hijacked-via-phishing-to-drop-malware/).
36+
37+
To briefly recap the attack pattern,
38+
the attackers established a domain name, SSL certificate,
39+
and server running in a Virtual Private Server (VPS) in the cloud,
40+
which would transparently proxy requests to PyPI.org.
41+
This could be termed a "forward proxy", many CDN providers operate their services this way.
42+
We cannot know for certain, but it appears that the attackers sent emails
43+
to addresses found in package metadata, which is publicly available due to
44+
users putting them in their `setup.py` or `pyproject.toml` files.
45+
The emails contained links to the phishing domain,
46+
asking them to verify their email address.
47+
When users clicked the link, they were directed to the phishing domain,
48+
which presented the PyPI.org website,
49+
but with a different URL in the browser address bar.
50+
51+
Normal traffic flow:
52+
53+
```mermaid
54+
flowchart LR
55+
A[Client Browser] -->|Request| B[PyPI.org]
56+
B -->|Response| A
57+
```
58+
59+
Phishing attack flow:
60+
61+
```mermaid
62+
flowchart LR
63+
A[Client Browser] -->|Click Email Link| B[Phishing Domain]
64+
B -->|Proxy Request| C[PyPI.org]
65+
C -->|Response| B
66+
B -->|Display Content| A
67+
```
68+
69+
This may be termed ["adversary in the middle" (AiTM) attack](https://capec.mitre.org/data/definitions/94.html),
70+
however the difference here is that the attacker is not intercepting traffic between the user and PyPI.org,
71+
rather is acting as a forward proxy,
72+
which is a common practice for content delivery networks (CDNs) and the like.
73+
74+
Since a browser's address bar showed `hxxps://pypj.org/...`,
75+
and the website content of legitimate `pypi.org`,
76+
users might have missed the small difference between a lowercase `i` and `j`,
77+
and tricked into thinking they were on the official PyPI website,
78+
and submitted their credentials to the phishing domain.
79+
80+
The phisher could then capture the credentials,
81+
and use them to log in to the real PyPI.org website,
82+
potentially compromising the user's account and any packages they maintain.
83+
84+
Having a form of two-factor authentication (2FA) enabled on the account
85+
generally prevents phishing attacks like this from being successful,
86+
as the attacker would need access to the second factor (e.g. a TOTP code)
87+
to complete the login process.
88+
89+
However, since the attacker was in between the user and PyPI.org,
90+
they could have captured the second factor as well, and could have used the TOTP code
91+
within a short time interval, or potentially even captured the session cookies
92+
provided during the response to the login request,
93+
and thus bypassed the need for 2FA for a short time.
94+
95+
If the user had enrolled a [Security Device](https://pypi.org/help/#utfkey)
96+
for PyPI second factor authentication, the attacker would not have been able to use the second factor,
97+
as the WebAuthn protocol requires the user to physically interact with a hardware security key,
98+
or use a browser-based implementation, which would not be possible
99+
if the user was not on the legitimate PyPI.org website ([Relying Party Identifier](https://www.w3.org/TR/webauthn-2/#relying-party)).
100+
101+
Also, there are a significant amount of relatively inactive users who pre-date 2FA on PyPI,
102+
and may not have 2FA enabled on their accounts.
103+
If a user in this category fell for the phishing attack,
104+
they would still need to complete email address verification, and 2FA enrollment.
105+
This process generates an email from PyPI to the user,
106+
which links back to the legitimate PyPI.org address.
107+
These user accounts were not as vulnerable to this specific attack.
108+
109+
## Timeline of events
110+
Times are in both Eastern Daylight Time (EDT) and UTC.
111+
Some events may be omitted for brevity.
112+
113+
_Keep in mind: in the USA, the weekend is Saturday and Sunday._
114+
115+
### 2025-07-26 Saturday
116+
- 13:14 EDT (17:14 UTC): A user emails PyPI Security about a suspicious phishing email they received,
117+
which appears to be from PyPI.org, but with a different domain name.
118+
- 14:01 EDT (18:01 UTC): A volunteer PyPI Admin posts a message about this phishing attack in the PyPI Admins chat.
119+
- 18:37 EDT (22:37 UTC): A community member posts about their experience on [Python Forums](https://discuss.python.org/t/pypi-org-phishing-attack/100267)
120+
- 19:18 EDT (23:18 UTC): On-call PyPI Admin sees the message and escalates to another volunteer PyPI Admin for assistance,
121+
while submitting abuse complaints to the domain registrar NameSilo (report #1) and content delivery network (CDN) provider Cloudflare (report #2).
122+
- 19:38 EDT (23:38 UTC): A volunteer PyPI Admin responds, and begins investigation of the phishing attack.
123+
- 20:07 EDT (00:07 UTC): The volunteer PyPI Admin shares findings and some actions taken in chat, and has to leave for personal reasons.
124+
125+
### 2025-07-27 Sunday
126+
- 06:34 EDT (10:34 UTC): On-call PyPI Admin updates that the registrar has indicated they should submit a different form, which they do.
127+
They also confirm that the [PSF Trademarks Working Group](https://www.python.org/psf/trademarks/) are also working on this from a trademark perspective to notify the CDN and registrar of the abuse.
128+
129+
### 2025-07-28 Monday
130+
- 08:57 EDT (12:57 UTC): PyPI Admin staff sees the comments in chat, begins investigation follow up
131+
- 09:20 EDT (13:20 UTC): Available PyPI Admins (volunteer and staff) & other PSF parties meet to determine next steps.
132+
- 10:22 EDT (14:22 UTC): PyPI Admins post an [initial report](2025-07-28-pypi-phishing-attack.md) to the PyPI blog, and share it on social media.
133+
- 10:44 EDT (14:44 UTC): PyPI Admins post a notice to [`pypi-announce` mailing list](https://mail.python.org/mailman3/lists/pypi-announce.python.org/),
134+
and a similar is posted to the more general [`security-announce` mailing list](https://mail.python.org/mailman3/lists/security-announce.python.org/).
135+
- 11:37 EDT (15:37 UTC): A [new feature is added to PyPI](https://github.com/pypi/warehouse/pull/18427) to detect and warn users about phishing domains,
136+
which is deployed to production.
137+
- 11:59 EDT (15:59 UTC): A threat hunter posts on Twitter about a finding in `num2words` 0.5.15
138+
- 12:01 EDT (16:01 UTC): Another PyPI Admin submits another abuse report to Cloudflare (report #3), with more context and details - requests, headers, IP address, and more.
139+
- 12:05 EDT (16:05 UTC): Cloudflare response to initial abuse complaint from Saturday (report #2) as "invalid".
140+
- 12:18 EDT (16:18 UTC): PyPI Admins observe that Cloudflare placed a "Suspected Phishing" warning when visiting the phishing domain,
141+
reducing the probability for users to fall for the attack, despite declining the abuse reports.
142+
- 12:20 EDT (16:20 UTC): NameSilo places the phishing domain under administrative `ClientHold` status.
143+
- 12:23 EDT (16:23 UTC): Cloudflare responds to second abuse report (report #3) as "invalid".
144+
- 12:38 EDT (16:38 UTC): `num2words` project owner responds on Twitter confirming removal of 0.5.15
145+
- 12:54 EDT (16:54 UTC): `num2words` project owner responds on Twitter confirming removal of 0.5.16 as well as suspicious API Token
146+
147+
After the domain registrar NameSilo placed the domain name under `clientHold` status,
148+
new DNS resolutions failed, as there are no name servers.
149+
Host records for a client to resolve the domain name to an IP address will then fail.
150+
As DNS caches expired, users no longer were able to resolve the domain name,
151+
and thus unable to access the phishing site.
152+
153+
Incident is considered resolved, PyPI Admins continue to monitor the situation,
154+
and analysis of the attack continues.
155+
156+
## Impact Analysis
157+
158+
The phishing attack was targeted at PyPI users,
159+
and since we were able to obtain at least one confirmed IP address of the hosting server,
160+
we have found 4 such successful phishing attacks, and have taken action on them.
161+
162+
A single account appears to have further activity beyond credentials being compromised,
163+
and the attacker uploaded releases to PyPI.org via a newly-provisioned API Token.
164+
`num2words` versions 0.5.15 and 0.5.16 were uploaded to PyPI.org,
165+
which included malware, and removed within hours of being uploaded by the owner of the account.
166+
167+
The impacted user [posted about the activity on Twitter](https://nitter.tiekoetter.com/SFLinux/status/1949906299308953827):
168+
169+
> Thanks for the heads up! There was a phishing attack on pypi this morning. The compromised version of num2words v 0.5.15 have been removed.
170+
171+
and later:
172+
173+
> I found a weird token in our pypi account, probably the attacker had created it. I removed the token and deleted the malicious version again (0.5.16). I also created new backup TOTP codes for MFA. Will keep an eye on it but hopefully they won't be able to reupload the malware.
174+
175+
An advisory has been published [PYSEC-2025-72](https://osv.dev/vulnerability/PYSEC-2025-72)
176+
to help end user and tooling to detect the malicious versions.
177+
178+
## Takeaways
179+
180+
### Anti-impersonation Email Prevention
181+
182+
PyPI has settings for [Sender Policy Framework (SPF)](https://postmarkapp.com/glossary/sender-policy-framework),
183+
[DomainKeys Identified Mail (DKIM)](https://dkim.org/),
184+
and [Domain-based Message Authentication, Reporting & Conformance (DMARC)](https://dmarc.org/)
185+
to help prevent phishing attacks by verifying the authenticity of emails sent from PyPI.org.
186+
187+
However, each mail server that receives an email from PyPI.org is responsible for checking these values,
188+
and taking action based on the results of those checks.
189+
If the receiving mail server does not check these headers, or does not take action based on the
190+
results, then the email may still be delivered to the user's inbox.
191+
In this case, it appears that the phishing email was delivered to some users' inboxes,
192+
despite the presence of these headers.
193+
194+
In other cases, the receiving mail server may have checked the headers,
195+
and presented the user with a "Looks like Spam" warning,
196+
leaving it up to the user to decide whether to trust the email or not.
197+
This is a common practice, as many email providers do not block emails outright,
198+
but instead mark them as spam or suspicious, allowing users to make the final decision.
199+
200+
Ultimately, it is up to the user to be vigilant and cautious when clicking links in emails,
201+
especially when the email is unexpected or from an unknown sender.
202+
203+
### Abuse Reporting & Responses
204+
205+
We were surprised that our reports to Cloudflare were rejected due to them being unable to confirm phishing,
206+
which lead to a longer-than-expected period of the phishing domain being online.
207+
We don't have additional visibility into why these reports were initially considered invalid
208+
by Cloudflare or why they did eventually result in a takedown,
209+
but we will review and determine if future reports could be made more actionable.
210+
211+
### Alternate spelling domain names
212+
213+
The PSF is exploring the transfer of domain names used in this attack
214+
and other similar domains to have them registered to the PSF,
215+
and prevent future abuse.
216+
This effort has significant costs associated with it, so the outcome is not guaranteed.
217+
218+
If successful, this would allow the PSF to take control of the domain names,
219+
and possibly redirect them to either a warning page,
220+
or to the official PyPI.org website,
221+
preventing future phishing attacks using these domain names.
222+
223+
## Call to action
224+
225+
Some things you can do to help prevent this kind of attack in the future:
226+
227+
* If you have a dormant PyPI account, consider removing it if you do not need it.
228+
This will help reduce the number of potential targets for attackers.
229+
* If you have an older PyPI account, and have not logged in since 2FA has been required,
230+
consider logging in and enabling 2FA on your account.
231+
This will help protect your account from future phishing attacks.
232+
* Use WebAuthn via browser or hardware security keys for 2FA on your PyPI account.
233+
This will help protect your account from phishing attacks,
234+
as the attacker would need access to the second factor to complete the login process.
235+
236+
Security is a never-ending process, and we are always looking for ways to improve our security posture.
237+
If you have specific ideas or suggestions for improving PyPI security,
238+
and are willing to help implement them, consider checking our [Issue Tracker](https://github.com/pypi/warehouse/issues)
239+
or if you have something more sensitive, you can email [[email protected]](mailto:[email protected]).
240+
241+
This work would not be possible without generous donations,
242+
please consider [supporting the PSF](https://www.python.org/psf/)
243+
to ensure this kind of work can continue to serve the worldwide Python community.
244+
245+
## Indicators of Compromise
246+
247+
Analysis exposed some indicators of compromise (IoCs) that may be useful for detecting this attack,
248+
and similar attacks in the future.
249+
250+
- Domain name: `pypj.org`
251+
- IP address: `45.9.148.108` - the phishing email sending server
252+
- IP address: `45.9.148.85` - the phishing domain hosting server
253+
- Domain name: `modirosa.com` - used by attackers to establish accounts on PyPI.org
254+
- Domain name: `necub.com` - used by attackers to establish accounts on PyPI.org
255+
- PyPI Package: `num2words` - versions 0.5.15 and 0.5.16
256+
257+
### Related CAPECs
258+
- [CAPEC-94](https://capec.mitre.org/data/definitions/94.html) - Adversary in the Middle (AiTM) Attack
259+
- [CAPEC-98](https://capec.mitre.org/data/definitions/98.html) - Phishing

docs/mkdocs-blog.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ markdown_extensions:
3232
- name: mermaid
3333
class: mermaid
3434
format: !!python/name:pymdownx.superfences.fence_code_format
35+
- pymdownx.tilde
3536
- abbr
3637
- attr_list
3738
- md_in_html

0 commit comments

Comments
 (0)