Hacking the LCA Lanyard

Spoilers!

Posted by Katie McLaughlin on February 7, 2016

Major Epic Spoilers

This post details spoilers as to how the Linux.conf.au Lanyards are able to be hacked. If you’ve always wanted to know, then now’s your chance.

“I HAVE A QUESTION. I HACKED MY LANYARD”

I’ve known about Linux.conf.au, also known as LCA, for a few years before I was first able to attend in 2015. I’d always heard mutterings about how the ‘Silly Description’ on the lanyards is able to be customised.

I’d heard that changing the lanyard description was progressively made harder over the years, so I wasn’t too upset when I failed in 2015. But, in 2016, I was able to crack it.

This post details how I did it.

But first, a warning:

SPOILERS SPOILERS SPOILERS SPOILERS SPOILERS SPOILERS SPOILERS SPOILERS SPOILERS

Content Warning: ROT13, horrible Python code, Zookeepr.


Registration for LCA happens via their website. When you fill in your registration form, you get a few custom fields to enter if you chose to, but then you see a dynamically generated field:

If you refresh the page, you get a different description. So people sometimes refresh the page until they get one they like

However, this is a webform. Surely we could just enter our own content, right?

Using the Inspection tool in Chrome, we can see the underlying code for this part of the form:

There’s a checksum. Mmm.. that probably means that if we try and submit the form without the description string resolving to this exact checksum, then things might not work so well. If only there was a way to see the server side code.

Now, the registration software looks pretty bespoke. It’s not like any other conference stuff I’ve seen before (not just iframed Eventbrite or Tito). So finding out what drives this thing is a bit tricky. There’s no visible copyright on the page or in the code to indicate where it comes from.

It got into my head that maybe, since LCA is all about linux and open source software, that there’s a high chance that this software could be open source. So, let’s throw that field value from the form into a search engine.

Oh, hello there. Zookeepr is it? Let’s dive into your code, and see what we can find…

Oh. Oh my.

Looking about, it looks like this bit of code generates the silly description checksum.



def silly_description_checksum(desc):
    import hashlib, math
    haiku = "Come to Ballarat"\
          "LCA Under the stars"\
          "Comets is landing..."

    #This is meant to be difficult to read, no telling me its indistinguishable from my normal code - Josh
    def fun(cion):
        e = 0.0
        a = 4.1963944517268459E+00
        b = -5.5753297516829114E+00
        c = 2.7916995626938470E+00
        d = -6.5696680861318413E-01
        f = 7.2840990594877031E-02
        g = -3.0390408978587477E-03

        e = g
        e = e * cion + f
        e = e * cion + d
        e = e * cion + c
        e = e * cion + b
        e = e * cion + a
        e = 1.0 / e
        return e

    false = ""
    true = False
    for ny in range(1,9):
        if (ny == 5) or (ny == 8):
            false=false+(haiku[int(math.floor(fun(ny)+1))],haiku[int(math.ceil(fun(ny)+1))])[true]
        else:
            false=false+(haiku[int(math.floor(fun(ny)))],haiku[int(math.ceil(fun(ny)))])[true]
        true = not true
    false=false.lower()+"("+")"

    # Some assistance provided here. All we're doing is taking the silly input string and hashing it with some mysterious salt. Mmmmmm salt
    salted = desc + haiku+eval(false+chr(0x5B)+chr(0x31)+chr(0x5D)).encode(rot_26)
    return  hashlib.sha1(salted.encode('latin1')).hexdigest()

But how do we work out what exactly this bit of code is doing?

We can reduce this function down into stand alone Python code to try and simplify it.

Attempt one

All I’m going to do is take the silly_description_checksum function, and add a little bit of testing code around it.

(link to code)

basil ~/git/blog/assets [gh-pages] basil /tmp $ lanyard.py 
Traceback (most recent call last):
  File "lanyard.py", line 44, in <module>
    silly_desc_parse = silly_description_checksum(silly_desc)
  File "lanyard.py", line 38, in silly_description_checksum
    salted = desc + haiku+eval(false+chr(0x5B)+chr(0x31)+chr(0x5D)).encode(rot_26)
  File "<string>", line 1, in <module>
NameError: name 'os' is not defined

Attempt two

Well, let’s add that import

(link to code)

