9. Collectors, Anonymizers, and Injection Infrastructure
The backend reconstructed in the preceding chapters was only the protected half of RCS. A working surveillance service also needed an edge: machines that could receive traffic, conceal the database tier, move material between trust zones, and, in some deployments, interfere with a target’s web traffic. The product divided those jobs among collectors, anonymizers, a network controller, Network Injectors, and connectors.
I drew the execution boundary at that edge. The lab runs the database, worker, and defensive web console. It does not run a collector, anonymizer, Network Injector, implant, or exploit, and it does not connect to any historical or current infrastructure. Synthetic collector, injector, and connector records make the System views inspectable; they do not show that the corresponding services operated. Unless a paragraph explicitly says otherwise, behavior in this chapter comes from static source or the RCS 9 manuals, not packets observed in the lab.
That limit is useful rather than frustrating. It forces three questions apart:
- What topology did HackingTeam describe to its customers?
- What did the surviving source actually implement?
- What could a defender infer from a host, database, or network trace?
The answers overlap, but they are not identical. A manual records design and operator guidance. Source records an implementation branch. Neither tells us that a particular historical customer installed it, exposed it to the Internet, or left every default unchanged.
The edge was a chain, not a single C2 server
Evidence boundary — MANUAL + SOURCE. No edge component was run.
The RCS 9 Administrator’s Guide defines the collector as the component that receives data sent by agents, either directly or through an anonymizer chain. The System Administrator’s Guide presents anonymizers as optional relays on Internet-connected rented servers, and says operators could drag an anonymizer onto another anonymizer or a collector to form a chain (Administrator’s Guide, p. V; System Administrator’s Guide, pp. 38–39).
The database model matches that picture. One Collector document type
represents both roles. Records with type: local are collectors; records with
type: remote are anonymizers. Both carry descriptive and monitoring fields,
while next and prev arrays encode topology. A remote record receives a
random cookie and key at creation. Its generated configuration chooses the
next hop from the linked record and formats that address for port 80
(db_objects/collector.rb:7-25,32-63).
The names are easy to misunderstand. “Local” does not mean loopback or a desktop process. It means the collector at the protected end of the forwarding chain. “Remote” means a relay intended for a less trusted location. The manual explicitly suggests a VPS that cannot be reconnected to the rest of the infrastructure. Its later statement that no data is saved in anonymizers is a design claim about the relay role, not proof that a seized relay contains no useful volatile state, configuration, logs, or operating-system traces (System Administrator’s Guide, pp. 38, 64).
A simplified intended path is:
public HTTP traffic
|
v
[anonymizer] -> [anonymizer] -> [collector :80]
|
+-------------+-------------+
| |
v v
[network controller] [RCS DB :443]
127.0.0.1:4499 authenticated TLS
|
v
storage and workers
stored evidence -> [connector] -> export/archive
Network Injectors sit beside, rather than inside, that return path. They were delivery infrastructure: their rules associated a target-identification condition with an action on selected cleartext web traffic. Connectors faced the other direction. They exported already received evidence to a local directory or a separately licensed archive system.
This separation matters during an investigation. Finding an anonymizer does not imply that it stored the evidence repository. Finding connector output does not identify the original collection route. A Network Injector record in MongoDB expresses configured delivery capability, not proof that any rule matched traffic. The complete system is a graph of roles and state transitions, not one “spyware server.”

