Created
June 29, 2026 05:19
-
-
Save praveenkumar/707930aa9006c1bf30c7d0506d663a1f to your computer and use it in GitHub Desktop.
Drop privileges to the real user
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //go:build darwin || linux | |
| // +build darwin linux | |
| package main | |
| import ( | |
| "bufio" | |
| "fmt" | |
| "io" | |
| "os" | |
| "syscall" | |
| ) | |
| func main() { | |
| if err := run(); err != nil { | |
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
| os.Exit(1) | |
| } | |
| } | |
| func run() error { | |
| // Get the real user ID (the user who invoked sudo/the setuid binary) | |
| // When running as setuid root, getuid() returns 0 (root) | |
| // but we want the original user who invoked the command | |
| realUID := syscall.Getuid() | |
| realGID := syscall.Getgid() | |
| // On macOS/Linux, if this is a setuid binary, we need to get the | |
| // effective UID before it was elevated | |
| // SUDO_UID and SUDO_GID env vars are set by sudo | |
| if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { | |
| fmt.Printf("Running via sudo, original user UID from SUDO_UID: %s\n", sudoUID) | |
| } | |
| euid := syscall.Geteuid() | |
| egid := syscall.Getegid() | |
| fmt.Printf("Real UID: %d, Effective UID: %d\n", realUID, euid) | |
| fmt.Printf("Real GID: %d, Effective GID: %d\n", realGID, egid) | |
| // Step 1: Open /etc/hosts with read/write permissions while privileged | |
| fmt.Println("\n[Step 1] Opening /etc/hosts while privileged...") | |
| hostsFile, err := os.OpenFile("/etc/hosts", os.O_RDWR, 0644) | |
| if err != nil { | |
| return fmt.Errorf("failed to open /etc/hosts (need root): %w", err) | |
| } | |
| defer hostsFile.Close() | |
| // Step 2: Drop privileges to the real user | |
| // IMPORTANT: Drop supplementary groups first, then GID, then UID | |
| fmt.Println("\n[Step 2] Dropping privileges...") | |
| // For a setuid binary, we want to drop to the real user | |
| targetUID := realUID | |
| targetGID := realGID | |
| // If running via sudo, the real UID will be 0, so we can't use it | |
| // In production, you'd parse SUDO_UID/SUDO_GID | |
| if targetUID == 0 { | |
| fmt.Println("Warning: Real UID is 0 (root). In production, parse SUDO_UID.") | |
| fmt.Println("Skipping privilege drop to avoid becoming nobody...") | |
| // Don't drop privileges in this demo if we can't determine the target user | |
| } else { | |
| // Drop supplementary groups | |
| if err := syscall.Setgroups([]int{}); err != nil { | |
| return fmt.Errorf("failed to drop supplementary groups: %w", err) | |
| } | |
| // Drop GID first (must be done before dropping UID on some systems) | |
| if err := syscall.Setgid(targetGID); err != nil { | |
| return fmt.Errorf("failed to setgid(%d): %w", targetGID, err) | |
| } | |
| // Drop UID last | |
| if err := syscall.Setuid(targetUID); err != nil { | |
| return fmt.Errorf("failed to setuid(%d): %w", targetUID, err) | |
| } | |
| // Verify we can't get privileges back | |
| if err := syscall.Setuid(0); err == nil { | |
| return fmt.Errorf("SECURITY ERROR: able to regain root after dropping privileges!") | |
| } | |
| fmt.Printf("Successfully dropped to UID %d, GID %d\n", targetUID, targetGID) | |
| } | |
| // Verify current privileges | |
| newUID := syscall.Getuid() | |
| newEUID := syscall.Geteuid() | |
| fmt.Printf("New Real UID: %d, New Effective UID: %d\n", newUID, newEUID) | |
| // Step 3: Read and parse /etc/hosts using the pre-opened file descriptor | |
| fmt.Println("\n[Step 3] Reading /etc/hosts using pre-opened FD...") | |
| if err := readHostsFile(hostsFile); err != nil { | |
| return fmt.Errorf("failed to read hosts: %w", err) | |
| } | |
| // Step 4: Write to /etc/hosts using the pre-opened file descriptor | |
| fmt.Println("\n[Step 4] Writing to /etc/hosts using pre-opened FD...") | |
| testEntry := "# Test entry added by privilege-drop demo\n" | |
| if err := appendToHosts(hostsFile, testEntry); err != nil { | |
| return fmt.Errorf("failed to write to hosts: %w", err) | |
| } | |
| fmt.Println("Successfully wrote to /etc/hosts after dropping privileges!") | |
| // Step 5: Verify we can't open /etc/hosts anymore (don't have permissions) | |
| fmt.Println("\n[Step 6] Verifying we can't open new privileged files...") | |
| if _, err := os.OpenFile("/etc/hosts", os.O_RDWR, 0644); err != nil { | |
| fmt.Printf("Good: Can't open /etc/hosts anymore: %v\n", err) | |
| } else { | |
| fmt.Println("WARNING: Still able to open /etc/hosts!") | |
| } | |
| return nil | |
| } | |
| func readHostsFile(f *os.File) error { | |
| // Seek to beginning | |
| if _, err := f.Seek(0, io.SeekStart); err != nil { | |
| return err | |
| } | |
| scanner := bufio.NewScanner(f) | |
| lineCount := 0 | |
| for scanner.Scan() { | |
| lineCount++ | |
| if lineCount <= 5 { | |
| fmt.Printf(" Line %d: %s\n", lineCount, scanner.Text()) | |
| } | |
| } | |
| fmt.Printf(" ... (read %d total lines)\n", lineCount) | |
| return scanner.Err() | |
| } | |
| func appendToHosts(f *os.File, entry string) error { | |
| // Seek to end | |
| if _, err := f.Seek(0, io.SeekEnd); err != nil { | |
| return err | |
| } | |
| // Write the entry | |
| if _, err := f.WriteString(entry); err != nil { | |
| return err | |
| } | |
| // Sync to disk | |
| return f.Sync() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment