How to endorse your entire Linkedin friends list for “Bow Hunting”

9/22/2016
SWOT Analysis happening in 15 minutes

I think I like LinkedIn because it reminds me of the breakfast room at mid-range hotels. There’s an aroma of “business” inside these beige walls, in-between the powdered eggs and mini-boxes of cereal. Even though it’s now filled with Floridian travelers and hungover groomsmen, this place is where companies once came to practice “professional development” in the name of productivity, innovation, and synergy.

LinkedIn is where professionals go to socialize. Built on the promise of new opportunity, LinkedIn is the Facebook of the corporate world. It’s the cheesy recruiter emails, profile view notifications, and odd business memes that keep me coming back for more.

LinkedIn also has a great Javascript application running their UI. If you can look past the dark patterns, you’ll notice a robust single page application powering your news feed. Powered by AJAX, you can seamlessly endorse, post, add, remove, and query friends.

And the endorsements are fantastic. At first I was happy to get endorsed for CSS, JS, Web design, Microsoft Excel, and teamwork. But then I found all these lesser known endorsements… Did you know you can be endorsed for Animal Husbandry? How about Solid Waste? You can even be endorsed for Endorsing!

How to acknowledge your coworkers talents

Let’s take a look at we can tell the world about our friends new mastery: Bow Hunting.

First: Find all your friends.

Easy enough, the LinkedIn API allows you to query all of them. Adjust the count to your total amount of friends, and then sit back an relax.

https://www.linkedin.com/connected/api/v2/contacts?start=0&count=10

Second: Get your CSRF token

Don’t worry, this one is easy. We’ll even use the LinkedIn client-side API to parse our cookies. This token is required to post to the API.

LI.readCookie(‘JSESSIONID’);

Third: Find the request payload for an endorsement, for Bow Hunting.

Pop open the network tab of the dev tools of choice, and watch what happens when you endorse a friend. You’ll see this form data:

{ 
‘endorsedItemName-0’:’Bowhunting’, 
‘endorsedItemType-0’:’skill’, 
‘endorsedItemId-0’:’35690', 
‘recipientType-0’:’member’, 
‘recipientId-0’: 12343242, 
‘endorse’:’Endorse’, 
‘endorsementCount’:’1' 
}

Also check out the request headers for these inclusions:

‘X-IsAJAXForm’: 1, 
‘X-LinkedIn-traceDataContext’: ‘X-LI-ORIGIN-UUID=SOMELONGASSNUMBER==’

Want to endorse your favorite friends for something else? Look up the endorsedItemId-0 here: https://www.linkedin.com/ta/skill?query=barking

We have access to all member’s of our friends list, and the API to endorse them. Now all we need is Jquery

Spoiler alert, you might not need Jquery. But if the developers at LinkedIn are giving you access to it in the global namespace, why not?

Tying it all together…. (don’t actually do this.)

Go to a LinkedIn page. Any page. Open the Javascript console. Enter this:

(function(){
    'use_strict';
    const count = 200; //How popular are you?
    const dontEndorseIfTheyWorkHere = "YOUR COMPANY";
    const csrf = LI.readCookie('JSESSIONID').replace(/"/gi,'');
    let userIDs = [];
    let step = 0;$.ajax(`https://www.linkedin.com/connected/api/v2/contacts?start=0&count=${count}`,
        {complete: data => parse(data)}
    );function parse(data) {
        userIDs = data.responseJSON.values.filter(shouldWeEndorse);
        endorseForBowhunting();
    }function shouldWeEndorse(user) {
        return user.company.name !== dontEndorseIfTheyWorkHere;
    }function endorseForBowhunting() {
        
        var URL = `https://www.linkedin.com/profile/endorse?_ed=0_crpQwclsqh40t8SP-6xKj6Xa8ECCs2_PCAxX_TMp93v2_X1nEvjx2hy1Tjmyz49eU_nQPZuodD3JQTX0n0nv3Q6XGJLmk-V8LmDIS6J5BzQ&csrfToken=${csrf}`var testData = {
            'endorsedItemName-0':'Bowhunting',
            'endorsedItemType-0':'skill',
            'endorsedItemId-0':'35690',
            'recipientType-0':'member',
            'recipientId-0': userIDs[step].memberId,
            'endorse':'Endorse',
            'endorsementCount':'1'
        };$.ajax(URL, {
            method: 'POST',
            headers: {
                'X-IsAJAXForm': 1,
                'X-LinkedIn-traceDataContext': 'X-LI-ORIGIN-UUID=SOMELONGASSNUMBER=='
            },
            data: testData,
            complete: res => {
                step++;
                if(step < userIDs.length) {
                    endorseForBowhunting();
                }
            }
        });
    }
    
}());

The code above will:

  • Get a list of all your connections.
  • Filter out people if they work at the same place you do, because it would embarrassing if you endorsed the director’s director.
  • Send a fancy new endorsement to literally everyone else using AJAX.

But why?

I could tell you that client-side web security is an important part of modern web development, but you already know to never trust the client. No one is going to convince people to open themselves up to malicious code by pasting arbitrary Javascript in their console… well not unless you ask Facebook.

So you’re telling me this giant blob of JS wont give me free coins in Farmville?

Sometimes it’s fun just to poke around websites and see how their client APIs are structured. It’s always a little surprising when you see a large portion of the code base exposed in the global namespace, because developing without a proper build system and modules is a living hell. Those things typically fix this problem by concealing the most important elements of your application through scoping. It’s a lot harder to prevent your REST endpoints from being hit by the client in ways that you didn’t anticipate though. You can prevent CSRF attacks (LinkedIn is) with tokens, but your users are still able to use the dev tools.

I’m tempted to classify this as a self-xss attack, but if you remove the social engineering component and clearly state what this does, is it really an attack?

Companies like Facebook are active trying to disable the dev console on their sites, it leads me to believe there really isn’t a better way to stop arbitrary code from using their APIs for fun. Just another reason why classical programmers snear at web development… “What do you mean anyone can read my source code, and use my APIs?!”

Update: Tested this in production, it definitely works. There is zero rate limiting on the endorsement API. I now know a lot of bow hunters.