I've written a simple go program using YAML and the MySQL drivers with the intention of providing a simple utility to update a database without exposing the username and password credentials to the user executing the program.
(I'm well aware that I could also write this in Python or some other scripting language and manage the permissions delegations using sudo but I'd like to try a different approach here, for my own edification).
After building the program I've used chgrp sys dbcreds.yaml && chmod 0640 dbcreds.yaml
and chgrp sys ./myprog && chmod g+s ./myprog
(as root) ... and everything seems to work. (I also tested that access was denied, as it should be, prior to the setGID step).
I also tested strace
and that results in permission denied (as it should be). (For fun I also ran ltrace -S
on it; this is under Linux. As expected I did not see many normal libc function calls ... through I am surprised to have seen a few pthread_....() and one malloc() calls in that listing. I guess the GO runtime does link to some system library functions after all).
My question: is this safe? Is there any known way to cause a Go program, such as this (below) to core dump or expose its memory after it has read these private credentials? Is there a way to drop my SGID privs after I've read my credentials? Are there any examples of SUID/SGID exploits on Go binaries? Is there a better way to do this? Is there a way to proactive prevent core dumps or ensure that sensitive data (credentials) would not be in core dumps?
One other note: I find the gopkg.in/yaml.v2 semantics to be a bit disconcerting. In my YAML file I have something like:
---
user me
pw mypassword
But in my code I have to use User and Pw (capitalized) rather than using lower case as I would have expected. I presume this is an implementation decision by the authors of Goyaml. Is that so?
#!go
package main
import (
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"strconv"
)
type Creds struct {
User string
Pw string
}
func main() {
filename := "./dbcreds.yaml"
var creds Creds
conf, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
err = yaml.Unmarshal(conf, &creds)
if err != nil {
panic(err)
}
var arg1 int
arg1, err = strconv.Atoi(os.Args[1])
if err != nil {
panic(err.Error()) // Just for example purpose. You should use proper error handling instead of panic
}
fmt.Println("arg1: ", arg1, "\n")
dsn := fmt.Sprintf("%s:%s@/mydatabase", creds.User, creds.Pw)
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err.Error())
}
defer db.Close()
err = db.Ping()
if err != nil {
panic(err.Error())
}
stmtOut, err := db.Prepare("SELECT quant FROM c WHERE id >= ?")
if err != nil {
panic(err.Error())
}
defer stmtOut.Close()
rows, err := stmtOut.Query(arg1)
if err != nil {
panic(err.Error())
}
defer rows.Close()
for rows.Next() {
var quant int
err = rows.Scan(&quant)
if err != nil {
panic(err.Error())
}
fmt.Println(quant)
}
}
A setuid/setgid Go program is reasonably safe, with one major caveat. Go setuid/setgid programs are in general no more, and no less, secure than C/C++ setuid/setgid programs.
It's true that you can force a Go program to dump core by running it with the environment variable GOTRACEBACK=crash and then sending it a signal. However, this is OK for your purposes because the Go program will (try to) create the core dump by sending itself the SIGABRT signal. The kernel will not generate a core dump for a setuid/setgid program killed by a signal.
The major caveat for Go is that on GNU/Linux systems you can not drop back to the original user ID. This is because of how setuid (and setgid, setgroups, setreuid, setregid, setresuid, and setresgid) are implemented for multi-threaded programs on GNU/Linux. The details are at http://golang.org/issue/1435 .
On your final note Uw and Pw need to be capitalized because the standard reflect package does not permit writing to unexported fields.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With