Compare commits

...

1 commit
main ... unroot

Author SHA1 Message Date
Felix Niederwanger 7721f4fcb9
WIP: Add Privilege droppings
This is a work in progress commit. Do not merge!

Adds privilege droppings, such that the main webserver process must not
run as root anymore.
2023-06-05 16:45:41 +02:00
3 changed files with 269 additions and 31 deletions

168
cmd/weblug/child.go Normal file
View file

@ -0,0 +1,168 @@
package main
/* Child process handling */
import (
"fmt"
"os"
"syscall"
)
type Process struct {
pid int // Child PID or 0, if not running
cmd string // Command
argv []string // Arguments
pipeIn int // stdin pipe file descriptor
pipeOut int // stdout pipe file descriptor
pipeErr int // stderr pipe file descriptor
env []string
}
type ProcessPool struct {
Processes []Process
}
// Create a Unix pipe
func pipe() (int, int, error) {
fd := make([]int, 2)
err := syscall.Pipe(fd)
if err != nil {
return 0, 0, err
}
return fd[0], fd[1], nil
}
func CreateProcess(cmd string, argv []string) Process {
child := Process{cmd: cmd, argv: argv}
return child
}
func (c *Process) SetEnv(env []string) {
c.env = env
}
func (c *Process) Close() {
// Close all pipes
if c.pipeIn != 0 {
syscall.Close(c.pipeIn)
c.pipeIn = 0
}
if c.pipeOut != 0 {
syscall.Close(c.pipeOut)
c.pipeOut = 0
}
if c.pipeErr != 0 {
syscall.Close(c.pipeErr)
c.pipeErr = 0
}
}
func (c *Process) Exec() error {
var err error
var (
pInIn, pOutOut, pOutErr int
)
// Create in- and output pipe
pInIn, c.pipeIn, err = pipe()
if err != nil {
return err
}
c.pipeOut, pOutOut, err = pipe()
if err != nil {
return err
}
c.pipeErr, pOutErr, err = pipe()
if err != nil {
return err
}
attr := syscall.ProcAttr{
// Use pipes as stdin, stdout and stderr
Files: []uintptr{uintptr(pInIn), uintptr(pOutOut), uintptr(pOutErr)},
Env: c.env,
}
c.pid, err = syscall.ForkExec(c.cmd, c.argv, &attr)
// Close pipe ends which go to the process
syscall.Close(pInIn)
syscall.Close(pOutOut)
syscall.Close(pOutErr)
if err != nil {
return err
}
return err
}
// Wait waits for the process to complete, returning the return value
func (c *Process) Wait() (int, error) {
if c.pid == 0 {
return 0, fmt.Errorf("no process")
}
proc, err := os.FindProcess(c.pid)
if err != nil {
return 0, err
}
state, err := proc.Wait()
if err != nil {
return 0, err
}
c.pid = 0
return state.ExitCode(), err
}
// Write writes a given buffer to the stdin of the child process
func (c *Process) Write(p []byte) (int, error) {
if c.pid == 0 {
return 0, fmt.Errorf("no process")
}
if c.pipeIn == 0 {
return 0, fmt.Errorf("no pipe")
}
return syscall.Write(c.pipeIn, p)
}
// ReadStdout reads from the stdout of the child process
func (c *Process) ReadStdout(p []byte) (int, error) {
if c.pid == 0 {
return 0, fmt.Errorf("no process")
}
if c.pipeOut == 0 {
return 0, fmt.Errorf("no pipe")
}
return syscall.Read(c.pipeOut, p)
}
// ReadStderr reads from the stderr of the child process
func (c *Process) ReadStderr(p []byte) (int, error) {
if c.pid == 0 {
return 0, fmt.Errorf("no process")
}
if c.pipeErr == 0 {
return 0, fmt.Errorf("no pipe")
}
return syscall.Read(c.pipeErr, p)
}
func (c *Process) Terminate() {
if c.pid != 0 {
c.Close()
syscall.Kill(c.pid, syscall.SIGTERM)
c.pid = 0
}
}
func (c *Process) Abort() {
if c.pid != 0 {
c.Close()
syscall.Kill(c.pid, syscall.SIGABRT)
c.pid = 0
}
}
func (c *Process) Kill() {
if c.pid != 0 {
c.Close()
syscall.Kill(c.pid, syscall.SIGKILL)
c.pid = 0
}
}

View file

