开源 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
是想避免解释每一个具体的参数是什么,有双引号,有空格,有转义,要准确地解释出每一个参数不简单啊。
大佬们有什么好想法吗?
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 。 |
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 。 |
3
LonnyWong OP @geelaw 因为 command 是未知的,如果你想输入准确的 argv ,那就需要先从一个字符串中解释出准确的 argv 来。
只是简单的空格分隔是不够的,因为有些参数本身可能就存在空格,然后还会有双引号包起来,然后双引号自己又可能会存在转义。如何保证从字符串中解释出来的参数是准确的? |
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 |
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 往返)。 |
6
shimanooo 2023-10-06 23:51:17 +08:00
当你想要解析字符串,而不是结构化地处理的时候,你往往在做错误的事情。
尤其当是这个命令是外部传入的。看看 ImageMagick 2020 年的那个注入漏洞。 |
7
LonnyWong OP @geelaw #5 ssh ProxyCommand 的本意就是原样执行,我抽空看看这个 ShellExecute/CreateProcess 是否适用, 需要操作 stdin 和 stdout 。
|
9
tool2d 2023-10-07 00:12:06 +08:00
动态生成一个临时 bat, 包含空格和双引号,用 cmd /c 调用应该是最简单的方法。
|
10
patrickyoung 2023-10-07 00:17:16 +08:00
这个问题我之前写自己的玩具的时候遇到过,不过是在 linux 平台,我记得是 exec/cmd 的 godoc 里有写一些注意事项。首楼中 issue 提到的配置可用于测试吗?可以的话我试着调调看,有能力的话给你发个 PR
|
11
lianyue 2023-10-07 00:18:18 +08:00
cmd := exec.Command("cmd", "/c", "C:\WINDOWS\System32\OpenSSH\ssh.exe", "-V")
|
12
LonnyWong OP |
14
LonnyWong OP |
15
ysc3839 2023-10-07 03:24:29 +08:00 via Android
Windows 和 Unix 进程的一大区别是,Unix 进程参数是字符串数组,而 Windows 进程参数只是一个字符串。因此 Unix 程序无需自行解析参数,参数是由 shell 解析成字符串数组的,而 Windows 则需要程序自己解析参数成字符串数组。楼上几位似乎都没提到这个根本区别。
@geelaw 有说法称 CommandLineToArgvW 和 MSVC CRT 内置的解析逻辑不同,cmd 的解析逻辑似乎也与前两者不同,我没有实际测试过情况如何,只是提醒一下可能遇到坑。 |
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 ,第一个参数也不一定是程序的名字或者路径。 |
17
Kisesy 2023-10-07 09:12:10 +08:00 1
可以用 golang.org/x/sys/windows 下的 DecomposeCommandLine 函数,内部调用系统的 CommandLineToArgv 函数,所以兼容性非常高
|
18
jorneyr 2023-10-07 09:56:26 +08:00 1
我是把要执行的命令写入 bat / sh 文件,然后执行文件,这样可以方便的支持管道等复杂命令。
|