Hidden in plain sight: Brute Forcing Slack private files

Last year we switched to using Slack for all our internal communication and it’s working out nicely. It’s very developer centric in that it offers integrations with lots of services like Travis CI, GitHub, etc.

When we started using Slack one of our developers was sending a file, had his Developer console open and noticed that even though he’d not chosen to share the file public, the API gave back a public URL anyway. Much to his dismay when he tried it out in a new private browsing window he could download his file without authentication!

Everything you share on Slack automatically becomes available on a public url.

Concerned with the security of our communications (we don’t share financials or credentials through Slack fortunately, but we may share company or customer sensitive information) I decided to look into it and make it a teachable moment on ‘secret URLs’.

Secret URLs, necessary evil?

I shot some video at a bachelor party that I shared to the participants, a lot of whom I didn’t particularly know, as YouTubes ‘Unlisted videos’. All they needed was the URL and they could watch the videos, but that URL was not published anywhere.

And I shared a PDF with some work to a mailinglist via Dropboxes ‘Share Link’ functionality.

All they needed for access was the URL.

As a user I love secret URLs. They give me some wiggle room between “completely private / protected” and “completely public”. Some content isn’t exactly secret but doesn’t deserve to be published and communicated to the world at large. They bear some resemblance to the physical act of handing out a document in a meeting or photos at a party. The only way to gain access is to be granted so by someone who has been granted access before, forming a trust chain.

However, unlike in the ‘real world’, what is shared is not something physical, but information. And information wants to be free. It is (in some cases frighteningly) easy and fast to share information these days.

Intended or unintended (think of browser plugins, HTTP caching proxies or well intentioned friends and colleagues) these secret URLs can become not so secret.

And even if the link no longer works, that doesn’t mean the content has not been copied.

Now if you were to implement secret urls, your second line of defence would be to empower the user by giving her the ability to revoke a public link, explicitly in the UI (as Slack appears to do, more on that later) and / or implicitly by letting the user choose how long to share the link for.

But the first line of defence is to let the user choose whether or not something should actually be public. And users should be informed that secret URLs should only be used for semi-public information.

Brute forcing secret URLs

Putting aside the likelihood that a URL is leaked through some means, having public URLs opens up the possibility for brute forcing. Now the question “How easy is it to brute force a secret URL?” is actually quite interesting and the answer has changed a bit recently.

Brute forcing depends on trying many combinations as fast as possible and is usually done on a local data set. This is mostly because of latency.

Going over a network is simply several orders of magnitude slower than trying a combination locally. The further you are from the target data, the more time is required to try a combination.

Fortunately for the brute forcer the advent of ‘the cloud’ has ensured two things:

Quickly and easily have an army of machines with public IPs available, only paying for the short amount of time you will use them.

Co-locating with your target in the cloud is much easier.

Okay, maybe not in the same virtual or physical machine so you’ll still have a network hop, but it will be much faster.

If you look at the latency infographic, this means going from milliseconds to microseconds.

A wholly unscientific benchmarking of default 404s against a small Digital Ocean LAMP VM produces the following ‘Request per second’ (rps) numbers:

laptop from office over internet, same country ~ 15 rps

small vm in same ‘region’ ~ 1400 rps

on actual destination server (no network) ~ 4000 rps

Brute forcing over the network does introduce some additional bottlenecks:

network throughput (fortunately most HTTP HEAD responses or 404 pages are tiny)

number of webserver processes available

It also introduces the possibility to be detected or stopped by generating too much load on the target.

Ironically the better tuned and configured the target is, the easier it is to brute force them.

At 1400 rps a space of 16 million combinations (6 character hexadecimal) is checked in 3 hours.

At 15 rps it will take 12 days.

So how many random characters does it take?

8, same as passwords, duh.

If only it was so easy. Deciding how many characters you need depends on:

How many Requests Per Second will your servers will be able to take?

The more hardware you have, the more optimisations, the more an attacker could try.



The more hardware you have, the more optimisations, the more an attacker could try. How soon you can respond?

A dedicated file sharing service would be wise to have a security team that monitors traffic actively, a product that gets sold and run at a remote site may seldom to never have it’s logs analysed.



A dedicated file sharing service would be wise to have a security team that monitors traffic actively, a product that gets sold and run at a remote site may seldom to never have it’s logs analysed. Which characters do you use?

The more possibilities the better, but not all characters are as easily used on the web. Decimals or hexadecimals are a sure bet (though leading zeros may be problematic) but including the full alphabet adds 26 more possibilities, in both upper and lower case this adds up to 52. Adding special characters (like {}[]\| etc.) may be more trouble than it’s worth.

Personally I’d go for [A-Za-z0-9], this gives 62 possibilities.



The more possibilities the better, but not all characters are as easily used on the web. Decimals or hexadecimals are a sure bet (though leading zeros may be problematic) but including the full alphabet adds 26 more possibilities, in both upper and lower case this adds up to 52. Adding special characters (like {}[]\| etc.) may be more trouble than it’s worth. Personally I’d go for [A-Za-z0-9], this gives 62 possibilities. How many files you plan to make public?

