Source Code Review Grep Script

Edit: This evolved over years, see the CRASS project.

As a pentester you sometimes get access to the source code of the application you are reviewing. Sometimes you can look manually through the files, but sometimes you get million lines of code and you don’t have time to spend sitting there, reading line after line.

The first approach that came to my mind was to use static code analysis tools. There are a lot of them out there, you can find lists of tools on the flawfinder page or you can have a look at Wikipedia’s list of tools for static code analysis. It definitely makes sense to use these tools, but it needs time to download/compile them and most of them are written for one particular language.

You will be reading a lot of code. If you want to do yourself a favour, use an IDE like eclipse to look at the code. Especially when looking at Java code you will find yourself quite often changing from one class to another and changing between files. With eclipse you only need one click for that. But still, you need different IDEs for different programming languages. So this is still not a universal approach.

As a penetration tester you often want to find the interesting parts of the code. To name some interesting things: everything related to cryptography, encryption, SQL queries, file read and writes, URLs and sockets, obfuscation, passwords and so on. And there is one really universal tool that let us find these parts of the code: grep.

The script I wrote here is pretty focused on Java/Android and Objective-C/iOS. But I also got some JSP and spring java framework specific code. So here we go, no rocket science, but I hope it’s helpful for someone in the future.

#!/bin/bash
#
# A simple code greper...
#
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42):
# <floyd at floyd dot ch> wrote this file. As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a beer in return
# floyd http://floyd.ch @floyd_ch <floyd at floyd dot ch>
# August 2012
# ----------------------------------------------------------------------------
#
# Tested under MAC OSX ONLY!
#
# This script isn't very advanced - exactly what's needed if you don't know where to start.
# It is not a real static analysis tool and it's not in any way a replacement for all the cool
# tools out there (checkstyle, jlint, etc.)
#
 
 
if [ $# -ne 1 ]
then
  echo "Usage: `basename $0` directory-to-grep-through"
  exit 0
fi
###
#OPTIONS
###
#Open the colored outputs with "less -R" or cat, otherwise remove --color=always
ADDITIONAL_GREP_ARGUMENTS="-A 1 -B 3 --color=always"
TARGET="./grep-output"
#In my opinion I would always leave all the options below here on true,
#because I did find strange android code in iphone apps and vice versa. I would only
#change it if the greping needs very long, you are greping a couple of hundret apps
#or if you have any other performance issues with this script.
DO_JAVA=true
DO_SPRING=true
DO_JSP=true
DO_ANDROID=true
DO_IOS=true
DO_PHP=true
DO_GENERAL=true
###
#END OPTIONS
#Normally you don't have to change anything below here...
###
 
GREP_ARGUMENTS="-nrP"
STANDARD_GREP_ARGUMENTS=$ADDITIONAL_GREP_ARGUMENTS" "$GREP_ARGUMENTS
SEARCH_FOLDER=$1
mkdir $TARGET
 
echo "Your standard grep arguments: $STANDARD_GREP_ARGUMENTS"
echo "Output will be put into this folder: $TARGET"
echo "You are currently greping through folder: $SEARCH_FOLDER"
sleep 2
 
#The Java stuff
if [ $DO_JAVA ]; then
    SEARCH_STRING='javax.crypto|bouncy.*?castle|new\sSecretKeySpec\(|messagedigest'
    OUTFILE="java_general_crypto.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
     
    SEARCH_STRING='toString\(\) *==|== *toString\(\)|" *==|== *"'
    OUTFILE="java_general_wrong_string_comparison.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #String comparison has to be done with .equals() in Java, not with ==
     
    SEARCH_STRING='\.exec\('
    OUTFILE="java_general_exec.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
     
    SEARCH_STRING='java\.net\.|java\.io\.|javax\.servlet|org\.apache\.http'
    OUTFILE="java_general_io.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
     
    SEARCH_STRING='@Entity|@ManyToOne|@OneToMany|@OneToOne|@Table|@Column'
    OUTFILE="java_persistent_beans.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #Special case, we only want to know matching files to know which beans get persisted, therefore -l to output matching files
    grep -l $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Find out which Java Beans get persisted with javax.persistence
     
    SEARCH_STRING='@Table\(|@Column\('
    OUTFILE="java_persistent_tables_and_columns_in_database.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #Case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #The source code shows the database table/column names... e.g. if you find a sql injection later on
     
    SEARCH_STRING='string .{0,10}password|string .{0,10}secret|string .{0,10}key|string .{0,10}cvv|string .{0,10}user|string .{0,10}hash(?!(table|map|set|code))|string .{0,10}passcode|string .{0,10}passphrase|string .{0,10}user|string .{0,10}pin|string .{0,10}credit'
    OUTFILE="java_confidential_data_in_strings.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #http://docs.oracle.com/javase/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx
fi
 
#The Java Spring specific stuff
if [ $DO_SPRING ]; then
    SEARCH_STRING="DataBinder\.setAllowedFields"
    OUTFILE="java_spring_mass_assignment.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #see e.g. http://blog.fortify.com/blog/2012/03/23/Mass-Assignment-Its-Not-Just-For-Rails-Anymore
fi
 
#The JSP specific stuff
if [ $DO_JSP ]; then
    SEARCH_STRING="escape\s*=\s*\"?\s*false|escape\s*=\s*\'?\s*false"
    OUTFILE="java_jsp_xss.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #can introduce XSS when using escape=false
     
    SEARCH_STRING="<s:file "
    OUTFILE="java_jsp_file_upload.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
fi
 
#The Android specific stuff
if [ $DO_ANDROID ]; then
 
    SEARCH_STRING='\.printStackTrace\(|Log\.(e|w|i|d|v)\('
    OUTFILE="android_logging.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #printStackTrace Logs to Android log, information leakage, etc.
     
    SEARCH_STRING='MODE_|\.openFile\(|\.openOrCreate|\.getDatabase\(|\.openDatabase\(|\.getShared|\.getCache|\.getExternalCache|query\(|rawQuery\(|compileStatement\('
    OUTFILE="android_access.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Android file io and access things
     
    SEARCH_STRING='<intent-filter>|\.getIntent\(\)\.getData\(\)|RunningAppProcessInfo'
    OUTFILE="android_intents.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
     
fi
 
#The iOS specific stuff
 
if [ $DO_IOS ]; then
    SEARCH_STRING='NSFileProtection|NSFileManager|NSPersistantStoreCoordinator|NSData' #sqlite, see sql.txt
    OUTFILE="ios_file_access.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #File protection APIs
 
    SEARCH_STRING='kSecAttrAccessible|SecItemAdd|KeychainItemWrapper|Security\.h'
    OUTFILE="ios_keychain.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Keychain stuff
 
    SEARCH_STRING='CFBundleURLSchemes|kCFStream|CFFTPStream|CFHTTP|CFNetServices|FTPURL|IOBluetooth'
    OUTFILE="ios_network.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Network stuff
 
    SEARCH_STRING='NSLog\('
    OUTFILE="ios_logging.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
 
    SEARCH_STRING='initWithFormat:|informativeTextWithFormat:|format:|stringWithFormat:|appendFormat:|predicateWithFormat:|NSRunAlertPanel'
    OUTFILE="ios_string_format_functions.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #just check if the first argument to these functions are user controlled, that could be a format string vulnerability
 
    SEARCH_STRING='handleOpenURL:|openURL:'
    OUTFILE="ios_url_handler.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
fi

#The PHP stuff

if [ $DO_PHP ]; then
    SEARCH_STRING='\$_GET|\$_POST'
    OUTFILE="php_get_post.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    
    SEARCH_STRING='crypt\('
    OUTFILE="php_crypt_call.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
fi

#The general stuff
 
if [ $DO_GENERAL ]; then
    SEARCH_STRING='\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,4}\b'
    OUTFILE="email.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Email addresses
    
    SEARCH_STRING='todo|workaround'
    OUTFILE="todo_workaround.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
 
    SEARCH_STRING='hack|crack|exploit|bypass|backdoor|backd00r'
    OUTFILE="exploit.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" | grep -vE 'Ack|setCdrBackdoor' | grep -viE 'imageshack' > $TARGET/$OUTFILE
 
    SEARCH_STRING='https?://'
    OUTFILE="https_and_http_urls.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #All URIs
 
    SEARCH_STRING='http://|ftp://|imap://|file://'
    OUTFILE="no_ssl_uris.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Non-SSL URIs
 
    SEARCH_STRING='malloc\(|realloc\('
    OUTFILE="initialisation.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #rather rare bug, but see issues CVE-2010-0041 and CVE-2010-0042... could also happen in java/android native code...
 
    SEARCH_STRING='memcpy\(|memset\(|strcat\(|strcpy\(|strncat\(|strncpy\(|sprintf\(|gets\('
    OUTFILE="insecure_c_functions.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #memcpy
    #memset
    #strcat --> strlcat
    #strcpy --> strlcpy
    #strncat --> strlcat
    #strncpy --> strlcpy
    #sprintf --> snprintf
    #vsprintf --> vsnprintf
    #gets --> fgets
 
    SEARCH_STRING='default.?password|passwo?r?d|passcode|hash.?(?!(table|map|set|code))|pass.?phrase|salt|encryption.?key|encrypt.?key|BEGIN CERTIFICATE---|PRIVATE KEY---|Proxy-Authorization|pin'
    OUTFILE="keys.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
 
    SEARCH_STRING='root.*?detection|rooted.*?Device|is.*?rooted|detect.*?root|jail.*?break'
    OUTFILE="root.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
 
    SEARCH_STRING='sql.{0,10}injection|xss|click.{0,10}jacking|xsrf|directory.{0,10}listing|buffer.{0,10}overflow|obfuscate'
    OUTFILE="hacking_techniques.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #e.g. find prevention techniques...
 
    SEARCH_STRING='`.{2,100}`'
    OUTFILE="backticks.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #-I for binaries=without-match
    grep -I $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #anything between backticks is suspicious, thinking about command execution in perl scripts in cgi-bin directories...
    SEARCH_STRING='location\.hash|location\.href|location\.pathname|location\.search|eval\(|\.appendChild\(|document\.write\(|document\.writeln\(|\.innerHTML\s*?=|\.outerHTML\s*?='
    OUTFILE="dom_xss.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    #case sensitive...
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
 
    SEARCH_STRING='SELECT.*?FROM|INSERT.*?INTO|DELETE.*?WHERE|sqlite'
    OUTFILE="sql.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
 
    SEARCH_STRING='^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$'
    OUTFILE="base64.txt"
    #case sensitive, the regex is insensitive anyway
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #Sometimes developers try to hide stuff in base64...
 
    SEARCH_STRING='GNU\sGPL|GPLv2|GPLv3|GPL\sVersion|General\sPublic\sLicense'
    OUTFILE="gpl.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
    #GPL violation, not security related, but your customer might be happy to know such stuff...
 
    SEARCH_STRING='stupid|fuck|shit|crap'
    OUTFILE="swear.txt"
    echo "Searching for $SEARCH_STRING --> writing to $OUTFILE"
    grep -i $STANDARD_GREP_ARGUMENTS "$SEARCH_STRING" "$SEARCH_FOLDER" > $TARGET/$OUTFILE
fi
 
echo "Done grep. Results in $TARGET. Have a grepy day."

Of course the script produces a lot of false positives, but it should be a tool that supports you in your manual analysis. I’m sure there are a million more interesting strings we can add to the script. If you think something is missing, leave it in the comments and I’ll add it.

Oh and if you already looked at one version of a source code and you get a new version, you better use the “diff” command line tool and first have a look at the parts that changed.

A Weird Idea – Java, Strings and Memory

Disclaimer: I’m not a Java internals specialist. Let me know if I got a point wrong here.

Wiping sensitive information when you don’t need it anymore is a very basic security rule. It makes sense to everyone when we talk about hard discs, but the same rule should be applied to memory (RAM). While memory management is quite straight forward in C derived languages (eg. allocate some memory, override it with random bytes when the sensitive information is not needed anymore), it’s quite hard to do it in Java. Is it even possible? Java Strings are immutable, that means you can not change the memory allocated for a String. The Java VM will make a copy of the String and change the copy. So from this perspective, Strings are not a good way to store sensitive information. So let’s do it in char arrays I thought and wrote the following Java class:

/**
* ----------------------------------------------------------------------------
* &quot;THE BEER-WARE LICENSE&quot; (Revision 42):
* &lt;floyd at floyd dot ch&gt; wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return
* floyd http://floyd.ch @floyd_ch &lt;floyd at floyd dot ch&gt;
* August 2012
* ----------------------------------------------------------------------------
**/

import java.util.Arrays;
import java.io.IOException;
import java.io.Serializable;
import java.lang.Exception;

/**
 * ATTENTION: This method is only ASCII safe, not Unicode.
 * It should be used to store sensitive information which are ASCII
 * 
 * This class keeps track of the passed in char[] and wipes all the old arrays
 * whenever they are changed. This means:
 * 
 * char[] r1 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41};
 * SecureString k = new SecureString(r1);
 * k.destroy();
 * System.out.println(r1); //Attention! r1 will be {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
 * 
 * char[] r2 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41};
 * SecureString k = new SecureString(r2);
 * k.append('C');
 * System.out.println(r2); //Attention! r2 will be {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
 * System.out.println(k.getByteArray()); //correct
 * 
 * General rule for this class:
 * - Never call toString() of this class. It will just return null:
 * char[] r3 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41};
 * SecureString k = new SecureString(r3);
 * System.out.println(k); //will print null, because it calls toString()
 * 
 * - Choose the place to do the destroy() wisely. It should be done when an operation
 * is finished and the string is not needed anymore.
 */

public class SecureString implements Serializable{
	private static final long serialVersionUID = 1L;
	private char[] charRepresentation;
	private char[] charRepresentationLower;
	private byte[] byteRepresentation;
	private boolean isDestroyed = false;
	private int length=0;
	private final static int OBFUSCATOR_BYTE = 0x55;
	
	/**
	 * After calling the constructor you should NOT wipe the char[]
	 * you just passed in (the stringAsChars reference)
	 * @param stringAsChars
	 */
	public SecureString(char[] stringAsChars){
		//Pointing to the same address as original char[]
		this.length = stringAsChars.length;
		this.initialise(stringAsChars);
	}
	
	/**
	 * After calling the constructor you should NOT wipe the byte[]
	 * you just passed in (the stringAsBytes reference)
	 * @param stringAsBytes
	 */
	public SecureString(byte[] stringAsBytes){
		//Pointing to the same address as original byte[]
		this.length = stringAsBytes.length;
		this.initialise(stringAsBytes);
	}
	
	/**
	 * @return length of the string
	 */
	public int length(){
		return this.length;
	}
	
	/**
	 * Initialising entire object based on a char array
	 * @param stringAsChars
	 */
	private void initialise(char[] stringAsChars){
		this.length = stringAsChars.length;
		charRepresentation = stringAsChars; 
		charRepresentationLower = new char[stringAsChars.length];
		byteRepresentation = new byte[stringAsChars.length];
		for(int i=0; i&lt;stringAsChars.length; i++){
			charRepresentationLower[i] = Character.toLowerCase(charRepresentation[i]);
			byteRepresentation[i] = (byte)charRepresentation[i];
		}
	}
	
	/**
	 * Initializing entire object based on a byte array
	 * @param stringAsBytes
	 */
	private void initialise(byte[] stringAsBytes){
		this.length = stringAsBytes.length;
		byteRepresentation = stringAsBytes;
		charRepresentation = new char[stringAsBytes.length];
		charRepresentationLower = new char[stringAsBytes.length];
		for(int i=0; i&lt;stringAsBytes.length; i++){
			charRepresentation[i] = (char) byteRepresentation[i];
			charRepresentationLower[i] = Character.toLowerCase(charRepresentation[i]);
		}
	}
	
	/**
	 * We first obfuscate the string by XORing with OBFUSCATOR_BYTE
	 * So it doesn't get serialized as plain text. THIS METHOD
	 * WILL DESTROY THIS OBJECT AT THE END.
	 * @param out
	 * @throws IOException
	 */
	private void writeObject(java.io.ObjectOutputStream out) throws IOException{		
		out.writeInt(byteRepresentation.length);
		for(int i = 0; i &lt; byteRepresentation.length; i++ ){
			out.write((byte)(byteRepresentation[i]) ^ (byte)(OBFUSCATOR_BYTE));
		}
		//TODO: Test if the Android Intent is really only calling writeObject once,
		//otherwise this could be a problem. Then I suggest that the SecureString
		//is not passed in an intent (and never serialized)
		destroy();
	}
	
	/**
	 * We first have to deobfuscate the string by XORing with OBFUSCATOR_BYTE
	 * @param out
	 * @throws IOException
	 */
	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
		//we first have to deobfuscate
		int newLength = in.readInt();
		byteRepresentation = new byte[newLength];
		for(int i = 0; i &lt; newLength; i++ ){
			byteRepresentation[i] = (byte) ((byte)(in.read()) ^ (byte)(OBFUSCATOR_BYTE));
		}
		initialise(byteRepresentation);
	}
	
	/**
	 * Use this method wisely
	 * 
	 * When the destroy() method of this SecureString is called, the byte[] 
	 * this method had returned will also be wiped!
	 * 
	 * Never WRITE directly to this byte[], but only READ
	 * 
	 * @return reference to byte[] of this SecureString
	 */
	public byte[] getByteArray(){
		if(this.isDestroyed)
			return null;
		return byteRepresentation;
	}
	
	/**
	 * This method should never be called.
	 */
	@Deprecated
	@Override
	public String toString(){
		return null;
	}
	
	/**
	 * This method should never be called, because it means you already stored the
	 * sensitive information in a String.
	 * @param string
	 * @return
	 * @throws Exception
	 */
	
	@Deprecated
	public boolean equals(String string) throws Exception{
		throw new Exception(&quot;YOU ARE NOT SUPPOSED TO CALL equals(String string) of SecureString, &quot;+
				&quot;because your are not supposed to have the other value in a string in the first place!&quot;);
	}
	/**
	 * This method should never be called, because it means you already stored the
	 * sensitive information in a String.
	 * @param string
	 * @return
	 * @throws Exception
	 */
	
	@Deprecated
	public boolean equalsIgnoreCase(String string) throws Exception{
		throw new Exception(&quot;YOU ARE NOT SUPPOSED TO CALL equalsIgnoreCase(String string) of SecureString, &quot;+
				&quot;because your are not supposed to have the other value in a string in the first place!&quot;);
	}
	/**
	 * Comparing if the two strings stored in the SecureString objects are equal
	 * @param secureString2
	 * @return true if strings are equal
	 */
	public boolean equals(SecureString secureString2){
		if(this.isDestroyed)
			return false;
		return Arrays.equals(this.charRepresentation, secureString2.charRepresentation);
	}
	
	/**
	 * Comparing if the two strings stored in the SecureString objects are equalsIgnoreCase
	 * @param secureString2
	 * @return true if strings are equal ignoring case
	 */
	public boolean equalsIgnoreCase(SecureString secureString2){
		if(this.isDestroyed)
			return false;
		return Arrays.equals(this.charRepresentationLower, secureString2.charRepresentationLower);
	}
	
	/**
	 * Delete a char at the given position
	 * @param index
	 */
	public void deleteCharAt(int index){
		if(this.isDestroyed)
			return;
		if(this.length == 0 || index &gt;= length)
			return;
		wipe(charRepresentationLower);
		wipe(byteRepresentation);
		char[] newCharRepresentation = new char[length-1];
		for(int i=0; i&lt;index; i++)
			newCharRepresentation[i] = charRepresentation[i];
		for(int i=index+1; i&lt;this.length-1; i++)
			newCharRepresentation[i-1] = charRepresentation[i];
		wipe(charRepresentation);
		initialise(newCharRepresentation);
	}
	
	/**
	 * Append a char at the end of the string
	 * @param c
	 */
	public void append(char c){
		wipe(charRepresentationLower);
		wipe(byteRepresentation);
		char[] newCharRepresentation = new char[length+1];
		for(int i=0; i&lt;this.length; i++)
			newCharRepresentation[i] = charRepresentation[i];
		newCharRepresentation[length-1] = c;
		wipe(charRepresentation);
		initialise(newCharRepresentation);
	}
	
	/**
	 * Internal method to wipe a char[]
	 * @param chars
	 */
	private void wipe(char[] chars){
		for(int i=0; i&lt;chars.length; i++){
			//set to hex AA
			int val = 0xAA;
			chars[i] = (char) val; 
			//set to hex 55
			val = 0x55;
			chars[i] = (char) val; 
			//set to hex 00
			val = 0x00;
			chars[i] = (char) val; 
		}
	}
	
	/**
	 * Internal method to wipe a byte[]
	 * @param chars
	 */
	private void wipe(byte[] bytes){
		for(int i=0; i&lt;bytes.length; i++){
			//set to hex AA
			int val = 0xAA;
			bytes[i] = (byte) val; 
			//set to hex 55
			val = 0x55;
			bytes[i] = (byte) val; 
			//set to hex 00
			val = 0x00;
			bytes[i] = (byte) val;
		}
	}
	
	/**
	 * Safely wipes all the data
	 */
	public void destroy(){
		if(this.isDestroyed)
			return;
		wipe(charRepresentation);
		wipe(charRepresentationLower);
		wipe(byteRepresentation);
		
		//loose references
		charRepresentation = null; 
		charRepresentationLower = null; 
		byteRepresentation = null; 
		
		this.length = 0;
		
		//TODO: call garbage collector?
		//Runtime.getRuntime().gc();
		
		this.isDestroyed = true;		
	}
	
	/**
	 * This method is only to check if non-sensitive data
	 * is part of the SecureString
	 * @return true if SecureString contains the given String
	 */
	public boolean contains(String nonSensitiveData){
		if(this.isDestroyed)
			return false;
		if(nonSensitiveData.length() &gt; this.length)
			return false;
		char[] nonSensitiveDataChars = nonSensitiveData.toCharArray();
		int positionInNonSensitiveData = 0;
		for(int i=0; i &lt; this.length; i++){
			if(positionInNonSensitiveData == nonSensitiveDataChars.length)
				return true;
			if(this.charRepresentation[i] == nonSensitiveDataChars[positionInNonSensitiveData])
				positionInNonSensitiveData++;
			else
				positionInNonSensitiveData = 0;
		}
		return false;
	}
}

