elastic / beats

:tropical_fish: Beats - Lightweight shippers for Elasticsearch & Logstash
https://www.elastic.co/products/beats
Other
109 stars 4.93k forks source link

[Packetbeat] Mysql message parser doesn't follow sequence numbers #40732

Open libvoid opened 2 months ago

libvoid commented 2 months ago

Hello,

The mysql message parser doesn't follow the mysql specification and some messages can be mistreated by the packbeat parser.

More precisely, it doesn't handle sequence numbers. The mysql specification states that :

Data between client and server is exchanged in packets of max 16MByte size [...] The sequence-id is incremented with each packet and may wrap around. It starts at 0 and is reset to 0 when a new command begins in the Command Phase.

Here is the current Packetbeat code:

if s.isClient {
    // starts Command Phase

    if m.seq == 0 && isRequest(m.typ) {
        // parse request
        m.isRequest = true
        m.start = s.parseOffset
        s.parseState = mysqlStateEatMessage
    } else {
        // ignore command
        m.ignoreMessage = true
        s.parseState = mysqlStateEatMessage
    }
} else if !s.isClient {
    // parse response
    m.isRequest = false

    if hdr[4] == 0x00 || hdr[4] == 0xfe {
        logp.Debug("mysqldetailed", "Received OK response")
        m.start = s.parseOffset
        s.parseState = mysqlStateEatMessage
        m.isOK = true
    } else if hdr[4] == 0xff {
        logp.Debug("mysqldetailed", "Received ERR response")
        m.start = s.parseOffset
        s.parseState = mysqlStateEatMessage
        m.isError = true
    } else if m.packetLength == 1 {
        logp.Debug("mysqldetailed", "Query response. Number of fields %d", hdr[4])
        m.numberOfFields = int(hdr[4])
        m.start = s.parseOffset
        s.parseOffset += 5
        s.parseState = mysqlStateEatFields
    } else {
        // something else. ignore
        m.ignoreMessage = true
        s.parseState = mysqlStateEatMessage
    }
}

https://github.com/elastic/beats/blob/main/packetbeat/protos/mysql/mysql.go#L306

As described in the Mysql specification, a query/response larger than 16MB is split into multiple packets. Those packets has a 4 bytes header with an incremented sequence_id.

The problem appears if the bits error sequence 0xFF is seen in the payload of split response packages with sequence_id > 1. Packetbeat will mark them as error messages except they are not. There is a similar problem for queries as a big query can be split on multiple packets and with the current code, those packets will be ignored.

Until sequence_id can properly be supported in the Packetbeat parser, I propose this quick fix for the problem with split server responses:

} else if m.seq == 1 && hdr[4] == 0xff {
    logp.Debug("mysqldetailed", "Received ERR response")
    m.start = s.parseOffset
    s.parseState = mysqlStateEatMessage
    m.isError = true
} 

This solved the issue we had for a client with a latin1 (yes I know) database.

There is no real specification of the maximum size of a mysql error message but it should be safe to assume it will not exceed 16MB. In the Mysql code we can see MYSQL_ERRMSG_SIZE: 512 anyway.

elasticmachine commented 2 months ago

Pinging @elastic/sec-linux-platform (Team:Security-Linux Platform)