我原本可以访问 17 TB 的微软记录。
I could've accessed 17T Microsoft records

原始链接: https://blog.faav.net/how-i-couldve-accessed-17-trillion-microsoft-records

在 2026 年 9 月 25 日发布的一份披露中,研究人员 Faav 报告了微软内部 Titan 分析服务的一个严重漏洞。Titan 会接受带有微软兼容的租户、受众、应用程序和用户声明的 JWT,却不验证其加密签名。Faav 将未签名令牌的 `upn` 声明设置为 `admin`,从而获得了该服务的本地管理员身份以及未授权的 SQL 查询权限。 该漏洞暴露了 17 个分析数据库中的 30 个活跃路由目标,这些数据库估计包含 17.3 万亿条存储记录和 9,863 个不同的表。可访问的元数据包括约 25,000 个应用账户和 17,990 条员工电子邮件记录。Faav 还通过两次受限的单行采样确认了能够访问 Bing 分析服务,但他没有导出数据集、识别用户、关联记录、访问个人身份信息(PII)或证明实际利用了该漏洞。 Faav 于 9 月 5 日向微软报告了此问题;微软在 9 月 9 日前禁用了相关端点,并授予了 5,000 美元的奖金。该文章还指出,微软在发布前编辑了披露内容,以削弱或重新表述部分有关影响范围的声明。

一名16岁的安全研究员披露了一个漏洞,可能导致约17 TB的微软记录暴露,其中包括住客登记和身份信息。微软向其支付了5,000美元,据称还要求对文章拥有编辑控制权,删减了部分内容,并淡化了漏洞的影响。 Hacker News 的评论者普遍称赞这位研究者的技术能力、成熟度和清晰的写作 Many 人批评微软的安全实践、组织 priorities,尤其是漏洞赏金过低,认为这种规模的漏洞应当获得接近10万美元或更高的报酬。另一些人指出,未成年研究者可能会非常看重声誉、指导、工作机会以及未来参与漏洞赏金项目的机会,因此5,000美元也可能是一笔值得接受的报酬。 这个漏洞涉及错误地接受使用 `alg: none` 设置的无签名 JWT。评论者们讨论了责任究竟在于微软的实现、JWT 容易出错的设计,还是两者兼有,并建议采用更严格的内部安全标准,例如微软的 MISE 框架。 讨论还涵盖了负责任的披露、担心遭到法律报复、漏洞赏金市场萎缩、AI 辅助写作,以及在黑市出售漏洞的伦理问题。总体而言,舆论强烈支持这位研究员,并对微软提出了尖锐批评。
相关文章

原文

An estimated 17.3 trillion stored rows across a wide range of Microsoft datasets were reachable through a single internal analytics service, all because it never checked the signature on a login token. That flaw let me claim an administrator’s identity and submit unauthorized SQL queries without any real credentials. I used only table descriptions, metadata, and bounded sample rows to understand the potential scope.

Two quick notes first. The impact I describe is hypothetical. It’s what an attacker could have done with this access, but luckily I found the bug instead, reported it, and never touched any customer data or PII. And for transparency: Microsoft had editorial control over this post, cutting sections and figures and reshaping how the impact is described before publication.

Microsoft said the following about this finding:

“We appreciate the opportunity to investigate the findings reported by Faav. Their submission and coordinated vulnerability disclosure helped us to better protect our customers by hardening our services. We value and appreciate safe security research under the terms of the Microsoft Bug Bounty Program and look forward to continuing to work with Faav in the future.”

Hey! I’m Faav. A little over a year ago, when I was 15, I published Break into any Microsoft building: Leaking PII in Microsoft Guest Check-In, my first Microsoft write-up. I’m 16 now, and this one is a little bigger.

Since then I’ve gone all-in on bug bounty. I’ve spent the year hacking Microsoft off and on around school, and finding bugs across Amazon, Google, Adobe, and a bunch of other companies. I also started building AI into how I hunt, which led me to develop Antares, my personal AI hackbot.

This one started as an automated lead that Antares couldn’t finish. Ten days later, after a Friday of schoolwork and one late-night hunch, it turned into the biggest Microsoft bug I’d ever found.

Finding the Titan API

On August 25, 2026, Antares identified an internal Microsoft service called Titan. Its web interface sat behind a VPN REQUIRED page for Microsoft employees, so the frontend was out of reach. But since when has a locked front door stopped anyone?

image

The “VPN REQUIRED” page shown to a non-employee visiting Titan’s frontend.