Does that make any sense? How does the Java VM handle this? And how does the Android Dalvik Java VM handle this? Does the Android system cache the UI inputs anyway (so there would be no point in wiping our copies)? Is the data really going to be wiped when I use this class? Where are the real Java internals heroes out there?

Update 1, 11th September 2013: Added Beerware license. Note: This was just some idea I had. Use at your own risk.

Sending generic HTTP(S) requests in python

During Web Application Penetration tests I always need to automate requests, e.g. for fuzzing. While most of the local proxy/testing softwares (Burp, WebScarab, w3af, etc.) include a repeater/fuzzer feature, I often want to do addtional computations in python (e.g. calculating a hash and sending it as a fuzzed value or comparing parts of the response). The following script will take an entire HTTP(S) request as a string, parse it and send it to the server. As I show with the POST parameter “fuzzableParam” in this example, values can easily be fuzzed.

def send_this_request(http_request_string, remove_headers=None):
    """
    Always HTTP/1.1
    """
    import urllib2
    if remove_headers is None:
        remove_headers=['content-length', 'accept-encoding', 'accept-charset', 
        'accept-language', 'accept', 'keep-alive', 'connection', 'pragma', 
        'cache-control']
    for i, remove_header in enumerate(remove_headers):
        remove_headers[i] = remove_header.lower()
    if '\n\n' in http_request_string:
        headers, body = http_request_string.split('\n\n',1)
    else:
        headers = http_request_string
        body = None
    headers = headers.split('\n')
    request_line = headers[0]
    headers = headers[1:]

    method, rest = request_line.split(" ", 1)
    url, protocol = rest.rsplit(" ", 1)

    merge_host_header_into_url = False
    if url.startswith("http"):
        merge_host_header_into_url = False
    elif url.startswith("/"):
        info("Warning: Defaulting to HTTP. Please write URL as https:// if you want SSL")
        merge_host_header_into_url = True
    else:
        fatalError("Protocol not supported. URL must start with http or /")

    header_tuples = []
    for header in headers:
        name, value = header.split(": ", 1)
        if merge_host_header_into_url and name.lower() == 'host':
            url = 'http://'+value+url
        if not name.lower() in remove_headers:
            header_tuples.append((name, value))
            
    opener = urllib2.build_opener()
    opener.addheaders = header_tuples
    urllib2.install_opener(opener)

    try:
        return urllib2.urlopen(url, body, 15).read()
    except urllib2.HTTPError, e:
        info('The server couldn\'t fulfill the request. Error code:', e.code)
    except urllib2.URLError, e:
        info("URLError:", e.reason)
    except Exception, e:
        error("DIDNT WORK:", e)
        
