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.
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
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
So let’s add the alias and see how that works.
basil /tmp $ lanyard.py
Unsolved.
4c707619c4fc836a558eb661e43cf662a7e88600 doesn't match ffb8b60a1e43f2fd0f2953b0f7430964727e134e
At least we’re compiling now!
Some things I know about programming:
eval
in them are possibly trying to hide things from youTrue
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
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?
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.
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.