Often forgotten, but if you plan on making a significant amount of files public then again this helps attackers that may only need / want 1 of those files.

Here is a gist with JavaScript code that can help you decide: brute-force-chances.js.

However, as a default I would state that:

A token of 8 alphanumeric upper and lower case truly cryptographically random characters should be able to withstand Twitter like brute forcing load for a year.

Note that they must be cryptographically random (for instance by using the excellent Zend\Math\Random library in PHP) any ordering (like with using timestamps) would defeat the purpose.

Brute Forcing Slack

Well… brute focing in theory then. Understandably Slack forbids automated testing:

Please only test with your own team when investigating bugs. Automated testing is not permitted.

- https://slack.com/whitehat

However with what we’ve seen and the JavaScript gist should be enough to prove the feasibility.

First let’s upload a file. In my own Slack Team (relaxnow.slack.com) I uploaded a file which made the Slack client do a request to /files.info that gives back some JSON data including the following:

{ "ok":true, "file":{ "id":"F02GJ6FPC", // ... "is\_public":false, "public\_url\_shared":false, "url":"https://slack-files.com/files-pub/T02EMLM07-F02GJ6FPC-a9d3f2/8cx20s4\_-\_imgur.jpg", "url\_download":"https://slack-files.com/files-pub/T02EMLM07-F02GJ6FPC-a9d3f2/download/8cx20s4\_-\_imgur.jpg", "url\_private":"https://files.slack.com/files-pri/T02EMLM07-F02GJ6FPC/8cx20s4\_-\_imgur.jpg", "url\_private\_download":"https://files.slack.com/files-pri/T02EMLM07-F02GJ6FPC/download/8cx20s4\_-\_imgur.jpg", "thumb\_64":"https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4\_-\_imgur\_64.png", "thumb\_80":"https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4\_-\_imgur\_80.png", "thumb\_360":"https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4\_-\_imgur\_360.png", "thumb\_360\_w":360, "thumb\_360\_h":203, "image\_exif\_rotation":1, "permalink":"https://relaxnow.slack.com/files/boy/F02GJ6FPC/8cx20s4\_-\_imgur.jpg", "permalink\_public":"https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2", // ... } }

First off we clearly see is_public: false, but a (working) URL for permalink_public.

Next we see a lot of locations that the file (or copies of it) are hosted on. And most of them actually include the filename, which is against OWASP recommendations and potentially introduces vulnerabilities.

Then again, it does help against brute force attacks, which are made significantly harder when in combination with a random file id.

However when we look at the public links we see a public permalink without file name in the following form:

https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2

Trying out a couple of uploads in quick succession (which Slack makes very easy with drag and drop) gives the following public permalinks:

https://slack-files.com/T02EMLM07-F02GHGJ8H-5d8bbf https://slack-files.com/T02EMLM07-F02GJCMF0-ee919f https://slack-files.com/T02EMLM07-F02GJCN0Q-c6e078 https://slack-files.com/T02EMLM07-F02GHGKNH-f15619 https://slack-files.com/T02EMLM07-F02GHGL9V-9c6a0a https://slack-files.com/T02EMLM07-F02GJCPMA-8bbfb6

Which looks to be in the following format:

Team ID: T0<7 character base 36 number> File ID: F02<6 character base 36 number> Token: 6 character hexadecimal (hash?).

Seems secure enough on the surface, but let’s look closer.

Cracking the Team ID

https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2

Very helpfully Slack includes the Team ID in the HTML output if you go to the log in page for a team:

var team\_id = 'T02EMLM07'; var email\_regex = new RegExp("\[a-z0-9!#$%&'\*+/=?^\_\`{|}~-\]+(?:\\\\.\[a-z0-9!#$%&'\*+/=?^\_\`{|}~-\]+)\*@(?:\[a-z0-9\](?:\[a-z0-9-\]\*\[a-z0-9\])?\\\\.)+\[a-z0-9\](?:\[a-z0-9-\]\*\[a-z0-9\])?", 'i');

One down, two to go.

Cracking the File ID

https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2

Fortunately for an attacker Slack helped by giving a different 404 when a file exists for the Team but the Token is invalid:

- https://slack-files.com/T02EMLM07-F02GJ6FPC-aaaaaa

Than it does when a File ID is invalid:

- https://slack-files.com/T02EMLM07-F02AAAAAA-a9d3f2

Next let’s look at the base36 numbers of the files uploaded in quick succession:

GHGJ8H GJCMF0 GJCN0Q GHGKNH GHGL9V GJCPMA

Or converted to base 10 and minus 990,000,000:

6783713

9960444

9961226

6785549

6786355

9964594

It seems that the numbers are sequential, perhaps for two file servers. Testing with another Slack instance reveals that this number is not bound to a Team. Likely this is the number of files uploaded.

While 36 to the 6th is still a significantly high number (35 days at 700 RPS), in 2014 already 55% could be discarded because it is sequential (< 17 days at 700 RPS for a single file).

However you can discard far more if you’re only interested in recent files.

Slack is very helpful here as it returns the time that the file was uploaded in the If-Modified-Since allowing you to use public links to correlate File IDs to a time.

Cracking the Token

https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2

The token was a 6 character hexadecimal, most likely a hash of some data. I could not find a correlation to file name or some other piece of data that an attacker might know.

When ‘revoking’ a public file in the UI, this token gets renewed.

However a > 50% chance of guessing this token can be achieved with 700 RPS in less than 7 hours. If you have the time and go at a less noticable 70 RPS it will take approximately 2 days to get the same chance.

Conclusion

Slack had taken a weak approach to securing private files. Combining this with the decision to make all files public lead to a rather large attack area that could be exploited by a dedicated attacker.

In fact for known file-sharing services these types of attacks are already happening.

While it took some time, Slack has made the following changes to fix this vulnerability:

Increased the token secret to 10 characters

Now gives the same 404 on wrong token as it does on a wrong File ID.

When a private file is first accessed on it’s public link the user is notified in Slack and shown the public link in the UI with the option to revoke.

Making it impractical to try brute forcing… for Slack that is :-).

Recommendations

Don’t lie to your users about their security for your implementation convenience, don’t make files public by default. Use at least 8 alphanumeric characters generated with a cryptographically random PRNG. If you segment your ID, make sure you don’t leak information in your 404 responses.

Related

OWASP: Brute Force Attack

Security StackExchange: Are random URLs a safe way to protect profile photos?

The Register: Think file-hosting sites guard your private data? Think again. Attacks already under way

Exposing the Lack of Privacy in File Hosting Services [PDF]

Disclosure timeline

I had heard that Slack was terrible with it’s bug bounty program, but this took over a year. Often with little to no response from Slack. I will certainly think twice about working with them in the future.

Julyish 2014 - My colleague initially disclosed this via a short blurb on the Slacks bug bounty program with HackerOne. Slack closed this as ‘Not a bug’.

August 27th 2014 - This prompted Private files are public and brute forcable"

September 2nd 2014 - Response from : To address the points individually: We should start rate-limiting failed attempts to load public URLs. We can’t easily remove the public URLs, since some API consumers rely on these, but we have a plan to phase this out. The brute-forceable portion of the URLs should just be longer. We’re working on these issues and will update here as changes are made.



September 3rd 2014 - Thanks for the feedback! Rate limiting based on IP would mitigate this, but is fraught with gotchas: would be undone if you would need to ever support IPv6. opens up another vector for attack. I can deny YOU access to files by tricking you into visiting a special page that spams slack-files.com people behind proxies (carrier grade NAT is becoming a thing with IPv4 depletion, though just normal work or college proxies are more common) would suffer from 1 bad apple. I would just advise some form of periodic monitoring coupled with stronger tokens. I would make that more visible in the UI. Just remove the whole ‘is_public’ bit and let it show that (for now at least) there is always a public URL and users can ‘refresh’ it. Yes, simple fix would be to make the token 8 character alphanumeric. That alone would be enough to make the attack no longer feasible. Thanks for keeping me up to date, I’d love to publicise the post with the changes that were made!



October 8th 2014 - , no response.

January 13th 2015 - , no response.

January 28th 2015 -

February 16h 2015 -

March 2nd 2015 - Response from : We apologize for the delayed reply. We track these issues via our internal bug system, and only reply to the reporter once the bug is resolved internally. We generally ignore messages asking for updates, as we receive a high volume of these (even for non-issues). Here is the update: This was partially resolved in December by increasing the length of the secret to 10 characters. Unfortunately, this is not one issue, as it requires updates to more than just the secret length. We are still working to resolve all affected apps before considering this issue closed.

March 2nd 2015 - :

Thank you for the update! Though it’s disheartening to know that you won’t even get this reply, I’ve certainly seen enough of the crap you guys get through hackerone to understand not paying too close attention. Just checked and the last bit appears to be 10 character random alphanum. This would be enough to make brute force impossible. I can understand wanting to get rid of the ‘public-by-default’ first but I have put a significant amount of time into the blog post and do want to see that published before we switch to another blog engine and I have to redo the entire thing. As it stands, though please correct me if I’m wrong, your users are not in any danger by posting and I’ve followed the Guidelines to the letter. I’ll post when I have time to update my blog with this correspondence.



March 10th 2015 - Response from : There are still changes to this system that we prefer to make before disclosure. If you have already disclosed, that is fine, but if not, please wait until we have fully resolved the issue.

September 28th 2015 -

October 2nd 2015 - Response from : Thanks for your patience on this issue. I’ll be in touch soon with a more detailed update about the changes we’ve made.

October 30th 2015 - : It has been over a year. Maybe if you give me a date (in 2015!) I can postpone publishing until then.



November 3rd 2015 - awards $500,- bounty

November 3rd 2015 - : Hi Boy, Thanks for your patience on this issue. As you recommended, we’ve increased the entropy on the tokens. We’re also continuously working on monitoring for brute-force attacks across our service. Cheers, and happy bug hunting! -Leigh



November 4th 2015 -

November 6th 2015 -

Congratulations!

You read it all! Sorry for the length. Hope you liked it and learned something! If you liked this post you may also like:

To find out more about Ibuildings and security see also: Security expertise at Ibuildings.