HackBlue

Tools · Network tools · 16 / 24

Zeek: what a defender reads in http.log

http.log is one row per cleartext HTTP request, with the method, host, URI, status, and user agent. A populated password column, or a secret in the URI, means the lab still has a cleartext hop.

15 min read · practice only with authorization

What one row already joined

Zeek puts the request and the response it paired on one line: method, host, uri, status_code, user_agent, request_body_len, response_body_len, and the uid. id.orig_h is the client. id.resp_h is the server. That is why http.log is faster than reconstructing the stream for a question about method and status.

The log exists for cleartext HTTP, or for HTTP that some lab device already turned back into cleartext before Zeek saw it. A capture of ordinary TLS has an ssl.log handshake and an empty http.log. If you expected a status code and the log is empty, go to the server, or to the TLS metadata, instead of assuming Zeek missed a path.

Columns a defender actually checks

Look for methods the service should not serve, hosts outside the lab's names, and URIs that carry tokens. user_agent is a hint about which program spoke. A rare agent is a question for the owner, not a verdict. status_code 401 and then 200 on the same client can be a lab login. If the username or password column is filled in, the secret crossed the network in the clear. Those columns exist so you can see that a secret was present. Leave the value out of the ticket.

resp_mime_types and the file id in resp_fuids point at files.log when Zeek extracted a body. Hash and store that body under the case rules. Do not open an extracted file on the analysis host. A MIME type you did not expect, such as an executable type on a service that should return text, is enough to escalate.

  • host comes from the Host header. It can disagree with the address in id.resp_h.
  • uri is the path and query. A query string with a token is the defect.
  • request_body_len and response_body_len are sizes, which often answer the question.
  • trans_depth greater than 1 means more than one request on the connection. The uid is still the join key.

A worked defensive case

http.log on the lab status capture shows GET /health from 10.8.0.20 to 10.8.0.40, status 200, user agent lab-health/1.0, and a small response. A second row is POST /lab/report?token=redacted, status 200, request_body_len 42, from the same client. The password column is empty. The token is in the URI.

You file a cleartext secret in a query string, cite the uid and the timestamp, and leave the token value out. The owner rotates it and moves the report off plain HTTP. /health can stay, because it carries no secret. The 443 capture from the other ticket stays a separate note with an empty http.log.

Where this is documented

HTTP::Info defines method, host, uri, status_code, user_agent, and the body lengths. Zeek's http.log page walks one row and shows the matching uid in conn.log.

Use a host-only lab network, or a capture the owner already handed you. These commands read that file or record a single lab host. They are not a scan of the public internet.

Commands

Method, host, URI, and status, without credential columns
zeek-cut -m ts uid id.orig_h id.resp_h method host uri status_code user_agent request_body_len response_body_len < /lab/zeek/http.log
JSON posts only, still without printing a secret
jq -c 'select(.method == "POST") | {ts, uid, orig: ."id.orig_h", host, uri, status_code, request_body_len}' /lab/zeek-json/http.log

Diagram

The request Zeek collapsed into one http.log row

Cleartext lab HTTP, secret kept out of the note

Token in the URIHealth check

http.log · POST · files.lab.internal

POST /lab/report?token=redacted HTTP/1.1
Host: files.lab.internal
User-Agent: lab-health/1.0
Content-Length: 42
HTTP/1.1 200 OK
Content-Type: text/csv
Content-Length: 18
  • One http.log row holds the method, host, URI, status, and both lengths.
  • The token in the URI is the finding. The ticket omits the value.
  • The uid on that row is the conn.log connection to quote.

http.log · GET · files.lab.internal

GET /health HTTP/1.1
Host: files.lab.internal
User-Agent: lab-health/1.0
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2
  • A health check with no credential can stay on the lab port.
  • status_code 200 and a short response_body_len match the row.
  • This row is not the incident. The POST beside it is.

Check yourself

1. conn.log shows service ssl for 10.8.0.20 to 10.8.0.40:443, and http.log has no row for that uid. What does that combination mean?

2. A row's uri is /lab/report?token=redacted and status_code is 200. What is the change?

3. user_agent is a string the lab has never seen, on a single workstation. What is the proportionate note?