What the collector actually did
Evidence boundary — SOURCE + MANUAL. The collector listener was not started or probed.
The collector was both a public HTTP endpoint and a broker into the protected
system. Its default configuration names an RCS database host, uses port 443 for
that connection, expects a certificate and a server-signature file, listens on
port 80, checks component state on 30-second intervals, and assigns port 4499
to its local network controller (rcs-collector/config.rb:24-33). The public
EventMachine listener binds 0.0.0.0 on the configured port
(rcs-collector/lib/rcs-collector/events.rb:191-204). By contrast, the network controller binds only
127.0.0.1, so its default trust boundary is the collector host rather than
the network (rcs-controller/network_controller.rb:11-26,58-65).
The public controller multiplexes several kinds of request on the same listener:
GETandHEADcan retrieve material from a public directory;- a recognized
POSTenters the synchronization parser; - a
POSTbearing a known network-protocol cookie is handed to the local controller; - privileged
PUSH,PUT,DELETE, andPROXYmethods are intended for the backend; and - malformed, unrecognized, or unauthorized requests enter decoy or rejection paths.
This is not a generic REST service. Method, URI, headers, cookies, and encrypted
content jointly decide which internal path receives a request. The source also
shows why a collector had to remain available when its database connection was
interrupted. On a healthy connection it downloads global signatures,
integrity-check material, and factory keys, then writes them to a local cache.
If the database is unavailable later, it reloads that cache rather than
requiring the central service for every decision
(rcs-collector/lib/rcs-collector/db.rb:135-200). That
improves continuity but enlarges the value of a compromised collector host:
the host is not merely a blind TCP relay.
Public-file handling is another distinct responsibility. The backend can put
material onto every healthy collector because it cannot predict which one will
receive a later request. The collector selects an architecture-specific suffix
from the request’s user agent, may redirect to that variant, streams a matching
file, and normally schedules deletion after serving it
(http_controller.rb:126-188; frontend.rb:72-109). Those mechanics explain
why a public directory, short-lived files, redirect responses, and backend
fan-out are relevant forensic artifacts. They do not justify recreating the
delivery flow in the lab.
The collector also records unusually explicit defensive logs. Attempts to use
privileged verbs without the expected backend header produce messages labeled
HACK ALERT; path traversal attempts do the same. Those strings are useful
host indicators when found beside other component artifacts, although an
attacker or unrelated program could copy them and a historical operator could
change logging policy.
Decoys, delays, and the meaning of 444
Evidence boundary — SOURCE only. Status 444 was not captured from a running collector.
An ordinary browser arriving at the collector was not meant to see an RCS
banner. The base REST controller calls its fallback a “fake page,” waits for a
random fraction of a second, closes the connection, logs a decoy event, and
constructs a response with numeric status 444 and an empty body
(rcs-collector/rest.rb:43-58,76-78). Invalid protocol handling uses the same
random delay and close; the source comment says the delay exists to prevent
latency discovery of the anonymizer chain
(rcs-collector/lib/rcs-collector/events.rb:60-75,97-103).
The 444 branch can be reached in several ways. A request for / has no
automatic index. A nonexistent public file generally falls back to the decoy.
A POST whose synchronization content cannot be parsed does the same. An
error while forwarding the network protocol also becomes a decoy
(http_controller.rb:19-48,126-177,414-429). Unsupported privileged-method
attempts take a related path: a 405 response object is created after the
connection-delay and “not allowed” logic, with the HACK ALERT log emitted by
the method handler.
Two cautions keep this useful for defenders.
First, 444 is not unique to RCS. It is widely associated with connections that servers intentionally drop, and any service can reproduce it. A rule that labels every empty or closed HTTP response “HackingTeam” will generate noise. The stronger signature is a combination: a candidate host and time window, port and method behavior, repeated empty closes, matching host artifacts or component logs, topology records, and perhaps the source-specific headers discussed below.
Second, source intent and wire behavior are different layers. The response object suppresses the normal status string for 444, and connection closure can prevent a client from receiving a conventional response at all. Middleboxes, anonymizers, TCP timing, and client libraries can further change what a sensor records. The defensible statement is therefore “this source contains an empty 444/close decoy path,” not “every RCS probe produces this exact packet.”
The deception is still architecturally revealing. Hiding product identity was not confined to encrypted implant traffic. It extended to error handling, timing, status codes, missing indexes, and response presentation. The edge was designed to reveal as little as possible to the wrong requester while keeping several privileged functions behind the same public listener.
Authentication changed at every boundary
Evidence boundary — SOURCE, with LAB conclusions limited to the backend assessment in Chapter 10.
The collector-to-database path is the strongest of the examined component
links. DB_rest creates a persistent TLS connection to database port 443,
loads a configured certificate, and defaults to peer verification
(rcs-collector/lib/rcs-collector/db_rest.rb:23-38;
rcs-collector/lib/rcs-collector/config.rb:24-33,55-77). The collector logs in at
/auth/login with its machine instance, reported external address, version,
component type, and the contents of its server-signature file
(rcs-collector/lib/rcs-collector/db.rb:41-52,74-99;
rcs-collector/lib/rcs-collector/db_rest.rb:78-94). The database accepts that password
when it equals the single signature whose scope is server, then grants a
generic server session (rcs-db/auth.rb:19-46).
That design authenticates possession of a customer-wide secret, not a unique collector identity. The instance name is derived from a MAC address or hostname and augmented with an address, but the backend’s decisive credential check is the shared signature. A stolen signature therefore has a larger blast radius than a per-device credential would. Chapter 10 treats that as a validated component-trust finding; it does not claim the secret was exposed in every installation.
The reverse path is weaker. When the database sends a PUSH, PROXY, PUT,
or DELETE request to a healthy collector, it places the same server signature
in X-Auth-Frontend and connects to collector port 80 with Net::HTTP
(frontend.rb:14-37,51-64,72-92,111-127). The collector authorizes the verb by
splitting that header and comparing its last value with its local signature
file (http_controller.rb:51-106,391-405). There is no TLS in this source path
and no visible message-specific signature, nonce, or channel binding.
The database-side implementation makes both properties explicit:
# rcs-db/lib/rcs-db/frontend.rb:26-37
headers = {}
sig = ::Signature.where({scope: 'server'}).first
headers['X-Auth-Frontend'] = sig[:value]
http = Net::HTTP.new(nc.address, 80)
http.read_timeout = 500
headers['Content-type'] = 'application/json'
content = object.attributes.reject{ |name| name.to_s == '_grid' }.to_json
resp = http.send_request('PUSH', '/', content, headers)
This excerpt is useful defensively because it ties an unusual header to its direction, port, credential scope, and HTTP method. The credential value is deliberately not reproduced.
This yields a concise security answer: compromise of the protected network path, a frontend host, or the shared credential could undermine more than monitoring. The credential gates content placement, deletion, proxying, and configuration forwarding at the collector. Exact consequences depend on reachability and deployed components, but “internal” is doing security work that cryptographic separation should have done. This is finding SEC-022 in the claim ledger.
One more trust assumption affects attribution. An anonymizer copies an
existing X-Forwarded-For header unchanged and only adds one when absent;
the collector then takes the first comma-separated value as the original peer
(rcs-anonymizer/src/bbproxy-proxy.c:54-94;
rcs-collector/lib/rcs-collector/events.rb:84-110). If a client can reach a member
of that chain without a trusted upstream stripping spoofed forwarding headers,
the logged origin can be attacker-controlled. This does not break the
encrypted synchronization protocol by itself. It does mean that a collector
log’s “peer” field is not strong attribution evidence unless the surrounding
network path is known.
Anonymizers were stateful enough to investigate
Evidence boundary — SOURCE + MANUAL. No anonymizer binary, installer, or service was built or run.
The manual describes an anonymizer as a disposable redirection point: a remote
Linux server, linked into a chain, monitored at roughly 30-second intervals,
and normally listening on port 443 in the documented installation. The source
snapshot is a small C proxy called bbproxy. Its compile-time defaults and
tunable files name a different default listener—port 80—and an installation
prefix /opt/bbproxy, with version, cookie, key, proxyport, and
nexthop files below it (bbproxy.h:4-20; doc/TUNABLES).
That difference is a good example of version and packaging skew. The manual’s
installed product path and port are customer guidance; the source default is a
component-level fallback. An installer could patch the binary prefix or write
proxyport, and the repository contains more than one anonymizer lineage.
Defenders should search for both families of evidence rather than declaring
one side wrong.
At startup the proxy reads its version, command cookie, key, listener, and next
hop. Missing cookie or key is fatal. A missing or disabled next hop leaves the
proxy unable to forward, while a configured one is reported in logs. The
process accepts HTTP, reads headers, and distinguishes management messages by
an exact cookie match. Ordinary traffic is connected to the next hop, with
X-Forwarded-For and X-Proxy-Version added only when the requester did not
already supply them (bbproxy.c:55-100; bbproxy-proxy.c:40-107).
So “no data is saved” should be read narrowly. The anonymizer is not intended
to retain the central evidence database, but the examined host necessarily has
configuration values, an executable, process and socket state, next-hop
knowledge, status and error logs, and potentially memory containing traffic in
flight. The System Administrator’s Guide itself directs investigators toward
syslog locations such as /var/log/messages and /var/log/syslog, and the
source logs startup state, next-hop connections, cookies, and keys at debug
level (System Administrator’s Guide, pp. 39, 78; bbproxy.c:57-100). Whether
sensitive debug entries reached disk depends on runtime logging configuration.
The chain supplies indirection, not end-to-end anonymity as a security property. Every hop knows its immediate peer and next hop. Forwarding headers carry claimed origin information. Configuration management knows the entire logical graph. The database stores addresses and adjacency. A responder who acquires several of those sources can reconstruct topology even if no single public relay stores collected evidence.
The local network controller
Evidence boundary — SOURCE only. The controller on port 4499 was not executed.
The collector’s second service boundary is easy to overlook because its
default listener is loopback-only. Requests recognized as network-protocol
traffic are forwarded from the public collector to 127.0.0.1 on the
configured controller port, carrying the claimed peer address and original
cookie. Backend PUSH requests also traverse that hop
(http_controller.rb:40-65,407-429). The network controller then parses its
own protocol and dispatches the work.
From a defensive perspective, loopback binding is helpful but not isolation. Collector compromise reaches the controller directly. Local malware, an unexpected container or namespace arrangement, or a changed bind address could alter the assumption. A process listening on TCP 4499 alongside the collector is therefore a strong component-role clue, while remote exposure of that port would be a deployment anomaly worth urgent review.
The controller also explains why monitoring records use multiple types. The database can see a collector and a network-controller status separately even when they occupy one host. Investigators should not deduplicate them merely because addresses coincide: they represent different processes and failure modes.
Network Injectors: delivery through cleartext traffic
Evidence boundary — MANUAL + SOURCE + WEB PORT. No injector, rule, uploaded file, or delivery action was executed.
The manuals describe two product forms. The appliance was intended for a fixed network position such as an ISP. The tactical version was portable and placed identification under an operator’s control. Both synchronized with RCS to obtain software and rules and return logs. The Technician’s Guide says the system acted on HTTP and explicitly could not monitor FTP or HTTPS connections (Administrator’s Guide, pp. VI–VII; Technician’s Guide, pp. 67–74).
An injection rule combined four ideas:
- whether and how often it was eligible to act;
- a target and a method for recognizing that target’s traffic;
- a pattern selecting a web resource; and
- an action that altered, inserted, redirected, or replaced content.
The manuals enumerate identifiers drawn from network addressing, DHCP, MAC, RADIUS attributes, strings in traffic, and tactical operator selection. They also enumerate actions for modifying executable downloads, adding HTML, presenting deceptive update content, or replacing a resource. Those categories are important historical and defensive facts: they show the observation points, data sources, and content transformations a provider-side investigation should consider. This book intentionally omits the manuals’ example match patterns and procedural instructions. Reproducing them would add operational guidance without improving attribution.
The database stores the same conceptual shape. An Injector has address,
port, polling, version, configuration, upgrade, redirect, and log state, with
embedded InjectorRule documents. Each rule records enabled state,
disable-after-sync behavior, probability, a target-id array, identifier type
and parameter, resource selector, action, action parameter, and an optional
GridFS object (db_objects/injector.rb:9-39,83-110). Creating, updating, and
deleting injector objects requires the appropriate system privileges; other
server or technician paths can read selected state
(rest/injector.rb:12-83).
The reconstructed console implements the System view and rule form so these documents can be inspected with synthetic data. File upload and rule CRUD are covered as UI/backend contracts where safe, but the demo’s injector record is only a fixture. The project never builds an injector package, sends a usable rule to hardware, or transforms traffic. The related anonymizer installer button is likewise render-only and is tested not to submit a build task.
For defenders, the most consequential limitation is the historical reliance on observable HTTP. Encryption narrows this particular interception model: the manual itself excludes HTTPS. In a suspected provider deployment, the valuable artifacts would therefore include rule/configuration records, management synchronization, appliance logs, cleartext flow metadata, content hash discontinuities, unexpected redirects, and discrepancies between origin content and subscriber-observed content. None is independently conclusive. Caching, CDNs, legitimate enterprise proxies, antivirus gateways, and captive portals can produce superficially similar effects.
Connectors exported the evidence boundary
Evidence boundary — SOURCE + MANUAL + WEB PORT. No operational export was run.
Connectors were not network-delivery components despite appearing beside them
in the System section. The manual calls them connection rules for third-party
software. A rule can select all evidence or a path within the
operation/target/agent hierarchy. A local connector writes JSON or XML to a
directory; a remote connector sends RCS-formatted material to an Archive-
licensed RCS installation. A keep choice determines whether the primary
database retains its copy (System Administrator’s Guide, pp. 103–105).
The source makes the retention consequence precise. Enabled connectors whose
paths match an evidence item receive queue entries. If any matching connector
says to keep the evidence, the main copy remains. Otherwise the evidence gets
a destruction countdown equal to the number of matching connectors and can be
removed after those deliveries finish (connector_manager.rb:10-42;
db_objects/connector.rb:114-130). A connector can also enqueue older evidence
when created, making its queue a mixture of real-time and retrospective work.
The model accepts LOCAL and REMOTE types and JSON, XML, and RCS
formats. Its path is an array of BSON object identifiers; a blank path matches
everything, while remote archive connectors are restricted to one operation.
The index API adds a live queued count as status, and deletion is refused
while queue entries remain (db_objects/connector.rb:12-52,56-67;
rest/connector.rb:9-46,73-88).
This creates three investigative implications. First, missing evidence in the primary database may be the intended result of connector policy rather than an ad hoc deletion. Second, the connector queue and destination should be preserved together; either alone gives an incomplete account of what left the system. Third, the destination becomes another sensitive repository that may outlive or diverge from the main database.
It also creates a local security risk. The local extractor constructs export
directories from user-controlled operation, target, and agent display names
before writing evidence and binary files. The backend does not visibly reduce
those names to safe path components (rcs-connector/extractor.rb:26-31,79-89).
Chapter 10 records the resulting conditional directory-escape finding as
SEC-026. The issue requires an enabled local connector, but it demonstrates a
recurring architectural problem: metadata considered harmless in one layer
can become control data in another.
A defensive footprint map
Evidence boundary — SOURCE + MANUAL + LAB CONFIG. The rows are leads, not a scanner signature.
The following map summarizes where an investigator could look. Defaults and manual paths are configurable, version-dependent, and non-unique.
| Layer | Candidate footprint | Evidentiary value and caution |
|---|---|---|
| Collector public edge | TCP 80 by source default; empty close/444 decoys; unusual PUSH, PROXY, WATCHDOG, and authenticated file-management verbs | Combine behavior with host or database evidence; a status code or verb alone is weak |
| Collector host | config.yaml, database certificate, server-signature file, public/, local cache, component logs | High-value role and trust material; preserve without using the credentials |
| Collector to database | TLS to TCP 443; /auth/login with component type; persistent cookie-authenticated REST | Stronger when endpoint, certificate, timing, and corresponding status records agree |
| Local controller | Loopback TCP 4499 by default; controller process and collector logs | Strong role clue on the same host; remote exposure would depart from the examined default |
| Anonymizer | Documented installed service on a VPS and port 443; source proxy default 80; /opt/bbproxy tunables; syslog; X-Forwarded-For and X-Proxy-Version | Manual/source versions differ; headers are spoofable and should support, not decide, attribution |
| Topology database | collectors documents with local/remote, addresses, cookies/keys, next/prev, status, and capped logs | Can reconstruct intended chains; a record does not prove the node was reachable or active |
| Network Injector | injectors documents, embedded rules, GridFS references, capped logs, status/version state, appliance syslog | Configuration proves capability or intent more readily than successful traffic modification |
| Connector | connectors and connector-queue documents; local JSON/XML trees or remote archive destination | Important for evidence egress and retention; destination must be preserved as a separate corpus |
The lab contributes only a boundary check to this table. Its Compose deployment publishes the backend and reconstructed console according to the project documentation while keeping the worker internal; it contains no active edge chain. Its synthetic Frontend graph proves that stored topology can be rendered and edited against the original backend. It provides no positive packet corpus for collector, anonymizer, or injector detections.
Could compromising the edge compromise RCS?
Evidence boundary — SOURCE + INFERENCE, grounded in the Chapter 10 finding set.
Yes, under realistic but deployment-dependent conditions. The collector holds customer-wide trust material, factory keys, protocol signatures, cached state, public files, and a route into the database. The anonymizer carries management credentials, next-hop configuration, and live traffic. The backend sends its broad shared server credential toward collectors over cleartext HTTP. Connector destinations replicate sensitive evidence, and local export crosses from database names into filesystem paths. A Network Injector combines targeting metadata with content-modification capability.
But “edge compromise” is not one severity. Taking a disposable anonymizer may expose only a neighboring hop, configuration, and transient traffic. Taking a collector is more serious because it sits at protocol termination and caches central material. Obtaining the shared server signature can cross component roles. Taking an archive or connector destination can expose a second evidence corpus without touching the public collection path. Network segmentation, firewalls, certificate handling, logging, and operator changes all affect the result.
A defensible assessment should therefore ask which asset was acquired and which boundary it crossed:
- Can it only forward public traffic, or can it terminate and interpret it?
- Does it hold a per-node secret or the backend-wide server signature?
- Can it reach database REST, a local controller, MongoDB, or only a next hop?
- Does it contain evidence, queued exports, public files, rules, or merely topology?
- Which logs exist outside the compromised component’s own trust domain?
Those questions turn a dramatic label into a scoped incident model. Chapter 10 applies the same discipline across the backend: prerequisite, trust boundary, affected security property, evidence, and limitation.
Where this chapter stops
Evidence boundary — explicit negative results.
No packet capture in this project demonstrates a live anonymizer chain. No collector decoy response was generated for the book. No Network Injector rule was delivered, and no HTTP resource was modified. No connector exported demo evidence. No installer or edge binary was built. The public paths, provider placements, customer-specific ports, firewall rules, and runtime hardening of historical deployments remain unknown unless a separate case record supplies them.
The source still establishes several facts: the roles were distinct; the collector multiplexed public and privileged functions; its decoy logic contained an empty 444/close path; anonymizers formed explicit next-hop chains; component authentication depended heavily on shared secrets; the reverse frontend path used that secret over HTTP; Network Injector rules combined identity, resource, and action; and connectors extended both the evidence and retention boundary.
That is enough to give defenders a map—and to show why the public edge cannot be assessed separately from the security of the platform behind it.
Sources and evidence
- Collector listener, decoy, forwarding, file, and authorization paths:
rcs-collector/lib/rcs-collector/config.rb:24-33,rcs-collector/lib/rcs-collector/events.rb:60-110,191-204,rcs-collector/lib/rcs-collector/rest.rb:43-58, andrcs-collector/lib/rcs-collector/http_controller.rb:19-188,381-429. - Collector database authentication and cached state:
rcs-collector/lib/rcs-collector/db.rb:41-99,135-200andrcs-collector/lib/rcs-collector/db_rest.rb:23-38,78-94. - Backend frontend operations and component authentication:
rcs-db/lib/rcs-db/frontend.rb:14-127andrcs-db/lib/rcs-db/auth.rb:19-46. - Topology and monitoring records:
rcs-db/lib/rcs-db/db_objects/collector.rb:7-94andrest/collector.rb:31-71,122-138,182-235. - Anonymizer implementation:
rcs-anonymizer/src/bbproxy.h:4-20,bbproxy.c:55-100,bbproxy-proxy.c:40-107, anddoc/TUNABLES. - Injector and connector models and controllers:
rcs-db/lib/rcs-db/db_objects/injector.rb:9-110,rest/injector.rb:12-113,db_objects/connector.rb:12-130,connector_manager.rb:10-42, andrest/connector.rb:9-88. - Original RCS 9 Administrator, System Administrator, and Technician manuals, especially Administrator p. V; System Administrator pp. 38–39, 64, 78, and 103–105; and Technician pp. 67–74.
- Lab boundary, synthetic topology, and current IOC catalogue:
docker-lab/README.md,console-web/MILESTONES.md,console-web/tests/seed-demo.js, anddocker-lab/IOCs.md.
↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life