iami233
iami233
文章175
标签37
分类4

文章分类

文章归档

CTF线下赛AWDP总结

CTF线下赛AWDP总结

写在前面

最近参加了 CISCN 的分区赛,其赛制为 AWDP,博主也是第一次打 AWDP,博主是 Web 手,同时带了一个茶歇 Ak 手,1v4太痛苦啦。一上午都在二等奖,最后黑灯模式变成了三等奖…这里记录一下博主赛前准备的内容,以及一些总结复盘,同时可以和我之前写的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虚拟机用来打包

1
tar -zcvf fix.tar.gz *

语言环境

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

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

代码片段

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

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

PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

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

Node

1
2
3
4
5
6
7
const keywords = ["apple", "banana", "cherry"];

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

Java

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

1
2
3
4
5
6
7
8
String[] filterList = {"apple", "banana", "cherry"};
String str = "ana"; // 匹配包含"ana"的字符串

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

Golang

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

通防脚本

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

1
2
3
4
5
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示例如下:

1
2
3
4
#!/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 &
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
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

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

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

1
2
3
4
5
6
7
8
9
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示例如下:

1
2
3
4
#!/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 &
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
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 的内容

1
2
3
4
5
6
7
8
9
10
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 成功

1
2
3
4
5
6
filter_list = ["{", "(", "lipsum", "attr"]
for i in filter_list:
if i in word:
word = "Hacker!"
if not waf(word):
word = "Hacker!"
本文作者:iami233
本文链接:https://5ime.cn/awdp.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可