Added rcon password hashing to prevent them from being sent as plain-text.

Commands+results are still plain-text so ideally use dtls/tls/wss/ssh/https for rcon instead, but this should be compatible with ezquake+mvdsv.


git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@5295 fc73d0e0-1445-4013-8a0c-d673dee63da5
This commit is contained in:
Spoike 2018-08-21 18:52:25 +00:00
parent f80c769ebe
commit 5886f59d76
2 changed files with 127 additions and 7 deletions

View file

@ -46,6 +46,7 @@ int startuppending;
void Host_FinishLoading(void); void Host_FinishLoading(void);
cvar_t cl_crypt_rcon = CVARFD("cl_crypt_rcon", "1", CVAR_ARCHIVE, "Controls whether to send a hash instead of sending rcon passwords as plain-text. Set to 1 for security, or 0 for backwards compatibility.\nYour command and any responses will still be sent as plain text.\nInstead, it is recommended to use rcon ONLY via dtls/tls/wss connections.");
cvar_t rcon_password = CVARF("rcon_password", "", CVAR_NOUNSAFEEXPAND); cvar_t rcon_password = CVARF("rcon_password", "", CVAR_NOUNSAFEEXPAND);
cvar_t rcon_address = CVARF("rcon_address", "", CVAR_NOUNSAFEEXPAND); cvar_t rcon_address = CVARF("rcon_address", "", CVAR_NOUNSAFEEXPAND);
@ -147,6 +148,9 @@ cvar_t noaim = CVARF("noaim", "", CVAR_ARCHIVE | CVAR_USERINFO);
cvar_t msg = CVARFD("msg", "1", CVAR_ARCHIVE | CVAR_USERINFO, "Filter console prints/messages. Only functions on QuakeWorld servers. 0=pickup messages. 1=death messages. 2=critical messages. 3=chat."); cvar_t msg = CVARFD("msg", "1", CVAR_ARCHIVE | CVAR_USERINFO, "Filter console prints/messages. Only functions on QuakeWorld servers. 0=pickup messages. 1=death messages. 2=critical messages. 3=chat.");
cvar_t b_switch = CVARF("b_switch", "", CVAR_ARCHIVE | CVAR_USERINFO); cvar_t b_switch = CVARF("b_switch", "", CVAR_ARCHIVE | CVAR_USERINFO);
cvar_t w_switch = CVARF("w_switch", "", CVAR_ARCHIVE | CVAR_USERINFO); cvar_t w_switch = CVARF("w_switch", "", CVAR_ARCHIVE | CVAR_USERINFO);
#ifdef HEXEN2
cvar_t cl_playerclass=CVARF("cl_playerclass","", CVAR_ARCHIVE | CVAR_USERINFO);
#endif
cvar_t cl_nofake = CVARD("cl_nofake", "2", "value 0: permits \\r chars in chat messages\nvalue 1: blocks all \\r chars\nvalue 2: allows \\r chars, but only from teammates"); cvar_t cl_nofake = CVARD("cl_nofake", "2", "value 0: permits \\r chars in chat messages\nvalue 1: blocks all \\r chars\nvalue 2: allows \\r chars, but only from teammates");
cvar_t cl_chatsound = CVAR("cl_chatsound","1"); cvar_t cl_chatsound = CVAR("cl_chatsound","1");
cvar_t cl_enemychatsound = CVAR("cl_enemychatsound", "misc/talk.wav"); cvar_t cl_enemychatsound = CVAR("cl_enemychatsound", "misc/talk.wav");
@ -1437,7 +1441,49 @@ void CL_Rcon_f (void)
message[4] = 0; message[4] = 0;
Q_strncatz (message, "rcon ", sizeof(message)); Q_strncatz (message, "rcon ", sizeof(message));
Q_strncatz (message, password, sizeof(message)); if (cl_crypt_rcon.ival)
{
char cryptpass[1024], crypttime[64];
const char *hex = "0123456789ABCDEF"; //must be upper-case for compat with mvdsv.
time_t clienttime;
time(&clienttime);
size_t digestsize;
unsigned char digest[64];
const unsigned char **tokens = alloca(sizeof(*tokens)*(4+Cmd_Argc()*2));
size_t *tokensizes = alloca(sizeof(*tokensizes)*(4+Cmd_Argc()*2));
int j, k;
for (j = 0; j < sizeof(time_t); j++)
{ //little-endian byte order, but big-endian nibble order. just screwed. for compat with ezquake.
crypttime[j*2+0] = hex[(clienttime>>(j*8+4))&0xf];
crypttime[j*2+1] = hex[(clienttime>>(j*8))&0xf];
}
crypttime[j*2] = 0;
tokens[0] = "rcon ";
tokens[1] = password;
tokens[2] = crypttime;
tokens[3] = " ";
for (j=0 ; j<Cmd_Argc()-i ; j++)
{
tokens[4+j*2+0] = Cmd_Argv(i+j);
tokens[4+j*2+1] = " ";
}
for (k = 0; k < 4+j*2; k++)
tokensizes[k] = strlen(tokens[k]);
digestsize = SHA1_m(digest, sizeof(digest), k, tokens, tokensizes);
for (j = 0; j < digestsize; j++)
{
cryptpass[j*2+0] = hex[digest[j]>>4];
cryptpass[j*2+1] = hex[digest[j]&0xf];
}
cryptpass[j*2] = 0;
Q_strncatz (message, cryptpass, sizeof(message));
Q_strncatz (message, crypttime, sizeof(message));
}
else
Q_strncatz (message, password, sizeof(message));
Q_strncatz (message, " ", sizeof(message)); Q_strncatz (message, " ", sizeof(message));
for ( ; i<Cmd_Argc() ; i++) for ( ; i<Cmd_Argc() ; i++)
@ -4242,6 +4288,7 @@ void CL_Init (void)
Cvar_Register (&m_forward, cl_inputgroup); Cvar_Register (&m_forward, cl_inputgroup);
Cvar_Register (&m_side, cl_inputgroup); Cvar_Register (&m_side, cl_inputgroup);
Cvar_Register (&cl_crypt_rcon, cl_controlgroup);
Cvar_Register (&rcon_password, cl_controlgroup); Cvar_Register (&rcon_password, cl_controlgroup);
Cvar_Register (&rcon_address, cl_controlgroup); Cvar_Register (&rcon_address, cl_controlgroup);
@ -4309,6 +4356,9 @@ void CL_Init (void)
Cvar_Register (&noaim, cl_controlgroup); Cvar_Register (&noaim, cl_controlgroup);
Cvar_Register (&b_switch, cl_controlgroup); Cvar_Register (&b_switch, cl_controlgroup);
Cvar_Register (&w_switch, cl_controlgroup); Cvar_Register (&w_switch, cl_controlgroup);
#ifdef HEXEN2
Cvar_Register (&cl_playerclass, cl_controlgroup);
#endif
Cvar_Register (&cl_demoreel, cl_controlgroup); Cvar_Register (&cl_demoreel, cl_controlgroup);
Cvar_Register (&record_flush, cl_controlgroup); Cvar_Register (&record_flush, cl_controlgroup);

