WaniCTF 2023 - (web) certified
Author: ciffelia
Description: We have released a secure web application using a state-of-the-art language!
https://certified-web.wanictf.org
There are two flags in this problem. Please submit the flag in file
/flag_A
to certified1 and one in the environment variableFLAG_B
to certified2.Note: “承認, ワニ博士” means “Approved, Dr. Wani” in Japanese.
Introduction
The challenge is a Rust webapp where we can upload an image and get the image with an approval stamp overlayed on it.
The challenge files are available here.
There are two flags in this challenge as explained in the description. We’ll call them flag A and flag B.
Flag A
Taking a look at the Dockerfile
, we notice that it downloads ImageMagick, which is used in process_image.rs
to add the stamp to the bottom-right of our uploaded image after resizing.
ARG MAGICK_URL="https://github.com/ImageMagick/ImageMagick/releases/download/7.1.0-51/ImageMagick--gcc-x86_64.AppImage"
RUN curl --location --fail -o /usr/local/bin/magick $MAGICK_URL && \
chmod 755 /usr/local/bin/magick
ENV APPIMAGE_EXTRACT_AND_RUN=1
let child = Command::new("sh")
.args([
"-c",
"timeout --signal=KILL 5s magick ./input -resize 640x480 -compose over -gravity southeast ./overlay.png -composite ./output.png",
])
.current_dir(working_directory)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.context("Failed to spawn")?;
A specific version is used (7.1.0-51), which is not the latest version at the time of the CTF (7.1.1-8). Since this tool is used for the main feature of the webapp, it made me suspicious about the presence of potential vulnerabilities in this specific version.
Researching CVE databases pointed me towards CVE-2022-44268. The vulnerability allows arbitrary files on the server to be read, which is exactly what we need since we want to read /flag_A
. Moreover, it triggers when the resize function is used, which is precisely the case here.
ImageMagick 7.1.0-49 is vulnerable to Information Disclosure. When it parses a PNG image (e.g., for resize), the resulting image could have embedded the content of an arbitrary. file (if the magick binary has permissions to read it).
We can easily find a PoC of this CVE on GitHub. We’ll follow the instructions and craft a image that contains the content of /flag_A
after being processed by the server.
Let’s run the following commands (Rust needs to be installed):
git clone https://github.com/voidz0r/CVE-2022-44268 && cd CVE-2022-44268 && cargo run "/flag_A"
Our payload will be generated in image.png
. Let’s upload it to the webapp and download the resulting image.
Now let’s take a look at the image metadata with ImageMagick, which should contain the flag hex-encoded (assuming we downloaded the image to imageA.png
and the ImageMagick binary is in the same folder):
./magick identify -verbose imageA.png
Decoding the hex string (with CyberChef, for example), we get the flag A!
FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}
Flag B
Now we need read the environment variables of the webapp process to get flag B. I didn’t find any known vulnerability of ImageMagick that would allow RCE, so I attempted to get the flag by fetching the /proc/self/environ
file with our previous method. It should also be in /proc/1/environ
, since the webapp process has a process ID of 1 because it’s the starting process of the Docker container. We can check that it indeed contains the flag by spinning up a local Docker container and exploring it:
However, both of them returned nothing for some reason. (Edit: After reading the official writeup, I found out that it was because /proc/self/environ
has an apparent size of 0, and the ImageMagick cannot read it because of that.)
At that point I decided to read the source code of the challenge to check for other possible vulnerabilities. Eventually, I noticed something strange in the code of the POST /create
endpoint, which processes and converts our uploaded image.
In the handle_create()
function of create.rs
, our uploaded file is first saved in /data/{uuid}/{filename}
, where {uuid} is an random UUID and {filename} is taken from our form parameter. Then, in the process_image()
function of process_image.rs
, it is copied once more to /data/{uuid}/input
, which seems redundant.
fs::write(
current_dir.join(file_name.file_name().unwrap_or("".as_ref())),
file_data,
)
fs::copy(
working_directory.join(input_filename),
working_directory.join("input"),
)
The join()
method of std::path::PathBuf
is called with a filename that we control. By reading the Rust documentation, it seems that this could allow path traversal:
We also notice that in the first case, the file_name()
method of std::path::PathBuf
is called with our filename which can strip path traversal sequences according to the documentation, but not in the second case.
For example, if we submit an image with /proc/self/environ
as filename, our image will first be copied to /data/{uuid}/environ
(with the filename properly sanitized), but then the contents of /proc/self/environ
will be copied to /data/{uuid}/input
, since file_name()
is not called!
If we then use /data/{uuid}/input
with the CVE PoC, the magick binary will be able to read it since its apparent size won’t be anymore. Let’s try uploading an arbitrary image and intercept with Burp Suite to change the filename (Note: ../../proc/self/environ
also works).
In the response, we get an image processing failure error (because /data/{uuid}/input
now contains the environments variables and the server tries to process it as an image). Luckily for us, the error message leaks the generated UUID, thus we know the exact path of the file we need to fetch!
Let’s execute the PoC once again, like we did for /flag_A
.
cargo run "/data/b531e6bd-bd2f-49d4-94bc-cf2b43a0c47c/input"
./magick identify -verbose imageB.png
We get the environment variables containing flag B!
FLAG{n0w_7hat_y0u_h4ve_7he_sec0nd_f1a9_y0u_4re_a_cert1f1ed_h4nk0_m@ster}