mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00

I missed one of these in review so a machine is probably better at catching them. I fixed the existing instances which where largely in code which look dated. Where possible I slightly reorganized the code so we do not have to leave values uninitialized, but did not touch up anything else.
620 lines
16 KiB
C++
620 lines
16 KiB
C++
// See the file "COPYING" in the main distribution directory for copyright.
|
|
|
|
#include "zeek/analyzer/protocol/login/NVT.h"
|
|
|
|
#include <cstdlib>
|
|
|
|
#include "zeek/Reporter.h"
|
|
#include "zeek/ZeekString.h"
|
|
#include "zeek/analyzer/protocol/login/events.bif.h"
|
|
#include "zeek/analyzer/protocol/tcp/TCP.h"
|
|
|
|
constexpr bool IS_3_BYTE_OPTION(unsigned int code) { return code >= 251 && code <= 254; }
|
|
|
|
static constexpr uint8_t TELNET_OPT_SB = 250;
|
|
static constexpr uint8_t TELNET_OPT_SE = 240;
|
|
|
|
static constexpr uint8_t TELNET_OPT_IS = 0;
|
|
static constexpr uint8_t TELNET_OPT_SEND = 1;
|
|
|
|
static constexpr uint8_t TELNET_OPT_WILL = 251;
|
|
static constexpr uint8_t TELNET_OPT_WONT = 252;
|
|
static constexpr uint8_t TELNET_OPT_DO = 253;
|
|
static constexpr uint8_t TELNET_OPT_DONT = 254;
|
|
|
|
static constexpr uint8_t TELNET_IAC = 255;
|
|
|
|
namespace zeek::analyzer::login {
|
|
|
|
TelnetOption::TelnetOption(NVT_Analyzer* arg_endp, unsigned int arg_code) : endp(arg_endp), code(arg_code) {}
|
|
|
|
void TelnetOption::RecvOption(unsigned int type) {
|
|
TelnetOption* peer = endp->FindPeerOption(code);
|
|
|
|
if ( ! peer ) {
|
|
reporter->AnalyzerError(endp, "option peer missing in TelnetOption::RecvOption");
|
|
return;
|
|
}
|
|
|
|
// WILL/WONT/DO/DONT are messages we've *received* from our peer.
|
|
switch ( type ) {
|
|
case TELNET_OPT_WILL:
|
|
if ( SaidDont() || peer->SaidWont() || peer->IsActive() )
|
|
InconsistentOption(type);
|
|
|
|
peer->SetWill();
|
|
|
|
if ( SaidDo() )
|
|
peer->SetActive(true);
|
|
break;
|
|
|
|
case TELNET_OPT_WONT:
|
|
if ( peer->SaidWill() && ! SaidDont() )
|
|
InconsistentOption(type);
|
|
|
|
peer->SetWont();
|
|
|
|
if ( SaidDont() )
|
|
peer->SetActive(false);
|
|
break;
|
|
|
|
case TELNET_OPT_DO:
|
|
if ( SaidWont() || peer->SaidDont() || IsActive() )
|
|
InconsistentOption(type);
|
|
|
|
peer->SetDo();
|
|
|
|
if ( SaidWill() )
|
|
SetActive(true);
|
|
break;
|
|
|
|
case TELNET_OPT_DONT:
|
|
if ( peer->SaidDo() && ! SaidWont() )
|
|
InconsistentOption(type);
|
|
|
|
peer->SetDont();
|
|
|
|
if ( SaidWont() )
|
|
SetActive(false);
|
|
break;
|
|
|
|
default: reporter->AnalyzerError(endp, "bad option type in TelnetOption::RecvOption"); return;
|
|
}
|
|
}
|
|
|
|
void TelnetOption::RecvSubOption(u_char* /* data */, int /* len */) {}
|
|
|
|
void TelnetOption::SetActive(bool is_active) { active = is_active; }
|
|
|
|
void TelnetOption::InconsistentOption(unsigned int /* type */) { endp->Event(inconsistent_option); }
|
|
|
|
void TelnetOption::BadOption() { endp->Event(bad_option); }
|
|
|
|
namespace detail {
|
|
|
|
void TelnetTerminalOption::RecvSubOption(u_char* data, int len) {
|
|
if ( len <= 0 ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
if ( data[0] == TELNET_OPT_SEND )
|
|
return;
|
|
|
|
if ( data[0] != TELNET_OPT_IS ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
endp->SetTerminal(data + 1, len - 1);
|
|
}
|
|
|
|
enum EncryptOptions : uint8_t {
|
|
ENCRYPT_SET_ALGORITHM = 0,
|
|
ENCRYPT_SUPPORT_ALGORITHM = 1,
|
|
ENCRYPT_REPLY = 2,
|
|
ENCRYPT_STARTING_TO_ENCRYPT = 3,
|
|
ENCRYPT_NO_LONGER_ENCRYPTING = 4,
|
|
ENCRYPT_REQUEST_START_TO_ENCRYPT = 5,
|
|
ENCRYPT_REQUEST_NO_LONGER_ENCRYPT = 6,
|
|
ENCRYPT_ENCRYPT_KEY = 7,
|
|
ENCRYPT_DECRYPT_KEY = 8,
|
|
};
|
|
|
|
void TelnetEncryptOption::RecvSubOption(u_char* data, int len) {
|
|
if ( ! active ) {
|
|
InconsistentOption(0);
|
|
return;
|
|
}
|
|
|
|
if ( len <= 0 ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
unsigned int opt = data[0];
|
|
|
|
if ( opt == ENCRYPT_REQUEST_START_TO_ENCRYPT )
|
|
++did_encrypt_request;
|
|
|
|
else if ( opt == ENCRYPT_STARTING_TO_ENCRYPT ) {
|
|
TelnetEncryptOption* peer = (TelnetEncryptOption*)endp->FindPeerOption(code);
|
|
|
|
if ( ! peer ) {
|
|
reporter->AnalyzerError(endp, "option peer missing in TelnetEncryptOption::RecvSubOption");
|
|
return;
|
|
}
|
|
|
|
if ( peer->DidRequest() || peer->DoingEncryption() || peer->Endpoint()->AuthenticationHasBeenAccepted() ) {
|
|
endp->SetEncrypting(1);
|
|
++doing_encryption;
|
|
}
|
|
else
|
|
InconsistentOption(0);
|
|
}
|
|
}
|
|
|
|
enum AuthOptions : uint8_t {
|
|
HERE_IS_AUTHENTICATION = 0,
|
|
SEND_ME_AUTHENTICATION = 1,
|
|
AUTHENTICATION_STATUS = 2,
|
|
AUTHENTICATION_NAME = 3,
|
|
};
|
|
|
|
constexpr int AUTH_REJECT = 1;
|
|
constexpr int AUTH_ACCEPT = 2;
|
|
|
|
void TelnetAuthenticateOption::RecvSubOption(u_char* data, int len) {
|
|
if ( len <= 0 ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
switch ( data[0] ) {
|
|
case HERE_IS_AUTHENTICATION: {
|
|
TelnetAuthenticateOption* peer = (TelnetAuthenticateOption*)endp->FindPeerOption(code);
|
|
|
|
if ( ! peer ) {
|
|
reporter->AnalyzerError(endp, "option peer missing in TelnetAuthenticateOption::RecvSubOption");
|
|
return;
|
|
}
|
|
|
|
if ( ! peer->DidRequestAuthentication() )
|
|
InconsistentOption(0);
|
|
} break;
|
|
|
|
case SEND_ME_AUTHENTICATION: ++authentication_requested; break;
|
|
|
|
case AUTHENTICATION_STATUS:
|
|
if ( len <= 1 ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
if ( data[1] == AUTH_REJECT )
|
|
endp->AuthenticationRejected();
|
|
else if ( data[1] == AUTH_ACCEPT )
|
|
endp->AuthenticationAccepted();
|
|
else {
|
|
// Don't complain, there may be replies we don't
|
|
// know about.
|
|
}
|
|
break;
|
|
|
|
case AUTHENTICATION_NAME: {
|
|
char* auth_name = new char[len];
|
|
util::safe_strncpy(auth_name, (char*)data + 1, len);
|
|
endp->SetAuthName(auth_name);
|
|
} break;
|
|
|
|
default: BadOption();
|
|
}
|
|
}
|
|
|
|
constexpr uint8_t ENVIRON_IS = 0;
|
|
constexpr uint8_t ENVIRON_SEND = 1;
|
|
constexpr uint8_t ENVIRON_INFO = 2;
|
|
|
|
constexpr uint8_t ENVIRON_VAR = 0;
|
|
constexpr uint8_t ENVIRON_VAL = 1;
|
|
constexpr uint8_t ENVIRON_ESC = 2;
|
|
constexpr uint8_t ENVIRON_USERVAR = 3;
|
|
|
|
void TelnetEnvironmentOption::RecvSubOption(u_char* data, int len) {
|
|
if ( len <= 0 ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
if ( data[0] == ENVIRON_SEND )
|
|
// ### We should track the dialog and make sure both sides agree.
|
|
return;
|
|
|
|
if ( data[0] != ENVIRON_IS && data[0] != ENVIRON_INFO ) {
|
|
BadOption();
|
|
return;
|
|
}
|
|
|
|
--len; // Discard code.
|
|
++data;
|
|
|
|
while ( len > 0 ) {
|
|
int code1;
|
|
int code2;
|
|
char* var_name = ExtractEnv(data, len, code1);
|
|
char* var_val = ExtractEnv(data, len, code2);
|
|
|
|
if ( ! var_name || ! var_val || (code1 != ENVIRON_VAR && code1 != ENVIRON_USERVAR) || code2 != ENVIRON_VAL ) {
|
|
// One of var_name/var_val might be set; avoid leak.
|
|
delete[] var_name;
|
|
delete[] var_val;
|
|
|
|
BadOption();
|
|
break;
|
|
}
|
|
|
|
static_cast<analyzer::tcp::TCP_ApplicationAnalyzer*>(endp->Parent())->SetEnv(endp->IsOrig(), var_name, var_val);
|
|
}
|
|
}
|
|
|
|
char* TelnetEnvironmentOption::ExtractEnv(u_char*& data, int& len, int& code) {
|
|
code = data[0];
|
|
|
|
if ( code != ENVIRON_VAR && code != ENVIRON_VAL && code != ENVIRON_USERVAR )
|
|
return nullptr;
|
|
|
|
// Move past code.
|
|
--len;
|
|
++data;
|
|
|
|
// Find the end of this piece of the option.
|
|
u_char* data_end = data + len;
|
|
u_char* d;
|
|
for ( d = data; d < data_end; ++d ) {
|
|
if ( *d == ENVIRON_VAR || *d == ENVIRON_VAL || *d == ENVIRON_USERVAR )
|
|
break;
|
|
|
|
if ( *d == ENVIRON_ESC ) {
|
|
++d; // move past ESC
|
|
if ( d >= data_end )
|
|
return nullptr;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int size = d - data;
|
|
char* env = new char[size + 1];
|
|
|
|
// Now copy into env.
|
|
int d_ind = 0;
|
|
int i;
|
|
for ( i = 0; i < size; ++i ) {
|
|
if ( data[d_ind] == ENVIRON_ESC )
|
|
++d_ind;
|
|
|
|
env[i] = data[d_ind];
|
|
++d_ind;
|
|
}
|
|
|
|
env[i] = '\0';
|
|
|
|
data = d;
|
|
len -= size;
|
|
|
|
return env;
|
|
}
|
|
|
|
void TelnetBinaryOption::SetActive(bool is_active) {
|
|
endp->SetBinaryMode(is_active);
|
|
active = is_active;
|
|
}
|
|
|
|
void TelnetBinaryOption::InconsistentOption(unsigned int /* type */) {
|
|
// I don't know why, but this gets turned on redundantly -
|
|
// doesn't do any harm, so ignore it. Example is
|
|
// in ex/redund-binary-opt.trace.
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
NVT_Analyzer::NVT_Analyzer(Connection* conn, bool orig) : analyzer::tcp::ContentLine_Analyzer("NVT", conn, orig) {}
|
|
|
|
NVT_Analyzer::~NVT_Analyzer() {
|
|
for ( int i = 0; i < num_options; ++i )
|
|
delete options[i];
|
|
|
|
delete[] auth_name;
|
|
}
|
|
|
|
TelnetOption* NVT_Analyzer::FindOption(unsigned int code) {
|
|
int i;
|
|
for ( i = 0; i < num_options; ++i )
|
|
if ( options[i]->Code() == code )
|
|
return options[i];
|
|
|
|
TelnetOption* opt = nullptr;
|
|
if ( i < NUM_TELNET_OPTIONS ) { // Maybe we haven't created this option yet.
|
|
switch ( code ) {
|
|
case TELNET_OPTION_BINARY: opt = new detail::TelnetBinaryOption(this); break;
|
|
|
|
case TELNET_OPTION_TERMINAL: opt = new detail::TelnetTerminalOption(this); break;
|
|
|
|
case TELNET_OPTION_ENCRYPT: opt = new detail::TelnetEncryptOption(this); break;
|
|
|
|
case TELNET_OPTION_AUTHENTICATE: opt = new detail::TelnetAuthenticateOption(this); break;
|
|
|
|
case TELNET_OPTION_ENVIRON: opt = new detail::TelnetEnvironmentOption(this); break;
|
|
}
|
|
}
|
|
|
|
if ( opt )
|
|
options[num_options++] = opt;
|
|
|
|
return opt;
|
|
}
|
|
|
|
TelnetOption* NVT_Analyzer::FindPeerOption(unsigned int code) {
|
|
assert(peer);
|
|
return peer->FindOption(code);
|
|
}
|
|
|
|
void NVT_Analyzer::AuthenticationAccepted() {
|
|
authentication_has_been_accepted = true;
|
|
Event(authentication_accepted, PeerAuthName());
|
|
}
|
|
|
|
void NVT_Analyzer::AuthenticationRejected() {
|
|
authentication_has_been_accepted = false;
|
|
Event(authentication_rejected, PeerAuthName());
|
|
}
|
|
|
|
const char* NVT_Analyzer::PeerAuthName() const {
|
|
assert(peer);
|
|
return peer->AuthName();
|
|
}
|
|
|
|
void NVT_Analyzer::SetTerminal(const u_char* terminal, int len) {
|
|
if ( login_terminal )
|
|
EnqueueConnEvent(login_terminal, ConnVal(), make_intrusive<StringVal>(new String(terminal, len, false)));
|
|
}
|
|
|
|
void NVT_Analyzer::SetEncrypting(int mode) {
|
|
encrypting_mode = mode;
|
|
SetSkipDeliveries(mode);
|
|
if ( mode )
|
|
Event(activating_encryption);
|
|
}
|
|
|
|
constexpr int MAX_DELIVER_UNIT = 128;
|
|
|
|
void NVT_Analyzer::DoDeliver(int len, const u_char* data) {
|
|
while ( len > 0 ) {
|
|
if ( pending_IAC )
|
|
ScanOption(len, data);
|
|
else
|
|
DeliverChunk(len, data);
|
|
}
|
|
}
|
|
|
|
void NVT_Analyzer::DeliverChunk(int& len, const u_char*& data) {
|
|
// This code is very similar to that for TCP_ContentLine. We
|
|
// don't virtualize out the differences because some of them
|
|
// would require per-character function calls, too expensive.
|
|
|
|
// Add data up to IAC or end.
|
|
for ( ; len > 0; --len, ++data ) {
|
|
if ( offset >= buf_len )
|
|
InitBuffer(buf_len * 2);
|
|
|
|
int c = data[0];
|
|
|
|
if ( binary_mode && c != TELNET_IAC )
|
|
c &= 0x7f;
|
|
|
|
switch ( c ) {
|
|
case '\r':
|
|
if ( CRLFAsEOL() & tcp::CR_as_EOL ) {
|
|
buf[offset] = '\0';
|
|
ForwardStream(offset, buf, IsOrig());
|
|
offset = 0;
|
|
}
|
|
else
|
|
buf[offset++] = c;
|
|
break;
|
|
|
|
case '\n':
|
|
if ( last_char == '\r' ) {
|
|
if ( CRLFAsEOL() & tcp::CR_as_EOL )
|
|
// we already emitted, skip
|
|
;
|
|
else {
|
|
--offset; // remove '\r'
|
|
buf[offset] = '\0';
|
|
ForwardStream(offset, buf, IsOrig());
|
|
offset = 0;
|
|
}
|
|
}
|
|
|
|
else if ( CRLFAsEOL() & tcp::LF_as_EOL ) {
|
|
buf[offset] = '\0';
|
|
ForwardStream(offset, buf, IsOrig());
|
|
offset = 0;
|
|
}
|
|
|
|
else {
|
|
if ( Conn()->FlagEvent(SINGULAR_LF) )
|
|
Weird("line_terminated_with_single_LF");
|
|
buf[offset++] = c;
|
|
}
|
|
break;
|
|
|
|
case '\0':
|
|
if ( last_char == '\r' )
|
|
// Allow a NUL just after a \r - Solaris
|
|
// Telnet servers generate these, and they
|
|
// appear harmless.
|
|
;
|
|
|
|
else if ( flag_NULs )
|
|
CheckNUL();
|
|
|
|
else
|
|
buf[offset++] = c;
|
|
break;
|
|
|
|
case TELNET_IAC:
|
|
pending_IAC = true;
|
|
IAC_pos = offset;
|
|
is_suboption = false;
|
|
buf[offset++] = c;
|
|
--len;
|
|
++data;
|
|
ScanOption(len, data);
|
|
return;
|
|
|
|
default: buf[offset++] = c; break;
|
|
}
|
|
|
|
if ( ! (CRLFAsEOL() & tcp::CR_as_EOL) && last_char == '\r' && c != '\n' && c != '\0' ) {
|
|
if ( Conn()->FlagEvent(SINGULAR_CR) )
|
|
Weird("line_terminated_with_single_CR");
|
|
}
|
|
|
|
last_char = c;
|
|
}
|
|
}
|
|
|
|
void NVT_Analyzer::ScanOption(int& len, const u_char*& data) {
|
|
if ( len <= 0 )
|
|
return;
|
|
|
|
if ( IAC_pos == offset - 1 ) { // All we've seen so far is the IAC.
|
|
unsigned int code = data[0];
|
|
|
|
if ( code == TELNET_IAC ) {
|
|
// An escaped 255, throw away the second
|
|
// instance and drop the IAC state.
|
|
pending_IAC = false;
|
|
last_char = code;
|
|
}
|
|
|
|
else if ( code == TELNET_OPT_SB ) {
|
|
is_suboption = true;
|
|
last_was_IAC = false;
|
|
|
|
if ( offset >= buf_len )
|
|
InitBuffer(buf_len * 2);
|
|
|
|
buf[offset++] = code;
|
|
}
|
|
|
|
else if ( IS_3_BYTE_OPTION(code) ) {
|
|
is_suboption = false;
|
|
|
|
if ( offset >= buf_len )
|
|
InitBuffer(buf_len * 2);
|
|
|
|
buf[offset++] = code;
|
|
}
|
|
|
|
else {
|
|
// We've got the whole 2-byte option.
|
|
SawOption(code);
|
|
|
|
// Throw it and the IAC away.
|
|
--offset;
|
|
pending_IAC = false;
|
|
}
|
|
|
|
--len;
|
|
++data;
|
|
return;
|
|
}
|
|
|
|
if ( ! is_suboption ) {
|
|
// We now have the full 3-byte option.
|
|
SawOption(u_char(buf[offset - 1]), data[0]);
|
|
|
|
// Delete the option.
|
|
offset -= 2; // code + IAC
|
|
pending_IAC = false;
|
|
|
|
--len;
|
|
++data;
|
|
return;
|
|
}
|
|
|
|
// A suboption. Spin looking for end.
|
|
for ( ; len > 0; --len, ++data ) {
|
|
if ( offset >= buf_len )
|
|
InitBuffer(buf_len * 2);
|
|
|
|
unsigned int code = data[0];
|
|
|
|
if ( last_was_IAC ) {
|
|
last_was_IAC = false;
|
|
|
|
if ( code == TELNET_IAC ) {
|
|
// This is an escaped IAC, eat
|
|
// the second copy.
|
|
continue;
|
|
}
|
|
|
|
if ( code != TELNET_OPT_SE )
|
|
// BSD Telnet treats this case as terminating
|
|
// the suboption, so that's what we do here
|
|
// too. Below we make sure to munch on the
|
|
// new IAC.
|
|
BadOptionTermination(code);
|
|
|
|
int opt_start = IAC_pos + 2;
|
|
int opt_stop = offset - 1;
|
|
int opt_len = opt_stop - opt_start;
|
|
SawSubOption((const char*)&buf[opt_start], opt_len);
|
|
|
|
// Delete suboption.
|
|
offset = IAC_pos;
|
|
pending_IAC = is_suboption = false;
|
|
|
|
if ( code == TELNET_OPT_SE ) {
|
|
--len;
|
|
++data;
|
|
}
|
|
else {
|
|
// Munch on the new (broken) option.
|
|
pending_IAC = true;
|
|
IAC_pos = offset;
|
|
buf[offset++] = TELNET_IAC;
|
|
}
|
|
return;
|
|
}
|
|
|
|
else {
|
|
buf[offset++] = code;
|
|
last_was_IAC = (code == TELNET_IAC);
|
|
}
|
|
}
|
|
}
|
|
|
|
void NVT_Analyzer::SawOption(unsigned int /* code */) {}
|
|
|
|
void NVT_Analyzer::SawOption(unsigned int code, unsigned int subcode) {
|
|
TelnetOption* opt = FindOption(subcode);
|
|
if ( opt )
|
|
opt->RecvOption(code);
|
|
}
|
|
|
|
void NVT_Analyzer::SawSubOption(const char* subopt, int len) {
|
|
unsigned int subcode = u_char(subopt[0]);
|
|
|
|
++subopt;
|
|
--len;
|
|
|
|
TelnetOption* opt = FindOption(subcode);
|
|
if ( opt )
|
|
opt->RecvSubOption((u_char*)subopt, len);
|
|
}
|
|
|
|
void NVT_Analyzer::BadOptionTermination(unsigned int /* code */) { Event(bad_option_termination); }
|
|
|
|
} // namespace zeek::analyzer::login
|