def info(*text):
    print "[PY-INFO] "+str(" ".join(str(i) for i in text))

def error(*text):
    print "[PY-ERROR] "+str(" ".join(str(i) for i in text))

request = '''POST http://example.com/ HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Content-Type: application/x-www-form-urlencoded;charset=utf-8
Referer: http://example.com
Content-Length: 132
Cookie: test=somevalue; abc=123
DNT: 1
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

id=123&fuzzableParam='''

additionalValue = "&anotherParam=abc"

for i in ['78', '-1']:
    print send_this_request(request+i+additionalValue)

Ack-All-Happy-Scapy – Finding a hole in a corporate firewall

When being located in a corporate environment (internal network), it is sometimes interesting to know if there are ports that are not outbound filtered, or in other words, if there is a hole where an attacker could connect to the outside world (damn perimeter-security). For example Apple products need port 5223 to be open for push notifications. So if the iPhones and iPads of managers should work, you have to open that outbound port 😀 . Of course you can simply chose one of those ports for your reverse shell when you take over one of their web servers in a later step. So what’s the easiest way to check if there is an open port, apart from knowing that they use the Apple push notification?

The following script can be run on every server, that has a public IP and Python/Scapy installed. When this script is running, it will send back a TCP SYN/ACK to every SYN coming from outside. It doesn’t matter which port. So if you do a NMAP SYN-Scan (-sS switch), all ports will be shown as open. Unless the corporate firewall between you and the server is blocking the SYN probes. So simply do a nmap SYN-Scan from the internal network of the company to the server and each open port is an open outbound port (unless there is some more filtering active such as deep packet inspection).

