我原本可以访问 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 美元的奖金。该文章还指出,微软在发布前编辑了披露内容,以削弱或重新表述部分有关影响范围的声明。

Hacker News 的讨论聚焦于一名 16 岁安全研究人员的说法:微软访客登记身份验证漏洞可能暴露 17 TB 的记录,其中包括个人身份信息(PII)。他表示自己在没有访问客户数据的情况下负责任地报告了该漏洞。微软奖励了他 5,000 美元,并据称要求对文章内容拥有编辑控制权,这导致他删除了部分章节和数字,并弱化了对该漏洞影响的描述。 评论者普遍谴责微软的处理方式,认为奖金过低,而且未成年人不应在没有法律顾问的情况下放弃对漏洞披露内容的控制权。另一些人认为,相较于立即获得报酬,他可能更重视声誉、职业发展、父母建议或避免法律责任。 讨论还探讨了负责任披露,以及企业可能给研究人员带来的风险。在技术方面,参与者指出,微软接受使用 `alg: none` 的未签名 JWT 是核心问题,同时批评了微软的安全文化以及 JWT 容易出错的 设计。一个较小的分支讨论则质疑,发布的文章中是否存在 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