Linux 二进制打包与权限管理小记

最近由于工作需要,正在探索一些方法,能够让 Linux 上的无 sudo 权限用户能够执行一些需要 sudo 权限才能运行的命令,目前这些命令已经写入了一个 bash 脚本中,理论上一个有权限用户使用 sudo bash test.sh 即可运行。

但出于安全原因,不能赋予某些 Linux 用户 sudo 权限,同时也不希望用户能看到脚本里的内容,所以问题就变得比较复杂了,希望能达到的效果是得到一个二进制文件,这个二进制文件的权限是 711 ,即普通用户只具备执行权限,不具备读写权限。

首先想到的是直接将 bash 脚本赋予 711 权限,然后在 bash 脚本里直接使用 ssh -S 显式接收密码,但实际上这样是不可以的,因为这个用户本身就不可以使用 sudo ,所以这种方法只适用于拥有 sudo 权限的用户。

下面是尝试的各种探索:

基于 python 的 fabric 和 pyinstaller 打包 python 脚本为二进制文件(不可行)

使用 Fabric 框架通过 SSH 连接上具有 sudo 权限的 shell 就可以执行需要管理员权限的 bash 了,然后使用 Pyinstaller 将这个 Python 脚本打包为可执行文件,即可达到需要的效果。

以上是理想的状态,但是在实际的测试过程中,突然意识到了其实 Python 是解释型语言,也就意味着即使是打包后的可执行文件也需要可读权限,而文件具有可读权限也就代表用户可以将其下载下来,进行反编译或者截取 SSH 请求报文等操作,进而破解 SSH 密码,故 此方法不可行 。下面是实现代码:

import logging
from fabric import Connection
from paramiko.ssh_exception import AuthenticationException


logging.getLogger('paramiko').setLevel(logging.WARNING)


def main():
    host = 'root@127.0.0.1'
    password = 'password'
    c = Connection(host, connect_kwargs={"password": password})
    cmd = "ls -a"
    try:
        res = c.sudo(cmd.strip(), password=password, hide=True)
    except AuthenticationException:
        logging.warning(f'认证失败 !')
    if not res.ok:
        raise Exception(f"命令执行出错 : {res}")
    elif res.stderr.strip() != '' and '[sudo] password' not in res.stderr.strip() :
        logging.warning(f'命令出错 : {res.stderr.strip()}')
    else:
        print(res.stdout.strip().replace('[sudo] password:', ''))


if __name__ == "__main__" :
    main()

之后使用 Pyinstaller 打包就可以了:

pyinstaller main.py

基于 visudo 和 shc 打包 bash 脚本为二进制可执行文件(不优雅)

使用 visudo 添加一行,可以实现让指定用户对特定的程序拥有 sudo 权限:

# testuser 对程序 /tmp/test 拥有 sudo 权限,且不需要输入密码就能执行
testuser ALL=(ALL) NOPASSWD: /tmp/test

之后可以安装 shc 直接将 bash 打包为二进制文件

# 安装 shc
sudo apt-get install shc
# 将 script.sh 打包为二进制文件
shc -f script.sh

为生成的 script.sh.x 文件赋予可读与执行权限:

chmod +rx script.sh.x

这样用户就可以直接使用 sudo script.sh.x 执行脚本文件了。这种办法虽然可以达到想要的效果,不需要借助管理员账号,但是却需要使用命令 visudo 去管理 sudo ,如果后续这样的脚本变多了,或者文件迁移,那么维护起来也会比较麻烦,因此这种方法 不够优雅

基于 golang 的 ssh 和 scp 编译为可执行文件(优雅)

通过 Golang 的跨平台编译,可以直接编译一个二进制文件,这个文件通过 SSH 管理员权限账户连接到本地后,可以通过 scp 将字符串中的 bash 脚本写入到临时文件中,之后执行完成这个 bash 脚本再将其删除,最终退出程序,就可以实现想要的效果了。

以下是相关代码:

package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"os"
	"strings"
	"time"

	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
)

type Connection struct {
	*ssh.Client
	password string
}

