Tulip is a flow analyzer meant for use during Attack / Defence CTF competitions. It allows players to easily find some traffic related to their service and automatically generates python snippets to replicate attacks.
Tulip was developed by Team Europe for use in the first International Cyber Security Challenge. The project is a fork of flower, but it contains quite some changes:
- New front-end (typescript / react / tailwind)
- New ingestor code, based on gopacket
- IPv6 support
- Vastly improved filter and tagging system.
- Deep links for easy collaboration
- Added an http decoding pass for compressed data
- Synchronized with Suricata.
- Flow diffing
- Time and size-based plots for correlation.
- Linking HTTP sessions together based on cookies (Experimental, disabled by default)
Before starting the stack, edit services/api/configurations.py:
vm_ip = "10.60.4.1"
services = [{"ip": vm_ip, "port": 18080, "name": "BIOMarkt"},
{"ip": vm_ip, "port": 5555, "name": "SaaS"},
]You can also edit this during the CTF, just rebuild the api service:
docker-compose up --build -d apiThe stack can be started with docker-compose, after creating an .env file. See .env.example as an example of how to configure your environment.
cp .env.example .env
# < Edit the .env file with your favourite text editor >
docker-compose up -d --buildTo ingest traffic, it is recommended to create a shared bind mount with the docker-compose. One convenient way to set this up is as follows:
- On the vulnbox, start a rotating packet sniffer (e.g. tcpdump, suricata, ...)
- Using rsync, copy complete captures to the machine running tulip (e.g. to /traffic)
- Add a bind to the assembler service so it can read /traffic
The ingestor will use inotify to watch for new pcap's and suricata logs. No need to set a chron job.
If you have a lot of traffic (or you want to see traffic in real-time), you might want to use PCAP-over-IP. This allows you to stream traffic directly to Tulip. To do this, you have to:
- Start the
pcap-over-ipservice on a machine. You could use pcap-broker for this. - Set the
PCAP_OVER_IPvariable in the.envfile to the IP of the vulnbox. - Start the stack with
docker-compose up -d --build.
Tulip can passively decrypt TLS (1.2 and 1.3, AEAD cipher suites) when you supply
the session secrets. Since you control your own vulnbox, you can export those
secrets as an NSS key log
(SSLKEYLOGFILE format) — the same mechanism Wireshark uses. The server's
private key is not enough (and is useless for forward-secret / TLS 1.3
traffic); you need the per-session secrets, keyed by client_random.
How it works: the assembler watches a key-log file/directory, and for every TLS
flow it has secrets for, it decrypts the records and adds the plaintext as a new
decrypted representation (selectable in the flow view's decoder dropdown).
Decrypted HTTPS is then parsed as HTTP just like cleartext (tags, decompression,
flag/flagid scanning), and "Copy as pwntools/requests" on the decrypted
representation emits a TLS-aware client (remote(..., ssl=True) /
https:// with verify=False).
To enable it:
- On the vulnbox, make each service export an
SSLKEYLOGFILE. Common ways:- Apps using OpenSSL/BoringSSL/NSS honour the
SSLKEYLOGFILEenv var (e.g. many Python, Node and Go services; curl; browsers). - Go services: set
tls.Config.KeyLogWriter. - Otherwise, dump secrets with an eBPF tool such as
eCapture, or
LD_PRELOADshims.
- Apps using OpenSSL/BoringSSL/NSS honour the
- Ship the key log to the Tulip host (e.g. via
rsync, alongside your pcaps). The format is append-only and may grow live; the assembler tails it. - Point
TLS_KEYLOG_HOSTin your.envat that file or directory (it defaults to./shared, which is bind-mounted to the assembler) and restart:docker-compose up -d --build assembler.
Secrets do not have to arrive before the traffic. This is the normal case in
practice: pcaps are usually ingested first and the key log is shipped a bit
later. A TLS flow seen without its secrets is tagged tls, kept as ciphertext,
and queued; as soon as the matching secrets appear in the key log, the assembler
decrypts the stored flow in place and backfills the decrypted representation
(plus HTTP parsing, flag/flagid scanning, etc.). This also survives a restart —
queued flows are picked up against whatever secrets are already present.
Note: flows whose handshake wasn't captured (resumed sessions, or a capture started mid-connection) can't be matched to a key and are left as ciphertext. Only AEAD cipher suites are supported (the modern default); legacy CBC suites and QUIC are not decrypted.
Tags are read from the metadata field of a rule. For example, here's a simple rule to detect a path traversal:
alert tcp any any -> any any (msg: "Path Traversal-../"; flow:to_server; content: "../"; metadata: tag path_traversal; sid:1; rev: 1;)
Once this rule is seen in traffic, the path_traversal tag will automatically be added to the filters in Tulip.
Suricata alerts are read directly from the eve.json file. Because this file can get quite verbose when all extensions are enabled, it is recommended to strip the config down a fair bit. For example:
# ...
- eve-log:
enabled: yes
filetype: regular #regular|syslog|unix_dgram|unix_stream|redis
filename: eve.json
pcap-file: false
community-id: false
community-id-seed: 0
types:
- alert:
metadata: yes
# Enable the logging of tagged packets for rules using the
# "tag" keyword.
tagged-packets: yes
# ...Sessions with matched alerts will be highlighted in the front-end and include which rule was matched.
Your Tulip instance will probably contain sensitive CTF information, like flags stolen from your machines. If you expose it to the internet and other people find it, you risk losing additional flags. It is recommended to host it on an internal network (for instance behind a VPN) or to put Tulip behind some form of authentication.
If you have an idea for a new feature, bug fixes, UX improvements, or other contributions, feel free to open a pull request or create an issue!
When opening a pull request, please target the devel branch.
Tulip was written by @RickdeJager and @Bazumo, with additional help from @Sijisu. Thanks to our fellow Team Europe players and coaches for testing, feedback and suggestions. Finally, thanks to the team behind flower for opensourcing their tooling.