The API wasn’t linked anywhere on the frontend, so Antares searched Microsoft subdomains and found a separate endpoint that resolved to an Azure Cloud Services host. Its public Swagger file listed four routes:

/GetConfiguration
/GetOnboardedTables
/v2/Query
/v2/Insert

The Swagger doc specified Azure AD bearer authentication for three of the four routes. The exception was /v2/Query, which also happened to be the one that accepted raw SQL. So naturally, that’s where I started poking.

The query needed a tableName, and Swagger gave no example values. I pulled 2023 snapshots of Titan’s login and privacy pages from the Wayback Machine, and reading the archived Superset configuration recovered 56 table definitions, including a routing value called TestData.

image

The archived Titan interface before the current VPN restriction.

I tried that value against the live API:

POST /v2/Query HTTP/1.1
Host: [redacted]
Content-Type: application/json

{"query":"SELECT 1","tableName":"TestData","rowLimit":1}

With no authorization header it returned 401 Unauthorized, so Antares started probing how it validated JWTs.

Breaking the JWT

Over the next ten days, while I worked through hundreds of other leads, Antares kept coming back to Titan and chipping away at its JWT checks one error at a time.

It started with a token from my external Entra test tenant, created months earlier and used regularly for testing. Titan threw back a tenant error. Changing the tenant to Microsoft’s reached an audience error. Changing the audience hit an application allowlist error. Changing the application ID finally reached a user lookup.

The payload kept changing while the signature stayed exactly the same, and Titan kept accepting the new claims, like a bouncer checking the name on every ID but never looking at the photo. That was the first big clue it wasn’t verifying signatures.

I’d exploited an unsigned JWT bypass by hand before I ever used AI, so I recognized the pattern immediately.

Next I replaced the token entirely with a synthetic JWT using this header:

{"alg":"none","typ":"JWT"}

A normal signed JWT has three populated sections: header.payload.signature. Mine ended with a bare period, because the third section was empty:

base64url(header).base64url(payload).

Titan wasn’t validating the signature at all.

The payload used the values Titan expected, but with a upn I controlled:

{
  "aud": "[redacted]",
  "tid": "[redacted]",
  "appid": "[redacted]",
  "upn": "[email protected]",
  "oid": "00000000-0000-0000-0000-000000000000"
}

That cleared the tenant, audience, and application checks, then returned:

Antares was running Codex and Claude on the lead. Because a UPN is normally an email-formatted Entra identity, both models kept testing placeholders, published service aliases, and Microsoft employee-style addresses.

The unsigned token was already reaching Titan’s local user lookup. Antares just couldn’t find a UPN Titan recognized, so it had no working query and no impact to show, and the finding stayed a lead.

Trying admin

I’d spent Friday on schoolwork. By the time the Titan lead popped up again and I decided to take another look myself, it was after 1 AM on Saturday, September 5.

I tried a few valid-looking UPNs and they failed too. So I stopped guessing and started thinking about what the backend was actually doing with the claim. What if it was using upn to look up a local application username? I changed the unsigned token’s upn from an email-formatted identity to admin.

The result was only the number 1, but after ten days of authentication errors, it was a very interesting 1. I was finally executing SQL as Titan’s administrator.

image

Titan accepted the unsigned administrator identity and executed the SQL query.

The funny part is that admin is obvious, but obviously not a valid UPN. That’s exactly why Antares never guessed it. I only tried it because I stopped taking the field name at face value and thought about what a developer might have done on the backend.

Titan used the unsigned upn claim as a local username. admin resolved to local user ID 1, which held the Admin role, and the SQL ran. Sometimes the answer really is just admin.

Inside the Databases

I tried TestData first, assuming it would only hold fake data, and it did: test cases in a test database. But SHOW DATABASES revealed the rest, including Titan’s platform metadata database. From there I could query application tables directly.

The metadata database held the real data. The first thing I found was a table of active application user accounts with names, email addresses, and login history. Across the platform metadata:

  • Approximately 25,000 account and email records.
  • 17,990 employee email records.
  • 15,001 employee organization records.
  • 355 database configurations.
  • 20,979 virtual-dataset SQL definitions.
  • 24,569 dashboards, 425,891 charts, and 27,347 dataset definitions.

image

An application user record with identifying information and login activity. The password field held a placeholder hash from Superset’s local user model, not a real Microsoft credential. Sensitive values are redacted.