func Connect(addr, user, password string) (*Connection, error) {
	sshConfig := &ssh.ClientConfig{
		User: user,
		Auth: []ssh.AuthMethod{
			ssh.Password(password),
		},
		HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
	}

	conn, err := ssh.Dial("tcp", addr, sshConfig)
	if err != nil {
		return nil, err
	}

	return &Connection{conn, password}, nil

}

func (conn *Connection) SendCommands(cmds string) ([]byte, error) {
	session, err := conn.NewSession()
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	modes := ssh.TerminalModes{
		ssh.ECHO:          0,     // disable echoing
		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
	}

	err = session.RequestPty("xterm", 80, 40, modes)
	if err != nil {
		return []byte{}, err
	}

	stdoutB := new(bytes.Buffer)
	session.Stdout = stdoutB
	in, _ := session.StdinPipe()

	go func(in io.Writer, output *bytes.Buffer) {
		for {
			if strings.Contains(string(output.Bytes()), "[sudo] password for ") {
				_, err = in.Write([]byte(conn.password + "\n"))
				if err != nil {
					break
				}
				// fmt.Println("put the password ---  end .")
				break
			}
		}
	}(in, stdoutB)

	err = session.Run(cmds)
	if err != nil {
		return []byte{}, err
	}
	return stdoutB.Bytes(), nil
}

func transfer(conn *Connection, filename string, content string) {
	// 创建 SFTP 客户端
	sftpClient, err := sftp.NewClient(conn.Client)
	if err != nil {
		log.Fatalf("Failed to create SFTP client: %s", err)
	}
	defer sftpClient.Close()

	// 将内容写入本地临时文件
	localTmpfile, err := ioutil.TempFile("", "script-*.sh")
	if err != nil {
		log.Fatalf("Failed to create temp file: %s", err)
	}
	defer os.Remove(localTmpfile.Name()) // clean up

	if _, err := localTmpfile.Write([]byte(content)); err != nil {
		log.Fatalf("Failed to write to temp file: %s", err)
	}
	if err := localTmpfile.Close(); err != nil {
		log.Fatalf("Failed to close temp file: %s", err)
	}

	// 打开本地临时文件
	localFile, err := os.Open(localTmpfile.Name())
	if err != nil {
		log.Fatalf("Failed to open temp file: %s", err)
	}
	defer localFile.Close()

	// 在远程服务器上创建目标文件
	remoteFile, err := sftpClient.Create(filename)
	if err != nil {
		log.Fatalf("Failed to create remote file: %s", err)
	}
	defer remoteFile.Close()

	// 将本地文件内容复制到远程文件
	if _, err := remoteFile.ReadFrom(localFile); err != nil {
		log.Fatalf("Failed to copy file contents: %s", err)
	}

	// fmt.Println("File created:", filename)
}

func main() {
	// ssh refers to the custom package above
	conn, err := Connect("127.0.0.1:22", "root", "password")
	if err != nil {
		log.Fatal(err)
	}

	// 生成文件名
	timestamp := time.Now().Format("20060102150405")
	remoteFilename := fmt.Sprintf("/tmp/test_%s.sh", timestamp)

	// 要写入的 bash 文件内容
	scriptContent := `#!/bin/bash
echo "hello world"
`

	transfer(conn, remoteFilename, scriptContent)
	// output, err := conn.SendCommands("sudo bash /tmp/test/clear_cache")
	output, err := conn.SendCommands(fmt.Sprintf("sudo bash %s", remoteFilename))
	if err != nil {
		log.Fatal(err)
	}
	firstNewlineIndex := strings.Index(string(output), "\n")
	if firstNewlineIndex == -1 {
		fmt.Println(string(output))
	} else {
		fmt.Println(string(output)[firstNewlineIndex+1:])
	}
	_, err = conn.SendCommands(fmt.Sprintf("rm %s", remoteFilename))
	if err != nil {
		log.Fatal(err)
	}
}

编译完成后将这个文件赋予 711 权限,即可将文件权限设置为只可以执行,用户无法读取这个文件的内容或者下载文件进行分析。



评论