Appendix H: API and Storage Map
This appendix maps the examined RCS database API to its MongoDB storage. It is
designed for source review, defensive reconstruction, and incident response—not
as an operator tutorial. Request bodies, credential values, implant
configuration, build parameters, and exploit-delivery instructions are
deliberately omitted. The map describes the frozen original rcs-db source;
lab-only proxying and safety controls are identified separately.
The central caution is that neither the URL structure nor the collection layout is an authorization boundary. The dispatcher authenticates a session, then individual controller actions apply symbolic privileges. Some actions also filter documents by denormalized user membership, while others do not. Chapter 10 assesses those inconsistencies. A responder should use this map to find evidence on an authorized forensic clone, not to infer that a reachable route is safe to call.
Service and transport boundary
The original database process exposes two adjacent TLS services. Its default
HTTPS REST listener is TCP 443, and the WebSocket listener is the next port,
444. The worker normally receives synthetic or collected evidence on TLS 442.
MongoDB is split among a mongos router on 27017, a shard server on 27018,
and a configuration server on 27019 in the reconstructed topology.
The lab maps only the database listeners to host loopback:
| Host endpoint | Internal endpoint | Function | Boundary |
|---|---|---|---|
127.0.0.1:44443 | rcs-db:443 | REST API | Original server behind a lab-only port mapping |
127.0.0.1:44444 | rcs-db:444 | WebSocket push | Original server behind a lab-only port mapping |
127.0.0.1:8080 | console-web:80 | Research SPA and same-origin reverse proxy | Web-port behavior, not an original RCS listener |
| none | rcs-worker:442 | Evidence staging and decoding | Compose-network internal only |
| none | mongo:27017-27019 | Router, shard, and configuration roles | Compose-network internal only |
The original HTTP response object sets Server: nginx even though the Ruby
EventMachine application generated the response. JSON is the default content
type; selected listings use gzip, and file/GridFS actions stream binary
content. Unknown root requests receive nginx-like HTML. These are protocol
facts and detection leads, not proof that nginx fronts the service
(rcs-db/lib/rcs-db/rest_response.rb:40-144;
rcs-db/lib/rcs-db/rest.rb:328-340).
How a request becomes an action
The parser accepts paths in three broad forms:
/<controller>
/<controller>/<action-or-id>
/<controller>/<action>/<id>
If the first path segment after the controller names a public method on that
controller, it becomes the action. Otherwise the dispatcher maps the HTTP verb
to index, show, create, update, or destroy:
| Request shape | Default action |
|---|---|
GET /thing | index |
GET /thing/<id> | show, with the identifier copied to _id |
POST /thing | create |
PUT /thing/<id> | update |
DELETE /thing/<id> | destroy |
/thing/<named-action>/... | The named public action, when present |
This is only a routing description. Named actions do not inherit a universal
verb constraint, and individual methods interpret their own parameters. The
controller name is capitalized and resolved dynamically from the registered
controller classes (rcs-db/lib/rcs-db/parser.rb:24-27,73-121;
rcs-db/lib/rcs-db/rest.rb:118-183,232-243).
Parameters can come from a CGI query string, JSON body, multipart body, or a
Ruby Marshal body. A body advertised as ordinary URL-encoded form data is not
actually parsed as a form body: outside multipart and Marshal branches, the
parser attempts JSON and otherwise treats the body as binary. Parsing happens
before controller session validation, a sequencing detail relevant to the
security review (rcs-db/lib/rcs-db/parser.rb:30-63,82-121;
rcs-db/lib/rcs-db/events.rb:122-163).
Most calls authenticate with a case-insensitive session=<UUID> cookie. The
controller retrieves the corresponding sessions document and then evaluates
action-specific privilege checks. Responses normally serialize objects to
JSON. The API has no original browser CORS requirement; the AIR client did not
need one. The web reconstruction therefore uses a same-origin nginx proxy
rather than changing the backend.
Authentication and authorization layers
Four layers recur across the surface:
- Session authentication. Every action requires a valid session unless
its controller lists it in
bypass_auth. - Licence checks. A small number of actions require a named entitlement before session handling.
- Role and sub-privilege checks.
require_auth_leveltests the symbolic array captured in the session at login. Sequential checks can require both a base role such asVIEWand a sub-privilege such asVIEW_DELETE. - Object membership. Some queries constrain
itemsorentitiesbyuser_ids, derived largely from groups. This layer is action-specific and is not consistently applied.
The ordinary roles are ADMIN, SYS, TECH, and VIEW, each with narrower
privileges such as user administration, backend management, configuration,
file execution, evidence editing/deletion/export, profiles, and alerts
(db_objects/user.rb:9-36). A component can instead receive a server
session. Session privileges are a login-time snapshot, so later user changes
do not automatically revise an already issued session.
The explicitly session-bypassed surface is small but important:
| Prefix/action | Replacement control | Purpose and caution |
|---|---|---|
/auth/login | Credentials or component signature | Creates a user or server session |
/auth/logout | None required to clear the presented cookie | Idempotent session termination |
/auth/reset | Source checks peer address equals 127.0.0.1 | Local password reset; proxy topology matters |
/position create | None in the controller | Resolves supplied map data through the position resolver |
/sync/evidence, /items, /status, /agent, /sync_event | Archive licence plus X-Sync-Signature | Archive-node synchronization surface |
/sync/setup | Archive licence; bootstrap semantics when signatures do not yet exist | Seeds synchronization signatures |
“Bypassed” means the normal cookie gate is skipped, not that the action has no
security assumptions. The synchronization actions implement their own shared
signature comparison, while setup has first-write bootstrap behavior
(rest/sync.rb:9-152). The worker’s separate evidence receiver does not use
this controller and stages a non-empty body without an equivalent application
authentication check (lib/rcs-worker/worker_controller.rb:7-34).
REST controller map
The following table is an inventory, not a list of supported client recipes. “Nominal access” summarizes explicit role checks; object-level membership and licence requirements can add constraints—or, as Chapter 10 documents, may be missing on a particular path.
| Route prefix | Principal or nominal access | Public actions in examined controller | Storage or effect |
|---|---|---|---|
/auth | Bypassed actions | login, reset, logout | Users, sessions, audit, WebSocket lifecycle |
/session | Admin | index, destroy | Session inventory and revocation |
/user | Mostly admin; self-related update/recent paths also span logged-in roles | index, show, create, update, add_recent, destroy, message | Users, groups, sessions, dashboard/recent state, pushes |
/group | Admin plus user-management privilege for mutations | index, show, create, update, destroy, user/operation membership actions, alert | Groups and denormalized item/entity access |
/audit | Admin plus audit privilege | index, count, filters | Sharded audit records and filter values |
/license | Valid session for limit/count; admin licence privilege for upload | limit, count, create | Runtime licence state and licence file |
/status | Admin, system, technician, or server depending on action | index, create, destroy, counters | Component status and counters |
/signature | Server, admin, or system | show | Component/shared signature scopes |
/version, /logo | Any valid session | index; version also show | Version metadata, console package, logo file |
/operation | Logged-in admin/tech/view reads; admin operations privilege mutates | CRUD | _kind: operation documents in items |
/target | Logged-in admin/tech/view reads; admin targets privilege mutates | CRUD plus move | _kind: target documents and target collection lifecycle |
/factory | Tech/view reads; tech mutates | index, show, update, destroy | _kind: factory documents and embedded configurations |
/agent | Server, tech, or view according to action | CRUD plus configuration, status, sync, request, upload/download, upgrade, purge, blacklist, and filesystem/command actions | _kind: agent documents, embedded requests/configs/stats, evidence lifecycle |
/search | Broad logged-in roles for index; admin/tech/view for show | index, show | Cross-item/entity discovery constrained by controller logic |
/entity | View plus profiles for most actions; promotion also requires admin target authority | CRUD, flow/positions, photo/handle/link actions, promotion, merge, summary actions | Entities, embedded handles/links, GridFS photos |
/evidence | View or narrower evidence privileges; server/tech import for ingestion actions | CRUD, bulk delete, translate, body, synchronization lifecycle, index, count, info, total, filesystem, commands, ips, worker lookup | Per-target evidence, GridFS, stats, alerts and processing queues |
/grid | Tech/view read; tech upload | show, create | Default or target GridFS buckets |
/file | Any ordinary role for read; destroy declares an unusual none privilege | show, destroy | Temporary task files |
/filter | View | index, create, destroy | Saved evidence filters |
/alert | View plus alerts privilege | CRUD, counters, log deletion actions | Per-user alerts with embedded logs |
/position | Create bypasses session auth | create | Position-resolution result; no MongoDB model write in controller |
/task | Broad valid roles plus type-dependent sub-privileges | index, show, create, destroy, download | In-memory per-user task manager and temporary files |
/upload | Tech | create | Multipart content moved to temporary storage |
/template | Tech plus configuration privilege | CRUD | Configuration templates |
/core | Broad read; system/tech mutation | CRUD | Core metadata and files; operational use excluded from the lab |
/build | Tech | create, symbian_conf | Build pipeline; prohibited and render-only in the web lab |
/exploit | Tech plus build privilege | index, show | Static exploit catalogue metadata; no package is run by this project |
/collector | Server/system/tech depending on action | CRUD, topology lookup, version/config/upgrade, logs, relay and cookie actions | Collector/anonymizer records and per-component capped logs |
/injector | Server/system/tech depending on action | CRUD, version/config/logs, rule management, upgrade | Injector records, embedded rules, capped logs; static/UI-only research boundary |
/connector | System plus connectors privilege | CRUD | Connector policy and asynchronous export queue |
/public | Tech build reads/deletes; server deletes delivered files | index, destroy, destroy_file | Public delivery documents and GridFS/file state; not exercised operationally |
/shard | System plus backend privilege | index, show, create, destroy | Shard topology and database statistics |
/backupjob | System plus backup privilege | index, create, run, update, destroy | Backup job documents and archive creation |
/backuparchive | System plus backup privilege | index, destroy, restore | On-disk backup archives and restoration |
/sync | Archive component signature; normal sessions bypassed | evidence, items, status, setup, agent, sync_event | Archive replication of items, evidence, GridFS, status and signatures |
Several names are easy to misread. The shard prefix is singular /shard.
Backup jobs and produced archives are separate controllers. The generic
evidence index excludes filesystem, info, command, and ip, which have
special actions. The examined controller provides ips, but the later console
library requests sync_history; that route is absent. The library also
expects an evidence/update_multi action not found in this snapshot.
The task surface has its own version defect: task creation keys the in-memory
map by user name, while list indexes it with the user object and then :name.
Consequently GET /task fails on a fresh examined session even though
individual task polling can work (tasks.rb:368-399). This is a fidelity seam,
not an invitation to repair the original behavior in the historical account.
WebSocket push map
The WebSocket channel is state notification, not an alternate REST API. A
client first sends an auth message carrying the existing session cookie. A
valid session receives auth: granted and server time; the socket is stored by
cookie. The server and client exchange ping and pong, and a received pong
refreshes the session’s last-contact time. The default timeout is 900 seconds
from that time (rcs-db/lib/rcs-db/websocket.rb:23-105;
rcs-db/lib/rcs-db/sessions.rb:99-129).
Observed/source-backed push families include logout, message, monitor/status, operation and agent changes, evidence/dashboard updates, alerts, and entity changes. Recipient selection generally uses user IDs rather than the exact old socket that caused an event. That design explains the same-user-login race described in Chapters 5 and 6: a queued logout for a user can reach a newly connected socket belonging to that user.
Malformed-message process impact remains an explicit security follow-up. The source parses JSON before its message switch and dereferences session state in the pong path, but this project has not promoted those traces into a process- termination claim without scoped validation.
Database topology
The source defines two Mongoid sessions:
MongoDB configuration database
└── shard membership and collection shard-key metadata
main application database (lab name: rcs, through mongos)
├── global control and investigation collections
├── evidence.<target_id>
├── aggregate.<target_id>
├── grid.<target_id>.files
└── grid.<target_id>.chunks
worker staging database (default name: rcs-worker, direct shard session)
├── grid.evidence.files
└── grid.evidence.chunks
The application session uses MONGOID_DATABASE through MONGOID_HOST and
MONGOID_PORT. The worker session defaults to database rcs-worker and a
host/port default of 127.0.0.1:27018; the lab points it at the internal shard
server. Acquisition must keep the two namespaces separate: grid.evidence
staging is not in the main rcs namespace
(config/mongoid.yaml:1-14).
On startup, the database enables sharding, creates global indexes, shards the
audit collection, creates default filters and queues, clears server sessions,
and prepares an ensured metadata backup (rcs-db/lib/rcs-db/db.rb:51-103). The MongoDB
configuration database is therefore part of the evidence needed to interpret
the logical application collections.
Global collection catalogue
| Collection | Principal records and relationships | Forensic cautions |
|---|---|---|
items | Polymorphic operations, targets, factories, and agents selected by _kind; hierarchy in path; access in user_ids/group_ids; configs, requests, and stats embedded | Also contains cryptographic/configuration material that routine output should omit |
users | Account name, bcrypt password field, privileges, enabled state, locale/timezone, dashboard and recent IDs | Do not expose hashes; current privileges may differ from session snapshot |
groups | Users and operation items; callbacks denormalize access into descendants and entities | Removal rebuild is asynchronous, so transient or orphaned ACL states are possible |
sessions | User or server identity, privilege array, cookie, address, last contact, version | Cookies are live credentials; acquire but redact from ordinary notes |
entities | Person/target/group/virtual records, hierarchy path, trust level, position, embedded handles and links, GridFS photo IDs | May contain highly identifying relationship and location data |
alerts | User-owned rules with path/type/keyword/action state and embedded trigger logs | Logs can reference evidence/entities and are automatically aged out after seven days |
audit | Time, actor, action, named object fields, description | Sharded by {time, actor}; useful but neither complete nor cryptographically chained |
audit_filters | Distinct values cached for audit filtering | Supporting index-like state, not an authoritative activity history |
collectors | Collector/anonymizer name, addresses, type, topology, cookie/key material | Secrets must be protected; related logs.<id> collection is separate |
injectors | Injector identity, address/state, embedded rules and configuration | Static/UI-only research scope; related capped log collection is separate |
connectors | Export type, destination/settings, hierarchy path, enabled and keep policy | Can explain data egress and deletion-after-export behavior |
connector_queue | Connector ID, scope, job type and data referencing target/evidence | Queue destruction can decrement evidence retention countdown |
statuses | Component name/type/address, status, version, resource information and time | A current retained status set, not necessarily a complete uptime history |
backups | Job name, schedule, scope, status and incremental IDs | Archives themselves live on disk, not solely in this collection |
signatures | Shared signature values by scope | Values are credentials; catalogue scope names without printing values |
cores | Available core package metadata | Operational artifacts excluded from execution; protect as sensitive binaries |
templates | Reusable agent configuration documents | Can contain collection behavior and infrastructure references |
filters | Saved evidence-filter presets, usually user-associated | Aids reconstruction of analyst workflow and search scope |
publics | Public-file metadata associated with delivery workflows | Not ordinary evidence; operational delivery use is outside scope |
peer_book | Normalized communication-handle book | Derived victim/associate identifiers can be highly sensitive |
watched_items | Dashboard item IDs to online-user IDs, rebuilt from users and sessions | Derived/volatile state; may be empty after restart or when nobody is online |
profile | Optional performance records when performance storage is enabled | URI and timing telemetry can reveal operator activity |
Mongoid creates some relationship keys and default collection names in addition to explicitly declared fields. Preserve actual BSON, indexes, and collection options rather than rebuilding a schema solely from this table.
Target-scoped evidence families
Creating a target prepares three related storage families:
| Namespace | Shard key or relationship | Core fields |
|---|---|---|
evidence.<target_id> | {type: 1, da: 1, aid: 1} | da acquisition time, dr receive time, type, rel, blo, note, string aid, data, kw |
aggregate.<target_id> | {type: 1, day: 1, aid: 1} | agent ID, YYYYMMDD/0 day, aggregation type, count, size, summary/timeframe info and data |
grid.<target_id>.files | GridFS file metadata | filename, length, chunk size, upload time, metadata; evidence refers through data._grid and _grid_size |
grid.<target_id>.chunks | {files_id: 1} | GridFS chunks joined to .files._id |
TargetScoped creates a duplicate model class whose collection name is the
lowercase prefix plus target ID (target_scoped.rb:13-49). The target ID in
the namespace organizes and shards records, but does not independently verify
that the caller belongs to that target.
The evidence type list includes address books, applications, calendars,
calls, camera images, chats, clipboards, device records, files, keylogs,
messages, microphone recordings, money, mouse data, passwords, positions,
prints, screenshots, and URLs. Four additional pseudo-view types—filesystem,
info, command, and IP—are excluded from normal statistics. Binary content is
stored in GridFS rather than inline when the processing path supplies a grid
reference (db_objects/evidence.rb:16-48).
The worker stages incoming encrypted blobs in rcs-worker as
grid.evidence.files/chunks, with filename set to the agent UID and a
creation timestamp in metadata. Per-instance processing fetches at most 100
files oldest-first, normally deletes the raw GridFS entry after processing,
and can leave decoded failure bytes in a worker filesystem directory
(instance_worker.rb:32-75,187-218). Those filesystem failures are outside
MongoDB and belong in an acquisition plan.
Queue and log collections
Notification queues are capped collections created with a default maximum of
100,000 entries and 50 MB; push_queue overrides that with 1,000 entries and
100 KB. Each uses a flag where zero is queued and one is processed. Because
the dispatcher flips the flag when claiming an item, processed entries may
remain in a capped queue until rotation.
| Collection | References or payload | Role |
|---|---|---|
alert_queue | Alert/evidence IDs, path, recipient and notification text | Alert dispatch |
push_queue | Push type and message hash | WebSocket notification delivery |
ocr_queue | Target and evidence IDs | OCR work |
trans_queue | Target and evidence IDs | Translation work |
aggregator_queue | Target/evidence IDs and type | Communication/location/URL aggregation |
intelligence_queue | Target ID, related evidence/aggregate ID and type | Entity/intelligence processing |
connector_queue | Connector, scope, action and referenced data | Local or remote export; not a capped NotificationQueue subclass |
Collectors and injectors receive dynamic capped collections named
logs.<component_id>. Each stores integer time, string type, and description,
with a maximum of 2,000 records or 1 MB. Deleting the component drops its log
collection; the log-clearing endpoint also drops rather than empties it
(db_objects/log.rb:6-46; collector/injector model callbacks). Collection
absence after a deletion is therefore expected behavior, not proof that no
logs ever existed.
Deletion and preservation relationships
The highest-risk storage actions cross collection boundaries:
- deleting one evidence document attempts to delete its referenced GridFS object and adjusts embedded statistics;
- bulk deletion records filter criteria rather than a durable list of every affected evidence ID;
- deleting an agent removes matching evidence from its parent target family;
- deleting a target drops its entire evidence, aggregate, and target GridFS collections;
- deleting an operation cascades through targets;
- connector queue completion can destroy evidence after all required exports;
- collector or injector deletion drops its capped component log; and
- backup restore can replace signatures and, depending on mode, drop collections before restoring them.
Forensic acquisition must consequently preserve relationships before invoking
the application. At minimum, record collection names, options, counts,
indexes, shard metadata, BSON types, GridFS file/chunk joins, filesystem
archives, worker staging, decoded failures, configuration, and logs. JSON-only
exports lose type distinctions such as string versus ObjectId aid values.
The read-only companion catalogue at
research/hackingteam-rcs/incident-response/MONGODB-QUERIES.md begins with
metadata and projections that omit password hashes, session cookies,
signature values, agent keys, and evidence bodies. All 24 blocks pass against
a network-disabled disposable MongoDB 2.6 synthetic clone, with eight targeted
assertions and unchanged pre/post database hashes. This does not make shell
access passive or authorize use against original evidence.
Known fidelity seams
This map applies to the examined source snapshot, not an abstract “RCS 9” API:
- component version markers disagree across
rcs-db, common library, and console sources; - the later console requests actions absent from the older controller, including evidence sync history and multi-update;
- task listing contains a source defect even though per-task polling works;
- the research web console adds
/api/and/wss/proxy prefixes that do not exist on the original backend; - mock fixtures and direct Moped seeds reproduce stored shapes without proving worker or implant provenance; and
- lab port mappings and internal-only worker/MongoDB exposure are defensive orchestration choices.
When a field, route, or collection differs in an acquired deployment, preserve the difference. It may reflect another version, customer configuration, migration, patch, corruption, defensive reconstruction, or compromise. The source map is a comparison baseline, not a repair specification.
Sources and evidence
- SOURCE:
lib/rcs-db/parser.rb,rest.rb,rest_response.rb,websocket.rb,sessions.rb,target_scoped.rb,grid.rb,db.rb,db_layer.rb,rest/*.rb, anddb_objects/*.rbin the frozen originalrcs-dbsnapshot; worker staging fromlib/rcs-worker/worker_controller.rbandinstance_worker.rb. - LAB:
docker-lab/docker-compose.ymlfor loopback mappings, internal service boundaries, database names, and MongoDB roles; live console tests described in Chapter 6; and the recorded but not yet publication-traced synthetic worker exercise described in Chapter 7. - INFERENCE: forensic prioritization follows from the source-defined relationships and deletion paths; it does not establish what a historical operator retained or exposed.
- LIMIT: no implant, exploit, collector, anonymizer, injector, build pipeline, or operational connector was executed for this appendix.
↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life