Video Screencast Help
Symantec to Separate Into Two Focused, Industry-Leading Technology Companies. Learn more.
Cyber Security Services

Fantasy’s Inadequate HAC

Created: 30 Jul 2012 • Updated: 30 Jul 2012
Christopher.Emerson's picture
+1 1 Vote
Login to vote

Horizontal Access Control issue in Fantasy Sports

Wow.  That just sounds inappropriate.  Moving on...

I recently posted a CSRF issue I discovered on one of my fantasy sports sites.  In the process of documenting that issue, I happened upon something far more devious/awesome.

In an effort to provide the vulnerable fantasy sports site with some anonymity, we shall call it… RANDOMFANTASYSPORTSSITE.ORG, also to be referenced as the "site."

The sites allow authenticated users to perform sensitive actions with/on behalf of any other user within their own league.  More specifically, a site customer can send themself a trade offer as if it were originating from another team.

The baseball and the football sites I tested operated differently.  As to why, I am not sure, but let’s walk through both examples to see what we can learn.

Fantasy Baseball Trade Request Body 

payload=%7B%22team_from%22%3A%226%22%2C%22team_to%22%3A%228%22%2C%22comment%22%3A%22%22%2C%22players%22%3A%5B%221708184%3A6%3A8%22%2C%221725348%3A8%3A6%22%5D%2C%22draft_picks%22%3A%5B%5D%7D&access_token=U2FsdGVkX18IG9iWsFKwpBUH0Uye-cWN8XqrImYf-C9b7aebOpKz8x67Zwj5Rnvxad31qBpHPipq6Fft3ODaMzqaWCs7-pw4C9ZCe0WbF_aXvFziFUP0HyAIOinZM-yl6Yh51o87f6JDYlJvY6A7sw&resultFormat=json&responseFormat=json

Or, decoded and slightly more legible:

payload={"team_from":"6","team_to":"8","comment":"THIS TRADE ROCKS","players":["1708184:6:8","1114751:8:6"],"draft_picks":[]}&resultFormat=json&responseFormat=json

The above JSON payload has a few interesting items to look at:

“team_from":"6" – This is the number associated to your team.  You can find this by hovering over a link to your team, and seeing what number is in the URL path.  (e.g. http://test.baseball.RANDOMFANTASYSPORTSSITE.ORG/teams/6 is our team)

"team_to":"8" - This is the number associated with the team you are offering the trade to.  Hopefully you had already figured that out.

"comment":"THIS TRADE ROCKS" - Smack talk.  Not very good smack talk, but smack talk nonetheless.  This can also be used to communicate additional information within a trade offer.

"players":["1708184:6:8","1725348:8:6"] - This is where things really get interesting.  It appears the format is [“your team’s player number”:”your team”:”opponent’s team”, “opponent team’s player number”:”opponent’s team”:”your team”]. Your team’s player number is the random number the site associated with that player, otherwise referred to as the player ID number.  In this case, 1708184 = Mike Montgomery.  You can find this number by visiting a player’s page and looking in the URL. (e.g., http://test.baseball.RANDOMFANTASYSPORTSSITE.ORG/players/playerpage/1708184). 

"draft_picks":[] - Honestly, my league doesn’t use this to track our draft picks, but this is probably exploitable as well by using the following techniques.

Hopefully you all see where I am going with this.  If we modify the values in the JSON payload, we could get something like this:

payload={"team_from":"8","team_to":"6","comment":"THIS TRADE ROCKS","players":["1114751:8:6","::"],"draft_picks":[]}&resultFormat=json&responseFormat=json

We have now changed the Request to place a trade offer from our opponent (team #8) to our team (team #6). 

Winning!

All we have to do is hit accept and we are the proud owner of Evan Longoria! 

Let’s now take a look at the football side of the site.

Fantasy Football Trade Request Body

dummy%3A%3Aform=1&form%3A%3Aform=form&team_by=12&team_by_12=1823355&team_to=5&team_to_5=411579&form%3A%3Aplayers_by=&form%3A%3Aplayers_to=&form%3A%3Acomment=&form%3A%3Aeffective_point=22&action=Offer+Trade

Or, again decoded and slightly more legible:

dummy::form=1&form::form=form&team_by=12&team_by_12=1823355&team_to=5&team_to_5=411579&form::players_by=&form::players_to=&form::comment=THISTRADEROCKS&form::effective_point=22&action=Offer+Trade

Looking at the above, we see the following interesting parameters/values:

team_by=12 - This is the number associated with your team.  You can find this by hovering over a link to your team, and seeing what number is in the URL path.  (e.g., http://test.football.RANDOMFANTASYSPORTSSITE.ORG/teams/page/12 is our team)

team_by_12=1823355 - This is the player ID for your team, as we discussed above.

team_to=5 - This is the number associated with the team you are offering the trade to. 

team_to_5=411579 - This is the player ID for your opponent’s team.  As before, you can find this number by visiting a player’s page and looking in the URL. (e.g., http://test.football.RANDOMFANTASYSPORTSSITE.ORG/teams/page/5). 

Same problems, different formatting.  If we modify the values in the Request, we could get something like this:

dummy::form=1&form::form=form&team_by=5&team_by_5=411579&team_to=12&team_to_12=1823355&form::players_by=&form::players_to=&form::comment=THISTRADEROCKS&form::effective_point=22&action=Offer+Trade

We have now changed the Request to place a trade offer from our opponent (team #5) to our team (team #12). 

Whoa.

Fortunately, remediating these issues isn't impossible.  The crux of the issue falls on the application server trusting the content of the request.  I believe Mulder said it best.

So how can we fix this?  Perhaps a 'Fantasy Bounty' system where real players go after Fantasy Sports players who cheat?  Probably not my best idea.

Instead, let's have the application server verify that the user is authorized to submit the request on behalf of the originating team.  In many Fantasy Sports sites, both the owner(s) and commisioner can make these types of requests.  We can base this authorization on the session identifier submitted with the request, and the associated permissions that are granted to the authenticated user.

Update

The site has remediated the issue on the Fantasy Baseball site by adding an access token parameter to the JSON request. 

Blog Entry Filed Under: