在本文中,我们继续为读者详细介绍如何利用GWTMap工具对GWT应用进行安全审查。
识别和重建分段后的代码
GWT中的代码分段(或者我认为它的正式名称是:“dead-for-now(DFN)型代码拆分”)是一种减少首次访问时所需下载的初始置换文件大小的方法,它将函数拆分为“推迟下载型”代码分段文件。这样可以通过减少应用程序的初始加载时间来提高性能,使得浏览器可以在需要时加载额外的代码。
那么,如何从静态代码中推断出这种行为呢?并且,如何重建这些分段呢?
经过一些研究后,我们发现每个置换文件都有一个名为“__GWTStartLoadingFragment”的函数,而不管该文件是否被分段处理:
function __gwtStartLoadingFragment(frag) { var fragFile = 'deferredjs/' + $strongName + '/' + frag + '.cache.js'; return __gwtModuleFunction.__startLoadingFragment(fragFile); }
这本身就很有用,因为它能告诉我们:给定置换文件的代码片段(即分段)总是位于以活动代码置换(也称为强名称)命名的目录中的“deferredjs”子目录中。此外,这个延迟型的JS目录始终位于模块库的根目录下。
我还发现,尽管这些分段在使用过程中似乎是以随机顺序(21、27、18、2、6)加载的,但它们是在编译过程中则是以增量顺序生成的。之所以无法观察到这些片段以任何特定顺序加载的原因是,正在加载的代码片段取决于用户正在执行的操作。
因此,置换文件“4D2DF0C23D1B58D2853828A450290F3F”的第一个代码片段可以在以下位置找到:
http://127.0.0.1/base/deferredjs/4D2DF0C23D1B58D2853828A450290F3F/1.cache.js
所以,尽管现在已经知道哪里可以找到代码片段了,但我们仍然需要确定一个置换文件是否真的被分段了。
结果表明,函数“__GWTStartLoadingFragment”保留了它未经混淆的名称,即使在经过混淆处理的置换文件中也是如此。此外,虽然这个函数总是被定义,但是只有置换文件确实被分段的情况下,后面的代码才会调用它。例如,下面是开始加载经过混淆处理的置换文件中的代码片段:
知道了这一点,就可以通过枚举置换文件中字符串“__gwtStartLoadingFragment”的所有实例,如果它被发现了不止一次,就说明该应用程序将在“某个时候”至少加载一个额外的代码片段。然后,我们就可以在活动的该置换文件的“deferredjs”目录中枚举递增形式的片段ID。
因此,现在的代码已经变得相当复杂,所以,我们需要实现额外的逻辑,对脚本正在考察的代码进行分类。就这里来说,我们将把“代码”分成三类:GWT的引导文件中的代码、置换文件中的代码和代码片段文件中的代码。其中引导文件用来识别并加载所需的浏览器置换文件,置换文件负责加载任何缺失的代码片段文件。
创建了一些新的正则表达式之后,我现在可以将脚本指向应用程序的引导文件("{module-name}.nocache.js"),枚举并选择一个置换文件("{hex}.cache.js"),识别任何丢失的代码片段文件("{int}.cache.js"),然后提取所有暴露的服务和方法。除此之外,我现在还可以直接将脚本指向这些代码类型中的任何一个——如果我想跳过引导文件,直接分析一个特定的置换文件的话,这将非常有用。
在确认它在现有样本中都能正常工作之后,我又实现了额外的逻辑,并改进了正则表达式,使该脚本能够透明地解析混淆型和非混淆型的置换文件。
./wip.py -u "http://192.168.22.120/olympian/olympian.nocache.js" [+] Analysing... http://192.168.22.120/olympian/olympian.nocache.js Permutation: http://192.168.22.120/olympian/46DC1658EDF5314D1F33E5554C8C54A7.cache.js + fragment : http://192.168.22.120/olympian/deferredjs/46DC1658EDF5314D1F33E5554C8C54A7/1.cache.js + fragment : http://192.168.22.120/olympian/deferredjs/46DC1658EDF5314D1F33E5554C8C54A7/2.cache.js [+] Methods Found... ... AuthenticationService.login( java.lang.String/2004016611, java.lang.String/2004016611 ) AuthenticationService.logout( com.ecorp.olympian.shared.model.ContextId/1042941669 ) ... SystemService.localCheck( com.ecorp.olympian.shared.model.ContextId/1042941669 ) SystemService.serverStatus( com.ecorp.olympian.shared.model.ContextId/1042941669, java.lang.String/2004016611 ) SystemService.contact( com.ecorp.olympian.shared.model.ContactForm/196160789 ) ...
现在,我们要做的事情就是为每个方法添加生成GWT-RPC请求示例的能力。
一种在Python中实现GWT-RPC序列化的方法
虽然解决这个挑战比我预想的要难,但通过考察Steven Seeley的“gwt.py”工具,我们意识到这在一定程度上是可行的。当回顾我的测试GWT应用程序如何序列化各种类型和对象时,我注意到其输出和我预期的有一些差异。但是,我却无法解释这些现象,这说明我对它的理解还不够深刻,所以,我决定换一种思路来处理它。
首先,我创建了一个“TestService”服务。它实现了一系列用于序列化各种Java类型的方法,这些类型包括:整型、字符串类型、布尔型、字符型、ArrayLists等。在运行每一个方法之后,我分析了每个类型是如何被序列化的,然后,通过阅读一些额外的文档(包括 “com.google.gwt.rpc.server.RPC.java”的实现),我注意到了一些非常特殊的行为。
限于篇幅,这里不可能深入介绍每种类型的处理方式,但我的一大重要发现是:GWT会将这些类型分为“简单”和“复杂”两类。
简单类型的序列化
简单类型在经过序列化处理的payload内具有简短的别名:
I -> java.lang.Integer D -> java.lang.Double F -> java.lang.Float B -> java.lang.Byte Z -> java.lang.Boolean S -> java.lang.Short C -> java.lang.Char J -> java.lang.Long
例如,在向服务器发送布尔值和字符串值的RPC请求中,经过序列化处理后的payload将是下面的样子:
...testStringAndBool|java.lang.String/2004016611|Z|Bob|1|2|3|4|2|5|6|7|0|
下面,我们对这些参数进行详细的介绍:
1|2|3|4| -> 字符串表的前4个元素(inc方法的名称) 2| -> 方法的参数数(2) 5| -> 参数1声明的类型的索引(java.lang.String) 6| -> 参数2声明的类型的索引(布尔值(Z)) 7| ->参数1的值的索引(Bob) 0| ->参数2的直接值(False(0))
这表明,当一个值是字符串类型(例如,不属于简单类型)时,参数映射将在字符串表本身中保存该值的索引。但是,当参数是简单类型(例如布尔值(Z))时,参数映射将直接保存该值,而不是字符串表索引。
这种行为似乎在所有简单类型中都是相对一致的,但长整型除外,这种类型的数字将以Base64型编码进行表示。相对来说,正如其他测试用例所揭示的那样,这在很大程度上取决于RPC调用是如何用Java编写的。
例如,对于完全相同的RPC请求,进行序列化后也可能存在差异:
testStringAndBoolean|java.lang.String/2004016611|java.lang.Boolean/476441737|Bob|1|2|3|4|2|5|6|7|6|0|
通过分解,我们可以得到以下结构:
1|2|3|4| ->字符串表的前4个元素(inc方法的名称) 2| ->方法的参数数量(2) 5| ->参数1声明的类型的索引(字符串) 6| ->参数2声明的的类型的索引(java.lang.Boolean) 7| ->参数1的值的索引(Bob) 6| ->参数2的运行时类型的索引(java.lang.Boolean) 0| ->参数2的直接值(False(0))
那为什么会不同呢?为了弄清楚这个问题,我们可以快速浏览一下“TestService”的Java接口:
@RemoteServiceRelativePath("testService") public interface TestService extends RemoteService { ... String testStringAndBool(String name, boolean flag); String testStringAndBoolean(String name, Boolean flag); ... }
回顾这两种定义之间的差异后,我发现只有使用基本java类型(例如“Boolean”类型),而不是通过对复杂java对象(例如“Boolean”类)的引用来定义类型时,才能将它们映射到相关的简单类型别名。对象引用会将简单类型转换为复杂类型,序列化也考虑到了这一点。
因此,尽管从实现的角度来看这并不重要,但我们需要意识到这一点,因为它是根据从混淆型代码中提取的签名来序列化简单类型的一种替代方法。
当然,这实现起来并不算太难。因为,我们可以使用已有的逻辑来解析方法调用,并创建两个列表,一个存放参数类型,另一个存放参数值,这样就能实现类型和值之间的一对一映射。但是,由于它可能包含重复项,因此不能直接在RPC请求中使用,并且每个RPC调用应仅包含任何特定字符串的一个实例(即使它是参数值)。
为了解决这个问题,我创建了第三个“规范化”的类型列表,其中只包含唯一的类型值。这样,我们就可以在RPC字符串表中使用这个列表了。然后,在建立字符串表和参数之间的映射时,我可以遍历每个参数值,当找到一个字符串时,直接将其追加到字符串表的唯一值列表之后。但是,当找到简单类型时,不会将任何内容添加到字符串表,而是将其值直接添加到参数映射中。
字符串和整数被序列化时的输出示例如下所示:
...|processStringAndInt|java.lang.String/2004016611|I|test|1|2|3|4|2|5|6|7|26|
序列化复杂类型
现在,当使用更复杂的Java类型时,序列化过程会有些不同。例如,让我们看看下面的“testListAndString”方法和“names”列表的定义:
String testListAndString(List List
请注意,这里的“names”其实就是“List
testListAndString|java.util.List|java.lang.String/2004016611|java.util.ArrayList/4159755760|Bob|Alice|Smith|1|2|3|4|2|5|6|7|2|6|8|6|9|10|
现在,我们对上述代码进行详细的介绍;我们会发现,它与只传递简单类型时的结构是略有不同的:
1|2|3|4| -> 字符串表的前4个元素(inc方法名) 2|->方法的参数数量(2) 5|->参数1的声明类型索引(java.util.List) 6|->参数2的声明类型索引(java.lang.String) 7|->参数1的运行时类型索引(java.util.ArrayList) 2| -> 列表元素的数量 (2) 6| -> 列表元素1的声明类型索引(java.lang.String) 8|->列表元素1的值的索引(Bob) 6| -> 列表元素2的声明类型索引(java.lang.String) 9| -> 列表元素2的值的索引(Alice) 10| ->参数2的值的索引(Smith)
我们可以用类似于区分Boolean/boolean的方式再次进行对比。其中,如果直接声明ArrayList,而不是作为List类的实现,序列化就会省略List对象的声明类型,序列化之后的请求中List的声明类型和运行时类型则会引用同一个值索引(ArrayList)。
testArrayListAndString|java.util.ArrayList/4159755760|java.lang.String/2004016611|Bob|Alice|Smith|1|2|3|4|2|5|6|5|2|6|7|6|8|9| 1|2|3|4| -> 字符串表的前4个元素(包含方法名) 2|->方法的参数数(2) 5|->参数1的声明类型索引(java.util.ArrayList) ... 5| ->参数1的运行时类型索引(java.util.ArrayList) 2| ->后面的列表元素的数量 (2) ...
这里在实现上变得稍微复杂了一些,因为方法声明模式——我们在混淆型代码中以它为起点——只包含参数的声明类型。因此,当我们找到一个List时,是无法确定它是以“ArrayList”类型还是其他列表类型实现的,甚至不知道它是一个字符串类型或整数类型的列表。我们可以在下面的示例中看到这个局限性,因为我们只找到了List类型和String类型的声明,但没有发现其他关于List实现的上下文信息:
function $testListAndString(this$static, names, sname, callback){ var ex, helper, streamWriter; helper = new RemoteServiceProxy$ServiceHelper(this$static, 'TestService_Proxy', 'testListAndString'); try { streamWriter = $start(helper, 'com.ecorp.olympian.client.asyncService.TestService', 2); $append(streamWriter, '' + $addString(streamWriter, 'java.util.List')); $append(streamWriter, '' + $addString(streamWriter, 'java.lang.String/2004016611')); $writeObject(streamWriter, names); $append(streamWriter, '' + $addString(streamWriter, sname)); $finish_0(helper, callback, ($clinit_RequestCallbackAdapter$ResponseReader() , STRING)); }
另一方面,就算它直接被定义为一个ArrayList,它也会显示为上面这样。然而,我们仍然无法确定它是一个字符串列表还是任何其他类型/自定义对象。另一个局限性在于,无法预先确定列表的长度,因为长度完全取决于用户在运行时提供的内容的长度。
为了弥补这一点,我实现了一个逻辑,即如果发现一个List类型,则把它看作是ArrayList的实现,并保留这些String。当然,这并不总是正确的,但在许多情况下却是可行的,而且这只是一个起点而已。
此外,我们还必须创建一个自定义的格式来保存这些花样繁多的List类型签名,这样就可以在为字符串表建立类型列表时再次将它们分解。在这里,我们选择了以下格式:
java.util.List< java.util.ArrayList/4159755760
然后,在构建规范化类型列表时,我可以使用以下正则表达式将这个花哨的“类型字符串”分解为各种具体的类型:
(java\.util\.(?:[A-Za-z]+)?List(?:[0-9/]+)?)< ([a-zA-Z0-9\./]+)[< >]?(?:(.*[^ >]))? >
序列化复杂对象
在考察了简单类型和复杂类型之后,我意识到实际上存在第三种类型。对于自定义Java对象来说,目前我还无法将其序列化,因为我不知道它们在经过混淆处理的的客户端代码中是如何组织的。
这是一个重大的缺陷,因为在GWT应用程序的客户端和服务器之间经常会使用和传递自定义对象。于是,我实现了一些逻辑,试图通过将它们“视作”字符串值并附加引用运行时类型来对它们的结构进行有根据的推测。
至少从理论上讲,是可以实现一些逻辑来识别和跟踪经过混淆处理的“$writeObject”调用及其参数的,从而进一步弄清楚给定List包含的内容或自定义对象可能具有的属性。
GWTMap:我的尝试性解决方案
经过大量的折腾之后,我终于把之前的小测试脚本变成了一个用户友好且功能齐全的工具!不管咋说,我终于搞出了这样一个工具:可以从混淆型和非混淆型的GWT变体中提取所有方法签名,包括碎片化的方法。
同时,这个工具可以“半可靠地”生成序列化的、可保存到相关文件中的GWT-RPC请求payload,然后,可以将其传递给像Burp suite这样的工具,或者使用这个工具本身进行自动测试。
最后,我还测试了许多用例,以检查该工具是否对以下所有GWT版本的混淆型和非混淆代码均有效:
· GWT Version: 2.9.0 - released 2020
· GWT Version: 2.8.2 - released 2017
· GWT Version: 2.8.1 - released 2017
· GWT Version: 2.8.0 - released 2016
· GWT Version: 2.7.0 - released 2014
或者说,至少根据我有限的测试应用样本来说,它看起来一直能够正常工作。
就这样,GWTMap诞生了!(我知道这个名字超酷超有创意!)
$ ./gwtmap.py -h usage: gwtmap.py [-h] [--version] [-u [-b [-f [--svc] [--code] [--color] [--backup [DIR]] [-q] Enumerates GWT-RPC methods from {hex}.cache.js permutation files Arguments: -h, --help show this help message and exit --version show program's version number and exit -u URL of the target GWT {name}.nocache.js bootstrap or {hex}.cache.js file -F path to the local copy of a {hex}.cache.js GWT permutation file -b specifies the base URL for a given permutation file in -F/--file mode -p URL for an optional HTTP proxy (e.g. -p http://127.0.0.1:8080) -c any cookies required to access the remote resource in -u/--url mode (e.g. 'JSESSIONID=ABCDEF; OTHER=XYZABC') -f case-sensitive method filter for output (e.g. -f AuthSvc.checkSession) --basic enables HTTP Basic authentication if require. Prompts for credentials --rpc attempts to generate a serialized RPC request for each method --probe sends an HTTP probe request to test each method returned in --rpc mode --svc displays enumerated service information, in addition to methods --code skips all and dumps the 're-formatted' state of the provided resource --color enables console output colors --backup [DIR] creates a local backup of retrieved code in -u/--url mode -q, --quiet enables quiet mode (minimal output) Example: ./gwtmap.py -u "http://127.0.0.1/example/example.nocache.js" -p "http://127.0.0.1:8080" --rpc --color
工作流程示例
为了演示工作流程,这里首先对目标应用程序运行GWTMap,并指定“--backup”标志,以确保进行相关处理时会在本地备份代码状态(包括代码片段):
$ ./gwtmap.py -u "http://192.168.22.120/olympian/olympian.nocache.js" --backup ___| \ / __ __| \ | \ _ \ | \ \ / | |\/ | _ \ | | | | \ \ / | | | ___ \ ___/ \____| _/\_/ _| _| _| _/ _\ _| version 0.1 [+] Analysing ==================== http://192.168.22.120/olympian/olympian.nocache.js Permutation: http://192.168.22.120/olympian/037A330198815EAE6A360B7107F8C442.cache.js + fragment : http://192.168.22.120/olympian/deferredjs/037A330198815EAE6A360B7107F8C442/1.cache.js + fragment : http://192.168.22.120/olympian/deferredjs/037A330198815EAE6A360B7107F8C442/2.cache.js [+] Module Info ==================== GWT Version: 2.9.0 Content-Type: text/x-gwt-rpc; charset=utf-8 X-GWT-Module-Base: http://192.168.22.120/olympian/ X-GWT-Permutation: 037A330198815EAE6A360B7107F8C442 [+] Methods Found ==================== ----- AccountService ----- AccountService.getBio( com.ecorp.olympian.shared.model.ContextId/1042941669 ) AccountService.validateToken( java.lang.String/2004016611 ) ... ----- TestService ----- TestService.testArrayList( java.util.ArrayList/4159755760 TestService.testXsrfToken( java.lang.String/2004016611 ) ... [+] Summary ==================== Backup: ./1601747650_037A330198815EAE6A360B7107F8C442.cache.js Showing 5/5 Services Showing 25/25 Methods
如您所见,这里识别出了5个服务和25个方法,同时,还对置换文件进行了完整的本地备份。
有了代码的本地备份之后,在分析过程中就无需重新连接到目标服务器了,相反,我们可以使用“--F/--file”标志来传递备份文件。此外,我们还可以使用“--filter”参数来过滤输出内容,使其只显示自己感兴趣的内容。例如,在本例中,我只对TestService的方法感兴趣,则:
$ ./gwtmap.py -F ./1601747650_037A330198815EAE6A360B7107F8C442.cache.js \ --filter TestService ___| \ / __ __| \ | \ _ \ | \ \ / | |\/ | _ \ | | | | \ \ / | | | ___ \ ___/ \____| _/\_/ _| _| _| _/ _\ _| version 0.1 [+] Analysing ==================== ./1601747650_037A330198815EAE6A360B7107F8C442.cache.js Warning: Individual permutation files in -F/--file mode do not include deferred fragments [+] Module Info ==================== GWT Version: 2.9.0 Content-Type: text/x-gwt-rpc; charset=utf-8 X-GWT-Module-Base: http://127.0.0.1/stub/ X-GWT-Permutation: 037A330198815EAE6A360B7107F8C442 [+] Methods Found ==================== ----- TestService ----- TestService.testArrayList( java.util.ArrayList/4159755760 TestService.testXsrfToken( java.lang.String/2004016611 ) TestService.testListAndList( java.util.List<java.util.ArrayList/4159755760 TestService.testIntAndList( I, java.util.List<java.util.ArrayList/4159755760 TestService.testListAndString( java.util.List<java.util.ArrayList/4159755760 TestService.testStringAndBoolean( java.lang.String/2004016611, java.lang.Boolean/476441737 ) TestService.testStringAndByte( java.lang.String/2004016611, B ) TestService.testStringAndChar( java.lang.String/2004016611, C ) TestService.testStringAndInt( java.lang.String/2004016611, I ) TestService.testStringAndBool( java.lang.String/2004016611, Z ) TestService.testStringAndFloat( java.lang.String/2004016611, F ) TestService.testStringAndLong( java.lang.String/2004016611, java.lang.Long/4227064769 ) TestService.testArrayListAndString( java.util.ArrayList/4159755760 TestService.testDetails( java.lang.String/2004016611, java.lang.String/2004016611, I, D, java.lang.String/2004016611 ) [+] Summary ==================== Showing 1/5 Services Showing 14/25 Methods
我们可以看到,这里的警告信息指出,单个置换文件是不包含延迟型的JavaScript片段的(事实上的确如此)。但是,在这种情况下,因为我们传递的是GWTMap生成的“全部”置换文件,而不是应用程序本身的单个置换文件,所以不用担心这种情况。
现在,假设我们要为其中的某个方法生成示例RPC请求。这时,我们可以进一步过滤特定的方法,并使用“--rpc”标志为其生成RPC请求。为了进一步减少输出内容,可以使用“-q”标志,让该工具在“安静模式”下运行。
不过,由于我现在分析的是本地文件,而不是远程URL,所以在RPC生成过程中,基础模块将是未知的。不过不用怕,你可以通过用“-base”参数传递模块的基础URL来解决这个问题:
$ ./gwtmap.py -F ./1601747650_037A330198815EAE6A360B7107F8C442.cache.js \ --base http://192.168.22.120/olympian/ --filter TestService.testDetails \ --rpc –q Warning: Individual permutation files in -F/--file mode do not include deferred fragments ----- TestService ----- POST /olympian/testService HTTP/1.1 Host: 192.168.22.120 Content-Type: text/x-gwt-rpc; charset=utf-8 X-GWT-Permutation: 037A330198815EAE6A360B7107F8C442 X-GWT-Module-Base: http://192.168.22.120/olympian/ Content-Length: 260 7|0|10|http://192.168.22.120/olympian/|67E3923F861223EE4967653A96E43846|com.ecorp.olympian.client.asyncService.TestService|testDetails|I|D|java.lang.String/2004016611|§param_Bob§|§param_Smith§|§param_Im_a_test§|1|2|3|4|5|7|7|5|6|7|8|9|§32§|§76.6§|10|
如上所示,这将输出一个GWT-RPC请求,实际上,我们既可以将其保存到文件中,也可以将其直接粘贴到Burp Intruder中。由于GWTMap输出的RPC payload中的参数与Burp使用的标志相同,当粘贴到Intruder中时,我们可以立即看到请求中的变量,并开始探测漏洞代码:
另外,我们也可以让GWTMap自动审查各种服务,同时,也可以将Burp设置为代理,以记录相关网络流量供以后使用。为此,只需加入“--probe”和“--proxy”参数即可。然后,GWTMap将使用生成的payload自动发送HTTP POST请求并输出响应内容:
$ ./gwtmap.py -F ./1601747650_037A330198815EAE6A360B7107F8C442.cache.js \ --base http://192.168.22.120/olympian/ --proxy http://127.0.0.1:8080 \ --filter TestService.testDetails --rpc --probe -q ----- TestService ----- TestService.testDetails( java.lang.String/2004016611, java.lang.String/2004016611, I, D, java.lang.String/2004016611 ) ... ... HTTP/1.1 200 //OK[1,["Name: param_Bob param_Smith\nAge: 32\nWeight: 76.6\nBio: param_Im_a_test\n"],0,7]
如果需要,还可以分别使用“--cookies”和“--Basic”标志来设置HTTP cookie和HTTP基本身份验证。但是,在本例中并不需要这样做,因为通过查看上面的输出,就可以RPC请求已经生成,并成功地用于与指定的服务进行了交互。
现在,请切换到Burp的history选项卡,找到该请求,就可以继续与该服务进行交互,并通过手动搜索来考察该方法中的漏洞了:
小结
GWT应用程序不仅非常有趣,同时也令人十分沮丧。我们希望这项研究可以帮助您更好地理解该主题,并且,希望GWTMap可令您的安全测试变得更加轻松。
GWTMap应该适用于2014年至2020年之间对所有版本的GWT(不管是否经过了混淆处理)。但是,它确实有一些局限性,因为它在生成RPC请求时只能序列化部分Java类型,并且只能处理特定的List和自定义对象的细节。
根据在客户端Java实现中初始化服务的方式的情况,它有时也可能无法检索服务路径,或者无法正确地将方法与给定服务进行关联。但是,如果发生这种情况,它将返回警告信息,并且GWTMap将根据所拥有的信息给出一些合理的建议。
如果您认为它存在某些不足之处或某些方面可以继续改进,或者您可以设法克服它的某些局限性,请随时为它做出贡献,以帮助它保持活力!
致谢
特别感谢发布相关工具(例如GDS,Steven Seeley和TheHackerish发布的工具)的安全研究人员所做出的努力和贡献。
参考资料
[1] Brian Slesinsky - The GWT-RPC wire protocol
https://docs.google.com/document/d/1eG0YocsYYbNAtivkLtcaiEE5IOF5u4LUol8-LL0TIKU/edit
[2] GDSSecurity - GWTEnum: Enumerating GWT-RPC Method Calls
https://blog.gdssecurity.com/labs/2010/7/20/gwtenum-enumerating-gwt-rpc-method-calls.html
[3] GDSSecurity - GWT-Penetration-Testing-Toolset
https://github.com/GDSSecurity/GWT-Penetration-Testing-Toolset
[4] srcincite.io (Steven Seeley) - From Serialised to Shell :: Auditing Google Web Toolkit
https://srcincite.io/blog/2017/04/27/from-serialised-to-shell-auditing-google-web-toolkit.html
[5] srcincite.io (Steven Seeley) – gwt.py
https://github.com/sourceincite/tools/blob/master/gwt.py
[6] TheHackerish - Hacking a Google Web Toolkit application
https://thehackerish.com/hacking-a-google-web-toolkit-application/
[7] TheHackerish – GWTab Burp Extension
https://github.com/thehackerish/GWTab
本文 翻译自:https://labs.f-secure.com/blog/gwtmap-reverse-engineering-google-web-toolkit-applications/如若转载,请注明原文地址: