PHP代码审计
审计套路
- 通读全文法 (麻烦,但是最全面) 
- 敏感函数参数回溯法 (最高效,最常用) 
- 定向功能分析法 (根据程序的业务逻辑来审计) - 初始安装
- 信息泄露
- 文件上传
- 文件管理
- 登录认证
- 数据库备份恢复
- 找回密码
- 验证码
- 越权
- 注入
- 第三方组件
- CSRF,SSRF,XSS……
 
审计方法
- 1.获取源码 
- 2.本地搭建调试 
 可先使用扫描器识别常见传统漏洞,验证扫描器结果,手动正则
- 3.把握大局 
 对网站结构,入口文件(查看包含了哪些文件),配置文件(看数据库编码),路由,伪全局变量和全局filter,资源加载顺序,了解数据库处理模式,考察filter是否绕过,了解XSS过滤机制,考察filter是否可绕过,错误信息输出控制,对每个模块的功能进行了解,配合文件数据库监控,从安装到后台功能使用和前台功能使用走一波,仔细观察每步的变化,找不到问题再开始认真审计
常见漏洞
安装问题
- 1.自动删除这个安装文件 
 通过生成一个lock文件来判断程序是否安装过
- 2.根本无验证 
 安装完成后不会自动删除文件,又不会生成lock判断是否安装过
 参考链接:PHPSHE B2C 重装
- 3.安装file 
 直接用GET提交step绕过,直接进入下一步
- 4.变量覆盖导致重装 
 可以GET,POST,COOKIE 任意提交一个变量名$insLockfile,给其赋空值,覆盖掉$insLockfile,从而让file_exists为false就不会退出
 参考链接:frcms 重装系统
- 5.判断lock后,无exit 
 判断是否存在lock文件,如果存在lock文件,就会header到index.php,但是header后并没有exit,所以 并不会退出,类似的还有javascript弹个框
 参考链接:开源轻论坛StartBBS前台getshell
- 6.解析漏洞 
 在安装完成后会将install.php 重命名为index.php.bak,但是由于Apache的解析漏洞:如果无法识别到最后一个后缀的话,就会向上解析,那么就又变成了php了,然后结合安装时的变量覆盖又成重装了。
- 7.满足一些条件不会退出的 
 参考链接:建站之星Sitestar前台Getshell一枚
包含漏洞
- 1.本地文件包含 
 很多都限制了包含的后缀结尾必须为.php,例如- include($a.'.php'),需要截断后面的- .php- 00截断gpc off && php < 5.3.4
- 长文件名截断
- 转换字符集造成的截断<iconv()截断>
- 伪协议 - 截取字符判断是不是.php
- 用zip(或者phar)协议绕过
 首先新建一个1.php,里面phpinfo,然后压缩成.zip,然后把zip的名字改成yu.jpg,然后把这个.jpg上传上去,然后包含
 Example:1 http://localhost/php/include.php?include_file=zip://C:\wamp\www.php\1.jpg%231.php` 
 
- 包含日志,环境变量 
 
- 00截断
- 2.见 - <PHP技巧之截断>
- 3.远程文件包含 - 包含远程文件,或者伪协议php://input data等
- 条件:allow_url_include on ,默认是off
- allow_url_include off 条件下RFI- allow_url_include为on ,allow_url_fopen为off
 伪协议: 
- allow_url_include && allow_url_fopen为off
 包含共享文件: 
 互联网上445端口基本上被过滤
 
- allow_url_include为on ,allow_url_fopen为off
- allow_url_fopen 默认是on
 
找回密码
- 1.验证token 
 在找回密码的时候生成一个token,然后存储到数据库中,然后把找回密码的地址发到邮箱中,url中就含有token,由用户点开后就能修改密码
- 2.延伸 
 一些cms的密码加密方式很难破掉,有时候拿到了管理的密码破不开,利用方法:一般找回密码是用的邮箱,首先把管理的邮箱注入出来,然后再去找回密码,再把数据库的token注入出来,构造一下地址,就能重置密码
- 3.rand函数生成token - $resetpwd=md5((rand());
 对rand()函数生成出来的数字进行MD5
 某些平台下(例如windows)RAND_MAX只有32768,如果需要的范围大于32768,那么指定min和max参数就可以生成大于RAND_MAX的数了,或者考虑用mt_rand()来替代它
 参考链接:Thinksaas找回密码处设计错误利用账户可找回密码。
- $encryptstring=md5($this->time.$verification.$auth);- 1 - $timetemp=date("Y-m-d H:i:s",$this->time);$auth=util::strcode($timetemp,'ENCODE');` - 算法的KEY并没有初始化,如果知道了这个时间,就可以生成加密的字符串 
 参考链接:Hdwiki设计缺陷知邮箱可改密码(包括管理员)
 
上传漏洞
- 1.未验证上传后缀 
- 2.验证上传后缀被bypass 
- 3.上传的文件验证了上传后缀,但是文件名不重命名 
 截断yu.php%00.jpg
- 4.上传路径可控 
- 5.解析漏洞 - Nginx
 yu.jpg/1.php
- Apache
 yu.php.xxx
 
- Nginx
- 6.验证方法 - MIME,客户端的JS验证,白名单,黑名单
 
- 7.绕过 - 大小写
- 文件名没trim
 在文件名后面加空格,windows下的x.php%81-%99 decode后仍为x.php,windows下的特性.php::$data
 
文件操作
任意文件删除,任意文件复制,任意文件重命名,任意文件移动,任意文件下载
首先尝试拿到配置文件中的数据库连接账号和密码,然后外链
拿到配置文件,拿到加密解密函数的key,生成加密字符串,结合具体的代码利用
- 1.文件删除 - 由于全局的过滤而不能注入时,可以用任意文件删除删掉这个文件
- 删除安装文件生成的lock文件,重装
- 参考链接:phpcms 2008 sp4 爆路径及任意文件删除漏洞
 
- 2.文件复制 - 要复制的文件  要复制的路径
 当两个都完全可控,可以直接把自己上传的图片复制成一个.php马
- 复制的文件可控 要复制的路径不可控 - 1 - copy(ROOT_PATH."$webdb[updir]/$value",ROOT_PATH."$webdb[updir]/{$value}.jpg") - 可以把$value控制为保存了qibocms的加密函数的key的配置文件,复制后成一个.jpg直接打开就可以看到key 
 
- 要复制的文件  要复制的路径
- 3.文件下载 
 下载配置文件 拿到key
 参考链接:qibocmsV7整站系统任意文件下载导致无限制注入多处
- 4.文件写入 
- 5.文件包含 
加密函数
拿到加密函数的key,加密一些特殊字符然后拿到加密的字符串
- 1.加密可逆 
 弱算法导致了知道明文,知道密文,可逆,拿到加密函数的key,从而自己生成一个想要的加密字符串
 参考链接:DedeCMS-V5.7-SP1(2014-07-25)sql注入+新绕过思路
 参考链接:phpcms最新版绕过全局防御暴力注入
- 2.加密可控 
 要加密的内容是可控的,密文会输出,这个可控的点能引入特殊字符,那么把一些特殊字符带入到这里面,拿到密文,再找到一处decode后会进行特殊操作的点,然后进行各种操作。
 参考链接:程氏舞曲CMS某泄露,导致sql注入
 参考链接:PHPCMS最新版(V9)SQL注入一枚
- 3.key泄露 
 参考链接:一个PHPWIND可拿shell的高危漏洞
XSS
- 1.输入输出 
- 2.foreach()的key值 
- 3.removeXSS函数 
- 多个函数处理,插入辣鸡数据绕过第一个函数后,第二个函数过滤了辣鸡数据 
CSRF
- 1.后台敏感操作
- 2.修改权限、导出数据等高危功能
- 3.Login Form CSRF
SSRF
- 1.绕过本地IP过滤(畸形IP,本地网段覆盖不完全) 
- 2.协议白名单 
- 3.跳转到本地IP 
- 4.DNS解析到本地IP 
- 5.DNS rebinding 
- 6. - Content-Disposition:attachment;filename="evil_file.exe;.txt"分号截断可绕过跳板机的filter
撸库
- 1.失败后没有清空session中的验证码 
- 2.ip第一次出现,验证码为默认值 
- 3.验证码md5显示在cookie中 
- 4.session保存到文件 
命令注入
- 常见命令注入函数
- 1.session 
- 2.算法 
XXE
- simplexml_load_string()
 默认情况下会解析外部实体,造成安全威胁,导致任意文件读取、命令执行漏洞。
越权
- 1.通过ID操作
- 2.通过cookie操作
注入
把用户可控的一些变量,带入到了数据库的各种操作中,并且没有做好过滤,例如:在注册用户的时候检测用户名是否存在,SQL语句是拼接SQL
- 1.select注入 
 一般使用union select 联合查询
- 2.update注入 - update set的位置
 看这个表的哪个column会被展示出来,就把查询出来的内容显示到这里
- where 后
 通过盲注的方式列出数据
 
- update set的位置
- 3.insert注入 
 把要输出的数据插入到这个column里面去
- 4.delete注入 
 通过盲注的方式列出数据
- 5.数字型注入 
 变量并没有用单引号括住,不需要用单引号区分数据与SQL命令,这样就会让一般的GPC等机制无用,因为不包括特殊字符
 强制类型转换intval
- 6.字符型、搜索型 - 有单引号括住,需要闭合单引号
- 全局没有做addslashes,在查询的时候再对一些用户可控的变量进行addslashes,遗漏了某些变量没addslashes
- 全局做addslashes,在全局文件中对GET POST COOKIE做addslashes
 首先get magic quotes gpc 判断GPC是否开启,如果没开启就调用addslashes来转义,如果开启就不调用addslashes
 
- 7.Magic_quotes_gpc - Magic_quotes_gpc在稍微高点的版本默认都是on,5.4已经废除,',",\,NULL会在前面添加上一个转义符
 
- Magic_quotes_gpc在稍微高点的版本默认都是on,5.4已经废除,
- 8.宽字节注入 - 数据库字符集GBK的宽字节注入
 数据库的连接方式不同,数据库与PHP的编码不一致,转换过程中可能存在
 错误方法:set names gbk
- 转换字符集造成的宽字节注入
 从gbk转到utf8
 参考链接:74cms 最新版 注入8-9
 从utf8转到gbk,錦从UTF8转成GBK之后成了%e5%5c,对GET POST COOKIE 做了addslashes,'转义后为\'->%5c %e5%5c5c'两个\,则引号出来
 参考链接:qibocms 下载系统SQL注入一枚
 
- 数据库字符集GBK的宽字节注入
- 9.解码导致 - 先提交encode的,那么就能不被转义,decode后再带入查询,造成了注入,无视GPC
- urlencode
- base64_decode
- XML
- Json_decode
 参考链接:qibocms B2b 注入一枚
 参考链接:phpdisk V7 sql注入2
 
- 10.变量覆盖 
 变量覆盖有extract、parse_str、$$- extract 
 extract($_POST)直接从POST数组中取出变量,覆盖掉之前的一些变量,覆盖的话,一般是覆盖掉表前缀之类的。- 1 - selet * from $pre_admin where xxx - 像这种就覆盖掉$pre,然后直接补全语句注入 
 参考链接:qibocms分类注入一枚可提升自己为管理
 参考链接:phpdisk V7 sql注入2
- $$
 参考链接:MetInfo最新版(5.2.4)一处SQL盲注漏洞
 
- 11.Replace - 把 - '替换成空,但是通过又全局有转义- ?<单引号- '转义为- \',然后替换- '为空格,留下- \,注释掉- ',破坏原本的SQL
 用户提交一个- '全局转义成- \',然后这过滤函数又会把- '替换成空,那么就留下- \导致可以吃掉一个单引号。
 需要double query ,两处可控输入- 1 - select * from c_admin where username=' admin\' and email=' inject#' 
- 有时会把 - '"都替换成空,然后提交之后去掉了- ',不把- '替换成空,但是- "也会被转义,那么提交一个- "就又剩下了一个转义符了。
 参考链接:PHPCMS全版本通杀SQL注入漏洞
- 一些replace 是用户可控的,就是说用户可以控制替换为空的内容 - 1 - $order_sn=str_replace($_GET['subject'],'',$_GET['out_trade_no']); - 这里因为会被转义,如果提交 - '就变成- \',并且这里替换为空的内容get来的,那就想办法把- \替换掉
 addslashes 会对- ',",\,NULL转义,- '变成- \',- "变成- \",- \变成- \\,- NULL变成- \0, 提交- %00'会被转义生成- \0\',这时候再提交把0替换成空,那么就剩下- \\',- \\表示- \的转义,- '单引号也就成功出来了。
 参考链接:cmseasy绕过补丁SQL注入一枚
 
- 12.server注入 
 -只对GET POST COOKIE 进行addslashes,没有对SERVER济宁转义,一些server的变量,用户可控并写入数据库- 1 - QUERY_STRING , X_FORWARDED_FOR , CLIENT_IP , HTTP_HOST , ACCEPT_LANGUAGE - 最常见的当然也就是X_FORWARDED_FOR,一般是在IP 函数中用户,没有验证ip 是否合法,直接return。 
 参考链接:Phpyun注入漏洞二- 正则验证错误
 参考链接:CmsEasy最新版本无限制SQL注射
 
- 正则验证错误
- 13.file注入 - 全局只对GET POST COOKIE 转义,遗漏了files,,且不受GPC,files注入一般是因为上传,会把上传的名字带到insert当中入库
 参考链接:qibocms 黄页系统SQL注入一枚
- 在入库的时候对文件的名字进行转义,在获取后缀后再入库时对文件名转义了却没有对后缀转义也导致了注入
 参考链接:Supesite 前台注入 #2 (Insert)
 
- 全局只对GET POST COOKIE 转义,遗漏了files,,且不受GPC,files注入一般是因为上传,会把上传的名字带到insert当中入库
- 14.未初始化造成的注入 
 php < 4.20时,register_globals默认都是on,逐渐register_globals默认都是off
 伪全局机制,遗漏了初始化
 参考链接:qibocms地方门户系统注入一个问题
 参考链接:qibocms地方门户系统注入
 参考链接:齐博地方门户系统SQL注入漏洞
 参考链接:齐博整站/地方门户SQL注入漏洞
- 15.数组中的key 
 判断GPC是否开启,如果off就对数组中的value进行addslashes,没有对数组中的key进行转义,key带入sql,听说低版本的php对二维数组中的key就算GPC ON 也不会转义
 参考链接:qibocms V7 整站系统最新版SQL注入一枚 & 另外一处能引入转义符的地方。
 参考链接:qibocms多个系统绕过补丁继续注入
 参考链接:qibocms全部开源系统 Getshell
 参考链接:Discuz 5.x 6.x 7.x 前台SQL注入漏洞一枚
- 16.offset 
 - $_GET[a]提交的是一个数组,且含有一个key为0,那么- $a就是对应的这个key的value,但是这里并没有强制要求为数组。
 提交一个字符串就为了- \,吃掉一个单引号,然后就在$b处写入inject 可以注入
 参考链接:qibocms 地方门户系统
- 17.第三方插件 
 常见的uc_cencert / alipay / tenpay / chinabank- 默认UC里面都会striplashes
- uckey默认的
- uckey这个常量没有初始化
- uckey可控
 参考链接:phpmps 注入 (可修改其他用户密码,官网成功)–UC
 参考链接:PHPEMS (在线考试系统) 设计缺陷 Getshell一枚(官网已shell)–UC
 参考链接:最土团购注入一枚可直接提升自己为管理 & 无限刷钱。–CHINABANK
 参考链接:Destoon Sql注入漏洞2(有条件)–TENPAY
 参考链接:Destoon Sql注入漏洞一枚(有条件)–TENPAY
 参考链接:CSDJCMS程式舞曲最新版Sql 一枚–TENPAY
 
- 18.数字型注入 - 一般数字型的都不会加单引号,$id没被单引号且没有被强制类型转换
 参考链接:qibocms 地方门户系统 注入#3
- 不是一些数字型,忘记加单引号 - 1 - $query=$_SGLOBAL['db']->query('select * from '.tname('spacetags').' where itemid=\''.$itemid.'\' and status = \''.status.'\''); - $itemid首先带入查询中,被单引号,如果查询有接过才会带入到delete中,如果无接过就不执行delete。在数据库中itemid中存储的是int类型,所以这里本意是只能提交数字型才能查询出结果,如果不是提交数字的话,那么就查询不出来结果,就不去执行下面的delete语句了。但是由于mysql的类型转换,因为这里存储的是int类型,所以4xxxx跟4 是一样的。 - 1 - $_SGLOBAL['db']->query('delete from '.tname('spacetags').' where itemid='.$itemid.' and tagid in ('.simplode($deletetagidarr).') and status=\''.$status.'\''); 
- PHP弱类型语言
 参考链接:phpyun v3.2 (20141226) 两处注入。
 
- 一般数字型的都不会加单引号,$id没被单引号且没有被强制类型转换
- 19.二次注入 - 涉及到的是入库和出库
 在入库时经过全局转义,insert into table(username) values (a\'');
 入库后转义符就会消失,那么就是a',把这个查询出来,那么出库的就是a',如果再带入到了查询,那么就成功的引入了单引号导致了注入,很多时候数据库中存储的长度是有限制的。 
 参考链接:phpyun v3.2 (20141226) 两处注入。
 参考链接:qibocms 地方门户系统 二次注入#5
 参考链接:74cms (20140709) 二枚二次注入
 参考链接:Hdwiki最新版二次注入一枚
 
- 涉及到的是入库和出库
- 20.查询当中key可控 
 把$_POST带入到了查询函数,然后foreach key ,foreach 出来的key做了查询中的column。
 防止方法一般是把数据库中的column查询出来,然后in_array判断一下$_POST出来的key是否在数据库中的column中。
 参考链接:云人才系统SQL注入,绕过WAF
 参考链接:Cmseasy SQL注射漏洞之三
- 21.striplashes 
 在全局addslashes后,在后面的文件中又stripslashes去掉了转义符,然后可以闭合单引号- 1 - $_SESSION['flow_consignee'] = stripslashes_deep($consignee); - 参考链接:ecshop全版本注入分析 
- 22.截取字符 - 会限制用户输入的长度,只截取一部分
- cutstr($asd,32);,只允许输入32个字符,没有在截取字符的后面加其他字符
 提交一个- 11111111111',被转义后成- 11111111111\',绕后截取32个字符就是- 11111111111\
 double query的话,吃掉一个单引号,然后下一个连着的可控变量可以注入
 参考链接:Hdwiki (20141205) 存在7处SQL注入漏洞(含之前处理不当安全的漏洞)
 
- 23.注册GLOBALS变量 
 把GET POST COOKIE 循环出来,然后注册一个变量,这里不允许创建GLOBALS变量,如果设置了REQUEST的GLOBALS,就直接退出
 低版本request order 是GPC ,在php5.3以后request order 默认成了GP ,也就是request成了get 和post ,不包含cookie,所以$_REQUEST里面就不包含COOKIE提交来的,而这里也把COOKIE循环出来,注册变量,所以这里在COOKIE里面提交GLOBALS就不会被检测出来,而且也成功注册了GLOBALS变量,所以再结合后面的一些些代码就造成了代码执行。
 参考链接:Discuz!某两个版本前台产品命令执行
- 24.PDO注入 - 查看prepare()硬编码的string是否可控
- PDO无法安全处理order by 需求 bool-blind order by if([expr],id,name)
 
敏感逻辑
- 1.认证与会话 - 重置、找回密码
- 人机验证绕过
- session固定
- 密码存储、加密算法是否合理 ,rand()/mt_rand() 注意种子生成逻辑
- sso / oauth / openid 使用合规
- 注意Cookie中包含的可读数据
 
- 2.交易 - 条件竞争 select <> for update
- 服务器端数据校验逻辑
 
- 3.投票、统计 - 未使用REMOTE_ADDR获取ip地址
 
PHP黑魔法
- 1.弱类型 - string 转 int
 0e+纯数字优先转换
 字符截断(int)/intval() wp content injection
 json_decode()引入数字类型
- bypass- 使左右结果为弱类型的0,构造0e科学计数法
 "1e3" bypass
 Magic hash
- 传入空类型让函数报错返回null
 如果结果来自数据库,让其取不到数据
- 传入数组类型让函数报错返回null
 string/array/null类型可以从GPC传入
- ===使左右结果为true/false/null
- is_numeric 传入hex编码的str返回true
 
- 使左右结果为弱类型的0,构造0e科学计数法
- 函数结果可控- in_array/intval/md5/strcmp<5.3/switch
 
 
- string 转 int
- 2.正则匹配 - preg_replace
 \0 $0
 php<7 /e,php<5.4用00截断构造/e,(regex) /e %00
 thinkphp url rce
- preg_match
 php<=5.3 传入数组报错
 
- preg_replace