Hello!
Today I would like to show you little crypto script, that helped me solve Nested Easter Egg in WebSec 101: JuiceShop ⭐⭐⭐⭐ challenges 2/3!
It’s very basic python implementation of shift cipher, also known as Caesar Cipher, Polybius cipher or ROT 13 (depends on shifting value), which is primitive form of substitution cipher.
I was intending to start writing about things related to cryptography, so we will begin with “back to the roots”!
History & simple math
History of ciphers is longer than anybody could’ve expected – it began thousands of years ago. First intentional modification of the text to hide some information occurred about 4000 years ago in ancient Egypt. It is believed that ancient Egyptians used unusual symbols to obscure meaning of the inscriptions.
Old Greeks over 2000 years ago used Scytale– ciphering ‘machine’ to produce transposition ciphertext, and to Greeks we owe cryptology term – Cryptography, or cryptology (from Ancient Greek: κρυπτός, romanized: kryptós “hidden, secret”; and γράφειν graphein, “to write”, or -λογία -logia, “study”)
Caesar Cipher is one of most well know ciphers even today – as I mentioned, it was described by Greek writer Polyibus, but first recored use was by Julius Caesar.
“If he had anything confidential to say, he wrote it in cipher, that is, by so changing the order of the letters of the alphabet, that not a word could be made out. If anyone wishes to decipher these, and get at their meaning, he must substitute the fourth letter of the alphabet, namely D, for A, and so with the others.”
Suetonius, Life of Julius Caesar
Basically, to encrypt the message Caesar was substituting the letter in the text by one that is three positions to the right.

If we would like to describe it using modern mathematic (or, to be more specific: modular arithmetic), we would write:
26 is the length of latin alphabet, and n=3 in Caesar version of this shifting cipher.
Decryption is performed similarly, but another way around:
Another version of this Caesar Cipher is ROT 13 – which rotates (shifts) the letter by 13 instead od 3. It has been reported that ROT13 was used by Netscape Communicator in 1990s to store user’s password.
It’s worth notice that Caesar(ROT13(x)) = ROT16(x)
, because Caesar is adding +3, ROT is +13.
Common joke in cryptology community is ROT26 or double ROT 13 – notice, that shifting letter +26 mod 26
is actually turning around… and no shifting! So ROT 13 is involution – that is its own inverse fucntion, both encrypting and decrypting!

