mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-20 01:06:45 +00:00
Complete overhaul
This commit is contained in:
290
client/ssh/server/userswitching_windows.go
Normal file
290
client/ssh/server/userswitching_windows.go
Normal file
@@ -0,0 +1,290 @@
|
||||
//go:build windows
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// validateUsername validates Windows usernames according to SAM Account Name rules
|
||||
func validateUsername(username string) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
}
|
||||
|
||||
// Windows SAM Account Name limits: 20 characters for users, 16 for computers
|
||||
// We use 20 as the general limit
|
||||
if len(username) > 20 {
|
||||
return fmt.Errorf("username too long (max 20 characters for Windows)")
|
||||
}
|
||||
|
||||
// Check for Windows SAM Account Name invalid characters
|
||||
// Prohibited: " / \ [ ] : ; | = , + * ? < >
|
||||
invalidChars := []rune{'"', '/', '\\', '[', ']', ':', ';', '|', '=', ',', '+', '*', '?', '<', '>'}
|
||||
for _, char := range username {
|
||||
for _, invalid := range invalidChars {
|
||||
if char == invalid {
|
||||
return fmt.Errorf("username contains invalid character '%c'", char)
|
||||
}
|
||||
}
|
||||
// Check for control characters (ASCII < 32 or == 127)
|
||||
if char < 32 || char == 127 {
|
||||
return fmt.Errorf("username contains control characters")
|
||||
}
|
||||
}
|
||||
|
||||
// Period cannot be the final character
|
||||
if strings.HasSuffix(username, ".") {
|
||||
return fmt.Errorf("username cannot end with a period")
|
||||
}
|
||||
|
||||
// Check for reserved patterns
|
||||
if username == "." || username == ".." {
|
||||
return fmt.Errorf("username cannot be '.' or '..'")
|
||||
}
|
||||
|
||||
// Warn about @ character (causes login issues)
|
||||
if strings.Contains(username, "@") {
|
||||
log.Warnf("username '%s' contains '@' character which may cause login issues", username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSecureUserSwitchCommand creates a command for Windows with user switching support
|
||||
func (s *Server) createSecureUserSwitchCommand(_ []string, localUser *user.User, session ssh.Session) (*exec.Cmd, error) {
|
||||
winCmd, err := s.createUserSwitchCommand(localUser, session, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Windows user switching failed for %s: %w", localUser.Username, err)
|
||||
}
|
||||
return winCmd, nil
|
||||
}
|
||||
|
||||
// createExecutorCommand creates a command using Windows executor for privilege dropping
|
||||
func (s *Server) createExecutorCommand(session ssh.Session, localUser *user.User, hasPty bool) (*exec.Cmd, error) {
|
||||
log.Debugf("creating Windows executor command for user %s (Pty: %v)", localUser.Username, hasPty)
|
||||
|
||||
username, _ := s.parseUsername(localUser.Username)
|
||||
if err := validateUsername(username); err != nil {
|
||||
return nil, fmt.Errorf("invalid username: %w", err)
|
||||
}
|
||||
|
||||
return s.createUserSwitchCommand(localUser, session, hasPty)
|
||||
}
|
||||
|
||||
// createDirectCommand is not supported on Windows - always use user switching with token creation
|
||||
func (s *Server) createDirectCommand(session ssh.Session, localUser *user.User) (*exec.Cmd, error) {
|
||||
return nil, fmt.Errorf("direct command execution not supported on Windows - use user switching with token creation")
|
||||
}
|
||||
|
||||
// createPtyUserSwitchCommand creates a Pty command with user switching for Windows
|
||||
func (s *Server) createPtyUserSwitchCommand(_ []string, localUser *user.User, ptyReq ssh.Pty, session ssh.Session) (*exec.Cmd, error) {
|
||||
return s.createUserSwitchCommand(localUser, session, true)
|
||||
}
|
||||
|
||||
// createSecurePtyUserSwitchCommand creates a Pty command with secure privilege dropping
|
||||
func (s *Server) createSecurePtyUserSwitchCommand([]string, *user.User, ssh.Pty, ssh.Session) (*exec.Cmd, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// createUserSwitchCommand creates a command with Windows user switching
|
||||
func (s *Server) createUserSwitchCommand(localUser *user.User, session ssh.Session, interactive bool) (*exec.Cmd, error) {
|
||||
username, domain := s.parseUsername(localUser.Username)
|
||||
|
||||
shell := getUserShell(localUser.Uid)
|
||||
|
||||
rawCmd := session.RawCommand()
|
||||
var command string
|
||||
if rawCmd != "" {
|
||||
command = rawCmd
|
||||
}
|
||||
|
||||
config := WindowsExecutorConfig{
|
||||
Username: username,
|
||||
Domain: domain,
|
||||
WorkingDir: localUser.HomeDir,
|
||||
Shell: shell,
|
||||
Command: command,
|
||||
Interactive: interactive || (rawCmd == ""),
|
||||
}
|
||||
|
||||
dropper := NewPrivilegeDropper()
|
||||
return dropper.CreateWindowsExecutorCommand(session.Context(), config)
|
||||
}
|
||||
|
||||
// parseUsername extracts username and domain from a Windows username
|
||||
func (s *Server) parseUsername(fullUsername string) (username, domain string) {
|
||||
// Handle DOMAIN\username format
|
||||
if idx := strings.LastIndex(fullUsername, `\`); idx != -1 {
|
||||
domain = fullUsername[:idx]
|
||||
username = fullUsername[idx+1:]
|
||||
return username, domain
|
||||
}
|
||||
|
||||
// Handle username@domain format
|
||||
if idx := strings.Index(fullUsername, "@"); idx != -1 {
|
||||
username = fullUsername[:idx]
|
||||
domain = fullUsername[idx+1:]
|
||||
return username, domain
|
||||
}
|
||||
|
||||
// Local user (no domain)
|
||||
return fullUsername, "."
|
||||
}
|
||||
|
||||
// validateUserSwitchingPrivileges validates Windows-specific user switching privileges
|
||||
// This checks for SeAssignPrimaryTokenPrivilege which is required for CreateProcessWithTokenW
|
||||
func validateUserSwitchingPrivileges() error {
|
||||
process := windows.CurrentProcess()
|
||||
|
||||
var token windows.Token
|
||||
err := windows.OpenProcessToken(
|
||||
process,
|
||||
windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY,
|
||||
&token,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open process token: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := windows.CloseHandle(windows.Handle(token)); err != nil {
|
||||
log.Warnf("close process token: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
hasAssignToken, err := hasPrivilege(windows.Handle(token), "SeAssignPrimaryTokenPrivilege")
|
||||
if err != nil {
|
||||
return fmt.Errorf("has validation: %w", err)
|
||||
}
|
||||
if !hasAssignToken {
|
||||
return ErrPrivilegeRequired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasPrivilege checks if the current process has a specific privilege
|
||||
func hasPrivilege(token windows.Handle, privilegeName string) (bool, error) {
|
||||
var luid windows.LUID
|
||||
if err := windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr(privilegeName), &luid); err != nil {
|
||||
return false, fmt.Errorf("lookup privilege value: %w", err)
|
||||
}
|
||||
|
||||
var returnLength uint32
|
||||
err := windows.GetTokenInformation(
|
||||
windows.Token(token),
|
||||
windows.TokenPrivileges,
|
||||
nil, // null buffer to get size
|
||||
0,
|
||||
&returnLength,
|
||||
)
|
||||
|
||||
if err != nil && !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) {
|
||||
return false, fmt.Errorf("get token information size: %w", err)
|
||||
}
|
||||
|
||||
buffer := make([]byte, returnLength)
|
||||
err = windows.GetTokenInformation(
|
||||
windows.Token(token),
|
||||
windows.TokenPrivileges,
|
||||
&buffer[0],
|
||||
returnLength,
|
||||
&returnLength,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("get token information: %w", err)
|
||||
}
|
||||
|
||||
privileges := (*windows.Tokenprivileges)(unsafe.Pointer(&buffer[0]))
|
||||
|
||||
// Check if the privilege is present and enabled
|
||||
for i := uint32(0); i < privileges.PrivilegeCount; i++ {
|
||||
privilege := (*windows.LUIDAndAttributes)(unsafe.Pointer(
|
||||
uintptr(unsafe.Pointer(&privileges.Privileges[0])) +
|
||||
uintptr(i)*unsafe.Sizeof(windows.LUIDAndAttributes{}),
|
||||
))
|
||||
if privilege.Luid == luid {
|
||||
return (privilege.Attributes & windows.SE_PRIVILEGE_ENABLED) != 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// enablePrivilege enables a specific privilege for the current process token
|
||||
// This is required because privileges like SeAssignPrimaryTokenPrivilege are present
|
||||
// but disabled by default, even for the SYSTEM account
|
||||
func enablePrivilege(token windows.Handle, privilegeName string) error {
|
||||
var luid windows.LUID
|
||||
if err := windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr(privilegeName), &luid); err != nil {
|
||||
return fmt.Errorf("lookup privilege value for %s: %w", privilegeName, err)
|
||||
}
|
||||
|
||||
privileges := windows.Tokenprivileges{
|
||||
PrivilegeCount: 1,
|
||||
Privileges: [1]windows.LUIDAndAttributes{
|
||||
{
|
||||
Luid: luid,
|
||||
Attributes: windows.SE_PRIVILEGE_ENABLED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := windows.AdjustTokenPrivileges(
|
||||
windows.Token(token),
|
||||
false,
|
||||
&privileges,
|
||||
0,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adjust token privileges for %s: %w", privilegeName, err)
|
||||
}
|
||||
|
||||
hasPriv, err := hasPrivilege(token, privilegeName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify privilege %s after enabling: %w", privilegeName, err)
|
||||
}
|
||||
if !hasPriv {
|
||||
return fmt.Errorf("privilege %s could not be enabled (may not be granted to account)", privilegeName)
|
||||
}
|
||||
|
||||
log.Debugf("Successfully enabled privilege %s for current process", privilegeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// enableUserSwitching enables required privileges for Windows user switching
|
||||
func enableUserSwitching() error {
|
||||
process := windows.CurrentProcess()
|
||||
|
||||
var token windows.Token
|
||||
err := windows.OpenProcessToken(
|
||||
process,
|
||||
windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY,
|
||||
&token,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open process token: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := windows.CloseHandle(windows.Handle(token)); err != nil {
|
||||
log.Debugf("Failed to close process token: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := enablePrivilege(windows.Handle(token), "SeAssignPrimaryTokenPrivilege"); err != nil {
|
||||
return fmt.Errorf("enable SeAssignPrimaryTokenPrivilege: %w", err)
|
||||
}
|
||||
log.Infof("Windows user switching privileges enabled successfully")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user