CTF线下赛AWDP总结

admin 2024年2月16日00:38:16评论24 views字数 8186阅读27分17秒阅读模式

写在前面

最近参加了某分区赛,其赛制为 AWDP...由于是第一次打,所以这里记录一下笔者赛前准备的内容,以及一些总结复盘,同时可以和我之前写的CTF线下赛AWD攻防总结一起食用

AWDP

首先了解一下什么是 AWDP ,AWDP模式(Attack,Defense,WebandPwn),分为 Break 与 Fix 环节。根据英文全称也可以看出来,只有 Web 和 Pwn 这两个方向的题目。

每个战队拥有相同的起始分数及相同配置的虚拟靶机,参赛队员需对平台中的GameBox发起攻击,向平台提交正确的flag(证明自己具备对该题的攻击能力);在此期间,由平台以轮次制的方式向参赛战队的靶机发起攻击,检查其他选手的漏洞是否修补成功,若修补成功则认为参赛战队具备该漏洞的防御能力。

简单来说,AWDP 和传统 CTF 并无任何区别,仅仅是多了一个 Fix 功能,也就是你提交 flag 后拿到的是攻击分,而 Fix 成功后才会拿到防御分

赛前准备

可以适当的准备一些通防脚本,通防住就是赚到,当然比赛的时候没防住😭,其次就是队伍内部一定要合理分工,不过笔者队伍就一个主力,没考虑分工的事情,这里就不多说了。

另外要清楚一点,能防不代表能打,反之亦然,不过笔者认为修复比攻击容易(也有可能是笔者拥有开发经验的缘故),所以真正到比赛的时候不要一直纠结于去攻击,可以看看如何修复。同时 Fix 或者 Break 越早越好,笔者是第三轮 Break 成功一个赛题,让我多拿很多轮次的分,所以说前期 Break / Fix 速度越快越好

另外一般比赛前都会让你提前熟悉平台以及提供测试赛题进行测试,一定要测明白...当时笔者感觉测试赛题没头没尾,直接没修复成功就跑路啦,导致第二天研究了俩小时如何进行打包出正确的 Fix 包,当时上传 Fix 包一直提示服务异常,后来看到公告才意识到是因为题目描述里给错了单词,patch.sh 写成了 pacth.sh 导致一直修复失败,另外感觉裁判机的 Check 机制也是很迷惑

打包的话,赛后看群友说 bandzip 可以直接对 tar.gz 进行压缩操作,笔者是直接起了一个Linux虚拟机用来打包

tar -zcvf fix.tar.gz *

语言环境

最好拥有多个语言的环境(PHPGolangJavaPythonNodeJS),当时比赛前确确实实是都装好了 (其实电脑上一直都有,不过 go 和 java 没写过) ,这五种语言的赛题确确实实也都遇到了,但当时 Golang 不熟悉语法同时没有扩展,并未打包成功,Java 题当时有思路修复,但不会打包 war 包...结果这两种语言的赛题全部扑街

其次就是像一些常见的库都提前安装一下,比如 flaskspringboot 之类的,最后就是学一下 build 的方法

代码片段

简单来说赛前可以收集一些写好的过滤规则,到时候直接调用,节省时间。由于 PHP 类的防御代码实在是太多啦,所以这里只简单列几个。

其实最主要的还是记录一下各个语言如何循环遍历匹配关键字即可,具体的黑名单内容还是得随机应变,具体可以看下面的赛题复盘

PHP

function wafrce($str){
    return !preg_match("/openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|scandir|assert|pcntl_exec|fwrite|curl|system|eval|assert|flag|passthru|exec|chroot|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore/i"$str);
}