basil /tmp $ lanyard.py 
Traceback (most recent call last):
File "lanyard.py", line 45, in <module>
  silly_desc_parse = silly_description_checksum(silly_desc)
File "lanyard.py", line 39, in silly_description_checksum
  salted = desc + haiku+eval(false+chr(0x5B)+chr(0x31)+chr(0x5D)).encode(rot_26)
NameError: global name 'rot_26' is not defined

What’s this rot_26? Let’s search this sucker…

Ah. It appears rot_26 is just “rot_13 twice”. Which could be an empty function with no effect…

… expect it appears in helpers.py as an alias for rot_13

Attempt 3

So let’s add the alias and see how that works.

(link to code)

basil /tmp $ lanyard.py 
Unsolved.
4c707619c4fc836a558eb661e43cf662a7e88600 doesn't match ffb8b60a1e43f2fd0f2953b0f7430964727e134e

At least we’re compiling now!


Some things I know about programming:

  • You’re not supposed to run code you don’t trust
  • Things with eval in them are possibly trying to hide things from you
  • Python uses True and False, not true and false like Ruby.

So when I see eval(false+chr(0x5B)+chr(0x31)+chr(0x5D)), I start to thing something silly is going on

Attempt 4

So, what just is this string supposed to be?

  print(eval(false+chr(0x5B)+chr(0x31)+chr(0x5D)))
basil /tmp $ lanyard.py
basil
basil /tmp $

basil? But.. I’m basil…

  print(false+chr(0x5B)+chr(0x31)+chr(0x5D)
basil /tmp $ python lanyard.py                                                                                 
os.uname()[1]

Oh, you cheeky thing! It’s trying to get my os.uname from the system!

If I run this code locally, I get something that looks like this

basil /tmp $ python
Python 2.7.6 (default, Jun 22 2015, 17:58:13) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.uname()
('Linux', 'basil', '3.13.0-76-generic', '#120-Ubuntu SMP Mon Jan 18 15:59:10 UTC 2016', 'x86_64')
>>> 

But, it’s adding [1] to the end, which is pointing to the array index 1:

>>> os.uname()[1]
'basil'

So, this will return basil on my machine, because my machine is called basil. But this code isn’t running on my machine when it’s generating the sillystring, it’s running on a web server.

So how do I get the webserver’s name?


Webserver findering

I know that the registration software is running on linux.conf.au, so let’s try and work out what server that is:

basil /tmp $ host linux.conf.au
linux.conf.au has address 192.55.98.190
linux.conf.au mail is handled by 1 linux.org.au.

Mmmm, I have an IP address. I wonder if that resolves to anything else?

basil /tmp $ host 192.55.98.190
190.98.55.192.in-addr.arpa domain name pointer zookeepr1.linux.org.au.

A ha! zookeepr1.linux.org.au.

This looks like a webserver, one of possibly more than one, in a system under a main domain.

Given this naming scheme, I’m going to guess that the server itself is called zookeepr1.

So let’s change the last bit of the code (link to code):

...

    server = "zookeepr1"

    salted = desc + haiku+server.encode(rot_26)
    return  hashlib.sha1(salted.encode('latin1')).hexdigest()

...
basil /tmp $ python lanyard.py 
Solved!

Gotcha!

Getting this output means I’m able to input the string I got on the rego page, and then output the string I saw on the form.

So, if I want to generate my own, I should just be able to input whatever I want, and then get out the hash I need.

(link to code)

basil /tmp $ python lanyard.py 
ONE COMPLETELY VALID QUESTION
83e9839da2f94a0d0f6e11cf7bb0cd5f506b3923

So, now what do I do?


Changing a form POST

What I can do is change the contents of the form before it’s submitted

By using the Inspect tool as before, you can change the elements on the page by double-clicking the inspection code.

So I can change the old code into this:

So then, if I POST the form, I get my result.

And the product on the day.


So what about that Haiku? Since LCA 2012 was in Ballart, it seems like that input just wasn’t altered. It’s the same in the lca2016 branch as well. If I had to go diving for that input, it’d be a much harder hack. However, that’s where social engineering could come in handy :)


Bare in mind that this is a software hack. There’s always the option of hacking the hardware itself (i.e. just draw on it)


Thank you to Trent for some initial pointers in my investigation, and Paul and Brenda for their customisations to my lanyard.

Full revision history of hacking the code