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());
+}