function wafsqli($str){
    return !preg_match("/select|and|*|x09|x0a|x0b|x0c|x0d|xa0|x00|x26|x7c|or|into|from|where|join|sleexml|extractvalue|+|regex|copy|read|file|create|grand|dir|insert|link|server|drop|=|>|<|;|"|'|^||/i"$str);
}

function wafxss($str){
    return !preg_match("/'|http|"|`|cookie|<|>|script/i"$str);
}

if (preg_match('/system|tail|flag|exec|base64/i'$_SERVER['REQUEST_URI'])) {
    die('no!');
}

Python

filter_list = ["apple""banana""cherry"]
strings = "ana"  # 匹配包含"ana"的字符串
for i in filter_list:
    if i in strings:
        print("Hacker!" )

Node

const keywords = ["apple""banana""cherry"];

for (const i of keywords) {
    if (code.includes(i)) {
        console.log("Hacker!"
    }
}

Java

由于笔者不会 Java,赛前提前去 Github 找了一个项目,JavaSecFilters JavaSec过滤器 ,给出的代码片段也非常的不错,但是比赛时候不会打包 war 直接扑街...

String[] filterList = {"apple""banana""cherry"};
String str = "ana"// 匹配包含"ana"的字符串

for (String s : filterList) {
    if (s.contains(str)) {
        System.out.println("Hacker!");
    }
}

Golang

filterList := []string{"apple""banana""cherry"}
str := "ana" // 匹配包含"ana"的字符串
for _, s := range filterList {
    if strings.Contains(s, str) {
        fmt.Println("Hacker!")
    }
}

通防脚本

不过感觉通防脚本适用场景不太行... 具体可以随机应变,据其他赛区的师傅说,使用 K4l0nG_WAF 防住了一道 ThinkPHP 的题目

https://github.com/leohearts/awd-watchbird
https://github.com/sharpleung/CTF-WAF
https://github.com/NonupleBroken/AWD_PHP_WAF
https://github.com/DasSecurity-HatLab/AoiAWD
https://github.com/dr0op/k4l0ng_WAF

赛题复盘

rceIt

The magical node patch说明: 远程patch路径为:/app/app.js 推荐pacth.sh示例如下:

#!/bin/sh
cp ./app.js /app/app.js
ps -ef | grep app.js | grep -v grep | awk '{print $2}' | xargs kill -9
nohup node /app/app.js || tail -f /dev/null &

const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const randomize = require('randomatic');
const path = require('path');
const { VM } = require('vm2');

const app = express();
const vm = new VM();

function merge(target, source) {
    for (let key in source) {
        if (key === 'escapeFunction' || key === 'outputFunctionName') {
            throw new Error("No RCE")
        }
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

app
    .use(bodyParser.urlencoded({extendedtrue}))
    .use(bodyParser.json());
app.use(express.static(path.join(__dirname, './static')));
app.set('views', path.join(__dirname, "./views"));
app.set('view engine''ejs');
app.use(session({
    name'tainted_node_session',
    secretrandomize('aA0'16),
    resavefalse,
    saveUninitializedfalse
}))

app.all("/login", (req, res) => {
    if (req.method == 'POST') {
        let userInfo = {}
        try {
            merge(userInfo, req.body)
        } catch (e) {
            return res.render("login", {message"Login Error"})
        }

        if (userInfo.username == "admin" && userInfo.password === "realpassword") {
            userInfo.logined = true
        }

        req.session.userInfo = userInfo
        if (userInfo.username == "admin" && userInfo.logined == true)
        {
            return res.redirect('/sandbox')
        }
        else {
            return res.render("login", {message"You are not admin"})
        }
    }else {
        if (req.session.userInfo){
            if (req.session.userInfo.logined == true && req.session.userInfo.username == "admin"){
                return res.redirect('/sandbox')
            }else{
                return res.render("login", {message"You are not admin"})
            }
        }else {
            return res.render('login', {message""});
        }
    }
});

app.all('/sandbox', (req, res) => {
    if (req.session.userInfo.logined != true || req.session.userInfo.username != "admin") {
        return res.redirect("/login")
    }
    
    const code = req.query.code || '';
    result = vm.run((code));
    res.render('sandbox', { result });
})

app.all('/', (req, res) => {
    return res.redirect('/login')
})

app.listen(8888, () => console.log(`listening on port 8888!`))

在我印象中这道题貌似到比赛结束没人攻击成功,不过根据代码逻辑可以很快速的定位到漏洞点 /sandbox

result = vm.run((code));
res.render('sandbox', { result });

我们直接加一个判断 Fix 成功

const keywords = ["flag""exec""read""open""ls""cat"];

for (const i of keywords) {
    if (code.includes(i)) {
        result = "Hacker!"  
    }else{
        result = vm.run((code));
    }
}

search_engine

小明写了一个搜索引擎,但是看起来似乎有些问题。 patch说明: 远程patch路径为:/app/app.py 推荐pacth.sh示例如下:

#!/bin/sh
cp app.py /app/app.py
ps -ef | grep python | grep -v grep | awk '{print $2}' | xargs kill -9 
cd /app && nohup python app.py &

from flask import *
import os
from waf import waf
import re

app = Flask(__name__)

pattern = r'([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):([0-9]{2,5})'
content = '''<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta ip="%s">
<meta port="%s">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> 
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ciscn Search Engine</title>
</head>
<body>
<div class="htmleaf-container">
    <div class="wrapper">
        <div class="container">
            <h1>Ciscn Search Engine</h1>
            <form class="form" method="post" action="/" id="Form">
                <input name="word" type="text" placeholder="word">
                <button type="submit" id="login-button">Search</button>
            </form>
        </div>
        <ul class="bg-bubbles">
            <li>%s</li>
        </ul>
    </div>
</body>
</html>'''


@app.route("/", methods=["GET""POST"])
def index():
    ip, port = re.findall(pattern,request.host).pop()
    if request.method == 'POST' and request.form.get("word"):
        word = request.form.get("word")
        if not waf(word):
            word = "Hacker!"
    else:
        word = ""

    return render_template_string(content % (str(ip), str(port), str(word)))


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=int(os.getenv("PORT")))

后来通过 SSTI 读出来了 waf.py 的内容

import re
def waf(data):
    pattern=r'^[x20-x7e]+$'
    if len(re.findall(pattern,data))!=1:
        return False
    blackwords=['message','listdir','self','url_for','_','"',"os","read","cat","more","`","[","]","class","config","+","eval","exec","join","import","popen","system","header","arg","form","os","read","write","flag","ls","ll","sort","nl","",";",":","\"]
    for blackword in  blackwords:
        if blackword in data:
            return False 
    return True

原本修复的话打算直接在 waf.py 的 blackwords 列表内再加几个函数,但是一直没 Fix 成功,这题卡我一上午(嗯...前面提到的卡我俩小时就是这题),最后直接在 waf(word) 前面再加一个判断 Fix 成功

filter_list = ["{""(""lipsum""attr"]
for i in filter_list:
    if i in word:
        word = "Hacker!"
if not waf(word):
    word = "Hacker!"

原文始发于微信公众号(FTC安全):CTF线下赛AWDP总结

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月16日00:38:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CTF线下赛AWDP总结http://cn-sec.com/archives/2168572.html

发表评论

匿名网友 填写信息