#!/usr/bin/python
# -*- coding: utf-8 -*-
DEBUG_ON=False
def ack-all-happy-scappy():
    from scapy.all import sniff, send, Ether, IP, TCP
    import os
    #################
    #CONFIG OPTIONS
    #################
    
    #Standard options
    my_ip = "xxx.xxx.xxx.xxx" #your external IP
    my_interface = "eth0"
    exclude_ports = ["22"] # Exclude ports, that already have a service running 22 = SSH,
    DEBUG_ON = False
    
    #Advanced options
    static_seq = 1337 #Specify as None for random seq number
    start_iptables_command = "iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP"
    end_iptables_command = "iptables -D OUTPUT -p tcp --tcp-flags RST RST -j DROP"
    
    #################
    #CONFIG END
    #################
    
    #Actual code start
    if os.geteuid() != 0:
      info("You must be root to run this script.")
      sys.exit(1)    
    
    info("##################################")
    info("The ACK-ALL-HAPPY-SCAPY script, written by floyd")
    info("This script can only be used with SYN-scans (nmap -sS)")
    info("Altough untested, this should work as well for IPv6")
    info("##################################")
    sleep(3)
    info("This is how the IPTABLES looked, before starting ACK-ALL-HAPPY-SCAPY:")
    executeInShell("iptables -L")
    
    def getSeqNumber():
        if static_seq:
            return static_seq
        else:
            import random
            return random.randint(1,4294967295)
        
    def handleEachSyn(synpacket):
        if DEBUG_ON:
            debug("IN:")
            synpacket.display()
        ethlen = len(Ether())
        iplen = len(IP())
        synpacket_raw = str(synpacket)
        i = IP(synpacket_raw[ethlen:])
        t = TCP(synpacket_raw[ethlen + iplen:])
        f = IP(src=i.dst, dst=i.src)/TCP(sport=t.dport, dport=t.sport, ack=t.seq+1, seq=getSeqNumber())
        if DEBUG_ON:
            debug("OUT:")
            f.display()
        send(f)
        
    try:
        #Setup
        info("Executing now:", start_iptables_command)
        executeInShell(start_iptables_command)
        info("Done!")
        #Work
        not_port_filter = " and not port "+" and not port ".join(exclude_ports)
        filter_string = 'tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) = 0 and dst '+my_ip+not_port_filter
        info("Using filter ", filter_string)
        info("Waiting for your scans on tcp ports 1-65535, except "+", ".join(exclude_ports)+", where already a real service should be waiting")
        info("Start your scan with: sudo nmap -PN -sS -p 1-65535 "+my_ip)
        sniff(filter=filter_string, iface=my_interface, prn=handleEachSyn)
    except KeyboardInterrupt:
        #Restoring
        info()
        info("You pressed Ctrl+C... please wait, restoring IPTABLES")
        info("Executing now:", end_iptables_command)
        for i in range(3):
            executeInShell(end_iptables_command)
        info("This is how the IPTABLES looks, after finishing ACK-ALL-HAPPY-SCAPY:")
        executeInShell("iptables -L")

