CVE-2020-15121 (C, published 2020-07-20T18:15Z)
Description: value: In radare2 before version 4.5.0, malformed PDB file names in the PDB server path cause shell injection. To trigger the problem it's required to open the executable in radare2 and run idpd to trigger the download. The shell code will execute, and will create a file called pwned in the current directory.
Severity: MEDIUM (CVSS v3 base score: 9.6)
Weakness: CWE-78 \u2014 Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
Repository: https://github.com/radareorg/radare2
Files changed:
libr/bin/pdb/pdb_downloader.c
libr/config.mk.tail
libr/include/r_socket.h
libr/include/r_types.h
libr/include/r_util/r_str.h
libr/socket/socket.c
libr/socket/socket_http.c
libr/util/str.c
libr/util/sys.c
meson.build
shlr/sdb/src/disk.c
test/db/formats/pdb
test/unit/test_str.c
Commit message: Fix command injection on PDB download (#16966)

* Fix r_sys_mkdirp with absolute path on Windows
* Fix build with --with-openssl
* Use RBuffer in r_socket_http_answer()
* r_socket_http_answer: Fix read for big responses
* Implement r_str_escape_sh()
* Cleanup r_socket_connect() on Windows
* Fix socket being created without a protocol
* Fix socket connect with SSL ##socket
* Use select() in r_socket_ready()
* Fix read failing if received only protocol answer
* Fix double-free
* r_socket_http_get: Fail if req. SSL with no support
* Follow redirects in r_socket_http_answer()
* Fix r_socket_http_get result length with R2_CURL=1
* Also follow redirects
* Avoid using curl for downloading PDBs
* Use r_socket_http_get() on UNIXs
* Use WinINet API on Windows for r_socket_http_get()
* Fix command injection
* Fix r_sys_cmd_str_full output for binary data
* Validate GUID on PDB download
* Pass depth to socket_http_get_recursive()
* Remove 'r_' and '__' from static function names
* Fix is_valid_guid
* Fix for comments

Vulnerable code:
static bool checkCurl() {
	const char nul[] = R_SYS_DEVNULL;
	if (r_sys_cmdf ("curl --version > %s", nul) != 0) {
	char *curl_cmd = NULL;
	char *extractor_cmd = NULL;
	char *abspath_to_archive = NULL;
	char *abspath_to_file = NULL;
	char *archive_name = NULL;
	size_t archive_name_len = 0;
	char *symbol_store_path = NULL;
	char *dbg_file = NULL;
	char *guid = NULL;
	char *archive_name_escaped  = NULL;
	char *user_agent = NULL;
	char *symbol_server = NULL;

	if (!checkCurl ()) {
		return 0;
	}
	// dbg_file len is > 0
	archive_name_len = strlen (opt->dbg_file);
	archive_name = malloc (archive_name_len + 1);
	if (!archive_name) {
		return 0;
	}
	memcpy (archive_name, opt->dbg_file, archive_name_len + 1);
	archive_name[archive_name_len - 1] = '_';
	symbol_store_path = r_str_escape (opt->symbol_store_path);
	dbg_file = r_str_escape (opt->dbg_file);
	guid = r_str_escape (opt->guid);
	archive_name_escaped = r_str_escape (archive_name);
	user_agent = r_str_escape (opt->user_agent);
	symbol_server = r_str_escape (opt->symbol_server);

	abspath_to_archive = r_str_newf ("%s%s%s%s%s%s%s",
			    symbol_store_path, R_SYS_DIR,
			    dbg_file, R_SYS_DIR,
			    guid, R_SYS_DIR,
			    archive_name_escaped);

	abspath_to_file = strdup (abspath_to_archive);
	abspath_to_file[strlen (abspath_to_file) - 1] = 'b';
		R_FREE (user_agent);
		R_FREE (abspath_to_archive);
		R_FREE (archive_name_escaped);
		R_FREE (symbol_store_path);
		R_FREE (dbg_file);
		R_FREE (guid);
		R_FREE (archive_name);
		R_FREE (abspath_to_file);
		R_FREE (symbol_server);
		res = 1;

		curl_cmd = r_str_newf ("curl -sfLA \"%s\" \"%s/%s/%s/%s\" --create-dirs -o \"%s\"",
		                       user_agent,
		                       symbol_server,
							   dbg_file,
							   guid,
		                       archive_name_escaped,
		                       abspath_to_archive);
		const char *cabextractor = "expand";
		const char *format = "%s %s %s";

		// extractor_cmd -> %1 %2 %3
		// %1 - 'expand'
		// %2 - absolute path to archive
		// %3 - absolute path to file that will be dearchive
		extractor_cmd = r_str_newf (format, cabextractor,
			abspath_to_archive, abspath_to_file);
		const char *cabextractor = "cabextract";
		const char *format = "%s -d \"%s\" \"%s\"";
		extractor_cmd = r_str_newf (format, cabextractor, abspath_to_dir, abspath_to_archive);
		R_FREE (abspath_to_dir);
		eprintf ("Attempting to download compressed pdb in %s\n", abspath_to_archive);
		if ((cmd_ret = r_sys_cmd (curl_cmd) != 0)) {
			eprintf("curl exited with error %d\n", cmd_ret);
			res = 0;
		}
		eprintf ("Attempting to decompress pdb\n");
		if (opt->extract > 0) {
		R_FREE (curl_cmd);
		res = 1;

		archive_name_escaped[strlen (archive_name_escaped) - 1] = 'b';

		curl_cmd = r_str_newf ("curl -sfLA \"%s\" \"%s/%s/%s/%s\" --create-dirs -o \"%s\"",
		                       opt->user_agent,
		                       opt->symbol_server,
		                       opt->dbg_file,
		                       opt->guid,
		                       archive_name_escaped,
		                       abspath_to_file);
		if ((cmd_ret = r_sys_cmd (curl_cmd) != 0)) {
			eprintf("curl exited with error %d\n", cmd_ret);
			res = 0;
		}
		R_FREE (curl_cmd);
	}
	R_FREE (abspath_to_archive);
	R_FREE (abspath_to_file);
	R_FREE (archive_name);
	R_FREE (extractor_cmd);
	R_FREE (symbol_store_path);
	R_FREE (dbg_file);
	R_FREE (guid);
	R_FREE (archive_name_escaped);
	R_FREE (user_agent);
	R_FREE (symbol_server);
/* Must be included before windows.h (r_types) */
#if defined(__WINDOWS__)
#include <ws2tcpip.h>
#endif

	struct hostent *he;
	TIMEVAL Timeout;
	Timeout.tv_sec = timeout;
	Timeout.tv_usec = 0;
	s->fd = socket (AF_INET, SOCK_STREAM, 0);
	if (s->fd == R_INVALID_SOCKET) {
		return false;
	}

	unsigned long iMode = 1;
	int iResult = ioctlsocket (s->fd, FIONBIO, &iMode);
	if (iResult != NO_ERROR) {
		eprintf ("ioctlsocket error: %d\n", iResult);
	}
	memset (&sa, 0, sizeof (sa));
	sa.sin_family = AF_INET;
	he = (struct hostent *)gethostbyname (host);
	if (he == (struct hostent*)0) {
#ifdef _MSC_VER
		closesocket (s->fd);
#else
		close (s->fd);
		return false;
	}
	sa.sin_addr = *((struct in_addr *)he->h_addr);
	s->port = r_socket_port_by_name (port);
	s->proto = proto;
	sa.sin_port = htons (s->port);
	if (!connect (s->fd, (const struct sockaddr*)&sa, sizeof (struct sockaddr))) {
#ifdef _MSC_VER
		closesocket (s->fd);
#else
		close (s->fd);
#endif
		return false;
	}
	iMode = 0;
	iResult = ioctlsocket (s->fd, FIONBIO, &iMode);
	if (iResult != NO_ERROR) {
		eprintf ("ioctlsocket error: %d\n", iResult);
	}
	if (timeout > 0) {
		r_socket_block_time (s, 1, timeout, 0);
	}
	fd_set Write, Err;
	FD_ZERO (&Write);
	FD_ZERO (&Err);
	FD_SET (s->fd, &Write);
	FD_SET (s->fd, &Err);
	select (0, NULL, &Write, &Err, &Timeout);
	if (FD_ISSET (s->fd, &Write)) {
		return true;
	}
	return false;
#elif __UNIX__
	struct addrinfo hints = {0};
				struct timeval tv;
				tv.tv_sec = timeout;
				tv.tv_usec = 0;
				FD_ZERO(&wfds);
				FD_SET(s->fd, &wfds);
						return true;
#endif
			r_socket_free (s);
		if (SSL_connect (s->sfd) != 1) {
			r_socket_free (s);
	struct timeval t;
	t.tv_sec = timeout;
	t.tv_usec = 0;
		struct timeval tv = {0};
		tv.tv_sec = sec;
		tv.tv_usec = usec;
// XXX: rewrite it to use select //
#if __UNIX__
	//int msecs = (1000 * secs) + (usecs / 1000);
	int msecs = (usecs / 1000);
	struct pollfd fds[1];
	fds[0].fd = s->fd;
	fds[0].events = POLLIN | POLLPRI;
	fds[0].revents = POLLNVAL | POLLHUP | POLLERR;
	return poll ((struct pollfd *)&fds, 1, msecs);
#elif __WINDOWS__
	struct timeval tv;
	tv.tv_sec = secs;
	tv.tv_usec = usecs;
#else
	return true; /* always ready if unknown */
#endif
#if __WINDOWS__
rep:
	{
	int ret = recv (s->fd, (void *)buf, len, 0);
	if (ret == -1) {
		goto rep;
	}
	return ret;
	}
#else
#endif
static int __socket_slurp (RSocket *s, ut8 *buf, int bufsz) {
	int i;
	int chsz = 1;
	// r_socket_block_time (s, 1, 1, 0);
	if (r_socket_read_block (s, (ut8 *) buf, 1) != 1) {
	for (i = 1; i < bufsz; i += chsz) {
		buf[i] =0;
		r_socket_block_time (s, 1, 0, 1000);
		int olen = r_socket_read_block (s, (ut8 *) buf + i , chsz);
		if (olen != chsz) {
			buf[i] = 0;
static char *r_socket_http_answer (RSocket *s, int *code, int *rlen) {
	int ret, len = 0, bufsz = 32768, delta = 0;
	char *dn, *buf = calloc (1, bufsz + 32); // XXX: use r_buffer here
	if (!buf) {
	int olen = __socket_slurp (s, (ut8*)buf, bufsz);
		goto fail;
fail:
// is 's' free'd? isn't this going to cause a double free?
R_API char *r_socket_http_get(const char *url, int *code, int *rlen) {
	if (curl_env && *curl_env) {
		char *encoded_url = r_str_escape (url);
		char *res = r_sys_cmd_strf ("curl '%s'", encoded_url);
		free (encoded_url);
				*rlen = strlen (res);
		free (curl_env);

	if (code) {
		*code = 0;
	}
	if (rlen) {
		*rlen = 0;
	}
		response = r_socket_http_answer (s, code, rlen);
R_API char *r_socket_http_post (const char *url, const char *data, int *code, int *rlen) {
	return r_socket_http_answer (s, code, rlen);
				if (!(bytes = read (sh_out[0], buffer, sizeof (buffer)-1))) {
				buffer[sizeof (buffer) - 1] = '\0';
				if (len) {
					*len += bytes;
				outputptr = r_str_append (outputptr, buffer);
				if (!read (sh_err[0], buffer, sizeof (buffer)-1)) {
				buffer[sizeof (buffer) - 1] = '\0';
				*sterr = r_str_append (*sterr, buffer);
			ptr = p + 2;
  platform_deps = [cc.find_library('ws2_32'), cc.find_library('psapi')]
		ptr = p + 2;
CMDS=!!rabin2 -PP ${R2_FILE} ~PDB

Fixed code:
static bool download_and_write(SPDBDownloaderOpt *opt, const char *file) {
	char *dir = r_str_newf ("%s%s%s%s%s",
		opt->symbol_store_path, R_SYS_DIR,
		opt->dbg_file, R_SYS_DIR,
		opt->guid);
	if (!r_sys_mkdirp (dir)) {
		free (dir);
	char *url = r_str_newf ("%s/%s/%s/%s", opt->symbol_server, opt->dbg_file, opt->guid, file);
	int len;
	char *file_buf = r_socket_http_get (url, NULL, &len);
	free (url);
	if (!len || R_STR_ISEMPTY (file_buf)) {
		free (dir);
		free (file_buf);
		return false;
	}
	char *path = r_str_newf ("%s%s%s", dir, R_SYS_DIR, opt->dbg_file);
	FILE *f = fopen (path, "wb");
	if (f) {
		fwrite (file_buf, sizeof (char), (size_t)len, f);
		fclose (f);
	}
	free (dir);
	free (path);
	free (file_buf);


	char *abspath_to_file = r_str_newf ("%s%s%s%s%s%s%s",
		opt->symbol_store_path, R_SYS_DIR,
		opt->dbg_file, R_SYS_DIR,
		opt->guid, R_SYS_DIR,
		opt->dbg_file);

		free (abspath_to_file);
		char *extractor_cmd = NULL;
		char *archive_name = strdup (opt->dbg_file);
		archive_name[strlen (archive_name) - 1] = '_';
		char *abspath_to_archive = r_str_newf ("%s%s%s%s%s%s%s",
			opt->symbol_store_path, R_SYS_DIR,
			opt->dbg_file, R_SYS_DIR,
			opt->guid, R_SYS_DIR,
			archive_name);

		eprintf ("Attempting to download compressed pdb in %s\n", abspath_to_archive);
		char *abs_arch_esc = r_str_escape_sh (abspath_to_archive);
		char *abs_file_esc = r_str_escape_sh (abspath_to_file);
		// expand %1 %2
		// %1 - absolute path to archive
		// %2 - absolute path to file that will be dearchive
		extractor_cmd = r_str_newf ("expand \"%s\" \"%s\"", abs_arch_esc, abs_file_esc);
		free (abs_file_esc);
		char *abs_dir_esc = r_str_escape_sh (abspath_to_dir);
		extractor_cmd = r_str_newf ("cabextract -d \"%s\" \"%s\"", abs_arch_esc, abs_dir_esc);
		free (abs_dir_esc);
		free (abspath_to_dir);
		free (abs_arch_esc);
		res = download_and_write (opt, archive_name);

		if (opt->extract > 0 && res) {
			eprintf ("Attempting to decompress pdb\n");
		free (archive_name);
		free (abspath_to_archive);
		res = download_and_write (opt, opt->dbg_file);
	}
	free (abspath_to_file);
static bool is_valid_guid(const char *guid) {
	if (!guid) {
		return false;
	}
	size_t i;
	for (i = 0; guid[i]; i++) {
		if (!isxdigit (guid[i])) {
			return false;
		}
	}
	return i >= 33; // len of GUID and age
}

	if (!is_valid_guid (info->guid)) {
		eprintf ("Invalid GUID for file\n");
		return 1;
	}

# open-ssl
ifeq (${HAVE_LIB_SSL},1)
BN_LIBS=${SSL_LDFLAGS}
endif

  #include <ws2tcpip.h>
R_API char *r_str_escape_sh(const char *buf);
#define gai_strerror gai_strerrorA
	struct addrinfo hints = { 0 };
#if __UNIX__
#endif
#if __UNIX__
#endif
				struct timeval tv = {timeout, 0};
				FD_ZERO (&wfds);
				FD_SET (s->fd, &wfds);
						goto success;
success:
			r_socket_close (s);
		int ret = SSL_connect (s->sfd);
		if (ret != 1) {
			int error = SSL_get_error (s->sfd, ret);
			int tries = 10;
			while (tries && ret && (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)) {
				struct timeval tv = {1, 0};
				fd_set rfds, wfds;
				FD_ZERO (&rfds);
				FD_ZERO (&wfds);
				if (error == SSL_ERROR_WANT_READ) {
					FD_SET (s->fd, &rfds);
				} else {
					FD_SET (s->fd, &wfds);
				}
				if ((ret = select (s->fd + 1, &rfds, &wfds, NULL, &tv)) < 1) {
					r_socket_close (s);
					return false;
				}
				ret = SSL_connect (s->sfd);
				if (ret == 1) {
					return true;
				}
				error = SSL_get_error (s->sfd, ret);
				tries--;
			}
			r_socket_close (s);
	struct timeval t = {timeout, 0};
		struct timeval tv = {sec, usec};
	struct timeval tv = {secs, usecs};
#if HAVE_LIB_SSL
			if (SSL_get_error (s->sfd, r) == SSL_ERROR_WANT_READ) {
				if (r_socket_ready (s, 1, 0) == 1) {
					continue;
				}
			}
#endif
#if __WINDOWS__
#include <WinInet.h>
#endif

#define SOCKET_HTTP_MAX_HEADER_LENGTH 0x2000
#define SOCKET_HTTP_MAX_REDIRECTS 5

static size_t socket_slurp(RSocket *s, RBuffer *buf) {
	size_t i;
	if (r_socket_ready (s, 1, 0) != 1) {
	r_socket_block_time (s, 1, 0, 1000);
	for (i = 0; i < SOCKET_HTTP_MAX_HEADER_LENGTH; i += 1) {
		ut8 c;
		int olen = r_socket_read_block (s, &c, 1);
		if (olen != 1) {
			r_buf_append_bytes (buf, (ut8 *)"", 1);
		r_buf_append_bytes (buf, &c, 1);
static char *socket_http_get_recursive(const char *url, int *code, int *rlen, ut32 redirections);

static char *socket_http_answer(RSocket *s, int *code, int *rlen, ut32 redirections) {
	int ret, len = 0, delta = 0;
	char *dn;
	RBuffer *b = r_buf_new ();
	if (!b) {
	size_t olen = socket_slurp (s, b);
	char *buf = malloc (olen + 1);
	if (!buf) {
		goto exit;
	}
	r_buf_read_at (b, 0, (ut8 *)buf, olen);
	buf[olen] = 0;
		goto exit;

	/* Follow redirects */
	p = r_str_casestr (buf, "Location:");
	if (p) {
		if (!redirections) {
			eprintf ("Too many redirects\n");
			goto exit;
		}
		p += strlen ("Location:");
		char *end_url = strchr (p, '\n');
		if (end_url) {
			int url_len = end_url - p;
			char *url = r_str_ndup (p, url_len);
			r_str_trim (url);
			res = socket_http_get_recursive (url, code, rlen, --redirections);
			free (url);
			len = *rlen;
		}
		goto exit;
	}

			if (!res) {
				goto exit;
			}
			olen -= dn - buf;
exit:
	r_buf_free (b);
#if __WINDOWS__
static char *http_get_w32(const char *url, int *code, int *rlen) {
	HINTERNET hInternet = InternetOpenA ("radare2 "R2_VERSION, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
	if (!hInternet) {
		r_sys_perror ("InternetOpenA");
		return NULL;
	}
	HINTERNET hOpenUrl = InternetOpenUrlA (hInternet, url, NULL, 0, 0, 0);
	if (!hOpenUrl) {
		r_sys_perror ("InternetOpenUrlA");
		InternetCloseHandle (hInternet);
		return NULL;
	}

	char *ret = NULL;
	size_t read_sz = 0x100000;
	DWORD r = 0, w = 0;
	bool res = true;
	do {
		w += r;
		if (!res && GetLastError () == ERROR_INSUFFICIENT_BUFFER) {
			read_sz *= 2;
		}
		char *tmp = realloc (ret, read_sz + w);
		if (!tmp) {
			R_FREE (ret);
			goto exit;
		}
		ret = tmp;
	} while (!(res = InternetReadFile (hOpenUrl, ret + w, read_sz, &r)) || r);

	if (w) {
		char *tmp = realloc (ret, (size_t)w + 1);
		if (tmp) {
			ret = tmp;
			ret[w] = 0;
		} else {
			R_FREE (ret);
		}
	} else {
		R_FREE (ret);
	}

exit:
	if (rlen) {
		*rlen = w;
	}
	if (code && w) {
		*code = 200;
	}
	InternetCloseHandle (hInternet);
	InternetCloseHandle (hOpenUrl);
	return ret;
}
#endif

static char *socket_http_get_recursive(const char *url, int *code, int *rlen, ut32 redirections) {
	if (code) {
		*code = 0;
	}
	if (rlen) {
		*rlen = 0;
	}
	if (!R_STR_ISEMPTY (curl_env) && atoi (curl_env)) {
		int len;
		char *escaped_url = r_str_escape_sh (url);
		char *command = r_str_newf ("curl -sfL -o - \"%s\"", escaped_url);
		char *res = r_sys_cmd_str (command, NULL, &len);
		free (escaped_url);
		free (command);
		free (curl_env);
		if (!res) {
			return NULL;
		}
				*rlen = len;
#if __WINDOWS__
	return http_get_w32 (url, code, rlen);
#else
#if !HAVE_LIB_SSL
	if (ssl) {
		eprintf ("Tried to get '%s', but SSL support is disabled, set R2_CURL=1 to use curl\n", url);
		return NULL;
	}
#endif
		response = socket_http_answer (s, code, rlen, redirections);
#endif
}

R_API char *r_socket_http_get(const char *url, int *code, int *rlen) {
	return socket_http_get_recursive (url, code, rlen, SOCKET_HTTP_MAX_REDIRECTS);
R_API char *r_socket_http_post(const char *url, const char *data, int *code, int *rlen) {
	return socket_http_answer (s, code, rlen, 0);
// Return MUST BE surrounded by double-quotes
R_API char *r_str_escape_sh(const char *buf) {
	r_return_val_if_fail (buf, NULL);
	char *new_buf = malloc (1 + strlen (buf) * 2);
	if (!new_buf) {
		return NULL;
	}
	const char *p = buf;
	char *q = new_buf;
	while (*p) {
		switch (*p) {
#if __UNIX__
		case '$':
		case '`':
#endif
		case '\\':
		case '"':
			*q++ = '\\';
			/* FALLTHRU */
		default:
			*q++ = *p++;
			break;
		}
	}
	*q = '\0';
	return new_buf;
}

		size_t err_len = 0, out_len = 0;
				if ((bytes = read (sh_out[0], buffer, sizeof (buffer))) < 1) {
				char *tmp = realloc (outputptr, out_len + bytes + 1);
				if (!tmp) {
					R_FREE (outputptr);
					break;
				outputptr = tmp;
				memcpy (outputptr + out_len, buffer, bytes);
				out_len += bytes;
				if ((bytes = read (sh_err[0], buffer, sizeof (buffer))) < 1) {
					break;
				}
				char *tmp = realloc (*sterr, err_len + bytes + 1);
				if (!tmp) {
					R_FREE (*sterr);
				*sterr = tmp;
				memcpy (*sterr + err_len, buffer, bytes);
				err_len += bytes;
		if (len) {
			*len = out_len;
		}
		if (*sterr) {
			(*sterr)[err_len] = 0;
		}
		if (outputptr) {
			outputptr[out_len] = 0;
		}
			ptr = p + 3;
  platform_deps = [cc.find_library('ws2_32'), cc.find_library('wininet'), cc.find_library('psapi')]
		ptr = p + 3;
CMDS=<<EOF
%R2_CURL=1
!!rabin2 -PP ${R2_FILE} ~PDB
EOF
bool test_r_str_escape_sh(void) {
	char *escaped = r_str_escape_sh ("Hello, \"World\"");
	mu_assert_streq (escaped, "Hello, \\\"World\\\"", "escaped \"double quotes\"");
	free (escaped);
	escaped = r_str_escape_sh ("Hello, \\World\\");
	mu_assert_streq (escaped, "Hello, \\\\World\\\\", "escaped backspace");
	free (escaped);
#if __UNIX__
	escaped = r_str_escape_sh ("Hello, $(World)");
	mu_assert_streq (escaped, "Hello, \\$(World)", "escaped $(command)");
	free (escaped);
	escaped = r_str_escape_sh ("Hello, `World`");
	mu_assert_streq (escaped, "Hello, \\`World\\`", "escaped `command`");
	free (escaped);
#endif
	mu_end;
}

	mu_run_test (test_r_str_escape_sh);