Those are very simple Additive/Substitution ciphers, but also must-know backgrounds 😊
Note: The cryptoanalysis of this cipher is trivial – there are some techniques based on frequency analysis, but also brute force requires up to 25 guesses.
Implementation
I’m going to write simple script in python to implement shift cipher function. To ease approach, I’m going to focus on lowercase alphabet only.
import string
alphabet = string.ascii_lowercase # 'abcdefghijklmnopqrstuvwxyz'
I’m want my function to take text (both plaintext or ciphertext), shift value, and mode (encrypt – enc
or decrypt – dec
)
def shift_cipher(text, shift, mode):
Then I’m going to lower my text, ‘modulo truncate’ shift (if somebody is going to try e.g. shift by 27 instead of 1…) and initialize output:
def shift_cipher(text, shift, mode):
plain = text.lower()
shift %= len(alphabet)
ciphertext = ''
For each letter in text I would like to find corresponding position (for a -> 0, b->1 ,…) add shift and truncate it (modulo) with alphabet length.
To find letter’s position in alphabet I’m going to use find()
function, which returns position of a letter if string. If letter cannot be found, it returns -1
value. So I’m going to check if the value is equal to -1
(for all special characters, such as space, slash, etc.) and leave it untouched, the rest I’m going to shift.
for letter in plain:
position = alphabet.find(letter)
if position == -1:
ciphertext += letter
continue
if mode == 'enc':
ciphertext += alphabet[(position+shift)%len(alphabet)]
elif mode == 'dec':
ciphertext += alphabet[(position-shift)%len(alphabet)
Whole magic function after concatenation:
def shift_cipher(text, shift, mode):
plain = text.lower()
shift %= len(alphabet)
ciphertext = ''
for letter in plain:
position = alphabet.find(letter)
if position == -1:
ciphertext += letter
continue
if mode == 'enc':
ciphertext += alphabet[(position+shift)%len(alphabet)]
elif mode == 'dec':
ciphertext += alphabet[(position-shift)%len(alphabet)]
return ciphertext
Cracking the egg
We will try with a simple encryption example:
>> print(f'head full of ciphers –
{shift_cipher("head full of ciphers",3,"enc")}')
head full of ciphers – khdg ixoo ri flskhuv
And decryption:
>> print(f'khdg ixoo ri flskhuv –
{shift_cipher ("khdg ixoo ri flskhuv",3,"dec")}')
khdg ixoo ri flskhuv – head full of ciphers
Also, ROT13(ROT13(x))
aka ROT26
checked:
>> shift_cipher(shift_cipher('abcd',13,'enc'),13,'enc')
'abcd'
But we have an egg to crack!
In WebSec 101: JuiceShop ⭐⭐⭐⭐ challenges 2/3 I was challenged by Base64 string encoded. Let’s call it
egg = 'L2d1ci9xcmlmL25lci9mYi9zaGFhbC9ndX\
JsL3V2cS9uYS9ybmZncmUvcnR0L2p2Z3V2YS9ndXIvcm5mZ3JlL3J0dA=='
I’m planning to:
- assign base64 value to
egg
variable - decode string with base64 decoder –
base64.b64decode()
- cast bytes to string (base64 decoder outputs bytes) with
str.decode("utf-8")
- bruteforce all possibile plaintextes with
for
loopin range (1,26)
Code is very simple:
egg = 'L2d1ci9xcmlmL25lci9mYi9zaGFhbC9ndX\
JsL3V2cS9uYS9ybmZncmUvcnR0L2p2Z3V2YS9ndXIvcm5mZ3JlL3J0dA==' #step1
egg_decoded = base64.b64decode(egg) #step 2
egg_decoded = egg_decoded.decode("utf-8") #step 3
print(f'step 1 - egg = {egg}')
print(f'step 2+3 - egg base64 decoded = {egg_decoded}')
for i in range(1,26): #step4
print(f'#{i} - {shift_cipher(egg_decoded,i,"dec")}')
And output solves the puzzle:
step 1 - egg = L2d1ci9xcmlmL25lci9mYi9zaGFhbC9ndXJsL3V2cS9uYS9ybmZncmUvcnR0L2p2Z3V2YS9ndXIvcm5mZ3JlL3J0dA==
step 2+3 - egg base64 decoded = /gur/qrif/ner/fb/shaal/gurl/uvq/na/rnfgre/rtt/jvguva/gur/rnfgre/rtt
#1 - /ftq/pqhe/mdq/ea/rgzzk/ftqk/tup/mz/qmefqd/qss/iuftuz/ftq/qmefqd/qss
#2 - /esp/opgd/lcp/dz/qfyyj/espj/sto/ly/pldepc/prr/htesty/esp/pldepc/prr
#3 - /dro/nofc/kbo/cy/pexxi/droi/rsn/kx/okcdob/oqq/gsdrsx/dro/okcdob/oqq
#4 - /cqn/mneb/jan/bx/odwwh/cqnh/qrm/jw/njbcna/npp/frcqrw/cqn/njbcna/npp
#5 - /bpm/lmda/izm/aw/ncvvg/bpmg/pql/iv/miabmz/moo/eqbpqv/bpm/miabmz/moo
#6 - /aol/klcz/hyl/zv/mbuuf/aolf/opk/hu/lhzaly/lnn/dpaopu/aol/lhzaly/lnn
#7 - /znk/jkby/gxk/yu/latte/znke/noj/gt/kgyzkx/kmm/coznot/znk/kgyzkx/kmm
#8 - /ymj/ijax/fwj/xt/kzssd/ymjd/mni/fs/jfxyjw/jll/bnymns/ymj/jfxyjw/jll
#9 - /xli/hizw/evi/ws/jyrrc/xlic/lmh/er/iewxiv/ikk/amxlmr/xli/iewxiv/ikk
#10 - /wkh/ghyv/duh/vr/ixqqb/wkhb/klg/dq/hdvwhu/hjj/zlwklq/wkh/hdvwhu/hjj
#11 - /vjg/fgxu/ctg/uq/hwppa/vjga/jkf/cp/gcuvgt/gii/ykvjkp/vjg/gcuvgt/gii
#12 - /uif/efwt/bsf/tp/gvooz/uifz/ije/bo/fbtufs/fhh/xjuijo/uif/fbtufs/fhh
#13 - /the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg
#14 - /sgd/cdur/zqd/rn/etmmx/sgdx/ghc/zm/dzrsdq/dff/vhsghm/sgd/dzrsdq/dff
#15 - /rfc/bctq/ypc/qm/dsllw/rfcw/fgb/yl/cyqrcp/cee/ugrfgl/rfc/cyqrcp/cee
#16 - /qeb/absp/xob/pl/crkkv/qebv/efa/xk/bxpqbo/bdd/tfqefk/qeb/bxpqbo/bdd
#17 - /pda/zaro/wna/ok/bqjju/pdau/dez/wj/awopan/acc/sepdej/pda/awopan/acc
#18 - /ocz/yzqn/vmz/nj/apiit/oczt/cdy/vi/zvnozm/zbb/rdocdi/ocz/zvnozm/zbb
#19 - /nby/xypm/uly/mi/zohhs/nbys/bcx/uh/yumnyl/yaa/qcnbch/nby/yumnyl/yaa
#20 - /max/wxol/tkx/lh/ynggr/maxr/abw/tg/xtlmxk/xzz/pbmabg/max/xtlmxk/xzz
#21 - /lzw/vwnk/sjw/kg/xmffq/lzwq/zav/sf/wsklwj/wyy/oalzaf/lzw/wsklwj/wyy
#22 - /kyv/uvmj/riv/jf/wleep/kyvp/yzu/re/vrjkvi/vxx/nzkyze/kyv/vrjkvi/vxx
#23 - /jxu/tuli/qhu/ie/vkddo/jxuo/xyt/qd/uqijuh/uww/myjxyd/jxu/uqijuh/uww
#24 - /iwt/stkh/pgt/hd/ujccn/iwtn/wxs/pc/tphitg/tvv/lxiwxc/iwt/tphitg/tvv
#25 - /hvs/rsjg/ofs/gc/tibbm/hvsm/vwr/ob/soghsf/suu/kwhvwb/hvs/soghsf/suu
One line looks interesting:
#13 - /the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg
It’s human readable, and as we know from corresponding article, it also solves the Nested Easter Egg challenge 😉
Conclusion
It is really entertaining to get back from time to time to classical cryptography and solve this kind of puzzles!
If you want to see how to solve mentioned challenge from cover to cover in 33 lines, please see below:
import string
import base64
alphabet = string.ascii_lowercase
def shift_cipher(text, shift, mode):
plain = text.lower()
shift %= len(alphabet)
ciphertext = ''
for letter in plain:
position = alphabet.find(letter)
if position == -1:
ciphertext += letter
continue
if mode == 'enc':
ciphertext += alphabet[(position+shift)%len(alphabet)]
elif mode == 'dec':
ciphertext += alphabet[(position-shift)%len(alphabet)]
return ciphertext
egg = 'L2d1ci9xcmlmL25lci9mYi9zaGFhbC9ndX\
JsL3V2cS9uYS9ybmZncmUvcnR0L2p2Z3V2YS9ndXIvcm5mZ3JlL3J0dA==' #step1
egg_decoded = base64.b64decode(egg) #step 2
egg_decoded = egg_decoded.decode("utf-8") #step 3
print(f'step 1 - egg = {egg}')
print(f'step 2+3 - egg base64 decoded = {egg_decoded}')
for i in range(1,26):#step4
print(f'#{i} - {shift_cipher(egg_decoded,i,"dec")}')
I truly believe that this post is another opening for very interesting cycle in cryptology area!
Source code for this project on my Github: HeadFullOfCiphers/CaesarCipher
That it for today 😊 if you liked this post, have any suggestions, questions or just want to contact me – do not hestiate!
Greeting from Greece 🇬🇷 – the cradle of cryptology!
Reference list:
- A Brief History of Cryptography – Cypher Research Laboratories
- Life of Julius Caesar, Suetonius (Bill Thayer‘s Web Site)
- Modern Cryptography: Applied Mathematics for Encryption and Information Security, Chuck Easttom
Check out similiar posts: