本文最后更新于:2024年12月28日 晚上
这段时间忙着考研,很久没有写博客了。这次国赛被神级逆向带进了半决赛,也算是第一次打awdp,记录一下吧。
第一次打awdp,队友实在是给力,最终拿到了15名,虽然有一些小小遗憾没有进决赛,但也基本上是满足了(甚至说有点意外,因为我几乎没做准备)。比较惭愧的是我们完全没拿到攻击分,靠着恰patch分进的15名😭。
search
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,可以不用管引号,经过测试是可行的
因为复用了一次get,所以长度也没有超标,至此完成完美getshell,执行cat /flag
获得flag
其实还是走了一段弯路,可以构造getallheader来得到请求头,利用请求头传参来实现rce
1
| $pi=base_convert,$pi(696468,10,36)(($pi(8768397090111664438,10,30))(){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的缝隙。也不要死磕一题,时间最重要,没有想法及时收手。