Poor Man's Premium API

Debugging JavaScript to use internal website APIs

Introduction

Tarkov Market is an amazing website for Escape From Tarkov players. It offers a lot of market data which can give players insight into profitable trades and prices for items. In my situation, I need all the item price data for a game tool. However, everything I need is locked behind the premium API.

At costs $5 / month! Am I going to pay for it? Hell No.

Part 1: Checking Chrome Network Tab

The website displays item data for the front page meaning it has to receive the data from somewhere such as an API. The network tab is the first place to look for this type of information to see what endpoints the web app may be using. After the front page loads, I am instantly dissapointed by the results:

The server fetches the front page HTML and makes no API requests. This means, the application is server side rendered as the data is already present in the HTML.

As I continue scrolling through the page, I begin to notice that requests continue to flood in. The twist is, the data is queried from an API rather than being continuously rendered on the server.

image

This is gold! The initial request even provides us with some parameters we could filter the results with. The job is done… right? Unfortunetly, the creators of the website thought they could deter possible attackers by making the data more difficult to extract. There is more work to be done.

image

It appears that the data is encoded. But checking the end of the “items” string reveals that it ends with xNDAtbSUyMGFza2VyaSUyMHBpbCUyMiU3RCU1RA==. This is most likely Base64.

Part 2: Writing C# Tooling

To begin, I wrote some quick template code which should help me get to a final result of decrypting this data.

HttpClient client = new HttpClient();

int skipCount = 0;
while (true)
{
    HttpRequestMessage request = new HttpRequestMessage
    {
        Method = HttpMethod.Get,
        RequestUri = new Uri($"https://tarkov-market.com/api/items" +
                                $"?lang=en&search=&tag=&sort=avgDayPrice&sort_direction=desc&trader=&skip={skipCount}&limit=20"),
    };

    HttpResponseMessage response = await client.SendAsync(request);
    EFTBase64Data base64Data = JsonSerializer.Deserialize<EFTBase64Data>(await response.Content.ReadAsStringAsync());
    if (base64Data != null)
    {
        IEnumerable<object> data = base64Data.DecodeBase64();
    }

    skipCount += 20;
}

The HttpClient will send GET request to the server and query 20 items which are sorted by price. THen I added a EFTBase64Data class which models the response from the servers API. On top of this, the EFTBase64Data class should also contain all the logic for properly decoding the data from the response.

public class EFTBase64Data
{
    [JsonPropertyName("result")]
    public string Result { get; set; }

    [JsonPropertyName("items")]
    public string Base64Data { get; set; }

    public IEnumerable<object> DecodeBase64()
    {
        string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(Base64Data));

        return JsonConvert.DeserializeObject<IEnumerable<object>>(
            decoded, Converter.Settings);
    }
}

Unfortunetly, I overestimated the complexity of the task and began getting errors right away.

Unhandled exception. System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.

This means that the data is probably mutated after the request is made which is preventing the decoding of the data.

Part 3: Using the Chrome JavaScript debugger

Never did I think I would have to use the browser JavaScript debugger, but here we are. I began searching for a possible breakpoint line by checking the folder contents of the website.

image

Immediately, the market.ef2e594.modern.js file looked intriguing because it did not seem like a boilerplate script that is usually generated by Nuxt to render pages. Peeking the contents of the file helped me discover this function:

fetchItems(t) {
    var e = this;
    return Object(l.a)((function*() {
        t = t || {};
        var {skip: r} = t
            , {$axios: c, searchText: l, sortBy: n, sortDesc: o, locale: d, filter: filter} = e
            , _ = t.trader || filter.trader || ""
            , h = e.tag ? e.tag.uid : "";
        l = l || "";
        var m = o ? "desc" : "asc";
        r = r || 0;
        var {items: f} = yield c.get("/api/items?lang=".concat(d, "&search=").concat(l, "&tag=").concat(h, "&sort=").concat(n, "&sort_direction=").concat(m, "&trader=").concat(_, "&skip=").concat(r, "&limit=20"));
        return f = Object(v.d)(f) || []
    }
    ))()
},

Although somewhat obfuscated, this is exactly what I was looking for. After receiving the information, Tarkov Market takes the “items” JSON property and stores it into the “f” variable. However, the data is possibly mutated at the return statement.

