Martin's Blog

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 endpointInternal endpointFunctionBoundary
127.0.0.1:44443rcs-db:443REST APIOriginal server behind a lab-only port mapping
127.0.0.1:44444rcs-db:444WebSocket pushOriginal server behind a lab-only port mapping
127.0.0.1:8080console-web:80Research SPA and same-origin reverse proxyWeb-port behavior, not an original RCS listener
nonercs-worker:442Evidence staging and decodingCompose-network internal only
nonemongo:27017-27019Router, shard, and configuration rolesCompose-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 shapeDefault action
GET /thingindex
GET /thing/<id>show, with the identifier copied to _id
POST /thingcreate
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:

  1. Session authentication. Every action requires a valid session unless its controller lists it in bypass_auth.
  2. Licence checks. A small number of actions require a named entitlement before session handling.
  3. Role and sub-privilege checks. require_auth_level tests the symbolic array captured in the session at login. Sequential checks can require both a base role such as VIEW and a sub-privilege such as VIEW_DELETE.
  4. Object membership. Some queries constrain items or entities by user_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/actionReplacement controlPurpose and caution
/auth/loginCredentials or component signatureCreates a user or server session
/auth/logoutNone required to clear the presented cookieIdempotent session termination
/auth/resetSource checks peer address equals 127.0.0.1Local password reset; proxy topology matters
/position createNone in the controllerResolves supplied map data through the position resolver
/sync/evidence, /items, /status, /agent, /sync_eventArchive licence plus X-Sync-SignatureArchive-node synchronization surface
/sync/setupArchive licence; bootstrap semantics when signatures do not yet existSeeds 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 prefixPrincipal or nominal accessPublic actions in examined controllerStorage or effect
/authBypassed actionslogin, reset, logoutUsers, sessions, audit, WebSocket lifecycle
/sessionAdminindex, destroySession inventory and revocation
/userMostly admin; self-related update/recent paths also span logged-in rolesindex, show, create, update, add_recent, destroy, messageUsers, groups, sessions, dashboard/recent state, pushes
/groupAdmin plus user-management privilege for mutationsindex, show, create, update, destroy, user/operation membership actions, alertGroups and denormalized item/entity access
/auditAdmin plus audit privilegeindex, count, filtersSharded audit records and filter values
/licenseValid session for limit/count; admin licence privilege for uploadlimit, count, createRuntime licence state and licence file
/statusAdmin, system, technician, or server depending on actionindex, create, destroy, countersComponent status and counters
/signatureServer, admin, or systemshowComponent/shared signature scopes
/version, /logoAny valid sessionindex; version also showVersion metadata, console package, logo file
/operationLogged-in admin/tech/view reads; admin operations privilege mutatesCRUD_kind: operation documents in items
/targetLogged-in admin/tech/view reads; admin targets privilege mutatesCRUD plus move_kind: target documents and target collection lifecycle
/factoryTech/view reads; tech mutatesindex, show, update, destroy_kind: factory documents and embedded configurations
/agentServer, tech, or view according to actionCRUD plus configuration, status, sync, request, upload/download, upgrade, purge, blacklist, and filesystem/command actions_kind: agent documents, embedded requests/configs/stats, evidence lifecycle
/searchBroad logged-in roles for index; admin/tech/view for showindex, showCross-item/entity discovery constrained by controller logic
/entityView plus profiles for most actions; promotion also requires admin target authorityCRUD, flow/positions, photo/handle/link actions, promotion, merge, summary actionsEntities, embedded handles/links, GridFS photos
/evidenceView or narrower evidence privileges; server/tech import for ingestion actionsCRUD, bulk delete, translate, body, synchronization lifecycle, index, count, info, total, filesystem, commands, ips, worker lookupPer-target evidence, GridFS, stats, alerts and processing queues
/gridTech/view read; tech uploadshow, createDefault or target GridFS buckets
/fileAny ordinary role for read; destroy declares an unusual none privilegeshow, destroyTemporary task files
/filterViewindex, create, destroySaved evidence filters
/alertView plus alerts privilegeCRUD, counters, log deletion actionsPer-user alerts with embedded logs
/positionCreate bypasses session authcreatePosition-resolution result; no MongoDB model write in controller
/taskBroad valid roles plus type-dependent sub-privilegesindex, show, create, destroy, downloadIn-memory per-user task manager and temporary files
/uploadTechcreateMultipart content moved to temporary storage
/templateTech plus configuration privilegeCRUDConfiguration templates
/coreBroad read; system/tech mutationCRUDCore metadata and files; operational use excluded from the lab
/buildTechcreate, symbian_confBuild pipeline; prohibited and render-only in the web lab
/exploitTech plus build privilegeindex, showStatic exploit catalogue metadata; no package is run by this project
/collectorServer/system/tech depending on actionCRUD, topology lookup, version/config/upgrade, logs, relay and cookie actionsCollector/anonymizer records and per-component capped logs
/injectorServer/system/tech depending on actionCRUD, version/config/logs, rule management, upgradeInjector records, embedded rules, capped logs; static/UI-only research boundary
/connectorSystem plus connectors privilegeCRUDConnector policy and asynchronous export queue
/publicTech build reads/deletes; server deletes delivered filesindex, destroy, destroy_filePublic delivery documents and GridFS/file state; not exercised operationally
/shardSystem plus backend privilegeindex, show, create, destroyShard topology and database statistics
/backupjobSystem plus backup privilegeindex, create, run, update, destroyBackup job documents and archive creation
/backuparchiveSystem plus backup privilegeindex, destroy, restoreOn-disk backup archives and restoration
/syncArchive component signature; normal sessions bypassedevidence, items, status, setup, agent, sync_eventArchive 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

