mirror of
https://github.com/openssh/openssh-portable.git
synced 2026-01-12 00:04:08 +08:00
upstream: rewrite SOCKS4/4A/5 parsing code to use sshbuf functions
instead of manual pointer fiddling. Should make the code safer and easier to read. feedback/ok markus@ OpenBSD-Commit-ID: 5ebd841fbd78d8395774f002a19c1ddcf91ad047
This commit is contained in:
committed by
Damien Miller
parent
ea367b4bbc
commit
a6f8f793d4
389
channels.c
389
channels.c
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: channels.c,v 1.452 2025/10/07 08:02:32 djm Exp $ */
|
||||
/* $OpenBSD: channels.c,v 1.453 2026/01/04 09:52:58 djm Exp $ */
|
||||
/*
|
||||
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
||||
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
|
||||
@@ -1507,115 +1507,93 @@ channel_pre_mux_client(struct ssh *ssh, Channel *c)
|
||||
}
|
||||
}
|
||||
|
||||
static inline int
|
||||
socks_decode_error(Channel *c, int status, const char *func, const char *msg)
|
||||
{
|
||||
if (status == SSH_ERR_MESSAGE_INCOMPLETE)
|
||||
return 0;
|
||||
else {
|
||||
debug_r(status, "%s: channel %d: decode %s",
|
||||
func, c->self, msg);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* try to decode a socks4 header */
|
||||
static int
|
||||
channel_decode_socks4(Channel *c, struct sshbuf *input, struct sshbuf *output)
|
||||
{
|
||||
const u_char *p;
|
||||
char *host;
|
||||
u_int len, have, i, found, need;
|
||||
char username[256];
|
||||
struct {
|
||||
u_int8_t version;
|
||||
u_int8_t command;
|
||||
u_int16_t dest_port;
|
||||
struct in_addr dest_addr;
|
||||
} s4_req, s4_rsp;
|
||||
int r;
|
||||
uint8_t socks_ver, socks_cmd, dest_addr[4];
|
||||
uint16_t dest_port;
|
||||
char *user = NULL, *host = NULL;
|
||||
int success = -1, socks4a = 0, r;
|
||||
struct sshbuf *b = NULL;
|
||||
|
||||
if (sshbuf_len(input) < 9)
|
||||
return 0;
|
||||
|
||||
/* We may not have a complete message, so work on a dup of the buffer */
|
||||
if ((b = sshbuf_fromb(input)) == NULL)
|
||||
fatal_f("sshbuf_fromb failed");
|
||||
|
||||
debug2("channel %d: decode socks4", c->self);
|
||||
if ((r = sshbuf_get_u8(b, &socks_ver)) != 0 ||
|
||||
(r = sshbuf_get_u8(b, &socks_cmd)) != 0 ||
|
||||
(r = sshbuf_get_u16(b, &dest_port)) != 0 ||
|
||||
(r = sshbuf_get(b, &dest_addr, sizeof(dest_addr))) != 0 ||
|
||||
(r = sshbuf_get_nulterminated_string(b, 1024, &user, NULL)) != 0) {
|
||||
success = socks_decode_error(c, r, __func__, "header");
|
||||
goto out;
|
||||
}
|
||||
|
||||
have = sshbuf_len(input);
|
||||
len = sizeof(s4_req);
|
||||
if (have < len)
|
||||
return 0;
|
||||
p = sshbuf_ptr(input);
|
||||
/* Is this a SOCKS4A request? (indicated by an address of 0.0.0.x) */
|
||||
if (dest_addr[0] == 0 && dest_addr[1] == 0 &&
|
||||
dest_addr[2] == 0 && dest_addr[3] != 0) {
|
||||
/* If so, then the hostname follows, also nul-terminated */
|
||||
if ((r = sshbuf_get_nulterminated_string(b, 1024,
|
||||
&host, NULL)) != 0) {
|
||||
success = socks_decode_error(c, r, __func__, "host");
|
||||
goto out;
|
||||
}
|
||||
socks4a = 1;
|
||||
} else {
|
||||
/* Plain SOCKS4 passes an IPv4 binary address; reconstruct */
|
||||
xasprintf(&host, "%d.%d.%d.%d",
|
||||
dest_addr[0], dest_addr[1], dest_addr[2], dest_addr[3]);
|
||||
}
|
||||
|
||||
need = 1;
|
||||
/* SOCKS4A uses an invalid IP address 0.0.0.x */
|
||||
if (p[4] == 0 && p[5] == 0 && p[6] == 0 && p[7] != 0) {
|
||||
debug2("channel %d: socks4a request", c->self);
|
||||
/* ... and needs an extra string (the hostname) */
|
||||
need = 2;
|
||||
}
|
||||
/* Check for terminating NUL on the string(s) */
|
||||
for (found = 0, i = len; i < have; i++) {
|
||||
if (p[i] == '\0') {
|
||||
found++;
|
||||
if (found == need)
|
||||
break;
|
||||
}
|
||||
if (i > 1024) {
|
||||
/* the peer is probably sending garbage */
|
||||
debug("channel %d: decode socks4: too long",
|
||||
c->self);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (found < need)
|
||||
return 0;
|
||||
if ((r = sshbuf_get(input, &s4_req.version, 1)) != 0 ||
|
||||
(r = sshbuf_get(input, &s4_req.command, 1)) != 0 ||
|
||||
(r = sshbuf_get(input, &s4_req.dest_port, 2)) != 0 ||
|
||||
(r = sshbuf_get(input, &s4_req.dest_addr, 4)) != 0) {
|
||||
debug_r(r, "channels %d: decode socks4", c->self);
|
||||
return -1;
|
||||
}
|
||||
have = sshbuf_len(input);
|
||||
p = sshbuf_ptr(input);
|
||||
if (memchr(p, '\0', have) == NULL) {
|
||||
error("channel %d: decode socks4: unterminated user", c->self);
|
||||
return -1;
|
||||
}
|
||||
len = strlen(p);
|
||||
debug2("channel %d: decode socks4: user %s/%d", c->self, p, len);
|
||||
len++; /* trailing '\0' */
|
||||
strlcpy(username, p, sizeof(username));
|
||||
if ((r = sshbuf_consume(input, len)) != 0)
|
||||
/* We have a complete SOCKS4 message; consume it from input */
|
||||
if ((r = sshbuf_consume_upto_child(input, b)) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
|
||||
/* Handle the request */
|
||||
debug2("channel %d: %s: user=\"%s\" command=%d destination=[%s]:%d",
|
||||
c->self, socks4a ? "SOCKS4A" : "SOCKS4", user, (int)socks_cmd,
|
||||
host, dest_port);
|
||||
if (socks_cmd != 1) {
|
||||
debug("channel %d: cannot handle %s command 0x%02x",
|
||||
c->self, socks4a ? "SOCKS4A" : "SOCKS4", socks_cmd);
|
||||
goto out;
|
||||
}
|
||||
free(c->path);
|
||||
c->path = NULL;
|
||||
if (need == 1) { /* SOCKS4: one string */
|
||||
host = inet_ntoa(s4_req.dest_addr);
|
||||
c->path = xstrdup(host);
|
||||
} else { /* SOCKS4A: two strings */
|
||||
have = sshbuf_len(input);
|
||||
p = sshbuf_ptr(input);
|
||||
if (memchr(p, '\0', have) == NULL) {
|
||||
error("channel %d: decode socks4a: host not nul "
|
||||
"terminated", c->self);
|
||||
return -1;
|
||||
}
|
||||
len = strlen(p);
|
||||
debug2("channel %d: decode socks4a: host %s/%d",
|
||||
c->self, p, len);
|
||||
len++; /* trailing '\0' */
|
||||
if (len > NI_MAXHOST) {
|
||||
error("channel %d: hostname \"%.100s\" too long",
|
||||
c->self, p);
|
||||
return -1;
|
||||
}
|
||||
c->path = xstrdup(p);
|
||||
if ((r = sshbuf_consume(input, len)) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
}
|
||||
c->host_port = ntohs(s4_req.dest_port);
|
||||
c->path = host;
|
||||
host = NULL; /* transferred */
|
||||
c->host_port = dest_port;
|
||||
|
||||
debug2("channel %d: dynamic request: socks4 host %s port %u command %u",
|
||||
c->self, c->path, c->host_port, s4_req.command);
|
||||
/* Reply to the SOCKS4 client */
|
||||
if ((r = sshbuf_put_u8(output, 0)) != 0 || /* vn: 0 for reply */
|
||||
(r = sshbuf_put_u8(output, 90)) != 0 || /* cd: req granted */
|
||||
(r = sshbuf_put_u16(output, 0)) != 0 || /* port: ignored */
|
||||
(r = sshbuf_put_u32(output, ntohl(INADDR_ANY))) != 0) /* ignored */
|
||||
fatal_fr(r, "channel %d: compose reply", c->self);
|
||||
|
||||
if (s4_req.command != 1) {
|
||||
debug("channel %d: cannot handle: %s cn %d",
|
||||
c->self, need == 1 ? "SOCKS4" : "SOCKS4A", s4_req.command);
|
||||
return -1;
|
||||
}
|
||||
s4_rsp.version = 0; /* vn: 0 for reply */
|
||||
s4_rsp.command = 90; /* cd: req granted */
|
||||
s4_rsp.dest_port = 0; /* ignored */
|
||||
s4_rsp.dest_addr.s_addr = INADDR_ANY; /* ignored */
|
||||
if ((r = sshbuf_put(output, &s4_rsp, sizeof(s4_rsp))) != 0)
|
||||
fatal_fr(r, "channel %d: append reply", c->self);
|
||||
return 1;
|
||||
/* success */
|
||||
success = 1;
|
||||
out:
|
||||
sshbuf_free(b);
|
||||
free(user);
|
||||
free(host);
|
||||
return success;
|
||||
}
|
||||
|
||||
/* try to decode a socks5 header */
|
||||
@@ -1627,73 +1605,110 @@ channel_decode_socks4(Channel *c, struct sshbuf *input, struct sshbuf *output)
|
||||
#define SSH_SOCKS5_CONNECT 0x01
|
||||
#define SSH_SOCKS5_SUCCESS 0x00
|
||||
|
||||
/*
|
||||
* Handles SOCKS5 authentication. Note 'b' must be a dup of 'input'
|
||||
* Returns 0 on insufficient queued date, 1 on authentication success or
|
||||
* -1 on error.
|
||||
*/
|
||||
static int
|
||||
channel_socks5_check_auth(Channel *c, struct sshbuf *b, struct sshbuf *input,
|
||||
struct sshbuf *output)
|
||||
{
|
||||
uint8_t socks_ver;
|
||||
uint8_t nmethods, method;
|
||||
int r;
|
||||
u_int i, found;
|
||||
|
||||
/* format: ver | nmethods | methods */
|
||||
if ((r = sshbuf_get_u8(b, &socks_ver)) != 0)
|
||||
return socks_decode_error(c, r, __func__, "version");
|
||||
if (socks_ver != 5) /* shouldn't happen; checked by caller^2 */
|
||||
fatal_fr(r, "channel %d: internal error: not socks5", c->self);
|
||||
if ((r = sshbuf_get_u8(b, &nmethods)) != 0)
|
||||
return socks_decode_error(c, r, __func__, "methods");
|
||||
for (found = i = 0; i < nmethods; i++) {
|
||||
if ((r = sshbuf_get_u8(b, &method)) != 0)
|
||||
return socks_decode_error(c, r, __func__, "method");
|
||||
if (method == SSH_SOCKS5_NOAUTH)
|
||||
found = 1;
|
||||
}
|
||||
if (!found) {
|
||||
debug("channel %d: didn't request SSH_SOCKS5_NOAUTH", c->self);
|
||||
return -1;
|
||||
}
|
||||
/* Consume completed request */
|
||||
if ((r = sshbuf_consume_upto_child(input, b)) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
|
||||
/* Compose reply: version, method */
|
||||
if ((r = sshbuf_put_u8(output, 0x05)) != 0 ||
|
||||
(r = sshbuf_put_u8(output, SSH_SOCKS5_NOAUTH)) != 0)
|
||||
fatal_fr(r, "channel %d: append reply", c->self);
|
||||
/* success */
|
||||
debug2("channel %d: socks5 auth done", c->self);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
channel_decode_socks5(Channel *c, struct sshbuf *input, struct sshbuf *output)
|
||||
{
|
||||
/* XXX use get/put_u8 instead of trusting struct padding */
|
||||
struct {
|
||||
u_int8_t version;
|
||||
u_int8_t command;
|
||||
u_int8_t reserved;
|
||||
u_int8_t atyp;
|
||||
} s5_req, s5_rsp;
|
||||
u_int16_t dest_port;
|
||||
uint8_t socks_ver, socks_cmd, socks_reserved, socks_atyp, addrlen;
|
||||
uint16_t dest_port;
|
||||
char dest_addr[255+1], ntop[INET6_ADDRSTRLEN];
|
||||
const u_char *p;
|
||||
u_int have, need, i, found, nmethods, addrlen, af;
|
||||
int r;
|
||||
u_int af;
|
||||
int r, success = -1;;
|
||||
struct sshbuf *b = NULL;
|
||||
|
||||
debug2("channel %d: decode socks5 %s", c->self,
|
||||
(c->flags & SSH_SOCKS5_AUTHDONE) ? "request" : "auth");
|
||||
|
||||
/* We may not have a complete message, so work on a dup of the buffer */
|
||||
if ((b = sshbuf_fromb(input)) == NULL)
|
||||
fatal_f("sshbuf_fromb failed");
|
||||
|
||||
debug2("channel %d: decode socks5", c->self);
|
||||
p = sshbuf_ptr(input);
|
||||
if (p[0] != 0x05)
|
||||
return -1;
|
||||
have = sshbuf_len(input);
|
||||
if (!(c->flags & SSH_SOCKS5_AUTHDONE)) {
|
||||
/* format: ver | nmethods | methods */
|
||||
if (have < 2)
|
||||
return 0;
|
||||
nmethods = p[1];
|
||||
if (have < nmethods + 2)
|
||||
return 0;
|
||||
/* look for method: "NO AUTHENTICATION REQUIRED" */
|
||||
for (found = 0, i = 2; i < nmethods + 2; i++) {
|
||||
if (p[i] == SSH_SOCKS5_NOAUTH) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
if ((r = channel_socks5_check_auth(c, b, input, output)) != 1) {
|
||||
success = r;
|
||||
goto out;
|
||||
}
|
||||
if (!found) {
|
||||
debug("channel %d: method SSH_SOCKS5_NOAUTH not found",
|
||||
c->self);
|
||||
return -1;
|
||||
}
|
||||
if ((r = sshbuf_consume(input, nmethods + 2)) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
/* version, method */
|
||||
if ((r = sshbuf_put_u8(output, 0x05)) != 0 ||
|
||||
(r = sshbuf_put_u8(output, SSH_SOCKS5_NOAUTH)) != 0)
|
||||
fatal_fr(r, "channel %d: append reply", c->self);
|
||||
c->flags |= SSH_SOCKS5_AUTHDONE;
|
||||
debug2("channel %d: socks5 auth done", c->self);
|
||||
return 0; /* need more */
|
||||
/* Continue to parse request in case client speculated ahead */
|
||||
}
|
||||
|
||||
/* Request messages (auth or connect) always start with the version */
|
||||
if ((r = sshbuf_get_u8(b, &socks_ver)) != 0) {
|
||||
success = socks_decode_error(c, r, __func__, "version");
|
||||
goto out;
|
||||
}
|
||||
if (socks_ver != 5) /* shouldn't happen */
|
||||
fatal_fr(r, "channel %d: internal error: not socks5", c->self);
|
||||
|
||||
/* Parse SOCKS5 request header */
|
||||
debug2("channel %d: socks5 post auth", c->self);
|
||||
if (have < sizeof(s5_req)+1)
|
||||
return 0; /* need more */
|
||||
memcpy(&s5_req, p, sizeof(s5_req));
|
||||
if (s5_req.version != 0x05 ||
|
||||
s5_req.command != SSH_SOCKS5_CONNECT ||
|
||||
s5_req.reserved != 0x00) {
|
||||
debug2("channel %d: only socks5 connect supported", c->self);
|
||||
return -1;
|
||||
if ((r = sshbuf_get_u8(b, &socks_cmd)) != 0 ||
|
||||
(r = sshbuf_get_u8(b, &socks_reserved)) != 0 ||
|
||||
(r = sshbuf_get_u8(b, &socks_atyp)) != 0) {
|
||||
success = socks_decode_error(c, r, __func__, "request header");
|
||||
goto out;
|
||||
}
|
||||
switch (s5_req.atyp){
|
||||
|
||||
if (socks_ver != 0x05 ||
|
||||
socks_cmd != SSH_SOCKS5_CONNECT ||
|
||||
socks_reserved != 0x00) {
|
||||
debug2("channel %d: only socks5 connect supported", c->self);
|
||||
goto out;
|
||||
}
|
||||
|
||||
switch (socks_atyp) {
|
||||
case SSH_SOCKS5_IPV4:
|
||||
addrlen = 4;
|
||||
af = AF_INET;
|
||||
break;
|
||||
case SSH_SOCKS5_DOMAIN:
|
||||
addrlen = p[sizeof(s5_req)];
|
||||
if ((r = sshbuf_get_u8(b, &addrlen)) != 0) {
|
||||
success = socks_decode_error(c, r, __func__, "addrlen");
|
||||
goto out;
|
||||
}
|
||||
af = -1;
|
||||
break;
|
||||
case SSH_SOCKS5_IPV6:
|
||||
@@ -1701,57 +1716,48 @@ channel_decode_socks5(Channel *c, struct sshbuf *input, struct sshbuf *output)
|
||||
af = AF_INET6;
|
||||
break;
|
||||
default:
|
||||
debug2("channel %d: bad socks5 atyp %d", c->self, s5_req.atyp);
|
||||
return -1;
|
||||
debug2("channel %d: bad socks5 atyp %d", c->self, socks_atyp);
|
||||
goto out;
|
||||
}
|
||||
need = sizeof(s5_req) + addrlen + 2;
|
||||
if (s5_req.atyp == SSH_SOCKS5_DOMAIN)
|
||||
need++;
|
||||
if (have < need)
|
||||
return 0;
|
||||
if ((r = sshbuf_consume(input, sizeof(s5_req))) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
if (s5_req.atyp == SSH_SOCKS5_DOMAIN) {
|
||||
/* host string length */
|
||||
if ((r = sshbuf_consume(input, 1)) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
}
|
||||
if ((r = sshbuf_get(input, &dest_addr, addrlen)) != 0 ||
|
||||
(r = sshbuf_get(input, &dest_port, 2)) != 0) {
|
||||
debug_r(r, "channel %d: parse addr/port", c->self);
|
||||
return -1;
|
||||
if ((r = sshbuf_get(b, &dest_addr, addrlen)) != 0 ||
|
||||
(r = sshbuf_get_u16(b, &dest_port)) != 0) {
|
||||
success = socks_decode_error(c, r, __func__, "addr/port");
|
||||
goto out;
|
||||
}
|
||||
dest_addr[addrlen] = '\0';
|
||||
|
||||
/* We have a complete SOCKS5 request; consume it from input */
|
||||
if ((r = sshbuf_consume_upto_child(input, b)) != 0)
|
||||
fatal_fr(r, "channel %d: consume", c->self);
|
||||
|
||||
free(c->path);
|
||||
c->path = NULL;
|
||||
if (s5_req.atyp == SSH_SOCKS5_DOMAIN) {
|
||||
if (addrlen >= NI_MAXHOST) {
|
||||
error("channel %d: dynamic request: socks5 hostname "
|
||||
"\"%.100s\" too long", c->self, dest_addr);
|
||||
return -1;
|
||||
}
|
||||
if (socks_atyp == SSH_SOCKS5_DOMAIN)
|
||||
c->path = xstrdup(dest_addr);
|
||||
} else {
|
||||
else {
|
||||
if (inet_ntop(af, dest_addr, ntop, sizeof(ntop)) == NULL)
|
||||
return -1;
|
||||
c->path = xstrdup(ntop);
|
||||
}
|
||||
c->host_port = ntohs(dest_port);
|
||||
c->host_port = dest_port;
|
||||
|
||||
debug2("channel %d: dynamic request: socks5 host %s port %u command %u",
|
||||
c->self, c->path, c->host_port, s5_req.command);
|
||||
c->self, c->path, c->host_port, socks_cmd);
|
||||
|
||||
s5_rsp.version = 0x05;
|
||||
s5_rsp.command = SSH_SOCKS5_SUCCESS;
|
||||
s5_rsp.reserved = 0; /* ignored */
|
||||
s5_rsp.atyp = SSH_SOCKS5_IPV4;
|
||||
dest_port = 0; /* ignored */
|
||||
|
||||
if ((r = sshbuf_put(output, &s5_rsp, sizeof(s5_rsp))) != 0 ||
|
||||
(r = sshbuf_put_u32(output, ntohl(INADDR_ANY))) != 0 ||
|
||||
(r = sshbuf_put(output, &dest_port, sizeof(dest_port))) != 0)
|
||||
/* Reply */
|
||||
if ((r = sshbuf_put_u8(output, 0x05)) != 0 || /* version */
|
||||
(r = sshbuf_put_u8(output, SSH_SOCKS5_SUCCESS)) != 0 || /* cmd */
|
||||
(r = sshbuf_put_u8(output, 0)) != 0 || /* reserved, ignored */
|
||||
(r = sshbuf_put_u8(output, SSH_SOCKS5_IPV4)) != 0 || /* addrtype */
|
||||
(r = sshbuf_put_u32(output, ntohl(INADDR_ANY))) != 0 || /* addr */
|
||||
(r = sshbuf_put_u16(output, dest_port)) != 0) /* port */
|
||||
fatal_fr(r, "channel %d: append reply", c->self);
|
||||
return 1;
|
||||
|
||||
/* success */
|
||||
success = 1;
|
||||
out:
|
||||
sshbuf_free(b);
|
||||
return success;
|
||||
}
|
||||
|
||||
Channel *
|
||||
@@ -1783,9 +1789,9 @@ channel_connect_stdio_fwd(struct ssh *ssh,
|
||||
static void
|
||||
channel_pre_dynamic(struct ssh *ssh, Channel *c)
|
||||
{
|
||||
const u_char *p;
|
||||
u_int have;
|
||||
int ret;
|
||||
u_char ver;
|
||||
int r, ret;
|
||||
|
||||
c->io_want = 0;
|
||||
have = sshbuf_len(c->input);
|
||||
@@ -1798,9 +1804,9 @@ channel_pre_dynamic(struct ssh *ssh, Channel *c)
|
||||
return;
|
||||
}
|
||||
/* try to guess the protocol */
|
||||
p = sshbuf_ptr(c->input);
|
||||
/* XXX sshbuf_peek_u8? */
|
||||
switch (p[0]) {
|
||||
if ((r = sshbuf_peek_u8(c->input, 0, &ver)) != 0)
|
||||
fatal_fr(r, "sshbuf_peek_u8");
|
||||
switch (ver) {
|
||||
case 0x04:
|
||||
ret = channel_decode_socks4(c, c->input, c->output);
|
||||
break;
|
||||
@@ -1808,6 +1814,7 @@ channel_pre_dynamic(struct ssh *ssh, Channel *c)
|
||||
ret = channel_decode_socks5(c, c->input, c->output);
|
||||
break;
|
||||
default:
|
||||
debug2_f("channel %d: unknown SOCKS version %u", c->self, ver);
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user