diff --git a/protocols/docs/NOW-spec.md b/protocols/docs/NOW-spec.md index 3f57c56..98d2436 100644 --- a/protocols/docs/NOW-spec.md +++ b/protocols/docs/NOW-spec.md @@ -3,7 +3,7 @@ TOC is generated in [Obsidian](obsidian.md) via [TOC plugin](https://github.com/hipstersmoothie/obsidian-plugin-toc) --> -# NOW-PROTO 1.6 +# NOW-PROTO 1.7 - [Messages](#messages) - [Transport](#transport) - [Message Syntax](#message-syntax) @@ -46,6 +46,7 @@ TOC is generated in [Obsidian](obsidian.md) via - [NOW_EXEC_BATCH_MSG](#now_exec_batch_msg) - [NOW_EXEC_WINPS_MSG](#now_exec_winps_msg) - [NOW_EXEC_PWSH_MSG](#now_exec_pwsh_msg) + - [Elevated Execution](#elevated-execution) - [RDM Messages](#rdm-messages) - [NOW_RDM_MSG](#now_rdm_msg) - [NOW_RDM_CAPABILITIES_MSG](#now_rdm_capabilities_msg) @@ -328,6 +329,7 @@ increment major version; Protocol implementations with different major version a | NOW_CAP_EXEC_STYLE_PWSH
0x0020 | PowerShell 7 (.ps1) execution style. | | NOW_CAP_EXEC_UNICODE_CONSOLE
0x0040 | Host supports encoding control flags (RAW_ENCODING, UNICODE_CONSOLE, and ENCODING_UTF8). | | NOW_CAP_EXEC_IO_REDIRECTION
0x1000 | Set if host implements exec session IO redirection. | +| NOW_CAP_EXEC_ELEVATE_SHELL
0x0080 | Set if host can elevate an exec session using the platform shell. Elevation may prompt the interactive user for consent, and IO redirection is unavailable for elevated sessions. See [Elevated Execution](#elevated-execution). | @@ -956,6 +958,7 @@ packet-beta | Flag | Meaning | |----------------------------------------|---------------------------| | NOW_EXEC_FLAG_RUN_DIRECTORY_SET
0x0001 | `directory` field contains non-default value. | +| NOW_EXEC_FLAG_RUN_ELEVATED
0x0002 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). | **sessionId (4 bytes)**: A 32-bit unsigned integer containing a unique remote execution session id. @@ -993,6 +996,7 @@ packet-beta | NOW_EXEC_FLAG_PROCESS_PARAMETERS_SET
0x0001 | `parameters` field contains non-default value. | | NOW_EXEC_FLAG_PROCESS_DIRECTORY_SET
0x0002 | `directory` field contains non-default value.| | NOW_EXEC_FLAG_PROCESS_ENCODING_UTF8
0x0004 | Enables OEM-to-UTF-8 transcoding for stdin, stdout, and stderr. Without this flag, data streams are passed through as raw bytes without encoding conversion. | +| NOW_EXEC_FLAG_PROCESS_ELEVATED
0x0008 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). | | NOW_EXEC_FLAG_PROCESS_IO_REDIRECTION
0x1000 | Enable stdio (stdout, stderr, stdin) redirection. | | NOW_EXEC_FLAG_PROCESS_DETACHED
0x8000 | Detached mode: the process is started without tracking execution or sending back output. | @@ -1035,6 +1039,7 @@ packet-beta |----------------------------------------|---------------------------| | NOW_EXEC_FLAG_SHELL_SHELL_SET
0x0001 | `shell` field contains non-default value. | | NOW_EXEC_FLAG_SHELL_DIRECTORY_SET
0x0002 | `directory` field contains non-default value. | +| NOW_EXEC_FLAG_SHELL_ELEVATED
0x0008 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). | | NOW_EXEC_FLAG_SHELL_IO_REDIRECTION
0x1000 | Enable stdio (stdout, stderr, stdin) redirection. | | NOW_EXEC_FLAG_SHELL_DETACHED
0x8000 | Detached mode: the shell is started without tracking execution or sending back output. | @@ -1077,6 +1082,8 @@ packet-beta | NOW_EXEC_FLAG_BATCH_DIRECTORY_SET
0x0001 | `directory` field contains non-default value. | | NOW_EXEC_FLAG_BATCH_RAW_ENCODING
0x0002 | Disables the default OEM-to-UTF-8 transcoding: data streams are passed through as raw bytes without any encoding conversion. | | NOW_EXEC_FLAG_BATCH_UNICODE_CONSOLE
0x0004 | Enables Unicode console: agent injects `@chcp 65001 > nul` and writes the script in BOM-less UTF-8. Implies UTF-8 stdout/stderr streams. | +| NOW_EXEC_FLAG_BATCH_ELEVATED
0x0008 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). | +| NOW_EXEC_FLAG_BATCH_NO_EXIT
0x0010 | Keeps the command interpreter running after the batch file completes (`cmd /K` rather than `/C`). MUST be ignored when the session redirects stdio, where the hidden interpreter would never exit. | | NOW_EXEC_FLAG_BATCH_IO_REDIRECTION
0x1000 | Enable stdio (stdout, stderr, stdin) redirection. | | NOW_EXEC_FLAG_BATCH_DETACHED
0x8000 | Detached mode: the batch is started without tracking execution or sending back output. | @@ -1132,6 +1139,7 @@ packet-beta | NOW_EXEC_FLAG_PS_DIRECTORY_SET
0x0100 | `directory` field contains non-default value and specifies command working directory | | NOW_EXEC_FLAG_PS_RAW_ENCODING
0x0200 | Disables the default OEM-to-UTF-8 transcoding: data streams are passed through as raw bytes without any encoding conversion. | | NOW_EXEC_FLAG_PS_UNICODE_CONSOLE
0x0400 | Enables Unicode console: agent injects `$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()` at script start. Implies stdin/stdout/stderr streams are UTF-8. | +| NOW_EXEC_FLAG_PS_ELEVATED
0x0800 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). | | NOW_EXEC_FLAG_PS_IO_REDIRECTION
0x1000 | Enable stdio (stdout, stderr, stdin) redirection. | | NOW_EXEC_FLAG_PS_SERVER_MODE
0x2000 | Run PowerShell in server mode. | | NOW_EXEC_FLAG_PS_DETACHED
0x8000 | Detached mode: PowerShell is started without tracking execution or sending back output. | @@ -1196,6 +1204,56 @@ packet-beta **configurationName (variable)**: A NOW_VARSTR structure, same as with NOW_EXEC_WINPS_MSG. +#### Elevated Execution + +Any exec message may request elevated execution by setting its `NOW_EXEC_FLAG_*_ELEVATED` flag. +The flag expresses *intent* only: it never names a mechanism, and the client does not choose one. + +`ELEVATED` raises the privileges of the identity that would otherwise run the command, which is the +session user. It does not select a different identity, and it carries no credentials. Running a +command as another user is a separate concern and would be expressed by a separate field. + +**Requesting elevation.** A client SHOULD NOT set an `ELEVATED` flag unless the server advertised +at least one `NOW_CAP_EXEC_ELEVATE_*` capability. A server that receives an `ELEVATED` flag without +having advertised any elevation capability MUST fail the session with `NOW_CODE_NOT_IMPLEMENTED`. +A server predating this version ignores the flag entirely and executes without elevation, so a +client MUST NOT treat the absence of a failure as evidence that elevation took place: capability +negotiation is the only reliable signal, and a client is expected to refuse the request locally +when no elevation capability was advertised. +A server MUST NOT execute a command without elevation after being asked to elevate it: silently +downgrading the request denies the client any way to detect that privileges were not granted. + +**Mechanism selection.** The server selects the mechanism and advertises it in `execCapset`. Only +`NOW_CAP_EXEC_ELEVATE_SHELL` is defined at present. A mechanism with different observable +properties, such as one that raises no consent prompt or preserves stdio redirection, is expected to +be advertised as an additional capability rather than by changing the meaning of an existing one, so +a client always learns what to expect before it sends a request. + +**Capability scope.** An elevation capability states that the server implements elevation, not that +every execution style it advertises can be elevated. A server MAY support elevation for some styles +and not others, and MUST fail a request for a style it cannot elevate rather than executing that +request without elevation. A client MUST therefore be prepared for a per-style failure even when an +elevation capability was advertised. + +**Semantics under NOW_CAP_EXEC_ELEVATE_SHELL.** The server elevates through the platform shell, +which on Windows raises a consent prompt on the interactive user's desktop: + +- A client SHOULD omit `NOW_EXEC_FLAG_*_IO_REDIRECTION` when requesting elevation, and MUST NOT + assume the server honours it. The server MUST NOT open stdio channels for the session unless it + advertises a capability stating that elevated sessions keep their stdio. +- Unless the request uses `NOW_EXEC_RUN_MSG`, which is never tracked, or sets + `NOW_EXEC_FLAG_*_DETACHED`, which promises no tracking, the server SHOULD still track the + session and report the real exit code in `NOW_EXEC_RESULT_MSG`, so a client can distinguish + success from failure without stdio. +- `NOW_EXEC_ABORT_MSG` and `NOW_EXEC_CANCEL_REQ_MSG` MAY fail with `NOW_CODE_NOT_IMPLEMENTED`, + because the elevated child runs at a higher integrity level than the server's session process. +- Elevation MAY require interactive consent, so the request MAY fail when no user is present to + grant it, or when the user declines. + +**Failure reporting.** A server that cannot grant elevation MUST report it distinctly rather than +succeeding: the account has no elevated token available, the user declined consent, or elevation is +not implemented for the requested exec style. + ### RDM Messages #### NOW_RDM_MSG @@ -1484,3 +1542,7 @@ packet-beta - Add `NOW_EXEC_FLAG_PROCESS_ENCODING_UTF8` flag for process exec commands. - Add `NOW_EXEC_FLAG_*_UNICODE_CONSOLE` flags for batch (cmd), winps, and pwsh exec commands. - Add `NOW_CAP_EXEC_UNICODE_CONSOLE` capability flag. +- 1.7 + - Add `NOW_EXEC_FLAG_*_ELEVATED` flags for run, process, shell, batch, winps, and pwsh exec messages. + - Add `NOW_CAP_EXEC_ELEVATE_SHELL` capability flag. + - Add `NOW_EXEC_FLAG_BATCH_NO_EXIT` flag for batch exec commands. diff --git a/protocols/dotnet/Devolutions.NowClient/src/AExecParams.cs b/protocols/dotnet/Devolutions.NowClient/src/AExecParams.cs index 8127622..6c5b085 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/AExecParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/AExecParams.cs @@ -42,6 +42,12 @@ public StartedHandler OnStarted } } + /// + /// Whether the caller asked for elevated execution. Inspected by + /// so a request cannot be sent to a server that never advertised an elevation capability. + /// + internal bool IsElevated { get; set; } + internal ExecSession ToExecSession(uint sessionId, ChannelWriter commandWriter) { return new ExecSession( diff --git a/protocols/dotnet/Devolutions.NowClient/src/ExecBatchParams.cs b/protocols/dotnet/Devolutions.NowClient/src/ExecBatchParams.cs index 0963aa4..1617b9a 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/ExecBatchParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/ExecBatchParams.cs @@ -54,6 +54,26 @@ public ExecBatchParams UnicodeConsole(bool enable = true) return this; } + /// + /// Keep the command interpreter open after the batch file completes (`cmd /K` rather than + /// `/C`). Ignored by the server when the session redirects stdio. + /// + public ExecBatchParams NoExit(bool enable = true) + { + _noExit = enable; + return this; + } + + /// + /// Execute with elevated privileges. Requires the host to advertise an elevation + /// capability; under shell-based elevation the session has no IO redirection. + /// + public ExecBatchParams Elevated(bool enable = true) + { + IsElevated = enable; + return this; + } + internal NowMsgExecBatch ToNowMessage(uint sessionId) { var builder = new NowMsgExecBatch.Builder(sessionId, command); @@ -83,6 +103,16 @@ internal NowMsgExecBatch ToNowMessage(uint sessionId) builder.EnableUnicodeConsole(); } + if (IsElevated) + { + builder.EnableElevated(); + } + + if (_noExit) + { + builder.EnableNoExit(); + } + return builder.Build(); } @@ -91,5 +121,6 @@ internal NowMsgExecBatch ToNowMessage(uint sessionId) private bool _detached = false; private bool _rawEncoding = false; private bool _unicodeConsole = false; + private bool _noExit = false; } } \ No newline at end of file diff --git a/protocols/dotnet/Devolutions.NowClient/src/ExecProcessParams.cs b/protocols/dotnet/Devolutions.NowClient/src/ExecProcessParams.cs index 2a5f57b..b556efb 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/ExecProcessParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/ExecProcessParams.cs @@ -1,4 +1,4 @@ -using Devolutions.NowProto.Messages; +using Devolutions.NowProto.Messages; namespace Devolutions.NowClient { @@ -54,6 +54,16 @@ public ExecProcessParams EncodingUtf8(bool enable = true) return this; } + /// + /// Execute with elevated privileges. Requires the host to advertise an elevation + /// capability; under shell-based elevation the session has no IO redirection. + /// + public ExecProcessParams Elevated(bool enable = true) + { + IsElevated = enable; + return this; + } + internal NowMsgExecProcess ToNowMessage(uint sessionId) { var builder = new NowMsgExecProcess.Builder(sessionId, filename); @@ -83,6 +93,11 @@ internal NowMsgExecProcess ToNowMessage(uint sessionId) builder.EnableEncodingUtf8(); } + if (IsElevated) + { + builder.EnableElevated(); + } + return builder.Build(); } diff --git a/protocols/dotnet/Devolutions.NowClient/src/ExecPwshParams.cs b/protocols/dotnet/Devolutions.NowClient/src/ExecPwshParams.cs index adb2b8f..aefaf3b 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/ExecPwshParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/ExecPwshParams.cs @@ -127,6 +127,16 @@ public ExecPwshParams UnicodeConsole(bool enable = true) return this; } + /// + /// Execute with elevated privileges. Requires the host to advertise an elevation + /// capability; under shell-based elevation the session has no IO redirection. + /// + public ExecPwshParams Elevated(bool enable = true) + { + IsElevated = enable; + return this; + } + internal NowMsgExecPwsh ToNowMessage(uint sessionId) { var builder = _serverMode @@ -193,6 +203,11 @@ internal NowMsgExecPwsh ToNowMessage(uint sessionId) builder.EnableUnicodeConsole(); } + if (IsElevated) + { + builder.EnableElevated(); + } + return builder.Build(); } diff --git a/protocols/dotnet/Devolutions.NowClient/src/ExecRunParams.cs b/protocols/dotnet/Devolutions.NowClient/src/ExecRunParams.cs index d312101..3e313af 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/ExecRunParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/ExecRunParams.cs @@ -18,6 +18,16 @@ public ExecRunParams Directory(string directory) return this; } + /// + /// Execute with elevated privileges. Requires the host to advertise an elevation + /// capability; under shell-based elevation the session has no IO redirection. + /// + public ExecRunParams Elevated(bool enable = true) + { + IsElevated = enable; + return this; + } + internal NowMsgExecRun ToNowMessage(uint sessionId) { var builder = new NowMsgExecRun.Builder(sessionId, command); @@ -27,6 +37,11 @@ internal NowMsgExecRun ToNowMessage(uint sessionId) builder.Directory(_directory); } + if (IsElevated) + { + builder.EnableElevated(); + } + return builder.Build(); } diff --git a/protocols/dotnet/Devolutions.NowClient/src/ExecShellParams.cs b/protocols/dotnet/Devolutions.NowClient/src/ExecShellParams.cs index 2c01438..e7ec7a3 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/ExecShellParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/ExecShellParams.cs @@ -46,6 +46,16 @@ public ExecShellParams Detached(bool enable = true) return this; } + /// + /// Execute with elevated privileges. Requires the host to advertise an elevation + /// capability; under shell-based elevation the session has no IO redirection. + /// + public ExecShellParams Elevated(bool enable = true) + { + IsElevated = enable; + return this; + } + internal NowMsgExecShell ToNowMessage(uint sessionId) { var builder = new NowMsgExecShell.Builder(sessionId, command); @@ -70,6 +80,11 @@ internal NowMsgExecShell ToNowMessage(uint sessionId) builder.EnableDetached(); } + if (IsElevated) + { + builder.EnableElevated(); + } + return builder.Build(); } diff --git a/protocols/dotnet/Devolutions.NowClient/src/ExecWinPsParams.cs b/protocols/dotnet/Devolutions.NowClient/src/ExecWinPsParams.cs index b11acf3..ea5753a 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/ExecWinPsParams.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/ExecWinPsParams.cs @@ -128,6 +128,16 @@ public ExecWinPsParams UnicodeConsole(bool enable = true) return this; } + /// + /// Execute with elevated privileges. Requires the host to advertise an elevation + /// capability; under shell-based elevation the session has no IO redirection. + /// + public ExecWinPsParams Elevated(bool enable = true) + { + IsElevated = enable; + return this; + } + internal NowMsgExecWinPs ToNowMessage(uint sessionId) { var builder = _serverMode @@ -194,6 +204,11 @@ internal NowMsgExecWinPs ToNowMessage(uint sessionId) builder.EnableUnicodeConsole(); } + if (IsElevated) + { + builder.EnableElevated(); + } + return builder.Build(); } diff --git a/protocols/dotnet/Devolutions.NowClient/src/NowClient.cs b/protocols/dotnet/Devolutions.NowClient/src/NowClient.cs index 0834b1a..c5943de 100644 --- a/protocols/dotnet/Devolutions.NowClient/src/NowClient.cs +++ b/protocols/dotnet/Devolutions.NowClient/src/NowClient.cs @@ -269,6 +269,8 @@ public async Task ExecRun(ExecRunParams execParams) ThrowCapabilitiesError("Run execution style"); } + ThrowIfElevationUnsupported(execParams); + var sessionId = _nextExecSessionId++; var message = execParams.ToNowMessage(sessionId); var command = new CommandExecRun(message); @@ -289,6 +291,8 @@ public async Task ExecProcess(ExecProcessParams execParams) ThrowCapabilitiesError("Process execution style"); } + ThrowIfElevationUnsupported(execParams); + var sessionId = _nextExecSessionId++; var message = execParams.ToNowMessage(sessionId); var execSession = execParams.ToExecSession(sessionId, _commandWriter); @@ -312,6 +316,8 @@ public async Task ExecShell(ExecShellParams execParams) ThrowCapabilitiesError("Shell execution style"); } + ThrowIfElevationUnsupported(execParams); + var sessionId = _nextExecSessionId++; var message = execParams.ToNowMessage(sessionId); var execSession = execParams.ToExecSession(sessionId, _commandWriter); @@ -335,6 +341,8 @@ public async Task ExecBatch(ExecBatchParams execParams) ThrowCapabilitiesError("Batch execution style"); } + ThrowIfElevationUnsupported(execParams); + var sessionId = _nextExecSessionId++; var message = execParams.ToNowMessage(sessionId); var execSession = execParams.ToExecSession(sessionId, _commandWriter); @@ -358,6 +366,8 @@ public async Task ExecWinPs(ExecWinPsParams execParams) ThrowCapabilitiesError("Windows PowerShell execution style"); } + ThrowIfElevationUnsupported(execParams); + var sessionId = _nextExecSessionId++; var message = execParams.ToNowMessage(sessionId); var execSession = execParams.ToExecSession(sessionId, _commandWriter); @@ -381,6 +391,8 @@ public async Task ExecPwsh(ExecPwshParams execParams) ThrowCapabilitiesError("Pwsh execution style"); } + ThrowIfElevationUnsupported(execParams); + var sessionId = _nextExecSessionId++; var message = execParams.ToNowMessage(sessionId); var execSession = execParams.ToExecSession(sessionId, _commandWriter); @@ -601,6 +613,24 @@ private async Task EnsureRdmCapabilitiesSent() } } + /// + /// Refuse an elevated request unless the negotiated capset advertises a way to satisfy it. + /// A server older than NOW-PROTO 1.7 does not know the ELEVATED flag and would run the + /// command without elevation, which the caller has no way to detect. + /// + private void ThrowIfElevationUnsupported(AExecParams execParams) + { + if (!execParams.IsElevated) + { + return; + } + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.ElevateShell)) + { + ThrowCapabilitiesError("Elevated execution"); + } + } + private static void ThrowCapabilitiesError(string capability) { throw new NowClientException($"{capability} is not supported by server."); diff --git a/protocols/dotnet/Devolutions.NowProto.Tests/src/MsgChannel.cs b/protocols/dotnet/Devolutions.NowProto.Tests/src/MsgChannel.cs index d8eac66..8fc1b61 100644 --- a/protocols/dotnet/Devolutions.NowProto.Tests/src/MsgChannel.cs +++ b/protocols/dotnet/Devolutions.NowProto.Tests/src/MsgChannel.cs @@ -1,4 +1,4 @@ -using Devolutions.NowProto.Capabilities; +using Devolutions.NowProto.Capabilities; namespace Devolutions.NowProto.Tests { @@ -16,7 +16,7 @@ void Capset() var encoded = new byte[] { - 0x0E, 0x00, 0x00, 0x00, 0x10, 0x01, 0x01, 0x00, 0x01, 0x00, 0x06, 0x00, 0x01, 0x00, 0x04, 0x00, 0x05, 0x00, 0x2C, 0x01, 0x00, 0x00 + 0x0E, 0x00, 0x00, 0x00, 0x10, 0x01, 0x01, 0x00, 0x01, 0x00, 0x07, 0x00, 0x01, 0x00, 0x04, 0x00, 0x05, 0x00, 0x2C, 0x01, 0x00, 0x00 }; var decoded = NowTest.MessageRoundtrip(msg, encoded); @@ -37,7 +37,7 @@ public void CapsetSimple() var encoded = new byte[] { - 0x0E, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + 0x0E, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; var decoded = NowTest.MessageRoundtrip(msg, encoded); diff --git a/protocols/dotnet/Devolutions.NowProto.Tests/src/NowExecStyles.cs b/protocols/dotnet/Devolutions.NowProto.Tests/src/NowExecStyles.cs index 1f70503..292935e 100644 --- a/protocols/dotnet/Devolutions.NowProto.Tests/src/NowExecStyles.cs +++ b/protocols/dotnet/Devolutions.NowProto.Tests/src/NowExecStyles.cs @@ -278,5 +278,163 @@ public void PwshSimple() Assert.False(decoded.NonInteractive); Assert.False(decoded.NoExit); } + + // Elevation flag round-trips for every exec style. Byte arrays are the same wire encodings + // asserted by the Rust test suite, which keeps both implementations in lockstep. + + [Fact] + public void RunElevated() + { + var msg = new NowMsgExecRun.Builder(0x12345678, "a") + .EnableElevated() + .Build(); + + var encoded = new byte[] + { + 0x09, 0x00, 0x00, 0x00, 0x13, 0x10, 0x02, 0x00, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + } + + [Fact] + public void ProcessElevated() + { + var msg = new NowMsgExecProcess.Builder(0x12345678, "a") + .EnableElevated() + .Build(); + + var encoded = new byte[] + { + 0x0B, 0x00, 0x00, 0x00, 0x13, 0x11, 0x08, 0x00, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + } + + [Fact] + public void ShellElevated() + { + var msg = new NowMsgExecShell.Builder(0x12345678, "a") + .EnableElevated() + .Build(); + + var encoded = new byte[] + { + 0x0B, 0x00, 0x00, 0x00, 0x13, 0x12, 0x08, 0x00, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + } + + [Fact] + public void BatchElevatedNoExit() + { + var msg = new NowMsgExecBatch.Builder(0x12345678, "a") + .EnableElevated() + .EnableNoExit() + .Build(); + + var encoded = new byte[] + { + 0x09, 0x00, 0x00, 0x00, 0x13, 0x13, 0x18, 0x00, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + Assert.True(decoded.NoExit); + } + + [Fact] + public void WinPsElevated() + { + var msg = new NowMsgExecWinPs.Builder(0x12345678, "a") + .EnableElevated() + .Build(); + + var encoded = new byte[] + { + 0x0D, 0x00, 0x00, 0x00, 0x13, 0x14, 0x00, 0x08, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + } + + [Fact] + public void PwshElevated() + { + var msg = new NowMsgExecPwsh.Builder(0x12345678, "a") + .EnableElevated() + .Build(); + + var encoded = new byte[] + { + 0x0D, 0x00, 0x00, 0x00, 0x13, 0x15, 0x00, 0x08, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + } + + + // ELEVATED and NO_EXIT are asserted independently as well as together: the combined 0x0018 + // mask alone would stay green if the two constants were swapped. + + [Fact] + public void BatchElevatedOnly() + { + var msg = new NowMsgExecBatch.Builder(0x12345678, "a") + .EnableElevated() + .Build(); + + var encoded = new byte[] + { + 0x09, 0x00, 0x00, 0x00, 0x13, 0x13, 0x08, 0x00, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.Elevated); + Assert.False(decoded.NoExit); + } + + [Fact] + public void BatchNoExitOnly() + { + var msg = new NowMsgExecBatch.Builder(0x12345678, "a") + .EnableNoExit() + .Build(); + + var encoded = new byte[] + { + 0x09, 0x00, 0x00, 0x00, 0x13, 0x13, 0x10, 0x00, 0x78, 0x56, + 0x34, 0x12, 0x01, 0x61, 0x00, 0x00, 0x00 + }; + + var decoded = NowTest.MessageRoundtrip(msg, encoded); + + Assert.True(decoded.NoExit); + Assert.False(decoded.Elevated); + } + } } \ No newline at end of file diff --git a/protocols/dotnet/Devolutions.NowProto/src/Capabilities/NowCapabilityExec.cs b/protocols/dotnet/Devolutions.NowProto/src/Capabilities/NowCapabilityExec.cs index c2cde9b..e75ae5e 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Capabilities/NowCapabilityExec.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Capabilities/NowCapabilityExec.cs @@ -62,6 +62,15 @@ public enum NowCapabilityExec : ushort /// UnicodeConsole = 0x0040, - All = Run | Process | Shell | Batch | WinPs | Pwsh | IoRedirection | UnicodeConsole, + /// + /// Set if host can elevate an exec session using the platform shell. Elevation may prompt + /// the interactive user for consent, and IO redirection is unavailable for elevated + /// sessions. + /// + /// NOW-PROTO: NOW_CAP_EXEC_ELEVATE_SHELL + /// + ElevateShell = 0x0080, + + All = Run | Process | Shell | Batch | WinPs | Pwsh | IoRedirection | UnicodeConsole | ElevateShell, } } \ No newline at end of file diff --git a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecBatch.cs b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecBatch.cs index 060d64c..1ac5b83 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecBatch.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecBatch.cs @@ -23,6 +23,8 @@ public class NowMsgExecBatch : INowSerialize, INowDeserialize (Directory != null ? MsgFlags.DirectorySet : 0) | (RawEncoding ? MsgFlags.RawEncoding : 0) | (UnicodeConsole ? MsgFlags.UnicodeConsole : 0) | + (Elevated ? MsgFlags.Elevated : 0) | + (NoExit ? MsgFlags.NoExit : 0) | (IoRedirection ? MsgFlags.IoRedirection : 0) | (Detached ? MsgFlags.Detached : 0) ); @@ -54,6 +56,8 @@ static NowMsgExecBatch INowDeserialize.Deserialize(ushort flags msgFlags.HasFlag(MsgFlags.DirectorySet) ? directory : null, msgFlags.HasFlag(MsgFlags.RawEncoding), msgFlags.HasFlag(MsgFlags.UnicodeConsole), + msgFlags.HasFlag(MsgFlags.Elevated), + msgFlags.HasFlag(MsgFlags.NoExit), msgFlags.HasFlag(MsgFlags.IoRedirection), msgFlags.HasFlag(MsgFlags.Detached) ); @@ -87,6 +91,22 @@ private enum MsgFlags : ushort /// UnicodeConsole = 0x0004, + /// + /// Execute the command with elevated privileges. The elevation mechanism is chosen by + /// the host and advertised in execCapset. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_ELEVATED + /// + Elevated = 0x0008, + + /// + /// Keeps the command interpreter running after the batch file completes (cmd /K rather + /// than /C). Ignored when the session redirects stdio. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_NO_EXIT + /// + NoExit = 0x0010, + /// /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -129,6 +149,18 @@ public Builder EnableUnicodeConsole() return this; } + public Builder EnableElevated() + { + _elevated = true; + return this; + } + + public Builder EnableNoExit() + { + _noExit = true; + return this; + } + public Builder EnableDetached() { _detached = true; @@ -137,7 +169,7 @@ public Builder EnableDetached() public NowMsgExecBatch Build() { - return new NowMsgExecBatch(_sessionId, _filename, _directory, _rawEncoding, _unicodeConsole, _ioRedirection, _detached); + return new NowMsgExecBatch(_sessionId, _filename, _directory, _rawEncoding, _unicodeConsole, _elevated, _noExit, _ioRedirection, _detached); } private readonly uint _sessionId = sessionId; @@ -145,17 +177,21 @@ public NowMsgExecBatch Build() private string? _directory = null; private bool _rawEncoding = false; private bool _unicodeConsole = false; + private bool _elevated = false; + private bool _noExit = false; private bool _ioRedirection = false; private bool _detached = false; } - internal NowMsgExecBatch(uint sessionId, string filename, string? directory, bool rawEncoding, bool unicodeConsole, bool ioRedirection, bool detached) + internal NowMsgExecBatch(uint sessionId, string filename, string? directory, bool rawEncoding, bool unicodeConsole, bool elevated, bool noExit, bool ioRedirection, bool detached) { SessionId = sessionId; Filename = filename; Directory = directory; RawEncoding = rawEncoding; UnicodeConsole = unicodeConsole; + Elevated = elevated; + NoExit = noExit; IoRedirection = ioRedirection; Detached = detached; } @@ -165,6 +201,8 @@ internal NowMsgExecBatch(uint sessionId, string filename, string? directory, boo public string? Directory { get; } public bool RawEncoding { get; } public bool UnicodeConsole { get; } + public bool Elevated { get; } + public bool NoExit { get; } public bool IoRedirection { get; } public bool Detached { get; } } diff --git a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecProcess.cs b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecProcess.cs index 39aef3f..e8331f6 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecProcess.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecProcess.cs @@ -23,6 +23,7 @@ public class NowMsgExecProcess : INowSerialize, INowDeserialize.Deserialize(ushort f msgFlags.HasFlag(MsgFlags.ParametersSet) ? parameters : null, msgFlags.HasFlag(MsgFlags.DirectorySet) ? directory : null, msgFlags.HasFlag(MsgFlags.EncodingUtf8), + msgFlags.HasFlag(MsgFlags.Elevated), msgFlags.HasFlag(MsgFlags.IoRedirection), msgFlags.HasFlag(MsgFlags.Detached) ); @@ -89,6 +91,14 @@ private enum MsgFlags : ushort /// EncodingUtf8 = 0x0004, + /// + /// Execute the command with elevated privileges. The elevation mechanism is chosen by + /// the host and advertised in execCapset. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PROCESS_ELEVATED + /// + Elevated = 0x0008, + /// /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -132,6 +142,12 @@ public Builder EnableEncodingUtf8() return this; } + public Builder EnableElevated() + { + _elevated = true; + return this; + } + public Builder EnableDetached() { _detached = true; @@ -140,7 +156,7 @@ public Builder EnableDetached() public NowMsgExecProcess Build() { - return new NowMsgExecProcess(_sessionId, _filename, _parameters, _directory, _encodingUtf8, _ioRedirection, _detached); + return new NowMsgExecProcess(_sessionId, _filename, _parameters, _directory, _encodingUtf8, _elevated, _ioRedirection, _detached); } private readonly uint _sessionId = sessionId; @@ -148,17 +164,19 @@ public NowMsgExecProcess Build() private string? _parameters = null; private string? _directory = null; private bool _encodingUtf8 = false; + private bool _elevated = false; private bool _ioRedirection = false; private bool _detached = false; } - internal NowMsgExecProcess(uint sessionId, string filename, string? parameters, string? directory, bool encodingUtf8, bool ioRedirection, bool detached) + internal NowMsgExecProcess(uint sessionId, string filename, string? parameters, string? directory, bool encodingUtf8, bool elevated, bool ioRedirection, bool detached) { SessionId = sessionId; Filename = filename; Parameters = parameters; Directory = directory; EncodingUtf8 = encodingUtf8; + Elevated = elevated; IoRedirection = ioRedirection; Detached = detached; } @@ -168,6 +186,7 @@ internal NowMsgExecProcess(uint sessionId, string filename, string? parameters, public string? Parameters { get; } public string? Directory { get; } public bool EncodingUtf8 { get; } + public bool Elevated { get; } public bool IoRedirection { get; } public bool Detached { get; } } diff --git a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecPwsh.cs b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecPwsh.cs index 734d848..4cad84e 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecPwsh.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecPwsh.cs @@ -102,6 +102,14 @@ private enum MsgFlags /// UnicodeConsole = 0x0400, + /// + /// Execute the command with elevated privileges. The elevation mechanism is chosen by + /// the host and advertised in execCapset. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PS_ELEVATED + /// + Elevated = 0x0800, + /// /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -218,6 +226,12 @@ public Builder EnableUnicodeConsole() return this; } + public Builder EnableElevated() + { + _flags |= MsgFlags.Elevated; + return this; + } + public Builder EnableDetached() { _flags |= MsgFlags.Detached; @@ -329,6 +343,7 @@ public ApartmentStateKind? ApartmentState public bool IoRedirection => _flags.HasFlag(MsgFlags.IoRedirection); public bool RawEncoding => _flags.HasFlag(MsgFlags.RawEncoding); public bool UnicodeConsole => _flags.HasFlag(MsgFlags.UnicodeConsole); + public bool Elevated => _flags.HasFlag(MsgFlags.Elevated); public bool ServerMode => _flags.HasFlag(MsgFlags.ServerMode); public bool Detached => _flags.HasFlag(MsgFlags.Detached); diff --git a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecRun.cs b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecRun.cs index 610cd2d..fa90fda 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecRun.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecRun.cs @@ -23,7 +23,8 @@ public class NowMsgExecRun : INowSerialize, INowDeserialize // -- INowSerialize -- ushort INowSerialize.Flags => (ushort)( - !string.IsNullOrEmpty(Directory) ? MsgFlags.DirectorySet : 0 + (!string.IsNullOrEmpty(Directory) ? MsgFlags.DirectorySet : 0) | + (Elevated ? MsgFlags.Elevated : 0) ); uint INowSerialize.BodySize => FixedPartSize @@ -60,7 +61,8 @@ NowReadCursor cursor { SessionId = sessionId, Command = command, - Directory = directory + Directory = directory, + Elevated = ((MsgFlags)flags).HasFlag(MsgFlags.Elevated) }; } @@ -71,6 +73,7 @@ public NowMsgExecRun(uint sessionId, string command) SessionId = sessionId; Command = command; Directory = null; + Elevated = false; } private NowMsgExecRun() @@ -78,6 +81,7 @@ private NowMsgExecRun() SessionId = 0; Command = string.Empty; Directory = null; + Elevated = false; } [Flags] @@ -89,6 +93,14 @@ private enum MsgFlags : ushort /// NOW-PROTO: NOW_EXEC_FLAG_RUN_DIRECTORY_SET /// DirectorySet = 0x0001, + + /// + /// Execute the command with elevated privileges. The elevation mechanism is chosen by + /// the host and advertised in execCapset. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_RUN_ELEVATED + /// + Elevated = 0x0002, } private const uint FixedPartSize = 4; // u32 SessionId @@ -101,21 +113,30 @@ public Builder Directory(string directory) return this; } + public Builder EnableElevated() + { + _elevated = true; + return this; + } + public NowMsgExecRun Build() { return new NowMsgExecRun { SessionId = sessionId, Command = command, - Directory = _directory + Directory = _directory, + Elevated = _elevated }; } private string? _directory = null; + private bool _elevated = false; } public uint SessionId { get; private init; } public string Command { get; private init; } public string? Directory { get; private init; } + public bool Elevated { get; private init; } } } \ No newline at end of file diff --git a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecShell.cs b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecShell.cs index 19b2c77..b75f90b 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecShell.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecShell.cs @@ -22,6 +22,7 @@ public class NowMsgExecShell : INowSerialize, INowDeserialize ushort INowSerialize.Flags => (ushort)( (Shell != null ? MsgFlags.ShellSet : 0) | (Directory != null ? MsgFlags.DirectorySet : 0) + | (Elevated ? MsgFlags.Elevated : 0) | (IoRedirection ? MsgFlags.IoRedirection : 0) | (Detached ? MsgFlags.Detached : 0) ); @@ -56,6 +57,7 @@ static NowMsgExecShell INowDeserialize.Deserialize(ushort flags filename, msgFlags.HasFlag(MsgFlags.ShellSet) ? parameters : null, msgFlags.HasFlag(MsgFlags.DirectorySet) ? directory : null, + msgFlags.HasFlag(MsgFlags.Elevated), msgFlags.HasFlag(MsgFlags.IoRedirection), msgFlags.HasFlag(MsgFlags.Detached) ); @@ -80,6 +82,14 @@ private enum MsgFlags : ushort /// DirectorySet = 0x0002, + /// + /// Execute the command with elevated privileges. The elevation mechanism is chosen by + /// the host and advertised in execCapset. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_ELEVATED + /// + Elevated = 0x0008, + /// /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -117,6 +127,12 @@ public Builder EnableIoRedirection() return this; } + public Builder EnableElevated() + { + _elevated = true; + return this; + } + public Builder EnableDetached() { _detached = true; @@ -125,23 +141,25 @@ public Builder EnableDetached() public NowMsgExecShell Build() { - return new NowMsgExecShell(_sessionId, _filename, _shell, _directory, _ioRedirection, _detached); + return new NowMsgExecShell(_sessionId, _filename, _shell, _directory, _elevated, _ioRedirection, _detached); } private readonly uint _sessionId = sessionId; private readonly string _filename = filename; private string? _shell = null; private string? _directory = null; + private bool _elevated = false; private bool _ioRedirection = false; private bool _detached = false; } - internal NowMsgExecShell(uint sessionId, string filename, string? shell, string? directory, bool ioRedirection, bool detached) + internal NowMsgExecShell(uint sessionId, string filename, string? shell, string? directory, bool elevated, bool ioRedirection, bool detached) { SessionId = sessionId; Filename = filename; Shell = shell; Directory = directory; + Elevated = elevated; IoRedirection = ioRedirection; Detached = detached; } @@ -150,6 +168,7 @@ internal NowMsgExecShell(uint sessionId, string filename, string? shell, string? public string Filename { get; } public string? Shell { get; } public string? Directory { get; } + public bool Elevated { get; } public bool IoRedirection { get; } public bool Detached { get; } } diff --git a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecWinPs.cs b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecWinPs.cs index 7e15d10..767cba0 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecWinPs.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecWinPs.cs @@ -102,6 +102,14 @@ private enum MsgFlags /// UnicodeConsole = 0x0400, + /// + /// Execute the command with elevated privileges. The elevation mechanism is chosen by + /// the host and advertised in execCapset. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PS_ELEVATED + /// + Elevated = 0x0800, + /// /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -220,6 +228,12 @@ public Builder EnableUnicodeConsole() return this; } + public Builder EnableElevated() + { + _flags |= MsgFlags.Elevated; + return this; + } + public Builder EnableDetached() { _flags |= MsgFlags.Detached; @@ -331,6 +345,7 @@ public ApartmentStateKind? ApartmentState public bool IoRedirection => _flags.HasFlag(MsgFlags.IoRedirection); public bool RawEncoding => _flags.HasFlag(MsgFlags.RawEncoding); public bool UnicodeConsole => _flags.HasFlag(MsgFlags.UnicodeConsole); + public bool Elevated => _flags.HasFlag(MsgFlags.Elevated); public bool ServerMode => _flags.HasFlag(MsgFlags.ServerMode); public bool Detached => _flags.HasFlag(MsgFlags.Detached); diff --git a/protocols/dotnet/Devolutions.NowProto/src/NowProtoVersion.cs b/protocols/dotnet/Devolutions.NowProto/src/NowProtoVersion.cs index aee9009..50c236d 100644 --- a/protocols/dotnet/Devolutions.NowProto/src/NowProtoVersion.cs +++ b/protocols/dotnet/Devolutions.NowProto/src/NowProtoVersion.cs @@ -2,7 +2,7 @@ { public readonly record struct NowProtoVersion(ushort Major, ushort Minor) : IComparable { - public static NowProtoVersion Current => new(1, 6); + public static NowProtoVersion Current => new(1, 7); // -- IComparable -- public int CompareTo(NowProtoVersion other) diff --git a/protocols/rust/now-proto-pdu/src/channel/capset.rs b/protocols/rust/now-proto-pdu/src/channel/capset.rs index 79472a8..b69f76a 100644 --- a/protocols/rust/now-proto-pdu/src/channel/capset.rs +++ b/protocols/rust/now-proto-pdu/src/channel/capset.rs @@ -93,6 +93,12 @@ bitflags! { /// /// NOW-PROTO: NOW_CAP_EXEC_UNICODE_CONSOLE const UNICODE_CONSOLE = 0x0040; + /// Set if host can elevate an exec session using the platform shell. Elevation may prompt + /// the interactive user for consent, and IO redirection is unavailable for elevated + /// sessions. + /// + /// NOW-PROTO: NOW_CAP_EXEC_ELEVATE_SHELL + const ELEVATE_SHELL = 0x0080; } } @@ -106,7 +112,7 @@ pub struct NowProtoVersion { impl NowProtoVersion { /// Represents the current version of the NOW protocol implemented by the library. - pub const CURRENT: Self = Self { major: 1, minor: 6 }; + pub const CURRENT: Self = Self { major: 1, minor: 7 }; /// Returns `true` if this version supports the encoding control exec flags /// (`NOW_EXEC_FLAG_*_RAW_ENCODING`, `NOW_EXEC_FLAG_*_UNICODE_CONSOLE`, and `NOW_EXEC_FLAG_PROCESS_ENCODING_UTF8`). diff --git a/protocols/rust/now-proto-pdu/src/exec/batch.rs b/protocols/rust/now-proto-pdu/src/exec/batch.rs index d5c8ffb..d889e61 100644 --- a/protocols/rust/now-proto-pdu/src/exec/batch.rs +++ b/protocols/rust/now-proto-pdu/src/exec/batch.rs @@ -27,6 +27,18 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_UNICODE_CONSOLE const UNICODE_CONSOLE = 0x0004; + /// Execute the command with elevated privileges. The elevation mechanism is chosen by the + /// host and advertised in `execCapset`; a host that cannot elevate fails the request + /// instead of silently executing without elevation. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_ELEVATED + const ELEVATED = 0x0008; + /// Keeps the command interpreter running after the batch file completes (`cmd /K` + /// rather than `/C`). MUST be ignored when the session redirects stdio, where the hidden + /// interpreter would never exit. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_NO_EXIT + const NO_EXIT = 0x0010; /// Enable stdio (stdout, stderr, stdin) redirection. /// /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_IO_REDIRECTION @@ -110,6 +122,26 @@ impl<'a> NowExecBatchMsg<'a> { self.flags.contains(NowExecBatchFlags::UNICODE_CONSOLE) } + #[must_use] + pub fn with_elevated(mut self) -> Self { + self.flags |= NowExecBatchFlags::ELEVATED; + self + } + + pub fn is_elevated(&self) -> bool { + self.flags.contains(NowExecBatchFlags::ELEVATED) + } + + #[must_use] + pub fn with_no_exit(mut self) -> Self { + self.flags |= NowExecBatchFlags::NO_EXIT; + self + } + + pub fn is_no_exit(&self) -> bool { + self.flags.contains(NowExecBatchFlags::NO_EXIT) + } + #[must_use] pub fn with_io_redirection(mut self) -> Self { self.flags |= NowExecBatchFlags::IO_REDIRECTION; diff --git a/protocols/rust/now-proto-pdu/src/exec/process.rs b/protocols/rust/now-proto-pdu/src/exec/process.rs index c9afb2f..ed515b3 100644 --- a/protocols/rust/now-proto-pdu/src/exec/process.rs +++ b/protocols/rust/now-proto-pdu/src/exec/process.rs @@ -29,6 +29,12 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_FLAG_PROCESS_ENCODING_UTF8 const ENCODING_UTF8 = 0x0004; + /// Execute the command with elevated privileges. The elevation mechanism is chosen by the + /// host and advertised in `execCapset`; a host that cannot elevate fails the request + /// instead of silently executing without elevation. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PROCESS_ELEVATED + const ELEVATED = 0x0008; /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -106,6 +112,16 @@ impl<'a> NowExecProcessMsg<'a> { Ok(self) } + #[must_use] + pub fn with_elevated(mut self) -> Self { + self.flags |= NowExecProcessFlags::ELEVATED; + self + } + + pub fn is_elevated(&self) -> bool { + self.flags.contains(NowExecProcessFlags::ELEVATED) + } + #[must_use] pub fn with_io_redirection(mut self) -> Self { self.flags |= NowExecProcessFlags::IO_REDIRECTION; diff --git a/protocols/rust/now-proto-pdu/src/exec/pwsh.rs b/protocols/rust/now-proto-pdu/src/exec/pwsh.rs index e2daf0c..6461299 100644 --- a/protocols/rust/now-proto-pdu/src/exec/pwsh.rs +++ b/protocols/rust/now-proto-pdu/src/exec/pwsh.rs @@ -189,6 +189,16 @@ impl<'a> NowExecPwshMsg<'a> { self.flags.contains(NowExecWinPsFlags::UNICODE_CONSOLE) } + #[must_use] + pub fn with_elevated(mut self) -> Self { + self.flags |= NowExecWinPsFlags::ELEVATED; + self + } + + pub fn is_elevated(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::ELEVATED) + } + #[must_use] pub fn with_detached(mut self) -> Self { self.flags |= NowExecWinPsFlags::DETACHED; diff --git a/protocols/rust/now-proto-pdu/src/exec/run.rs b/protocols/rust/now-proto-pdu/src/exec/run.rs index eb3d50b..912ba4f 100644 --- a/protocols/rust/now-proto-pdu/src/exec/run.rs +++ b/protocols/rust/now-proto-pdu/src/exec/run.rs @@ -16,6 +16,12 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_FLAG_RUN_DIRECTORY_SET const DIRECTORY_SET = 0x0001; + /// Execute the command with elevated privileges. The elevation mechanism is chosen by the + /// host and advertised in `execCapset`; a host that cannot elevate fails the request + /// instead of silently executing without elevation. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_RUN_ELEVATED + const ELEVATED = 0x0002; } } @@ -74,6 +80,16 @@ impl<'a> NowExecRunMsg<'a> { Ok(self) } + #[must_use] + pub fn with_elevated(mut self) -> Self { + self.flags |= NowExecRunFlags::ELEVATED; + self + } + + pub fn is_elevated(&self) -> bool { + self.flags.contains(NowExecRunFlags::ELEVATED) + } + pub fn session_id(&self) -> u32 { self.session_id } @@ -102,23 +118,29 @@ impl<'a> NowExecRunMsg<'a> { Self::FIXED_PART_SIZE + self.command.size() + self.directory.size() } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); - let mut flags = NowExecRunFlags::empty(); + // Flags are preserved as sent, otherwise every flag other than DIRECTORY_SET would be + // silently dropped on decode. + let mut flags = NowExecRunFlags::from_bits_retain(header.flags); let session_id = src.read_u32(); let command: NowVarStr<'_> = NowVarStr::decode(src)?; - // Directory field has been added in v1.1. + // Directory field has been added in v1.1, so DIRECTORY_SET is derived from the payload + // instead of being trusted from the header. let directory = if !src.is_empty() { let directory = NowVarStr::decode(src)?; - if !directory.is_empty() { + if directory.is_empty() { + flags.remove(NowExecRunFlags::DIRECTORY_SET); + } else { flags |= NowExecRunFlags::DIRECTORY_SET; } directory } else { + flags.remove(NowExecRunFlags::DIRECTORY_SET); NowVarStr::default() }; diff --git a/protocols/rust/now-proto-pdu/src/exec/shell.rs b/protocols/rust/now-proto-pdu/src/exec/shell.rs index b14622a..47d67b9 100644 --- a/protocols/rust/now-proto-pdu/src/exec/shell.rs +++ b/protocols/rust/now-proto-pdu/src/exec/shell.rs @@ -21,6 +21,12 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_DIRECTORY_SET const DIRECTORY_SET = 0x0002; + /// Execute the command with elevated privileges. The elevation mechanism is chosen by the + /// host and advertised in `execCapset`; a host that cannot elevate fails the request + /// instead of silently executing without elevation. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_ELEVATED + const ELEVATED = 0x0008; /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -133,6 +139,16 @@ impl<'a> NowExecShellMsg<'a> { } } + #[must_use] + pub fn with_elevated(mut self) -> Self { + self.flags |= NowExecShellFlags::ELEVATED; + self + } + + pub fn is_elevated(&self) -> bool { + self.flags.contains(NowExecShellFlags::ELEVATED) + } + #[must_use] pub fn with_io_redirection(mut self) -> Self { self.flags |= NowExecShellFlags::IO_REDIRECTION; diff --git a/protocols/rust/now-proto-pdu/src/exec/win_ps.rs b/protocols/rust/now-proto-pdu/src/exec/win_ps.rs index aa975b0..065351b 100644 --- a/protocols/rust/now-proto-pdu/src/exec/win_ps.rs +++ b/protocols/rust/now-proto-pdu/src/exec/win_ps.rs @@ -64,6 +64,12 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_FLAG_PS_UNICODE_CONSOLE const UNICODE_CONSOLE = 0x0400; + /// Execute the command with elevated privileges. The elevation mechanism is chosen by the + /// host and advertised in `execCapset`; a host that cannot elevate fails the request + /// instead of silently executing without elevation. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PS_ELEVATED + const ELEVATED = 0x0800; /// Enable stdio (stdout, stderr, stdin) redirection. /// @@ -296,6 +302,16 @@ impl<'a> NowExecWinPsMsg<'a> { self.flags.contains(NowExecWinPsFlags::UNICODE_CONSOLE) } + #[must_use] + pub fn with_elevated(mut self) -> Self { + self.flags |= NowExecWinPsFlags::ELEVATED; + self + } + + pub fn is_elevated(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::ELEVATED) + } + #[must_use] pub fn with_detached(mut self) -> Self { self.flags |= NowExecWinPsFlags::DETACHED; diff --git a/protocols/rust/now-proto-testsuite/tests/proto/channel.rs b/protocols/rust/now-proto-testsuite/tests/proto/channel.rs index 01269e4..55608f0 100644 --- a/protocols/rust/now-proto-testsuite/tests/proto/channel.rs +++ b/protocols/rust/now-proto-testsuite/tests/proto/channel.rs @@ -15,7 +15,7 @@ fn roundtrip_channel_capset() { let decoded = now_msg_roundtrip( msg, - expect!["[0E, 00, 00, 00, 10, 01, 01, 00, 01, 00, 06, 00, 01, 00, 04, 00, 05, 00, 2C, 01, 00, 00]"], + expect!["[0E, 00, 00, 00, 10, 01, 01, 00, 01, 00, 07, 00, 01, 00, 04, 00, 05, 00, 2C, 01, 00, 00]"], ); let actual = match decoded { @@ -40,7 +40,7 @@ fn roundtrip_channel_capset_simple() { let decoded = now_msg_roundtrip( msg, - expect!["[0E, 00, 00, 00, 10, 01, 00, 00, 01, 00, 06, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]"], + expect!["[0E, 00, 00, 00, 10, 01, 00, 00, 01, 00, 07, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]"], ); let actual = match decoded { diff --git a/protocols/rust/now-proto-testsuite/tests/proto/exec.rs b/protocols/rust/now-proto-testsuite/tests/proto/exec.rs index 30b514c..aa88f20 100644 --- a/protocols/rust/now-proto-testsuite/tests/proto/exec.rs +++ b/protocols/rust/now-proto-testsuite/tests/proto/exec.rs @@ -444,3 +444,151 @@ fn roundtrip_exec_pwsh_simple() { assert!(actual.execution_policy().is_none()); assert!(actual.configuration_name().is_none()); } + +// Elevation flag round-trips for every exec style. The encoded flags field must carry the +// per-message ELEVATED bit and nothing else, so a host cannot mistake it for another option. + +#[test] +fn roundtrip_exec_run_elevated() { + let msg = NowExecRunMsg::new(0x12345678, "a").unwrap().with_elevated(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[09, 00, 00, 00, 13, 10, 02, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Run(msg)) => msg, + _ => panic!("Expected NowExecRunMsg"), + }; + + assert!(actual.is_elevated()); +} + +#[test] +fn roundtrip_exec_process_elevated() { + let msg = NowExecProcessMsg::new(0x12345678, "a").unwrap().with_elevated(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0B, 00, 00, 00, 13, 11, 08, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Process(msg)) => msg, + _ => panic!("Expected NowExecProcessMsg"), + }; + + assert!(actual.is_elevated()); +} + +#[test] +fn roundtrip_exec_shell_elevated() { + let msg = NowExecShellMsg::new(0x12345678, "a").unwrap().with_elevated(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0B, 00, 00, 00, 13, 12, 08, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Shell(msg)) => msg, + _ => panic!("Expected NowExecShellMsg"), + }; + + assert!(actual.is_elevated()); +} + +#[test] +fn roundtrip_exec_batch_elevated_no_exit() { + let msg = NowExecBatchMsg::new(0x12345678, "a") + .unwrap() + .with_elevated() + .with_no_exit(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[09, 00, 00, 00, 13, 13, 18, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Batch(msg)) => msg, + _ => panic!("Expected NowExecBatchMsg"), + }; + + assert!(actual.is_elevated()); + assert!(actual.is_no_exit()); +} + +#[test] +fn roundtrip_exec_winps_elevated() { + let msg = NowExecWinPsMsg::new(0x12345678, "a").unwrap().with_elevated(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0D, 00, 00, 00, 13, 14, 00, 08, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::WinPs(msg)) => msg, + _ => panic!("Expected NowExecWinPsMsg"), + }; + + assert!(actual.is_elevated()); +} + +#[test] +fn roundtrip_exec_pwsh_elevated() { + let msg = NowExecPwshMsg::new(0x12345678, "a").unwrap().with_elevated(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0D, 00, 00, 00, 13, 15, 00, 08, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Pwsh(msg)) => msg, + _ => panic!("Expected NowExecPwshMsg"), + }; + + assert!(actual.is_elevated()); +} + +// ELEVATED and NO_EXIT are asserted independently as well as together: the combined 0x0018 mask +// alone would stay green if the two constants were swapped. + +#[test] +fn roundtrip_exec_batch_elevated_only() { + let msg = NowExecBatchMsg::new(0x12345678, "a").unwrap().with_elevated(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[09, 00, 00, 00, 13, 13, 08, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Batch(msg)) => msg, + _ => panic!("Expected NowExecBatchMsg"), + }; + + assert!(actual.is_elevated()); + assert!(!actual.is_no_exit()); +} + +#[test] +fn roundtrip_exec_batch_no_exit_only() { + let msg = NowExecBatchMsg::new(0x12345678, "a").unwrap().with_no_exit(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[09, 00, 00, 00, 13, 13, 10, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Batch(msg)) => msg, + _ => panic!("Expected NowExecBatchMsg"), + }; + + assert!(actual.is_no_exit()); + assert!(!actual.is_elevated()); +}