Vaudit白盒PHP代码审计笔记
本文最后更新于 2024-08-29,文章内容可能已经过时。
Vaudit审计
一、安装
下载连接:
https://github.com/virink/VAuditDemo/tree/master
1、上传vaudit压缩文件到/opt/lampp/htdocs
2、解压缩
unzip VAuditDemo-master.zip
3、修改目录名称
cd /opt/lampp/htdocs
cd VAuditDemo-master
mv VAuditDemo_Debug vaudit
cd ..
mv VAuditDemo-master vaudit
4、修改httpd.conf文件
vi /opt/lampp/etc/httpd.conf
添加一个端口
Listen 81
保存并退出。
5、编辑httpd-vhost.conf
vi /opt/lampp/etc/extra/httpd-vhosts.conf
把文件中的原来内容全部删除,并把下面的内容输入到文件中
<VirtualHost *:81>
serverName localhost
DocumentRoot "/opt/lampp/htdocs/vaudit/vaudit"
</VirtualHost>
6、重启lampp
/opt/lampp/xampp restart
7、访问项目首页
http://192.168.217.128:81/index.php
可以看到url跳转到系统安装的地址
http://192.168.217.128:81/install/install.php
8、修改目录和文件权限
chmod o+w /opt/lampp/htdocs/vaudit/vaudit/sys
chmod o+w /opt/lampp/htdocs/vaudit/vaudit/uploads
chmod o+w /opt/lampp/htdocs/vaudit/vaudit/sys/config.php
再次访问页面,
http://192.168.217.128:81/install/install.php
看到下面的页面,说明修改成功
在以上页面输入正确的数据库地址,用户名,密码点击安装按钮。数据库名称不用修改,就使用vauditdemo。
如果数据库已经存在,那么先删掉数据库,再点击安装按钮。
看到下面的页面,说明安装成功。
二、代码整体结构
_├── about.inc ---说明信息
├── admin ---管理员目录
│ ├── captcha.php --生成图片验证码
│ ├── delUser.php ---删除用户
│ ├── index.php --管理员的主页面
│ ├── logCheck.php --检查管理员登录的用户名和密码
│ ├── login.php --登录页面
│ ├── manageAdmin.php --添加新的管理员
│ ├── manageCom.php --管理留言
│ ├── manage.php --管理其他功能的入口页面
│ ├── manageUser.php --管理普通用户
│ └── ping.php --ping可以探测网络是否联通
├── css --- css文件
│ ├── bootstrap.css
│ ├── bootstrap.min.css
│ ├── bootswatch.less
│ ├── bootswatch.min.css
│ └── variables.less
├── footer.php ---页脚
├── header.php ----页头
├── images ---图片目录
│ └── default.jpg
├── index.php ---入口文件
├── install ---安装文件
│ ├── install.php
│ └── install.sql
├── js ---js文件
│ ├── bootstrap.min.js
│ ├── bootswatch.js
│ ├── bsa.js
│ └── check.js
├── messageDetail.php ---留言详情页面
├── message.php ---留言页面
├── messageSub.php ---留言添加到数据库
├── search.php ----搜索页面
├── sys ---配置目录
│ ├── config.php --配置文件
│ ├── install.lock
│ └── lib.php --库函数
├── uploads -----图片上传目录
└── user -----普通用户目录
├── avatar.php --管理用户头像
├── edit.php --编辑用户信息
├── logCheck.php --验证登录的用户名和密码
├── login.php --登录页面
├── logout.php --退出登录
├── regCheck.php --注册新用户
├── reg.php --注册页面
├── updateAvatar.php --更新头像
├── updateName.php --更新用户名
├── updatePass.php --更新密码
└── user.php --用户界面
三、功能列表
1、注册新用户
2、用户登录
3、退出登录
4、修改用户名,用户密码,上传头像图片
5、用户发表留言
6、查看某个留言详细信息
7、根据关键字查询留言列表
8、查看留言列表
9、关于页面
10、管理员登录
11、添加管理员
12、删除管理员
13、管理员删除普通用户
14、管理员删除评论
15、管理员Ping的功能
四、漏洞审计
系统重新安装漏洞
在看到上面的安装成功页面后,如果再次访问安装的地址
http://192.168.217.128:81/install/install.php
可以使用抓包工具burp或者fiddler,以下是fiddler抓包结果
可以看到虽然跳转到了index.php,但是install.php被调用后,还是返回了安装页面一些内容。这会导致一些敏感信息的泄露。
原因分析:
导致上面这个问题的根本原因在于,在判断了安装已经完毕后,虽然使用请求头
header( "Location: ../index.php" );
来让页面跳转,但是在php中,页面即使跳转,但是后面的代码也会依然继续执行。
解决方案
在第5行增加die(),终止下面的代码继续执行
所以即使安装完毕之后,可以通过burp抓包,看到install.php页面返回的安装表单。
可以根据安装表单,来自己构造一个安装请求,注入一句话木马到config.php
POST /install/install.php HTTP/1.1
Host: 192.168.218.128:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Cookie: PHPSESSID=p1t2n3j5ejheqf2hbohuhuk337
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 79
dbhost=localhost&dbuser=root&dbpass=1234&Submit=sub&dbname=aaaa;#";phpinfo();//
命令注入
通过管理员登录,并进入Ping的页面
可以看到这个使用了管道符,执行了其他的系统命令
产生的原因
ping.php中拿到用户提交的参数,并没有做任何过滤,直接使用了
解决方案
可以考虑对传入的参数进行严格过滤,只能限制输入ip地址(正则校验)
SQL注入
查看留言详细信息页面
messageDetail.php的代码
看看这个sqlwaf的函数是怎么做的,参考lib.php文件
上面这个函数对传入的参数做了一些防护。但是,这里存在着逻辑漏洞
由于把&&,|| 这些替换成"",那么我们可以在select这些关键字中间增加||来绕过,
例如
selec||t * fro||m tablename
所以最后的payload是:
id=9 unio||n selec||t 1,database(),3,4 lim||it 1,1
存储型XSS
在管理员查看普通用户信息的地方,
可以查看登录用户的IP地址
manageUser.php代码
user_name和login_id是从数据库中查询出来的,但是用户名经过了htmlspecialchars的转义,继续查找ip地址是怎么传入数据库的。
转到logCheck.php
显示用get_client_ip()获取ip地址,再经过sqlwaf处理
来到lib.php看到了get_client_ip(),里面有个可以用请求头x-forwarded-for注入的点。
这里可以进行存储型XSS注入
拦截住登录请求,添加一个请求头
x-forwarded-for: <script>alert(1)</script>
看到数据库表中已经保存了xss脚本
再次去页面查看ip地址,会弹出警告框
根本原因
对请求头没有做充分的过滤防护
解决方法
针对从请求头中获取的ip地址,可以做精确的过滤处理
验证码绕过
管理员的登录页面存在验证码,当需要爆破密码进行登录时就需要识别验证码或者绕过验证码。
admin/login.php
可以看到验证码生成后,放入session中
登录验证,admin/logCheck.php
比较验证码的值
具体操作步骤:
先打开firefox并且设置代理到burp
打开burp,设置拦截
打开管理员登录页面
http://192.168.217.128:81/admin/login.php
并检查一下是否有cookie,如果有清空所有的cookie
输入用户名,密码,验证码,然后登录
此时burp拦截了请求,可以看到请求中没有cookie,但是参数中有captcha=1714
此时,把captcha=1714这个参数删掉
请求变成了,直接点击forward按钮放行
看到下面这个页面说明验证码绕过成功
原理分析
在比较验证码是否正确的时候,使用了代码
if(@$_POST['captcha'] !== $_SESSION['captcha']){
header('Location: login.php');
exit;
}
由于我们在拦截请求后,删除了参数captcha=1714,而且我们也删除了cookie中的PHPSESSID,所以
$_POST['captcha']获取到的是null , 此时 $_SESSION['captcha'] 得到的也是null
因此 if(null !==null) 这个判断是false,不会进入到这个if代码体内部。
这样就绕过了验证码判断
解决方案
在验证码比较的时候要判断是否得到是值是null,不管是用户的参数,还是session中获取的,只要没有值,或者不相等,都认为验证失败。
任意文件读取
avatar.php,这里的代码是从session中根据avatar的值获取内容,然后根据内容,读取文件
file_get_contents($_SESSION['avatar']);
跟踪$_SESSION[‘avatar’]看看是在什么地方设置的值来到user/logCheck.php
发现来自数据库表读取的值,那么再继续跟踪,看看是什么时候设置进入数据库表的。
来到updateAvatar.php中发现了其更新语句
这个update语句的语法
UPDATE users SET user_avatar = '../images/default.jpg' WHERE user_id = 9
可以改成
UPDATE users SET user_avatar = '../images/default.jpg',user_avatar = '/etc/passwd' WHERE user_id = 9
更新结束可以看到,右面的一个值被更新成功
根据这个更新规则,我们构造一个payload
', user_avatar = '../sys/config.php' WHERE user_name = 'zhangsan'#.jpg
重现步骤:
用普通用户zhangsan登录后,编辑用户信息页面,上传一个头像,打开burp拦截住上传请求
根据我们的payload,修改filename这个参数的值,使用我们的payload
', user_avatar = '../sys/config.php' WHERE user_name = 'zhangsan'#.jpg
发现结果不对,
原因是文件名中不能含有路径,那么我们把文件名中的路径做16进制编码,注意文件名转成16进制的时候,包裹文件名’…/sys/config.php’的单引号就可以去掉了,直接把…/sys/config.php转为16进制:0x2e2e2f7379732f636f6e6669672e706870
更新成功
接下来登录然后读取头像文件就可以看到config.php文件的内容。
文件包含
index.php,使用了include函数包含about.inc文件,这里限制了后缀只能是.inc
格式的,可以尝试使用伪协议绕过,例如phar://, 因为phar可以读取压缩文件中的文件
利用步骤:
在上传文件处上传一个一句话木马,注意需要将木马文件后缀该为.inc,并将其压缩,压缩文件后缀该为.jpg格式。
木马文件test.inc
<?php phpinfo();?>
把test.inc 放入压缩文件test.zip中,并且把test.zip的扩展名改为test.jpg
上传test.jpg,注意记住上传的具体时间,具体到秒
根据服务端updateAvatar.php的上传文件的重命名规则
上传后文件名称是
根据我们记录的上传的时间,转换成整数形式的时间,并且拼接成上图的文件名称
payload是
phar://uploads/u_1711268439_test.jpg/test
访问一下index.php
http://192.168.109.128:81/index.php?module=phar://uploads/u_1711268439_test.jpg/test
可以看到木马test.inc中的php代码被执行了
越权
编辑用户信息中的修改用户名
在updateName.php中发现更新用户名时根据用户id是从浏览器传入的。这样就导致了可以修改任意用户用户名的越权问题。
查看数据库,lisi的id是10,
提交的请求如下
可以看到数据库id=10的用户名原来是lisi,现在是wangwu
SQL注入+越权
登录成功后跳转,看到user.php中,
$query = "SELECT * FROM users WHERE user_name = '{$_SESSION['username']}'";
$_SESSION['user_id'] = $result['user_id'];
怀疑有sql注入风险,如果用户名中有单引号就可以造成注入。根据用户名查询到的user_id放入到session,那么,以后更新操作都根据user_id,就可能导致越权。
下面就是研究如何能让session的username有单引号出现。
查看regCheck.php,这里clean_input只是对单引号,双引号,反斜线做了防御
$clean_name = clean_input($_POST['user']);
$clean_pass = clean_input($_POST['passwd']);
那么这个insert语句就可以注册用户名含有单引号的账号
$query = "INSERT INTO users(user_name,user_pass,user_avatar,join_date) VALUES ('$clean_name',SHA('$clean_pass'),'$avatar','$date')";
再去看logCheck.php
$query = "SELECT * FROM users WHERE user_name = '$clean_name' AND user_pass = SHA('$clean_pass')";
从数据库中查询用户信息,包含用户名和用户的id,并且把从数据库查到的用户名放入session,那么session中的用户名就会存在单引号。
$_SESSION['username'] = $row['user_name'];
我们可以考虑先注册一个带有单引号的用户名,然后在去更改用户信息。就能够越权修改其他用户的信息了
注册的用户名长度不能超过16个字符长度,所以可以使用
'||1 limit 1,1#
这里在数据库中的形式就是这样:
这样虽然使用 '||1 limit 1,1#
这个账号登录,实际浏览器在数据库中拿到的却是wang\
这个用户的账号的信息。这样就修改这个账户的密码就是修改wang\
这个用户的密码
二次注入
先观察messageSub.php,添加留言到数据库的代码,如果从session取出的username中包含有反斜线,那么这个insert语句中,values中的username左边的单引号就会跟clean_message 变量左边的单引号形成闭合,我们就可以考虑通过message输入项进行sql注入
message的值经过了clean_input函数处理,追踪一下clean_input函数的代码,可以看到,先用stringslashes去掉输入值中的反斜线,然后mysql_real_escape_string来处理输入值中的单引号,双引号,反斜线,NULL等字符,并没有把一些数据库的关键字过滤掉
看到这里,我们考虑的利用步骤:
(1)先尝试注册一个名字结尾是反斜线的用户看看是否能成功
来到regCheck.php,查看注册的代码
这里仅仅使用了clean_input函数来简单的处理,所以可以注册一个名字结尾有反斜线的用户
发现能够注册成功
退出,重新登录一下,让用户名刷新到session中
看到这个报错,不用理会,直接点击留言。
点击“发留言”按钮,可以来到发留言的页面
根据对添加新留言到数据的insert语句的分析
INSERT INTO comment(user_name,comment_text,pub_date) VALUES ('{$_SESSION['username']}','$clean_message',now());
针对留言内容,我们可以构造如下的payload
,(select database()),now())#
点击“留言”按钮,返回到留言列表,会直接看到数据库名字
接下来在爆库的字段:
点击“留言”按钮,返回到留言列表,会直接看到数据库字段的名字
,(select GROUP_CONCAT(table_name) from information_schema.tables where table_schema=database()),now())#
- 感谢你赐予我前进的力量