Canarytokens are useful, but rebuilding the primitive by hand shows what the callback really means — and how passive OS fingerprinting can enrich CTI, pentest, and red team analysis.
Press enter or click to view image in full size
At first, I thought Canarytokens were the whole story: generate a token, place it somewhere interesting, wait for the alert, and use the callback as an early signal. That is already valuable, especially for security teams that need lightweight tripwires in places where suspicious interaction should never happen.
But during my lab, I wanted to understand what was happening underneath the alert. What actually happens when a document calls home? What can we learn from the request before turning it into an intelligence conclusion? And, more importantly, what context can we collect that a standard Canarytoken alert may not clearly provide?
That question changed the direction of the experiment. This was no longer only about creating a simple canary. It became a small CTI and red team reconnaissance lab built around a document callback, a Python listener, HTTP headers, User-Agent analysis, source IP observation, and one extra layer that made the exercise much more interesting: passive operating system fingerprinting with p0f.
The goal was not to replace Canarytokens. They are excellent for quick operational alerting. The goal was to rebuild the primitive by hand, understand the signal, and enrich it with information that can help CTI, pentest, and red team teams during authorized investigations and security assessments.
Canarytokens are great because they are simple. You place a token in a document, link, credential, folder, repository, or cloud resource, and if something interacts with it, you get an alert. For many teams, that is already enough to start asking better questions.
A triggered token may show that a document was opened, a fake credential was tested, a decoy link was visited, or a resource that should have stayed untouched was accessed. In CTI, pentest, and red team work, this kind of signal can be extremely useful because it creates visibility where there would normally be silence.
But there is a limitation. A standard callback usually gives you information such as the time of the request, the source IP, the token type, and some request metadata. That is useful, but it does not always tell you what kind of system touched the resource. Was it a Windows endpoint? A Linux sandbox? A proxy? A scanner? An email security gateway? A browser preview engine? A security product detonating the document before the user ever saw it?
This matters because the same callback can mean very different things depending on the infrastructure behind it. A request from a corporate endpoint, a cloud sandbox, a NAT gateway, or a security inspection service should not be interpreted the same way.
That was the gap I wanted to explore.
This lab has three main components. First, we create a Python HTTP listener that receives the callback and logs metadata. Second, we create a Word document that requests a remote transparent pixel. Third, we run p0f to passively observe the connection and attempt to infer operating system characteristics.
This is not production infrastructure and it is not a replacement for Canarytokens. It is a learning lab for authorized environments. The purpose is to understand the primitive, enrich the signal, and practice interpretation without overclaiming.
The goal is security intelligence, not covert surveillance.
The first part of the lab is a small HTTP server. Its job is to receive a GET request, print useful metadata, and return a transparent 1x1 PNG. This simulates the basic behavior of a document callback.
Objective: receive the document callback and log the HTTP metadata.
#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime
import base64# Transparent 1x1 PNG pixel
PIXEL = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
)
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
return
def do_GET(self):
print("\n" + "=" * 70)
print("[+] Callback received:", datetime.now().isoformat())
print("[+] Source IP:", self.client_address[0])
print("[+] Path:", self.path)
print("[+] User-Agent:", self.headers.get("User-Agent"))
print("[+] Headers:")
for k, v in self.headers.items():
print(f" {k}: {v}")
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(PIXEL)
if __name__ == "__main__":
host = "0.0.0.0"
port = 8080
print(f"[+] Listening on http://{host}:{port}/pixel.png")
HTTPServer((host, port), Handler).serve_forever()Run the server with:
python3 server.pyIf everything works, the server should start listening on port 8080:
[+] Listening on http://0.0.0.0:8080/pixel.pngIf it fails, the first thing to check is whether another process is already using the port:
sudo ss -lntp | grep 8080At this point, we have the application-layer listener. It can show us the source IP as seen by the server, the requested path, the User-Agent, and all HTTP headers. That is useful, but still incomplete. We now need another layer of observation.
The Python listener tells us what the HTTP request looks like. p0f helps us observe the TCP/IP behavior passively. This is important because headers can be manipulated or generated by intermediate systems, while passive fingerprinting may provide another hint about what kind of system is interacting with the listener.
p0f does not ask the remote machine what it is. It quietly observes how the machine builds its TCP packets — things like window size, TTL, MSS, TCP options, option order, timestamps, and other small implementation quirks. Then it compares those patterns against known fingerprints to infer the likely operating system.
Again, this does not create certainty. It creates context. That distinction matters.
Objective: passively observe the connection and attempt to infer operating system characteristics.
Join Medium for free to get updates from this writer.
Install p0f:
sudo apt install -y p0fThen run it on the interface that will receive the callback. In my example, the interface is ens3, but yours may be different:
sudo p0f -i ens3 -o /tmp/p0f_local.log 'tcp port 8080'The expected result is that p0f writes passive fingerprinting observations to:
/tmp/p0f_local.logIf p0f does not capture anything, verify the interface name:
ip aDepending on your environment, the interface may be eth0, ens3, tun0, wlan0, or another name. If you are testing across a VPN, virtual machine, cloud host, or lab network, make sure p0f is listening on the interface that actually receives the traffic.
Now the lab has two layers. The Python server captures the application layer, while p0f observes the network layer. This allows us to compare what the client claims through HTTP with how the connection appears to behave on the wire.
That comparison is where the investigation becomes more interesting.
For the document callback, I used a Word field that references the remote image. The detail that usually causes problems is that Word field braces are special. You cannot type them manually. Word must create them.
Objective: make the Word document request the remote pixel from the Python listener.
Open Microsoft Word, place the cursor where you want the field to be inserted, and press:
Ctrl + F9Word will create special field braces:
{ }Inside those braces, insert the INCLUDEPICTURE field pointing to your listener:
INCLUDEPICTURE "http://IP:8080/pixel.png" \dThe final field should look like this:
{ INCLUDEPICTURE "http://IP:8080/pixel.png" \d }Then update the field by pressing:
F9You can also select the whole document and update fields with:
Ctrl + A
F9If everything works, the document requests the remote pixel, the Python listener logs the callback, and p0f observes the connection. You now have a small canary-like lab that captures both HTTP metadata and passive network-layer context.
Press enter or click to view image in full size
Press enter or click to view image in full size
If it fails, do not immediately assume the technique is broken. Check whether the Python server is running, whether the Word machine can reach the listener, whether port 8080 is allowed, whether Word is blocking external content, whether the file is in Protected View, whether the field braces were created correctly, and whether a proxy, sandbox, or security gateway is changing the behavior.
A failed callback can still be a finding. If external content is blocked, that may indicate good hardening. If the callback comes from a sandbox instead of the endpoint, that reveals inspection. If it comes directly from the endpoint, that tells a different story about egress and document handling.
🚀 If this helped you rethink threat attribution, follow me for more practical cybersecurity lessons from labs, tabletop exercises, red team thinking, and defensive analysis.
If this helped you rethink Canarytokens, drop a comment with what you would enrich first in your own lab: HTTP headers, proxy logs, EDR telemetry, DNS logs, p0f fingerprinting, or SIEM correlation.