V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
LonnyWong
V2EX  ›  程序员

go exec cmd /c 处理空格和双引号问题,大家有什么好办法吗?

  •  
  •   LonnyWong · 2023-10-06 22:17:29 +08:00 · 1818 次点击
    这是一个创建于 412 天前的主题,其中的信息可能已经有所发展或是发生改变。

    开源 issue: https://github.com/trzsz/trzsz-ssh/issues/46

    复现 demo:

    package main
    
    import (
    	"fmt"
    	"os/exec"
    )
    
    func main() {
    	command := `"C:\WINDOWS\System32\OpenSSH\ssh.exe" -V`
    	// command = `"C:\Program Files\ssh.exe" -V`
    	cmd := exec.Command("cmd", "/c", command)
    	output, _ := cmd.CombinedOutput()
    	fmt.Println(string(output))
    }
    

    错误输出:

    '\"C:\WINDOWS\System32\OpenSSH\ssh.exe\"' is not recognized as an internal or external command,
    operable program or batch file.
    

    原始问题,用户在 ~/.ssh/config 中配置任意的 ProxyCommand,要用 os/exec 来执行它,并且获取它的标准输出。用 cmd /c 是想避免解释每一个具体的参数是什么,有双引号,有空格,有转义,要准确地解释出每一个参数不简单啊。

    大佬们有什么好想法吗?

    18 条回复    2023-10-07 09:56:26 +08:00
    geelaw
        1
    geelaw  
       2023-10-06 22:31:26 +08:00 via iPhone
    为什么要多此一举透过 cmd ?另外从 go 的 API 设计可以看出它必须用 Unix 的方式传入 argv ,而不是 Windows 的 command line 。而 cmd 是按照 command line 而不是 argv 读取命令的,因为同一组 argv 有无限种不同的 command line 表示,而且这些会被 cmd 理解为不同的意思,所以不存在可靠的用 go 的 exec 调用 cmd 的方法。

    如果你要调用 ssh 并传入 -V ,可以直接传入合适的 argv 。
    geelaw
        2
    geelaw  
       2023-10-06 22:34:44 +08:00 via iPhone
    例子:
    exec.Command("C:\\WINDOWS\\System32\|OpenSSH\\ssh.exe", "-V")

    go 和 ssh 理应 Windows 上能正确互转 command line 和 argv 。
    LonnyWong
        3
    LonnyWong  
    OP
       2023-10-06 22:46:24 +08:00
    @geelaw 因为 command 是未知的,如果你想输入准确的 argv ,那就需要先从一个字符串中解释出准确的 argv 来。

    只是简单的空格分隔是不够的,因为有些参数本身可能就存在空格,然后还会有双引号包起来,然后双引号自己又可能会存在转义。如何保证从字符串中解释出来的参数是准确的?
    geelaw
        4
    geelaw  
       2023-10-06 23:25:51 +08:00 via iPhone   ❤️ 2
    @LonnyWong Windows 上标准解析命令行的方式是有文档的,https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw

    解析方法如下(我随便瞎写的,不一定准确):
    0. 若字符串只有空格,则设置 argc 为 1 ,argv[0] 为空串,否则转到 1
    1. 去掉开头的空格,若已经没有字符,则结束,否则设置状态为无 quote ,设置当前实参为空字符串,转到 2
    2. 下一个字符
    2.1. 如果现在是无 quote 且这个字符是空格,则把当前实参加入 argv 并转到 1
    2.2. 如果接下来有 2n 个 \ 之后紧跟一个 ",则在当前实参中追加 n 个 \,反转 quote 状态,并转到 2
    2.3. (2n+1) \ + ",追加 n 个 \ 和 1 个 ",转到 2
    2.4. 其他字符,则追加当前字符并转到 2
    3. 把最后一个实参加入 argv
    geelaw
        5
    geelaw  
       2023-10-06 23:32:07 +08:00 via iPhone
    另外 cmd 本身还有自己的内部命令和转义,比如 ^、管道、重定向,除非你的目的就是完全按照 cmd 执行命令,否则用 cmd /c 是错误的。

    如果你想用 cmd /c ,那就不能用 argv 传参,而要用直接可以 ShellExecute/CreateProcess 的 API ,这样才能确保命令行准确传递(而不是经过 argv 互转,这个转换对 command line 来说不“往返”,对 argv 往返)。
    shimanooo
        6
    shimanooo  
       2023-10-06 23:51:17 +08:00
    当你想要解析字符串,而不是结构化地处理的时候,你往往在做错误的事情。

    尤其当是这个命令是外部传入的。看看 ImageMagick 2020 年的那个注入漏洞。
    LonnyWong
        7
    LonnyWong  
    OP
       2023-10-07 00:04:11 +08:00
    @geelaw #5 ssh ProxyCommand 的本意就是原样执行,我抽空看看这个 ShellExecute/CreateProcess 是否适用, 需要操作 stdin 和 stdout 。
    LonnyWong
        8
    LonnyWong  
    OP
       2023-10-07 00:05:28 +08:00
    @shimanooo ssh ProxyCommand 是用户自己配置,自己执行的,要注入也是用户自己搞自己。
    tool2d
        9
    tool2d  
       2023-10-07 00:12:06 +08:00
    动态生成一个临时 bat, 包含空格和双引号,用 cmd /c 调用应该是最简单的方法。
    patrickyoung
        10
    patrickyoung  
       2023-10-07 00:17:16 +08:00
    这个问题我之前写自己的玩具的时候遇到过,不过是在 linux 平台,我记得是 exec/cmd 的 godoc 里有写一些注意事项。首楼中 issue 提到的配置可用于测试吗?可以的话我试着调调看,有能力的话给你发个 PR
    lianyue
        11
    lianyue  
       2023-10-07 00:18:18 +08:00
    cmd := exec.Command("cmd", "/c", "C:\WINDOWS\System32\OpenSSH\ssh.exe", "-V")
    LonnyWong
        12
    LonnyWong  
    OP
       2023-10-07 00:30:37 +08:00
    @patrickyoung 可以测的,也可以直接用 ssh 来测:

    ProxyCommand C:\ssh.exe -W %h:%p jumpserver
    LonnyWong
        13
    LonnyWong  
    OP
       2023-10-07 00:33:53 +08:00
    @tool2d 临时 bat 不知会不会引起杀毒软件的误告。
    LonnyWong
        14
    LonnyWong  
    OP
       2023-10-07 01:31:02 +08:00
    ysc3839
        15
    ysc3839  
       2023-10-07 03:24:29 +08:00 via Android
    Windows 和 Unix 进程的一大区别是,Unix 进程参数是字符串数组,而 Windows 进程参数只是一个字符串。因此 Unix 程序无需自行解析参数,参数是由 shell 解析成字符串数组的,而 Windows 则需要程序自己解析参数成字符串数组。楼上几位似乎都没提到这个根本区别。
    @geelaw 有说法称 CommandLineToArgvW 和 MSVC CRT 内置的解析逻辑不同,cmd 的解析逻辑似乎也与前两者不同,我没有实际测试过情况如何,只是提醒一下可能遇到坑。
    geelaw
        16
    geelaw  
       2023-10-07 03:57:19 +08:00 via iPhone
    @ysc3839 #15 我以为之前已经算是提到了这个区别了。cmd 有自己的转义,但 cmd 当然不负责外部命令如何理解命令行。MSVC CRT 解析 argv 不是 CommandLineToArgvW 我倒是不知道,另外我刚发现 CommandLineToArgvW 读取空格开头的字符串时会把第一个 argv 设置为空串 orz

    除了提醒 lpCmdLine 不需要有 argv 的格式,还应该注意即使 lpCmdLine 解析为 argv ,第一个参数也不一定是程序的名字或者路径。
    Kisesy
        17
    Kisesy  
       2023-10-07 09:12:10 +08:00   ❤️ 1
    可以用 golang.org/x/sys/windows 下的 DecomposeCommandLine 函数,内部调用系统的 CommandLineToArgv 函数,所以兼容性非常高
    jorneyr
        18
    jorneyr  
       2023-10-07 09:56:26 +08:00   ❤️ 1
    我是把要执行的命令写入 bat / sh 文件,然后执行文件,这样可以方便的支持管道等复杂命令。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3934 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 05:25 · PVG 13:25 · LAX 21:25 · JFK 00:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.