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.
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
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
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
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:
evalin them are possibly trying to hide things from you
So when I see
eval(false+chr(0x5B)+chr(0x31)+chr(0x5D)), I start to thing something silly is going on
So, what just is this string supposed to be?
basil /tmp $ lanyard.py basil basil /tmp $
basil? But.. I’m basil…
basil /tmp $ python lanyard.py os.uname()
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
 to the end, which is pointing to the array index 1:
>>> os.uname() '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?
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 126.96.36.199 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 188.8.131.52 184.108.40.206.in-addr.arpa domain name pointer 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
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!
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?
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