@ -1,9 +1,12 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"os/exec"
"strconv"
"strings"
"sync/atomic"
"syscall"
@ -23,6 +26,8 @@ type Hook struct {
BlockedAddresses []string `yaml:"blocked"` // Addresses that are explicitly blocked
HttpBasicAuth BasicAuth `yaml:"basic_auth"` // Optional requires http basic auth
Env map[string]string `yaml:"env"` // Optional environment variables
proc Process // Child process responsible for this webhook
}
type BasicAuth struct {
@ -78,33 +83,6 @@ func cmdSplit(command string) []string {
return ret
}
// Run executes the given command and return it's return code. It also respects the given concurrency number and will block until resources are free
func (hook *Hook) Run() error {
split := cmdSplit(hook.Command)
args := make([]string, 0)
if len(split) > 1 {
args = split[1:]
}
cmd := exec.Command(split[0], args...)
if hook.UID > 0 || hook.GID > 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(hook.UID), Gid: uint32(hook.GID)}
}
cmd.Env = make([]string, 0)
if hook.Env != nil {
// Build environment variable list as expected by cmd.Env
for k, v := range hook.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
if hook.Output {
buf, ret := cmd.Output()
fmt.Println(string(buf))
return ret
}
return cmd.Run()
}
func isAddressInList(addr string, addrList []string) (bool, error) {
ip, _, err := net.ParseCIDR(addr)
if err != nil {
@ -180,3 +158,77 @@ func (hook *Hook) IsAddressAllowed(addr string) (bool, error) {
return true, nil
}
// Run as child process
func hookChildProcessRun() {
var err error
registerTerminationSignal()
if len(os.Args) < 4 {
fmt.Fprintf(os.Stderr, "not enough arguments\n")
os.Exit(1)
}
cmd := os.Args[2]
uid_s := os.Args[3]
gid_s := os.Args[3]
uid, gid := 0, 0
// Drop privileges
if uid_s != "" {
uid, err = strconv.Atoi(uid_s)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid uid\n")
os.Exit(1)
}
}
if gid_s != "" {
gid, err = strconv.Atoi(gid_s)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid gid\n")
os.Exit(1)
}
}
if gid > 0 {
if err := syscall.Setgid(gid); err != nil {
panic(err)
}
}
if uid > 0 {
if err := syscall.Setuid(uid); err != nil {
panic(err)
}
}
// Prepare command
split := cmdSplit(cmd)
args := make([]string, 0)
if len(split) > 1 {
args = split[1:]
}
// Ready to receive the signal to run a webhook
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { // Every newline is a signal, ignore the rest
// Note: Env must have been sanatized before forking child process.
cmd := exec.Command(split[0], args...)
err := cmd.Run()
buf, _ := cmd.Output()
rc := 0
if err != nil {
rc = cmd.ProcessState.ExitCode()
}
// Failed
fmt.Printf("%d %d\n", rc, len(buf))
fmt.Println(buf)
}
}
// Run executes the given command and return it's return code. It also respects the given concurrency number and will block until resources are free
func (hook *Hook) Run() error {
// Trigger webhook child program
if _, err := hook.proc.Write([]byte("\n")); err != nil {
panic(err)
}
}

View file

@ -25,10 +25,10 @@ func usage() {
fmt.Println("The program loads the given yaml files for webhook definitions")
}
// awaits SIGINT or SIGTERM
func awaitTerminationSignal() {
// exit on SIGINT, SIGTERM or SIGABRT
func registerTerminationSignal() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
go func() {
sig := <-sigs
fmt.Println(sig)
@ -56,6 +56,11 @@ func main() {
if len(os.Args) < 2 {
usage()
}
if os.Args[1] == "--hook" {
// Webhook child process handler
hookChildProcessRun()
os.Exit(1)
}
for _, arg := range os.Args[1:] {
if arg == "" {
continue
@ -101,7 +106,7 @@ func main() {
http.HandleFunc(hook.Route, createHandler(hook))
}
awaitTerminationSignal()
registerTerminationSignal()
log.Printf("Launching webserver on %s", cf.Settings.BindAddress)
log.Fatal(http.ListenAndServe(cf.Settings.BindAddress, nil))
@ -109,6 +114,19 @@ func main() {
// create a http handler function from the given hook
func createHandler(hook Hook) Handler {
// Create child process for this webhook
prog := os.Args[0]
hook.proc = CreateProcess(prog, []string{prog, "--hook", hook.Command, fmt.Sprintf("%d", hook.UID), fmt.Sprintf("%d", hook.GID)})
// Build environment variable list as expected by cmd.Env
hook.proc.env = make([]string, 0)
for k, v := range hook.Env {
hook.proc.env = append(hook.proc.env, fmt.Sprintf("%s=%s", k, v))
}
if err := hook.proc.Exec(); err != nil {
// We cannot recover from this error
panic(err)
}
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("GET %s %s", r.RemoteAddr, hook.Name)