写在前面
最近参加了某分区赛,其赛制为 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 *
语言环境
最好拥有多个语言的环境(PHP
,Golang
,Java
,Python
,NodeJS
),当时比赛前确确实实是都装好了 (其实电脑上一直都有,不过 go
和 java
没写过) ,这五种语言的赛题确确实实也都遇到了,但当时 Golang
不熟悉语法同时没有扩展,并未打包成功,Java
题当时有思路修复,但不会打包 war
包...结果这两种语言的赛题全部扑街
其次就是像一些常见的库都提前安装一下,比如 flask
,springboot
之类的,最后就是学一下 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({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.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总结
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论