CollectionPrincipal records and relationshipsForensic cautions
itemsPolymorphic operations, targets, factories, and agents selected by _kind; hierarchy in path; access in user_ids/group_ids; configs, requests, and stats embeddedAlso contains cryptographic/configuration material that routine output should omit
usersAccount name, bcrypt password field, privileges, enabled state, locale/timezone, dashboard and recent IDsDo not expose hashes; current privileges may differ from session snapshot
groupsUsers and operation items; callbacks denormalize access into descendants and entitiesRemoval rebuild is asynchronous, so transient or orphaned ACL states are possible
sessionsUser or server identity, privilege array, cookie, address, last contact, versionCookies are live credentials; acquire but redact from ordinary notes
entitiesPerson/target/group/virtual records, hierarchy path, trust level, position, embedded handles and links, GridFS photo IDsMay contain highly identifying relationship and location data
alertsUser-owned rules with path/type/keyword/action state and embedded trigger logsLogs can reference evidence/entities and are automatically aged out after seven days
auditTime, actor, action, named object fields, descriptionSharded by {time, actor}; useful but neither complete nor cryptographically chained
audit_filtersDistinct values cached for audit filteringSupporting index-like state, not an authoritative activity history
collectorsCollector/anonymizer name, addresses, type, topology, cookie/key materialSecrets must be protected; related logs.<id> collection is separate
injectorsInjector identity, address/state, embedded rules and configurationStatic/UI-only research scope; related capped log collection is separate
connectorsExport type, destination/settings, hierarchy path, enabled and keep policyCan explain data egress and deletion-after-export behavior
connector_queueConnector ID, scope, job type and data referencing target/evidenceQueue destruction can decrement evidence retention countdown
statusesComponent name/type/address, status, version, resource information and timeA current retained status set, not necessarily a complete uptime history
backupsJob name, schedule, scope, status and incremental IDsArchives themselves live on disk, not solely in this collection
signaturesShared signature values by scopeValues are credentials; catalogue scope names without printing values
coresAvailable core package metadataOperational artifacts excluded from execution; protect as sensitive binaries
templatesReusable agent configuration documentsCan contain collection behavior and infrastructure references
filtersSaved evidence-filter presets, usually user-associatedAids reconstruction of analyst workflow and search scope
publicsPublic-file metadata associated with delivery workflowsNot ordinary evidence; operational delivery use is outside scope
peer_bookNormalized communication-handle bookDerived victim/associate identifiers can be highly sensitive
watched_itemsDashboard item IDs to online-user IDs, rebuilt from users and sessionsDerived/volatile state; may be empty after restart or when nobody is online
profileOptional performance records when performance storage is enabledURI 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:

NamespaceShard key or relationshipCore 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>.filesGridFS file metadatafilename, 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.

CollectionReferences or payloadRole
alert_queueAlert/evidence IDs, path, recipient and notification textAlert dispatch
push_queuePush type and message hashWebSocket notification delivery
ocr_queueTarget and evidence IDsOCR work
trans_queueTarget and evidence IDsTranslation work
aggregator_queueTarget/evidence IDs and typeCommunication/location/URL aggregation
intelligence_queueTarget ID, related evidence/aggregate ID and typeEntity/intelligence processing
connector_queueConnector, scope, action and referenced dataLocal 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:

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:

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

↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life