最近由于工作需要,正在探索一些方法,能够让 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
权限,即可将文件权限设置为只可以执行,用户无法读取这个文件的内容或者下载文件进行分析。
评论