OWASP Juice Shop Sensitive Data Exposure Walkthrough
Table of Contents
This writeup is to help NKCyber Club Members walk through the steps for the first few solutions to the Sensitive Data Exposure section of OWASP Juice Shop. Similar articles include instructions for administering OWASP’s Juice Shop and last week’s XSS solutions.
Solutions
Confidential Document
Access a confidential document. (Difficulty Level: 1)
The description alone doesn’t give a lot to work with, so it’s worth taking a look at the first hint:
/* /ftp directory browsing and file download */ .use("/ftp", serveIndexMiddleware, serveIndex("ftp", { icons: true })); app.use("/ftp(?!/quarantine)/:file", fileServer()); app.use("/ftp/quarantine/:file", quarantineServer()); app /* /encryptionkeys directory browsing */ .use( app"/encryptionkeys", , serveIndexMiddlewareserveIndex("encryptionkeys", { icons: true, view: "details" }) ; ).use("/encryptionkeys/:file", keyServer()); app /* /logs directory browsing */ .use( app"/support/logs", , serveIndexMiddlewareserveIndex("logs", { icons: true, view: "details" }) ; ).use("/support/logs/:file", logFileServer()); app /* Swagger documentation for B2B v2 endpoints */ .use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app .use(express.static(path.resolve("frontend/dist/frontend"))); app.use(cookieParser("kekse")); app
Here, we see a lot of routes defined on an Express.js router, with app.use.
For each line that we see app.use
, we can imagine we’re
taking some part of the URL, and doing something special when we
navigate to that path.
Let’s take a look at the first line, for example:
.use("/ftp", serveIndexMiddleware, serveIndex("ftp", { icons: true })); app
The app
(defined elsewhere) will match all routes
starting with /ftp
, and then serve an index of all of the
files to the user. Then, it will call some function serveIndex
with the parameter "ftp"
. We can look up that function, and
find where it came from.
Looking at the npm page for serve-index:
The
path
is based off thereq.url
value, so areq.url
of'/some/dir
with apath
of'public'
will look at'public/some/dir'
. If you are using something likeexpress
, you can change the URL “base” withapp.use
(see the express example).
So, great! Let’s try going to /ftp
on the server, and
seeing what happens.
Woah, neat! It creates an index of all of the files on the FTP server.
Some of them are pretty boring. Here’s legal.md
, for
example:
# Legal Information
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit
... [document continues]
However, one document in particular stands out,
acquisitions.md
:
# Planned Acquisitions
> This document is confidential! Do not distribute!
... [document continues]
This seems like the confidential document.
Once we have requested this file, we can return to the home page at
/
.
From there, we should get our flag for the challenge Confidential Document.
Exposed Metrics
Find the endpoint that serves usage data to be scraped by a popular monitoring system. (Difficulty Level: 1)
Looking at the problem description, I saw that there was a link to the Prometheus monitoring tool, which I had never heard of before.
I searched “where is prometheus default endpoint” on Google, and got the featured snippet:
Starting Prometheus You can also verify that Prometheus is serving metrics about itself by navigating to its own metrics endpoint: http://localhost:9090/metrics.
So, I went to /metrics
, and got the flag.
Looking through the code they provided in the hint, we can find the route defined on line 4.
See full code
/* Serve metrics */
let metricsUpdateLoop: any;
const Metrics = metrics.observeMetrics();
.get("/metrics", metrics.serveMetrics());
app.title = `${config.get(
errorhandler"application.name"
} (Express ${utils.version("express")})`;
)
const registerWebsocketEvents = require("./lib/startup/registerWebsocketEvents");
const customizeApplication = require("./lib/startup/customizeApplication");
export async function start(readyCallback: any) {
const datacreatorEnd = startupGauge.startTimer({ task: "datacreator" });
await sequelize.sync({ force: true });
await datacreator();
datacreatorEnd();
const port = process.env.PORT ?? config.get("server.port");
process.env.BASE_PATH =
process.env.BASE_PATH ?? config.get("server.basePath");
= Metrics.updateLoop();
metricsUpdateLoop
.listen(port, () => {
server.info(
logger.cyan(`Server listening on port ${colors.bold(`${port}`)}`)
colors;
).set({ task: "ready" }, (Date.now() - startTime) / 1000);
startupGaugeif (process.env.BASE_PATH !== "") {
.info(
logger.cyan(
colors`Server using proxy base path ${colors.bold(
`${process.env.BASE_PATH}`
} for redirects`
)
);
)
}registerWebsocketEvents(server);
if (readyCallback) {
readyCallback();
};
})
}
export function close(exitCode: number | undefined) {
if (server) {
clearInterval(metricsUpdateLoop);
.close();
server
}if (exitCode !== undefined) {
process.exit(exitCode);
} }
.get("/metrics", metrics.serveMetrics()); app
Once we see that the server routes to that path, we know we can visit it.
From there, we can navigate back to the homepage to get the flag.
Meta Geo Stalking
Determine the answer to John’s security question by looking at an upload of him to the Photo Wall and use it to reset his password via the Forgot Password mechanism. (Difficulty Level: 2)
To start this challenge, I went to the Photo Wall:
Looking through the gallery of happy juice connoisseurs, we stumble upon a post by j0hNny, who very well might be the John from the problem description:
Okay, so let’s go to the Forgot Password page, and see what information we need to find from this image.
First, we can enter John’s email: john@juice-sh.op
.
It looks like John has selected “What’s your favorite place to go hiking?” as his security question, and then conveniently posted a picture of him going hiking.
Now we just need to figure out where this photo was taken.
Unfortunately, Rainbolt seems to be too busy to figure this one out, and I’m not very good at GeoGuessr. We’re going to have to inspect the image he’s posted more carefully.
Luckily, there’s more to an image than just the visuals. Many common
file types (including .jpg
, .png
,
.webp
, and more) include something called Exif data which stores
additional information about the image, including camera settings, image
metrics, date and time information, and potentially the geolocation
where the image was taken.
Using your provided Kali VM, you may note that exiv2
is
installed, which is a special tool for managing image metadata.
You can use it to analyze more information about the image John posted:
$ # First, ensure that exiv2 is installed
$ which exiv2
/usr/bin/exiv2
$ # Then, download John's image
$ wget http://localhost:3000/assets/public/images/uploads/favorite-hiking-place.png
[...output ommitted...]
$ # Finally, get all of the Exif data from the image
$ exiv2 -g GPS favorite-hiking-place.png
Exif.Image.GPSTag Long 1 50
Exif.GPSInfo.GPSVersionID Byte 4 2.2.0.0
Exif.GPSInfo.GPSLatitudeRef Ascii 2 North
Exif.GPSInfo.GPSLatitude Rational 3 36deg 58' 0"
Exif.GPSInfo.GPSLongitudeRef Ascii 1 West
Exif.GPSInfo.GPSLongitude Rational 3 84deg 21' 0"
Exif.GPSInfo.GPSMapDatum Ascii 6 WGS-84
You can also use identify
from ImageMagick:
$ # ensure identify is installed
$ which identify
/usr/bin/identify
$ identify -verbose favorite-hiking-place.png | grep GPS
exif:GPSInfo: 50
exif:GPSLatitude: 36/1, 57523/1000, 0/1
exif:GPSLatitudeRef: N
exif:GPSLongitude: 84/1, 20893/1000, 0/1
exif:GPSLongitudeRef: W
exif:GPSMapDatum: WGS-84
exif:GPSVersionID: ....
Either way works, and it gives us the coordinates
36°58'0"N 84°21'0"W
(view on Google
Maps).
This is part of Daniel Boone National Forest. (Super beautiful place btw. I can totally recommend it.)
I got lucky that John had the same taste in capitalization as Google Maps, but I imagine there are quite a number of possible locations that one might want to enter.
Either way, this is a good reminder of how security questions rely on information that can be deduced or leaked, and are not very secure.
Also, it’s important to automatically strip exif location data from user images before hosting them publicly. Almost all social media websites do this automatically.
Poison Null Byte
Bypass a security control with a Poison Null Byte to access a file not meant for your eyes. (Difficulty Level: 4)
From the linked definition:
A null byte (NUL character) can have different meanings across representations or languages. For example, it is a string terminator in standard C libraries, but Perl and PHP strings do not treat it as a terminator. When two representations are crossed - such as when Perl or PHP invokes underlying C functionality - this can produce an interaction error with unexpected results. Similar issues have been reported for ASP. Other interpreters written in C might also be affected.
The poison null byte is frequently useful in path traversal attacks by terminating hard-coded extensions that are added to a filename. It can play a role in regular expression processing in PHP.
— Null Byte Interaction Error (Poison Null Byte) - Martello Security
Because basically, when we go to a URL, we are requesting a resource
with a certain name. Whether we can see that resource depends on if it
ends in .md
or .pdf
. So, the null byte is a
tool we can use to create a separation between the resource we’re
requesting and what the string ends with, such that we can bypass checks
for file extensions.
Here are two examples:
Forgotten Sales Backup
Access a salesman’s forgotten backup file. (Difficulty Level: 4)
It’s great to keep backups! It’s not great for random people to be able to steal your backups.
This begins similarly to the Confidential Document challenge, shown above.
Let’s look back at our ftp server:
In particular, we’re looking for a sales backup.
Out of all the files here, it looks like two could be backups:
coupons_2013.md.bak
package.json.bak
Of those, the package.json.bak
doesn’t look like
something related to a salesperson, so let’s take a look at
coupons_2013.md.bak
.
Ah, it looks like we’re not allowed to view .bak
files.
So, we somehow need to convince the server that we’re accessing a
.md
or .pdf
file, when we’re actually
requesting the .md.bak
file.
Using the technique from the Poison Null Byte challenge, we can start
by identifying the resource we want to download. In this case, that’s
/ftp/coupons_2013.md.bak
.
Now, we append a file extension that is allowed, and join it with a
null byte: /ftp/coupons_2013.md.bak%00.pdf
Next, we URL
encode the percent sign:
/ftp/coupons_2013.md.bak%2500.pdf
.
Finally, we can download the file to get our flag:
$ wget http://localhost:3000/ftp/coupons_2013.md.bak%2500.pdf -O coupons_2013.md
Note that you will have to go back to the Juice Shop index to see your flag for this challenge.
You should see the flag for the Poison Null Byte challenge as well.
Forgotten Developer Backup
Using the exact same technique, we can simply do:
$ wget http://localhost:3000/ftp/package.json.bak%2500.md -O package.json.bak
to complete this challenge.
Conclusion
Congratulations! 🎉
I hope you learned something new today.
Come back next week for our foreign exchange meeting, where we’ll be learning more about other injection techniques.
As always, feel free to reach out to me with feedback about this week’s lesson. Thanks! 😊