Stepping into the return statement leads into a routine that mutates the string.

, Y = e=>{
var r = null;
try {
    e = e.substring(0, 9) + e.substring(18),
    e = t[b](e),
    r = p(c(e))
} catch (t) {}
return r

The functionality of the first line inside of the try statement is very obvious, but the next two are not. This is where the Chrome debugger comes into play and gives insight into what the the function inside this array does.

image image

atob decodes a Base64 string and decodeURIComponent decodes a URL encoded string.

image

Once finally decoded, a parased JSON result is returned. After fully understanding how the response data is handled, it is clear why the Base64 decoding failed.

Part 4: Fixing the C# Code

Before, the code had failed to properly substring the text and decode the url format. Now, this functioanlity is implemented and the code runs perfectly.

public IEnumerable<object> DecodeBase64()
{
    //e = e.substring(0, 9) + e.substring(18),
    Base64Data = Base64Data[..9] + Base64Data[18..];

    //atob
    string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(Base64Data));

    //decodeURIComponent
    return JsonConvert.DeserializeObject<IEnumerable<object>>(
        WebUtility.UrlDecode(decoded), Converter.Settings);
}

Finally getting a clear text JSON response means we can create a type to model the output. I did this using QuickType.

image

Part 5: Reaping the Results

Using the JSON type I am able to easily interact with properties and use them to display a desired result.

IEnumerable<EFTItemData> data = base64Data.DecodeBase64();
foreach (var eftItemData in data)
{
    string safeName = eftItemData.EnName.Replace("\"", "\\\"");
    Console.WriteLine($"{{ \"{eftItemData.BsgId}\", {{ \"{safeName}\", {eftItemData.AvgWeekPrice} }} }},");
}
{ "5b6d9ce188a4501afc1b2b25", { "T H I C C Weapon case", 17777778 } },
{ "5c110624d174af029e69734c", { "T-7 Thermal Goggles with a Night Vision mount", 15372000 } },
{ "5c093ca986f7740a1867ab12", { "Secure container Kappa", 4900000 } },
{ "59fb023c86f7746d0d4b423c", { "Weapon case", 4816667 } },
{ "601948682627df266209af05", { "UVSR Taiga-1 survival machete", 4000000 } },
{ "5c0126f40db834002a125382", { "Red Rebel ice pick", 3420833 } },
{ "5c1d0efb86f7744baf2e7b7b", { "TerraGroup Labs keycard (Red)", 3535999 } },
{ "5c0a840b86f7742ffa4f2482", { "T H I C C item case", 3100000 } },
{ "5e42c71586f7747f245e1343", { "ULTRA medical storage key", 2271042 } },
{ "5857a8bc2459772bad15db29", { "Secure container Gamma", 1850000 } },
{ "5bffdd7e0db834001b734a1a", { "Miller Bros. Blades M-2 Tactical Sword", 1570000 } },
{ "5d9f1fa686f774726974a992", { "RB-ST key", 1400000 } },
{ "59fb042886f7746c5005a7b2", { "Item case", 1229994 } },
{ "5ede7a8229445733cb4c18e2", { "RB-PKPM marked key", 961088 } },
{ "5e42c81886f7742a01529f57", { "Object #11SR keycard", 1166136 } },
{ "5b7c710788a4506dec015957", { "Lucky Scav Junk box", 1932580 } },
{ "59db794186f77448bc595262", { "Secure container Epsilon", 1100000 } },
{ "61aa5aed32a4743c3453d319", { "Police truck cabin key", 800000 } },
{ "59fafd4b86f7745ca07e1232", { "Key tool", 882145 } },
{ "5d03794386f77420415576f5", { "6-STEN-140-M military battery", 715311 } },

Conclusion

image

I saved myself $5 by doing 3 hours of work (mostly writing this blog). Time well spent.

On a more serious note, Tarkov Market should try rendering components on the server using Nuxt. Other than that, there is not much that can be done to prevent people from ripping data from the site. The $5 donation supports the creators of the website and gives access to private pages that have information about barters, quest maps, and price charts. Even though this was fun to reverse engineer, the site deserves the $5 donation for the great content it provides.