找了一个比较小众的版本的PHP开源程序,选择了一个比较老的版本,参考了网上一些别人的审计方法同时我自己也进行了部分审计。本片文章就是记录自己的审计过程。
重新安装+GetShell
初识安装过程
文件的安装位置在:install/goinstall.php
在goinstall.php引入了文件1
2require_once("../class/c_md5.php");
require_once("install_fun.php");
c_md5.php,是封装好的用于对密码加密的函数
install_func.php,包含了每一步需要执行的函数的方法,如step1()
、step2()
、step3()
、step4()
以及chkoutput()
。chkoutput()
用于检查是否是非法提交,是否按照安装顺序安装网站。
安装绕过
网站的具体的安装逻辑如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22switch ($_GET["g"]) {
case "step2":
chkoutpost();
step2();
break;
case "step3":
chkoutpost();
step3();
break;
case "step4":
chkoutpost();
step4();
break;
default:
if (file_exists("../cmsconfig.php")) {
echo "<p align=center><br><br><br><font color=red>系统已安装!若要重新安装,请删除文件 cmsconfig.php ! </font></p>";
break;
} else {
step1();
break;
}
}
通过switch对参数g
判断目前安装进度。默认情况下,通过检查是否存在cmsconfig.php
文件判断网站是否已经安装。而其中的chkoutpost()
方法用于判断是否越权提交。
install/install_fun.php:chkoutpost()
:1
2
3
4
5
6
7function chkoutpost() {
$fromurl = $_SERVER['HTTP_REFERER'];
if ($fromurl == '') {
echo '<p align=center><br><font color=red>禁止非法提交!</font><br></p>';
die();
}
}
通过referer判断是否为越权安装,很明显是被可以绕过的。
getshell
步骤4:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32function step4() {
$root = $_POST["root"];
$dbuser = $_POST["dbuser"];
$dbpsw = $_POST["dbpsw"];
$dbname = $_POST["dbname"];
$tabhead = $_POST["tabhead"];
$ad_user = $_POST["ad_user"];
$ad_psw = $_POST["ad_psw"];
$webname = $_POST["webname"];
$weburl = $_POST["weburl"];
$webinfo = $_POST["webinfo"];
$webkeywords = $_POST["webkeywords"];
$webauthor = $_POST["webauthor"];
// some other code
// .......
$file = "config_empty.php";
$fp = fopen($file, "r"); //以写入方式打开文件
$text2 = fread($fp, 4096); //读取文件内容
$text2 = str_replace('@root@', $root, $text2);
$text2 = str_replace('@dbuser@', $dbuser, $text2);
$text2 = str_replace('@dbpsw@', $dbpsw, $text2);
$text2 = str_replace('@dbname@', $dbname, $text2);
$text2 = str_replace('@tabhead@', $tabhead, $text2);
$text2 = str_replace('@webname@', $webname, $text2);
$text2 = str_replace('@weburl@', $weburl, $text2);
$text2 = str_replace('@webinfo@', $webinfo, $text2);
$text2 = str_replace('@webkeywords@', $webkeywords, $text2);
$text2 = str_replace('@webauthor@', $webauthor, $text2);
$file = "../cmsconfig.php"; //定义文件
$fp = fopen($file, "w"); //以写入方式打开文件
fwrite($fp, $text2);
}
在step4()
函数中,第1行到12行是读取用户输入,虽然将用户的配置写入到config_empty.php
生成config.php
,至此整个系统安装完毕。
install/config_empty.php
代码:1
2
3
4
5
6
7
8
9
10
11$root="@root@"; #MySQL服务器
$dbuser="@dbuser@"; #MySQL用户名
$dbpsw="@dbpsw@"; #MySQL密码
$dbname="@dbname@"; #数据库名
$tabhead="@tabhead@"; #表前缀
$webname="@webname@"; #网站名称
$webkeywords="@webkeywords@"; #网站关键字
$webinfo="@webinfo@"; #网站简介
$weburl= "@weburl@"; #网站链接,根目录可填写/,便于移动网站
$webauthor="@webauthor@"; #网站编辑,前台显示
$webbegindate="2017-05-30"; #网站建立日期
所有的变量名都是使用双引号,那么就可以执行命令了。
POC
通过firefox的插件设置referer值
在重新安装中加入代码
访问cmsconfig.php
修复
这个漏洞在新版1.0.4中已经修复了,在安装成功之后直接删除goinstall.php
即可。
登录绕过
登录逻辑
登录绕过这个代码写的比较的有意思。
管理员登录的代码在login.php
中ad/login.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34function jsloginpost() {
global $tabhead;
global $txtchk;
@$user = $_POST["user"];
@$psw = $_POST["psw"];
$psw = authcode(@$psw, 'ENCODE', 'key', 0);
@$loginlong = $_POST["loginlong"];
setcookie("lggqsj", date('Y-m-d H:i:s', time() + $loginlong), time() + 60 * 60 * 24, "/; HttpOnly", "", '');
$tab = $tabhead . "adusers";
$chk = " where adnaa='" . $user . "' and adpss='" . $psw . "' ";
mysql_select_db($tab);
$sql = mysql_query("select * from " . $tab . $chk);
if (!$sql) {
$jieguo = "<div id=redmsg>(数据库查询失败!)</div>";
} else {
$num = mysql_num_rows($sql);
if ($num == 0) {
$jieguo = '<div id=redmsg>登录失败:账户或密码错误!</div>';
} else {
loginpass($loginlong);
$jieguo = '<div id=bluemsg>登录成功!正在前往<a href="index.php">后台</a>。。。</div><meta http-equiv="refresh" content="1;url=index.php">';
@$chkmoblie = isMobile();
if ($chkmoblie == 1) {
$jieguo = '<div id=bluemsg>登录成功!正在前往<a href="wap.php">后台</a>。。。</div><meta http-equiv="refresh" content="1;url=wap.php">';
}
}
}
$json_arr = array("jieguo" => $jieguo);
$json_obj = json_encode($json_arr);
echo $json_obj;
}
jsloginpost()
函数就是用来处理管理员登录。
在其中调用了loginpass($loginlong);
,此函数是在c_login.php
中定义的。
ad/c_login.php
代码进行了加密,为了方便看,对部分代码进行了删减:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// 判断用户是否已经登录的函数
function chkadcookie()
{
@$file = "../cache/txtchkad.txt"; //定义文件
@$fp = fopen($file, "r"); //以写入方式打开文件
@$txtchkad = fread($fp, 4096); //读取文件内容
$txtchkad2 = str_replace(@$_COOKIE["chkad"], '', $txtchkad);
if (@$_SESSION["chkad"] == '' && @$_COOKIE["chkad"] == '') {
header("Content-type:text/html; charset=utf-8");
echo '<div id=redmsg>请<a href="login.php">登录</a>。。。</div><script>tiao();</script>';
exit;
}
if ($txtchkad == $txtchkad2) {
header("Content-type:text/html; charset=utf-8");
echo '<div id=redmsg>请<a href="login.php">登录</a>。。。</div><script>tiao();</script>';
exit;
}
}
// 记录当前登录的用户
function loginpass($str) {
global $webauthor;
global $date;
$txtchkad = $_SERVER['HTTP_USER_AGENT'] . '_' . $_SERVER['REMOTE_ADDR'] . '_' . $date;
if (!file_exists('../cache')) {
mkdir('../cache');
}
$file = "../cache/txtchkad.txt"; //定义文件
if (file_exists($file)) {
$txt = file_get_contents($file);
$txt = $txtchkad . "\r\n" . $txt;
} else {
$txt = $txtchkad . "\r\n";
}
file_put_contents($file, $txt);
setcookie("chkad", $txtchkad, time() + $str, "/; HttpOnly", "", '');
$_SESSION["chkad"] = $txtchkad;
}
函数loginpass($str)
中的参数$str
表示的cookie的有效时间。
登录之后在,拼凑用户的表示$txtchkad = $_SERVER['HTTP_USER_AGENT'] . '_' . $_SERVER['REMOTE_ADDR'] . '_' . $date;
,将其写入到/cache/txtchkad.txt
。
在判断登录的函数中的判断逻辑是:1
2
3
4// 读取txtchkad.txt文件
@$file = "../cache/txtchkad.txt"; //定义文件
@$fp = fopen($file, "r"); //以写入方式打开文件
@$txtchkad = fread($fp, 4096); //读取文件内容
管理员登录的逻辑判断代码为:
判断一:1
@$_SESSION["chkad"] == '' && @$_COOKIE["chkad"] == ''
如果session和cookie中得chkad
都会空,则认定管理员没有登录
判断二:1
2
3
4@$txtchkad = fread($fp, 4096); //读取文件内容
// 读取cookie中的$_COOKIE["chkad"],此值是在loginpass()函数中已经设置
$txtchkad2 = str_replace(@$_COOKIE["chkad"], '', $txtchkad);
$txtchkad == $txtchkad2
如果$txtchkad == $txtchkad2
,则需要用户登录,那么就说明$txtchkad中不含@$_COOKIE["chkad"]
.
绕过方法
自此,绕过方法就很简单了,主要str_replace(@$_COOKIE["chkad"], '', $txtchkad);
能够执行,即$txtchkad
中含有@$_COOKIE["chkad"]
内容即可。又因为在写入到txtchkad
包括$_SERVER['HTTP_USER_AGENT'] . '_' . $_SERVER['REMOTE_ADDR'] . '_' . $date
。那么就可以将chkad
设置为user-agent,_,:
等都可以绕过了。
POC
在登录页面,添加chkad
的cookie值
访问ad/index.html
直接进入后台。
修复
这个漏洞在1.0.4中仍然存在
SQL注入
所有的SQL注入完全是拼接的写法,没有使用pdo,也没有对输入进行过滤,所以在很多地方都存在SQL注入。
管理员登录
ad/login.php:jsloginpost()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34function jsloginpost() {
global $tabhead;
global $txtchk;
@$user = $_POST["user"];
@$psw = $_POST["psw"];
$psw = authcode(@$psw, 'ENCODE', 'key', 0);
@$loginlong = $_POST["loginlong"];
setcookie("lggqsj", date('Y-m-d H:i:s', time() + $loginlong), time() + 60 * 60 * 24, "/; HttpOnly", "", '');
$tab = $tabhead . "adusers";
$chk = " where adnaa='" . $user . "' and adpss='" . $psw . "' ";
mysql_select_db($tab);
$sql = mysql_query("select * from " . $tab . $chk);
if (!$sql) {
$jieguo = "<div id=redmsg>(数据库查询失败!)</div>";
} else {
$num = mysql_num_rows($sql);
if ($num == 0) {
$jieguo = '<div id=redmsg>登录失败:账户或密码错误!</div>';
} else {
loginpass($loginlong);
$jieguo = '<div id=bluemsg>登录成功!正在前往<a href="index.php">后台</a>。。。</div><meta http-equiv="refresh" content="1;url=index.php">';
@$chkmoblie = isMobile();
if ($chkmoblie == 1) {
$jieguo = '<div id=bluemsg>登录成功!正在前往<a href="wap.php">后台</a>。。。</div><meta http-equiv="refresh" content="1;url=wap.php">';
}
}
}
$json_arr = array("jieguo" => $jieguo);
$json_obj = json_encode($json_arr);
echo $json_obj;
}
判断管理员是否登录的代码:1
2
3
4$tab = $tabhead . "adusers";
$chk = " where adnaa='" . $user . "' and adpss='" . $psw . "' ";
mysql_select_db($tab);
$sql = mysql_query("select * from " . $tab . $chk);
没有进行任何的过滤,万能用户名直接可以进。
POC
使用万能用户名登录
hit.php注入
1 | $g = $_GET['g']; |
很明显id存在SQL注入
但是上面update
语句会报错,update axublog_arts set hit=hit+1 WHERE id= -1 union select 1,2,3,4,5,6,7,8,9,10;
art.php
art.php是管理员后台页面用于对当前的文章进行管理,但是并没有做登录验证,直接可以访问到所有的文章。
URL为host\ad\art.php
其中的主要逻辑代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54@$g = $_GET["g"];
switch ($g) {
case "addart":
addart();
break;
case "addsave":
addsave();
break;
case "del":
del();
break;
case "edit":
edit();
break;
case "editsave":
editsave();
break;
case "delall":
delall();
break;
case "chkall":
chkall();
break;
default:
artlist();
break;
}
function del() {
chkoutpost();
$id = $_GET['id'];
global $tabhead;
$tab = $tabhead . "arts";
$sql = "DELETE FROM " . $tab . " WHERE id=" . $id;
if (mysql_query($sql)) {
echo "<div id=ok>文章删除成功</div>";
} else {
echo "<div id=err>文章删除失败,请检查分类是否存在,请检查数据库!</div>";
jump('javascript:history.back()', 1);
}
global $tabhead;
$tab = $tabhead . "nav_art";
$sql = "DELETE FROM " . $tab . " WHERE artid=" . $id;
if (mysql_query($sql)) {
echo "<div id=ok>后续处理成功</div>";
jump('javascript:history.back()', 1);
} else {
echo "<div id=err>文章删除失败,请检查数据库!</div>";
jump('javascript:history.back()', 1);
}
}
可以看到其中的del()
没有对ic
参数进过滤,直接进行了Delelte的操作DELETE FROM " . $tab . " WHERE id=" . $id
。
delete语句同样会存在sql注入的问题,但是delete的错误语句不会回显,所以无法显示内容,所以不存在SQL注入。
art.php
中的文章编辑函数1
2
3
4
5
6
7
8
9
10
11
12function edit() {
$id = $_GET['id'];
if ($id == '') {
echo "<div id=err>文章id为空,请检查!</div>";
jump('?', 1);
}
$a = artidgetart($id);
$author = $a['author'];
$title = $a['title'];
$content = $a['content'];
$content = stripslashes($content);
// some other code
其中的artidgetart()
函数位于class/c_db.php
中,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function artidgetart($artid) {
global $tabhead;
$tab = $tabhead . "arts";
$chk = " where id=" . $artid . " ";
mysql_select_db($tab);
$sql = mysql_query("select * from " . $tab . $chk . " ");
if (!$sql) {
echo "<font color=red>(artidgetart打开数据库时遇到错误!)</font>";
return false;
}
while ($row = mysql_fetch_array($sql)) {
@$a = array("author" => $row['author'], "title" => $row['title'], "content" => stripslashes($row['content']), "htmlname" => $row['htmlname'], "type" => $row['type'], "edate" => $row['edate'], "hit" => $row['hit'], "tags" => $row['tags']);
}
return @$a;
}
很明显的SQL注入。
POC如下:
修复
这个漏洞在新版中也已经进行了修复。修复的方式主要对权限绕过进行了限制,同时也增加了SQL注入的判断。
总结
发现很多的漏洞都与后台的管理有关,但是如果将后台管理员的目录ad
修改为其他的目录,可能上述的很多漏洞都无法使用了。估计这个cms还是存在很多的漏洞,以后再接着发吧。