def executeInShell(command):
    import subprocess
    process = subprocess.Popen(command, shell=True)
    process.wait()

def sleep(seconds):
    import time
    time.sleep(seconds)

def info(*text):
    print "[PY-INFO] "+str(" ".join(str(i) for i in text))

def debug(*text):
    if DEBUG_ON:
        print "[PY-DEBUG] "+str(" ".join(str(i) for i in text))

main()

Today it shouldn’t be a big problem to start this script on your server, even when you can’t use your corporate network internet access. Just use your mobile phone to connect to the server and start the script.

Btw, Scapy is one of the most amazing Python libraries I’ve ever seen. Extremely powerful.

DNS zone transfer

Today I thought it would be cool to have a list of all domains that exist in Switzerland. As it turns out, the swiss registrar (Switch) has configured their nameservers correctly, so you can not do a DNS zone transfer 🙁 . But I found out that a lot of other TLDs allow to make zone transfers. I don’t know if its on purpose, but I don’t think so, because not all of their DNS root servers allow to do the transfer… Try it yourself (bash script):

tlds="AC AD AE AERO AF AG AI AL AM AN AO AQ AR ARPA AS ASIA AT AU AW AX AZ BA BB BD BE BF BG BH BI BIZ BJ BM BN BO BR BS BT BV BW BY BZ CA CAT CC CD CF CG CH CI CK CL CM CN CO COM COOP CR CU CV CX CY CZ DE DJ DK DM DO DZ EC EDU EE EG ER ES ET EU FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GOV GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN INFO INT IO IQ IR IS IT JE JM JO JOBS JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MG MH MIL MK ML MM MN MO MOBI MP MQ MR MS MT MU MUSEUM MV MW MX MY MZ NA NAME NC NE NET NF NG NI NL NO NP NR NU NZ OM ORG PA PE PF PG PH PK PL PM PN PR PRO PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR ST SU SV SY SZ TC TD TEL TF TG TH TJ TK TL TM TN TO TP TR TRAVEL TT TV TW TZ UA UG UK US UY UZ VA VC VE VG VI VN VU WF WS XN XXX YE YT ZA ZM ZW"
    
