YAMPA is Yet Another Mitm Proxy for use in A/D CTFs. It is intended to sit in between the gamenet and vulnbox and can be customized via plugins to perform different functionality on the traffic.
Fun fact: Yampa is also the short name for various plants native to western North America.
Since YAMPA runs in docker, you hardly have to take care of prerequisites -- except for docker, of course.
Note that a fairly recent version of both docker and the docker compose plugin is required.
To set up YAMPA, do the following:
- Clone the repository.
(In the rest of the documentantion,
./
refers to the repository.) - Generate a sample environment using
./generate-env.sh
- Edit
.env
and adjust the config as you need
At this point, YAMPA should be ready to run. To start it, use the following command:
docker compose up -d
When YAMPA is started up freshly without any plugins*, the proxy will behave transparently. That is, it will forward traffic in both directions without modifying, filtering or logging it. To do so, you will want to add plugins.
A plugin implements multiple hooks. The hooks have the following intended functionality:
- decrypting
- filtering
- logging
- encrypting
You can think of a data packet as walking through these stages in the above order.
When multiple plugins are loaded, the respective hooks of all plugins are executed one after the other with the same input parameters.
When a hook returns None
, the hook of the next plugin in sequence will be called.
If a hook returns something different from None
(for instance, when the decryption hook returns decrypted data), the rest of the chain is dropped and this output will be taken as the output of the chain.
This means that by returning None
, a hook can indicate that a certain data packet is None
of its business and that the next plugin (if any) should take care of it.
Exception to this are such hooks that don't have a return value, like logging, tcp_new_connection
and tcp_connection_close
.
For these hooks, the respective implementations of all loaded plugins are executed in parallel.
Find more details on the implementation of plugins in the section "Writing Plugins".
Note * filter_plugin.py
is always installed and active.
However, without adding meaningful rules into ./rules/
, it will not filter anything.
To add your plugins, add them to your specified plugin directory (e.g. ./plugins
).
For the changes to take effect during runtime, call ./reload.sh
.
YAMPA will then unload all plugins, reload the code and initialize them again.
If an old version of a plugin is running and an error arises while loading the newer version, the old version will keep running. That is, plugins are only replaced if at least the load process is successful.
On a technical level, a plugin is a python module (or package) that exposes a function of the following signature:
def constructor() -> PluginBase: ...
That is, the constructor returns an instance of a direct subclass of PluginBase
.
You can use the constructor to do some initialization (like loading filter rules from a file).
The class PluginBase
defines the available hooks and their signatures.
To find out what hooks are available, what parameters they take and what output is expected, find the extensive documentation in ./yampa/plugins/base.py
, where all details are layed out.
You can implement a hook by simply overriding the base function. Since PluginBase only specifies a default for when no plugin overrides a given hook, there is no need to call super().
If a plugin has third-party dependencies, they can be installed in ./dependencies
.
For instance, when installing dependencies with pip
, use the flag --target=./dependencies
like so:
pip install requests --target=./dependencies
pip install -r some/requirements.txt --target=./dependencies
Since ./dependencies
is mounted into the docker container, you can install dependencies at runtime and issue a ./reload.sh
afterwards.
If more complex cryptographic protocols are used or if you have other very specific requirements, the decryption and encryption hooks might not be powerful enough to implement the desired functionality. This case arises, for instance, in TLS. To handle such situations, a plugin can wrap a connection.
By calling something like my_connection.wrap()
, a plugin can take full and exclusive control over future read and write access to the specified stream(s).
This way, the plugin has direct access to the underlying socket.
Note that wrappers will not be removed when a plugin is unloaded or reloaded, to keep the connection alive. This might be problematic if you wrap a single connection twice.
As a demonstration, see ./demo_plugins/ssl_termination_plugin.py
.
For a more technical documentation, see ./yampa/proxy/connection.py
.
To familiarize yourself with plugins, some demonstrations can be found in the ./demo_plugins
folder.
my_awesome_plugin.py
does some very basic filtering and logging for TCP connections. When a new TCP connection is built, we are notified by a logging message. If data is transmitted that contains the stringAAAAAAA
, the connection is closed. If it contains the stringflag
, the packet is accepted. Otherwise, the filter ignores the packet. In the logging stage, we are notified about the data that is transmitted and about the action the filter has taken.my_more_complex_plugin.py
demonstrates how packages can be used as plugins. It simply prints out the data transmitted via TCP connections and additionally makes sends it to a request catcher.traffic_dump_wireguard.py
starts a third wireguard endpoint on port 51822 and creates a live replay of traffic passing through the proxy. You can use this (after tweaking the config) to extract live traffic captures from the "middle" of the proxy chain, i.e. the log stage.ssl_termination_plugin
reads a tls certificate and key from the environment and intercepts any connections targeting port 443 on the inside of the proxy. It uses the connection wrapping API to transparently decrypt and re-encrypt TLS. You can re-use this plugin by simply swapping out the key, cert and port used.
The current (very limited) filter engine is implemented in rust and used in python using pyo3 bindings and the maturin build system.
Upon plugin (re)load, all rules/*.rls
files are read and concatenated (files are concatenated in alphabetic order).
To avoid syntax errors at runtime (which only cause needless overhead), you can run ./lint-rules.sh
to quickly check the rules directory.
In case of syntax errors, the linter will print out all faulty lines and exit with code 1.
If there is no error parsing the rules, the linter will output nothing and exit with code 0.
This allows for simple combination of the linter with the reload script like so:
./lint-rules.sh && ./reload.sh
Furthermore, the filter plugin will automatically generate a log similar to suricata's eve.json.
This log will be placed under ./rules/eve.json
.
The rule format is as follows:
RULE ::= EFFECTS
:
DIRECTION:
MATCHERS;
EFFECTS ::= (ACTION | TAGS | FLOWS)
ACTION ::= (
ACCEPT
|ALERT
|DROP
)[(
(QUOTED_STRING))
]TAGS ::=
TAGS(
{QUOTED_STRING}+)
FLOWS ::=
FLOWS(
{QUOTED_STRING}+)
DIRECTION ::= (
IN
|OUT
)[(
U16 [,
U16])
]MATCHERS ::= (REGEX | FLOW)
REGEX ::= QUOTED_STRING
FLOW ::=
SET(
QUOTED_STRING)
One rule might look like this:
DROP("contains flag") TAG("whatever") : OUT(8080) : "FLAG\{";
Here, a packet where the ascii bytes of FLAG{
would be contained with the message "contains flag" and emits the tag "whatever"
This part is optimized for what we think is the most likely needed on a regular basis: You can specify the direction as incoming or outgoing and then either no port (all ports match), one port, which is the port on the vulnbox side or two ports. In this case the first is the port on the vulnbox side and the second one is the other port. The case where only one port is specified will usually be the most required.
Flow bits are currently quite limited. They are managed per connection by the python plugin, one can add flow bits as an effect of the rule and match them in the matcher part.
The parser for the rules is written in nom
, a powerful combination parser.
It is therefore quite easy to add capability to the rules.