Wpis jest kontynuacją rozwiązań hackme Natas na OverTheWire. Rozwiązania etapów 6-10 przedstawione zostały w tym wpisie. W związku ze wzrostem skomplikowania zadań, rozwiązania stają się coraz dłuższe, więc kolejne rozwiązania będą pojawiać się w odrębnych artykułach.

Analiza

Hasłem uzyskanym na poprzednim etapie logujemy się do kolejnego poziomu. Tym razem otrzymujemy formularz, dzięki któremu możemy zmienić tło strony. Dostępny jest również kod źródłowy:

<?
$defaultdata = array(
    "showpassword" => "no",
    "bgcolor" => "#ffffff"
);

function xor_encrypt($in)
{
    $key     = '<censored>';
    $text    = $in;
    $outText = '';
    
    // Iterate through each character
    for ($i = 0; $i < strlen($text); $i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }
    return $outText;
}

function loadData($def)
{
    global $_COOKIE;
    $mydata = $def;
    if (array_key_exists("data", $_COOKIE)) {
        $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
        if (is_array($tempdata) && array_key_exists("showpassword", $tempdata) && array_key_exists("bgcolor", $tempdata)) {
            if (preg_match('/^#(?:[a-f\d]{6})$/i', $tempdata['bgcolor'])) {
                $mydata['showpassword'] = $tempdata['showpassword'];
                $mydata['bgcolor']      = $tempdata['bgcolor'];
            }
        }
    }
    return $mydata;
}

function saveData($d)
{
    setcookie("data", base64_encode(xor_encrypt(json_encode($d))));
}

$data = loadData($defaultdata);

if (array_key_exists("bgcolor", $_REQUEST)) {
    if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {
        $data['bgcolor'] = $_REQUEST['bgcolor'];
    }
}

saveData($data);
?>

<h1>natas11</h1>
<div id="content">
<body style="background: <?= $data['bgcolor'] ?>;">
Cookies are protected with XOR encryption<br/><br/>

<?
if ($data["showpassword"] == "yes") {
    print "The password for natas12 is <censored><br>";
}

?>

Przeanalizujmy jego działanie w kolejności wykonywania. Cała mechanika skryptu opiera się o następujący fragment:

$data = loadData($defaultdata);

if (array_key_exists("bgcolor", $_REQUEST)) {
    if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {
        $data['bgcolor'] = $_REQUEST['bgcolor'];
    }
}

saveData($data);

Skrypt wczytuje dane funkcją loadData(), następnie sprawdza, czy strona wczytywana jest z parametrem “bgcolor”. Jeśli przesłano kolor, poddawany jest on testowi wyrażenia regularnego (wyklucza to możliwość manipulacji tym polem) i w razie pozytywnego przejścia testu ustawiany jest kolor.

Bez względu na to, czy wysłano kolor, ostatecznie wywoływana jest jeszcze funkcja saveData(). Potencjalne luki bezpieczeństwa mogą zatem znajdować się w funkcjach wczytujących i zapisujących dane, zatem to im się przyjrzymy.

Funkcja loadData() przyjmuje jako parametr tablicę danych domyślnych. Jest ona jednak ignorowana, jeśli użytkownik ma ustawione cookie data. W takim wypadku zawartość ciastka jest rozpakowywana. Zauważamy zatem, że naszym celem jest spreparowanie zawartości data tak, aby jego pole showpassword ustawione było na “yes”. Dalsza część funkcji loadData() zajmuje się jedynie weryfikacją i ustawieniem danych do zwracanej struktury.

Rozwiązanie

Kluczowym w pakowaniu elementem jest funkcja xor_encrypt(), która posługuje się nie znanym nam kluczem. Zastosujemy jednak pewną cechę tego kodowania, która pozwoli nam odzyskać klucz. Rzecz w tym, że prawidłowe są wyrażenia:

Dane XOR Klucz = Zakodowane-Dane

oraz

Dane XOR Zakodowane-Dane = Klucz

Stwórzmy zatem modyfikację xor_encrypt() i zastosujmy tam nasze obserwacje:

<?
// dane do zakodowania
$data = array(
    "showpassword" => "no",
    "bgcolor" => "#ffffff"
);

function xor_encrypt($in)
{
    $key     = base64_decode("ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw="); // dane ustawione w mojej przeglądarce przez skrypt, wygenerowane dla #000000
    $text    = $in;
    $outText = '';
    
    // Iterate through each character
    for ($i = 0; $i < strlen($text); $i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }
    return $outText;
}

print base64_decode("ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw");
print "\n";
print json_encode($data);
print "\n";

print xor_encrypt(json_encode($data));
print "\n";
?>

Uruchomienie skryptu spowodowało następujący rezultat:

UK"H+O%pSWh]UZ-T%UhR^,^h

{"showpassword":"no","bgcolor":"#ffffff"}
qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jq

Zauważamy powtarzający się ciąg qw8J analiza pętli zawartej w funkcji każe jednak zauważyć, że klucz używany jest cyklicznie, co stanowi zabezpieczenie przy kodowaniu ciągu innej niż klucz długości. W związku z tym wnioskujemy, że klucz jest tym jednym, powtarzającym się fragmentem. Teraz, znając klucz, zakodujmy własną tablicę.

<?
function xor_encrypt($in)
{
    $key     = 'qw8J';
    $text    = $in;
    $outText = '';
    
    // Iterate through each character
    for ($i = 0; $i < strlen($text); $i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }
    return $outText;
}

// dane do zakodowania
$data = array(
    "showpassword" => "yes",
    "bgcolor" => "#ffffff"
);

print base64_encode(xor_encrypt(json_encode($data)));
?>

Wynikiem działania skryptu jest nowe ciastko data:

ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK

Po podmianie ciastka i odświeżeniu strony otrzymujemy hasło do następnego poziomu:

The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3