View file

@ -60,7 +60,9 @@ cvar_t fraglog_details = CVARD("fraglog_details", "1", "Bitmask\n1: killer+ki
cvar_t timeout = CVAR("timeout","65"); // seconds without any message cvar_t timeout = CVAR("timeout","65"); // seconds without any message
cvar_t zombietime = CVAR("zombietime", "2"); // seconds to sink messages cvar_t zombietime = CVAR("zombietime", "2"); // seconds to sink messages
// after disconnect
cvar_t sv_crypt_rcon = CVARFD("sv_crypt_rcon", "", CVAR_ARCHIVE, "Controls whether the rcon password must be hashed or not. Hashed passwords also partially prevent replay attacks, but does NOT prevent malicious actors from reading the commands/results.\n0: completely insecure. ONLY allows plain-text passwords. Do not use.\n1: Mandatory hashing (recommended).\nEmpty: Allow either, whether the password is secure or not is purely the client's responsibility/fault. Only use this for comptibility with old clients.");
cvar_t sv_crypt_rcon_clockskew = CVARFD("sv_timestamplen", "60", CVAR_ARCHIVE, "Limits clock skew to reduce (delayed) replay attacks");
#ifdef SERVERONLY #ifdef SERVERONLY
cvar_t developer = CVAR("developer","0"); // show extra messages cvar_t developer = CVAR("developer","0"); // show extra messages
@ -3328,16 +3330,83 @@ client_t *SVC_DirectConnect(void)
return newcl; return newcl;
} }
static int dehex(int i)
{
if (i >= '0' && i <= '9')
return (i-'0');
else if (i >= 'A' && i <= 'F')
return (i-'A'+10);
else
return (i-'a'+10);
}
int Rcon_Validate (void) int Rcon_Validate (void)
{ {
if (!strlen (rcon_password.string)) const char *realpass = rcon_password.string;
const char *pass = Cmd_Argv(1);
if (!strlen (realpass))
return 0; return 0;
if (strcmp (Cmd_Argv(1), rcon_password.string) ) if (!sv_crypt_rcon.ival)
return 0; { //vanilla-compatible
if (!strcmp (pass, realpass) )
return 1;
}
if (sv_crypt_rcon.ival || !*sv_crypt_rcon.string)
{ //ezquake-compatible
//the password arg is "[SHA1Digest][unixtime-in-hex]" where the digest is "[arg0] password[unixtime] arg0 arg1 argn "
time_t clienttime = 0;
time_t servertime = 0;
intptr_t timediff;
qbyte b;
return 1; const size_t digestsize = 20;
size_t i, k;
unsigned char digest[digestsize];
const unsigned char **tokens = alloca(sizeof(*tokens)*(Cmd_Argc()*2+5)); //overallocation in case argc is 0.
size_t *toksizes = alloca(sizeof(*toksizes)*(Cmd_Argc()*2+5)); //overallocation in case argc is 0.
if (strlen(pass) > digestsize*2)
{
for (i = 0; pass[digestsize*2+i] && i < sizeof(time_t)*2; i++)
{ //mixed endian for compat with ezquake
if (!(i & 1))
clienttime |= dehex(pass[digestsize*2+i]) << (((i/2)*8)+4);
else
clienttime |= dehex(pass[digestsize*2+i]) << (((i/2)*8)+0);
}
time(&servertime);
timediff = servertime-clienttime;
//make sure the client's time is within our allowed bounds, to prevent (extreme) replay attacks.
if (!sv_crypt_rcon_clockskew.value || (timediff >= -sv_crypt_rcon_clockskew.ival && timediff <= sv_crypt_rcon_clockskew.ival))
{
tokens[0] = Cmd_Argv(0);
tokens[1] = " ";
tokens[2] = realpass;
tokens[3] = pass+digestsize*2;
tokens[4] = " ";
for (i = 0; i < Cmd_Argc()-2; i++)
{
tokens[5+i*2+0] = Cmd_Argv(i+2);
tokens[5+i*2+1] = " "; //a trailing space is required.
}
for (k = 0; k < 5+i*2; k++)
toksizes[k] = strlen(tokens[k]);
if (digestsize > 0 && digestsize == SHA1_m(digest, sizeof(digest), k, tokens, toksizes))
{
for (i = 0;;i++)
{
if (i == digestsize)
return 1;
if (!pass[i*2+0] || !pass[i*2+1])
break; //premature termination
b = dehex(pass[i*2+0])*16+dehex(pass[i*2+1]);
if (b != digest[i])
break;
}
}
}
}
}
return 0;
} }
/* /*
@ -5077,6 +5146,7 @@ void SV_InitLocal (void)
Log_Init(); Log_Init();
} }
rcon_password.restriction = RESTRICT_MAX; //no cheatie rconers changing rcon passwords... rcon_password.restriction = RESTRICT_MAX; //no cheatie rconers changing rcon passwords...
Cvar_Register (&sv_crypt_rcon, cvargroup_servercontrol);
Cvar_Register (&spectator_password, cvargroup_servercontrol); Cvar_Register (&spectator_password, cvargroup_servercontrol);
Cvar_Register (&sv_mintic, cvargroup_servercontrol); Cvar_Register (&sv_mintic, cvargroup_servercontrol);