What we want
In addition to allowing to connect to a remote machine, SSH offers several ways to forward tcp ports,
which can be very useful to access a terminal A to access to a server C it cannot reach directly,
provided that it can reach an SSH server that can both speak to A and C, such as in the diagram below:
terminal A <======> server B <======> server C
1.2.3.4 1.2.3.5 & 192.168.2.1
192.168.1.1
ssh client ssh server a server !
a client <---------------------------->
In this diagram,
- A can speak to B
- A cannot speak to C
- A wants to speak to C
- B can speak to C
We want to speak from C to A via the ssh tunnel between A & B.
The most known way to forward TCP ports using SSH is probably through the -L
option, which allows to automagically connect a local port to a remote port via the following syntax:
ssh -L 1234:remote_server:443 user@ssh_server
This command does the following:
- Open an SSH connection to
ssh_server
using theuser
account, - Listen on the 1234 TCP port,
- Forward traffic reaching
localhost:1234
to TCP port 443 ofremote_server
through the SSH tunnel.
Another way to forward TCP ports is via the -D
option, which allows to dynamically forward TCP ports. To sum up, ssh -D 9999 user@ssh_server
will do the following:
- Open an SSH connection to
ssh_server
using theuser
account, - Listen on the 9999 TCP port,
- Forward traffic reaching
localhost:9999
to a dynamically chosen port a of a dynamically chosen server through the SSH tunnel.
The man ssh
command is very useful to better understand what this -D
option does:
-D [bind_address:]port
Specifies a local “dynamic” application-level port forwarding. This works by allocating a socket to listen to port on the local side, optionally bound to the specified bind_address. Whenever
a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine.
Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server. Only root can forward privileged ports. Dynamic port forwardings can also be specified in the
configuration file.
IPv6 addresses can be specified by enclosing the address in square brackets. Only the superuser can forward privileged ports. By default, the local port is bound in accordance with the
GatewayPorts setting. However, an explicit bind_address may be used to bind the connection to a specific address. The bind_address of “localhost” indicates that the listening port be bound
for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces.
TL;DR :
- When launched with
-D 9999
option, the ssh client will act as a socks server (SOCK5 by default) on the client side. - Traffic will be automagically teleported to the SSH server and will be reemitted from it. This magic requires a small tool, proxychains.
Socks protocol 101
SOCKSv5 is described in the RFC 1928.
The introduction of this RFC describes what is SOCKSv5:
« a general framework (…) to transparently and securely traverse a firewall ».
Below in the RFC is described how SOCKSv5 works:
« When a TCP-based client wishes to establish a connection to an object
that is reachable only via a firewall (…), it must open a TCP connection to the
appropriate SOCKS port on the SOCKS server system. »
and:
« If the connection
request succeeds, the client enters a negotiation for the
authentication method to be used, authenticates with the chosen
method, then sends a relay request. The SOCKS server evaluates the
request, and either establishes the appropriate connection or denies
it. »
Ok, let’s try to see how SOCKSv5 packets are made, and what they do.
Client connection
The first SOCKSv5 packet is sent by the client to connect to the server.
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
- The
VER
field is set to0x5
for this version of the protocol. - The
NMETHODS
field contains the number of method identifier octets that appear in theMETHODS
field.
Server response
The message is sent by the server as a response to the client connection request.
The server selects from one of the methods given in `METHODS`, and
sends a `METHOD` selection message:
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 |
+----+--------+
If the selected METHOD is `0xFF`, none of the methods listed by the
client are acceptable, and the client MUST close the connection.
The values currently defined for METHOD are:
o X'00' NO AUTHENTICATION REQUIRED
o X'01' GSSAPI
o X'02' USERNAME/PASSWORD
o X'03' to X'7F' IANA ASSIGNED
o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
o X'FF' NO ACCEPTABLE METHODS
Requests
If the server successfully responsed to the client connection packet,
the client sends a request packet which tells the SOCKSv5 server which address and port it wants to reach.
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
Where:
o VER protocol version: X'05'
o CMD
o CONNECT X'01'
o BIND X'02'
o UDP ASSOCIATE X'03'
o RSV RESERVED
o ATYP address type of following address
o IP V4 address: X'01'
o DOMAINNAME: X'03'
o IP V6 address: X'04'
o DST.ADDR desired destination address
o DST.PORT desired destination port in network octet order
Replies
The server evaluates the request, and
returns a reply formed as follows:
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
Where:
o VER protocol version: X'05'
o REP Reply field:
o X'00' succeeded
o X'01' general SOCKS server failure
o X'02' connection not allowed by ruleset
o X'03' Network unreachable
o X'04' Host unreachable
o X'05' Connection refused
o X'06' TTL expired
o X'07' Command not supported
o X'08' Address type not supported
o X'09' to X'FF' unassigned
o RSV RESERVED
o ATYP address type of following address
o IP V4 address: X'01'
o DOMAINNAME: X'03'
o IP V6 address: X'04'
o BND.ADDR server bound address
o BND.PORT server bound port in network octet order
Fields marked RESERVED (RSV) must be set to X'00'.
UDP case
+----+------+------+----------+----------+----------+
|RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
+----+------+------+----------+----------+----------+
| 2 | 1 | 1 | Variable | 2 | Variable | +----+------+------+----------+----------+----------+
The fields in the UDP request header are:
o RSV Reserved X'0000'
o FRAG Current fragment number
o ATYP address type of following addresses:
o IP V4 address: X'01'
o DOMAINNAME: X'03'
o IP V6 address: X'04'
o DST.ADDR desired destination address
o DST.PORT desired destination port
o DATA user data
How to connect to C from A ?
The first step is to connect to an SSH server with the -D
argument for dynamic port forwarding:
thomas@tolva:~/ ssh -D 9999 my@ssh_server
(...)
Then to reach the web server on 192.168.2.1 through the ssh server, we simply launch the « normal » command but prefixed by proxychains
:
thomas@tolva:~/ proxychains wget https://192.168.2.1
(...)
PROXYCHAINS
How is configured proxychains ?
The default proxychains configuration file looks like this:
# proxychains.conf VER 3.1
#
# HTTP, SOCKS4, SOCKS5 tunneling proxifier with DNS.
#
# The option below identifies how the ProxyList is treated.
# only one option should be uncommented at time,
# otherwise the last appearing option will be accepted
#
#dynamic_chain
#
# Dynamic - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# at least one proxy must be online to play in chain
# (dead proxies are skipped)
# otherwise EINTR is returned to the app
#
strict_chain
#
# Strict - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# all proxies must be online to play in chain
# otherwise EINTR is returned to the app
#
#random_chain
#
# Random - Each connection will be done via random proxy
# (or proxy chain, see chain_len) from the list.
# this option is good to test your IDS :)
# Make sense only if random_chain
#chain_len = 2
# Quiet mode (no output from library)
#quiet_mode
# Proxy DNS requests - no leak for DNS data
proxy_dns
# Some timeouts in milliseconds
tcp_read_time_out 15000
tcp_connect_time_out 8000
# ProxyList format
# type host port [user pass]
# (values separated by 'tab' or 'blank')
#
#
# Examples:
#
# socks5 192.168.67.78 1080 lamer secret
# http 192.168.89.3 8080 justu hidden
# socks4 192.168.1.49 1080
# http 192.168.39.93 8080
#
# (...)
#
[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
#socks4 127.0.0.1 9050
socks5 127.0.0.1 9999
What does proxychains do exactly ?
Code is law ! Let’s have a look at the code from https://github.com/haad/proxychains
usage is something such as:
$ proxychains4 -f /etc/proxychains-other.conf wget https://www.targethost2.com
But by default -f
option is not needed and /etc/proxychains.conf
configuration file will be used.
Where is proxychains ?
thomas@tolva:~$ file $(which proxychains)
/usr/bin/proxychains: symbolic link to /etc/alternatives/proxychains
thomas@tolva:~$ file /etc/alternatives/proxychains
/etc/alternatives/proxychains: symbolic link to /usr/bin/proxychains4
thomas@tolva:~$ file /usr/bin/proxychains4
/usr/bin/proxychains4: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=35739dc2dc6e333399f3b93a3b14a1a89f6e0bdd, for GNU/Linux 3.2.0, (...)
What does it depend on ?
$ ldd /usr/bin/proxychains4
linux-vdso.so.1 (0x00007ffdc771a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff7fb3be000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff7fb5bf000)
Here are the most important parts of proxychains/src/main.c :
(...)
int main(int argc, char *argv[]) {
(...)
const char *prefix = NULL;
for(i = 0; i < MAX_COMMANDLINE_FLAGS; i++) {
if(start_argv < argc && argv[start_argv][0] == '-') {
if(argv[start_argv][1] == 'q') {
quiet = 1;
start_argv++;
} else if(argv[start_argv][1] == 'v') {
printf("Proxychains4 version: %d.%d.%d\n", PROXYCHAINS_VERSION_MAJOR, PROXYCHAINS_VERSION_MINOR, PROXYCHAINS_VERSION_BUGFIX);
exit(EXIT_SUCCESS);
} else if(argv[start_argv][1] == 'f') {
if(start_argv + 1 < argc)
path = argv[start_argv + 1];
else
return usage(argv);
start_argv += 2;
}
} else
break;
}
(...)
/* Set PROXYCHAINS_CONF_FILE to get proxychains lib to use new config file. */
setenv(PROXYCHAINS_CONF_FILE_ENV_VAR, path, 1);
(...)
// search DLL
set_own_dir(argv[0]);
i = 0;
while(dll_dirs[i]) {
snprintf(buf, sizeof(buf), "%s/%s", dll_dirs[i], dll_name);
if(access(buf, R_OK) != -1) {
prefix = dll_dirs[i];
break;
}
i++;
}
(...)
snprintf(buf, sizeof(buf), "%s/%s", prefix, dll_name);
setenv("LD_PRELOAD", buf, 1);
(...)
execvp(argv[start_argv], &argv[start_argv]);
(...)
}
To summarize,
proxychains wget https://192.168.2.1
launches
wget https://192.168.2.1
with /usr/lib/x86_64-linux-gnu/libproxychains.so.4
preloaded via LD_PRELOAD
.
The preloaded library source code is in libproxychains.c.
Here is some commented extracts of its code:
(...)
connect_t true_connect;
gethostbyname_t true_gethostbyname;
getaddrinfo_t true_getaddrinfo;
freeaddrinfo_t true_freeaddrinfo;
getnameinfo_t true_getnameinfo;
gethostbyaddr_t true_gethostbyaddr;
Those true_
functions will be called in hooks:
The proxychained program will use redefined versions of connect
, gethostbyname
and so on.
Those redefined versions are called via the LD_PRELOAD
mechanic,
and are implemented below. Each fake function will in fine call the real one.
Below is a part of the code of the load_sym()
function:
(...)
static void* load_sym(char* symname, void* proxyfunc) {
void *funcptr = dlsym(RTLD_NEXT, symname);
if(!funcptr) {
fprintf(stderr, "Cannot load symbol '%s' %s\n", symname, dlerror());
exit(1);
} else {
PDEBUG("loaded symbol '%s'" " real addr %p wrapped addr %p\n", symname, funcptr, proxyfunc);
}
if(funcptr == proxyfunc) {
PDEBUG("circular reference detected, aborting!\n");
abort();
}
return funcptr;
}
This load_sym
function loads the real versions of hooked functions.
load_sym()
is called via SETUP_SYM
macro,
called via do_init()
function,
called via init_lib_wrapper()
function,
called in INIT()
macro,
called at the begining of each hook.
As an example, let’s have a look to what the fake connect()
does:
(...)
/******* HOOK FUNCTIONS *******/
int connect(int sock, const struct sockaddr *addr, socklen_t len) {
int socktype = 0, flags = 0, ret = 0;
socklen_t optlen = 0;
ip_type dest_ip;
(...)
struct in_addr *p_addr_in;
struct sockaddr_in new_addr;
dnat_arg *dnat = NULL;
unsigned short port;
size_t i;
int remote_dns_connect = 0;
INIT();
optlen = sizeof(socktype);
getsockopt(sock, SOL_SOCKET, SO_TYPE, &socktype, &optlen);
if(!(SOCKFAMILY(*addr) == AF_INET && socktype == SOCK_STREAM))
return true_connect(sock, addr, len);
p_addr_in = &((struct sockaddr_in *) addr)->sin_addr;
port = ntohs(((struct sockaddr_in *) addr)->sin_port);
(...)
// check if connect called from proxydns
remote_dns_connect = (ntohl(p_addr_in->s_addr) >> 24 == remote_dns_subnet);
// more specific first
for(i = 0; i < num_dnats && !remote_dns_connect && !dnat; i++)
if(dnats[i].orig_dst.s_addr == p_addr_in->s_addr)
if(dnats[i].orig_port && (dnats[i].orig_port == port))
dnat = &dnats[i];
for(i = 0; i < num_dnats && !remote_dns_connect && !dnat; i++)
if(dnats[i].orig_dst.s_addr == p_addr_in->s_addr)
if(!dnats[i].orig_port)
dnat = &dnats[i];
if (dnat) {
if (dnat->new_port)
new_addr.sin_port = htons(dnat->new_port);
else
new_addr.sin_port = htons(port);
new_addr.sin_addr = dnat->new_dst;
addr = (struct sockaddr *)&new_addr;
p_addr_in = &((struct sockaddr_in *) addr)->sin_addr;
port = ntohs(((struct sockaddr_in *) addr)->sin_port);
}
for(i = 0; i < num_localnet_addr && !remote_dns_connect; i++) {
if((localnet_addr[i].in_addr.s_addr & localnet_addr[i].netmask.s_addr)
== (p_addr_in->s_addr & localnet_addr[i].netmask.s_addr)) {
if(!localnet_addr[i].port || localnet_addr[i].port == port) {
PDEBUG("accessing localnet using true_connect\n");
return true_connect(sock, addr, len);
}
}
}
flags = fcntl(sock, F_GETFL, 0);
if(flags & O_NONBLOCK)
fcntl(sock, F_SETFL, !O_NONBLOCK);
dest_ip.as_int = SOCKADDR(*addr);
ret = connect_proxy_chain(sock,
dest_ip,
SOCKPORT(*addr),
proxychains_pd, proxychains_proxy_count, proxychains_ct, proxychains_max_chain);
fcntl(sock, F_SETFL, flags);
if(ret != SUCCESS)
errno = ECONNREFUSED;
return ret;
}
To summarize, this function handles several cases where traffic shall not be proxified, and in its main path calls connect_proxy_chain()
.
This function is implemented in core.c. It performs a switch
on the ct
parameter.
int connect_proxy_chain(int sock, ip_type target_ip,
unsigned short target_port, proxy_data * pd,
unsigned int proxy_count, chain_type ct, unsigned int max_chain) {
(...)
switch (ct) {
case DYNAMIC_TYPE:
(...)
case STRICT_TYPE:
(...)
case RANDOM_TYPE:
(...)
}
(...)
}
This ct
parameter is an enum
defined in core.h:
typedef enum {
DYNAMIC_TYPE,
STRICT_TYPE,
RANDOM_TYPE}
chain_type;
Its purpose is to represent how proxies in the proxychains ProxyList are treated.
The possible values in /etc/proxychains.conf
are
dynamic_chain
: proxies are chained in their order of appearance in the config file. At least one proxy shall be alive and dead proxies are skippedstrict_chain
: proxies are chained in their order of appearance in the config file. All of them must be alive.random_chain
: some proxies will be randomly chosen in the config file.
The default value in /etc/proxychains.conf is strict_chain
:
#dynamic_chain
#
# Dynamic - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# at least one proxy must be online to play in chain
# (dead proxies are skipped)
# otherwise EINTR is returned to the app
#
strict_chain
#
# Strict - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# all proxies must be online to play in chain
# otherwise EINTR is returned to the app
#
#random_chain
Thus, *ct
is set to STRICT_TYPE
in the get_chain_data()
function, which is (indirectly) called by the INIT()
macro:
} else if(strstr(buff, "random_chain")) {
*ct = RANDOM_TYPE;
} else if(strstr(buff, "strict_chain")) {
*ct = STRICT_TYPE;
} else if(strstr(buff, "dynamic_chain")) {
*ct = DYNAMIC_TYPE;
Let’s therefore focus on the case STRICT_TYPE
of connect_proxy_chain()
function:
case STRICT_TYPE:
calc_alive(pd, proxy_count);
offset = 0;
if(!(p1 = select_proxy(FIFOLY, pd, proxy_count, &offset))) {
PDEBUG("select_proxy failed\n");
goto error_strict;
}
if(SUCCESS != start_chain(&ns, p1, ST)) {
PDEBUG("start_chain failed\n");
goto error_strict;
}
while(offset < proxy_count) {
if(!(p2 = select_proxy(FIFOLY, pd, proxy_count, &offset)))
break;
if(SUCCESS != chain_step(ns, p1, p2)) {
PDEBUG("chain_step failed\n");
goto error_strict;
}
p1 = p2;
}
//proxychains_write_log(TP);
p3->ip = target_ip;
p3->port = target_port;
if(SUCCESS != chain_step(ns, p1, p3))
goto error;
break;
This piece of code calls the start_chain()
function.
Then a select_proxy()
is called in a loop, until offset
reaches proxy_count
to select the next proxy in the proxychain (offset
is incremented by select_proxy()
).
As i’m not a proxychains power-user, i use the default configuration which only contains one proxy, and the code in this while
loop is executed only one time.
The start_chain()
function calls timed_connect()
function which in its turn calls the true_connect()
function.
The chain_steps()
function is then called. This function itself calls the tunnel_to()
function:
static int chain_step(int ns, proxy_data * pfrom, proxy_data * pto) {
int retcode = -1;
char *hostname;
char ip_buf[16];
PDEBUG("chain_step()\n");
if(pto->ip.octet[0] == remote_dns_subnet) {
hostname = string_from_internal_ip(pto->ip);
if(!hostname)
goto usenumericip;
} else {
usenumericip:
inet_ntop(AF_INET, &pto->ip.octet[0], ip_buf, sizeof(ip_buf));
hostname = ip_buf;
}
proxychains_write_log(TP " %s:%d ", hostname, htons(pto->port));
retcode = tunnel_to(ns, pto->ip, pto->port, pfrom->pt, pfrom->user, pfrom->pass);
switch (retcode) {
case SUCCESS:
pto->ps = BUSY_STATE;
break;
case BLOCKED:
pto->ps = BLOCKED_STATE;
proxychains_write_log("<--denied\n");
close(ns);
break;
case SOCKET_ERROR:
pto->ps = DOWN_STATE;
proxychains_write_log("<--socket error or timeout!\n");
close(ns);
break;
}
return retcode;
}
This tunnel_to()
function contains a switch over pt
, the proxy type.
If the proxy is a socks5 proxy, the relevant switch case is case SOCKS5_TYPE
, and a SOCKS5 packets exchange is made, as seen in the pcap.
Currently, proxychains supports four types of proxy:
typedef enum {
HTTP_TYPE,
RAW_TYPE,
SOCKS4_TYPE,
SOCKS5_TYPE
} proxy_type;
Again, in the default configuration, proxychains will attempt to connect to a SOCKS5 proxy, the pt
argument will therefore be set to SOCKS5_TYPE
.
Here is a sumup of the switch
inside tunnel_to()
:
static int tunnel_to(int sock, ip_type ip, unsigned short port, proxy_type pt, char *user, char *pass) {
(...)
switch (pt) {
(...)
case SOCKS5_TYPE:{
(...)
}
break;
}
err:
return SOCKET_ERROR;
}
In the SOCKS5_TYPE
, the first packet sent to the socks server depends on authentication is used:
case SOCKS5_TYPE:{
if(user) {
buff[0] = 5; //version
buff[1] = 2; //nomber of methods
buff[2] = 0; // no auth method
buff[3] = 2; /// auth method -> username / password
if(4 != write_n_bytes(sock, (char *) buff, 4))
goto err;
} else {
buff[0] = 5; //version
buff[1] = 1; //nomber of methods
buff[2] = 0; // no auth method
if(3 != write_n_bytes(sock, (char *) buff, 3))
goto err;
}
In the simplest configuration, no authentication is done, the if(user)
won’t be True
, and the first packet sent to the socks server will be
b'\x05\x01\x00'
. This is indeed what is observed if we capture the traffic while interacting with an https server through to socks server:
The socks server response (that is to say the response of the SSH client on port 9999) is the following:
The socks server response is therefore b'\x05\x00'
: Server accepts to continue and no authentication is required.
The following part of code in the switch
ensures that this server response is at least two bytes long:
if(2 != read_n_bytes(sock, (char *) buff, 2))
goto err;
Then some checks are performed to ensure the first byte (encoding the protocol version) is set to 0x05
,
and the second byte is set to 0x00
(no authentication) or 0x02
(user/password based authentication):
if(buff[0] != 5 || (buff[1] != 0 && buff[1] != 2)) {
if(buff[0] == 5 && buff[1] == 0xFF)
return BLOCKED;
else
goto err;
}
As we have seen, the packet received from the server is b'\x05\x00
. The next block of code is executed only in case of user/password based authentication, so it will be skipped in our case as no authentication is in place.
The following piece of code to be executed will be this one:
size_t buff_iter = 0;
buff[buff_iter++] = 5; // version
buff[buff_iter++] = 1; // connect
buff[buff_iter++] = 0; // reserved
if(!dns_len) {
buff[buff_iter++] = 1; // ip v4
memcpy(buff + buff_iter, &ip, 4); // dest host
buff_iter += 4;
} else {
buff[buff_iter++] = 3; //dns
buff[buff_iter++] = dns_len & 0xFF;
memcpy(buff + buff_iter, dns_name, dns_len);
buff_iter += dns_len;
}
memcpy(buff + buff_iter, &port, 2); // dest port
buff_iter += 2;
if(buff_iter != write_n_bytes(sock, (char *) buff, buff_iter))
goto err;
As we can see, a buffer having this structure:
- first byte is the version byte (
0x05
) - second byte contains the value
0x01
, which is theCONNECT
method (see analysis of the rfc 1928, above) - third byte is the
RSV
byte, set to0x00
, as described in the rfc - Then comes a byte set to either
0x01
, to indicate an IPv4 address, or0x03
, to indicate a DNS hostname. The following bytes are either the IPv4 address or hostname we want traffic be forwarded to - The last two bytes indicates the TCP port we we want traffic be forwared to.
is sent to the socks server.
The request packet observed in the pcap is b'\x05\x01\x00\x01\xc0\xa8\x02\x01\x01\xbb'
:
The IPv4 embedded in this packet is b\xc0\xa8\x02\x01
, that is to say 192.168.2.1
, and the TCP port is b\x01\xbb
, that is to say 443
, which is consistent with the initial command.
Then, the response from the socks server is read and analysed:
if(4 != read_n_bytes(sock, (char *) buff, 4))
goto err;
if(buff[0] != 5 || buff[1] != 0)
goto err;
switch (buff[3]) {
case 1:
len = 4;
break;
case 4:
len = 16;
break;
case 3:
len = 0;
if(1 != read_n_bytes(sock, (char *) &len, 1))
goto err;
break;
default:
goto err;
}
As previously, the server response shall begin with 0x05
.
Second byte shall be 0x00
(any other value means that connection didn’t succeed),
third byte is ignored (this byte, the RSV
byte, should be set to 0x00
, but the client doesn’t perform any check),
and a switch over buff[3]
(that is to say over the value of ATYP
) is performed.
The allowed values are 0x01
(in case of an IPv4 address), 0x04
(in case of an IPv6 address), and 0x03
(in case of a domain name).
In our study-case, the value 0x01
will be received, which will cause the len
variable to be set to 0x04
.
Finally, a check is performed to ensure that the length of remaining data in the server reply is consistent:
if(len + 2 != read_n_bytes(sock, (char *) buff, len + 2))
goto err;
The last message is the server response who tells it accepts forwarding:
To sum up, the following dialogs takes place between proxychains and the socks server:
- proxychained program sends a connection message
- socks server (that is to say the SSH client) sends back a response
- proxychained program sends a CONNECT request who basically tells « hi, i want my traffic to be forwared to the TCP port 443 of IPv4 address 192.168.2.1 »
- sock server sends back a response telling whether it accepts or not the request.
Once done, if everything went well the traffic of proxychained program will be sent to the 9999 TCP port of SSH client, and will be therefore automagically transmitted to the 192.168.2.1:443 server via the SSH tunnel.
Then next packets exchanged with localhost:9999 after the socks handshake are packets from the TLS handshake:
Conclusion
We’ve seen how proxychains establishes a connection with a SOCKS server, and transparently hooks the real network functions to reroute traffic toward the SOCKS server who automagically forwards it.
But we know there is no such thing as « automagically » ! We will soon see how this magic works in SSH.