for tld in $tlds
do
   echo "Doing TLD $tld"
   for f in `dig ns $tld. | grep "NS" | cut -f 7 | grep "$tld." | grep -v "ANSWER"`
   do
       echo "$tld : $f"
       dig axfr $tld @$f >> output.txt
   done
done

For me it worked for the following TLDs: an, bi, ci, cr, er, et, ga, ge, gy, jm, km, mc, mm, mo, mw, ni, np, pg, pro, sk, sv, tt, uk, uy, ye, zw. Might change in the future. For me the winner is… Slovakia (sk)! Never seen so many DNS entries in one file 😀

Update: I just uploaded my results here. When I talked to Max he decided to put his treasures (.DE for example!) up as well, you’ll find his domains here.

Detect shared hosting with Bing

Bing has a pretty cool IP advanced search operator. It can be used to detect shared hosting. It is quite annoying to type in each IP manually when you have to check several IPs (e.g. corporate IP network). The following script will scan an entire range of IPs. The examples below (Google IPs 74.125.39.103 to 74.125.39.106) give some pretty interesting results…

def printBingSharedHosting(ip_start, ip_end):
    import urllib2
    import re
    
    bing_url = 'http://www.bing.com/search?q=ip%3A'
    no_results_string = "No results"
    
    def bing_shared_hosting_get_matches(response):
        regex = '<h3><a\shref="(.*?)"\sonmousedown="'
        mo = re.finditer(regex, response)
        urls = []
        for i in mo:
            urls.extend(i.groups())
        return urls
    
    def bing_shared_hosting_query_bing(ip_str):
        body = urllib2.urlopen(bing_url+ip_str).read()
        if not no_results_string in body:
            for i in bing_shared_hosting_get_matches(body):
                result(ip_str+" : "+i)
        else:
            error("Bing page did not show '"+no_results_string+"' for ip: "+ip_str)

    info("Starting bing shared hosting search. I'm not printing anything until i find something")
    info("This method is only searching through the first result page of bing!")    
    for ip in getIpRangeList(ip_start, ip_end):
        bing_shared_hosting_query_bing(ip)