The Titan user and usage directory exposed employee details such as job titles, departments, and management hierarchy for staff associated with Titan. This data only covered a subset of Microsoft employees, not the full employee directory. It could’ve helped an attacker craft targeted social-engineering attempts, though I never tested or demonstrated that.

At that point the employee data looked like the main finding. Then I noticed a separate Bing analytics source.

Validating a Bounded Bing Analytics Sample

I requested one row from the latest available Bing analytics partition. A second one-row query returned a different record from the same partition. These bounded samples showed that Bing search analytics were reachable through the service. I limited my testing to two one-row samples. Bing was only one of the databases reachable this way.

image

One analytics record containing search, identifier, and high-level location fields. My query pulled only a few of the available fields. Note that the location values didn’t contain precise user locations, but rather country or state-level information from reverse IP. Sensitive values are redacted.

Because MUIDs appeared in more than one dataset, it’s plausible that user activity could’ve been correlated across services, though I never actually did this. I didn’t identify anyone, link records across datasets, or build any profile from what I sampled.

I’d started drafting a report to MSRC as soon as I found the employee records. After seeing the Bing data I reported it immediately.

17.3 Trillion Rows

The total scale was the last thing I worked out.

I tested all 56 routing values from the archived config with SELECT 1. 30 were still active. Each routing value pointed to a backend configuration, and each configuration contained one or more databases, so the 30 live values resolved through 24 configurations to 17 connected analytics databases spanning 9,863 unique table names.

There was no convenient grand total. I had separate metadata counts from 17 ClickHouse databases, so I handed them to the AI to sum while I checked how each Distributed table mapped to its backing cluster. I counted one replica per shard and verified the total through two metadata paths: system.tables.total_rows and active system.parts.

The sum came back unformatted:

I had to split it into groups of three just to read it:

Seventeen trillion - a storage estimate from metadata that likely includes historical, duplicated, and derived data, but quite the high number nonetheless. That was the estimated scale of the environment technically reachable through this bypass.

My first thought was that the replicas had been double-counted, or a few extra zeroes had slipped in. I checked again. They hadn’t. Both paths returned the same number.

It was 2 AM. I wanted to yell, or at least say something out loud, but my parents were asleep. So I just sat there staring at 17,333,335,124,315 and checked the math again.

Closing Thoughts

There are two things I took away from this.

First, AI and human intuition compounded here. Antares did ten days of work I didn’t have to: enumerating subdomains, grinding through JWT error messages one field at a time, mapping the whole attack surface, and getting the unsigned token through four layers of validation. What it couldn’t do was realize that upn wasn’t actually a UPN. Looking back, the User not found error should’ve been the tell. The crafted JWT had already passed Titan’s authentication checks, and it was simply trying to match upn to one of its own users. Antares wouldn’t have gotten here alone, and neither would I. Its persistence, plus one human hunch, is what made this find possible.

Second, the bug. Titan validated the contents of the JWT (tenant, audience, app ID, user) but never verified the signature, the most important part of any authentication check. The authentication checks felt like a hotel where every door had a working keycard reader, but any keycard unlocked any room. Despite all the access-control logic existing in the app, the one missing piece made it all pointless. If you’re a developer (or coding agent) reading this, the most important takeaway from this post is to make sure you verify signatures above all else when building auth.

Timeline

  • 08/25/26 - Antares surfaced Titan’s public API and saved it as a lead. I recovered the archived Superset configuration and its 56 table definitions from the Wayback Machine.
  • 08/25/26–09/05/26 - Antares repeatedly worked through Titan’s JWT checks and tested email-formatted UPNs.
  • 09/05/26 - I returned to the request, changed upn to admin, and got the SQL query working.
  • 09/05/26 - Confirmed access to 30 live routing targets and 17 connected analytics databases without dumping the underlying records, then reported the vulnerability to MSRC. Case 144051 was opened the same day.
  • 09/06/26–09/08/26 - MSRC asked me to stop testing and requested my IP address to confirm there was no activity beyond my security research. They confirmed the issue was receiving priority attention.
  • 09/09/26 - The API endpoint was locked down. MSRC indicated that the report prompted immediate investigation and remediation to address the remaining exposure.
  • 09/17/26 - Awarded $5,000
  • 09/22/26 - Met with Microsoft to discuss the finding and coordinate disclosure.
  • 09/22/26–09/24/26 - Reworked this writeup at Microsoft’s request, rewording impact and removing some sections and figures ahead of publication.
联系我们 contact @ memedata.com