axublog审计

找了一个比较小众的版本的PHP开源程序,选择了一个比较老的版本,参考了网上一些别人的审计方法同时我自己也进行了部分审计。本片文章就是记录自己的审计过程。

重新安装+GetShell

初识安装过程

文件的安装位置在:install/goinstall.php
在goinstall.php引入了文件

1
2
require_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
22
switch ($_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
7
function 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
32
function 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
34
function 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
34
function 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

使用万能用户名登录
Alt text

hit.php注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$g = $_GET['g'];

if ($g == 'arthit') {
$id = $_GET['id'];
if ($id != '') {

$tab = $tabhead . "arts";
mysql_select_db($tab);
$sql = mysql_query("UPDATE " . $tab . " SET hit=hit+1 where id=" . $id);
$sql = mysql_query("select * from " . $tab . " where id=" . $id);
$row = mysql_fetch_array($sql);
$str = $row['hit'];
echo 'document.write(' . $str . ');';
}
}

很明显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
12
function 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
15
function 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还是存在很多的漏洞,以后再接着发吧。