Local MDM Command Execution

I found a handy module by Michael Niehaus some time ago called LocalMDM. I’ve since converted it to be usable as an ImmyBot module. This allows me to create a task that runs MDM commands just like Intune, without the need to be enrolled.

An unfortunate side-effect is the requirement to have Windows in embedded mode. The script will handle enabling it, however it means that there will be no Microsoft support if there are any issues. This is because embedded mode is technically only supported by IoT Core and IoT Enterprise editions of Windows. Though it has been tested and works on Pro, Education, and Enterprise as well.

Here is the module itself, called LocalMDM:

function Unregister-LocalMDM {
        Unregisters the local MDM server.
        Unregisters the local MDM server.  This will in some cases revert any policies configured via the local MDM server back to their default values.
        A message confirming the unregister operation.
        Invoke-ImmyCommand -Context System -ScriptBlock {
            $rc = [MDMLocal.Interface]::UnregisterDeviceWithLocalManagement()
            Write-Host "Unregisterd, rc = $rc"
        Write-Progress "The device has been unregistered from LocalMDM. Policy enforcement is removed."

Function Send-LocalMDMRequest {
            Sends a SyncML request to the local MDM server.

            This module leverages the Windows MDM local management APIs to directly
            process MDM policy requests (SyncML) on Windows devices. Windows Embedded mode
            will be enabled when this is run. IoT Core and IoT Enterprise are the only 
            operating systems with official support for Embedded mode. It can be enabled on 
            Windows 10/11 Pro, Education, and Enterprise SKUs, but is not officially supported.

        .PARAMETER SyncML
            Specifies the explicit SyncML XML string that should be sent to the local MDM service.

        .PARAMETER OmaUri
            Specifies the OMA-URI path that should be used to construct a SyncML request.

        .PARAMETER Cmd
            Specifies the MDM command that should be used to construct a SyncML request.  Valid values are "Get", "Add", "Atomic", "Delete", "Exec", "Replace", and "Result".  The default is "Get".

        .PARAMETER Format
            Specifies the format of the data value to be included in the SyncML request.  The default value is "int".

        .PARAMETER Type
            Specifies the type of the data value to be included in the SyncML request.  The default value is "text/plain".

        .PARAMETER Data
            Specifies the data to be included in the SyncML request.  This is optional for some requests (e.g. "Get").

        .PARAMETER Raw
            Specifies that the result should be returned as a raw string (exactly as returned by the local MDM service) rather than as a PowerShell object.

            Send-LocalMDMRequest -OmaUri "./DevDetail/Ext/Microsoft/ProcessorArchitecture"

            Send-LocalMDMRequest -OmaUri "./DevDetail/Ext/Microsoft/ProcessorArchitecture"

            The result of the SyncML request.  If -Raw is specified, this will be an XML string.  Otherwise, it will be a PowerShell object.

        [Parameter(ParameterSetName='Raw', Mandatory = $true)]
        [Parameter(ParameterSetName='Assisted', Mandatory = $true)]
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [ValidateSet("Get", "Add", "Atomic", "Delete", "Exec", "Replace", "Result")]
        [String]$Cmd = "Get",
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [String]$Format = "int",
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [String]$Type = "text/plain",
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [String]$Data = "",
        [Switch]$Raw = $false
    BEGIN {
        $source = @"
using System;
using System.Runtime.InteropServices;

namespace MDMLocal
    public class Interface
        [DllImport("mdmlocalmanagement.dll", CharSet = CharSet.Unicode, SetLastError = true)] 
        internal static extern uint RegisterDeviceWithLocalManagement(out uint alreadyRegistered);

        [DllImport("mdmlocalmanagement.dll", CharSet = CharSet.Unicode, SetLastError = true)] 
        public static extern uint UnregisterDeviceWithLocalManagement();

        [DllImport("mdmlocalmanagement.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern uint ApplyLocalManagementSyncML(string syncMLRequest, out IntPtr syncMLResult);

        internal static extern uint LocalFree(IntPtr hMem);

        public static uint Apply(string syncML, out string syncMLResult)
            uint rc;
            uint alreadyRegistered;
            IntPtr resultPtr;

            rc = RegisterDeviceWithLocalManagement(out alreadyRegistered);

            rc = ApplyLocalManagementSyncML(syncML, out resultPtr);
            syncMLResult = "";
            if (resultPtr != null)
                syncMLResult = Marshal.PtrToStringUni(resultPtr);
            return rc;

        Write-Progress "Enabling Windows embedded mode..."
        $uuidBytes = Invoke-ImmyCommand -Context System -ScriptBlock { ([GUID](Get-WMIObject -Class win32_computersystemproduct).UUID).ToByteArray() }
        $hash = Invoke-ImmyCommand -Context System -ScriptBlock { 
            $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256') 
            $hash = $hasher.ComputeHash($($using:uuidBytes))
            return $hash
        Get-WindowsRegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\embeddedmode\Parameters" -Name "Flags" | RegistryShould-Be -Value $hash

        # Invoke-ImmyCommand -Context System -ScriptBlock { 
        #     $uuidBytes = ([GUID](Get-WMIObject -Class win32_computersystemproduct).UUID).ToByteArray()
        #     $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
        #     $hash = $hasher.ComputeHash($uuidBytes)
        #     Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\embeddedmode\Parameters" -Name "Flags" -Value $hash
        # }
        Write-Progress "Targetting $OmaUri"
        if ($Cmd -eq "Get"){
            Write-Progress "Running as a $Cmd command."
        } else {
            Write-Progress "Running as a $Cmd command with the following data: $Data"
        Invoke-ImmyCommand -Context System -ScriptBlock {
            Add-Type -TypeDefinition $($using:source) -Language CSharp
            $global:localMDMCmdCounter = 0

            # Depending on the parameter set, build or use SyncML
            if ($PSCmdlet.ParameterSetName -eq "Raw") {
                $useSyncML = $using:SyncML
            } else {
                $useSyncML = @"
                <Format xmlns="syncml:metinf">$($using:Format)</Format>
                <Type xmlns="syncml:metinf">$($using:Type)</Type>

            # Make sure we have a unique command ID
            [xml] $xml = $useSyncML
            $xml.SyncBody.FirstChild.CmdID = $global:localMDMCmdCounter.ToString()
            $cmdId, $locURI, $updatedSyncML = $global:localMDMCmdCounter, $xml.SyncBody.FirstChild.Item.Target.LocURI, $xml.OuterXml
            # Make a request and check for fatal errors
            $syncMLResultString = ""
            $rc = [MDMLocal.Interface]::Apply($updatedSyncML, [ref]$syncMLResultString)
            if ($rc -eq 2147549446) {
                throw "MDM local management requires running powershell.exe with -MTA."
            } elseif ($rc -eq 2147746132) {
                throw "MDM local management requires a 64-bit process."
            } elseif ($syncMLResultString -like "Error*") {
                throw $syncMLResultString
            } elseif ($rc -ne 0) {
                Write-Host -ForegroundColor Yellow "$syncMLResultString"
                throw "Unexpected return code from MDM local management: $rc"

            # Return the response details (Status of 200 is success)
            if ($($using:Raw)) {
            } else {
                [xml] $syncMLResult = $syncMLResultString
                $status = $syncMLResult.SyncML.SyncBody.Status[1]
                $Result = New-Object PSObject -Property ([ordered] @{
                    CmdId = $cmdId
                    Cmd = $status.Cmd
                    Status = $status.Data
                    OmaUri = $locURI
                    Data = $syncMLResult.SyncML.SyncBody.Results.Item.Data
                Write-Host -Fore Green "The policy has been applied. Result:"
                return $Result

Export-ModuleMember -Function Unregister-LocalMDM,Send-LocalMDMRequest

And the combined script for a task is:

If set to $true, other parameters will be unused.

Import-Module LocalMDM

        $TestResult = Send-LocalMDMRequest -OMAURI $OMAURI -Cmd Get
        Write-Host -Fore DarkGreen $TestResult

        if ($null -eq $TestResult.Data -or $TestResult.Data -ne $DataValue -and $TestResult.Status -eq "200"){
            return $false
            return $true

        return $TestResult.Data
        if ($Unregister){
        if ($null -ne $DataValue -and $SetCmd -eq "Add"){
            $SetCmd = "Replace"
        Send-LocalMDMRequest -OMAURI $OMAURI -Cmd $SetCmd -Data $DataValue

It’s a bit tricky, so there’s some things I’m still working on to smooth it out. It’s great for different types of remote wipes, though. For example, You could add a line in the task to remove the ppkg from C:\Recovery\Customizations and run a protected wipe. Alternatively, you can run a wipe and persist user data if needed. Wipes are a bit weird, so don’t expect the session to end in a compliant state. It’s difficult to determine if the wipe happened or not programmatically.

If you’re interested in this module and how it works, this is where I found it:

1 Like

An example of the parameters used for a remote wipe and persist user data:

Unregister - Leave default (false)

OMAURI - ./Vendor/MSFT/RemoteWipe/doWipePersistUserData
SetCmd - Exec
DataValue - 1

More about the RemoteWipe CSP