C1011: Looking into Typo3 v10.4.3 source code - part 1
A few weeks ago, I became interested in Typo3 source code. At that time, the latest version available of the CMS was version 10.4.3 (version replaced by the current v10.4.4). My objective was to discover potential new vulnerabilities, which I didn’t do by the way. But because there’s always a but… I discovered that if you can read arbitrary files, then you can write arbitrarily files on the underlying file system.
The keymaker
The configuration file
<ROOT>/typo3conf/LocalConfiguration.php
contains an entry named encryptionKey
that once obtained, allows an
unauthenticated attacker to write files on the system and therefore allows him
to execute commands.
The local configuration file is basically a long array which is simply returned when the file is included. It represents the global TYPO3 CMS configuration. Link
It is thus necessary to have at least a arbitrary file read to exploit what will be presented to you.
The door
I first used grep to list the occurrences of the string unserialize(base64_decode($
▶ grep -R 'unserialize(base64_decode(\$'
vendor/symfony/var-dumper/Server/DumpServer.php: $payload = @unserialize(base64_decode($message), ['allowed_classes' => [Data::class, Stub::class]]);
typo3/sysext/frontend/Classes/Controller/ShowImageController.php: $parameters = unserialize(base64_decode($parametersEncoded));
typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php: $this->formState = unserialize(base64_decode($serializedFormState));
And then read the code on the pages that included that string. When focusing on
the code on the page <ROOT>/typo3_src/typo3/sysext/frontend/Classes/Controller/ShowImageController.php
and trying to figure out where this one was called, I realized that the code of
function initialize()
was accessible by non-authenticated users via the
following route <BASE_URL>/index.php?eID=tx_cms_showpic.
To make sure that this is really the case let’s add a little debugging.
File: <ROOT>/typo3_src/typo3/sysext/frontend/Classes/Controller/ShowImageController.php
public function initialize()
{
echo "[DEBUG]: Start". "\n";
$fileUid = $this->request->getQueryParams()['file'] ?? null;
$parametersArray = $this->request->getQueryParams()['parameters'] ?? null;
// If no file-param or parameters are given, we must exit
if (!$fileUid || !isset($parametersArray) || !is_array($parametersArray)) {
throw new \InvalidArgumentException('No valid fileUid given', 1476048455);
}
// rebuild the parameter array and check if the HMAC is correct
$parametersEncoded = implode('', $parametersArray);
/* For backwards compatibility the HMAC is transported within the md5 param */
$hmacParameter = $this->request->getQueryParams()['md5'] ?? null;
$hmac = GeneralUtility::hmac(implode('|', [$fileUid, $parametersEncoded]));
if (!is_string($hmacParameter) || !hash_equals($hmac, $hmacParameter)) {
throw new \InvalidArgumentException('hash does not match', 1476048456);
}
// decode the parameters Array
$parameters = unserialize(base64_decode($parametersEncoded));
foreach ($parameters as $parameterName => $parameterValue) {
$this->{$parameterName} = $parameterValue;
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
$this->file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$fileUid);
} else {
$this->file = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($fileUid);
}
$this->frame = $this->request->getQueryParams()['frame'] ?? null;
}
The following line calculates a hmac according to the parameters $fileUid
and
$parametersEncoded
.
$hmac = GeneralUtility::hmac(implode('|', [$fileUid, $parametersEncoded]));
Let’s see what the function hmac()
from class GeneralUtility
does.
File: <ROOT>/typo3_src/typo3/sysext/core/Classes/Utility/GeneralUtility.php
public static function hmac($input, $additionalSecret = '')
{
$hashAlgorithm = 'sha1';
$hashBlocksize = 64;
$secret = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
if (extension_loaded('hash') && function_exists('hash_hmac') && function_exists('hash_algos') && in_array($hashAlgorithm, hash_algos())) {
$hmac = hash_hmac($hashAlgorithm, $input, $secret);
} else {
// Outer padding
$opad = str_repeat(chr(92), $hashBlocksize);
// Inner padding
$ipad = str_repeat(chr(54), $hashBlocksize);
if (strlen($secret) > $hashBlocksize) {
// Keys longer than block size are shorten
$key = str_pad(pack('H*', call_user_func($hashAlgorithm, $secret)), $hashBlocksize, "\0");
} else {
// Keys shorter than block size are zero-padded
$key = str_pad($secret, $hashBlocksize, "\0");
}
$hmac = call_user_func($hashAlgorithm, ($key ^ $opad) . pack('H*', call_user_func(
$hashAlgorithm,
($key ^ $ipad) . $input
)));
}
return $hmac;
}
In its simplest operation (by default) this function generates a hmac based on
the variable $input
and a secret with the help of the hash function sha1()
.
The secret is stored as encryptionKey
in the file
<ROOT>/typo3conf/LocalConfiguration.php as
explained at the beginning of the chapter. That’s why it is necessary to be able
to read this file in order to exploit the unserialize()
.
To return to the analysis of the initialize()
function, this one calls
hmac()
with parameters that we control $fileUid
and $parametersEncoded
and
compares the result of the one with $hmacParameter
which we also control. If
they’re equal, the string $parametersEncoded
is base64 decoded and then
unserialize.
That’s when we can have fun :)
Let’s assume that:
encryptionKey = ad2274f12d58b2ab6e3e9365bbcb93e7b64bf4908e685f1b68ab05ba49755ddd5fdf95f433219c6bf665e8bf146f6708
Let’s add a debug line:
File: <ROOT>/typo3_src/typo3/sysext/frontend/Classes/Controller/ShowImageController.php
public function initialize()
{
echo "[DEBUG]: Start". "\n";
$fileUid = $this->request->getQueryParams()['file'] ?? null;
$parametersArray = $this->request->getQueryParams()['parameters'] ?? null;
// If no file-param or parameters are given, we must exit
if (!$fileUid || !isset($parametersArray) || !is_array($parametersArray)) {
throw new \InvalidArgumentException('No valid fileUid given', 1476048455);
}
// rebuild the parameter array and check if the HMAC is correct
$parametersEncoded = implode('', $parametersArray);
/* For backwards compatibility the HMAC is transported within the md5 param */
$hmacParameter = $this->request->getQueryParams()['md5'] ?? null;
$hmac = GeneralUtility::hmac(implode('|', [$fileUid, $parametersEncoded]));
if (!is_string($hmacParameter) || !hash_equals($hmac, $hmacParameter)) {
throw new \InvalidArgumentException('hash does not match', 1476048456);
}
// decode the parameters Array
$parameters = unserialize(base64_decode($parametersEncoded));
echo "[DEBUG]: Hit! after unserialize" . "\n";
foreach ($parameters as $parameterName => $parameterValue) {
$this->{$parameterName} = $parameterValue;
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
$this->file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$fileUid);
} else {
$this->file = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($fileUid);
}
$this->frame = $this->request->getQueryParams()['frame'] ?? null;
}
Let’s calculate the value of the hmac:
▶ php -r "echo hash_hmac('sha1','XXXX|XXXX','ad2274f12d58b2ab6e3e9365bbcb93e7b64bf4908e685f1b68ab05ba49755ddd5fdf95f433219c6bf665e8bf146f6708');"
1d9f8be4b6a3ea3d148c75337546fb1e52a17df4
And let’s see if our expectations are being met.
And they are !