def getIpRangeList(ip_start, ip_end):

    result_list = []

    one, two, three, four = ip_start.split('.')
    one, two, three, four = (int(one), int(two), int(three), int(four))
    end_one, end_two, end_three, end_four = ip_end.split('.')
    end_one, end_two, end_three, end_four = (int(end_one), int(end_two), int(end_three), int(end_four))

    while one <= end_one:
        end_two_tmp = end_two
        if not one == end_one:
            end_two_tmp = 255
        while two <= end_two_tmp:
            end_three_tmp = end_three
            if not two == end_two:
                end_three_tmp = 255
            while three <= end_three_tmp:
                end_four_tmp = end_four
                if not three == end_three:
                    end_four_tmp = 255
                while four <= end_four_tmp:
                    result_list.append("%i.%i.%i.%i"%(one, two, three, four))
                    #debug(str(one)+" "+str(two)+" "+str(three)+" "+str(four))
                    four += 1
                four = 0
                three += 1
            three = 0
            two += 1
        two = 0
        one += 1
    
    return result_list

   
def error(*text):
    print "[PY-ERROR] "+str(" ".join(str(i) for i in text))
    if SLEEP_TIME_ON_ERROR > 0:
        sleep(SLEEP_TIME_ON_ERROR)

def result(*text):
    print "[PY-RESULT] "+str(" ".join(str(i) for i in text))

