4

CTF线下赛AWDP总结

 1 year ago
source link: https://5ime.cn/awdp.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
CTF线下赛AWDP总结

CTF线下赛AWDP总结

最近参加了 CISCN 的分区赛,其赛制为 AWDP,博主也是第一次打 AWDP,博主是 Web 手,同时带了一个茶歇 Ak 手,1v4太痛苦啦。一上午都在二等奖,最后黑灯模式变成了三等奖…这里记录一下博主赛前准备的内容,以及一些总结复盘,同时可以和我之前写的CTF线下赛AWD攻防总结一起食用

首先了解一下什么是 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 导致一直修复失败

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

tar -zcvf fix.tag.gz *

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

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

简单来说赛前可以收集一些写好的过滤规则,到时候直接调用,节省时间。由于 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!" )
const keywords = ["apple", "banana", "cherry"];

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

由于博主不会 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({extended: true}))
.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',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))

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.pyblackwords 列表内再加几个函数,但是一直没 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!"

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK