type: #WEB #whitebox
difficulty: #hard
SOLVED by: #writeup
First, review the application overview.
- The application returns a screenshot of any page as a PNG image.
- The URL is specified by a GET parameter like
/api/screenshot?url=https%3A%2F%2Fwww.google.com%2F
. - The backend is implemented in JavaScript (Node.js) and opens the page with Headless Chromium to render the content.
- The flag is stored in
/flag.txt
.
In applications where the server accesses the URL specified by the user, an attack technique called SSRF (Server Side Request Forgery) is known.
(Note: The following article is written in Japanese; you may find a similar information in English by Googling.)
A Thorough Introduction to SSRF (Server Side Request Forgery) | Hiroshi Tokumaru’s Diary
Chrome can display the contents of local files using URLs starting with file://
, such as file:///etc/passwd
. Let’s try to use this to see if we can get it to take a screenshot of file:///flag.txt
. If you enter file:///flag.txt
in the form and submit it, unfortunately, a “Bad Request” error shows up and the attack will fail.
Let’s read the distributed source code to see why an error is displayed when we submit file:///flag.txt
. If you open index.js
, you will find the following code on line 20.
if (!req.query.url.includes("http") || req.query.url.includes("file")) {
res.status(400).send("Bad Request");
return; }
}
It seems that the system inspects the value of the req.query.url
, or url
parameter of the query string, and returns an error if it does not contain http
or contains file
as a substring.
console.log(req.query.url) // => "file:///flag.txt"
console.log(req.query.url.includes("http")) // => if the string coitains "http" (false)
console.log(req.query.url.includes("file")) // => if the string coitains "file" (true)
Actually, there is a flaw in this check.
When a query with multiple url
parameters is passed, such as ?url=abc&url=def
, the contents of req.query.url
will be an array. Since Array.prototype.includes()
is a method that returns whether a particular element is included or not, it cannot check whether a particular string is included as a substring.
console.log(req.query.url) // => ["abc", "def"].
console.log(req.query.url.includes("http")) // => if the **array** contains "http" (false)
console.log(req.query.url.includes("file")) // => if the **array** contains "file" (false)
Thus, when a query string such as ?url=file%3A%2F%2F%2Fflag.txt&url=http
is passed, it will pass the checks. Such a bug is sometimes called parameter type confusion.
Let’s look at what happens to the URLs opened in Chromium when multiple
url
parameters are passed. In line 27 of index.js
, we see the following
const params = new URLSearchParams(req.url.slice(req.url.indexOf("?")));
await page.goto(params.get("url"));
This code extracts the back part of the URL after the ?
, and uses URLSearchParams
to retrieve the value of the url
parameter. If you read the specification of URLSearchParams.prototype.get()
, you will see that when multiple identical parameters are passed, the first one is returned. That is, ?url=file%3A%2F%2F%2Fflag.txt&url=http
will return file:///flag.txt
.
it’s array url=[“file:///flag.txt” “http”] insted of string so the check won’t work
Thus, accessing /api/screenshot?url=file%3A%2F%2F%2Fflag.txt&url=http
will allow you to pass the checks and have Chrome open file:///flag.txt
.
Note that an unintended solution found during the event is to specify filE:///http/../flag.txt
as the URL. You can also get the flag this way (thanks for the report).
file scheme require :/// after it if you do a path relative path it will trigger error.
so the correct way is ///path “the absloute path”
FLAG{beawre_of_parameter_type_confusion!} DONE!!