/**
* WS-Attacker - A Modular Web Services Penetration Testing Framework Copyright
* (C) 2010 Christian Mainka
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package wsattacker.plugin.soapActionSpoofing;
import com.eviware.soapui.impl.wsdl.WsdlInterface;
import com.eviware.soapui.impl.wsdl.WsdlOperation;
import com.eviware.soapui.impl.wsdl.WsdlRequest;
import com.eviware.soapui.impl.wsdl.WsdlSubmit;
import com.eviware.soapui.impl.wsdl.WsdlSubmitContext;
import com.eviware.soapui.impl.wsdl.support.soap.SoapUtils;
import com.eviware.soapui.model.iface.Operation;
import com.eviware.soapui.model.iface.Request.SubmitException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.xmlbeans.XmlException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import wsattacker.main.composition.plugin.AbstractPlugin;
import wsattacker.main.composition.plugin.option.AbstractOption;
import wsattacker.main.composition.plugin.option.AbstractOptionBoolean;
import wsattacker.main.composition.plugin.option.AbstractOptionChoice;
import wsattacker.main.composition.plugin.option.AbstractOptionVarchar;
import wsattacker.main.composition.testsuite.RequestResponsePair;
import wsattacker.main.plugin.PluginState;
import wsattacker.main.plugin.option.OptionSimpleBoolean;
import wsattacker.main.plugin.option.OptionSimpleVarchar;
import wsattacker.main.plugin.option.OptionSoapAction;
import wsattacker.main.testsuite.TestSuite;
import wsattacker.util.SoapUtilities;
import wsattacker.util.SortedUniqueList;
public class SoapActionSpoofing
extends AbstractPlugin
implements PropertyChangeListener
{
private static final long serialVersionUID = 2L;
private static final String NAME = "SOAPAction Spoofing";
private static final String DESCRIPTION =
"<html><p>This attack plugin checks if the server is vulnerable to SOAPAction Spoofing.</p>"
+ "<p>In automatic mode, all SOAPAction Headers, which are present in the WSDL, are used.</p>"
+ "<p>Manual mode can be used to use only a specific operation,"
+ "e.g. a public operation which does not damage the server.</p></html>";
private static final String AUTHOR = "Christian Mainka";
private static final String VERSION = "1.1 / 2013-06-26";
private static final String[] CATEGORY = new String[] { "Spoofing Attacks" };
private static final int MAXPOINTS = 3;
final private AbstractOptionBoolean automaticOption = new OptionSimpleBoolean( "Automatic", true,
"Choose SOAPAction automatically" );;
final private AbstractOptionChoice operationChooserOption = new OptionSoapAction( "Operation",
"Choose action manually" );;
final private AbstractOptionVarchar actionOption = new OptionSimpleVarchar( "Action", "", "Concrete action uri" );
private transient WsdlRequest originalRequest, attackRequest; // for loading
// a config,
// only
// options
// are
// important
private String originalAction;
final private List<AbstractOption> automaticModeOptions = new ArrayList<AbstractOption>();
final private List<AbstractOption> manualModeOption = new ArrayList<AbstractOption>();
@Override
public void initializePlugin()
{
initData();
initOptions();
initInternalState();
}
public void initData()
{
setName( NAME );
setDescription( DESCRIPTION );
setAuthor( AUTHOR );
setVersion( VERSION );
setCategory( CATEGORY );
setMaxPoints( MAXPOINTS );
}
private void initOptions()
{
// listeners:
automaticOption.addPropertyChangeListener( this );
operationChooserOption.addPropertyChangeListener( this );
actionOption.addPropertyChangeListener( this );
// The automatic options:
automaticModeOptions.add( automaticOption );
// The manual options:
manualModeOption.add( automaticOption );
manualModeOption.add( operationChooserOption );
manualModeOption.add( actionOption );
// Start with automatic Mode
getPluginOptions().setOptions( automaticModeOptions );
}
private void initInternalState()
{
setState( PluginState.Ready );
originalRequest = null;
originalAction = null;
}
@Override
public void attackImplementationHook( RequestResponsePair original )
{
// TODO: Add support for <wsa:action> SOAPAction Spoofing
// save needed pointers
originalRequest = original.getWsdlRequest();
originalAction = originalRequest.getOperation().getAction();
attackRequest = originalRequest.getOperation().addNewRequest( getName() + " ATTACK" );
// create an attack request
originalRequest.copyTo( attackRequest, true, true );
// detect first body child
Node originalChild;
try
{
originalChild = getBodyChild( original.getWsdlResponse().getContentAsString() );
info( "Using first SOAP Body child '" + originalChild.getNodeName() + "' as reference" );
}
catch ( Exception e )
{
log().error( "Could not detect first body child from response content. Plugin aborted \n"
+ originalRequest.getResponse().getContentAsString() );
setState( PluginState.Failed );
return;
}
// get attacking action
if ( automaticOption.isOn() )
{
info( "Automatic Mode" );
info( "Creating attack vector" );
List<String> attackActions = findAttackActions( originalRequest );
int anz = attackActions.size();
if ( anz == 0 )
{
info( "Could not find any suitable SOAPActions\n"
+ "This could indicate, that the server does not use SOAPAction Header\n"
+ "You could also choose a SOAPAction manually" );
setState( PluginState.Failed );
}
else
{
info( "Found " + anz + " suitable SOAPActions: " + attackActions.toString() );
trace( "Starting attack for each vector" );
for ( String soapAction : attackActions )
{
if ( getCurrentPoints() == getMaxPoints() )
{
// we can stop if we already got maximum number of
// points
info( "Stopping attack since we got the maximum number of points (" + getMaxPoints() + ")" );
break;
}
doAttackRequest( attackRequest, soapAction, originalChild );
}
setState( PluginState.Finished );
}
}
else
{
info( "Manual Mode" );
doAttackRequest( attackRequest, actionOption.getValueAsString(), originalChild );
}
// remove attack request
originalRequest.getOperation().removeRequest( attackRequest );
// delete references
attackRequest = null;
originalAction = null;
originalRequest = null;
switch ( getCurrentPoints() )
{
case 0:
info( "(0/3) Points: No attack possible. The Web Service is not vulnerable." );
break;
case 1:
important( "(1/3) Points: The server seems to have problems with the attack vectors. It should always return a SOAP Fault." );
break;
case 2:
critical( "(2/3) Points: The server ignores SOAPAction Header.\n"
+ "This can be abused to execute unauthorized operations, if authentication is controlled by HTTP." );
break;
case 3:
critical( "(3/3) Points: The server executes the Operation specified by the SOAPAction Header.\n"
+ "This can be abused to execute unauthorized operations, if authentication is controlled by the SOAP message." );
break;
}
}
@Override
public void propertyChange( PropertyChangeEvent pce )
{
if ( pce.getSource() == automaticOption )
{
if ( automaticOption.isOn() )
{
log().info( "Setting automatic mode options." );
getPluginOptions().setOptions( automaticModeOptions );
}
else
{
log().info( "Setting manual mode options." );
getPluginOptions().setOptions( manualModeOption );
}
}
else if ( pce.getSource() == operationChooserOption )
{
// try to get action by operationname
try
{
String chosenOperationName;
chosenOperationName = operationChooserOption.getValueAsString();
String soapActionValue;
soapActionValue =
TestSuite.getInstance().getCurrentInterface().getWsdlInterface().getOperationByName( chosenOperationName ).getAction();
log().info( String.format( "Setting SOAPAction: %s -> %s", chosenOperationName, soapActionValue ) );
actionOption.setValue( soapActionValue );
}
catch ( NullPointerException e )
{
actionOption.setValue( "No current Service" );
}
catch ( Exception e )
{
actionOption.setValue( "Error: " + e.getMessage() );
}
}
checkState();
}
@Override
public void clean()
{
setCurrentPoints( 0 );
setState( PluginState.Ready );
}
@Override
public void stopHook()
{
// restore possible data corruption
if ( originalAction != null && originalRequest != null
&& !originalRequest.getOperation().getAction().equals( originalAction ) )
{
originalRequest.getOperation().setAction( originalAction );
originalRequest = null;
originalAction = null;
}
if ( attackRequest != null )
{
attackRequest.getOperation().removeRequest( attackRequest );
attackRequest = null;
}
}
@Override
public boolean wasSuccessful()
{
// successfull only server is vulnerable for one method
// note: one point = possible server misconfiguration
return isFinished() && ( getCurrentPoints() > 1 );
}
public AbstractOptionBoolean getAutomaticOption()
{
return automaticOption;
}
public AbstractOptionChoice getOperationChooserOption()
{
return operationChooserOption;
}
public AbstractOptionVarchar getActionOption()
{
return actionOption;
}
/**
* Gets the first child of the SOAP Body from an XML String. This Version uses XPath.
*
* @param xmlContent
* @return
* @throws SAXException
*/
public Node getBodyChildWithXPath( String xmlContent )
throws SAXException
{
// final String SEARCH =
// "/*[namespace::'http://www.w3.org/2003/05/soap-envelope']";
String SEARCH = "/Envelope/Body/*[1]";
Document doc = SoapUtilities.stringToDom( xmlContent );
XPath xpath = XPathFactory.newInstance().newXPath();
Node node = null;
try
{
node = (Node) xpath.evaluate( SEARCH, doc, XPathConstants.NODE );
}
catch ( XPathExpressionException e )
{
log().warn( e );
}
return node;
}
/**
* Gets the first child of the SOAP Body from an XML String. This does exactly the same as getBodyChildWithXPath but
* it demonstrates the power of WS-Attackers SoapUtilities.
*
* @param xmlContent
* @return
* @throws SOAPException
*/
public Node getBodyChild( String xmlContent )
throws SOAPException
{
Node result = null;
SOAPMessage sm = SoapUtilities.stringToSoap( xmlContent );
// we need to return the first soapChild because there could also
// be a TextNode (whitespaces) as sm.getSOAPBody().getFirstChild()
List<SOAPElement> bodyChilds = SoapUtilities.getSoapChilds( sm.getSOAPBody() );
if ( bodyChilds.size() > 0 )
{
result = bodyChilds.get( 0 );
}
return result;
}
@Override
public void restoreConfiguration( AbstractPlugin plugin )
{
if ( plugin instanceof SoapActionSpoofing )
{
SoapActionSpoofing old = (SoapActionSpoofing) plugin;
// try to restore chooser
actionOption.setValue( old.getActionOption().getValue() );
operationChooserOption.setSelectedAsString( old.getOperationChooserOption().getValueAsString() );
// restore automatic mode
automaticOption.setOn( old.getAutomaticOption().isOn() );
}
}
private void doAttackRequest( WsdlRequest request, String soapAction, Node originalChild )
{
// set SOAPAction
info( "Using SOAPAction Header '" + soapAction + "'" );
request.getOperation().setAction( soapAction );
try
{
WsdlSubmit<WsdlRequest> submit = request.submit( new WsdlSubmitContext( request ), false );
String responseContent = submit.getResponse().getContentAsString();
if ( responseContent == null )
{
important( "The server's answer was empty. Server misconfiguration?\n" + "Got 1/3 Points" );
setCurrentPoints( 1 );
return;
}
trace( "Request:\n" + submit.getRequest().getRequestContent() + "\n\nResponse:\n" + responseContent );
try
{
if ( SoapUtils.isSoapFault( responseContent, request.getOperation().getInterface().getSoapVersion() ) )
{
info( "No attack possible, you got a SOAP error message." );
// exit
return;
}
}
catch ( XmlException e )
{
info( "The answer is not valid XML. Server missconfiguration?" );
setCurrentPoints( 1 );
}
// determine which operation corresponds to the response
Node responseChild;
try
{
responseChild = getBodyChild( responseContent );
if ( responseChild == null )
{
important( "There is no Child in the SOAP Body. Misconfigured Server?\n" + "Got 1/3 Points." );
setCurrentPoints( 1 );
return;
}
info( "Detected first body child: '" + responseChild.getNodeName() + "'" );
// this is for using getBodyChildWithXPath()
// } catch (SAXException e) {
// warn("Could not detect first body child from response content. Attack aborted \n"
// + responseContent);
// return;
}
catch ( SOAPException e )
{
info( "Could not parse response. " + e.getMessage() );
return;
}
if ( responseChild.getNodeName().equals( originalChild.getNodeName() ) )
{
important( "The server ignored the SOAPAction Header. It still executes the first child of the Body.\n"
+ "Got 2/3 Points" );
setCurrentPoints( 2 );
}
else
{
important( "The server accepts the SOAPAction Header " + soapAction
+ " and executes the corresponding operation.\n" + "Got 3/3 Points" );
setCurrentPoints( 3 );
}
}
catch ( SubmitException e )
{
info( "Could not submit the request. " + e.getMessage() );
}
finally
{
request.getOperation().setAction( originalAction );
}
}
private void checkState()
{
if ( automaticOption.isOn() )
{
setState( PluginState.Ready );
}
else
{
if ( operationChooserOption.getSelectedIndex() > 0 )
{
setState( PluginState.Ready );
}
}
}
private List<String> findAttackActions( WsdlRequest request )
{
List<String> ret = new SortedUniqueList<String>();
// Get the responding interface
WsdlInterface iface = request.getOperation().getInterface();
// loop through all available operations
for ( Operation op : iface.getOperationList() )
{
if ( op instanceof WsdlOperation )
{
// add action to return list
String action = ( (WsdlOperation) op ).getAction();
ret.add( action );
}
}
// remove current request action, since this action can not
// be used for SOAPAction Spoofing
ret.remove( request.getOperation().getAction() );
return ret;
}
}