HexBrain

Valve · Report #769014

[GoldSrc] RCE via 'spk' Console Command

Validstatus
highseverity
$350bounty
23upvotes
19 Aug 2020disclosed

Summary

Details: #Description RCE can be achieved on clients via the 'spk' console command due to missing length checks before copying into a stack based buffer.

#POC

  1. Place the attached cfg file in the root directory of the game: {F676967}
  2. Launch the game and bring up the console with ~
  3. Type in exec rce.cfg and press enter
  4. View calc pop

#Vulnerable Code The problem arises because VOX_LoadSound calls VOX_GetDirectory which copies into the stack buffer szpath without any length validation which leads to a buffer overflow.


char *VOX_GetDirectory(char *szpath, char *psz)
{
	char c;
	int cb = 0;
	char *pszscan = psz + strlen(psz) - 1;

	// scan backwards until first '/' or start of string
	c = *pszscan;
	while (pszscan > psz && c != '/')
	{
		c = *(--pszscan);
		cb++;
	}

	if (c != '/')
	{
		// didn't find '/', return default directory
		strcpy(szpath, "vox/");
		return psz;
	}

	cb = strlen(psz) - cb;
	memcpy(szpath, psz, cb); // missing length validation
	szpath[cb] = 0;
	return pszscan + 1;
}

aud_sfxcache_t *VOX_LoadSound(aud_channel_t *pchan, char *pszin)
{
	char buffer[512];
	int i, j, k, cword;
	char	pathbuffer[64];
	char	szpath[32];
	aud_sfxcache_t *sc;
	voxword_t rgvoxword[CVOXWORDMAX];
	char *psz;

	if (!pszin)
		return NULL;

	memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX);
	memset(buffer, 0, sizeof(buffer));

	// lookup actual string in (*gAudEngine.rgpszrawsentence), 
	// set pointer to string data

	psz = VOX_LookupString(pszin, NULL);

	if (!psz)
	{
		gEngfuncs.Con_DPrintf ("VOX_LoadSound: no sentence named %s\n",pszin);
		return NULL;
	}

	// get directory from string, advance psz
	psz = VOX_GetDirectory(szpath, psz);

	if (strlen(psz) > sizeof(buffer) - 1)
	{
		gEngfuncs.Con_DPrintf ("VOX_LoadSound: sentence is too long %s\n",psz);
		return NULL;
	}

	// copy into buffer
	strncpy(buffer, psz, sizeof(buffer) - 1);
	buffer[sizeof(buffer) - 1] = 0;
	psz = buffer;

	// parse sentence (also inserts null terminators between words)
	
	VOX_ParseString(psz);

	// for each word in the sentence, construct the filename,
	// lookup the sfx and save each pointer in a temp array	

	i = 0;
	cword = 0;
	while (rgpparseword[i])
	{
		// Get any pitch, volume, start, end params into voxword

		if (VOX_ParseWordParams(rgpparseword[i], &rgvoxword[cword], i == 0))
		{
			// this is a valid word (as opposed to a parameter block)
			_snprintf(pathbuffer, sizeof(pathbuffer), "%s%s.wav", szpath, rgpparseword[i]);
			pathbuffer[sizeof(pathbuffer) - 1] = 0;

			if (strlen(pathbuffer) >= sizeof(pathbuffer))
				continue;

			// find name, if already in cache, mark voxword
			// so we don't discard when word is done playing
			rgvoxword[cword].sfx = S_FindName(pathbuffer, &(rgvoxword[cword].fKeepCached));
			cword++;
		}
		i++;
	}

	k = VOX_IFindEmptySentence();
	if (k < 0)
		return NULL;

	j = 0;
	while (rgvoxword[j].sfx != NULL)
		rgrgvoxword[k][j] = rgvoxword[j++];

	pchan->isentence = k;
	pchan->iword = 0;
	pchan->sfx = rgvoxword[0].sfx;

	sc = S_LoadSound(pchan->sfx, pchan);
	if (!sc)
	{
		S_FreeChannel(pchan);
		return NULL;
	}

	return sc;
}

Impact

RCE allows for an attacker to execute any arbitrary code on a chosen victim.

How it was found

The write-up for this report has not been downloaded yet, or the program disclosed it without one. Read it on HackerOne.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original