def info(*text):
    print "[PY-INFO] "+str(" ".join(str(i) for i in text))

def debug(*text):
    print "[PY-DEBUG] "+str(" ".join(str(i) for i in text))

printBingSharedHosting("74.125.39.103", "74.125.39.106")

I admit, the getIpRangeList function could be a little bit more elegant, but I didn’t want to use an external library, didn’t find any suitable code snippet and in the end, it does its job.

AES encryption/decryption in python

Sometimes I just need some encryption, so I wrote a script that fits some cases. The functions use the python Crypto library.

The security of the used encryption is ok, I wrote a PBKDF2-like Key Derivation Function, that hashes the password before truncating and using it as the AES key. The encryption function does not add random padding. This means an attacker can guess how long the plaintext was. Additionally, CBC is a non-authenticated mode, therefore if somebody flips a bit in your ciphertext the decryption routine won’t notice. This usually means an attacker can flip one bit, but the remaining blocks will be corrupted. So flipping a bit in the last block is easy. Moreover 13’370 derivation rounds might be too much or not enough for you.

def AESencrypt(password, plaintext, base64=False):
    import hashlib, os
    from Crypto.Cipher import AES
    SALT_LENGTH = 32
    DERIVATION_ROUNDS=13370
    BLOCK_SIZE = 16
    KEY_SIZE = 32
    MODE = AES.MODE_CBC
    
    salt = os.urandom(SALT_LENGTH)
    iv = os.urandom(BLOCK_SIZE)
    
    paddingLength = 16 - (len(plaintext) % 16)
    paddedPlaintext = plaintext+chr(paddingLength)*paddingLength
    derivedKey = password
    for i in range(0,DERIVATION_ROUNDS):
        derivedKey = hashlib.sha256(derivedKey+salt).digest()
    derivedKey = derivedKey[:KEY_SIZE]
    cipherSpec = AES.new(derivedKey, MODE, iv)
    ciphertext = cipherSpec.encrypt(paddedPlaintext)
    ciphertext = ciphertext + iv + salt
    if base64:
        import base64
        return base64.b64encode(ciphertext)
    else:
        return ciphertext.encode("hex")

def AESdecrypt(password, ciphertext, base64=False):
    import hashlib
    from Crypto.Cipher import AES
    SALT_LENGTH = 32
    DERIVATION_ROUNDS=13370
    BLOCK_SIZE = 16
    KEY_SIZE = 32
    MODE = AES.MODE_CBC
    
    if base64:
        import base64
        decodedCiphertext = base64.b64decode(ciphertext)
    else:
        decodedCiphertext = ciphertext.decode("hex")
    startIv = len(decodedCiphertext)-BLOCK_SIZE-SALT_LENGTH
    startSalt = len(decodedCiphertext)-SALT_LENGTH
    data, iv, salt = decodedCiphertext[:startIv], decodedCiphertext[startIv:startSalt], decodedCiphertext[startSalt:]
    derivedKey = password
    for i in range(0, DERIVATION_ROUNDS):
        derivedKey = hashlib.sha256(derivedKey+salt).digest()
    derivedKey = derivedKey[:KEY_SIZE]
    cipherSpec = AES.new(derivedKey, MODE, iv)
    plaintextWithPadding = cipherSpec.decrypt(data)
    paddingLength = ord(plaintextWithPadding[-1])
    plaintext = plaintextWithPadding[:-paddingLength]
    return plaintext
    
a = AESencrypt("password", "ABC")
print AESdecrypt("password", a)

Automating JD-GUI decompilation on a Mac with AppleScript

I know the guys over at Java Decompiler don’t want to release a command line tool, because they fear that companies will use their code in commercial product. See the discussion here. I found a solution to my problem, so that I can still automate the decompilation process. During my Android research I really need to decompile a lot of jar files, therefore I wrote a simple AppleScript that saves me the sources to my /opt folder.

tell application "JD-GUI"
	activate
end tell

tell application "System Events"
	keystroke "s" using {command down, option down}
end tell
tell application "System Events"
	keystroke tab
	keystroke tab
	keystroke tab
	key code 125 #Down
	key code 125 #Down
	key code 125 #Down
	key code 125 #Down
	key code 125 #Down
	key code 36 #Enter
	delay 1
	key code 36 #Enter
	delay 2
end tell

repeat while appIsRunning("JD-GUI")
	tell application "System Events"
		keystroke "q" using {command down} #Close JD-GUI
	end tell
	delay 2
end repeat

on appIsRunning(appName)
	tell application "System Events" to (name of processes) contains appName
end appIsRunning

After saving the script as decompile_jar.applescript with the AppleScript Editor, you can invoke it from your bash script like this:

/Applications/JD-GUI.app/Contents/MacOS/jd-gui example.jar &
sleep 1
osascript decompile_jar.applescript
mv /opt/example.src.zip /your/destination