siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Makima

Overview

Background

Enumeration

In this challenge, we can download a file:

┌[siunam♥earth]-(~/ctf/pbctf-2023/Web/Makima)-[2023.02.19|09:30:35(HKT)]
└> file dist.zip    
dist.zip: Zip archive data, at least v1.0 to extract, compression method=store
┌[siunam♥earth]-(~/ctf/pbctf-2023/Web/Makima)-[2023.02.19|09:30:38(HKT)]
└> unzip dist.zip   
Archive:  dist.zip
   creating: chall/
  inflating: chall/docker-compose.yml  
   creating: chall/cdn/
  inflating: chall/cdn/app.py        
  inflating: chall/cdn/Dockerfile    
   creating: chall/web/
   creating: chall/web/php/
   creating: chall/web/php/uploads/
  inflating: chall/web/php/uploads/makima.png  
  inflating: chall/web/php/index.php  
 extracting: chall/web/flag.txt      
   creating: chall/web/nginx/
  inflating: chall/web/nginx/www.conf  
  inflating: chall/web/nginx/default.conf  
  inflating: chall/web/Dockerfile

Right of the bat, we see cdn, php, uploads. Does that mean this challenge is about CDN (Content Delivery Network) and file upload in PHP?

docker-compose.yml:

┌[siunam♥earth]-(~/ctf/pbctf-2023/Web/Makima/chall)-[2023.02.19|09:38:29(HKT)]
└> cat docker-compose.yml 
version: '3.7'

services:
  web:
    build: ./web/
    ports:
        - "127.0.0.1:80:8080"

  cdn:
    build: ./cdn/

So this challenge has 2 services, web and cdn.

web/Dockerfile:

┌[siunam♥earth]-(~/ctf/pbctf-2023/Web/Makima/chall/web)-[2023.02.19|09:39:48(HKT)]
└> cat Dockerfile 
FROM debian:bullseye

RUN apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y \
        nginx \
        php7.4-fpm \
        php7.4-gd \
    && rm -rf /var/lib/apt/lists/

# nginx + fastcgi
RUN rm -rf /var/www/html/*
COPY nginx/default.conf /etc/nginx/sites-enabled/default
COPY nginx/www.conf /etc/php/7.4/fpm/pool.d/www.conf

RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
    ln -sf /dev/stderr /var/log/nginx/error.log

# php
RUN mkdir -p /var/www/html/uploads && \
    chmod 703 /var/www/html/uploads

COPY php/uploads/makima.png /var/www/html/uploads/makima.png
COPY php/index.php /var/www/html/index.php

COPY flag.txt /flag

USER root
EXPOSE 8080

CMD /etc/init.d/php7.4-fpm start && \
    nginx -g 'daemon off;'

The flag is in /flag.

web/php/index.php:

<?php
function makeimg($data, $imgPath, $mime) {
    $img = imagecreatefromstring($data);
    switch($mime){
        case 'image/png':
            $with_ext = $imgPath . '.png';
            imagepng($img, $with_ext);
            break;
        case 'image/jpeg':
            $with_ext = $imgPath . '.jpg';
            imagejpeg($img, $with_ext);
            break;
        case 'image/webp':
            $with_ext = $imgPath . '.webp';
            imagewebp($img, $with_ext);
            break;
        case 'image/gif':
            $with_ext = $imgPath . '.gif';
            imagegif($img, $with_ext);
            break;
        default:
            $with_ext = 0;
            break;
        }
    return $with_ext;
}

if(isset($_POST["url"])){ 
    $cdn_url = 'http://localhost:8080/cdn/' . $_POST["url"];
    $img = @file_get_contents($cdn_url);
    $f = finfo_open();
    $mime_type = finfo_buffer($f, $img, FILEINFO_MIME_TYPE);
    $fileName = 'uploads/' . substr(md5(rand()), 0, 13);
    $success = makeimg($img, $fileName, $mime_type);
    if ($success !== 0) {
        $msg = $success;
    }
} 
?>
[...]
            <h3> Submit Makima fan art: </h3>
            <?php if (isset($msg)) { ?>
                <p>Message: <?= htmlspecialchars($msg) ?></p>
            <?php } ?>
            <form method="post">
            <label>Upload Image:</label>
            <input type="text" name="url">
            </form>
[...]

Let’s break it down!

Hmm… At the first galance, I think it suffers path traversal and file upload vulnerability? Maybe we can try to upload a PHP webshell.

cdn/app.py:

from flask import *
import requests

app = Flask(__name__)

@app.errorhandler(requests.exceptions.MissingSchema)
@app.errorhandler(requests.exceptions.InvalidSchema)
def bad_schema(e):
    return 'no HTTP/S?', 400

@app.errorhandler(requests.exceptions.ConnectionError)
def no_connect(e):
    print("CONNECT ERR")
    return 'I did not understand that URL', 400


    
@app.route("/cdn/<path:url>")
def cdn(url):
    mimes = ["image/png", "image/jpeg", "image/gif", "image/webp"]
    r = requests.get(url, stream=True)
    if r.headers["Content-Type"] not in mimes:
        print("BAD MIME")
        return "????", 400
    img_resp = make_response(r.raw.read(), 200)
    for header in r.headers:
        if header == "Date" or header == "Server":
            continue
        img_resp.headers[header] = r.headers[header]
    return img_resp
# ZoneMinder

if __name__ == "__main__":
    app.run(debug=False, port=8081)

It’s a Flask back-end.

In here, it has a route /cdn/<url>:

Home page:

Armed with above information, we could try to exploit the file upload.

Exploitation

First, how do we upload a file?

In index.php, the image is from /cdn/<url>.

How can we reach there? It’s an internal service!

I tried figure that out, but still no dice…