Author:jkgh006@敏信安全审计

DWR框架简介

DWR是一个远程web调用框架,利用该框架使得Ajax开发变得简单。利用DWR可以在客户端使用JavaScript直接调用服务器端的Java方法,并返回值给JavaScript;就好像直接在本地客户端调用一样(DWR根据Java类来动态生成JavaScript代码)

参考链接:http://directwebremoting.org/dwr/index.html

前言

很多人私下问过我,如果现实审计中碰到dwr框架,应该怎么去构造payload,怎么根据流程分析出结果,所以这次我们只讲dwr在实际应用场景的审计和防御思路

默认配置&&安全

根据官网给出来的默认配置如下web.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app id="dwr">

    <display-name>DWR (Direct Web Remoting)</display-name>
    <description>A Simple Demo DWR</description>

    <listener>
        <listener-class>org.directwebremoting.servlet.DwrListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>dwr-invoker</servlet-name>
        <display-name>DWR Servlet</display-name>
        <description>Direct Web Remoter Servlet</description>
        <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>

        <init-param>
            <param-name>fileUploadMaxBytes</param-name>
            <param-value>25000</param-value>
        </init-param>

        <!-- This should NEVER be present in live -->
        <init-param>
            <param-name>debug</param-name>
            <param-value>true</param-value>
        </init-param>

        <init-param>
            <param-name>accessLogLevel</param-name>
            <param-value>runtimeexception</param-value>
        </init-param>

        <!-- Remove this unless you want to use active reverse ajax -->
        <init-param>
            <param-name>activeReverseAjaxEnabled</param-name>
            <param-value>true</param-value>
        </init-param>

        <!-- By default DWR creates application scope objects when they are first
        used. This creates them when the app-server is started -->
        <init-param>
            <param-name>initApplicationScopeCreatorsAtStartup</param-name>
            <param-value>true</param-value>
        </init-param>

        <!-- WARNING: allowing JSON-RPC connections bypasses much of the security
        protection that DWR gives you. Take this out if security is important -->
        <init-param>
            <param-name>jsonRpcEnabled</param-name>
            <param-value>true</param-value>
        </init-param>

        <!-- WARNING: allowing JSONP connections bypasses much of the security
        protection that DWR gives you. Take this out if security is important -->
        <init-param>
            <param-name>jsonpEnabled</param-name>
            <param-value>true</param-value>
        </init-param>

        <!-- data: URLs are good for small images, but are slower, and could OOM for
        larger images. Leave this out (or keep 'false') for anything but small images -->
        <init-param>
            <param-name>preferDataUrlSchema</param-name>
            <param-value>false</param-value>
        </init-param>

        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dwr-invoker</servlet-name>
        <url-pattern>/dwr/*</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

上面的配置效果,再加上程序开发者的拿来主义习惯,可能会产生两个大问题

  1. 没有关闭debug
  2. dwr的访问目录可被猜测,这里给出的/dwr/ (如果是给用户展示的部分本身就是一个dwr应用,那么就没有必要改写次路径,如果是给第三方调用类似webservice那种,就很有必要修改),官方默认配置还有一个/exec/

以上两个组合访问效果如下:

里面的每一个接口都可以通过浏览器抓包获取,常规意识下这些第三方调用未授权的概率几乎可以达到百分之百

DWR框架讲解

为了方便对dwr的每一种类型进行测试,我们简单的编写了一些测试类

简单类型参数

首先看commontest目录下,这里我们主要对int和string参数进行测试

package com.example.dwr.commontest;
public class CommonParams {

    public static String stringTest(String data) {
        return data;
    }

    public static int inTest(int data) {
        return data;
    }
}

在dwr.xml中加入

<create javascript="commonparams" creator="new">
      <param name="class" value="com.example.dwr.commontest.CommonParams" />
    </create>

这样在js调用层我们可以通过/dwr/interface/commonparams.js的commonparams变量调用java层编写的函数

例如index.jsp:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>

  <title>dwr common test</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="description" content="This is my page">

  <script type='text/javascript' src='/dwr/engine.js'></script>
  <script type='text/javascript' src='/dwr/util.js'></script>
  <script type='text/javascript' src='/dwr/interface/commonparams.js'></script>

  <script type="text/javascript">
    function stringTest(){
      commonparams.stringTest("abcd");
    }
    function integerTest(){
      commonparams.inTest(1234);
    }
  </script>
</head>

<body>
<input type="button" name="stringTest" onclick="stringTest()" value="stringTest">
<input type="button" name="integerTest" onclick="integerTest()" value="integerTest">
</body>
</html>

访问效果和http包如下

根据上下文判断dwr框架写法其实有一定的规律

上面调用的stringTest方法,如果我们调用的是inTest,只需要改这几个地方,数据类型定义成为int即可

POST /dwr/call/plaincall/commonparams.inTest.dwr HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: text/plain
Referer: http://localhost:8080/
Content-Length: 212
Cookie: UM_distinctid=160cb8347c532e-02170ecaf6aeb-4c322f7c-1fa400-160cb8347c662d; CNZZDATA1261218610=1741751127-1515241945-%7C1515241945; JSESSIONID=DBEB32C68B89CE0D8815DB6ADF207376; DWRSESSIONID=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m
X-Forwarded-For: 127.0.0.1
Connection: close

callCount=1
nextReverseAjaxIndex=0
c0-scriptName=commonparams
c0-methodName=inTest
c0-id=0
c0-param0=int:1234
batchId=0
instanceId=0
page=%2F
scriptSessionId=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m/JZRRo9m-dCmbaYdn5

数组类型参数

对array参数进行测试

package com.example.dwr.arraytest;

import org.apache.commons.lang.StringUtils;

public class ArrayParams {
    public ArrayParams() {
    }
    public String iniArrayTest(int[] data) {
        String template = "";
        String tmp = "";
        for(int i=0;i<data.length;i++) tmp = tmp+String.valueOf(data[i])+",";
        template = template+"int array as:["+ tmp+"]";
        return template;
    }
    public String strArrayTest(String[] data) {
        String template = "";
        String joinStr = StringUtils.join(data, ",");
        template = template+"string array as:["+ joinStr+"]";
        return template;
    }
}

dwr.xml添加如下配置

<create javascript="arrayparams" creator="new">
      <param name="class" value="com.example.dwr.arraytest.ArrayParams" />
    </create>

例如arrtest.jsp:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>

    <title>dwr arr test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="description" content="This is my page">

    <script type='text/javascript' src='/dwr/engine.js'></script>
    <script type='text/javascript' src='/dwr/util.js'></script>
    <script type='text/javascript' src='/dwr/interface/arrayparams.js'></script>

    <script type="text/javascript">
        function iniArrayTest(){
            var a=[1,2,3,4];
            arrayparams.iniArrayTest(a);
        }
        function strArrayTest(){
            var a = ['a','b','c','d']
            arrayparams.strArrayTest(a);
        }
    </script>
</head>

<body>
<input type="button" name="iniArrayTest" onclick="iniArrayTest()" value="iniArrayTest">
<input type="button" name="strArrayTest" onclick="strArrayTest()" value="strArrayTest">
</body>
</html>

访问和请求包如下:

post包的结构相对于普通类型是有所变化的,照猫画虎修改为strArrayTest函数

POST /dwr/call/plaincall/arrayparams.strArrayTest.dwr HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: text/plain
Referer: http://localhost:8080/arrtest.jsp
Content-Length: 351
Cookie: UM_distinctid=160cb8347c532e-02170ecaf6aeb-4c322f7c-1fa400-160cb8347c662d; CNZZDATA1261218610=1741751127-1515241945-%7C1515241945; JSESSIONID=6B9103592284CBB4A787F99E8C21DE4A; DWRSESSIONID=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m
X-Forwarded-For: 127.0.0.1
Connection: close

callCount=1
nextReverseAjaxIndex=0
c0-scriptName=arrayparams
c0-methodName=strArrayTest
c0-id=0
c0-e1=string:a
c0-e2=string:b
c0-e3=string:c
c0-e4=string:d
c0-param0=array:[reference:c0-e1,reference:c0-e2,reference:c0-e3,reference:c0-e4]
batchId=1
instanceId=0
page=%2Farrtest.jsp
scriptSessionId=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m/WNnUo9m-uxKcm4x0i

对象类型参数

package com.example.dwr.objecttest;

public class ObjectTest {
    public ObjectTest() {
    }

    public String addUser(UserBean user){
        return "Name:"+user.getName();
    }
}

上面可以看出来传递的是一个java的bean对象,代码简单编写如下

package com.example.dwr.objecttest;

public class UserBean {
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    private String name;
}

此时我们就要用到dwr中的convert,作用就是通过Bean Converter将javascript变量user转变成java的UserBean类型

dwr.xml添加如下配置:

<create javascript="objecttest" creator="new">
      <param name="class" value="com.example.dwr.objecttest.ObjectTest"/>
    </create>
    <convert match="com.example.dwr.objecttest.UserBean" converter="bean"/>

objtest的内容编写如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>

    <title>dwr obj test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="description" content="This is my page">

    <script type='text/javascript' src='/dwr/engine.js'></script>
    <script type='text/javascript' src='/dwr/util.js'></script>
    <script type='text/javascript' src='/dwr/interface/objecttest.js'></script>

    <script type="text/javascript">
        function objectTest(){
            var user={name:"jkgh006"};
            objecttest.addUser(user);
        }
    </script>
</head>

<body>
<input type="button" name="objectTest" onclick="objectTest()" value="objectTest">
</body>
</html>

访问和请求包如下:

post包的结构这时候就变得复杂了,但是分析程序,还是可以简单的编写包结构

POST /dwr/call/plaincall/objecttest.addUser.dwr HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: text/plain
Referer: http://localhost:8080/objtest.jsp
Content-Length: 271
Cookie: UM_distinctid=160cb8347c532e-02170ecaf6aeb-4c322f7c-1fa400-160cb8347c662d; CNZZDATA1261218610=1741751127-1515241945-%7C1515241945; JSESSIONID=14ED5B8693320646E08DD451F5F411D2; DWRSESSIONID=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m
X-Forwarded-For: 127.0.0.1
Connection: close

callCount=1
nextReverseAjaxIndex=0
c0-scriptName=objecttest
c0-methodName=addUser
c0-id=0
c0-e1=string:jkgh006
c0-param0=Object_Object:{name:reference:c0-e1}
batchId=1
instanceId=0
page=%2Fobjtest.jsp
scriptSessionId=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m/Y*v$o9m-3BE1kSgDb

文件类型参数

package com.example.dwr.filetest;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.directwebremoting.WebContext;
import org.directwebremoting.WebContextFactory;

public class FileTest {
    public String upload(InputStream inputStream, String fileName) throws IOException {
        String tempFileName= FilenameUtils.getName(fileName);
        String path=getRealPath("upload");
        File file=new File(path+ File.separator+tempFileName);
        FileUtils.copyInputStreamToFile(inputStream, file);
        return file.getPath();
    }
    public String getRealPath(String dir){
        WebContext context= WebContextFactory.get();
        return context.getSession().getServletContext().getRealPath(dir);
    }
}

dwr.xml添加如下配置:

<create javascript="filtest" creator="new">
  <param name="class" value="com.example.dwr.filetest.FileTest" />
</create>

filetest的内容编写如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>dwr file test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="description" content="This is my page">
    <script type='text/javascript' src='/dwr/engine.js'></script>
    <script type='text/javascript' src='/dwr/util.js'></script>
    <script type='text/javascript' src='/dwr/interface/filtest.js'></script>
    <script type="text/javascript">
        function upload(){
            var file=dwr.util.getValue("file");
            filtest.upload(file,file.value,function(result){
                alert(result);
            });
        }
    </script>
</head>

<body>
<input type="file" id="file" name="file"><br/>
<button onclick="upload();">uploadtest</button>
</body>
</html>

访问和请求包如下:

根据构造的包可以看出来,只是转换成了formdata形式,其他的构造都跟正常的差不多

POST /dwr/call/htmlcall/filtest.upload.dwr HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Content-Type: multipart/form-data; boundary=---------------------------252533190231463
Content-Length: 1414
Referer: http://localhost:8080/filetest.jsp
Cookie: UM_distinctid=160cb8347c532e-02170ecaf6aeb-4c322f7c-1fa400-160cb8347c662d; CNZZDATA1261218610=1741751127-1515241945-%7C1515241945; JSESSIONID=F647906117B5319F3161C493B1C03F95; DWRSESSIONID=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m
X-Forwarded-For: 127.0.0.1
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------252533190231463
Content-Disposition: form-data; name="callCount"

1
-----------------------------252533190231463
Content-Disposition: form-data; name="nextReverseAjaxIndex"

0
-----------------------------252533190231463
Content-Disposition: form-data; name="c0-scriptName"

filtest
-----------------------------252533190231463
Content-Disposition: form-data; name="c0-methodName"

upload
-----------------------------252533190231463
Content-Disposition: form-data; name="c0-id"

0
-----------------------------252533190231463
Content-Disposition: form-data; name="c0-param0"; filename="up.gif"
Content-Type: image/gif

1
-----------------------------252533190231463
Content-Disposition: form-data; name="c0-param1"

string:C%3A%5Cfakepath%5Cup.gif
-----------------------------252533190231463
Content-Disposition: form-data; name="batchId"

0
-----------------------------252533190231463
Content-Disposition: form-data; name="instanceId"

0
-----------------------------252533190231463
Content-Disposition: form-data; name="page"

%2Ffiletest.jsp
-----------------------------252533190231463
Content-Disposition: form-data; name="scriptSessionId"

J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m/52L6p9m-LLzEisz9f
-----------------------------252533190231463--

综合类型参数

根据上面每种参数类型的单独举例,给出来一个包含除了文件外的所有参数的调用方式

package com.example.dwr.complextest;
import com.example.dwr.objecttest.UserBean;
import org.apache.commons.lang.StringUtils;


public class ComplexParams {
    public ComplexParams() {
    }
    public String intAndStringAndArrayAndObjTest(int a, String b, int[] as, String[] bs,UserBean user,UserBean[] users) {
        String template = "";
        template = template+"int a:"+ String.valueOf(a)+"\n";
        template = template+"string b:"+ String.valueOf(a)+"\n";
        String tmp = "";
        for(int i=0;i<as.length;i++) tmp = tmp+String.valueOf(as[i])+",";
        template = template+"int array as:["+ tmp+"]\n";
        String joinStr = StringUtils.join(bs, ",");
        template = template+"string array as:["+ joinStr+"]\n";
        return template;
    }
}

上面已经存在参数嵌套了,不过没关系,不管多么复杂的参数结构,我们都可以构造出来

dwr.xml配置如下:

<create javascript="complexparams" creator="new">
      <param name="class" value="com.example.dwr.complextest.ComplexParams" />
    </create>

complextest的内容编写如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>

    <title>dwr obj test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="description" content="This is my page">

    <script type='text/javascript' src='/dwr/engine.js'></script>
    <script type='text/javascript' src='/dwr/util.js'></script>
    <script type='text/javascript' src='/dwr/interface/complexparams.js'></script>

    <script type="text/javascript">
        function complexTest(){
            var a = 1234;
            var b = 'abcd';
            var c = [1,2,3,4];
            var d = ['a','b','c','d'];
            var e ={name:"jkgh006"};
            var f = [{name:"jkgh006"},{name:"jkgh007"},{name:"jkgh008"}]
            complexparams.intAndStringAndArrayAndObjTest(a,b,c,d,e,f);
        }
    </script>
</head>

<body>
<input type="button" name="complexTest" onclick="complexTest()" value="complexTest">
</body>
</html>

查看到的构造包如下

请求后的构造包现在看起来就非常复杂了,但是构造思路在js里面还是比较清晰的,可以层层往下推

POST /dwr/call/plaincall/complexparams.intAndStringAndArrayAndObjTest.dwr HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: text/plain
Referer: http://localhost:8080/complextest.jsp
Content-Length: 899
Cookie: UM_distinctid=160cb8347c532e-02170ecaf6aeb-4c322f7c-1fa400-160cb8347c662d; CNZZDATA1261218610=1741751127-1515241945-%7C1515241945; JSESSIONID=0E19EB66E71E4439A5CFD4FAF253BB3B; DWRSESSIONID=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m
X-Forwarded-For: 127.0.0.1
Connection: close

callCount=1
nextReverseAjaxIndex=0
c0-scriptName=complexparams
c0-methodName=intAndStringAndArrayAndObjTest
c0-id=0
c0-param0=number:1234
c0-param1=string:abcd
c0-e1=number:1
c0-e2=number:2
c0-e3=number:3
c0-e4=number:4
c0-param2=array:[reference:c0-e1,reference:c0-e2,reference:c0-e3,reference:c0-e4]
c0-e5=string:a
c0-e6=string:b
c0-e7=string:c
c0-e8=string:d
c0-param3=array:[reference:c0-e5,reference:c0-e6,reference:c0-e7,reference:c0-e8]
c0-e9=string:jkgh006
c0-param4=Object_Object:{name:reference:c0-e9}
c0-e11=string:jkgh006
c0-e10=Object_Object:{name:reference:c0-e11}
c0-e13=string:jkgh007
c0-e12=Object_Object:{name:reference:c0-e13}
c0-e15=string:jkgh008
c0-e14=Object_Object:{name:reference:c0-e15}
c0-param5=array:[reference:c0-e10,reference:c0-e12,reference:c0-e14]
batchId=0
instanceId=0
page=%2Fcomplextest.jsp
scriptSessionId=J2YAzcntFgQYepoW~g!fuZdxeAR6Qy4ho9m/uwpcp9m-VQn1SkeIe

总结

  1. 实际的网站发布debug模式是关闭状态,我们做黑盒测试就要去猜测两个默认目录,分别为/exec/和/dwr
  2. 审计可以套用上面的请求包的模板,在你认为存在问题的地方构造java接口调用的请求数据包
  3. 网站发布dwr接口,通常都是未授权调用,包含内容比较多,比如用户,管理等api接口
  4. 如果参数构造有不确定因素,可以把对应的dwr接口空实现,然后转接到我们自己可以本地模拟的代码上面来

源链接

Hacking more

...