Hacker News new | past | comments | ask | show | jobs | submit login
Sockets in Your Shell (who23.github.io)
233 points by signa11 on Dec 3, 2020 | hide | past | favorite | 57 comments



  if [ exec 1>/dev/null 2>/dev/null 3>/dev/tcp/localhost/4000 ] ; then
This code runs `[ exec ]` (which tests if "exec" is a non-empty string, thus it returns true) and redirects its output and error to /dev/null, and fd 3 to /dev/tcp/whatever. If the socket opening fails, `[ exec ]` is not even executed, and the overall command returns nonzero. Completely equivalent to `: 3>/dev/tcp/localhost/4000`, which also returns nonzero if the socket can't be opened.

It happens to work for the intended purpose, but not for the reasons the author intended.


A testament to the brokenness of making `[` a command


I prefer to always use the `test` command instead (it’s the exact equivalent of `[`), and never use `[[` bashisms (well, almost never).


And here I thought I was the only one!


I also do this.


shellcheck has this to say:

    In exec.sh line 1:
    if [ exec 1>/dev/null 2>/dev/null 3>/dev/tcp/localhost/4000 ] ; then
    ^-- SC1009: The mentioned syntax error was in this if expression.
       ^-- SC1073: Couldn't parse this test expression. Fix to allow more checks.
         ^-- SC1014: Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.
              ^-- SC1072: Expected test to end here (don't wrap commands in []/[[]]). Fix any mentioned problems and try again.
    
    For more information:
      https://www.shellcheck.net/wiki/SC1014 -- Use 'if cmd; then ..' to check ex...
      https://www.shellcheck.net/wiki/SC1072 -- Expected test to end here (don't ...
      https://www.shellcheck.net/wiki/SC1073 -- Couldn't parse this test expressi...


Hey I'm the author - I did not know about this! I suppose I should have guessed somebody made a shell script linter at some point though. Very useful!


I'm not sure if the code example changed on the linked page after your comment, but I see this code:

  #!/bin/bash
  if exec 3>/dev/tcp/localhost/4000 ; then
 echo "server up!"
  else
 echo "server down."
  fi
Which also confused me for similar reasons to your comment. I'm not familiar with exec, but generally non-zero return means a fail, and zero means success. So why is the "server up" for non-zero result?


Sure enough, I tested this, and the server up/down is reversed from what it should be:

   > exec 3>/dev/tcp/localhost/4000
     bash: connect: Connection refused
     bash: /dev/tcp/localhost/4000: Connection refused
   > echo $?
    1
But if a localhost port is listening (in another terminal window: > nc -l localhost 4000):

   > exec 3>/dev/tcp/localhost/4000
   > echo $?
     0


In the quoted code, "server up" is for a zero result - `if` is reversed from what you think it is:

    > if true; then echo yup; fi
    yup
    > true
    > echo $?
    0


indeed you are right. Thanks for the reply. I'm too accustomed to seeing code like:

  if [ $? -eq 0 ]; then echo "pass";else echo "fail";fi
But without the expression '[ ... ]' shell/bash does indeed treat zero return code (success) as boolean true. For whatever reason I have not seen much (if any) bash code that simply uses:

  if COMMAND; then ...


You really really should use if COMMAND logic, in the event that all you care about is zero or non-zero result. I find it easier to follow for shell control flow.

It also works for shell functions.


From one perspective,

    if [ $? -eq 0 ]; then ...
is actually an instance of

    if COMMAND; then ...
because [ is itself a command.


yep, I think that's the key point I need to drill into my head. The unix/linux idiom where 0 is true/success and non-zero is a failure can be counter-intuitive when combined with "if" (at least for me, hence my mistake reading the code). In most/other computing contexts "if 0" is never taken. I think this might explain why I've seen a lot more bash code that is explicitly written in the form more familiar to "if" usage in other languages (pseudo-code): "if res == 0". But "if COMMAND" is taking care of the interpretation of the return code, so doing the explicit compare is redundant.


Hey I'm the author. Bash treats 0 exit codes as a success, so the "server up" echo runs if the connection is made.


Hi I'm the author! Yes, I wrote this code quite quickly and when I originally posted this on reddit someone pointed out my mistake. The more you know, I suppose.


I stumbled upon Bash network redirections last year and spent some trying to figure out how echoing to /dev/$PROTO/$HOST/$PORT works: https://www.anmolsarma.in/post/bash-net-redirections/


Note that this is a bashism, netcat is (a bit, due to BSD vs GNU) more portable. I think Hurd can provide similar functionality at the filesystem level thanks to its translators. Plan 9 likely can, too, and it would be possible to cook something together with FUSE.

I leveraged this some time ago to sent WOL packets. I think it went something like:

   echo -e [16 times the target MAC written as \xFF] > /dev/udp/255.255.255.255/9
BTW, I'm not entirely sure how broadcast IPv4 addresses work. I think they are set by the DHCP server, and the computer sends packets to these addresses as MAC broadcasts? (leaving the dest IP as the configured broadcast one).


> netcat is (a bit, due to BSD vs GNU) more portable

Note that GNU doesn't have a netcat implementation. There's a netcat implementation that calls itself "GNU netcat", but it isn't actually associated with GNU, and the authors are jerks for naming it that.

But also, there are more netcat implementations than just 2!

- OpenBSD netcat

- "GNU" Netcat (http://netcat.sourceforge.net)

- BusyBox netcat

- Nmap's `ncat`

- Netcat6 (http://www.deepspace6.net/projects/netcat6.html)

And all of them are just different enough from eachother...


Thanks for that tidbit about "GNU" netcat. That's quite strange, I'll look more into this.

I like the fact that Solaris netcat supports socks proxies, that's really useful, especially in combination with `ssh -D`.


On Plan 9 it would be

    {
        netdir=/net/tcp/`{cat}
        echo connect $ip!$port >$netdir/ctl || exit 'cannot connect'
        echo GET / HTTP/1.1 > $netdir/data
        echo > $netdir/data

        cat $netdir/data
    } < /net/tcp/clone
It used for example in IRC client implemented in rc shell http://web.archive.org/web/20060620053701/http://plan9.bell-...


The broadcast address of a given subnet is "!netmask | hostIp" e.g. if the host IP is 192.168.1.12/24 then it's 192.168.1.12 | 0.0.0.255 = 192.168.1.255. The "human" way of thinking about it is it's the last address in the subnet. The IP4 broadcast address for the current network (even if the client doesn't known what it is) is 255.255.255.255.

Once the packet arrives at the destination subnet (which can be immediately or several routing hops away) it will (in the case of Ethernet) you're right that's to the FF:FF:FF:FF:FF:FF destination MAC.

In the case of DHCP client <-> server on different subnets there is a special "DHCP relay" role configured in the network and that relay is what handles broadcast to unicast conversions https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Pro...


this is pretty common in pentesting to get a reverse shell from a system with minimal capabilities

https://github.com/swisskyrepo/PayloadsAllTheThings/blob/mas...


While I generally don’t condone them normally, I’ve used them to debug devops pipelines which didn’t have any other mechanism for access :P


> echo "text!" > /dev/$PROTO/$HOST/$PORT

This being a bash-ism is really misleading. This looks as if my computer has device nodes for $PROTO, and I suppose that's not the case. I think it would make more sense as (optional) /dev nodes giving all programs access to it.


The man page for bash says that /dev/tcp and /dev/udp actually can be real files that any program can use, but that if they're absent, it emulates the behavior on its own.


> The man page for bash says that /dev/tcp and /dev/udp actually can be real files that any program can use

But that's really just a bit of wishful thinking -- those files/directories don't exist on any mainstream UNIX OS I'm aware of.


Hey I'm the author. Yes, as far as I know it's just sugar for opening a socket, and there's almost never a file that exists. To treat it as a file, you need to assign a file descriptor to it, which you can then read and write from.


Actually, I don't have that device in my normal shell, mksh, but the command works in bash? Is bash putting things in /dev? That seems rather... bad, to be honest.


Bash isn't putting anything in /dev. It checks to see if those files exist (I've never seen them actually exist though), and if they aren't there, it just parses the string and creates a network connection as though they did exist.


Bash is not even pretending such special files exist: /dev/tcp/somewhere is only an elegant syntactic shortcut for requesting a telnet-like network connected pipe in a context in which a filename is required. For the child process, it's just a file descriptor.


I don't think it's elegant at all. Magically treating special strings in this manner seems like a violation of "everything is a file".


how is this a bash-ism? you mean it's related to linux /dev tree?


No, it's not related to the actual /dev tree. Bash handles redirects to /dev/tcp/* and /dev/udp/* specially if there are no files at those locations.




This won't enhance security, if your shell allows arbitrary commands. A firewall helps.


Yes! Having netcat or telnet on the system already invalidates the disable


A reduction in malicious avenues is always welcome.


A lot of things to chase though. Lots of installed-by-default stuff like Perl and Gawk do sockets.


More generally, does this mean we can open a socket like a file? For example, in Python, can we do:

    with open('/dev/tcp/127.0.0.1/8000') as f:
       d = f.read()
       ...


No this is a special hack in bash itself, not one of the dumbed-down-plan-9 style abstractions in the linux filesystem.


The post shows use of ztcp, but zsh also has zsocket:

http://zsh.sourceforge.net/Doc/Release/Zsh-Modules.html#The-...

The difference seems to be that zsocket works with Unix domain sockets and, only for incoming connections, sockets already opened and with a file descriptor. I suppose this latter one can be used for any kind of socket if opened before exec'ing zsh.


Hey I'm the author. Yeah, I stumbled on this while doing a little research into zsh socket capabilities, but I decided to exclude it because the post was more about network sockets than Unix domain sockets.


Yeah, when I originally wrote the comment I thought that it was more generic. It was only later that I saw it was more aimed at domain sockets, and edited that in.


Thx for sharing!

Tiny typo:

> For example, if a local build of a web app runs on port 8000 [...]

8000 --> 4000


A few years ago I made a modular IRC bot using ztcp. Probably comes with a bunch of security holes: https://github.com/tiagoad/zshbot/


Nice. Now what I want are higher level protocols than tcp:

    cat /dev/https/news.ycombinator.com | grep -i shell


You might be interested in looking into a fuse based filesystem like httpfs2¹ if you are looking for this sort of capability. The would allows you to mount http/https locations as a filesystem.

¹ http://manpages.ubuntu.com/manpages/latest/en/man1/httpfs2.1...


I know I'm preaching to the choir for this, but it's stuff like this that makes me absolutely adore Unix and its entire design. The fact that virtually everything has a file abstraction greatly simplifies a lot of work.


I think this would be nice to wait for a socket to be available instead of doing `sleep n`.


I had no idea this was possible. The *nix userland just keeps getting cooler the more I learn about it.


Pretty sure this is disabled at compilation time for debian based distros, at least.


It was disabled at one point, but definitely enabled at the moment in buster at least (try docker run -it debian:buster-slim /bin/bash if you don’t have Debian at hand).

Looking at https://sources.debian.org/src/bash/5.0-4/debian/changelog/#...,

  bash (4.0-5) unstable; urgency=low
  
    * Re-add dependency on dash, lost with the upload of 4.0.
    * Don't configure with --disable-net-redirections.
    * Fix name of system wide bash_logout in bash(1). Closes: #546200.
    * Stop shipping the sh and sh(1) symlinks. Closes: #545103.
    * Apply upstream patches 029 - 033.
  
   -- Matthias Klose <doko@debian.org>  Sun, 13 Sep 2009 12:55:54 +0200
it seems net-redirections has been reenabled more than a decade ago.


Using Debian 9.0 "Stretch" derived Bunsen Helium. Its enabled.


Does anyone know the motivation for this in bash? A quick glance at the git history shows this goes all the way back to bash-2.04. As a grumpy old *nix person I can't really I like seeing extra "features" like this.


this is useful for network connectivity troubleshooting in any environment (container, embedded device, and so on) where curl, nc, telnet, etc. are not installed




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: