CVE-2024-52010 (Go)

Zoraxy is a general purpose HTTP reverse proxy and forwarding tool. A command injection vulnerability in the Web SSH feature allows an authenticated attacker to execute arbitrary commands as root on the host. Zoraxy has a Web SSH terminal feature that allows authenticated users to connect to SSH servers from their browsers. In HandleCreateProxySession the request to create an SSH session is handled. An attacker can exploit the username variable to escape from the bash command and inject arbitrary commands into sshCommand. This is possible, because, unlike hostname and port, the username is not validated or sanitized.

Weakness:
CWE-94: name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CWE-77: name: Improper Neutralization of Special Elements used in a Command ('Command Injection')
description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
CWE-78: name: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

Repository: https://github.com/tobychui/zoraxy

Vulnerable code:
file_path: src/mod/sshprox/sshprox.go
snippet: func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIpAddr string, remotePort int) error {
	//Create a gotty instance
	connAddr := remoteIpAddr
	if username != "" {
		connAddr = username + "@" + remoteIpAddr
	}
	configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
	title := username + "@" + remoteIpAddr
	if remotePort != 22 {
		title = title + ":" + strconv.Itoa(remotePort)
	}

	sshCommand := []string{"ssh", "-t", connAddr, "-p", strconv.Itoa(remotePort)}
	cmd := exec.Command(i.ExecPath, "-w", "-p", strconv.Itoa(listenPort), "--once", "--config", configPath, "--title-format", title, "bash", "-c", strings.Join(sshCommand, " "))
	cmd.Dir = filepath.Dir(i.ExecPath)
	cmd.Env = append(os.Environ(), "TERM=xterm")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	go func() {
		cmd.Run()
		i.Destroy()
	}()
	i.tty = cmd
	i.AssignedPort = listenPort
	i.RemoteAddr = remoteIpAddr
	i.RemotePort = remotePort

	//Create a new proxy agent for this root
	path, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(listenPort))
	if err != nil {
		return err
	}

	//Create new proxy objects to the proxy
	proxy := reverseproxy.NewReverseProxy(path)

	i.conn = proxy
	return nil
}

Fixed code:
file_path: src/mod/sshprox/sshprox.go
snippet: func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIpAddr string, remotePort int) error {
	//Create a gotty instance
	connAddr := remoteIpAddr
	if username != "" {
		connAddr = username + "@" + remoteIpAddr
	}

	//Trim the space in the username and remote address
	username = strings.TrimSpace(username)
	remoteIpAddr = strings.TrimSpace(remoteIpAddr)

	//Validate the username and remote address
	err := ValidateUsernameAndRemoteAddr(username, remoteIpAddr)
	if err != nil {
		return err
	}

	configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
	title := username + "@" + remoteIpAddr
	if remotePort != 22 {
		title = title + ":" + strconv.Itoa(remotePort)
	}

	sshCommand := []string{"ssh", "-t", connAddr, "-p", strconv.Itoa(remotePort)}
	cmd := exec.Command(i.ExecPath, "-w", "-p", strconv.Itoa(listenPort), "--once", "--config", configPath, "--title-format", title, "bash", "-c", strings.Join(sshCommand, " "))
	cmd.Dir = filepath.Dir(i.ExecPath)
	cmd.Env = append(os.Environ(), "TERM=xterm")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	go func() {
		cmd.Run()
		i.Destroy()
	}()
	i.tty = cmd
	i.AssignedPort = listenPort
	i.RemoteAddr = remoteIpAddr
	i.RemotePort = remotePort

	//Create a new proxy agent for this root
	path, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(listenPort))
	if err != nil {
		return err
	}

	//Create new proxy objects to the proxy
	proxy := reverseproxy.NewReverseProxy(path)

	i.conn = proxy
	return nil
}

file_path: src/mod/sshprox/utils.go
snippet: func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error {
	// Validate and sanitize the username to prevent ssh injection
	validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
	if !validUsername.MatchString(username) {
		return errors.New("invalid username, only alphanumeric characters, dots, underscores and dashes are allowed")
	}

	//Check if the remoteIpAddr is a valid ipv4 or ipv6 address
	if net.ParseIP(remoteIpAddr) != nil {
		//A valid IP address do not need further validation
		return nil
	}

	// Validate and sanitize the remote domain to prevent injection
	validRemoteAddr := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
	if !validRemoteAddr.MatchString(remoteIpAddr) {
		return errors.New("invalid remote address, only alphanumeric characters, dots, underscores and dashes are allowed")
	}

	return nil
}

file_path: src/webssh.go
snippet: func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) {
	//Get what ip address and port to connect to
	ipaddr, err := utils.PostPara(r, "ipaddr")
	if err != nil {
		http.Error(w, "Invalid Usage", http.StatusInternalServerError)
		return
	}

	portString, err := utils.PostPara(r, "port")
	if err != nil {
		portString = "22"
	}

	username, err := utils.PostPara(r, "username")
	if err != nil {
		username = ""
	}

	port, err := strconv.Atoi(portString)
	if err != nil {
		utils.SendErrorResponse(w, "invalid port number given")
		return
	}

	if !*allowSshLoopback {
		//Not allow loopback connections
		if sshprox.IsLoopbackIPOrDomain(ipaddr) {
			//Request target is loopback
			utils.SendErrorResponse(w, "loopback web ssh connection is not enabled on this host")
			return
		}
	}

	//Check if the target is a valid ssh endpoint
	if !sshprox.IsSSHConnectable(ipaddr, port) {
		utils.SendErrorResponse(w, ipaddr+":"+strconv.Itoa(port)+" is not a valid SSH server")
		return
	}

	//Create a new proxy instance
	instance, err := webSshManager.NewSSHProxy("./tmp/gotty")
	if err != nil {
		utils.SendErrorResponse(w, strings.ReplaceAll(err.Error(), "\\", "/"))
		return
	}

	//Create an ssh process to the target address
	err = instance.CreateNewConnection(webSshManager.GetNextPort(), username, ipaddr, port)
	if err != nil {
		utils.SendErrorResponse(w, err.Error())
		return
	}

	//Return the instance uuid
	js, _ := json.Marshal(instance.UUID)
	utils.SendJSONResponse(w, string(js))
}