/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2013 The ZAP Development team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.parosproxy.paros.core.scanner;
/**
* Simplified GWT RPC Variant only set to not-empty strings parameter...
* It takes the RPC call as it has been retrieved, check only for
* java.util.String objects, and give back only the one that has a
* set value. If a string has been set to null (so it comes in the
* request with the value 0 inside the index) it's discarded.
* To manage null string we need to rebuild the overall payload
* and currently there are some lack of information regarding the
* real serialization model used by the GWT environment (e.g. how
* Long are encoded, how can be serialized vectors, how validation
* classes work, how are managed proxy objects and, finally,
* the use of encoded types.
*
* @author yhawke
*/
public class VariantGWTQuery extends VariantAbstractRPCQuery {
public static final String GWT_RPC_CONTENT_TYPE = "text/x-gwt-rpc";
public static final int RPC_SEPARATOR_CHAR = '|';
public static final int FLAG_RPC_TOKEN_INCLUDED = 0x2;
/**
*
* @param contentType
* @return
*/
@Override
public boolean isValidContentType(String contentType) {
return contentType.startsWith(GWT_RPC_CONTENT_TYPE);
}
/**
*
* @param content
*/
@Override
public void parseContent(String content) {
GWTStringTokenizer st = new GWTStringTokenizer(content, RPC_SEPARATOR_CHAR);
int version = Integer.parseInt(st.nextToken());
int flags = Integer.parseInt(st.nextToken());
// Read the type name table
int columns = Integer.parseInt(st.nextToken());
int[] stringTableIndices = new int[columns + 1];
String[] stringTable = new String[columns];
for (int i = 0; i < columns; i++) {
stringTableIndices[i] = st.getPosition();
stringTable[i] = st.nextToken();
}
// Now get the index of the last one
stringTableIndices[columns] = st.getPosition();
// Start read the first elements
// inside the RPC stringTable
// following the RPC index table
// ---------------------------------------
String moduleBaseUrl = stringTable[Integer.parseInt(st.nextToken()) - 1];
String strongName = stringTable[Integer.parseInt(st.nextToken()) - 1];
String rpcToken = null;
// rpc request has an rpc/xsrf token
if ((flags & FLAG_RPC_TOKEN_INCLUDED) == FLAG_RPC_TOKEN_INCLUDED) {
// Read the RPC token
rpcToken = stringTable[Integer.parseInt(st.nextToken()) - 1];
}
// Get service and method name
// should be obfuscated but it's not important for
// this request parser
String serviceInterfaceName = stringTable[Integer.parseInt(st.nextToken()) - 1];
String serviceMethodName = stringTable[Integer.parseInt(st.nextToken()) -1];
// Get the number of parameters
int paramCount = Integer.parseInt(st.nextToken());
// Get each parameter type
String[] parameterTypes = new String[paramCount];
for (int i = 0; i < parameterTypes.length; i++) {
// --
// Simpler method for parameter type retrieval
// should be interpreted the hash value after the / char
// see also encoded types manipulation how works
// --
parameterTypes[i] = stringTable[Integer.parseInt(st.nextToken()) - 1];
}
for (int i = 0; i < paramCount; i++) {
String strIndex = st.nextToken();
if (parameterTypes[i].startsWith("java.lang.String")) {
int idx = Integer.parseInt(strIndex);
if (idx > 0) {
addParameter(String.valueOf(i), stringTableIndices[idx - 1], stringTableIndices[idx] - 1, false, true);
}
}
}
}
/**
* Escape a GWT string according to the client implementation found on
* com.google.gwt.user.client.rpc.impl.ClientSerializationStreamWriter
* http://www.gwtproject.org/
*
* @param value the value that need to be escaped
* @param toQuote
* @return
*/
@Override
public String getEscapedValue(String value, boolean toQuote) {
// Escape special characters
StringBuilder buf = new StringBuilder(value.length());
int idx = 0;
int ch;
while (idx < value.length()) {
ch = value.charAt(idx++);
if (ch == 0) {
buf.append("\\0");
} else if (ch == 92) { // backslash
buf.append("\\\\");
} else if (ch == 124) { // vertical bar
// 124 = "|" = AbstractSerializationStream.RPC_SEPARATOR_CHAR
buf.append("\\!");
} else if ((ch >= 0xD800) && (ch < 0xFFFF)) {
buf.append(String.format("\\u%04x", ch));
} else {
buf.append((char) ch);
}
}
return buf.toString();
}
/**
* Unescape a GWT serialized string according to the server implementation found on
* com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader
* http://www.gwtproject.org/
*
* @param value the value that need to be deserialized
* @return the deserialized value
*/
@Override
public String getUnescapedValue(String value) {
// Change quoted characters back
if (value.indexOf('\\') < 0) {
return value;
} else {
StringBuilder buf = new StringBuilder(value.length());
int idx = 0;
char ch;
while (idx < value.length()) {
ch = value.charAt(idx++);
if (ch == '\\') {
if (idx == value.length()) {
//Unmatched backslash, skip the backslash
break;
}
ch = value.charAt(idx++);
switch (ch) {
case '0':
buf.append('\u0000');
break;
case '!':
buf.append((char)RPC_SEPARATOR_CHAR);
break;
case '\\':
buf.append(ch);
break;
case 'u':
try {
if (idx + 4 < value.length()) {
ch = (char)Integer.parseInt(value.substring(idx, idx + 4), 16);
buf.append(ch);
} else {
//Invalid Unicode hex number
//skip the sequence
}
} catch (NumberFormatException ex) {
//Invalid Unicode escape sequence
//skip the sequence
}
idx += 4;
break;
default:
//Unexpected escape character
//skip the sequence
}
} else {
buf.append(ch);
}
}
return buf.toString();
}
}
/* Code available for complete GWT parsing -----
private List<NameValuePair> params = new ArrayList();
private RPCRequest request = null;
@Override
public void setMessage(HttpMessage msg) {
// First check if it's a gwt rpc form data request
// Otherwise give back an empty param list
String contentType = msg.getRequestHeader().getHeader(HttpHeader.CONTENT_TYPE);
if (contentType.startsWith(GWT_RPC_CONTENT_TYPE)) {
request = RPCRequestHandler.decodeRequest(msg.getRequestBody().toString());
for (RPCParameter p : request.getParameters()) {
if (p.getTypeSignature().startsWith("java.lang.String")) {
params.add(new NameValuePair(String.valueOf(p.getPosition()), (String)p.getValue(), p.getPosition()));
}
}
}
}
*
private String setParameter(HttpMessage msg, NameValuePair originalPair, String name, String value, boolean escaped) {
String query = RPCRequestHandler.encodeRequest(request, originalPair.getPosition(), value);
msg.getRequestBody().setBody(query);
return query;
}
*/
/**
* This is a replacement for the standard StringTokenizer which handles
* multiple delimiters differently. Given the input "A,B,,,E", a
* StringTokenizer would return three tokens: "A", "B", and "E" (collapsing
* the repeated ",,," into a single delimiter ","). A
* NoncollapsingStringTokenizer instead returns five tokens: "A", "B", "",
* "", "E". The repeated delimiters are taken to indicate empty fields, so
* an empty string "" is returned where appropriate.
*/
protected class GWTStringTokenizer {
private String str;
private int delim;
private int currentPosition;
public GWTStringTokenizer(String str, int delim) {
this.str = str;
this.delim = delim;
}
public String nextToken() {
int nextDelimPosition = str.indexOf(delim, currentPosition);
if (nextDelimPosition < 0) {
nextDelimPosition = str.length();
}
String token = str.substring(currentPosition, nextDelimPosition);
currentPosition = nextDelimPosition + 1;
return token;
}
public boolean hasMoreTokens() {
return (currentPosition < str.length());
}
public int getPosition() {
return currentPosition;
}
}
}